import assert from 'node:assert/strict' import { mkdir, open, readFile } from 'node:fs/promises' import { resolve, dirname, relative, isAbsolute } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' import { createHash } from 'node:crypto' import { AcceptanceRun } from './admin-test-env-acceptance.mjs' import { guardFleetWrites, redactFleetWrite } from './fleet-test-env-acceptance.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const API = '/api/platform/v1/last-leg', WORK = `${API}/appointment-work` const id = value => String(value ?? ''), enc = encodeURIComponent const digest = value => createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : JSON.stringify(value)).digest('hex') const requireFact = (condition, message) => { if (!condition) throw new Error(message) } const ok = r => r?.http === 200 && id(r.body?.code) === '200' const refused = (r, status) => (r?.http === status || r?.http === 200) && id(r?.body?.code) === `A0${status}` && r.body?.data == null const sort = rows => rows.map(row => JSON.stringify(row)).sort() const source = row => ({ customerId: id(row.customerId), sourceKey: row.sourceKey, destinationKey: row.destinationKey, packageCount: Number(row.packageCount) }) const count = value => { if (typeof value !== 'number' && !(typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value))) return NaN const n = Number(value) return Number.isSafeInteger(n) && n >= 0 ? n : NaN } export function plan() { return { environment: 'TEST', execute: false, entry: 'scripts/appointment-work-test-env.mjs', modes: { read: 'Only login/logout and GET: selected current-run work, exact FBA allocations/private dispatch pallets, shared trips, explicit retired replacements and actual dates.', workflow: 'Also writes SAVE_FOLLOWUP on one pending fixture item; verifies reader denial, exact replay, stale version and changed-payload rejection. Optional CUSTOMER SAVE_REQUIREMENT records no booking, without execution.' }, command: 'node --env-file=/test-acceptance.env scripts/appointment-work-test-env.mjs --execute --environment=TEST --run-id= --mode=read|workflow --fixture=reports//appointment-work-fixture.json', prerequisites: ['Latest IAM/Firstleg/Lastleg and permission/DDL baseline deployed by coordinator', 'Current-run formal HTTP producer report, its SHA256 and an explicit fixture; no historical discovery or fabricated inventory', 'CHONGCHONG_ENV=test and CHONGCHONG_ADMIN_API_BASE_URL ending /api; CHONGCHONG_ADMIN_USERNAME/PASSWORD', 'CHONGCHONG_MERCHANT_USERNAME/PASSWORD for customer denial; workflow also APPOINTMENT_WORK_READER_USERNAME/PASSWORD (platform list + source reads, without followup/manage-work)', 'Owner is an active platform IAM user; scheduled synchronization complete before execution'], fixture: { schemaVersion: 1, environment: 'TEST', runId: '', producerRunId: '', producedAt: '', producerReport: 'reports//report.json', producerSha256: '', cases: [{ name: 'customer', containerBookingId: '', type: 'DELIVERY', flow: 'TRANSFER', requirementKey: 'DISPATCH:', batchKey: 'DISPATCH:', batchNo: '', expected: { executionStatus: 'PENDING', planStatus: 'UNPLANNED', dateBasis: 'NONE', calendarDate: null }, containerIds: [''] }], batches: [{ case: 'customer', kind: 'CUSTOMER', dispatchNo: '', packageCount: 1, pallets: [{ planPalletId: '', packageCount: 1 }] }], sharedTrips: [], retired: [], workflow: { case: 'customer', ownerId: '', followupDate: '', noBooking: true } }, fixtureNotes: ['FBA batch entries instead use kind=FBA, appointmentNo, sources:[{customerId,sourceKey,destinationKey,packageCount}], minimumReadyCandidates (0 allowed).', 'sharedTrips:[{case,tripNo,tripId,containerIds,dispatches:[{dispatchNo,customerId,packageCount}]}]. The case must select TRANSPORT.', 'retired:[{workItemKey,containerBookingId,replacementCases:[name]}] checks absence from active container work and exact named replacements in old detail.', 'For COMPLETED cases provide expected.actual and calendarDate/dateBasis=ACTUAL; completed rows are loaded with their date filter.', 'workflow requires one pending item exposing SAVE_FOLLOWUP; noBooking additionally requires editable CUSTOMER DELIVERY and no original appointment plan.'], minimumProducer: { entry:'scripts/admin-test-env-acceptance.mjs', arguments:'--environment test --modules phase4-carrier-pickup-ready --wait-split-count 1 --wait-split-package-count 4 --output reports//producer.json', notes:'Uses its existing ADMIN_TEST_* and ADMIN_PDA_* configuration. Dependency closure creates a new customer/order, measurement/loading/customs/outbound and formal ship-node evidence. Select only the current report createdResources.phase3CarrierScheduleQuery.containerBookingId. First acceptance can target its UNKNOWN ROUTE work with workflow.noBooking=false; this does not prove stable batches.', batchPrerequisites:'Then use current formal route, cargo-verification/FBA appointment or PRIVATE receiving/pre-arrange APIs to prepare new stable batches; shared trip and completion stages need actual original dispatch/fleet/PDA execution before a new read report.', batchProducer:'Select phase4-overseas-inbound-operations with the same four-box arguments to exercise independent cargo verification, dedicated DELETE cancellation, split/merge conservation, immutable batch identity and replacement history. It also runs prerequisite PDA and reversible inventory writes; offline checks do not establish HTTP readiness. Retain samples and obtain the corresponding environment/cancellation authorization before execution.' }, remaining: ['This package does not create sailed containers, appointments, dispatches, stock or fleet trips, cancel original businesses, pick/depart/sign, or run internal synchronization.', 'Prepare/advance those samples with existing formal producers; run a new read report after each state transition. This is not end-to-end execution proof.', 'PRIVATE per-order pick quantity conservation and DIRECT receiving-write rejection require the original PDA/dispatch producer; this reader checks public batch identity/pallet quantities and completed facts only.', '1920x1080 manual page evidence remains separate.'], boundary: 'Default plan is zero network/file writes. Execute claims a new report directory, journals before every write, stops on unknown outcome, retains samples, and logs out only its sessions. No SQL, deployment, deletes or automatic resume.' } } export function options(args, execArgs, env) { if (!args.length || args.length === 1 && args[0] === '--plan') return { execute: false } const values = new Map() for (const arg of args) { const match = /^--(execute|environment|run-id|mode|fixture)(?:=(.*))?$/.exec(arg) requireFact(match && !values.has(match[1]), 'Unknown or duplicate argument') values.set(match[1], match[1] === 'execute' && match[2] === undefined ? true : match[2]) } requireFact(values.size === 5 && values.get('execute') === true && values.get('environment') === 'TEST' && /^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(values.get('run-id') || '') && ['read', 'workflow'].includes(values.get('mode')) && values.get('fixture'), 'Explicit TEST, new run-id, mode and fixture required') requireFact(execArgs.length === 1 && execArgs[0].startsWith('--env-file=') && resolve(execArgs[0].slice(11)).replaceAll('\\', '/').endsWith('/.codex/env/chongchong-global/test-acceptance.env'), 'Explicit authorized TEST env-file required') requireFact(env.CHONGCHONG_ENV === 'test', 'TEST profile required') const base = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL) requireFact(['http:', 'https:'].includes(base.protocol) && base.pathname.replace(/\/$/, '') === '/api' && !base.username && !base.password && !base.search && !base.hash, 'Unified TEST API base required') const credentialKeys = ['CHONGCHONG_ADMIN_USERNAME', 'CHONGCHONG_ADMIN_PASSWORD', 'CHONGCHONG_MERCHANT_USERNAME', 'CHONGCHONG_MERCHANT_PASSWORD'] if (values.get('mode') === 'workflow') credentialKeys.push('APPOINTMENT_WORK_READER_USERNAME', 'APPOINTMENT_WORK_READER_PASSWORD') requireFact(credentialKeys.every(key => typeof env[key] === 'string' && env[key]), 'Required actor credentials missing') return { execute: true, runId: values.get('run-id'), mode: values.get('mode'), fixture: values.get('fixture'), gateway: base.origin, credentialKeys } } export function validateFixture(f, config, now = Date.now()) { requireFact(f?.schemaVersion === 1 && f.environment === 'TEST' && f.runId === config.runId && /^[A-Za-z0-9_-]{1,80}$/.test(f.producerRunId || '') && /^[a-f0-9]{64}$/.test(f.producerSha256 || '') && typeof f.producerReport === 'string', 'Current-run HTTP producer provenance required') const age = now - Date.parse(f.producedAt) requireFact(Number.isFinite(age) && age >= 0 && age < 24 * 3600000, 'Fixture must have been prepared in the current 24-hour window') requireFact(Array.isArray(f.cases) && f.cases.length > 0 && f.cases.length <= 30, 'One to thirty explicit work cases required') const names = new Set() for (const c of f.cases) { requireFact(/^[A-Za-z0-9_-]{1,40}$/.test(c.name || '') && !names.has(c.name), 'Unique case names required'); names.add(c.name) requireFact(typeof c.containerBookingId === 'string' && c.containerBookingId.length > 0 && ['ROUTE','PICKUP','WAREHOUSE_RECEIPT','DELIVERY','TRANSPORT','EMPTY_RETURN'].includes(c.type) && ['UNKNOWN','TRANSFER','DIRECT'].includes(c.flow) && Array.isArray(c.containerIds) && c.containerIds.includes(c.containerBookingId), 'Exact case scope required') requireFact(['PENDING','IN_PROGRESS','COMPLETED'].includes(c.expected?.executionStatus) && ['UNPLANNED','TARGET_SET','AWAITING_REPLY','CONFIRMED','NOT_REQUIRED'].includes(c.expected?.planStatus) && ['NONE','TARGET','CONFIRMED','ACTUAL'].includes(c.expected?.dateBasis) && Object.hasOwn(c.expected, 'calendarDate'), 'Explicit status and calendar expectation required') if (c.expected.executionStatus === 'COMPLETED') requireFact(c.expected.actual && c.expected.dateBasis === 'ACTUAL' && /^\d{4}-\d{2}-\d{2}$/.test(c.expected.calendarDate || ''), 'Completion requires independent actual evidence and date') } for (const key of ['batches', 'sharedTrips', 'retired']) requireFact(Array.isArray(f[key]) && f[key].length <= 30, `${key} must be an explicit bounded list`) for (const b of f.batches) { const c = f.cases.find(c => c.name === b.case) requireFact(c?.type === 'DELIVERY' && c.batchKey && ['FBA','CUSTOMER'].includes(b.kind), 'Batch requires its delivery case') if (b.kind === 'FBA') requireFact(/^FBA_APPOINTMENT:[1-9]\d*$/.test(c.batchKey) && b.appointmentNo && Array.isArray(b.sources) && b.sources.length > 0 && b.sources.every(s => s.customerId && s.sourceKey && s.destinationKey && Number.isSafeInteger(s.packageCount) && s.packageCount > 0) && Number.isSafeInteger(b.minimumReadyCandidates) && b.minimumReadyCandidates >= 0, 'Exact FBA source expectations required') else requireFact(/^DISPATCH:[1-9]\d*$/.test(c.batchKey) && b.dispatchNo && Number.isSafeInteger(b.packageCount) && b.packageCount > 0 && Array.isArray(b.pallets) && b.pallets.length > 0 && b.pallets.every(p => typeof p.planPalletId === 'string' && Number.isSafeInteger(p.packageCount) && p.packageCount > 0), 'Exact private dispatch physical expectations required') } for (const t of f.sharedTrips) requireFact(f.cases.some(c => c.name === t.case && c.type === 'TRANSPORT') && t.tripNo && typeof t.tripId === 'string' && Array.isArray(t.containerIds) && t.containerIds.length > 0 && Array.isArray(t.dispatches) && t.dispatches.length > 0, 'Exact trip source expectations required') for (const r of f.retired) requireFact(r.workItemKey && r.containerBookingId && r.replacementCases?.length > 0 && r.replacementCases.every(n => names.has(n)), 'Explicit retired replacements required') if (config.mode === 'workflow') requireFact(names.has(f.workflow?.case) && typeof f.workflow.ownerId === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(f.workflow.followupDate || '') && typeof f.workflow.noBooking === 'boolean', 'Workflow target, owner and follow-up required') return f } export function validatePage(page, number, size = 100) { const total = count(page?.total) requireFact(page && Number.isSafeInteger(total) && count(page.page) === number && count(page.size) === size && count(page.pages) === Math.ceil(total / size) && Array.isArray(page.records) && page.records.length === Math.min(size, Math.max(0, total - (number - 1) * size)), 'Incomplete work page') return page.records } export function assertItem(item, c) { requireFact(item?.workItemKey && item.type === c.type && item.flow === c.flow, `Case ${c.name}: wrong work identity`) for (const key of ['requirementKey','batchKey','batchNo']) if (Object.hasOwn(c, key)) assert.equal(item[key], c[key], `Case ${c.name}: ${key}`) assert.deepEqual(item.containers.map(r => id(r.containerBookingId)).sort(), [...c.containerIds].sort(), `Case ${c.name}: current container scope`) for (const [key, value] of Object.entries(c.expected)) { requireFact(['executionStatus','planStatus','dateBasis','calendarDate','actual','reviewRequired','timezone'].includes(key), 'Unsupported expectation field'); assert.deepEqual(['calendarDate','actual','timezone'].includes(key) ? item[key] ?? null : item[key], value, `Case ${c.name}: ${key}`) } if (item.executionStatus !== 'COMPLETED') assert.equal(item.actual ?? null, null, 'Unfinished work must not invent completion') if (c.flow === 'DIRECT') requireFact(item.type !== 'WAREHOUSE_RECEIPT', 'DIRECT cannot create warehouse receipt work') return item } /** Runs only on explicit current-run identities. Caller provides authenticated, journaled transport. */ export async function verify(context, fixture, config) { const checks = [], resolved = new Map() const check = (name, condition) => { requireFact(condition, name); checks.push(name) } async function data(method, path, body, actor = 'admin') { const r = await context.request(actor, method, path, body) requireFact(ok(r), `HTTP_FAILURE ${method} ${path.split('?')[0]} http=${r?.http} code=${id(r?.body?.code)} requestId=${id(r?.requestId)} traceId=${id(r?.traceId)}`) return r.body.data } async function rows(container, date) { const result = []; let total = null for (let page = 1; page <= 100; page++) { const query = new URLSearchParams({ view: 'ALL', page: id(page), size: '100', containerBookingId: container, ...(date ? { date } : {}) }) const response = await data('GET', `${WORK}/items?${query}`), next = validatePage(response, page) requireFact(total == null || total === count(response.total), 'Work scope changed during pagination'); total = count(response.total) result.push(...next); if (page >= count(response.pages)) { requireFact(result.length === total && new Set(result.map(i => i.workItemKey)).size === total, 'Duplicate or missing work rows'); return result } } throw Error('Work pagination limit exceeded') } const anonymous = await context.request('anonymous', 'GET', `${WORK}/summary`) check('anonymous cannot read work', [401,403].includes(anonymous?.http) && anonymous.body?.data == null) check('customer cannot read platform work', refused(await context.request('merchant', 'GET', `${WORK}/summary`), 403)) const summary = await data('GET', `${WORK}/summary`) check('summary contains complete initialized counters', ['all','followToday','overdue','undated','review'].every(k=>Number.isSafeInteger(count(summary[k])))) for (const c of fixture.cases) { const current = await rows(c.containerBookingId, c.expected.executionStatus === 'COMPLETED' ? c.expected.calendarDate : null) if (c.flow === 'DIRECT') check(`${c.name}: no active warehouse receiving branch`, !current.some(i => i.type === 'WAREHOUSE_RECEIPT')) const matches = current.filter(i => i.type === c.type && i.flow === c.flow && ['requirementKey','batchKey','batchNo'].every(k => !Object.hasOwn(c,k) || i[k] === c[k])) check(`${c.name}: exactly one current item`, matches.length === 1); assertItem(matches[0], c) const detail = await data('GET', `${WORK}/items/${enc(matches[0].workItemKey)}`); assertItem(detail.item, c) const second = await data('GET', `${WORK}/items/${enc(matches[0].workItemKey)}`) assert.deepEqual(second, detail, 'Repeated GET must not write history or versions') resolved.set(c.name, detail); checks.push(`${c.name}: detail/get-no-write/status/date verified`) if (c.expected.calendarDate) { const day = c.expected.calendarDate, calendar = await data('GET', `${WORK}/calendar?from=${enc(day)}&to=${enc(day)}`) check(`${c.name}: calendar uses authoritative date`, calendar.days.some(d => d.date === day && d.total > 0 && d.typeCounts[c.type] > 0)) } } for (const b of fixture.batches) { const item = resolved.get(b.case).item if (b.kind === 'FBA') { const batch = await data('GET', `${API}/appointment-planning/batches/${enc(item.batchKey)}`) check(`${b.case}: immutable FBA batch and business number`, batch.appointment.batchKey === item.batchKey && batch.appointment.appointmentNo === b.appointmentNo && item.batchNo === b.appointmentNo) assert.deepEqual(sort(batch.appointment.items.map(source)), sort(b.sources.map(source)), 'FBA per-source quantities must match producer evidence') const candidates = await data('GET', `${API}/appointment-planning/batches/${enc(item.batchKey)}/dispatch-candidates`) check(`${b.case}: complete FBA candidates`, candidates.batchKey === item.batchKey && candidates.version === batch.appointment.version && Array.isArray(candidates.candidates) && Array.isArray(candidates.blockReasons)) const ready = candidates.candidates.filter(c => c.blockReasons.length === 0) check(`${b.case}: required real physical candidates`, ready.length >= b.minimumReadyCandidates) for (const candidate of ready) check(`${b.case}: candidate per-source quantity conservation`, candidate.allocations.length > 0 && candidate.allocations.every(a => a.batchKey === item.batchKey && id(a.customerId) === id(candidate.customerId) && id(a.planPalletId) === id(candidate.planPalletId)) && candidate.allocations.reduce((n,a) => n+a.packageCount,0) === candidate.packageCount) } else { const dispatch = await data('GET', `${API}/dispatches/${enc(b.dispatchNo)}`) check(`${b.case}: original private dispatch identity`, item.batchKey === `DISPATCH:${dispatch.id}` && item.batchNo === dispatch.dispatchNo && dispatch.dispatchNo === b.dispatchNo) check(`${b.case}: physical total`, dispatch.packageCount === b.packageCount && dispatch.items.reduce((n,p) => n+p.packageCount,0) === b.packageCount) assert.deepEqual(sort(dispatch.items.map(p => ({planPalletId:id(p.planPalletId),packageCount:p.packageCount}))), sort(b.pallets), 'Private original pallet allocation drift') } } for (const t of fixture.sharedTrips) { const detail = resolved.get(t.case), trip = await data('GET', `${API}/fleet/trips/${enc(t.tripNo)}`) check(`${t.case}: canonical trip identity`, detail.item.businessNo === t.tripNo && detail.references.some(r => r.kind === 'FLEET_TRIP' && r.key === t.tripId && r.businessNo === t.tripNo) && trip.trip.tripNo === t.tripNo) assert.deepEqual(detail.item.containers.map(c => c.containerBookingId).sort(), [...t.containerIds].sort(), 'Shared trip container membership drift') assert.deepEqual(sort(trip.dispatches.map(d => ({dispatchNo:d.dispatchNo,customerId:id(d.customerId),packageCount:d.packageCount}))), sort(t.dispatches), 'Shared trip original dispatch membership drift') for (const container of t.containerIds) check(`${t.case}: shared transport counted once per container`, (await rows(container, detail.item.calendarDate || undefined)).filter(i => i.type === 'TRANSPORT' && i.workItemKey === detail.item.workItemKey).length === 1) } for (const r of fixture.retired) { check('retired item excluded from active scope', !(await rows(r.containerBookingId)).some(i => i.workItemKey === r.workItemKey)) const old = await data('GET', `${WORK}/items/${enc(r.workItemKey)}`) check('retired item retains history and read-only actions', old.history.some(h => h.action === 'REPLACED') && old.item.allowedActions.every(a => ['VIEW_HISTORY','VIEW_DETAIL'].includes(a.code))) for (const name of r.replacementCases) check('original requirement restored or explicitly replaced', old.relatedItems.some(i => i.workItemKey === resolved.get(name).item.workItemKey)) } if (config.mode === 'workflow') { const w = fixture.workflow, before = resolved.get(w.case), key = before.item.workItemKey, path = `${WORK}/items/${enc(key)}` check('workflow sample is unfinished and exposes followup', before.item.executionStatus !== 'COMPLETED' && before.item.allowedActions.some(a => a.code === 'SAVE_FOLLOWUP')) await data('GET', path, undefined, 'reader') const command = { command:'SAVE_FOLLOWUP', expectedVersion:before.item.version, idempotencyKey:`AW-${config.runId}-FOLLOWUP`, payload:{ownerId:w.ownerId,followupDate:w.followupDate,note:`预约工作HTTP验收 ${config.runId}`} } check('list-only platform cannot write followup', refused(await context.request('reader','POST',`${path}/commands`,command),403)) assert.deepEqual(await data('GET',path),before,'Denied write changed original work') const result = await data('POST',`${path}/commands`,command), saved = await data('GET',path) check('followup resolves the active platform Auth subject owner', result.item.owner?.id === w.ownerId && typeof result.item.owner.name === 'string' && result.item.owner.name.length > 0 && result.item.followupDate === w.followupDate) assert.deepEqual(result.item.actual,before.item.actual,'Followup invented execution'); assert.equal(result.item.executionStatus,before.item.executionStatus) check('one command creates one history entry',saved.history.length===before.history.length+1) assert.deepEqual(await data('POST',`${path}/commands`,command),result,'Idempotent replay changed response') assert.deepEqual(await data('GET',path),saved,'Idempotent replay wrote history or version') check('changed payload under same key rejected',refused(await context.request('admin','POST',`${path}/commands`,{...command,payload:{...command.payload,note:'变更内容应拒绝'}}),409)) check('stale version rejected',refused(await context.request('admin','POST',`${path}/commands`,{...command,idempotencyKey:`AW-${config.runId}-STALE`}),409)) assert.deepEqual(await data('GET',path),saved,'Rejected stale/tampered write changed work') if(w.noBooking) { check('explicit customer requirement is editable',before.item.type==='DELIVERY' && saved.requirement && saved.item.allowedActions.some(a=>a.code==='SAVE_REQUIREMENT')) const updated=await data('POST',`${path}/commands`,{command:'SAVE_REQUIREMENT',expectedVersion:saved.item.version,idempotencyKey:`AW-${config.runId}-NOBOOK`,payload:{target:null,bookingRequired:false,receivingRequirements:'本轮测试客户仓按到货接收,无需外部预约',reason:`明确收货要求 ${config.runId}`}}) check('no booking does not mean executed',updated.item.planStatus==='NOT_REQUIRED' && updated.item.executionStatus===before.item.executionStatus && updated.item.actual==null) } } return { checks, resolved: Object.fromEntries([...resolved].map(([name,d])=>[name,{workItemKey:d.item.workItemKey,version:d.item.version,batchKey:d.item.batchKey,batchNo:d.item.batchNo,executionStatus:d.item.executionStatus}])) } } function inReports(path) { const absolute=resolve(ROOT,path), rel=relative(resolve(ROOT,'reports'),absolute) requireFact(rel && !rel.startsWith('..') && !isAbsolute(rel), 'Fixture and producer evidence must stay inside this TEST workspace reports directory') return absolute } export async function execute(args=process.argv.slice(2),execArgs=process.execArgv,env=process.env) { const config=options(args,execArgs,env);if(!config.execute)return plan() requireFact(ROOT.replaceAll('\\','/').endsWith('/chongchong-global/baiyue-firstleg'),'Registered TEST workspace required') const fixtureBytes=await readFile(inReports(config.fixture));requireFact(fixtureBytes.length<=1024*1024,'Fixture too large') const fixture=validateFixture(JSON.parse(fixtureBytes),config) const producer=await readFile(inReports(fixture.producerReport));requireFact(digest(producer)===fixture.producerSha256,'Producer evidence digest mismatch') const directory=resolve(ROOT,'reports',`appointment-work-${config.runId}`) await mkdir(dirname(directory),{recursive:true});await mkdir(directory) let sequence=0 const report={runId:config.runId,producerRunId:fixture.producerRunId,fixtureSha256:digest(fixtureBytes),mode:config.mode,status:'RUNNING',startedAt:new Date().toISOString(),deploymentBlocker:false,checks:[],sessionsClosed:[],remaining:plan().remaining} const secrets=config.credentialKeys.map(k=>env[k]),sessions=new Map() async function save(name,value) {const file=await open(resolve(directory,name),'wx',0o600);try{await file.writeFile(JSON.stringify(redactFleetWrite(value,secrets),null,2)+'\n');await file.sync()}finally{await file.close()}} async function checkpoint(entry) { // Public query responses can include unrelated records; persist only own request and diagnostic metadata. const {responseBody,raw,...evidence}=entry if(evidence.path?.startsWith('/api/auth/')) evidence.body={sessionOperation:true} await save(`${String(++sequence).padStart(5,'0')}-${entry.status||'event'}.json`,{...evidence,responseCode:responseBody?.code}) } await save('claim.json',report) const transport=new AcceptanceRun({environment:'test',gateway:config.gateway,username:env.CHONGCHONG_ADMIN_USERNAME,password:env.CHONGCHONG_ADMIN_PASSWORD,quiet:true,modules:[]}) const cleanupRequest=transport.request.bind(transport) guardFleetWrites(transport,checkpoint) const request=async(actor,method,path,body)=> { requireFact(method==='GET' || method==='POST' && (['/api/auth/login','/api/auth/logout'].includes(path) || /^\/api\/platform\/v1\/last-leg\/appointment-work\/items\/[^/]+\/commands$/.test(path)),'Unexpected write boundary') const response=await transport.request(method,path,body,sessions.get(actor)?.accessToken||'') if(method==='GET')await checkpoint({status:'READ',actor,method,path,http:response.http,code:response.body?.code,requestId:response.requestId,traceId:response.traceId}) return response } try { const actors=[['admin','CHONGCHONG_ADMIN'],['merchant','CHONGCHONG_MERCHANT'],...(config.mode==='workflow'?[['reader','APPOINTMENT_WORK_READER']]:[])] for(const[name,prefix]of actors){const r=await request(name,'POST','/api/auth/login',{loginId:env[`${prefix}_USERNAME`],password:env[`${prefix}_PASSWORD`]});requireFact(ok(r)&&r.body.data?.accessToken&&r.body.data?.refreshToken,'Incomplete login');sessions.set(name,r.body.data);secrets.push(r.body.data.accessToken,r.body.data.refreshToken)} Object.assign(report,await verify({request},fixture,config));report.status='VERIFIED' }catch(error){report.status=String(error.message).includes('Unknown write outcome')?'UNKNOWN_STOP':'FAILED';report.deploymentBlocker=true;report.failure=redactFleetWrite(String(error.message),secrets)} finally { for(const[name,session]of sessions){ try {await checkpoint({status:'LOGOUT_SENDING',actor:name,path:'/api/auth/logout'});const r=await cleanupRequest('POST','/api/auth/logout',{refreshToken:session.refreshToken},session.accessToken);await checkpoint({status:'LOGOUT_RESULT',actor:name,http:r.http,code:r.body?.code,requestId:r.requestId,traceId:r.traceId});requireFact(ok(r),'Logout failed');report.sessionsClosed.push(name)} catch{report.deploymentBlocker=true;report.logoutFailure=true;if(report.status==='VERIFIED')report.status='FAILED'} } report.completedAt=new Date().toISOString();await save('report.json',report) } return {success:report.status==='VERIFIED',status:report.status,checks:report.checks.length,report:resolve(directory,'report.json')} } if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href){execute().then(result=>{console.log(JSON.stringify(result,null,2));if(result.success===false)process.exitCode=1}).catch(()=>{console.error('Appointment work acceptance not started: verify explicit TEST configuration, current-run fixture and unused report directory.');process.exitCode=1})}