2026-10-03 06:26:33 +08:00
|
|
|
import test from 'node:test'
|
|
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { mkdtempSync, mkdirSync, readFileSync, existsSync, rmSync } from 'node:fs'
|
|
|
|
|
import { tmpdir } from 'node:os'
|
|
|
|
|
import { join, dirname, resolve } from 'node:path'
|
2026-10-03 14:52:04 +08:00
|
|
|
import { parseFleetArguments, fleetExecutionPlan, runFleetAcceptance, reviewedFullCheckpoint, verifyFleetTerminalState, runFleetHandoverStage, fleetCostShares, prepareFleetCostActors } from './fleet-test-env-acceptance.mjs'
|
2026-10-03 06:26:33 +08:00
|
|
|
|
|
|
|
|
test('fleet entry defaults to a no-side-effect plan and requires explicit TEST and unique run ID', () => {
|
|
|
|
|
const p = parseFleetArguments([])
|
|
|
|
|
assert.equal(p.execute, false); assert.equal(fleetExecutionPlan(p).execution, 'PLAN')
|
|
|
|
|
for (const args of [['--execute'], ['--execute', '--run-id', 'new'], ['--execute', '--environment', 'prod', '--run-id', 'new'],
|
|
|
|
|
['--execute', '--environment', 'TEST', '--run-id', '../old'], ['--plan', '--execute'], ['--force'], ['--run-id', 'a', '--run-id', 'b']]) {
|
|
|
|
|
assert.throws(() => parseFleetArguments(args))
|
|
|
|
|
}
|
|
|
|
|
assert.equal(parseFleetArguments(['--execute', '--environment', 'TEST', '--run-id', 'new']).execute, true)
|
2026-10-03 14:52:04 +08:00
|
|
|
const handover = parseFleetArguments(['--plan', '--environment', 'TEST', '--run-id', 'fleet-handover-20261003', '--output', 'reports/fleet-handover-live-20261003'])
|
|
|
|
|
assert.equal(fleetExecutionPlan(handover).outputDir, 'reports/fleet-handover-live-20261003')
|
|
|
|
|
for (const output of ['reports/../outside', 'C:/temp/output', 'reports', 'reports/nested/path']) assert.throws(() => parseFleetArguments(['--output', output]))
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-03 16:11:52 +08:00
|
|
|
test('return redispatch is explicit, fresh-only and declares source-event proof separately', () => {
|
|
|
|
|
const config = parseFleetArguments(['--plan', '--environment', 'TEST', '--run-id', 'return-new', '--scenario', 'return-redispatch'])
|
|
|
|
|
const plan = fleetExecutionPlan(config)
|
|
|
|
|
assert.equal(plan.scenario, 'return-redispatch'); assert.ok(plan.returnRedispatch.required.includes('Two current-run domestic sources'))
|
|
|
|
|
assert.throws(() => parseFleetArguments(['--scenario', 'return-redispatch', '--resume-report', 'reports/old/report.json', '--resume-stage', 'FULL_COVERAGE']), /fresh run/)
|
|
|
|
|
assert.throws(() => parseFleetArguments(['--scenario', 'unknown']))
|
|
|
|
|
})
|
|
|
|
|
|
2026-10-03 14:52:04 +08:00
|
|
|
test('handover stage exports only a verified same-run departure fixture before the optional UI checkpoint', async () => {
|
|
|
|
|
const order = [], departure = { tripNo: 'TR-NEW', status: 'IN_TRANSIT' }
|
|
|
|
|
const context = { marker: 'CURRENT', context: { saveFleetHandoverFixture(value) { order.push('save'); assert.equal(value.marker, 'CURRENT') } },
|
|
|
|
|
createdResources: { fleetAcceptance: { departure } } }
|
|
|
|
|
const trip = { tripNo: 'TR-NEW', warehouseId: '92', costAcceptance: { status: 'VERIFIED', marker: 'CURRENT', expected: { finalCny: '100.03', usd: '13.07' } } }, details = [{ dispatchNo: 'PC-NEW' }]
|
|
|
|
|
const verified = { status: 'VERIFIED', marker: 'CURRENT', originalDeparture: { snapshot: { confirmedLoad: { pallets: [] } } },
|
|
|
|
|
browser: { tripNo: 'TR-NEW' }, handoverNo: 'HO-NEW' }
|
|
|
|
|
const result = await runFleetHandoverStage(context, trip, 'secret-picker-token', details, {
|
|
|
|
|
verify: async (ctx, module, proof, token, dispatches) => { order.push('verify'); assert.equal(ctx, context); assert.equal(proof, trip); assert.equal(token, 'secret-picker-token'); assert.equal(dispatches, details); return verified },
|
|
|
|
|
onReady: async fixture => { order.push('ui'); assert.equal(fixture.status, 'DEPART_AND_HANDOVER_VERIFIED'); assert.equal(fixture.departure, departure) },
|
|
|
|
|
})
|
|
|
|
|
assert.deepEqual(order, ['verify', 'save', 'ui']); assert.ok(!JSON.stringify(result).includes('secret-picker-token'))
|
|
|
|
|
assert.deepEqual(result.costs, trip.costAcceptance)
|
|
|
|
|
for (const invalid of [{ ...verified, status: 'FAILED' }, { ...verified, marker: 'OLD' }, { ...verified, originalDeparture: null }]) {
|
|
|
|
|
await assert.rejects(runFleetHandoverStage(context, trip, 'token', details, { verify: async () => invalid,
|
|
|
|
|
onReady: async () => assert.fail('invalid evidence must not reach the browser checkpoint') }), /current verified/)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('cost shares use controlled physical pallets and the original external booking, never statistical totals', () => {
|
|
|
|
|
const trip = { externalReceipt: { status: 'VERIFIED', bookingNo: 'BK-ONE', warehousePalletId: '3' }, externalDispatch: { dispatchNo: 'EXT' } }
|
|
|
|
|
const details = [{ dispatchNo: 'OWN', items: [{ warehousePalletId: '1' }, { warehousePalletId: '1' }, { warehousePalletId: '2' }] },
|
|
|
|
|
{ dispatchNo: 'EXT', items: [{ warehousePalletId: '3' }] }]
|
|
|
|
|
assert.deepEqual(fleetCostShares(trip, details), [
|
|
|
|
|
{ businessNo: 'OWN', sourceType: 'OWN', dispatchNo: 'OWN', warehousePalletIds: ['1', '2'], palletSpaces: 2 },
|
|
|
|
|
{ businessNo: 'BK-ONE', sourceType: 'EXTERNAL', dispatchNo: 'EXT', warehousePalletIds: ['3'], palletSpaces: 1 },
|
|
|
|
|
])
|
|
|
|
|
assert.throws(() => fleetCostShares(trip, [{ ...details[0], items: [{ warehousePalletId: '3' }] }, details[1]]), /distinct/)
|
|
|
|
|
assert.throws(() => fleetCostShares(trip, [details[0], { ...details[1], items: [{ warehousePalletId: '4' }] }]), /receipt/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('missing isolated cost actor preparation rejects before readers or fixture creation', async () => {
|
|
|
|
|
let touched = false
|
|
|
|
|
await assert.rejects(runFleetAcceptance({ execute: true, environment: 'TEST' }, {
|
|
|
|
|
env: { CHONGCHONG_ENV: 'test' }, prepareCostActors: null, readers: { preflight() { touched = true } },
|
|
|
|
|
}), /actor preparation/)
|
|
|
|
|
assert.equal(touched, false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const failLogin of [false, true]) test(`cost actors create exact isolated roles and disable only owned accounts; login failure=${failLogin}`, async () => {
|
|
|
|
|
const calls = [], saved = [], roles = [], users = []
|
|
|
|
|
const context = { marker: 'RUN', token: 'admin-secret', createdResources: {}, async findAvailablePhone() { return '19900000000' },
|
|
|
|
|
async checkpoint() { saved.push(structuredClone(context.createdResources)) },
|
|
|
|
|
async request(method, path, body, token) {
|
|
|
|
|
const pending = saved.at(-1).fleetCostActors.requests.at(-1)
|
|
|
|
|
assert.equal(pending.status, 'SENDING'); assert.equal(pending.path, path)
|
|
|
|
|
calls.push({ method, path, body, token })
|
|
|
|
|
let data
|
|
|
|
|
if (path.endsWith('/authorization/permissions')) data = token === 'session-1' ? ['platform:fleet:trip:list'] : []
|
|
|
|
|
else if (path.endsWith('/permissions') && method === 'GET') data = [{ id: '7', permissionCode: 'platform:fleet:trip:list', status: 'ACTIVE' }]
|
|
|
|
|
else if (path.endsWith('/roles') && method === 'POST') { data = { ...body, id: String(roles.length + 1), roleType: 'CUSTOM' }; roles.push(data) }
|
|
|
|
|
else if (path.endsWith('/roles/permissions')) data = body
|
|
|
|
|
else if (path.endsWith('/users') && method === 'POST') { data = { ...body, membershipId: String(users.length + 10) }; users.push(data) }
|
|
|
|
|
else if (path === '/api/auth/login') { if (failLogin) throw Error('login unavailable'); data = { accessToken: `session-${users.length}` } }
|
|
|
|
|
else if (path.endsWith('/status')) data = body
|
|
|
|
|
else assert.fail(path)
|
|
|
|
|
return { http: 200, body: { code: '200', data }, requestId: 'request', traceId: 'trace' }
|
|
|
|
|
} }
|
|
|
|
|
if (failLogin) await assert.rejects(prepareFleetCostActors(context), /login unavailable/)
|
|
|
|
|
else {
|
|
|
|
|
const actors = await prepareFleetCostActors(context)
|
|
|
|
|
assert.equal(actors.listOnlyToken, 'session-1'); assert.equal(actors.deniedToken, 'session-2')
|
|
|
|
|
assert.equal(calls.filter(row => row.path.endsWith('/roles/permissions'))[1].body.permissionIds.length, 0)
|
|
|
|
|
await actors.close()
|
|
|
|
|
const count = calls.length; await actors.close(); assert.equal(calls.length, count)
|
|
|
|
|
}
|
|
|
|
|
assert.equal(context.createdResources.fleetCostActors.status, 'DISABLED')
|
|
|
|
|
assert.equal(calls.filter(row => /\/users\/\d+\/status$/.test(row.path)).length, users.length)
|
|
|
|
|
assert.equal(calls.filter(row => /\/roles\/\d+\/status$/.test(row.path)).length, roles.length)
|
|
|
|
|
const evidence = JSON.stringify(saved)
|
|
|
|
|
assert.ok(!evidence.includes('session-1')); assert.ok(!evidence.includes('admin-secret'))
|
|
|
|
|
for (const user of users) assert.ok(!evidence.includes(user.password))
|
|
|
|
|
assert.ok(!calls.some(row => row.method === 'DELETE'))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('main chain performs handover after actual DEPART and before receipts, and cleanup follows current-resource closure', async () => {
|
|
|
|
|
const source = readFileSync(new URL('./fleet-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
|
|
|
|
const savedDeparture = source.indexOf('proof.departure = departed'), handover = source.indexOf("stage('IN_TRANSIT_HANDOVER')")
|
|
|
|
|
const receipt = source.indexOf("stage('RECEIPTS_AND_RESOURCE_RELEASE')"), closure = source.indexOf('proof.closure = await finishFleetTrip')
|
|
|
|
|
const cleanup = source.indexOf('await cleanupFleetHandoverResources(context, MODULE, trip)')
|
|
|
|
|
assert.ok(savedDeparture > 0 && savedDeparture < handover && handover < receipt && receipt < closure && closure < cleanup)
|
|
|
|
|
assert.match(source, /finally \{ await disablePicker\(context, picker, 71\) \}/)
|
2026-10-03 06:26:33 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
async function executeFixture(failProducer = false) {
|
|
|
|
|
const temporary = mkdtempSync(join(tmpdir(), 'fleet-entry-contract-'))
|
|
|
|
|
const root = join(temporary, 'chongchong-global', 'baiyue-firstleg'); mkdirSync(root, { recursive: true })
|
|
|
|
|
const calls = [], secrets = 'never-print-this-secret'
|
|
|
|
|
const env = { CHONGCHONG_ENV: 'test', CHONGCHONG_ADMIN_API_BASE_URL: 'https://test.invalid', CHONGCHONG_ADMIN_USERNAME: 'fixture-admin',
|
|
|
|
|
CHONGCHONG_ADMIN_PASSWORD: secrets, ADMIN_PDA_USERNAME: 'fixture-pda', ADMIN_PDA_PASSWORD: secrets }
|
|
|
|
|
const config = parseFleetArguments(['--execute', '--environment', 'TEST', '--run-id', 'fixture'])
|
|
|
|
|
const createContext = options => ({ options, context: {}, createdResources: {}, steps: [], modules: [], coverageGaps: [], token: secrets,
|
|
|
|
|
async executeModule(name, handler) {
|
|
|
|
|
try { await handler(); this.modules.push({ name, success: true }) }
|
|
|
|
|
catch { this.modules.push({ name, success: false }); this.steps.push({ name: 'producer failed', passed: false }) }
|
|
|
|
|
},
|
|
|
|
|
async run() {
|
|
|
|
|
await this.executeModule('producer', async () => { calls.push('producer'); if (failProducer) throw Error('fixture failure') })
|
|
|
|
|
await this.executeModule('phase4-overseas-dispatch', async () => { calls.push('dispatch'); this.createdResources.current = { accessToken: secrets } })
|
|
|
|
|
return { success: true, steps: this.steps, modules: this.modules, createdResources: this.createdResources, summary: { failed: 0 } }
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
try {
|
|
|
|
|
const options = { root, env, createContext, readers: { async preflight() { calls.push('preflight') }, async readReceiving() { throw Error('not used') } },
|
2026-10-03 14:52:04 +08:00
|
|
|
scenario: async context => {
|
|
|
|
|
calls.push('fleet'); context.createdResources.fleetAcceptance = { status: 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP' }
|
|
|
|
|
const command = { requests: [{ method: 'POST', status: 'SENDING', body: { idempotencyKey: 'original-key' }, accessToken: secrets }] }
|
|
|
|
|
await context.context.checkpointFleetHandover(command)
|
|
|
|
|
const firstPath = join(root, 'reports/fleet-fixture/evidence/handover-000001.json')
|
|
|
|
|
const before = readFileSync(firstPath, 'utf8'), saved = JSON.parse(before)
|
|
|
|
|
assert.equal(saved.proof.requests[0].body.idempotencyKey, 'original-key')
|
|
|
|
|
assert.equal(saved.proof.requests[0].status, 'SENDING'); assert.ok(!before.includes(secrets))
|
|
|
|
|
command.requests[0].status = 'RESPONDED'
|
|
|
|
|
await context.context.checkpointFleetHandover(command)
|
|
|
|
|
assert.equal(readFileSync(firstPath, 'utf8'), before)
|
|
|
|
|
assert.equal(JSON.parse(readFileSync(join(root, 'reports/fleet-fixture/evidence/handover-000002.json'), 'utf8')).proof.requests[0].status, 'RESPONDED')
|
|
|
|
|
} }
|
2026-10-03 06:26:33 +08:00
|
|
|
const result = await runFleetAcceptance(config, options)
|
|
|
|
|
const report = JSON.parse(readFileSync(result.report, 'utf8'))
|
|
|
|
|
assert.ok(!readFileSync(result.report, 'utf8').includes(secrets))
|
|
|
|
|
assert.equal(existsSync(join(root, 'reports/.fleet-acceptance.lock')), false)
|
|
|
|
|
const oldReport = readFileSync(result.report, 'utf8')
|
|
|
|
|
const repeated = await runFleetAcceptance(config, options)
|
|
|
|
|
assert.equal(repeated.success, false)
|
|
|
|
|
assert.equal(readFileSync(result.report, 'utf8'), oldReport, 'existing run is never overwritten or resumed')
|
|
|
|
|
return { result, report, calls }
|
|
|
|
|
} finally {
|
|
|
|
|
assert.equal(dirname(resolve(temporary)), resolve(tmpdir()))
|
|
|
|
|
assert.ok(resolve(temporary).startsWith(join(resolve(tmpdir()), 'fleet-entry-contract-')))
|
|
|
|
|
rmSync(temporary, { recursive: true, force: true })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('fresh entry runs dedicated fleet scenario after prerequisites without altering the default F40 chain', async () => {
|
|
|
|
|
const { result, report, calls } = await executeFixture()
|
|
|
|
|
assert.equal(result.success, true)
|
|
|
|
|
assert.deepEqual(calls, ['preflight', 'producer', 'dispatch', 'fleet'])
|
|
|
|
|
assert.equal(report.fleetGoalComplete, false)
|
|
|
|
|
assert.equal(report.createdResources.current.accessToken, '[REDACTED]')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('failed prerequisite stops before further fixture creation and retains the failed report', async () => {
|
|
|
|
|
const { result, report, calls } = await executeFixture(true)
|
|
|
|
|
assert.equal(result.success, false)
|
|
|
|
|
assert.deepEqual(calls, ['preflight', 'producer'])
|
|
|
|
|
assert.equal(report.fatal, 'Producer stopped: producer')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('environment mismatch rejects before invoking any readers or contexts', async () => {
|
|
|
|
|
let touched = false
|
|
|
|
|
await assert.rejects(() => runFleetAcceptance({ execute: true, environment: 'TEST', runId: 'new' }, {
|
|
|
|
|
env: { CHONGCHONG_ENV: 'production' }, readers: { preflight() { touched = true } },
|
|
|
|
|
}), /TEST/)
|
|
|
|
|
assert.equal(touched, false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('reviewed resume requires explicit paired checkpoint options', () => {
|
|
|
|
|
assert.throws(() => parseFleetArguments(['--resume-report', 'reports/old/report.json']))
|
|
|
|
|
assert.throws(() => parseFleetArguments(['--resume-report', 'reports/old/report.json', '--resume-stage', 'PICK_EACH_DISPATCH']))
|
|
|
|
|
const c = parseFleetArguments(['--execute', '--environment', 'TEST', '--run-id', 'reviewed', '--resume-report', 'reports/old/report.json', '--resume-stage', 'FULL_COVERAGE'])
|
|
|
|
|
assert.equal(c.resumeStage, 'FULL_COVERAGE')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('checkpoint guard forbids repeating commands whose write outcome may be unknown', () => {
|
|
|
|
|
const report = { success: false, createdResources: {
|
|
|
|
|
fleetAcceptance: { marker: 'M', stage: 'FULL_COVERAGE', status: 'CHECKING', tripNo: 'T', originalDispatch: { dispatchNo: 'D' } },
|
|
|
|
|
fleetTrips: [{ tripNo: 'T', dispatchNo: 'D' }],
|
|
|
|
|
fleetFeeCoverageCases: [{ mode: 'FULL', marker: 'M', dispatchNo: 'D', proof: { status: 'CHECKING', requests: [{ method: 'GET' }] } }],
|
|
|
|
|
} }
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(report).marker, 'M')
|
|
|
|
|
const mutated = structuredClone(report)
|
|
|
|
|
mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.push({ method: 'POST' })
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(mutated), /unknown outcomes/)
|
|
|
|
|
mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.pop()
|
|
|
|
|
mutated.createdResources.fleetFeeCoverageCases[0].proof.outboundNo = 'OUT-1'
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(mutated), /unknown outcomes/)
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED'), /exact successful command/)
|
|
|
|
|
Object.assign(mutated.createdResources.fleetFeeCoverageCases[0].proof, { deliveryNos: ['DEL-1'], confirmationRequest: { idempotencyKey: 'original-key' } })
|
|
|
|
|
mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.push({ label: '正式保存本次原费用服务覆盖并转派送', method: 'POST', http: 200, code: '200' })
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED').persisted, true)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('receipt-ready reconciliation cannot repeat a receipt that has already started', () => {
|
|
|
|
|
const report = { success: false, steps: [{ name: '创建海外PDA仓库操作员71', code: 'A0409', message: '手机号已存在' }], createdResources: {
|
|
|
|
|
fleetAcceptance: { marker: 'M', stage: 'RESERVATIONS', status: 'CHECKING', tripNo: 'T' },
|
|
|
|
|
fleetTrips: [{ tripNo: 'T', dispatchNo: 'D2', reservation: { status: 'VERIFIED' } }],
|
|
|
|
|
fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2', proof: { status: 'VERIFIED' } }],
|
|
|
|
|
} }
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(report, 'EXTERNAL_RECEIPT_READY').receiptReady, true)
|
|
|
|
|
report.createdResources.fleetTrips[0].externalReceipt = { status: 'CHECKING' }
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(report, 'EXTERNAL_RECEIPT_READY'), /no receipt writes/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('palletized checkpoint requires a saved successful command, owned operator, and no later confirmation', () => {
|
|
|
|
|
const report = { success: false, createdResources: {
|
|
|
|
|
fleetAcceptance: { marker: 'M', stage: 'EXTERNAL_RECEIPT', status: 'CHECKING', tripNo: 'T', picker: { membershipId: '71' }, pickerMarker: 'M-PDA' },
|
|
|
|
|
fleetTrips: [{ tripNo: 'T', dispatchNo: 'D2', reservation: { status: 'VERIFIED' }, externalReceipt: { status: 'CHECKING',
|
|
|
|
|
warehousePalletId: 'P1', palletizeRequest: { idempotencyKey: 'original' }, requests: [
|
|
|
|
|
{ label: 'PDA正式完成外接收货及库存入账', method: 'POST', http: 200, code: '200' },
|
|
|
|
|
] } }],
|
|
|
|
|
fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2', proof: { status: 'VERIFIED' } }],
|
|
|
|
|
} }
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(report, 'EXTERNAL_PALLETIZED').palletized, true)
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(report, 'EXTERNAL_RECEIPT_CONFIRMED'), /exact successful PDA command/)
|
|
|
|
|
const confirmed = structuredClone(report)
|
|
|
|
|
confirmed.createdResources.fleetTrips[0].externalReceipt.receiptRequest = { idempotencyKey: 'confirm-original' }
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(confirmed, 'EXTERNAL_RECEIPT_CONFIRMED'), /exact successful PDA command/)
|
|
|
|
|
confirmed.createdResources.fleetTrips[0].externalReceipt.requests.push({ label: '运营按实际1托位和新单价30确认实收', method: 'POST', http: 200, code: '200' })
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(confirmed, 'EXTERNAL_RECEIPT_CONFIRMED').receiptConfirmed, true)
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(confirmed, 'EXTERNAL_PALLETIZED'), /exact successful PDA command/)
|
|
|
|
|
for (const mutate of [
|
|
|
|
|
r => { r.fleetTrips[0].externalReceipt.requests[0].code = 'A0409' },
|
|
|
|
|
r => { delete r.fleetTrips[0].externalReceipt.palletizeRequest.idempotencyKey },
|
|
|
|
|
r => { delete r.fleetAcceptance.picker },
|
|
|
|
|
r => { r.fleetTrips[0].externalReceipt.receiptRequest = { idempotencyKey: 'later' } },
|
|
|
|
|
r => { r.fleetTrips[0].externalDispatch = { status: 'CHECKING' } },
|
|
|
|
|
]) {
|
|
|
|
|
const invalid = structuredClone(report); mutate(invalid.createdResources)
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(invalid, 'EXTERNAL_PALLETIZED'), /exact successful PDA command/)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
function terminalFixture(fault = '') {
|
|
|
|
|
const finalTrip = { status: 'COMPLETED', completedAt: '2026-10-02T22:01:00Z', recordVersion: '30', vehicleRelease: { endLocation: 'ONT8' }, driverRelease: { endLocation: 'ONT8' } }
|
|
|
|
|
const closure = { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED', finalTrip, probes: [{ tripNo: 'PROBE' }] }
|
|
|
|
|
const trip = { tripNo: 'T', closure, externalReceipt: { status: 'VERIFIED' }, reassignment: { status: 'VERIFIED' }, externalDispatch: { status: 'VERIFIED', dispatchNo: 'EXT', deliveryNo: 'DEL-EXT' } }
|
|
|
|
|
const proof = { marker: 'M', tripNo: 'T', customerId: '92', stage: 'RECEIPTS_AND_RESOURCE_RELEASE', closure,
|
|
|
|
|
receipts: ['DEL-OWN', 'DEL-EXT'].map(deliveryNo => ({ deliveryNo, signedAt: '2026-10-02T22:00:00' })),
|
|
|
|
|
departure: { shippedPackages: 6 }, finance: { status: 'CONSUMER_PERSISTED', source: { sourceFeeId: 'F1', sourceVersion: 1 } } }
|
|
|
|
|
const resources = { fleetAcceptance: proof, fleetTrips: [trip], fleetFeeCoverageCases: [{ mode: 'PARTIAL', dispatchNo: 'OWN', proof: { deliveryNos: ['DEL-OWN'] } }] }
|
|
|
|
|
const calls = []
|
|
|
|
|
const context = { createdResources: resources, assert() {}, async request(method, path) {
|
|
|
|
|
calls.push({ method, path }); assert.equal(method, 'GET')
|
|
|
|
|
let data
|
|
|
|
|
if (path.endsWith('/trips/T')) data = { trip: finalTrip, dispatches: ['OWN', 'EXT'].map(dispatchNo => ({ dispatchNo })) }
|
|
|
|
|
else if (path.endsWith('/trips/PROBE')) data = { trip: { status: 'CANCELLED' } }
|
|
|
|
|
else if (path.includes('/dispatches/')) { const number = path.split('/').at(-1), count = number === 'OWN' ? 5 : 1
|
|
|
|
|
data = { dispatchNo: number, status: 'DEPARTED', packageCount: count, pickedPackageCount: count, shippedPackageCount: fault === 'quantity' ? 0 : count } }
|
|
|
|
|
else if (path.includes('/delivery-orders/')) data = { order: { deliveryNo: path.split('/').at(-1).split('?')[0], status: fault === 'unsigned' ? 'WAIT_DELIVERY' : 'DELIVERED', signedAt: '2026-10-02T22:00:00' } }
|
|
|
|
|
else if (path.includes('/fee-pool?')) data = [{ customerId: '92', sourceId: 'F1', sourceVersion: fault === 'wrong-fee-version' ? 2 : 1, amount: 30, currency: 'CNY', sourceType: 'OVERSEAS_DELIVERY_FEE' }]
|
|
|
|
|
else throw Error('unexpected path')
|
|
|
|
|
return { http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'r', traceId: 't' }
|
|
|
|
|
} }
|
|
|
|
|
return { context, calls, report: { success: false, createdResources: resources, steps: [{ name: '本次车次货物结案及车辆司机释放完成', passed: false }] } }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
test('terminal reconciliation reads actual completion, every dispatch/delivery and exact fee without replaying writes', async () => {
|
|
|
|
|
const f = terminalFixture()
|
|
|
|
|
assert.equal(reviewedFullCheckpoint(f.report, 'TERMINAL_READONLY').terminal, true)
|
|
|
|
|
const result = await verifyFleetTerminalState(f.context)
|
|
|
|
|
assert.equal(result.terminalReadback.status, 'VERIFIED')
|
|
|
|
|
assert.equal(f.calls.length, 7)
|
|
|
|
|
assert.ok(f.calls.every(row => row.method === 'GET'))
|
|
|
|
|
const invalid = terminalFixture().report
|
|
|
|
|
invalid.steps.push({ name: '真实业务失败', passed: false })
|
|
|
|
|
assert.throws(() => reviewedFullCheckpoint(invalid, 'TERMINAL_READONLY'), /known aggregate assertion/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
for (const fault of ['quantity', 'unsigned', 'wrong-fee-version']) test(`terminal readback rejects ${fault}`, async () => {
|
|
|
|
|
const f = terminalFixture(fault)
|
|
|
|
|
await assert.rejects(() => verifyFleetTerminalState(f.context))
|
|
|
|
|
assert.equal(f.context.createdResources.fleetAcceptance.terminalReadback.status, 'CHECKING')
|
|
|
|
|
})
|