import test from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' const source = await readFile(new URL('../api/pda-loading.js', import.meta.url), 'utf8') const http = await readFile(new URL('../api/http.js', import.meta.url), 'utf8') const page = await readFile(new URL('../pages/picking/picking.vue', import.meta.url), 'utf8') test('sealed pallets use a single authoritative pallet command and prohibit partial picking', () => { assert.match(source, /pallets\/pick/) assert.match(page, /submitImmediateItemAction\(item, 'PICK_PALLET', \{ palletNo: item.palletNo \}\)/) assert.match(page, /整托货物不能跳拣/) assert.match(page, /整托货物不能部分拣货/) assert.match(page, /expectedTaskVersion: Number\(this.pickingVersion\)/) }) test('DP scan records success only after the atomic server command succeeds', async () => { const method = page.match(/async pickScannedPallet\(item, palletNo\) \{[\s\S]*?\n\t\t\t\},/)?.[0] assert.ok(method) const behavior = new Function('ITEM_STATUS', 'return ({' + method + '})')({ PICKED: 'picked' }) const item = { id: '1', palletNo: 'DP-260914-0001', qty: 5 } const calls = [] let succeeds = false const context = { itemStates: {}, partialQuantities: {}, scannedBoxCodes: {}, async submitImmediateItemAction(...args) { calls.push(args); return succeeds }, recordScanResult() {}, showToast() {}, persistSession() {} } assert.equal(await behavior.pickScannedPallet.call(context, item, item.palletNo), false) assert.deepEqual(context.itemStates, {}) succeeds = true assert.equal(await behavior.pickScannedPallet.call(context, item, item.palletNo), true) assert.deepEqual(context.itemStates, { 1: 'picked' }) assert.deepEqual(calls[0], [item, 'PICK_PALLET', { palletNo: item.palletNo }]) assert.deepEqual(calls[0], calls[1]) }) test('PDA loading adapter uses only task-scoped cabinet evidence paths', () => { for (const path of [ '/first-leg/pda-loading/tasks/pending' ]) assert.match(source, new RegExp(path.replace(/[?]/g, '\\?'))) assert.match(source, /\/first-leg\/pda-loading\/warehouses/) assert.match(source, /query\.push\(`warehouseId=\$\{encodeURIComponent\(warehouseId\)\}`\)/) assert.match(source, /first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence/) assert.match(source, /'Idempotency-Key'/) assert.match(source, /PHOTO_STEP_1[\s\S]*PHOTO_STEP_4[\s\S]*SEAL/) for (const label of ['空仓照片', '半仓照片', '满仓照片', '锁仓照片', '带锁码照']) assert.match(source, new RegExp(label)) assert.match(source, /apiUpload\(/) assert.match(http, /uni\.uploadFile/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\?\$\{query\.join\('&'\)\}/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/void/) assert.doesNotMatch(source, /inbounds\/\$\{encodeURIComponent\(inboundId\)\}\/loading\/evidence/) assert.doesNotMatch(source, /loading\/evidence\/pda\?tenantId=/) assert.match(source, /export function completeContainerPickingTask\(taskId, payload = \{\}\)/) assert.match(source, /first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/complete/) assert.match(source, /expectedTaskVersion/) }) test('PDA adapter does not treat non-200 response bodies or network failures as success', () => { assert.match(http, /if \(code !== '200'\)/) assert.match(http, /next\.network = true/) assert.match(source, /throw new Error\('缺少装柜任务标识,无法上传装柜照片/) }) test('C02 identifiers and separate picking/inbound versions are normalized explicitly', () => { assert.match(source, /inboundId: row\.inboundId/) assert.match(source, /inboundVersion: row\.inboundVersion/) assert.match(source, /pickingVersion: row\.pickingVersion \?\? row\.loading\?\.pickingVersion/) assert.match(source, /pickingVersion: task\.pickingVersion \?\? task\.version/) assert.match(page, /version: Number\(this\.pickingVersion\)/) assert.match(page, /ITEM_STATUS, classifyScan, createItemStates, getPickSummary, isSealedPalletItem, submitStates/) assert.match(page, /taskItemId: item\.id \?\? item\.itemId/) assert.match(page, /submit\.batch/) assert.match(page, /item\.itemStatus === 'PICKED'/) assert.match(page, /item\.itemStatus === 'PARTIAL'/) assert.match(page, /!pickingCompleted && !summary\.canSubmit/) assert.doesNotMatch(page, /loadingVersion/) assert.match(page, /状态尚未同步,请刷新重试/) }) test('PDA normalizes the frozen active and merged container identity fields', () => { for (const field of ['sourceType', 'recordStatus', 'mergedIntoContainerBookingId']) { assert.match(source, new RegExp(`${field}: row\\.${field} \\?\\? row\\.container\\?\\.${field}`)) assert.match(source, new RegExp(`${field}: task\\.${field} \\?\\? task\\.container\\?\\.${field}`)) } }) test('picking page exposes five evidence slots, retry and remote completion states', () => { for (const text of ['PHOTO_STEP_1', 'PHOTO_STEP_4', 'SEAL', '网络失败', '确认装柜完成']) { assert.match(`${source}\n${page}`, new RegExp(text)) } assert.match(page, /createEvidenceUploadQueue/) assert.match(page, /retryEvidence/) assert.match(page, /uploadEvidenceFile/) assert.match(page, /submitting \|\| \(!pickingCompleted && !summary\.canSubmit\)/) assert.doesNotMatch(page, /completePicking\(/) assert.match(page, /sourceType: \['camera'\]/) assert.doesNotMatch(page, /sourceType: \['camera', 'album'\]/) assert.match(page, /evidenceEntries\(slot\.type\)/) assert.match(page, /voidEvidencePhoto\(/) assert.match(page, /evidenceComplete/) }) test('PDA evidence binding normalizes string ids from the task-scoped DTO', () => { assert.match(page, /evidenceItems\(result\)[\s\S]*?id: String\(item\.evidenceId \?\? item\.id/) assert.match(page, /fileId: String\(item\.fileId/) assert.match(page, /applyEvidenceResponse\(result, type, localPath, identity\)[\s\S]*?this\.evidenceFiles = remoteItems[\s\S]*?\.reduce/) assert.match(page, /cacheEvidencePreview\(saved\.fileId, localPath/) }) test('PDA downloads authenticated task-scoped evidence previews', () => { assert.match(source, /export function getEvidencePreviewUrl\(taskId, evidenceId, warehouseId\)/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/evidence\/\$\{encodeURIComponent\(evidenceId\)\}\/content\?warehouseId=/) assert.match(source, /export function downloadEvidencePreview\(previewUrl\)/) assert.match(source, /apiDownload\(\{ url \}\)/) assert.match(http, /uni\.downloadFile\(/) assert.match(http, /headers\.Authorization = `Bearer \$\{accessToken\}`/) assert.match(source, /response\.tempFilePath/) assert.match(source, /evidence\.preview\.response/) assert.match(source, /\^image\\\//) assert.match(source, /resolveEvidencePreviewUrl/) assert.match(source, /const url = resolveEvidencePreviewUrl\(previewUrl, session\.apiBase/) }) test('cabinet evidence never selects an inbound owner or builds a container route in the PDA', () => { const evidenceAdapter = source.match(/export function getEvidence\(taskId, warehouseId\)[\s\S]*?export function completeContainerPickingTask/)?.[0] || '' for (const field of ['inboundId', 'tenantId', 'containerBookingId']) assert.doesNotMatch(evidenceAdapter, new RegExp(field)) assert.match(page, /captureEvidenceIdentity\(\) \{[\s\S]*?taskId:/) assert.match(page, /uploadEvidenceFile\(item\.taskId, item\.warehouseId, item, item\.localPath/) assert.match(page, /queue\.add\(\{ taskId: identity\.taskId, warehouseId: identity\.warehouseId, evidenceType: type, expectedVersion/) assert.doesNotMatch(page, /evidenceIdentity\?\.inboundId|identity\.inboundId/) assert.doesNotMatch(page, /currentBox\.warehouseId \|\| session\.currentUser/) assert.match(source, /warehouseId=\$\{encodeURIComponent\(warehouseId\)\}/) }) test('PDA cabinet picking adapter claims online and submits one atomic batch', () => { assert.match(source, /\/first-leg\/pda-loading\/tasks\/pending/) assert.doesNotMatch(source, /query\.push\(`cabinetNo=/) assert.doesNotMatch(source, /query\.push\(`loadingPlanId=/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/claim/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/items\/\$\{encodeURIComponent\(itemId\)\}\/\$\{path\}/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/pallets\/pick/) assert.match(source, /tasks\/\$\{encodeURIComponent\(task\.taskId\)\}\/batch/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\/inventory-snapshot\?warehouseId=/) assert.match(source, /submitContainerPickingBatch/) assert.match(source, /stableIdempotencyKey/) assert.match(source, /Math\.imul\(hash, 16777619\)/) assert.match(source, /slice\(0, 128\)/) assert.match(page, /claimContainerPickingTask\(/) assert.match(page, /submitImmediateItemAction\(item, 'PICK'/) assert.match(page, /submitImmediateItemAction\(item, 'PARTIAL'/) assert.match(page, /submitImmediateItemAction\(item, 'SKIP'/) assert.match(page, /submitImmediateItemAction\(item, 'PICK_PALLET'/) assert.match(page, /item\.itemStatus === 'PICKING' \|\| item\.itemStatus === 'PARTIAL'/) assert.doesNotMatch(page, /@tap="undoItem\(item\.id\)"/) assert.match(page, /submitContainerPickingBatch\(/) assert.match(source, /palletNo: action\.type === 'PICK_PALLET' \? action\.palletNo : null/) assert.match(page, /type: 'PICK_PALLET'[\s\S]*?palletNo: item\.palletNo/) assert.match(page, /扫描托盘码[\s\S]*?托内任一箱码/) assert.match(page, /确认整托拣出/) assert.match(page, /async pickItem[\s\S]*?isSealedPalletItem\(item\)[\s\S]*?submitImmediateItemAction\(item, 'PICK_PALLET', \{ palletNo: item\.palletNo \}\)/) assert.match(page, /或点击“确认整托拣出”后再提交/) assert.match(page, /sanitizeUserMessage\(error\.message, '任务状态已变化,请刷新后重试'\)/) assert.match(source, /data: \{ warehouseId, loadingPlanId: task\.loadingPlanId, containerBookingId: task\.containerBookingId, expectedVersion, inventorySnapshotToken, items \}/) assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /releaseMode|locationIds|reservationIds/) assert.doesNotMatch(source, /query\.push\(`containerNo=/) assert.doesNotMatch(page, /getContainerPickingTask\([^\n]*containerNo:/) assert.match(source, /tasks\/\$\{encodeURIComponent\(taskId\)\}\?warehouseId=/) assert.match(source, /currentContainerNo/) assert.match(source, /getPickingInventorySnapshot[\s\S]*?\.then\(normalizeInventorySnapshot\)/) assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /containerNo/) assert.doesNotMatch(page, /claim-[^`]*currentBox\.boxNo/) assert.doesNotMatch(page.match(/stableOperationId\('submit'[\s\S]*?\n/)?.[0] || '', /boxNo|containerNo|currentContainerNo/) assert.match(page, /await getPickingInventorySnapshot\([\s\S]*await submitContainerPickingBatch/) assert.match(page, /库存已变化,请刷新后重试/) assert.doesNotMatch(source.match(/export async function submitContainerPickingBatch[\s\S]*?\n\}/)?.[0] || '', /for \(/) assert.match(page, /version: Number\(item\.version \|\| 0\)/) assert.match(page, /refreshTask/) assert.match(page, /remoteError && !pickingCompleted/) assert.match(page, /库存快照未同步,请刷新任务后重试/) assert.match(page, /拣货完成未提交,请查看错误提示/) assert.match(page, /if \(remoteTask\.workflowStatus === 'UNASSIGNED'\)[\s\S]*claimContainerPickingTask/) assert.doesNotMatch(page, /loadingVersion/) assert.match(page, /taskRequestId/) assert.match(page, /resetEvidenceState/) assert.match(page, /availablePickQuantity/) assert.match(page, /库存\/库位可用量不足/) }) test('PDA final completion delegates automatic cabinet side effects to the task complete endpoint', () => { const complete = source.match(/export function completeContainerPickingTask\(taskId, payload = \{\}\) \{[\s\S]*?\n\}/)?.[0] || '' assert.match(complete, /\/first-leg\/pda-loading\/tasks\/\$\{encodeURIComponent\(taskId\)\}\/complete/) assert.doesNotMatch(complete, /拖车|trucking/i) })