112 lines
8.8 KiB
JavaScript
112 lines
8.8 KiB
JavaScript
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 { run } from './phase4-overseas-warehouse.mjs'
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
const source = fs.readFileSync(path.join(here, 'phase4-overseas-warehouse.mjs'), 'utf8')
|
|
|
|
function response(data) { return { http: 200, body: { code: '200', message: 'success', data }, requestId: 'request-test', traceId: 'trace-test' } }
|
|
function page(records = []) { return { records, page: 1, size: 20, total: records.length, pages: records.length ? 1 : 0 } }
|
|
function contextFixture({ tenants = [{ id: '10001', status: 'ACTIVE' }], warehouseRecords = [{ id: '20001', status: 'ACTIVE', warehouseCode: 'LAX', warehouseName: '洛杉矶仓' }] } = {}) {
|
|
const calls = []; const steps = []; const gaps = []; let createdPallet = null
|
|
const context = {
|
|
context: {}, createdResources: {}, marker: 'ACC-PHASE4-TEST', calls, steps, gaps,
|
|
async request(method, requestPath) { calls.push({ channel: 'domestic', method, path: requestPath }); return response(page(tenants)) },
|
|
async requestOverseas(method, requestPath, body) {
|
|
calls.push({ channel: 'overseas', method, path: requestPath, body })
|
|
if (method === 'POST' && requestPath.endsWith('/warehouse-admin/20001/pallets')) { createdPallet = { id: '71001', customerId: '0', warehouseId: '20001', locationId: '72001', ...body }; return response('71001') }
|
|
if (method === 'PUT' && requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) { createdPallet = { ...createdPallet, ...body }; return response(null) }
|
|
if (requestPath.startsWith('/api/platform/v1/last-leg/orders?')) return response(page([{ id: '30001', orderNo: 'LL-1', currentStatus: 'PENDING_ASSIGNMENT' }]))
|
|
if (requestPath.includes('/last-leg/orders/30001?')) return response({ order: { id: '30001' }, cargo: [], statusHistory: [] })
|
|
if (requestPath.startsWith('/api/platform/v1/last-leg/relabel-applications?')) return response(page([{ id: '40001', relabelNo: 'RL-1', status: 'PENDING_PROCESSING' }]))
|
|
if (requestPath.includes('/relabel-applications/RL-1?')) return response({ application: { relabelNo: 'RL-1' }, items: [], history: [] })
|
|
if (requestPath.startsWith('/api/platform/v1/last-leg/inbound-plans?')) return response(page([{ id: '50001', sourceOrderId: '30001', planNo: 'IP-1', customerId: '10001', status: 'PENDING_ASSIGNMENT' }]))
|
|
if (requestPath.includes('/inbound-plans/IP-1?')) return response({ planNo: 'IP-1', sourceOrderId: '30001', lines: [], pallets: [], allocations: [] })
|
|
if (requestPath.includes('/inventory/summary?')) return response({ inboundOrderCount: 1, palletCount: 1, totalPackages: 10, availablePackages: 10, holdPackages: 0, inTransitPackages: 0, receivingPackages: 0, procurementPackages: 0 })
|
|
if (requestPath.includes('/inventory/balances?')) return response(page([{ id: '60001', customerId: '10001', skuCode: 'SKU-1', totalPackages: 10, availablePackages: 10, updatedAt: '2026-08-21T10:00:00' }]))
|
|
if (requestPath.includes('/inventory/balances/60001/positions?')) return response([{ id: '61001', balanceId: '60001', totalPackages: 10 }])
|
|
if (requestPath.includes('/inventory/ledger?')) return response(page([{ id: '62001', customerId: '10001', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-21T10:00:00' }]))
|
|
if (requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response(page(warehouseRecords))
|
|
if (requestPath.includes('/warehouse-admin/20001?')) return response({ warehouse: { id: '20001' } })
|
|
if (requestPath.endsWith('/warehouse-admin/20001/pallets?page=1&size=20')) return response(page([{ id: '70001', customerId: '0', warehouseId: '20001', palletCode: 'PLT-1', status: 'IDLE' }]))
|
|
if (requestPath.endsWith('/warehouse-admin/20001/pallets/70001')) return response({ id: '70001', customerId: '0', warehouseId: '20001' })
|
|
if (requestPath.endsWith('/warehouse-admin/20001/pallets/71001')) return response(createdPallet)
|
|
throw new Error(`Unexpected path: ${requestPath}`)
|
|
},
|
|
async requestOverseasBinary(method, requestPath) { calls.push({ channel: 'overseas-binary', method, path: requestPath }); return { http: 200, bytes: Buffer.from('id,status\n'), contentType: 'text/csv', requestId: 'request-binary', traceId: 'trace-binary' } },
|
|
apiSuccess(_module, _name, result) { return result?.http === 200 && String(result?.body?.code) === '200' ? result.body.data : null },
|
|
assert(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed: Boolean(passed), evidence, options }) },
|
|
addStep(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed: Boolean(passed), evidence, options }) },
|
|
addCoverageGap(...args) { gaps.push(args) },
|
|
}
|
|
return context
|
|
}
|
|
|
|
test('模块只依赖注入请求,不读取环境或自行联网', () => {
|
|
assert.match(source, /export async function run\(context\)/)
|
|
assert.match(source, /context\.requestOverseas\('GET'/)
|
|
assert.match(source, /context\.requestOverseasBinary\('GET'/)
|
|
assert.doesNotMatch(source, /https?:\/\//)
|
|
assert.doesNotMatch(source, /process\.env|fetch\(|requestBinary/)
|
|
})
|
|
|
|
test('仅托盘生命周期允许写请求,其他海外业务保持只读', () => {
|
|
assert.doesNotMatch(source, /requestOverseas\(['"]PATCH['"]/)
|
|
const context = contextFixture()
|
|
return run(context).then(() => {
|
|
assert.ok(context.calls.length >= 12)
|
|
assert.ok(context.calls.every((call) => ['GET', 'POST', 'PUT'].includes(call.method)))
|
|
assert.equal(context.calls.filter((call) => call.channel === 'domestic').length, 1)
|
|
assert.ok(context.calls.filter((call) => call.channel === 'overseas').every((call) => call.path.startsWith('/api/platform/v1/')))
|
|
const writes = context.calls.filter((call) => call.method !== 'GET')
|
|
assert.deepEqual(writes.map((call) => call.method), ['POST', 'PUT', 'PUT'])
|
|
assert.ok(writes.every((call) => call.path.includes('/warehouse-admin/20001/pallets')))
|
|
assert.ok(context.steps.every((step) => step.passed), context.steps.filter((step) => !step.passed).map((step) => `${step.name}: ${step.evidence}`).join('\n'))
|
|
})
|
|
})
|
|
|
|
test('覆盖冻结的海外只读列表与有记录才查详情', async () => {
|
|
const context = contextFixture()
|
|
await run(context)
|
|
for (const pathToken of ['/last-leg/orders?', '/relabel-applications?', '/inbound-plans?', '/inventory/summary?', '/inventory/balances?', '/inventory/ledger?', '/warehouse-admin?', '/pallets?']) {
|
|
assert.ok(context.calls.some((call) => call.channel === 'overseas' && call.path.includes(pathToken)), pathToken)
|
|
}
|
|
for (const detailToken of ['/orders/30001?', '/relabel-applications/RL-1?', '/inbound-plans/IP-1?', '/balances/60001/positions?', '/warehouse-admin/20001?', '/pallets/70001']) {
|
|
assert.ok(context.calls.some((call) => call.path.includes(detailToken)), detailToken)
|
|
}
|
|
assert.deepEqual(context.context.phase4OverseasWarehouse, { customerId: '10001', warehouseId: '20001', palletId: '71001' })
|
|
assert.deepEqual(context.createdResources.phase4OverseasWarehouse, { marker: 'ACC-PHASE4-TEST', customerId: '10001', warehouseId: '20001', palletId: '71001', terminalStatus: 'SCRAPPED' })
|
|
})
|
|
|
|
test('缺少真实客户或仓库时记录 coverageGap,不使用固定业务ID补洞', async () => {
|
|
const noCustomer = contextFixture({ tenants: [] })
|
|
await run(noCustomer)
|
|
assert.equal(noCustomer.gaps.length, 1)
|
|
assert.match(noCustomer.gaps[0][3], /requestId=request-test.*traceId=trace-test/)
|
|
assert.equal(noCustomer.calls.filter((call) => call.channel === 'overseas').length, 0)
|
|
|
|
const noWarehouse = contextFixture({ warehouseRecords: [] })
|
|
await run(noWarehouse)
|
|
assert.equal(noWarehouse.gaps.some((gap) => gap[2] === '公共托盘真实仓库前置'), true)
|
|
assert.equal(noWarehouse.calls.some((call) => call.path.includes('/pallets')), false)
|
|
})
|
|
|
|
test('合同覆盖库存CSV并明确排除暂缓、离仓、PDA与内部路径', () => {
|
|
assert.match(source, /inventory\/balances\/export/)
|
|
assert.match(source, /inventory\/ledger\/export/)
|
|
assert.match(source, /inventory\/summary[^\n]+customerId, page: 1, size: 200/)
|
|
assert.match(source, /inventory\/balances\/export[^\n]+customerId, page: 1, size: 200/)
|
|
assert.match(source, /inventory\/ledger\/export[^\n]+customerId, page: 1, size: 200/)
|
|
for (const forbidden of ['/cartons', '/products', '/outbound', '/dispatch', '/delivery', '/pda', '/internal']) assert.equal(source.includes(forbidden), false, forbidden)
|
|
assert.match(source, /requestId=/)
|
|
assert.match(source, /traceId=/)
|
|
assert.match(source, /longId\(plan\.sourceOrderId\)/)
|
|
assert.match(source, /id\(pallet\.customerId\) === '0'/)
|
|
assert.match(source, /换标创建与状态生命周期/)
|
|
assert.match(source, /旧版本并发冲突/)
|
|
})
|