import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import test from 'node:test' import { fileURLToPath } from 'node:url' import { QUERY_STATUSES, STANDARD_NODES, 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') function response(data) { return { http: 200, body: { code: '200', data } } } function contextFor(routes) { const calls = [] return { calls, request: async (method, requestPath) => { calls.push({ method, path: requestPath }) assert.equal(method, 'GET') 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}`) }, } } const task = { id: '9007199254740993', queryNo: 'CSQ9007199254740993', customerId: '10001', oceanBookingId: '20001', containerBookingId: '30001', carrierCode: 'COSCO', trackingType: 'CONTAINER_NO', trackingNo: 'CSNU1234567', status: 'STOPPED', latestStandardNode: 'PICKUP_READY', latestNodeObservedAt: '2026-08-20T12:00:00', lastSuccessAt: '2026-08-20T12:00:00', sourceUpdatedAt: '2026-08-20T12:00:00', expiresAt: null, stoppedAt: '2026-08-20T12:01:00', stopReason: '目的港可提柜后停止', } const node = { id: '40001', providerCode: 'COSCO', providerNodeCode: 'PICKUP', standardNode: 'PICKUP_READY', nodeSequence: 60, observedAt: '2026-08-20T12:00:00', estimatedAt: null, actualAt: '2026-08-20T11:55:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: null, 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('readonly module statically forbids writes and internal execution endpoints', () => { assert.match(source, /context\.request\('GET'/) assert.doesNotMatch(source, /\bfetch\s*\(/) for (const method of ['POST', '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 and node sets are locked to the phase-4 read contract', () => { assert.deepEqual(QUERY_STATUSES, ['READY', 'MANUAL_REQUIRED', 'ARRIVED', 'PICKUP_READY', 'STOPPED']) assert.deepEqual(STANDARD_NODES, [ 'DEPARTED_WAREHOUSE', 'AT_ORIGIN_PORT', 'DEPARTED_ORIGIN_PORT', 'IN_TRANSIT', 'ARRIVED', 'PICKUP_READY', ]) }) 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('existing task reads page, detail and history with nullable node times', 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], 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.at(-1).path, `${base}/${task.id}/history`) })