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 { 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 = '/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('carton relabel uses one scan input and shows the confirmed FBA mapping after saving', async () => { const page = await read('../src/pages/relabel/relabel.vue') assert.equal((page.match(/name="quickCarton"/g) || []).length, 1) assert.match(page, /name="quickSku"/) assert.match(page, /v-if="taskTab === 'sku' && selectedItem && action !== 'RELABEL' && remaining\(selectedItem\) > 0"/) assert.match(page, /selectItem\(item\) \{ if \(this\.taskTab === 'carton'\) \{ this\.activeTarget = 'quick-carton'; return \}/) const start = page.indexOf('async submitCarton(raw)') const scan = page.slice(start, page.indexOf('removePhoto(index)', start)) assert.ok(scan.indexOf('await scanRelabelCarton') < scan.indexOf('this.applyCurrent(result)')) assert.ok(scan.indexOf('this.applyCurrent(result)') < scan.indexOf('uni.showModal')) assert.match(scan, /showCancel: false/) assert.match(scan, /confirmedItem\?\.fbaCartonMark/) }) test('mixed relabel keeps SKU work open and return transport gates pallet work', async () => { const page = await read('../src/pages/relabel/relabel.vue') const body = page.match(/remaining\(item\) \{([^\n]+)\},/)[1] const remaining = new Function('item', body) const mixed = { operationType: 'CARTON_AND_SKU', expectedPackages: 1, relabelledPackages: 1, cartonProcessedPackages: 1, skuProcessedPackages: 0, skippedPackages: 0, exceptionPackages: 0 } assert.equal(remaining.call({ taskTab: 'sku' }, mixed), 1) assert.equal(remaining.call({ taskTab: 'carton' }, mixed), 0) assert.match(page, / { const page = await read('../src/pages/relabel/relabel.vue') assert.doesNotMatch(page, /name="relabelTask"/) assert.match(page, /@tap="openExecution\(item\)"/) assert.match(page, /async openExecution\(item\)/) assert.match(page, /const confirmed = await this\.confirmStartTask\(item\)[\s\S]*if \(!confirmed\) return[\s\S]*claimRelabelTask/) assert.match(page, /relabelExecute&task=\$\{encodeURIComponent\(item\.relabelNo\)\}/) assert.match(page, /当前账号正在执行 \$\{active\.relabelNo\}/) assert.match(page, /v-if="showCurrent" class="detail-toolbar"[\s\S]*@tap="closeCurrent">\u2039 返回列表<\/button>/) assert.match(page, /closeCurrent\(\) \{ this\.current = null;[\s\S]*this\.taskCode = '' \}/) assert.match(page, /updateWarehouseWork\('relabel', \{ currentNo: '', photoFileIds: \[\], photoNames: \[\], remark: '' \}\)/) assert.match(page, /if \(completed\) this\.goToClaimList\(\)/) assert.match(page, /goToClaimList\(\) \{ this\.taskCode = ''; uni\.redirectTo\(\{ url: '\/pages\/relabel\/relabel\?op=relabelReceive' \}\) \}/) }) test('relabel review opens a pending task as customer-confirmation read-only detail', async () => { const page = await read('../src/pages/relabel/relabel.vue') const body = page.match(/showCurrent\(\) \{([^\n]+)\},/)[1] const visible = new Function(body) const context = { stage: 'execute', entryTaskNo: '', current: { relabelNo: 'RL-1', taskStatus: 'PROCESSING' }, candidates: [{ relabelNo: 'RL-1', taskStatus: 'PROCESSING', claimedByCurrentOperator: true }] } assert.equal(visible.call(context), true, 'detail without ownership field must use server candidate ownership') assert.equal(visible.call({ ...context, candidates: [] }), false) assert.equal(visible.call({ ...context, stage: 'review', current: { ...context.current, taskStatus: 'WAIT_REVIEW' } }), true) assert.equal(visible.call({ ...context, current: { ...context.current, taskStatus: 'WAIT_REVIEW' } }), false) assert.match(page, /[\s\S]*@tap="openReview\(item\)"/) assert.match(page, /查看并复核/) assert.match(page, /async openReview\(item\)/) assert.match(page, /现场换标已完成,等待客户确认/) assert.doesNotMatch(page, /@tap="review\('APPROVE'\)"/) assert.match(page, /