This commit is contained in:
2026-09-08 14:07:31 +08:00
parent d318f4a450
commit 07688005a1
15 changed files with 695 additions and 87 deletions
@@ -5,7 +5,7 @@ import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { EXCEPTION_CODES, NODE_SEQUENCES, QUERY_STATUSES, STANDARD_NODES,
manualNodePayload, markerOwnedCarrierQuery, run } from './phase4-carrier-nodes.mjs'
manualNodePayload, markerOwnedCarrierQuery, run, runUntilPickupReady } from './phase4-carrier-nodes.mjs'
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
const here = path.dirname(fileURLToPath(import.meta.url))
@@ -32,22 +32,25 @@ test('focused carrier node execution injects the complete outbound-confirm produ
'phase3-loading-finalize', 'phase3-customs', 'ocean-bookings', 'phase3-port-schedules',
'truck-appointments', 'service-center-flow', 'phase3-outbound-dispatch',
'phase3-carrier-schedule-queries', 'phase4-carrier-pickup-ready',
'phase4-overseas-pda', 'phase4-carrier-nodes']
'phase4-tail-order-controls', 'phase4-overseas-pda', 'phase5-terminal-drayage',
'phase4-carrier-nodes']
for (const module of required) assert.ok(modules.includes(module), module)
for (let index = 1; index < required.length; index += 1) {
assert.ok(modules.indexOf(required[index - 1]) < modules.indexOf(required[index]), required[index])
}
})
test('real lifecycle opens the shipping gate before PDA and records pickup and empty return afterwards', () => {
test('real lifecycle opens the shipping gate before PDA and delegates pickup and empty return to terminal drayage', () => {
const modules = normalizeModuleOrder(['phase4-overseas-pda', 'phase4-carrier-nodes'])
const pickup = modules.indexOf('phase4-carrier-pickup-ready')
const pda = modules.indexOf('phase4-overseas-pda')
const drayage = modules.indexOf('phase5-terminal-drayage')
const completion = modules.indexOf('phase4-carrier-nodes')
assert.ok(pickup >= 0 && pickup < pda)
assert.ok(pda < completion)
assert.ok(pda < drayage && drayage < completion)
assert.match(source, /phase4CarrierPickupReady/)
assert.match(source, /STANDARD_NODES\.slice\(STANDARD_NODES\.indexOf\('PICKED_UP'\)\)/)
assert.match(source, /phase5TerminalDrayage/)
assert.doesNotMatch(source, /PDA完成后补录实际节点/)
})
test('canonical states, ten ordered nodes, exception codes and complete payload are frozen', () => {
@@ -100,16 +103,11 @@ test('ownership requires current marker, long query id and outbound-confirm sour
} }), null)
})
test('missing phase3 outbound query is an explicit deployment blocker and performs no write', async () => {
test('missing terminal drayage completion is an explicit deployment blocker and performs no write', async () => {
const calls = []; const steps = []
const context = {
marker: MARKER, token: 'token', createdResources: {}, calls,
request: async (method, requestPath) => {
calls.push({ method, path: requestPath })
if (requestPath === `${BASE}/carriers`) return ok([])
if (requestPath === `${BASE}?page=1&size=20`) return ok({ records: [], page: 1, size: 20, total: 0, pages: 0 })
throw new Error(`unexpected path ${requestPath}`)
},
request: async (method, requestPath) => { calls.push({ method, path: requestPath }); throw new Error('unexpected request') },
apiSuccess: (_module, _name, response) => response.body.data,
assert: (_module, name, _response, condition, evidence) => assert.ok(condition, `${name}: ${evidence}`),
addStep: (_module, name, _response, passed, evidence, options) => steps.push({ name, passed, evidence, options }),
@@ -117,11 +115,11 @@ test('missing phase3 outbound query is an explicit deployment blocker and perfor
await run(context)
assert.deepEqual(calls.map((item) => item.method), ['GET', 'GET'])
assert.deepEqual(calls, [])
assert.equal(steps.length, 1)
assert.equal(steps[0].passed, false)
assert.equal(steps[0].options.deploymentBlocker, true)
assert.match(steps[0].name, /禁止借历史任务写节点/)
assert.match(steps[0].name, /取得本轮码头拖车还柜终态/)
})
function markerOwnedContext() {
@@ -225,13 +223,13 @@ function markerOwnedContext() {
return { context, task, nodes, exceptions, histories, steps, assertions }
}
test('marker-owned chain covers idempotency conflicts, all actual nodes, exception DTO and empty-return stop', async () => {
test('marker-owned pre-PDA chain covers idempotency conflicts through pickup-ready', async () => {
const fixture = markerOwnedContext()
await run(fixture.context)
await runUntilPickupReady(fixture.context)
const posts = fixture.context.calls.filter((item) => item.method === 'POST')
assert.equal(posts.length, 17)
assert.equal(posts.length, 15)
assert.ok(posts.every((item) => item.path === `${BASE}/${QUERY_ID}/manual-nodes`))
assert.ok(posts.every((item) => item.headers['Idempotency-Key']))
assert.ok(posts.every((item) => Object.keys(item.body).length === 8))
@@ -240,11 +238,11 @@ test('marker-owned chain covers idempotency conflicts, all actual nodes, excepti
assert.ok(fixture.assertions.some((item) => /更早实际节点A0409后task、node、history零漂移/.test(item.name)))
assert.ok(fixture.steps.some((item) => /同级actual失败请求同键重试.*operation claim零残留/.test(item.name)))
assert.ok(fixture.steps.some((item) => /更早actual失败请求同键重试.*operation claim零残留/.test(item.name)))
assert.equal(fixture.task.status, 'STOPPED')
assert.equal(fixture.task.latestActualNode, 'EMPTY_RETURNED')
assert.equal(fixture.task.stopReason, '已还柜')
assert.equal(fixture.task.status, 'READY')
assert.equal(fixture.task.latestActualNode, 'PICKUP_READY')
assert.equal(fixture.task.stopReason, null)
const actualNodes = new Set(fixture.nodes.filter((item) => item.actualAt != null).map((item) => item.standardNode))
assert.deepEqual([...actualNodes].sort(), [...STANDARD_NODES].sort())
assert.deepEqual([...actualNodes].sort(), [...STANDARD_NODES.slice(0, 8)].sort())
assert.equal(fixture.exceptions[0].exceptionCode, 'CUSTOMS_HOLD')
assert.ok(fixture.nodes.every((item) => /^\d{16,}$/.test(item.id)))
assert.equal(fixture.context.calls.some((item) => /attempts|scheduler|internal|\/stop/.test(item.path)), false)