Files
baiyue-firstleg/scripts/appointment-work-test-env.mjs
T

262 lines
28 KiB
JavaScript
Raw Normal View History

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=<private>/test-acceptance.env scripts/appointment-work-test-env.mjs --execute --environment=TEST --run-id=<new> --mode=read|workflow --fixture=reports/<producer>/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: '<new>', producerRunId: '<current producer>', producedAt: '<ISO instant>', producerReport: 'reports/<producer>/report.json', producerSha256: '<sha256>',
cases: [{ name: 'customer', containerBookingId: '<id>', type: 'DELIVERY', flow: 'TRANSFER', requirementKey: 'DISPATCH:<id>', batchKey: 'DISPATCH:<id>', batchNo: '<dispatch business number>',
expected: { executionStatus: 'PENDING', planStatus: 'UNPLANNED', dateBasis: 'NONE', calendarDate: null }, containerIds: ['<id>'] }],
batches: [{ case: 'customer', kind: 'CUSTOMER', dispatchNo: '<business number>', packageCount: 1, pallets: [{ planPalletId: '<id>', packageCount: 1 }] }],
sharedTrips: [], retired: [], workflow: { case: 'customer', ownerId: '<active platform user authUserId (Auth subject ID)>', followupDate: '<YYYY-MM-DD>', 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/<new-producer>/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})}