merge: sync test and stabilize full acceptance

This commit is contained in:
2026-09-09 21:14:25 +08:00
40 changed files with 595 additions and 134 deletions
@@ -32,6 +32,7 @@ test('initial fee preparation is reusable without confirming or publishing the f
assert.match(helper, /\/fees\/latest/)
assert.match(helper, /\/fees\/calculate/)
assert.match(helper, /feeStatus === 'CALCULATED'/)
assert.match(helper, /\$\{inboundId\}-inbound-fee-initial-calculate/)
assert.doesNotMatch(helper, /\/fees\/confirm|\/fees\/send/)
})
@@ -46,6 +47,9 @@ test('automatic fee adjustment confirms the ten percent tolerance boundary', asy
test('customer confirms chargeable quantity and manual unit price in one locked version', async () => {
const source = await readFile(moduleFile, 'utf8')
assert.match(source, /const orderId = id\(fixture\?\.orderId\)/)
assert.match(source, /context\.context\?\.merchantQualificationFixture/)
assert.match(source, /merchantFixture\?\.username/)
assert.match(source, /merchantFixture\?\.password/)
assert.doesNotMatch(source, /const orderId = id\(context\.createdResources\.orders\?\.orderId\)/)
assert.match(source, /Client 读取已推送计费量、对客单价和费用合计/)
assert.match(source, /Client 一次确认计费量、对客单价和费用/)
@@ -39,7 +39,7 @@ export async function ensureInitialCalculated(context, fixture,
if (!success(initialResponse) && String(initialResponse?.body?.code) === 'A0404') {
initialResponse = await context.request('POST',
`/api/platform/v1/inbounds/${encodeURIComponent(inboundId)}/fees/calculate?tenantId=${encodeURIComponent(tenantId)}`,
undefined, undefined, requestHeaders(`${context.marker}-inbound-fee-initial-calculate`))
undefined, undefined, requestHeaders(`${context.marker}-${inboundId}-inbound-fee-initial-calculate`))
}
const initial = context.apiSuccess(MODULE, '读取或补齐审核后初始费用', initialResponse, '', options)
context.assert(MODULE, '初始费用保持 WAIT_SPLIT 且尚未发布', initialResponse,
@@ -132,8 +132,9 @@ export async function run(context) {
`status=${sent?.feeStatus || 'missing'}, process=${sent?.nextProcessStatus || 'missing'}, publication=${published?.publicationStatus || 'missing'}`, OPTIONS)
const orderId = id(fixture?.orderId)
const merchantFixture = context.context?.merchantQualificationFixture
const merchantLoginResponse = await context.request('POST', '/api/auth/login', {
loginId: context.options.merchantUsername, password: context.options.merchantPassword,
loginId: merchantFixture?.username, password: merchantFixture?.password,
}, '')
const merchantLogin = context.apiSuccess(MODULE, 'Client 登录并准备确认计费量和费用', merchantLoginResponse,
`orderId=${orderId || 'missing'}`, OPTIONS)
@@ -19,6 +19,11 @@ function operationId(context, label) {
return `${context.marker}-manual-loading-${label}-${crypto.randomUUID()}`.slice(0, 128)
}
async function post(context, path, body, label) {
return context.request('POST', path, body, context.token,
{ 'Idempotency-Key': operationId(context, label) })
}
async function ensureCompletionEvidence(context, chain, label) {
const taskId = id(chain.task?.id)
const warehouseId = id(chain.source?.warehouseId)
@@ -47,7 +47,7 @@ function context(resources = {}) {
if (token === this.overseasToken) return response(path, null, 403, 'A0403')
if (path.includes('/overview')) return response(path, { container: {
id: '3001', containerNo: 'MSCU1234567', unloadingCompletedAt: '2026-08-24T10:00:00Z',
unloadingPhotoUploadedAt: '2026-08-24T09:00:00Z', estimatedEmptyReportedAt: '2026-08-26T09:00:00Z',
unloadingPhotoUploadedAt: '2026-08-24T09:00:00Z', estimatedEmptyReportedAt: '2026-08-27T10:00:00Z',
actualEmptyReportedAt: null, emptyReportSourceVersion: 7,
} })
return response(path, { records: [{ container: { id: '3001', containerNo: 'MSCU1234567' } }] })
@@ -103,6 +103,13 @@ test('cross-region producers do not repeat the destructive A0 snapshot mutation'
assert.match(fixtureCall, /preparePhase3LoadingFixture\('', false\)/)
})
test('tail controls receive their own completed producer matrix before they consume it', async () => {
const entry = await import('node:fs/promises').then((fs) => fs.readFile(
new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8'))
assert.match(entry, /module === 'phase4-tail-order-controls' \|\| module === 'phase4-overseas-pda'/)
assert.match(entry, /await this\.ensurePhase4PdaProducerMatrix\(\)/)
})
test('contract covers dual audience, canonical 403, both event directions and diagnostics', () => {
for (const token of [
'context.token)', 'context.overseasToken)', 'cn-overseas-event-not-delivered',
@@ -126,7 +126,7 @@ export async function run(context) {
const estimatedAt = Date.parse(snapshot.estimatedEmptyReportedAt)
const validTimeline = Number.isFinite(completedAt) && Number.isFinite(uploadedAt)
&& Number.isFinite(estimatedAt) && completedAt >= uploadedAt
&& estimatedAt - uploadedAt === 2 * 24 * 60 * 60 * 1_000
&& estimatedAt - completedAt === 72 * 60 * 60 * 1_000
context.assert(MODULE, '海外柜完成经Bridge回写Firstleg时间事实', writtenBack.response,
validTimeline && traced(writtenBack.response),
evidence(writtenBack.response,
@@ -33,6 +33,11 @@ test('relabel transfer and discard cover create replay conflict list detail and
assert.match(source, /chargeAmount: 0\.01/)
assert.match(source, /cartonMark:/)
assert.match(source, /fbaCartonMark:/)
for (const field of [
'operationWarehouseId:', 'sourceInventoryBalanceId:', 'sourceInventoryPositionId:', 'sourceInboundOrderId:',
'destinationFbaWarehouseCode:',
]) assert.match(source, new RegExp(field))
assert.match(source, /query\(`\$\{RELABEL\}\/inventory-candidates`, \{/)
for (const token of [
'客户创建贴标申请', '客户贴标申请幂等重放', '贴标同幂等键异载荷返回A0409',
'客户查询自身贴标申请列表', '客户查询自身贴标申请详情', 'PENDING_PROCESSING',
@@ -89,13 +89,30 @@ async function verifyRelabel(context, token, order) {
`orderId=${id(order.row?.id) || 'missing'}, sku=${cargo?.skuCode || 'missing'}, warehouse=${order.row?.overseasWarehouseId || 'missing'}`)
return false
}
const inventory = await merchant(context, token, 'GET', '客户查询贴标权威库存候选',
query(`${RELABEL}/inventory-candidates`, {
operationWarehouseId: order.row.overseasWarehouseId,
inboundCode: order.row.inboundCode,
}))
const source = (Array.isArray(inventory.data) ? inventory.data : []).find((item) =>
id(item?.inventoryBalanceId) && id(item?.inboundOrderId)
&& item?.skuCode === cargo.skuCode
&& item?.positions?.some?.((position) => id(position?.positionId) && Number(position?.availablePackages) > 0))
const position = source?.positions?.find?.((item) => id(item?.positionId) && Number(item?.availablePackages) > 0)
context.assert(MODULE, '贴标申请使用当前客户真实库存余额、位置和来源入库单', inventory.response,
Boolean(source && position),
`balanceId=${id(source?.inventoryBalanceId) || 'missing'}, positionId=${id(position?.positionId) || 'missing'}, inboundOrderId=${id(source?.inboundOrderId) || 'missing'}`, OPTIONS)
if (!source || !position) return false
const body = {
customerOrderNo: order.row.orderNo,
overseasWarehouseId: order.row.overseasWarehouseId,
operationWarehouseId: order.row.overseasWarehouseId,
labelDeadline: future(7), contactName: '客户贴标验收联系人', contactPhone: '13800000006',
chargeAmount: 0.01,
remark: context.marker, idempotencyKey: `${context.marker}-CLIENT-RELABEL`.slice(0, 255),
items: [{
sourceInventoryBalanceId: id(source.inventoryBalanceId),
sourceInventoryPositionId: id(position.positionId),
sourceInboundOrderId: id(source.inboundOrderId),
skuCode: cargo.skuCode, productImageFileId: null, productImageFileName: null, productImageUrl: null,
productNameZh: cargo.productNameZh, totalPackageCount: 1, notifiedPackageCount: 1,
shipmentNo: cargo.shipmentNo || `${context.marker}-RELABEL-SHIP`, trackingCode: cargo.fbaTrackingNo || null,
@@ -103,6 +120,7 @@ async function verifyRelabel(context, token, order) {
relabelFileId: null, relabelFileName: null, relabelFileUrl: null,
cartonMark: `CM-${String(context.marker).slice(-32)}`,
fbaCartonMark: `FBA-${String(context.marker).slice(-32)}`,
destinationFbaWarehouseCode: source.destinationFbaWarehouseCode,
}],
}
const created = await merchant(context, token, 'POST', '客户创建贴标申请', RELABEL, body)
@@ -36,6 +36,7 @@ test('module covers the public tail-order write surface before PDA receiving', (
assert.match(source, /feeItemCode/)
assert.match(source, /feeItemName/)
assert.match(source, /direction: 'RECEIVABLE'/)
assert.match(source, /allIds\.length === 1 \|\| nonBlank\(groupRead\.data\?\.order\?\.palletMergeBatchId\)/)
assert.match(source, /businessDate:/)
assert.match(source, /originalCurrency === 'USD'/)
assert.match(source, /currency === 'CNY'/)
@@ -183,7 +183,9 @@ async function verifyDestinationMergeFeeAndExport(context, details, producer) {
const groupRead = await detail(context, representative, '回查批量合托关系')
context.assert(MODULE, '批量合托关系包含本轮订单', groupRead.response,
groupRead.data?.order?.palletMergeFlag === 'REQUIRED'
&& nonBlank(groupRead.data?.order?.palletMergeBatchId), trace(groupRead.response), OPTIONS)
&& (allIds.length === 1 || nonBlank(groupRead.data?.order?.palletMergeBatchId)),
`orders=${allIds.length}, batchId=${groupRead.data?.order?.palletMergeBatchId || 'single-order'}; ${trace(groupRead.response)}`,
OPTIONS)
const batchCleared = await api(context, 'PUT', '批量取消PDA合托分组', batchMergePath,
{ orderIds: allIds, palletMergeRequired: false })
context.assert(MODULE, '批量合托分组取消成功', batchCleared.response, batchCleared.data == null, trace(batchCleared.response), OPTIONS)
@@ -43,7 +43,7 @@ test('phase-five module proves the client delivery create, list and nested detai
assert.match(source, /id\(profile\?\.id\) === customerId/)
assert.match(source, /客户创建派送并原子转出库单/)
assert.match(source, /deliveryMethod: 'TRUCK_PALLET'/)
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10/)
assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',\s*currency: 'USD', amount: 10/)
assert.match(source, /客户分页回查自身派送单/)
assert.match(source, /客户读取自身派送详情/)
assert.match(source, /merchantDetail\.data\?\.order\?\.deliveryNo/)
@@ -179,7 +179,8 @@ export async function run(context) {
vehicleType: 'TRUCK', vehicleSize: 'MEDIUM', driverName: updateBody.driverName,
driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate,
appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`,
fees: [{ feeType: 'DELIVERY_FEE', currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',
currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }],
}
const transferred = await merchant(context, merchantToken, 'POST', '客户创建派送并原子转出库单',
query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody)
@@ -44,6 +44,8 @@ test('four real command payloads mutate contract fields and preserve remarks and
const forbidden = index < 2 ? 'remark' : 'reason'
assert.equal(Object.hasOwn(body, forbidden), false, `${actionPath} must not send ${forbidden}`)
const key = headers['Idempotency-Key']
assert.match(key, /^[A-Za-z0-9:_-]+$/)
assert.ok(key.endsWith(`terminal-${actionPath}`))
const saved = commands.get(key)
if (saved) {
if (JSON.stringify(saved.body) === JSON.stringify(body)) { replays++; return response(saved.state) }
@@ -98,6 +100,8 @@ test('all four commands cover idempotent replay hash conflict and lifecycle sema
'SELECT_FLEET', 'SCHEDULE_PICKUP', 'RECORD_PICKUP', 'RECORD_EMPTY_RETURN', 'RESOLVED',
]) assert.ok(source.includes(token), `missing ${token}`)
assert.match(source, /JSON\.stringify\(replay\.data\) === JSON\.stringify\(first\.data\)/)
assert.match(source, /terminal-\$\{path\}.*replace\(\/\[\^A-Za-z0-9:_-\]\//)
assert.doesNotMatch(source, /terminal-\$\{action\}/)
})
test('target is strictly current-marker pickup and completed-PDA container', () => {
@@ -38,7 +38,7 @@ function fixture(context) {
return null
}
async function command(context, target, action, path, body) {
const key = `${context.marker}-terminal-${action}`.slice(0, 128)
const key = `${context.marker}-terminal-${path}`.replace(/[^A-Za-z0-9:_-]/g, '').slice(0, 128)
const first = await api(context, 'POST', `目的港码头拖车${action}`, `${BASE}/${target.containerBookingId}/${path}`,
body, { 'Idempotency-Key': key })
if (!success(first.response)) return first