Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-business-contract.test.mjs
T

568 lines
35 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import { randomUUID } from 'node:crypto'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { run, createPartnerWarehouse, retirePartnerWarehouse } from './phase4-overseas-business.mjs'
import { verifyUsTruckFulfillment } from './us-truck-fulfillment.mjs'
test('fixed partner warehouse is reused and never retired', async () => {
const calls = []
const context = { options: { fixedCustomerId: '123' }, marker: 'RUN', createdResources: {},
async request(method, path) { calls.push({ method, path }); return { body: { data: [{ warehouseId: '456' }] } } },
apiSuccess(_module, _name, response) { return response.body.data },
}
const warehouse = await createPartnerWarehouse(context)
assert.equal(warehouse.id, '456')
await retirePartnerWarehouse(context, warehouse)
assert.equal(calls.length, 1)
assert.equal(calls[0].method, 'GET')
})
const here = path.dirname(fileURLToPath(import.meta.url))
const source = fs.readFileSync(path.join(here, 'phase4-overseas-business.mjs'), 'utf8')
test('卡车履约接受正式Long字符串版本并保持原单重放一致', async () => {
const context = fixture(), request = context.request.bind(context)
context.request = async (...args) => {
const response = structuredClone(await request(...args))
if (args[1].includes('/us-truck-orders/') && response.body?.data?.summary)
response.body.data.summary.version = String(response.body.data.summary.version)
return response
}
await run(context)
assert.equal(context.createdResources.phase4OverseasBusiness.truck.fulfillment.status, 'HTTP_VERIFIED')
})
test('卡车请求先持久化,正常与失败响应均留有证据', async () => {
const context = fixture(), original = context.request.bind(context)
context.request = async (method, path, ...args) => {
if (context.createdResources.phase4OverseasBusiness?.truck?.fulfillment) {
const last = context.savedTruckEvidence.at(-1)?.requests.at(-1)
if (path.includes('/us-truck-orders/TK1')) {
assert.equal(last?.status, 'PREPARED')
assert.equal(last?.method, method)
}
}
return original(method, path, ...args)
}
await run(context)
assert.equal(context.savedTruckEvidence.at(-1).status, 'HTTP_VERIFIED')
assert.ok(context.savedTruckEvidence.some(e => e.requests.some(r => r.expectedRejection && r.status === 'REJECTED')))
})
test('卡车检查点写入失败不发送请求,不能误记未知网络结果', async () => {
const proof = { marker: 'BATCH', orderNo: 'TRUCK-1', customerId: '101', finalStatus: 'WAITING_INBOUND' }
let calls = 0
const context = { marker: 'BATCH', createdResources: { phase4OverseasBusiness: { truck: proof } },
request: async () => { calls++; throw Error('must not send') }, checkpoint: async () => { throw Error('disk full') } }
await assert.rejects(verifyUsTruckFulfillment(context, proof), /disk full/)
assert.equal(calls, 0)
assert.equal(proof.fulfillment.requests[0].status, 'PREPARED')
assert.equal(proof.fulfillment.status, 'FAILED')
})
test('卡车默认落盘禁止新进程用同批次覆盖失败证据', async () => {
const marker = `TRUCK-OFFLINE-${randomUUID()}`
const file = new URL(`../../reports/us-truck-checkpoints/${marker}.json`, import.meta.url)
let calls = 0
const make = () => {
const proof = { marker, orderNo: 'TRUCK-1', customerId: '101', finalStatus: 'WAITING_INBOUND' }
return { proof, context: { marker, createdResources: { phase4OverseasBusiness: { truck: proof } },
request: async () => { calls++; throw Error('connection lost') } } }
}
try {
const first = make()
await assert.rejects(verifyUsTruckFulfillment(first.context, first.proof), /connection lost/)
const saved = fs.readFileSync(file, 'utf8')
assert.equal(JSON.parse(saved).fulfillment.requests[0].status, 'UNKNOWN_RESULT')
const second = make()
await assert.rejects(verifyUsTruckFulfillment(second.context, second.proof), /EEXIST/)
assert.equal(calls, 1)
assert.equal(fs.readFileSync(file, 'utf8'), saved)
} finally {
if (fs.existsSync(file)) fs.unlinkSync(file)
}
})
for (const action of ['receive-pallets', 'allocate-pallets']) {
for (const fault of ['accepted', 'unknown', 'wrong rejection', 'drift']) {
test(`卡车 ${action} 无效命令拒绝校验: ${fault}`, async () => {
const context = fixture(), original = context.request.bind(context)
let rejected = false
context.request = async (method, path, body, ...args) => {
if (method === 'POST' && path.includes(`/${action}?`) && body?.idempotencyKey?.endsWith('-invalid')) {
rejected = true
if (fault === 'accepted') return response({})
if (fault === 'unknown') return { http: 0 }
if (fault === 'wrong rejection') return response(null, 'A0403')
}
const result = await original(method, path, body, ...args)
if (fault === 'drift' && rejected && method === 'GET' && path.includes('/us-truck-orders/TK1?')) {
result.body.data.summary.version++
}
return result
}
await assert.rejects(run(context))
assert.equal(context.createdResources.phase4OverseasBusiness.truck.fulfillment.status, 'FAILED')
assert.equal(context.calls.some(c => c.method === 'POST' && c.path.includes(`/${action}?`) && !c.body.idempotencyKey.endsWith('-invalid')), false)
})
}
}
for (const fault of ['network', 'wrong customer', 'wrong pallet count', 'replay drift']) {
test(`truck fulfillment stops and retains original order on ${fault}`, async () => {
const context = fixture(), original = context.request.bind(context)
let receipts = 0
context.request = async (method, path, ...args) => {
if (method === 'POST' && path.includes('/receive-pallets?') && !args[0]?.idempotencyKey?.endsWith('-invalid')) {
receipts += 1
if (fault === 'network') return { http: 0 }
const result = structuredClone(await original(method, path, ...args))
if (fault === 'wrong customer') result.body.data.summary.customerId = 'other'
if (fault === 'wrong pallet count') result.body.data.pallets.push({ ...result.body.data.pallets[0] })
if (fault === 'replay drift' && receipts === 2) result.body.data.summary.version += 1
return result
}
return original(method, path, ...args)
}
await assert.rejects(run(context))
const proof = context.createdResources.phase4OverseasBusiness.truck
assert.equal(proof.fulfillment.status, 'FAILED')
assert.equal(proof.retained, true)
assert.equal(receipts, fault === 'replay drift' ? 2 : 1)
assert.equal(context.calls.some(c => c.path.includes('/allocate-pallets?') || c.path.includes('/TK1/status?')), false)
await assert.rejects(verifyUsTruckFulfillment(context, proof), /本轮未执行/)
})
}
for (const fault of ['accepted', 'wrong code', 'missing trace', 'detail drift']) {
test(`卡车部分分配待派车拒绝检查失败即停止: ${fault}`, async () => {
const context = fixture(), original = context.request.bind(context)
let rejected = false
context.request = async (method, path, body, ...args) => {
const result = await original(method, path, body, ...args)
if (body?.idempotencyKey?.endsWith('prepare-dispatch-partial')) {
rejected = true
if (fault === 'accepted') result.body.code = 200
if (fault === 'wrong code') result.body.code = 'A0400'
if (fault === 'missing trace') delete result.traceId
} else if (rejected && fault === 'detail drift' && method === 'GET' && path.includes('/us-truck-orders/TK1?')) {
result.body.data.summary.version++
}
return result
}
await assert.rejects(run(context))
assert.equal(context.createdResources.phase4OverseasBusiness.truck.fulfillment.status, 'FAILED')
assert.equal(context.calls.some(c => c.body?.idempotencyKey?.endsWith('allocate-pallets-1')), false)
})
}
for (const fault of ['premature complete', 'unselected allocated', 'selected unchanged']) {
test(`卡车部分分配拒绝错误结果: ${fault}`, async () => {
const context = fixture(), original = context.request.bind(context)
context.request = async (method, path, body, ...args) => {
const result = await original(method, path, body, ...args)
if (method === 'POST' && path.includes('/allocate-pallets?') && body?.idempotencyKey?.endsWith('-0')) {
if (fault === 'premature complete') result.body.data.summary.status = 'ALLOCATED'
if (fault === 'unselected allocated') result.body.data.pallets[1].status = 'ALLOCATED'
if (fault === 'selected unchanged') result.body.data.pallets[0].status = 'RECEIVED'
}
return result
}
await assert.rejects(run(context))
assert.equal(context.createdResources.phase4OverseasBusiness.truck.fulfillment.status, 'FAILED')
assert.equal(context.calls.some(c => c.path.includes('/prepare-dispatch?')), false)
assert.equal(context.calls.some(c => c.body?.idempotencyKey?.endsWith('allocate-pallets-1')), false)
})
}
test('fixed customer beyond first page is found without creating or retiring a partner warehouse', async () => {
const context = fixture(), request = context.request.bind(context), pages = []
context.options = { fixedCustomerId: '10001' }
context.request = async (method, path, ...args) => {
if (path.startsWith('/api/platform/v1/iam/tenants?')) {
pages.push(path)
if (path.includes('page=1&')) return response({ ...page([{ id: '9999', status: 'ACTIVE' }]), size: 200, total: 201, pages: 2 })
return response(page([{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]))
}
return request(method, path, ...args)
}
await run(context)
assert.equal(pages.length, 2)
assert.ok(context.createdResources.phase4OverseasBusiness.purchase)
assert.ok(context.calls.filter(call => call.path.includes('/warehouse-admin')).every(call => call.method === 'GET'))
})
function response(data, code = '200') {
return { http: 200, body: { code, message: code === '200' ? 'success' : 'conflict', data },
requestId: `request-${code}`, traceId: `trace-${code}` }
}
function page(records = []) { return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 } }
function blankToNull(value) { return typeof value === 'string' && value.trim() === '' ? null : value }
function fixture(options = {}) {
const calls = []; const steps = []; const gaps = []; const blockers = []
const marker = 'HTTP-20260824-001'
const state = {
warehouseStatus: 'ACTIVE', purchase: null, purchaseHash: '', truck: null, truckHash: '', looseboard: null, looseboardHash: '',
purchaseHistories: [], truckHistories: [], looseboardHistories: [],
purchaseReviews: new Map(), truckReviews: new Map(), truckStatuses: new Map(), looseboardStatuses: new Map(),
}
const activeCustomers = options.activeCustomers ?? [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' }]
const warehouseOptions = options.warehouseOptions ?? [
{ warehouseId: '20001', warehouseCode: 'LAX', warehouseName: '洛杉矶仓', countryCode: 'US' },
]
const credentials = options.credentials === false ? {} : { username: 'merchant', password: 'secret' }
function purchaseDetail() {
return { summary: state.purchase, remark: state.purchase?.remark ?? null, histories: state.purchaseHistories }
}
function purchaseCustomer() { return { order: state.purchase, fees: [], progress: [], boxes: [], shipments: [], exceptionSummaries: [] } }
function purchasePlatform() { return { customerView: purchaseCustomer(), internalRemark: state.purchase?.remark ?? null, reviewHistory: state.purchaseHistories } }
function truckDetail() { return structuredClone({ summary: state.truck, pallets: state.truckPallets || [], remark: state.truck?.remark ?? null, histories: state.truckHistories }) }
function looseboardDetail() { return { summary: state.looseboard, remark: state.looseboard?.remark ?? null, histories: state.looseboardHistories } }
const context = {
marker, calls, steps, gaps, blockers,
savedTruckEvidence: [],
async checkpoint() {
if (this.createdResources.phase4OverseasBusiness?.truck?.fulfillment)
this.savedTruckEvidence.push(structuredClone(this.createdResources.phase4OverseasBusiness.truck.fulfillment))
},
context: { merchantQualificationFixture: credentials },
createdResources: { merchantQualificationFixture: { tenantId: '10001', marker } },
async request(...args) {
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/.test(String(args[1]));
const handler = route ? async function(method,requestPath,body,token,headers) {
calls.push({ channel: 'overseas', method, path: requestPath, body, tokenPresent: Boolean(token) })
if (requestPath === '/api/merchant/v1/last-leg/overseas-procurements' && method === 'POST') {
assert.equal(token, 'merchant-overseas-token')
const hash = JSON.stringify(body)
if (state.purchase) return hash === state.purchaseHash ? response(purchaseCustomer()) : response(null, 'A0409')
state.purchaseHash = hash
state.purchase = { id: '30001', orderNo: 'OP1', customerId: '10001', customerName: body.customerName,
purchaseWarehouseId: null, purchaseWarehouseName: null, purchaseCategory: body.purchaseCategory,
productName: body.productName, productSpecification: body.productSpecification, productUrl: body.productUrl,
purchaseQuantity: body.purchaseQuantity,
status: 'PENDING_REVIEW', version: 0, remark: body.remark }
state.purchaseHistories.push({ operationType: 'CREATE', fromStatus: null, toStatus: 'PENDING_REVIEW', remark: marker })
return response(purchaseCustomer())
}
if (requestPath.includes('/customer-options')) return response(state.purchase
? [{ customerId: '10001', customerName: '客户甲' }] : [])
if (requestPath.includes('/warehouse-options')) return response(warehouseOptions)
if (requestPath.includes('/overseas-procurements?') && method === 'GET') return response(page(state.purchase ? [state.purchase] : []))
if (requestPath.includes('/overseas-procurements/OP1/review?') && method === 'POST') {
assert.equal(Number.isInteger(Number(body.version)), true)
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
const hash = JSON.stringify(body)
const replay = state.purchaseReviews.get(body.idempotencyKey)
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
if (state.purchase.status !== 'PENDING_REVIEW') return response(null, 'A0409')
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
const next = body.decision === 'REJECT' ? 'REJECTED' : 'WAIT_PURCHASE'
state.purchase = { ...state.purchase, status: next, version: state.purchase.version + 1 }
state.purchaseHistories.push({ operationType: 'REVIEW', fromStatus: 'PENDING_REVIEW', toStatus: next, remark: body.remark })
const result = structuredClone(purchaseDetail())
state.purchaseReviews.set(body.idempotencyKey, { hash, result })
return response(result)
}
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'GET') return response(purchasePlatform())
if (requestPath.includes('/overseas-procurements/OP1?') && method === 'PATCH') {
if (Number(body.version) !== state.purchase.version) return response(null, 'A0409')
if (Object.keys(body).some(key => !['version', 'remark'].includes(key))) return response(null, 'A0400')
if (!Object.hasOwn(body, 'remark') || blankToNull(body.remark) === state.purchase.remark) return response(purchaseDetail())
state.purchase = { ...state.purchase, remark: blankToNull(body.remark), version: state.purchase.version + 1 }
state.purchaseHistories.push({ operationType: 'UPDATE', fromStatus: state.purchase.status,
toStatus: state.purchase.status, changeDetail: JSON.stringify({ marker }) })
return response(purchaseDetail())
}
if (requestPath.includes('/us-truck-orders?') && method === 'POST') {
const hash = JSON.stringify(body)
if (state.truck) return hash === state.truckHash ? response(truckDetail()) : response(null, 'A0409')
state.truckHash = hash
state.truck = { id: '40001', orderNo: 'TK1', customerId: '10001', receivedPalletCount: 0, allocatedPalletCount: 0, status: 'PENDING_REVIEW', version: 0, ...body }
state.truckHistories.push({ fromStatus: null, toStatus: 'PENDING_REVIEW', remark: marker })
return response(truckDetail())
}
if (requestPath.includes('/us-truck-orders?') && method === 'GET') return response(page(state.truck ? [state.truck] : []))
if (requestPath.includes('/us-truck-orders/TK1/review?') && method === 'POST') {
assert.equal(Number.isInteger(Number(body.version)), true)
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
const hash = JSON.stringify(body)
const replay = state.truckReviews.get(body.idempotencyKey)
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
if (state.truck.status !== 'PENDING_REVIEW' || Number(body.version) !== state.truck.version) {
return response(null, 'A0409')
}
const next = body.decision === 'APPROVE' ? 'WAITING_INBOUND' : 'REJECTED'
const from = state.truck.status
state.truck = { ...state.truck, status: next, version: state.truck.version + 1 }
state.truckHistories.push({ fromStatus: from, toStatus: next, remark: body.remark })
const result = structuredClone(truckDetail())
state.truckReviews.set(body.idempotencyKey, { hash, result })
return response(result)
}
const stages = { 'receive-pallets': ['WAITING_INBOUND', 'WAITING_CONSOLIDATION'],
'allocate-pallets': ['WAITING_CONSOLIDATION', 'ALLOCATED'], 'prepare-dispatch': ['ALLOCATED', 'PENDING_DISPATCH'],
dispatch: ['PENDING_DISPATCH', 'DISPATCHED'], 'start-transit': ['DISPATCHED', 'IN_TRANSIT'], complete: ['IN_TRANSIT', 'COMPLETED'] }
const action = requestPath.match(/\/us-truck-orders\/TK1\/([^?]+)\?/)?.[1]
if (method === 'POST' && stages[action]) {
if (action === 'prepare-dispatch' && state.truck.status === 'PARTIALLY_ALLOCATED') {
assert.equal(body.version, state.truck.version)
return response(null, 'A0409')
}
const existing = state.truckStatuses.get(body.idempotencyKey)
if (existing) return response(truckDetail())
let [from, to] = stages[action]
if (action === 'allocate-pallets') {
from = state.truck.allocatedPalletCount === 0 ? 'WAITING_CONSOLIDATION' : 'PARTIALLY_ALLOCATED'
to = state.truck.allocatedPalletCount === 0 ? 'PARTIALLY_ALLOCATED' : 'ALLOCATED'
}
assert.equal(state.truck.status, from)
assert.equal(body.version, state.truck.version)
if (action === 'receive-pallets') {
if (body.palletPackageCounts.reduce((sum, count) => sum + count, 0) !== body.packageCount) return response(null, 'A0400')
assert.deepEqual(body.palletPackageCounts, [1, 1])
assert.equal(body.actualPalletCount, 2)
state.truckPallets = [{ id: '701', palletNo: 'TK1-P001', packageCount: 1, status: 'RECEIVED' },
{ id: '702', palletNo: 'TK1-P002', packageCount: 1, status: 'RECEIVED' }]
state.truck.receivedPalletCount = 2
}
if (action === 'allocate-pallets') {
if (new Set(body.palletIds).size !== body.palletIds.length) return response(null, 'A0400')
assert.deepEqual(body.palletIds, [state.truck.allocatedPalletCount === 0 ? '701' : '702'])
state.truckPallets.find(row => row.id === body.palletIds[0]).status = 'ALLOCATED'
state.truck.allocatedPalletCount++
}
state.truck.status = to; state.truck.version += 1
state.truckHistories.push({ fromStatus: from, toStatus: to, idempotencyKey: body.idempotencyKey })
state.truckStatuses.set(body.idempotencyKey, true)
return response(truckDetail())
}
if (requestPath.includes('/us-truck-orders/TK1/status?') && method === 'PATCH') {
assert.equal(body.status, 'CANCELLED')
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
const hash = JSON.stringify(body)
const replay = state.truckStatuses.get(body.idempotencyKey)
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
if (!['PENDING_REVIEW', 'WAITING_INBOUND'].includes(state.truck.status)) return response(null, 'A0409')
const from = state.truck.status
state.truck = { ...state.truck, status: 'CANCELLED', version: state.truck.version + 1 }
state.truckHistories.push({ fromStatus: from, toStatus: 'CANCELLED', remark: body.remark })
const result = structuredClone(truckDetail())
state.truckStatuses.set(body.idempotencyKey, { hash, result })
return response(result)
}
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'GET') return response(truckDetail())
if (requestPath.includes('/us-truck-orders/TK1?') && method === 'PATCH') {
if (Number(body.version) !== state.truck.version) return response(null, 'A0409')
state.truck = { ...state.truck, ...body, version: state.truck.version + 1 }
state.truckHistories.push({ fromStatus: state.truck.status, toStatus: state.truck.status, remark: marker })
return response(truckDetail())
}
if (requestPath.includes('/local-looseboard-orders?') && method === 'POST') {
const hash = JSON.stringify(body)
if (state.looseboard) return hash === state.looseboardHash ? response(looseboardDetail()) : response(null, 'A0409')
state.looseboardHash = hash
state.looseboard = { id: '50001', orderNo: 'LB1', customerId: '10001', status: 'PENDING_ACCEPTANCE', version: 0, ...body }
state.looseboardHistories.push({ operationType: 'CREATE', fromStatus: null, toStatus: 'PENDING_ACCEPTANCE', remark: marker })
return response(looseboardDetail())
}
if (requestPath.includes('/local-looseboard-orders?') && method === 'GET') return response(page(state.looseboard ? [state.looseboard] : []))
if (requestPath.includes('/local-looseboard-orders/LB1/status?') && method === 'PATCH') {
assert.equal(typeof body.idempotencyKey === 'string' && body.idempotencyKey.length > 0, true)
const hash = JSON.stringify(body)
const replay = state.looseboardStatuses.get(body.idempotencyKey)
if (replay) return replay.hash === hash ? response(replay.result) : response(null, 'A0409')
if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409')
const allowed = state.looseboard.status === 'PENDING_ACCEPTANCE' && body.status === 'CANCELLED'
if (!allowed) return response(null, 'A0409')
const from = state.looseboard.status
state.looseboard = { ...state.looseboard, status: body.status, version: state.looseboard.version + 1 }
state.looseboardHistories.push({ operationType: 'STATUS', fromStatus: from, toStatus: body.status, remark: body.remark })
const result = structuredClone(looseboardDetail())
state.looseboardStatuses.set(body.idempotencyKey, { hash, result })
return response(result)
}
if (requestPath.includes('/pending-looseboard-demands?') && method === 'GET') return response(page([state.looseboard]))
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'GET') return response(looseboardDetail())
if (requestPath.includes('/local-looseboard-orders/LB1?') && method === 'PUT') {
if (Number(body.version) !== state.looseboard.version) return response(null, 'A0409')
state.looseboard = { ...state.looseboard, ...body, version: state.looseboard.version + 1 }
state.looseboardHistories.push({ operationType: 'UPDATE', fromStatus: state.looseboard.status,
toStatus: state.looseboard.status, changeDetail: JSON.stringify({ marker }) })
return response(looseboardDetail())
}
throw new Error(`unexpected ${method} ${requestPath}`)
} : async function(method,requestPath,body) {
calls.push({ channel: 'domestic', method, path: requestPath, body })
if (requestPath.includes('/iam/tenants?')) return response(page(activeCustomers))
if (requestPath === '/api/auth/login') return response({ accessToken: 'merchant-overseas-token', refreshToken: 'merchant-refresh-token' })
if (requestPath === '/api/auth/logout') { assert.equal(body.refreshToken, 'merchant-refresh-token'); return response(null) }
if (method === 'POST' && requestPath === '/api/platform/v1/warehouse-admin') return response('20001')
if (method === 'PATCH' && requestPath.includes('/warehouse-admin/20001/status?')) {
state.warehouseStatus = 'INACTIVE'; return response(null)
}
throw new Error(`unexpected domestic ${method} ${requestPath}`)
};
return handler.apply(this,args);
},
apiSuccess(module, name, result, evidence, stepOptions) {
const passed = result.http === 200 && String(result.body?.code) === '200'
this.addStep(module, name, result, passed, evidence, stepOptions)
return passed ? result.body.data : null
},
assert(module, name, result, passed, evidence, stepOptions) {
this.addStep(module, name, result, Boolean(passed), evidence, stepOptions)
},
addStep(module, name, result, passed, evidence, stepOptions = {}) {
const step = { module, name, passed: Boolean(passed), result, evidence, options: stepOptions,
requestId: result?.requestId || null, traceId: result?.traceId || null }
steps.push(step)
if (stepOptions.deploymentBlocker && !step.passed) blockers.push(step)
},
addCoverageGap(...args) { gaps.push(args) },
}
return context
}
test('module is context injected, blocks missing prerequisites and excludes forbidden surfaces', () => {
assert.match(source, /export async function run\(context\)/)
assert.doesNotMatch(source, /https?:\/\/|process\.env|fetch\(/)
assert.equal((source.match(/addCoverageGap\(/g) || []).length, 1)
assert.match(source, /looseboard-pool-fulfillment/)
assert.match(source, /deploymentBlocker: true/)
assert.match(source, /contactPhone: '\d+'/)
assert.doesNotMatch(source, /contactPhone: '\+[^']+'/)
for (const forbidden of ['/outbounds', '/receive-pallets', '/outbound', '/delivery', '/dispatch', '/export']) {
assert.equal(source.includes(forbidden), false, forbidden)
}
assert.doesNotMatch(source, /request\('DELETE'/)
})
test('procurement creation review boundaries and transport cancellation paths pass with legal marker terminals', async () => {
const context = fixture()
await run(context)
assert.equal(context.steps.every((step) => step.passed), true,
context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
assert.equal(context.gaps.length, 1)
assert.equal(context.gaps[0][1], 'looseboard-pool-fulfillment')
assert.equal(context.blockers.length, 0)
assert.equal(context.calls.filter(call => call.path === '/api/auth/login').length, 1)
assert.equal(context.calls.filter(call => call.path === '/api/auth/logout').length, 1)
const procurementWrites = context.calls.filter((call) => call.path.includes('/overseas-procurements') && call.method !== 'GET')
assert.deepEqual(procurementWrites.map((call) => call.method),
['POST', 'POST', 'POST', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'PATCH', 'POST', 'POST', 'POST', 'POST'])
assert.equal(procurementWrites.filter((call) => call.method === 'PATCH').length, 5)
assert.ok(procurementWrites.some((call) => call.method === 'PATCH' && call.body.remark === null))
const procurementReviews = procurementWrites.filter((call) => call.path.includes('/review?'))
assert.equal(procurementReviews.length, 4)
assert.ok(procurementReviews.every((call) => Number.isInteger(Number(call.body.version))
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
assert.equal(new Set(procurementReviews.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
assert.notEqual(procurementReviews[3].body.idempotencyKey, procurementReviews[0].body.idempotencyKey)
const truckCalls = context.calls.filter((call) => call.path.includes('/us-truck-orders'))
assert.equal(truckCalls.filter((call) => call.method === 'POST').length, 24)
assert.equal(truckCalls.filter(call => call.body?.idempotencyKey?.endsWith('prepare-dispatch-partial')).length, 1)
assert.equal(truckCalls.filter(call => call.method === 'POST' && call.body?.idempotencyKey?.endsWith('-invalid')).length, 2)
assert.equal(truckCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
const truckReviews = truckCalls.filter((call) => call.method === 'POST' && call.path.includes('/review?'))
assert.equal(truckReviews.length, 4)
assert.ok(truckReviews.every((call) => Number.isInteger(Number(call.body.version))
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
assert.equal(new Set(truckReviews.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
assert.notEqual(truckReviews[3].body.idempotencyKey, truckReviews[0].body.idempotencyKey)
const truckStatuses = truckCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?'))
assert.equal(truckStatuses.length, 0)
assert.ok(truckStatuses.every((call) => call.body.status === 'CANCELLED'
&& typeof call.body.idempotencyKey === 'string' && call.body.idempotencyKey.length > 0))
const looseboardCalls = context.calls.filter((call) => call.path.includes('/local-looseboard-orders'))
assert.equal(looseboardCalls.filter((call) => call.method === 'POST').length, 3)
assert.equal(looseboardCalls.filter((call) => call.method === 'GET' && call.path.includes('page=1')).length, 1)
assert.equal(looseboardCalls.filter((call) => call.method === 'PUT').length, 2)
const looseboardStatuses = looseboardCalls.filter((call) => call.method === 'PATCH' && call.path.includes('/status?'))
assert.equal(looseboardStatuses.length, 4)
assert.ok(looseboardStatuses.every((call) => typeof call.body.idempotencyKey === 'string'
&& call.body.idempotencyKey.length > 0))
assert.equal(new Set(looseboardStatuses.slice(0, 3).map((call) => call.body.idempotencyKey)).size, 1)
assert.equal(new Set(looseboardStatuses.slice(3).map((call) => call.body.idempotencyKey)).size, 1)
assert.ok(looseboardStatuses.slice(3).every((call) => call.body.idempotencyKey !== looseboardStatuses[0].body.idempotencyKey))
const registry = context.createdResources.phase4OverseasBusiness
assert.equal(registry.purchase.finalStatus, 'REJECTED')
assert.equal(registry.purchase.retained, false)
assert.equal(registry.truck.finalStatus, 'COMPLETED')
assert.equal(registry.truck.retained, true)
assert.equal(registry.truck.fulfillment.status, 'HTTP_VERIFIED')
assert.equal(registry.truck.fulfillment.partialAllocationVerified, true)
assert.equal(registry.truck.fulfillment.pallets.length, 2)
assert.equal(registry.looseboard.finalStatus, 'PENDING_ACCEPTANCE')
assert.equal(registry.looseboard.retained, true)
assert.equal(registry.partnerWarehouse.finalStatus, 'INACTIVE')
assert.equal(registry.partnerWarehouse.retained, false)
})
test('old versions and reverse transitions are exact A0409 conflicts with trace evidence', async () => {
const context = fixture()
await run(context)
const conflictSteps = context.steps.filter((step) => /A0409|异载荷|逆向/.test(step.name))
assert.ok(conflictSteps.length >= 7)
assert.ok(conflictSteps.every((step) => step.passed))
assert.ok(conflictSteps.every((step) => step.requestId && step.traceId))
assert.ok(conflictSteps.filter((step) => step.name.includes('旧 version')).every((step) => step.result.body.code === 'A0409'))
assert.ok(context.steps.filter((step) => /流水/.test(step.name)).every((step) => step.passed))
})
test('missing merchant fixture is an explicit deployment blocker and never a coverage gap', async () => {
const context = fixture({ credentials: false })
await run(context)
assert.equal(context.gaps.length, 0)
assert.ok(context.blockers.some((step) => step.name === '采购客户登录前置完整'))
assert.equal(context.calls.some((call) => call.path.includes('/overseas-procurements')), false)
assert.equal(context.calls.some((call) => call.path.includes('/us-truck-orders')), false)
assert.equal(context.calls.some((call) => call.path.includes('/local-looseboard-orders')), false)
})
test('fixture customer must itself be active and cannot borrow another customer identity', async () => {
const context = fixture({ activeCustomers: [{ id: '10002', tenantName: '其他客户', status: 'ACTIVE' }] })
await run(context)
assert.equal(context.gaps.length, 0)
assert.ok(context.blockers.some((step) => step.name === '本轮资格客户属于 ACTIVE 候选'))
assert.equal(context.calls.some((call) => call.path === '/api/auth/login'), false)
assert.equal(context.calls.some((call) => call.path.includes('/overseas-procurements')), false)
})
test('candidate blocker still rejects partial procurement and retires partner warehouse', async () => {
const context = fixture({ warehouseOptions: [] })
await run(context)
assert.equal(context.gaps.length, 0)
assert.ok(context.blockers.some((step) => step.name === '采购仓候选存在 ACTIVE 仓'))
const registry = context.createdResources.phase4OverseasBusiness
assert.equal(registry.purchase.finalStatus, 'REJECTED')
assert.equal(registry.purchase.retained, false)
assert.equal(registry.partnerWarehouse.finalStatus, 'INACTIVE')
assert.equal(registry.partnerWarehouse.retained, false)
assert.equal(context.calls.some((call) => call.path.includes('/us-truck-orders')), false)
assert.equal(context.calls.some((call) => call.path.includes('/local-looseboard-orders')), false)
})
test('contract locks candidates three-state idempotency histories legal terminals and trace evidence', () => {
for (const token of ['/api/platform/v1/iam/tenants?page=1&size=200&status=ACTIVE',
'/overseas-procurements/customer-options?limit=200', '/overseas-procurements/warehouse-options',
'/api/merchant/v1/last-leg/overseas-procurements', '采购备注显式清空', '缺失备注不修改且版本不漂移',
'同幂等键同载荷精确重放', '同幂等键异载荷返回冲突', '旧 version 返回 A0409',
'状态正向推进', '状态逆向回退返回冲突', '流水覆盖', "status: 'CANCELLED'",
'/review', 'truck-review-approve', 'looseboard-status-dispatched',
'requestId=', 'traceId=', 'createdResources.phase4OverseasBusiness']) assert.ok(source.includes(token), token)
})