Files
pda-overseas-uniapp/tests/warehouse-workflows.test.mjs
T

128 lines
7.5 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { readFile } from 'node:fs/promises'
import { session } from '../src/stores/session.js'
import {
countStocktake, createStocktake, getStocktake, listStocktakes, moveInventory,
resolveInventory, resolveWarehouseLocation, resolveWarehousePallet, submitStocktake
} from '../src/api/warehouse-operations.js'
import {
claimRelabelTask, completeRelabelTask, getRelabelTask, listRelabelCandidates, relabelUnit,
reportRelabelException, reviewRelabelTask, skipRelabelUnit
} from '../src/api/relabel.js'
import {
releaseWarehouseKey, resetWarehouseWork, restoreWarehouseWork, updateWarehouseWork,
warehouseIdempotencyKey, warehouseWork
} from '../src/stores/warehouse-work.js'
const read = (path) => readFile(new URL(path, import.meta.url), 'utf8')
function requestMock() {
const calls = []
session.apiBase = '/overseas-api'; session.token = 'warehouse-token'
globalThis.uni = {
request(options) {
calls.push({ method: options.method, url: options.url, data: options.data, authorization: options.header.Authorization })
let data = {}
if (options.url.includes('/stocktakes?')) data = { records: [], page: 1, size: 100, total: 0, pages: 0 }
else if (options.url.endsWith('/stocktakes')) data = { task: { taskNo: 'CC-1', version: 0 }, items: [] }
else if (/\/stocktakes\/[^/]+$/.test(options.url)) data = { task: { taskNo: 'CC/1', version: 0 }, items: [] }
else if (options.url.includes('/stocktakes/') && options.url.endsWith('/submit')) data = { task: { taskNo: 'CC/1', status: 'SUBMITTED', version: 1 }, items: [] }
else if (options.url.includes('/stocktakes/') && options.url.endsWith('/counts')) data = { positionId: '9', actualPackages: 2 }
else if (options.url.endsWith('/inventory-moves')) data = { moveNo: 'MV-1', palletCode: 'PLT-1', status: 'COMPLETED' }
else if (options.url.endsWith('/inventory/resolve')) data = { queryCode: 'SKU-1', items: [] }
else if (options.url.endsWith('/locations/resolve')) data = { locationCode: 'A-01' }
else if (options.url.endsWith('/pallets/resolve')) data = { palletCode: 'PLT-1', version: 2, inventory: [] }
else if (options.url.endsWith('/relabel-tasks/candidates')) data = []
else data = { relabelNo: 'RL/1', taskStatus: 'PROCESSING', version: 1, items: [] }
options.success({ statusCode: 200, data: { code: '200', data }, header: {} })
}
}
return calls
}
test('warehouse API calls exact PDA paths and never supplies client warehouse scope', async () => {
const calls = requestMock()
await listStocktakes(); await createStocktake({ scopeType: 'LOCATION', scopeCode: 'A-01', idempotencyKey: 'create-1' })
await getStocktake('CC/1'); await countStocktake('CC/1', { positionId: '9', actualPackages: 2, expectedVersion: 0, idempotencyKey: 'count-1' })
await submitStocktake('CC/1', { expectedVersion: 1, reason: 'cycle', idempotencyKey: 'submit-1' })
await moveInventory({ sourcePalletCode: 'PLT-1', targetLocationCode: 'B-01', expectedPalletVersion: 2, reason: 'capacity', idempotencyKey: 'move-1' })
await resolveInventory('SKU-1'); await resolveWarehouseLocation('A-01'); await resolveWarehousePallet('PLT-1')
assert.equal(calls.length, 9)
for (const path of ['/stocktakes?page=1&size=100&status=IN_PROGRESS', '/stocktakes/CC%2F1/counts', '/stocktakes/CC%2F1/submit', '/inventory-moves', '/inventory/resolve', '/locations/resolve', '/pallets/resolve']) {
assert.ok(calls.some((call) => call.url.endsWith(path)), path)
}
assert.ok(calls.every((call) => call.authorization === 'Bearer warehouse-token'))
for (const call of calls) {
assert.equal(Object.hasOwn(call.data || {}, 'customerId'), false)
assert.equal(Object.hasOwn(call.data || {}, 'warehouseId'), false)
}
})
test('relabel API covers claim, unit outcomes, completion and review with encoded business numbers', async () => {
const calls = requestMock()
await listRelabelCandidates(); await getRelabelTask('RL/1')
await claimRelabelTask('RL/1', { idempotencyKey: 'claim-1' })
await relabelUnit('RL/1', { itemId: '7', oldLabel: 'OLD', newLabel: 'NEW', expectedVersion: 1, idempotencyKey: 'unit-1' })
await skipRelabelUnit('RL/1', { itemId: '7', oldLabel: 'OLD-2', reason: 'skip', expectedVersion: 2, idempotencyKey: 'skip-1' })
await reportRelabelException('RL/1', { itemId: '7', oldLabel: 'OLD-3', exceptionCode: 'DAMAGED', reason: 'damage', expectedVersion: 3, idempotencyKey: 'exception-1' })
await completeRelabelTask('RL/1', { photoFileIds: ['8'], expectedVersion: 4, idempotencyKey: 'complete-1' })
await reviewRelabelTask('RL/1', { decision: 'APPROVE', expectedVersion: 5, idempotencyKey: 'review-1' })
for (const path of ['/relabel-tasks/candidates', '/relabel-tasks/RL%2F1', '/claim', '/units', '/units/skip', '/exceptions', '/complete', '/review']) {
assert.ok(calls.some((call) => call.url.endsWith(path)), path)
}
assert.ok(calls.every((call) => !/[?&](?:customerId|warehouseId)=/.test(call.url)))
})
test('warehouse drafts are isolated by operator and warehouse and retain retry keys until success', () => {
const values = new Map()
globalThis.uni = {
setStorageSync(key, value) { values.set(key, structuredClone(value)) },
getStorageSync(key) { return structuredClone(values.get(key)) },
removeStorageSync(key) { values.delete(key) }
}
resetWarehouseWork()
assert.equal(restoreWarehouseWork('operator-a:warehouse-a'), false)
updateWarehouseWork('move', { sourcePalletCode: 'PLT-1', targetLocationCode: 'A-01', reason: 'capacity' })
const first = warehouseIdempotencyKey('move', 'move', { palletCode: 'PLT-1', locationCode: 'A-01' })
const retry = warehouseIdempotencyKey('move', 'move', { palletCode: 'PLT-1', locationCode: 'A-01' })
assert.equal(retry.value, first.value)
warehouseWork.move.sourcePalletCode = ''
assert.equal(restoreWarehouseWork('operator-a:warehouse-a'), true)
assert.equal(warehouseWork.move.sourcePalletCode, 'PLT-1')
releaseWarehouseKey('move', first.signature)
assert.equal(restoreWarehouseWork('operator-b:warehouse-a'), false)
assert.equal(warehouseWork.move.sourcePalletCode, '')
})
test('new operation pages wire scans, manual inputs, conflict refresh, loading and diagnostics', async () => {
const [stocktake, move, relabel, query, workbench, pages, api, relabelApi] = await Promise.all([
read('../src/pages/stocktake/stocktake.vue'), read('../src/pages/move/move.vue'),
read('../src/pages/relabel/relabel.vue'), read('../src/pages/query/query.vue'),
read('../src/data/overseas-workbench.js'), read('../src/pages.json'),
read('../src/api/warehouse-operations.js'), read('../src/api/relabel.js')
])
for (const page of [stocktake, move, relabel, query]) {
assert.match(page, /startScanBroadcast/)
assert.match(page, /ScanTargetInput/)
assert.match(page, /requestId[\s\S]*traceId/)
assert.match(page, /正在|加载|查询中/)
assert.match(page, /暂无|未找到|无库存|不是处理中/)
assert.doesNotMatch(page, /模拟成功|customerId:|warehouseId:/)
}
for (const page of [stocktake, move, relabel]) {
assert.match(page, /warehouseIdempotencyKey/)
assert.match(page, /releaseWarehouseKey/)
assert.match(page, /isConflict[\s\S]*刷新/)
}
assert.match(stocktake, /expectedVersion: item\.version/)
assert.match(move, /expectedPalletVersion: Number\(this\.source\.version/)
assert.match(relabel, /expectedVersion: this\.current\.version/)
assert.doesNotMatch(workbench, /PROTOTYPES|getOperationPrototype/)
for (const route of ['stocktake/stocktake', 'move/move', 'relabel/relabel', 'query/query']) assert.match(pages, new RegExp(route))
assert.doesNotMatch(api + relabelApi, /\/api\/platform\/|customerId|warehouseId/)
})