import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { dirname, resolve } from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' const here = dirname(fileURLToPath(import.meta.url)) const read = (name) => readFileSync(resolve(here, name), 'utf8') test('phase3 downstream modules turn missing current-marker prerequisites into deployment blockers', () => { const expectations = new Map([ ['phase3-loading-model-bridge.mjs', '取得同 marker 的预装与柜次桥接前置'], ['phase3-pda-convergence.mjs', '取得同 marker 的 PDA 收敛前置'], ['phase3-evidence-gate.mjs', '取得同 marker 的五证据门禁前置'], ['phase3-outbound-dispatch.mjs', '取得同 marker 的 LOADED 国内出仓前置'], ['phase3-outbound-aggregate.mjs', '取得同 marker 的出仓主聚合前置'], ['phase3-dispatch-gate.mjs', '取得同 marker 的国内出仓门禁前置'], ['phase3-waiting-for-loading.mjs', '取得同 marker 的 WAITING_FOR_LOADING 前置'], ]) for (const [file, step] of expectations) { const source = read(file) assert.match(source, /deploymentBlocker: true/, file) assert.match(source, new RegExp(`addStep\\(MODULE, '${step}'[\\s\\S]{0,500}false`), file) assert.match(source, /fixtureMarker=.*runMarker=/, file) } }) test('phase3 inventory mutation and downstream gates cannot pass as coverage gaps', () => { const loading = read('phase3-loading.mjs') const convergence = read('phase3-pda-convergence.mjs') const dispatch = read('phase3-outbound-dispatch.mjs') const aggregate = read('phase3-outbound-aggregate.mjs') assert.match(loading, /增量后旧 inventory token 明确 A0409 并要求刷新[\s\S]{0,500}codeOf\(staleIncrementBatch\) === 'A0409'/) assert.match(loading, /增量旧 token 冲突后 plan\/task\/items 零写入/) assert.match(convergence, /fixture\?\.dynamicIncrementVerified !== true[\s\S]{0,500}不能跳过增量重开、旧 token A0409、刷新续拣和已拣项保护/) assert.match(dispatch, /国内出仓六项硬门禁必须在同轮全部通过[\s\S]{0,300}false/) assert.match(dispatch, /拖车与独立船期仅作聚合参考且不参与出仓门禁/) assert.match(dispatch, /'CUSTOMS_CLEARANCE'/) assert.match(dispatch, /const expectedCodes = \['INBOUND_LOADED', 'LOADING_PLAN_COMPLETED', 'PDA_PICK_COMPLETED',[\s\S]{0,120}'CUSTOMS_CLEARANCE'\]/) assert.match(aggregate, /'OCEAN_BOOKING', 'CUSTOMS', 'OUTBOUND_DISPATCH'/) assert.match(aggregate, /拖车和独立船期时间线只作参考/) assert.doesNotMatch(aggregate, /报关与船期历史并入统一时间线|当前服务端聚合未公开/) }) test('outbound lifecycle checks confirm replay, immutable departure reverse boundary and final reread', () => { const source = read('phase3-outbound-dispatch.mjs') for (const step of [ 'confirm-dispatch 幂等重放', '相同 Idempotency-Key 不同请求拒绝', '实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409', '禁止撤销边界重复请求仍精确返回A0409', '负向与幂等验证后回查出仓聚合', '出仓聚合保持 WAITING_FOR_LOADING、LOADED 且无成功撤销历史', ]) assert.match(source, new RegExp(step)) assert.match(source, /if \(!confirmed\?\.aggregate\) return/) assert.match(source, /hashConflict\.http === 200 && codeOf\(hashConflict\) === 'A0409'/) const confirm = source.indexOf('确认国内出仓进入等待装船') const reverse = source.indexOf('实际DEPARTED_WAREHOUSE节点产生后撤销精确返回A0409') const finalRead = source.indexOf('负向与幂等验证后回查出仓聚合') assert.ok(confirm >= 0 && reverse > confirm && finalRead > reverse) assert.match(source, /confirmedStatus: confirmed\.aggregate\.dispatchStatus/) assert.doesNotMatch(source, /outbound-confirm-final|finalConfirmed|reversedVersion/) const waiting = read('phase3-waiting-for-loading.mjs') assert.match(waiting, /confirmations\.length === 1 && reversed\.length === 0/) assert.match(waiting, /实际离仓节点冻结后保持一次确认且零次成功撤销/) assert.match(waiting, /detail\?\.aggregate\?\.dispatchStatus === 'WAITING_FOR_LOADING'/) assert.match(waiting, /inbounds\?\.every/) })