Test #1
@@ -100,7 +100,7 @@ export async function run(context) {
|
||||
const containerDetail = context.apiSuccess(MODULE, '回查货柜详情', containerDetailResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '货柜详情保留候选货物路线约束', containerDetailResponse,
|
||||
containerDetail?.destinationWarehouseCode === (candidate.destinationWarehouseCode || null)
|
||||
&& containerDetail?.planStatus === 'EDITING',
|
||||
&& containerDetail?.planStatus === 'WAIT_LOADING',
|
||||
`planStatus=${containerDetail?.planStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const planResponse = await request(context, 'POST', `${SMART}/plans`, { bookingId: containerId, remark: context.marker })
|
||||
@@ -147,24 +147,24 @@ export async function run(context) {
|
||||
const pushResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
|
||||
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey })
|
||||
const pushed = context.apiSuccess(MODULE, '推送预装并创建 PDA 任务', pushResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '推送后计划进入 LOADING 且版本递增', pushResponse,
|
||||
pushed?.status === 'LOADING' && Number(pushed?.version) === Number(assigned.version) + 1,
|
||||
context.assert(MODULE, '推送后计划保持 EDITING 且版本递增', pushResponse,
|
||||
pushed?.status === 'EDITING' && Number(pushed?.version) === Number(assigned.version) + 1,
|
||||
`status=${pushed?.status || 'missing'}, version=${pushed?.version ?? 'missing'}`, OPTIONS)
|
||||
if (pushed?.status !== 'LOADING') return
|
||||
if (pushed?.status !== 'EDITING') return
|
||||
|
||||
const pushReplayResponse = await request(context, 'POST', `${SMART}/plans/${planId}/push-preload`,
|
||||
{ expectedVersion: Number(assigned.version) }, { 'Idempotency-Key': pushKey })
|
||||
const pushReplay = context.apiSuccess(MODULE, '推送预装 Idempotency-Key 重放', pushReplayResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '推送重放保持同一 LOADING 计划', pushReplayResponse,
|
||||
id(pushReplay?.id) === planId && pushReplay?.status === 'LOADING'
|
||||
context.assert(MODULE, '推送重放保持同一 EDITING 计划', pushReplayResponse,
|
||||
id(pushReplay?.id) === planId && pushReplay?.status === 'EDITING'
|
||||
&& Number(pushReplay?.version) === Number(pushed.version),
|
||||
`planId=${id(pushReplay?.id) || 'missing'}, version=${pushReplay?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const preloadPageResponse = await context.request('GET',
|
||||
`${FIRST_LEG}/loading-plans?warehouseId=${encodeURIComponent(warehouseId)}&keyword=${encodeURIComponent(containerNo)}&page=1&size=20`)
|
||||
const preloadPage = context.apiSuccess(MODULE, '查询本轮预装清单', preloadPageResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单包含本轮 LOADING 计划', preloadPageResponse,
|
||||
records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'LOADING'),
|
||||
context.assert(MODULE, '预装清单包含本轮已推送 EDITING 计划', preloadPageResponse,
|
||||
records(preloadPage).some((item) => id(item.planId) === planId && item.status === 'EDITING'),
|
||||
`records=${records(preloadPage).length}`, OPTIONS)
|
||||
|
||||
const taskResponse = await context.request('GET',
|
||||
@@ -185,7 +185,8 @@ export async function run(context) {
|
||||
failure(wrongWarehouseResponse), `HTTP=${wrongWarehouseResponse.http}, code=${codeOf(wrongWarehouseResponse)}`, OPTIONS)
|
||||
|
||||
const claimResponse = await request(context, 'POST', `${PDA}/tasks/${taskId}/claim`, {
|
||||
expectedAssignmentVersion: 0, operationId: `${context.marker}-pda-claim`.slice(0, 128),
|
||||
expectedAssignmentVersion: Number(task?.assignment?.version || 0),
|
||||
operationId: `${context.marker}-pda-claim`.slice(0, 128),
|
||||
})
|
||||
const claimed = context.apiSuccess(MODULE, '领取本轮柜次 PDA 任务', claimResponse, '', OPTIONS)
|
||||
context.assert(MODULE, 'PDA 任务只分配给当前登录作业员', claimResponse,
|
||||
@@ -193,6 +194,12 @@ export async function run(context) {
|
||||
&& Number(claimed?.assignment?.version) === 0,
|
||||
`workflow=${claimed?.workflowStatus || 'missing'}, assigneeId=${id(claimed?.assignment?.assigneeId) || 'missing'}`, OPTIONS)
|
||||
|
||||
const claimedPlanResponse = await context.request('GET', `${SMART}/plans/${planId}`)
|
||||
const claimedPlan = context.apiSuccess(MODULE, '领取后回查装柜计划', claimedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '仅领取任务不提前开始装柜', claimedPlanResponse,
|
||||
claimedPlan?.status === 'EDITING' && Number(claimedPlan?.version) === Number(pushed?.version),
|
||||
`status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const batchPath = `${PDA}/tasks/${taskId}/batch`
|
||||
const batchItems = [{ itemId: id(pickItem.id), action: 'PICK', pickedPackageCount: 1,
|
||||
finalPickedPackageCount: null, releaseMode: 'MANUAL', locationIds: [],
|
||||
@@ -261,6 +268,7 @@ export async function run(context) {
|
||||
id(confirmedDetail?.summary?.planId) === planId && confirmedDetail?.summary?.status === 'COMPLETED'
|
||||
&& confirmedDetail?.items?.some?.((item) => id(item.inboundId) === inboundId
|
||||
&& id(item.accountId) === tenantId && Boolean(item.frozenAt))
|
||||
&& confirmedDetail?.histories?.some?.((item) => item.actionType === 'START_LOADING')
|
||||
&& confirmedDetail?.histories?.some?.((item) => item.actionType === 'PDA_COMPLETE_LOADING'
|
||||
&& Boolean(item.operatorName) && Boolean(item.occurredAt)),
|
||||
`items=${confirmedDetail?.items?.length ?? 'n/a'}, histories=${confirmedDetail?.histories?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
@@ -169,46 +169,17 @@ export async function run(context) {
|
||||
{ expectedVersion: Number(link.loadingPlanVersion) },
|
||||
{ 'Idempotency-Key': `${context.marker}-bridge-push`.slice(0, 128) })
|
||||
const pushedLoadingPlan = context.apiSuccess(MODULE, '桥接后推送柜次计划并生成真实 PDA 任务', pushResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '关联柜次计划进入 LOADING', pushResponse,
|
||||
pushedLoadingPlan?.status === 'LOADING', `status=${pushedLoadingPlan?.status || 'missing'}`, OPTIONS)
|
||||
context.assert(MODULE, '推送后柜次计划保持 EDITING 且版本递增', pushResponse,
|
||||
pushedLoadingPlan?.status === 'EDITING'
|
||||
&& Number(pushedLoadingPlan?.version) === Number(link.loadingPlanVersion) + 1,
|
||||
`status=${pushedLoadingPlan?.status || 'missing'}, version=${pushedLoadingPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
|
||||
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
|
||||
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2,
|
||||
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
|
||||
|
||||
const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) })
|
||||
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
|
||||
context.assert(MODULE, '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
|
||||
confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
|
||||
&& Number(confirmed?.preloadPlan?.version) === 3,
|
||||
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmed?.mainStatus !== 'LOADING') return
|
||||
|
||||
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload,
|
||||
id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId
|
||||
&& preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker,
|
||||
`planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const loadingResponse = await context.request('GET', detailPath)
|
||||
const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS)
|
||||
const loadingVersion = Number(loading?.summary?.version)
|
||||
context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse,
|
||||
loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion),
|
||||
`status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS)
|
||||
if (!Number.isInteger(loadingVersion)) return
|
||||
|
||||
const blockedBeforePicking = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: loadingVersion, remark: 'must require picking' },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-picking`.slice(0, 128) })
|
||||
context.addStep(MODULE, '未完成 PDA 拣货时拒绝完成装柜', blockedBeforePicking, failure(blockedBeforePicking),
|
||||
`HTTP=${blockedBeforePicking.http}, code=${codeOf(blockedBeforePicking)}`, OPTIONS)
|
||||
const blockedBeforePreloadConfirm = await request(context, 'POST', `${base}/loading/complete?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: initialVersion, remark: 'must confirm preload first' },
|
||||
{ 'Idempotency-Key': `${context.marker}-p3-complete-before-preload-confirm`.slice(0, 128) })
|
||||
context.addStep(MODULE, '预装未确认时拒绝完成装柜', blockedBeforePreloadConfirm,
|
||||
failure(blockedBeforePreloadConfirm),
|
||||
`HTTP=${blockedBeforePreloadConfirm.http}, code=${codeOf(blockedBeforePreloadConfirm)}`, OPTIONS)
|
||||
|
||||
const canonicalTaskResponse = await context.request('GET',
|
||||
`${pdaBase}/tasks?containerNo=${encodeURIComponent(containerNo)}&warehouseId=${encodeURIComponent(warehouseId)}`)
|
||||
@@ -222,13 +193,20 @@ export async function run(context) {
|
||||
if (!canonicalTaskId || !canonicalItem?.id) return
|
||||
|
||||
const claim = await request(context, 'POST', `${pdaBase}/tasks/${canonicalTaskId}/claim`, {
|
||||
expectedAssignmentVersion: 0, operationId: `${context.marker}-bridge-claim`.slice(0, 128),
|
||||
expectedAssignmentVersion: Number(canonicalTask?.assignment?.version || 0),
|
||||
operationId: `${context.marker}-bridge-claim`.slice(0, 128),
|
||||
})
|
||||
const claimed = context.apiSuccess(MODULE, '领取柜次 PDA 任务', claim, '', OPTIONS)
|
||||
context.assert(MODULE, '柜次 PDA 任务绑定当前作业员', claim,
|
||||
claimed?.workflowStatus === 'IN_PROGRESS' && Boolean(claimed?.assignment?.assigneeId),
|
||||
`workflow=${claimed?.workflowStatus || 'missing'}`, OPTIONS)
|
||||
|
||||
const claimedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const claimedPlan = context.apiSuccess(MODULE, '领取后回查柜次计划', claimedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '仅领取 PDA 任务不提前开始装柜', claimedPlanResponse,
|
||||
claimedPlan?.status === 'EDITING' && Number(claimedPlan?.version) === Number(pushedLoadingPlan?.version),
|
||||
`status=${claimedPlan?.status || 'missing'}, version=${claimedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const batchPath = `${pdaBase}/tasks/${canonicalTaskId}/batch`
|
||||
const batchKey = `${context.marker}-bridge-batch`.slice(0, 128)
|
||||
const batchPayload = {
|
||||
@@ -326,6 +304,44 @@ export async function run(context) {
|
||||
atomicBatchReplayVerified,
|
||||
`HTTP=${canonicalBatchReplay.http}, code=${codeOf(canonicalBatchReplay)}, taskVersion=${canonicalBatchReplay.body?.data?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const pickedPlanResponse = await context.request('GET', `${smartLoadingBase}/plans/${loadingPlanId}`)
|
||||
const pickedPlan = context.apiSuccess(MODULE, '首笔真实拣货后回查柜次计划', pickedPlanResponse, '', OPTIONS)
|
||||
context.assert(MODULE, '首笔真实拣货触发装柜状态并由单明细批次直接完成', pickedPlanResponse,
|
||||
directPdaCompletion && pickedPlan?.status === 'COMPLETED',
|
||||
`status=${pickedPlan?.status || 'missing'}, version=${pickedPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
|
||||
const confirming = await request(context, 'POST', `${base}/preload/confirming?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(link.preloadVersion) }, { 'Idempotency-Key': `${context.marker}-p3-confirming`.slice(0, 128) })
|
||||
const confirmingData = context.apiSuccess(MODULE, '提交预装清单确认', confirming, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单进入 CONFIRMING 且版本递增', confirming,
|
||||
confirmingData?.preloadPlan?.status === 'CONFIRMING' && Number(confirmingData?.preloadPlan?.version) === 2,
|
||||
`status=${confirmingData?.preloadPlan?.status || 'missing'}, version=${confirmingData?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmingData?.preloadPlan?.status !== 'CONFIRMING') return
|
||||
|
||||
const confirm = await request(context, 'POST', `${base}/preload/confirm?tenantId=${encodeURIComponent(tenantId)}`,
|
||||
{ expectedVersion: Number(confirmingData.preloadPlan.version) }, { 'Idempotency-Key': `${context.marker}-p3-confirm`.slice(0, 128) })
|
||||
const confirmed = context.apiSuccess(MODULE, '确认预装并进入装柜中', confirm, '', OPTIONS)
|
||||
context.assert(MODULE, '预装确认推进主状态 LOADING 和计划 CONFIRMED', confirm,
|
||||
confirmed?.mainStatus === 'LOADING' && confirmed?.preloadPlan?.status === 'CONFIRMED'
|
||||
&& Number(confirmed?.preloadPlan?.version) === 3,
|
||||
`mainStatus=${confirmed?.mainStatus || 'missing'}, planStatus=${confirmed?.preloadPlan?.status || 'missing'}, version=${confirmed?.preloadPlan?.version ?? 'missing'}`, OPTIONS)
|
||||
if (confirmed?.mainStatus !== 'LOADING') return
|
||||
|
||||
const preload = await context.request('GET', `${base}/preload?tenantId=${encodeURIComponent(tenantId)}`)
|
||||
const preloadData = context.apiSuccess(MODULE, '回查已确认预装清单', preload, '', OPTIONS)
|
||||
context.assert(MODULE, '预装清单归属、明细和 marker 持久化', preload,
|
||||
id(preloadData?.id) === planId && id(preloadData?.inboundId) === inboundId && id(preloadData?.tenantId) === tenantId
|
||||
&& preloadData?.items?.[0]?.containerCode === `P3-${suffix}` && preloadData?.remark === context.marker,
|
||||
`planId=${id(preloadData?.id) || 'missing'}, items=${preloadData?.items?.length ?? 'n/a'}`, OPTIONS)
|
||||
|
||||
const loadingResponse = await context.request('GET', detailPath)
|
||||
const loading = context.apiSuccess(MODULE, '读取装柜中入库单版本', loadingResponse, '', OPTIONS)
|
||||
const loadingVersion = Number(loading?.summary?.version)
|
||||
context.assert(MODULE, '入库单已进入 LOADING 且版本有效', loadingResponse,
|
||||
loading?.summary?.processStatus === 'LOADING' && Number.isInteger(loadingVersion),
|
||||
`status=${loading?.summary?.processStatus || 'missing'}, version=${loading?.summary?.version ?? 'missing'}`, OPTIONS)
|
||||
if (!Number.isInteger(loadingVersion)) return
|
||||
|
||||
// The deployed settlement contract completes a one-item task during pick. A
|
||||
// review call is therefore a negative compatibility assertion, never a second
|
||||
// positive action that can poison the marker fixture with A0409.
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { selectCustomsFlow } from './phase3-customs.mjs'
|
||||
import { buildFinalizeResource } from './phase3-loading-finalize.mjs'
|
||||
|
||||
function source(name) {
|
||||
return readFileSync(new URL(name, import.meta.url), 'utf8')
|
||||
}
|
||||
|
||||
function assertOrder(value, snippets) {
|
||||
let cursor = -1
|
||||
for (const snippet of snippets) {
|
||||
const next = value.indexOf(snippet)
|
||||
assert.ok(next > cursor, `${snippet} must appear after the previous production step`)
|
||||
cursor = next
|
||||
}
|
||||
}
|
||||
|
||||
test('pairs customs source fixture with the matching loading container', () => {
|
||||
const resources = {
|
||||
orchestrationFixtures: {
|
||||
@@ -50,3 +64,26 @@ test('records LOADED and clears the reason only after a real LOADED reread', ()
|
||||
assert.equal(result.finalizeSucceeded, true)
|
||||
assert.equal(result.notCleanedReason, null)
|
||||
})
|
||||
|
||||
test('container loading follows push, claim, real pick, completed sequence', () => {
|
||||
const value = source('./phase3-container-loading.mjs')
|
||||
assert.match(value, /containerDetail\?\.planStatus === 'WAIT_LOADING'/)
|
||||
assertOrder(value, [
|
||||
"pushed?.status === 'EDITING'",
|
||||
"claimedPlan?.status === 'EDITING'",
|
||||
"const pickResponse = await request",
|
||||
"finalPlan?.status === 'COMPLETED'",
|
||||
])
|
||||
})
|
||||
|
||||
test('bridge loading starts through a real PDA batch before preload confirmation', () => {
|
||||
const value = source('./phase3-loading.mjs')
|
||||
assertOrder(value, [
|
||||
"pushedLoadingPlan?.status === 'EDITING'",
|
||||
"claimedPlan?.status === 'EDITING'",
|
||||
"const canonicalPick = await request",
|
||||
"pickedPlan?.status === 'COMPLETED'",
|
||||
"const confirming = await request",
|
||||
])
|
||||
assert.doesNotMatch(value, /关联柜次计划进入 LOADING/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user