import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import test from 'node:test' import { selectCustomsFlow } from './phase3-customs.mjs' import { buildFinalizeResource } from './phase3-loading-finalize.mjs' import { AcceptanceRun } from '../admin-test-env-acceptance.mjs' function source(name) { return readFileSync(new URL(name, import.meta.url), 'utf8') } function assertOrder(value, snippets) { let cursor = -1 for (const snippet of snippets) { const next = value.indexOf(snippet) assert.ok(next > cursor, `${snippet} must appear after the previous production step`) cursor = next } } test('pairs customs source fixture with the matching loading container', () => { const resources = { orders: { tenantId: 10001, orderId: 101, inboundId: 201 }, orchestrationFixtures: { phase3Container: { tenantId: 10001, orderId: 102, inboundId: 202 }, }, phase3Loading: { tenantId: 10001, inboundId: 201, containerBookingId: 301 }, phase3ContainerLoading: { tenantId: 10001, inboundId: 202, containerBookingId: 302 }, } const selected = selectCustomsFlow(resources) assert.equal(selected.fixture.orderId, 101) assert.equal(selected.container.containerBookingId, 301) }) test('falls back to the matching container fixture without crossing source identities', () => { const resources = { orchestrationFixtures: { phase3Container: { tenantId: 10001, orderId: 102, inboundId: 202 } }, phase3Loading: { tenantId: 10001, inboundId: 201, containerBookingId: 301 }, phase3ContainerLoading: { tenantId: 10001, inboundId: 202, containerBookingId: 302 }, } const selected = selectCustomsFlow(resources) assert.equal(selected.fixture.orderId, 102) assert.equal(selected.container.containerBookingId, 302) }) test('records LOADING and a cleanup reason when finalize returns S0500', () => { const result = buildFinalizeResource( { inboundStatus: 'LOADING', inboundVersion: 5, finalStatus: 'COMPLETED' }, { summary: { processStatus: 'LOADING', version: 5 } }, { http: 200, body: { code: 'S0500' } }, ) assert.equal(result.finalStatus, 'LOADING') assert.equal(result.finalizeSucceeded, false) assert.match(result.notCleanedReason, /S0500/) }) test('records LOADED and clears the reason only after a real LOADED reread', () => { const result = buildFinalizeResource( { inboundStatus: 'LOADING', inboundVersion: 5, finalStatus: 'COMPLETED' }, { summary: { processStatus: 'LOADED', version: 6 } }, { http: 200, body: { code: '200' } }, ) assert.equal(result.finalStatus, 'LOADED') assert.equal(result.finalizeSucceeded, true) assert.equal(result.notCleanedReason, null) }) test('container loading follows push, claim, real pick, completed sequence', () => { const value = source('./phase3-container-loading.mjs') assert.match(value, /\['EDITING', 'WAIT_LOADING'\]\.includes\(containerDetail\?\.planStatus\)/) assertOrder(value, [ "pushed?.status === 'EDITING'", "claimedPlan?.status === 'LOADING'", "const pickResponse = await request", "const completeResponse = await request", "finalPlan?.status === 'COMPLETED'", ]) assert.match(value, /picked\?\.workflowStatus === 'AWAITING_REVIEW'/) assert.match(value, /`\$\{PDA\}\/tasks\/\$\{taskId\}\/complete`/) assert.match(value, /expectedTaskVersion: Number\(picked\?\.version\)/) assert.match(value, /operationId: `\$\{context\.marker\}-pda-complete`\.slice\(0, 128\)/) }) test('bridge loading starts through a real PDA batch before preload confirmation', () => { const value = source('./phase3-loading.mjs') assertOrder(value, [ "pushedLoadingPlan?.status === 'EDITING'", "claimedPlan?.status === 'LOADING'", "const canonicalPick = await request", "pickedPlan?.status === 'COMPLETED'", "const confirming = await request", ]) assert.match(value, /confirmed\?\.mainStatus === 'LOADING' && confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/) assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMED'/) assert.doesNotMatch(value, /关联柜次计划进入 LOADING/) assert.match(value, /confirmed\?\.mainStatus === 'LOADING'/) assert.match(value, /confirmed\?\.preloadPlan\?\.status === 'CONFIRMING'/) assert.match(value, /Number\(confirmed\?\.preloadPlan\?\.version\) === 2/) }) test('PDA inventory snapshot gates batch and records the only safe mutation gap', () => { const loading = source('./phase3-loading.mjs') assertOrder(loading, [ '读取 PDA 提交前实时库存快照', '未发生库存变化时重复读取实时库存快照', '客户身份无权读取平台 PDA 实时库存快照', 'const overflowResource =', '通过公开 A0 换绑接口制造真实库存变化', '真实库存变化后旧 snapshot token 提交返回冲突', '旧 token 冲突对任务、明细、已拣数量和操作历史零写入', '库存变化后刷新 PDA 实时库存快照', 'const canonicalPick = await request', 'const canonicalBatchReplay = await request', ]) assert.match(loading, /inventorySnapshotToken: refreshedInventory\.snapshotToken/) assert.match(loading, /inventorySnapshotVerified: inventorySnapshotVerified && directPdaCompletion && atomicBatchReplayVerified/) assert.match(loading, /inventoryMutationVerified, inventoryMutationCleanup/) assert.match(loading, /当前唯一平台代理库存变更入口只允许 A0 临时占用换绑/) const convergence = source('./phase3-pda-convergence.mjs') assert.doesNotMatch(convergence, /未公开实时库存查询或库存版本快照端点/) assert.match(convergence, /fixture\?\.inventorySnapshotVerified !== true/) assert.match(convergence, /fixture\?\.inventoryMutationVerified !== true/) }) test('container loading creates and confirms booking before scoped candidates', () => { const value = source('./phase3-container-loading.mjs') assertOrder(value, [ "const bookingCreate = await request", "const submit = await request", "const confirm = await request", "const candidateResponse = await context.request('GET', `${SMART}/candidates?bookingId=", "const planResponse = await request(context, 'POST', `${SMART}/plans`", "const assignResponse = await request", ]) assert.match(value, /channelCode: channelCode/) assert.match(value, /warehouseId, warehouseName/) const draft = value.slice(value.indexOf('const bookingDraft = {'), value.indexOf('context.assert(MODULE, \'订舱渠道与仓库快照完整\'')) assert.doesNotMatch(draft, /billOfLadingNo|vesselName|voyageNo|estimatedSailingAt|estimatedArrivalAt/) assert.match(value, /containerAllocations\?\.\[0\]/) assert.match(value, /bookingId=\$\{encodeURIComponent\(containerId\)\}&warehouseId=/) assert.match(value, /tasks\?loadingPlanId=\$\{encodeURIComponent\(planId\)\}&warehouseId=/) assert.match(value, /loadingPlanId: planId, containerBookingId: containerId/) assert.ok(value.includes("'PATCH', `${CONTAINERS}/${containerId}/transport")) assert.match(value, /绑定本轮真实箱号并补建 PDA 任务/) assert.doesNotMatch(value, /POST', CONTAINERS, containerPayload/) }) test('PDA completion acceptance verifies immutable BATCH and COMPLETE_LOADING audit with operator metadata', () => { const container = source('./phase3-container-loading.mjs') const convergence = source('./phase3-pda-convergence.mjs') assert.match(container, /回查完成后的 PDA 操作审计/) assert.match(container, /item\.actionType === 'BATCH'/) assert.match(container, /item\.actionType === 'COMPLETE_LOADING'/) assert.match(container, /Boolean\(item\.operatorId\) && Boolean\(item\.createdAt\)/) assert.match(convergence, /PDA 原子批次完成审计包含 BATCH 操作/) assert.match(convergence, /item\.actionType === 'BATCH'/) }) test('phase3 loading proves mixed-line rollback across task, all items and operations', () => { const loading = source('./phase3-loading.mjs') assertOrder(loading, [ '非法混合批次前读取 PDA 全量任务基线', 'PDA 原子批次任一非法明细时整批拒绝', '回查失败批次后的 PDA 任务快照', '非法混合批次后回查 PDA 操作历史', '合法明细加非法明细失败后任务、全部明细和操作历史零写入', ]) assert.match(loading, /taskItemsSignature\(unchangedAfterInvalid\) === taskItemsSignature\(beforeInvalidTask\)/) assert.match(loading, /operationsSignature\(afterInvalidOperations\) === operationsSignature\(beforeInvalidOperations\)/) }) test('phase3 loading proves stale inventory zero-write for all items and completion audit', () => { const loading = source('./phase3-loading.mjs') assert.match(loading, /taskItemsSignature\(afterStaleTask\) === taskItemsSignature\(beforeMutationTask\)/) assertOrder(loading, [ '旧 token 冲突对任务、明细、已拣数量和操作历史零写入', '库存变化后刷新 PDA 实时库存快照', '原子提交柜次 PDA 整批拣货', 'PDA 原子批次同参同键重放返回同一终态', '回查原子 Batch 完成审计', '合法 Batch 完成后保留 CLAIM/BATCH 操作者审计且无伪造 REVIEW', ]) }) test('phase3 loading fixture forces A0 through reversible public location status APIs', () => { const entry = source('../admin-test-env-acceptance.mjs') const measurement = source('./inbound-measurement.mjs') const orders = source('./orders.mjs') assertOrder(entry, [ 'disableAvailableLocationsForA0(warehouseId)', 'this.context.phase3LoadingRequireSharedOverflow = true', "executeModule('inbound-measurement-phase3-loading-fixture'", 'restoreAvailableLocationsAfterA0(warehouseId, disabledLocations)', ]) assert.match(entry, /\/locations\/\$\{encodeURIComponent\(location\.id\)\}\/status/) assert.match(entry, /\{ status: 'DISABLED', version: Number\(location\.version\) \}/) assert.match(entry, /\{ status: 'AVAILABLE', version: location\.version \}/) assert.match(measurement, /phase3LoadingRequireSharedOverflow !== true/) assert.match(orders, /receivingWarehouseId: id\(master\.receiving\.id\)/) }) test('customs provisions released booking and completed truck for the selected loading container', () => { const value = source('./phase3-customs.mjs') assertOrder(value, [ 'selectCustomsFlow(context.createdResources)', 'if (!await ensureServiceCenterFlow(context, container)) return', 'const slicesPath =', ]) assert.match(value, /oceanBookings\.run\(context\)/) assert.match(value, /serviceCenter\.run\(context\)/) }) test('downstream producers prefer the phase3Loading container fixture', () => { const ocean = source('./ocean-bookings.mjs') const customs = source('./phase3-customs.mjs') const serviceCenter = source('./service-center-flow.mjs') assert.match(ocean, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/) assert.match(customs, /createdResources\?\.phase3Loading,\s*\n\s*createdResources\?\.phase3ContainerLoading/) assert.match(serviceCenter, /context\.createdResources\?\.phase3Loading\s*\|\|\s*context\.createdResources\?\.phase3ContainerLoading/) assert.match(ocean, /await action\('放舱', 'release'/) }) test('default acceptance order verifies loading evidence before finalizing LOADED', () => { const entry = source('../admin-test-env-acceptance.mjs') assertOrder(entry, [ "'phase3-loading'", "'phase3-loading-model-bridge'", "'phase3-pda-convergence'", "'phase3-evidence-gate'", "'phase3-customs'", "'phase3-loading-finalize'", ]) }) test('phase3 loading provisions its own marker-isolated WAIT_SPLIT fixture', async () => { const run = new AcceptanceRun({ gateway: 'http://127.0.0.1', modules: [], username: '', password: '', output: '' }) const entry = source('../admin-test-env-acceptance.mjs') assert.match(entry, /module === 'phase3-loading'/) assert.match(entry, /preparePhase3LoadingFixture\(\)/) assert.match(entry, /orders-phase3-loading-fixture/) assert.match(entry, /inbound-measurement-phase3-loading-fixture/) assert.match(entry, /marker: originalMarker/) assert.match(entry, /sourceMarker: fixtureMarker/) assert.match(entry, /phase3Loading: loadingFixture,\s*\n\s*inboundMeasurement: loadingFixture/) assert.equal(run.marker.includes('-LOAD'), false) }) test('customs and finalize consume the same loading inbound identity', () => { const loadingFixture = { marker: 'RUN-1', tenantId: 10001, orderId: 101, inboundId: 201 } const loading = { marker: 'RUN-1', tenantId: 10001, inboundId: 201, containerBookingId: 301, loadingPlanId: 401, finalStatus: 'COMPLETED', } const selected = selectCustomsFlow({ orchestrationFixtures: { phase3Loading: loadingFixture, inboundMeasurement: loadingFixture, }, phase3Loading: loading, }) assert.equal(selected.fixture.marker, loading.marker) assert.equal(selected.fixture.inboundId, loading.inboundId) assert.equal(selected.container.inboundId, loading.inboundId) const finalized = buildFinalizeResource(loading, { summary: { processStatus: 'LOADED', version: 6 }, }, { http: 200, body: { code: '200' } }) assert.equal(finalized.marker, loading.marker) assert.equal(finalized.inboundId, loading.inboundId) assert.equal(finalized.finalStatus, 'LOADED') }) test('loading evidence acceptance covers idempotency, reversible missing-type gate and scope isolation', () => { const loading = source('./phase3-loading.mjs') assertOrder(loading, [ '装柜证据绑定同键同参幂等重放', '装柜证据绑定同键异参返回冲突', '装柜证据作废旧版本返回冲突', '临时作废唯一 PHOTO_STEP_4 证据', '证据作废同键同参重放保持同一 VOID 结果', '证据作废同键异参返回冲突', '已作废证据使用新请求重复作废返回冲突', '作废唯一证据类型后 LOADED 门禁拒绝', '补传恢复 PHOTO_STEP_4 有效证据', '客户身份无权访问平台装柜证据', '错误客户作用域不可读取本轮装柜证据', '错误入库单不可读取本轮装柜证据', ]) assert.match(loading, /missingTypeVoidedEvidenceId/) assert.match(loading, /replacementEvidenceFileId/) }) test('loaded boundary acceptance proves evidence and transport immutability with zero drift', () => { const finalize = source('./phase3-loading-finalize.mjs') assertOrder(finalize, [ 'LOADED 后使用新键重复 complete 幂等保持终态', 'LOADED complete 同键异参返回冲突', 'LOADED 后新增装柜证据被拒绝', 'LOADED 后作废装柜证据被拒绝', 'LOADED 后运输信息修改被拒绝', '回查 LOADED 高风险边界后的入库快照', '回查 LOADED 高风险边界后的证据快照', '回查 LOADED 高风险边界后的计划快照', 'LOADED 后拒绝与幂等请求保持状态、版本、证据、计划和审计零漂移', ]) assert.match(finalize, /evidenceSignature\(afterEvidence\) === immutableEvidenceSignature/) assert.match(finalize, /afterHistorySignature === historySignature/) assert.match(finalize, /afterPlanSignature === immutablePlanSignature/) })