test: harden carrier node HTTP acceptance

This commit is contained in:
2026-08-24 19:44:28 +08:00
parent ac806704be
commit a811c2ca06
4 changed files with 422 additions and 342 deletions
@@ -4,145 +4,202 @@ import path from 'node:path'
import test from 'node:test'
import { fileURLToPath } from 'node:url'
import { EXCEPTION_CODES, QUERY_STATUSES, STANDARD_NODES, markerOwnedCarrierQuery, run } from './phase4-carrier-nodes.mjs'
import { EXCEPTION_CODES, NODE_SEQUENCES, QUERY_STATUSES, STANDARD_NODES,
manualNodePayload, markerOwnedCarrierQuery, run } from './phase4-carrier-nodes.mjs'
const here = path.dirname(fileURLToPath(import.meta.url))
const source = fs.readFileSync(path.join(here, 'phase4-carrier-nodes.mjs'), 'utf8')
const BASE = '/api/platform/v1/carrier-schedule-queries'
const QUERY_ID = '9007199254740993001'
const CUSTOMER_ID = '9007199254740993002'
const OCEAN_ID = '9007199254740993003'
const CONTAINER_ID = '9007199254740993004'
const MARKER = 'T-PHASE4'
function response(data) { return { http: 200, body: { code: '200', data } } }
function contextFor(routes, createdResources = {}) {
const calls = []
return {
calls,
marker: 'T-PHASE4', token: 'token', createdResources,
request: async (method, requestPath, body, _token, headers = {}) => {
calls.push(method === 'POST' ? { method, path: requestPath, body, headers } : { method, path: requestPath })
assert.ok(['GET', 'POST'].includes(method))
assert.ok(Object.hasOwn(routes, requestPath), `unexpected path: ${requestPath}`)
return response(routes[requestPath])
},
apiSuccess: (_module, _name, result) => {
assert.equal(result.http, 200)
assert.equal(result.body.code, '200')
return result.body.data
},
assert: (_module, name, _result, condition, evidence) => {
assert.ok(condition, `${name}: ${evidence}`)
},
}
function ok(data, requestId = 'req-ok') {
return { http: 200, body: { code: '200', data, requestId }, requestId, traceId: 'trace-ok' }
}
function conflict(requestId) {
return { http: 200, body: { code: 'A0409', message: 'conflict', requestId }, requestId, traceId: 'trace-conflict' }
}
function clone(value) { return structuredClone(value) }
const task = {
id: '9007199254740993', queryNo: 'CSQ9007199254740993', customerId: '10001',
oceanBookingId: '20001', containerBookingId: '30001', carrierCode: 'COSCO',
trackingType: 'CONTAINER_NO', trackingNo: 'CSNU1234567', status: 'STOPPED',
latestActualNode: 'PICKED_UP', latestActualAt: '2026-08-20T12:00:00', version: 5,
lastSuccessAt: '2026-08-20T12:00:00', sourceUpdatedAt: '2026-08-20T12:00:00',
expiresAt: null, stoppedAt: null, stopReason: null,
}
const node = {
id: '40001', providerCode: 'COSCO', providerEventId: 'event-40001', providerNodeCode: 'OUTGATE', standardNode: 'PICKED_UP',
nodeSequence: 90, observedAt: '2026-08-20T12:00:00', estimatedAt: null,
actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX', locationName: 'Los Angeles',
sourceReference: null, rawPayloadHash: 'a'.repeat(64), createdAt: '2026-08-20T12:00:01',
}
const manualNode = { ...node, id: '40002', providerCode: 'MANUAL', providerEventId: 'manual-40002',
providerNodeCode: 'PICKED_UP', estimatedAt: '2026-08-20T12:00:00', actualAt: null, rawPayloadHash: 'b'.repeat(64) }
const exception = { id: '45001', providerCode: 'MATSON_PUBLIC_TRACKING', providerEventId: 'hold-1',
providerStatus: 'CONTAINER ON HOLD - Customs Clearance', exceptionCode: 'CUSTOMS_HOLD', active: true,
observedAt: '2026-08-20T12:00:00', locationCode: 'USLAX', locationName: 'Long Beach',
sourceReference: 'MATSON:9974749', rawPayloadHash: 'c'.repeat(64), createdAt: '2026-08-20T12:00:01' }
const history = {
id: '50001', action: 'STOP', fromVersion: 4, toVersion: 5,
reason: null, operatorId: null, operatorName: '系统', createdAt: '2026-08-20T12:01:00',
}
const carriers = [{
carrierCode: 'COSCO', carrierName: 'COSCO', officialUrl: 'https://example.invalid',
accessMode: 'INTERNAL_PROVIDER', automated: true, coverageGap: false,
}]
test('module only permits the documented manual-node write and forbids internal execution endpoints', () => {
assert.match(source, /context\.request\('GET'/)
assert.doesNotMatch(source, /\bfetch\s*\(/)
assert.match(source, /context\.request\('POST',[\s\S]*\/manual-nodes/)
assert.match(source, /'Idempotency-Key'/)
for (const method of ['PUT', 'PATCH', 'DELETE']) {
assert.doesNotMatch(source, new RegExp(`context\\.request\\(['"]${method}['"]`))
}
for (const endpoint of ['/scheduler', '/attempts', '/manual-results', '/internal', '/pda']) {
assert.doesNotMatch(source, new RegExp(endpoint.replace('/', '\\/'), 'i'))
}
})
test('canonical task node and exception sets are locked to the phase-4 contract', () => {
test('canonical states, ten ordered nodes, exception codes and complete payload are frozen', () => {
assert.deepEqual(QUERY_STATUSES, ['READY', 'MANUAL_REQUIRED', 'STOPPED'])
assert.deepEqual(STANDARD_NODES, [
'DEPARTED_WAREHOUSE', 'AT_ORIGIN_PORT', 'LOADED_ON_VESSEL', 'DEPARTED_ORIGIN_PORT',
'IN_TRANSIT', 'ARRIVED_DESTINATION_PORT', 'DISCHARGED_FROM_VESSEL', 'PICKUP_READY',
'PICKED_UP', 'EMPTY_RETURNED',
])
assert.deepEqual(Object.values(NODE_SEQUENCES), [10, 20, 30, 40, 50, 60, 70, 80, 90, 100])
assert.deepEqual(EXCEPTION_CODES, ['CUSTOMS_HOLD', 'TERMINAL_HOLD', 'DOCUMENT_HOLD', 'UNKNOWN_HOLD'])
assert.deepEqual(Object.keys(manualNodePayload({ expectedVersion: 7, standardNode: 'PICKED_UP',
observedAt: '2026-08-24T12:00:00', actualAt: '2026-08-24T11:59:00', marker: MARKER })),
['expectedVersion', 'standardNode', 'observedAt', 'estimatedAt', 'actualAt',
'locationCode', 'locationName', 'reason'])
})
test('marker ownership requires explicit module ownership, query id and exact current marker', () => {
assert.equal(markerOwnedCarrierQuery({ marker: 'M', createdResources: {} }), null)
assert.equal(markerOwnedCarrierQuery({ marker: 'M', createdResources: {
phase3CarrierScheduleQuery: { queryId: '1', marker: 'OLD', ownedByModule: true },
test('module exposes no scheduler, attempt, internal or arbitrary stop endpoint', () => {
assert.match(source, /\/manual-nodes/)
assert.match(source, /'Idempotency-Key'/)
assert.doesNotMatch(source, /\/manual-results|\/attempts|\/scheduler|\/internal|\/pda|\/stop/)
assert.doesNotMatch(source, /\bfetch\s*\(/)
for (const method of ['PUT', 'PATCH', 'DELETE']) {
assert.doesNotMatch(source, new RegExp(`context\\.request\\(['"]${method}['"]`))
}
})
test('ownership requires current marker, long query id and outbound-confirm source', () => {
const valid = { queryId: QUERY_ID, marker: MARKER, ownedByModule: true,
triggerSource: 'OUTBOUND_CONFIRM' }
assert.equal(markerOwnedCarrierQuery({ marker: MARKER,
createdResources: { phase3CarrierScheduleQuery: valid } }).queryId, QUERY_ID)
assert.equal(markerOwnedCarrierQuery({ marker: 'OLD',
createdResources: { phase3CarrierScheduleQuery: valid } }), null)
assert.equal(markerOwnedCarrierQuery({ marker: MARKER, createdResources: {
phase3CarrierScheduleQuery: { ...valid, queryId: '1' },
} }), null)
assert.equal(markerOwnedCarrierQuery({ marker: 'M', createdResources: {
phase3CarrierScheduleQuery: { queryId: '1', marker: 'M', ownedByModule: false },
assert.equal(markerOwnedCarrierQuery({ marker: MARKER, createdResources: {
phase3CarrierScheduleQuery: { ...valid, triggerSource: 'MANUAL' },
} }), null)
assert.equal(markerOwnedCarrierQuery({ marker: 'M', createdResources: {
phase3CarrierScheduleQuery: { queryId: '1', marker: 'M', ownedByModule: true },
} }).queryId, '1')
})
test('empty page validates read-only pagination without detail calls', async () => {
const context = contextFor({
'/api/platform/v1/carrier-schedule-queries/carriers': carriers,
'/api/platform/v1/carrier-schedule-queries?page=1&size=20': { records: [], page: 1, size: 20, total: 0, pages: 0 },
})
await run(context)
assert.deepEqual(context.calls, [
{ method: 'GET', path: '/api/platform/v1/carrier-schedule-queries/carriers' },
{ method: 'GET', path: '/api/platform/v1/carrier-schedule-queries?page=1&size=20' },
])
})
test('standalone existing historical task is read-only and never receives manual nodes', async () => {
const base = '/api/platform/v1/carrier-schedule-queries'
const context = contextFor({
[`${base}/carriers`]: carriers,
[`${base}?page=1&size=20`]: { records: [{ query: task, expired: false }], page: 1, size: 20, total: 1, pages: 1 },
[`${base}/${task.id}`]: { query: { query: task, expired: false }, nodes: [node], exceptions: [exception], histories: [history] },
[`${base}/${task.id}/history`]: [history],
})
await run(context)
assert.deepEqual(context.calls.map((item) => item.method), ['GET', 'GET', 'GET', 'GET'])
assert.equal(context.calls.some((item) => item.method === 'POST'), false)
assert.equal(context.calls.at(-1).path, `${base}/${task.id}/history`)
})
test('phase4 writes only the exact current-marker query registered by phase3', async () => {
const base = '/api/platform/v1/carrier-schedule-queries'
const ownedTask = { ...task, id: '9007199254740994', queryNo: 'CSQ9007199254740994',
status: 'READY', trackingNo: 'TSTU1234567', version: 6 }
const resource = { queryId: ownedTask.id, marker: 'T-PHASE4', ownedByModule: true, trackingNo: ownedTask.trackingNo }
const context = contextFor({
[`${base}/carriers`]: carriers,
[`${base}?page=1&size=20`]: { records: [{ query: task, expired: false }], page: 1, size: 20, total: 1, pages: 1 },
[`${base}/${ownedTask.id}`]: { query: { query: ownedTask, expired: false }, nodes: [node], exceptions: [exception], histories: [history] },
[`${base}/${ownedTask.id}/manual-nodes`]: { query: { query: { ...ownedTask, version: 7 }, expired: false },
nodes: [node, manualNode], exceptions: [exception], histories: [history] },
[`${base}/${ownedTask.id}/history`]: [history],
}, { phase3CarrierScheduleQuery: resource })
test('missing phase3 outbound query 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}`)
},
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 }),
}
await run(context)
assert.deepEqual(context.calls.map((item) => item.method), ['GET', 'GET', 'GET', 'POST', 'GET'])
const write = context.calls.find((item) => item.method === 'POST')
assert.equal(write.path, `${base}/${ownedTask.id}/manual-nodes`)
assert.equal(write.body.expectedVersion, ownedTask.version)
assert.equal(write.body.actualAt, null)
assert.equal(write.headers['Idempotency-Key'], `T-PHASE4-carrier-manual-${ownedTask.id}`)
assert.deepEqual(calls.map((item) => item.method), ['GET', 'GET'])
assert.equal(steps.length, 1)
assert.equal(steps[0].passed, false)
assert.equal(steps[0].options.deploymentBlocker, true)
assert.match(steps[0].name, /禁止借历史任务写节点/)
})
function markerOwnedContext() {
let nextId = 9007199254740994000n
const calls = []; const steps = []; const idempotency = new Map()
const task = {
id: QUERY_ID, queryNo: `CSQ${QUERY_ID}`, customerId: CUSTOMER_ID, oceanBookingId: OCEAN_ID,
containerBookingId: CONTAINER_ID, carrierCode: 'ZIM', trackingType: 'CONTAINER_NO',
trackingNo: 'TSTU1234567', status: 'READY', triggerSource: 'OUTBOUND_CONFIRM',
latestActualNode: 'DEPARTED_WAREHOUSE', latestActualAt: '2026-08-24T08:00:00', version: 0,
lastSuccessAt: null, sourceUpdatedAt: '2026-08-24T08:00:00', expiresAt: null,
stoppedAt: null, stopReason: null,
}
const nodes = [{
id: String(nextId++), providerCode: 'FIRSTLEG', providerEventId: 'OUTBOUND_CONFIRM',
providerNodeCode: 'OUTBOUND_CONFIRM', standardNode: 'DEPARTED_WAREHOUSE', nodeSequence: 10,
observedAt: '2026-08-24T08:00:00', estimatedAt: null, actualAt: '2026-08-24T08:00:00',
locationCode: null, locationName: null, sourceReference: 'OUTBOUND_CONFIRM',
rawPayloadHash: 'a'.repeat(64), createdAt: '2026-08-24T08:00:01',
}]
const exceptions = [{
id: String(nextId++), providerCode: 'MATSON_PUBLIC_TRACKING', providerEventId: 'hold-1',
providerStatus: 'CONTAINER ON HOLD - Customs Clearance', exceptionCode: 'CUSTOMS_HOLD', active: true,
observedAt: '2026-08-24T08:01:00', locationCode: 'USLAX', locationName: 'Long Beach',
sourceReference: 'MATSON:MASKED', rawPayloadHash: 'b'.repeat(64), createdAt: '2026-08-24T08:01:01',
}]
const histories = [{
id: String(nextId++), action: 'AUTO_CREATE', fromVersion: null, toVersion: 0,
reason: '国内确认出仓自动创建', operatorId: CUSTOMER_ID, operatorName: 'fixture',
createdAt: '2026-08-24T08:00:01',
}]
const detail = () => ({ query: { query: clone(task), expired: true },
nodes: clone(nodes), exceptions: clone(exceptions), histories: clone(histories) })
const context = {
marker: MARKER, token: 'token', calls, steps,
createdResources: { phase3CarrierScheduleQuery: {
queryId: QUERY_ID, marker: MARKER, ownedByModule: true, triggerSource: 'OUTBOUND_CONFIRM',
customerId: CUSTOMER_ID, oceanBookingId: OCEAN_ID, containerBookingId: CONTAINER_ID,
trackingNo: task.trackingNo, version: 0,
} },
request: async (method, requestPath, body, _token, headers = {}) => {
calls.push(method === 'POST' ? { method, path: requestPath, body: clone(body), headers: clone(headers) }
: { method, path: requestPath })
if (method === 'GET' && requestPath === `${BASE}/carriers`) return ok([{
carrierCode: 'ZIM', carrierName: 'ZIM', officialUrl: 'https://api.zim.com/',
accessMode: 'OFFICIAL_API', automated: true, coverageGap: false,
}])
if (method === 'GET' && requestPath === `${BASE}?page=1&size=20`) return ok({ records: [{
query: { ...task, id: '9007199254740993999', trackingNo: 'HISTORICAL000' }, expired: false,
}], page: 1, size: 20, total: 2, pages: 1 })
if (method === 'GET' && requestPath === `${BASE}/${QUERY_ID}`) return ok(detail())
if (method === 'GET' && requestPath === `${BASE}/${QUERY_ID}/history`) return ok(clone(histories))
if (method !== 'POST' || requestPath !== `${BASE}/${QUERY_ID}/manual-nodes`) {
throw new Error(`unexpected ${method} ${requestPath}`)
}
const key = headers['Idempotency-Key']; const signature = JSON.stringify(body)
if (idempotency.has(key)) {
const previous = idempotency.get(key)
return previous.signature === signature ? ok(clone(previous.detail), `req-replay-${key}`)
: conflict(`req-hash-${key}`)
}
if (Number(body.expectedVersion) !== Number(task.version)) return conflict(`req-version-${key}`)
const providerEventId = `manual-${String(nextId)}`
nodes.push({
id: String(nextId++), providerCode: 'MANUAL', providerEventId, providerNodeCode: body.standardNode,
standardNode: body.standardNode, nodeSequence: NODE_SEQUENCES[body.standardNode],
observedAt: body.observedAt, estimatedAt: body.estimatedAt, actualAt: body.actualAt,
locationCode: body.locationCode, locationName: body.locationName, sourceReference: `MANUAL:${CUSTOMER_ID}`,
rawPayloadHash: 'c'.repeat(64), createdAt: body.observedAt,
})
task.version += 1
if (body.actualAt != null) {
task.latestActualNode = body.standardNode; task.latestActualAt = body.actualAt
if (body.standardNode === 'EMPTY_RETURNED') {
task.status = 'STOPPED'; task.stopReason = '已还柜'; task.stoppedAt = body.observedAt
} else task.status = 'READY'
}
histories.push({ id: String(nextId++), action: 'MANUAL_NODE', fromVersion: task.version - 1,
toVersion: task.version, reason: body.reason, operatorId: CUSTOMER_ID, operatorName: 'fixture',
createdAt: body.observedAt })
const saved = detail(); idempotency.set(key, { signature, detail: clone(saved) })
return ok(saved, `req-${key}`)
},
apiSuccess: (_module, _name, response) => {
assert.equal(response.http, 200); assert.equal(response.body.code, '200'); return 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, requestId: response?.requestId || null })
assert.ok(passed, `${name}: ${evidence}`)
},
}
return { context, task, nodes, exceptions, histories, steps }
}
test('marker-owned chain covers idempotency conflicts, all actual nodes, exception DTO and empty-return stop', async () => {
const fixture = markerOwnedContext()
await run(fixture.context)
const posts = fixture.context.calls.filter((item) => item.method === 'POST')
assert.equal(posts.length, 14)
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))
assert.equal(fixture.steps.filter((item) => /A0409/.test(item.name)).length, 2)
assert.equal(fixture.task.status, 'STOPPED')
assert.equal(fixture.task.latestActualNode, 'EMPTY_RETURNED')
assert.equal(fixture.task.stopReason, '已还柜')
const actualNodes = new Set(fixture.nodes.filter((item) => item.actualAt != null).map((item) => item.standardNode))
assert.deepEqual([...actualNodes].sort(), [...STANDARD_NODES].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)
const firstPageQuery = fixture.context.calls.find((item) => item.path === `${BASE}?page=1&size=20`)
assert.ok(firstPageQuery)
})