251 lines
15 KiB
JavaScript
251 lines
15 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 { expandOverseasCartonCandidates, run } from './phase4-overseas-pda.mjs'
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
const source = fs.readFileSync(path.join(here, 'phase4-overseas-pda.mjs'), 'utf8')
|
|
|
|
test('PDA module is injected, token-scoped, traceable, and contains no direct network or secret source', () => {
|
|
assert.match(source, /export async function run\(context\)/)
|
|
assert.match(source, /requestOverseas\(/)
|
|
assert.match(source, /requestOverseasMultipart\(/)
|
|
assert.match(source, /requestId=/)
|
|
assert.match(source, /traceId=/)
|
|
assert.doesNotMatch(source, /https?:\/\//)
|
|
assert.doesNotMatch(source, /fetch\(|process\.env/)
|
|
assert.doesNotMatch(source, /planNo|allocationId/)
|
|
})
|
|
|
|
test('PDA module covers the container-number receiving contract and excludes outbound domains', () => {
|
|
for (const token of ['/candidates', '/prepare', '/current/unload-photos/files', '/current/sign',
|
|
'/current/cartons/resolve', '/current/pallets/resolve',
|
|
'/current/receipts/bulk', '/current/receipts/carton', '/current/complete']) assert.ok(source.includes(token), token)
|
|
for (const token of ['/inventory/resolve', '/locations/resolve', '/pallets/resolve', '/operations?page=1&size=50']) {
|
|
assert.ok(source.includes(token), token)
|
|
}
|
|
for (const role of ['LICENSE_PLATE', 'CONTAINER_SEAL', 'DOOR_OPEN']) assert.ok(source.includes(role), role)
|
|
for (const forbidden of ['/outbound', '/dispatch', '/delivery', '/internal/']) assert.equal(source.includes(forbidden), false, forbidden)
|
|
assert.match(source, /同键同载荷重放/)
|
|
assert.match(source, /同键异载荷冲突/)
|
|
assert.match(source, /跨柜切换锁返回409/)
|
|
assert.match(source, /item\.operable === true && item\.allPlansReady === true/)
|
|
assert.match(source, /第二仓库操作员抢占同柜返回409/)
|
|
assert.match(source, /OVERSEAS_WAREHOUSE_PDA_OPERATOR/)
|
|
assert.match(source, /lastleg:pda:operate/)
|
|
assert.match(source, /安全投影已收敛/)
|
|
assert.match(source, /projectionReady = success\(projectionResponse\) \|\| notFound\(projectionResponse\)/)
|
|
assert.match(source, /setTimeout\(resolve, 500\)/)
|
|
assert.match(source, /超收被拒绝/)
|
|
assert.match(source, /重复箱新键被拒绝/)
|
|
assert.match(source, /key\(context, `fba-bulk-\$\{index\}`\)/)
|
|
assert.match(source, /packageCount = Number\(inbound\.pendingPackages \|\| 0\)/)
|
|
assert.match(source, /if \(packageCount <= 0\) return true/)
|
|
assert.match(source, /key\(context, `overseas-carton-\$\{index\}`\)/)
|
|
assert.match(source, /模式硬拒绝/)
|
|
assert.match(source, /current返回404正常空态/)
|
|
assert.match(source, /安全扫描返回A0400/)
|
|
assert.match(source, /warehouse-admin\?tenantId=0&.*capabilityCode=RECEIVING&warehouseType=OVERSEAS&status=ACTIVE/)
|
|
assert.match(source, /193000000000200003.*193000000000200004/)
|
|
assert.doesNotMatch(source, /requestOverseas\('POST', '\/api\/platform\/v1\/warehouse-admin'/)
|
|
assert.doesNotMatch(source, /requestOverseas\('GET', '\/api\/platform\/v1\/warehouse-admin'/)
|
|
assert.match(source, /receiving-containers\/current\/pallets\/resolve/)
|
|
assert.match(source, /overseasWarehouseId, password/)
|
|
assert.match(source, /inbound\?\.overseasWarehouseId/)
|
|
assert.doesNotMatch(source, /const warehouseId = id\(inbound\?\.destinationWarehouseId\)/)
|
|
assert.match(source, /目的仓 .* 不得代替物理仓/)
|
|
assert.match(source, /retained as acceptance evidence/)
|
|
assert.match(source, /preferredProducer\?\.cartonMarks\?\.length/)
|
|
assert.match(source, /const cartonCandidates =/)
|
|
assert.doesNotMatch(source, /current\/skus\/resolve/)
|
|
assert.doesNotMatch(source, /skuCode:/)
|
|
assert.match(source, /FBA箱码与OVERSEAS真实manifest箱码必须HTTP 200\/code 200并返回当前柜唯一入仓码/)
|
|
assert.match(source, /排除未实装箱码候选/)
|
|
assert.match(source, /const matches = inbounds\.filter/)
|
|
assert.match(source, /!Array\.isArray\(resolved\?\.inboundCode\)/)
|
|
assert.match(source, /matches\.length === 1/)
|
|
assert.match(source, /destinations\.size === 1/)
|
|
assert.doesNotMatch(source, /\.split\([^\n]*['"]#['"]/)
|
|
assert.match(source, /`\$\{item\.inboundCode\}#PDA-CONTRACT-\$\{index \+ 1\}`/)
|
|
for (const negative of ['无#箱码被服务端拒绝', '空#前缀箱码被服务端拒绝',
|
|
'未命中入仓码前缀被服务端拒绝', '其他柜箱码前缀被当前锁定柜拒绝',
|
|
'OVERSEAS缺少真实manifest行时不得按标准箱码推导']) {
|
|
assert.ok(source.includes(negative), negative)
|
|
}
|
|
assert.doesNotMatch(source, /FBA标准箱码序号超范围被拒绝/)
|
|
assert.match(source, /FBA与OVERSEAS箱码resolve均为纯查询且零状态写入/)
|
|
assert.match(source, /receivingSnapshot\(afterResolve\.data\) === beforeResolve/)
|
|
assert.match(source, /const fixturesConserved = overseasFixtures\.length === expectedPending/)
|
|
assert.match(source, /OVERSEAS实装箱唛与待收箱数守恒/)
|
|
assert.match(source, /for \(const \[index, fixture\] of overseasFixtures\.entries\(\)\)/)
|
|
assert.doesNotMatch(source, /for \(const \[index, cartonMark\] of cartonMarks\.entries\(\)\)/)
|
|
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
|
|
assert.match(source, /phase4OverseasPda\.orderId = id\(order\.id\)/)
|
|
assert.match(source, /phase4OverseasPda\.customerId = id\(order\.customerId\)/)
|
|
assert.match(source, /现场库存查询只返回Token绑定仓且包含库存位置/)
|
|
assert.match(source, /作业历史包含本轮柜号收货动作且分页稳定/)
|
|
assert.match(source, /awaitProducerConsumption\(context, allProducers\)/)
|
|
assert.match(source, /last-leg\/orders\?page=1&size=200&containerNo=/)
|
|
assert.match(source, /跨区广播消费观察/)
|
|
assert.match(source, /跨区广播消费观察 \$\{containerNo\}`,[\s\S]*?response,[\s\S]*?true,/)
|
|
assert.match(source, /OBSERVATION_OPTIONS = \{ scenario: SCENARIO, deploymentBlocker: false \}/)
|
|
assert.doesNotMatch(source, /blockerGap\(context, '跨区消费生产矩阵'/)
|
|
assert.match(source, /awaitPickupReadyCandidates\(context, primary\.token, producers, allProducers\)/)
|
|
assert.match(source, /missingPickupReady=/)
|
|
assert.match(source, /item\?\.shippingReady === false/)
|
|
assert.match(source, /!expectedSet\.has\(id\(item\?\.containerNo\)\)/)
|
|
assert.match(source, /unexpectedAwaiting=/)
|
|
assert.match(source, /candidatesByContainer\.get\(containerNo\)\?\.operable === true/)
|
|
assert.match(source, /candidatesByContainer\.get\(containerNo\)\?\.operable !== true/)
|
|
assert.match(source, /missingPickupReady\.length === 0 && unexpectedAwaiting\.length === 0/)
|
|
assert.doesNotMatch(source, /shippingReady == null|shippingReady !== true/)
|
|
assert.match(source, /nonBlank\(item\?\.resourceKey\)/)
|
|
assert.match(source, /item\?\.shippingReady !== false/)
|
|
assert.match(source, /unexpectedHistorical=/)
|
|
assert.doesNotMatch(source, /awaitProducerCandidates/)
|
|
})
|
|
|
|
test('OVERSEAS carton discovery probes the frozen sequence and lets the server exclude skipped cartons', () => {
|
|
assert.deepEqual(expandOverseasCartonCandidates([
|
|
{ inboundCode: 'NYEXX2609020042', destinationType: 'OVERSEAS' },
|
|
{ inboundCode: 'FBA2609020001', destinationType: 'FBA' },
|
|
], ['NYEXX2609020042#8-5', 'NYEXX2609020042#8-2', 'NYEXX2609020042#8-1']), [
|
|
'NYEXX2609020042#8-1', 'NYEXX2609020042#8-2', 'NYEXX2609020042#8-3', 'NYEXX2609020042#8-4',
|
|
'NYEXX2609020042#8-5', 'NYEXX2609020042#8-6', 'NYEXX2609020042#8-7', 'NYEXX2609020042#8-8',
|
|
])
|
|
assert.deepEqual(expandOverseasCartonCandidates([
|
|
{ inboundCode: 'NYEXX2609020042', destinationType: 'OVERSEAS' },
|
|
], ['NYEXX2609020042#2001-1']), [])
|
|
})
|
|
|
|
test('real-device legacy FBA carton rejection is retained as regression evidence', () => {
|
|
const evidence = {
|
|
http: 200,
|
|
code: 'A0409',
|
|
message: 'FBA destination must use bulk count',
|
|
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
|
|
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
|
|
}
|
|
assert.deepEqual(evidence, {
|
|
http: 200,
|
|
code: 'A0409',
|
|
message: 'FBA destination must use bulk count',
|
|
requestId: '5e925a3d-8184-41e1-b0d2-f4e1aae86639',
|
|
traceId: '62fdf603b65fd7f43ec452bb8a6ca185',
|
|
})
|
|
assert.match(source, /a0409Message\(response, \/\^FBA destination must use bulk count\$\//)
|
|
assert.match(source, /a0409Message\(response, \/\^OVERSEAS destination must use carton-by-carton scanning; bulk count is forbidden\$\//)
|
|
})
|
|
|
|
test('real-device deployed FBA resolve A0404 is retained until server-side inbound-prefix matching lands', () => {
|
|
const evidence = {
|
|
http: 200,
|
|
code: 'A0404',
|
|
message: 'Carton mark was not found',
|
|
requestId: 'a716e2c2-dc10-4d07-8014-7438afb63090',
|
|
traceId: '6ab8fcfcd812056ad6570d3b1652157b',
|
|
databaseFact: 'current FBA has no ll_inbound_plan_carton manifest',
|
|
}
|
|
assert.equal(evidence.code, 'A0404')
|
|
assert.equal(evidence.message, 'Carton mark was not found')
|
|
assert.match(evidence.databaseFact, /no ll_inbound_plan_carton manifest/)
|
|
assert.match(source, /a0404Message\(response, \/\^Carton mark was not found\$\//)
|
|
})
|
|
|
|
test('PDA receiving lifecycle is ordered from candidate claim through photos, mode-specific receipts, pallet and complete', () => {
|
|
const orderedTokens = [
|
|
"'查询当前客户待收柜号'", "'选择柜号并建立WAIT_SIGN会话'",
|
|
'uploadPhotos(context, token)', "'绑定车牌封签开柜三角色照片并签收'",
|
|
"`解析${inbound.destinationType}实物托盘`", 'receiveFba(context, token',
|
|
'receiveOverseas(context, token', "'回查柜内逐入仓码实收进度'",
|
|
"'完成当前柜并写入库存与卸柜事件'", "'完成后current返回404正常空态'",
|
|
]
|
|
let cursor = -1
|
|
for (const token of orderedTokens) {
|
|
const next = source.indexOf(token, cursor + 1)
|
|
assert.ok(next > cursor, `${orderedTokens[Math.max(0, orderedTokens.indexOf(token) - 1)]} -> ${token}`)
|
|
cursor = next
|
|
}
|
|
for (const token of [
|
|
'/current/receipts/bulk', '/current/receipts/carton',
|
|
"'FBA模式硬拒绝逐箱收货'", "'OVERSEAS模式硬拒绝批量收货'",
|
|
'PHOTO_ROLES.every', "finalStatus === 'COMPLETED'", 'completedContainers.push',
|
|
]) assert.ok(source.includes(token), token)
|
|
})
|
|
|
|
test('missing OVERSEAS_WAREHOUSE_PDA_OPERATOR overseas authority is a deployment blocker', async () => {
|
|
const calls = []; const gaps = []; const steps = []
|
|
const context = {
|
|
marker: 'PDA-CONTRACT', context: {}, createdResources: {},
|
|
async request(method, requestPath) {
|
|
calls.push({ method, requestPath })
|
|
return { method, path: requestPath, http: 200, body: { code: '200', data: [] },
|
|
requestId: 'request-contract-1', traceId: 'trace-contract-0001' }
|
|
},
|
|
async requestOverseas(...args) { calls.push(args); throw new Error('must not request without credentials') },
|
|
async requestOverseasMultipart(...args) { calls.push(args); throw new Error('must not request without credentials') },
|
|
apiSuccess(module, name, response) { return response.body.data },
|
|
addStep(...args) { steps.push(args) },
|
|
assert(module, name, response, passed, evidence, options) { steps.push({ module, name, passed, evidence, options }) },
|
|
addCoverageGap(...args) { gaps.push(args) },
|
|
}
|
|
await run(context)
|
|
assert.ok(calls.some((call) => call.requestPath === '/api/platform/v1/iam/roles'))
|
|
assert.ok(calls.some((call) => call.requestPath === '/api/platform/v1/iam/permissions'))
|
|
assert.ok(gaps.some((gap) => gap[2] === '海外PDA仓库操作员权限'))
|
|
assert.ok(steps.some((step) => step?.passed === false && step?.options?.deploymentBlocker))
|
|
})
|
|
|
|
test('empty candidate response cannot become a zero-business-request pass', async () => {
|
|
const calls = []; const gaps = []; const steps = []
|
|
const response = (data, requestPath, method = 'GET') => ({ method, path: requestPath, http: 200,
|
|
body: { code: '200', data }, requestId: 'req-contract', traceId: 'trace-contract' })
|
|
let userSequence = 0
|
|
const roleId = '4000005'; const permissionId = '5107015'
|
|
const context = {
|
|
marker: 'PDA-CONTRACT', overseasToken: '', createdResources: {}, context: {},
|
|
async findAvailablePhone() { userSequence += 1; return `1750000000${userSequence}` },
|
|
async request(method, requestPath, body) {
|
|
calls.push({ channel: 'domestic', method, requestPath })
|
|
if (method === 'GET' && requestPath.endsWith('/roles')) return response([
|
|
{ id: roleId, roleCode: 'OVERSEAS_WAREHOUSE_PDA_OPERATOR', status: 'ACTIVE', permissionIds: [permissionId] },
|
|
], requestPath, method)
|
|
if (method === 'GET' && requestPath.endsWith('/permissions')) return response([
|
|
{ id: permissionId, permissionCode: 'lastleg:pda:operate' },
|
|
], requestPath, method)
|
|
if (method === 'GET' && requestPath.startsWith('/api/platform/v1/warehouse-admin?')) return response({
|
|
records: [{ id: '193000000000200003', warehouseName: '洛杉矶仓', warehouseType: 'OVERSEAS', status: 'ACTIVE' }],
|
|
page: 1, size: 200, total: 1, pages: 1,
|
|
}, requestPath, method)
|
|
if (method === 'POST' && requestPath.endsWith('/users')) return response({
|
|
membershipId: String(9000 + userSequence), username: body.username, tenantId: '0', roleIds: [roleId],
|
|
overseasWarehouseId: String(body.overseasWarehouseId), status: 'ACTIVE',
|
|
}, requestPath, method)
|
|
if (method === 'POST' && requestPath === '/api/auth/login') return response({ overseasAccessToken: `pda-token-${userSequence}` }, requestPath, method)
|
|
if (method === 'PUT' && /\/users\/\d+\/status$/.test(requestPath)) return response({ status: 'DISABLED' }, requestPath, method)
|
|
throw new Error(`unexpected domestic request ${method} ${requestPath}`)
|
|
},
|
|
async requestOverseas(method, requestPath, body, headers, token) {
|
|
calls.push({ channel: 'overseas', method, requestPath })
|
|
if (requestPath.endsWith('/current')) return { method, path: requestPath, http: 200,
|
|
body: { code: 'A0404', message: 'not found' }, requestId: 'req-current', traceId: 'trace-current' }
|
|
return response([], requestPath, method)
|
|
},
|
|
apiSuccess(module, name, result) { return result.body.data },
|
|
addStep(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed, evidence, options }) },
|
|
assert(module, name, result, passed, evidence, options) { steps.push({ module, name, result, passed, evidence, options }) },
|
|
addCoverageGap(...args) { gaps.push(args) },
|
|
}
|
|
await run(context)
|
|
assert.ok(calls.some((call) => call.requestPath.endsWith('/candidates')))
|
|
assert.ok(steps.some((step) => step.name.includes('正常空态') && step.passed))
|
|
assert.ok(steps.some((step) => step.name.includes('PDA柜号候选覆盖前置') && !step.passed && step.options.deploymentBlocker))
|
|
assert.equal(gaps.length, 1)
|
|
assert.equal(context.createdResources.phase4OverseasPda.finalStatus, null)
|
|
assert.equal(context.createdResources.phase4OverseasPda.pickerMembershipIds.length, 2)
|
|
assert.equal(calls.filter((call) => /\/users\/\d+\/status$/.test(call.requestPath)).length, 2)
|
|
})
|