diff --git a/tests/unit/handlers/work.orders.handlers.test.js b/tests/unit/handlers/work.orders.handlers.test.js index 32989f7..0ce70e0 100644 --- a/tests/unit/handlers/work.orders.handlers.test.js +++ b/tests/unit/handlers/work.orders.handlers.test.js @@ -81,8 +81,31 @@ test('handlers: createWorkOrder Type 2 (move) relocates the part on its own rack body: { type: 2, deviceType: 'psu', deviceModel: 'P', deviceIdentifier: 'SN-1', info: { location: 'site.warehouse' } } }) const partPush = pushed.find(p => p.action === 'updateThing') + const regPush = pushed.find(p => p.action === 'registerThing') t.is(partPush.params[0].rackId, 'psu-rack-1', 'relocation targets the part rack') t.is(partPush.params[0].info.location, 'site.warehouse', 'part moved to the destination') + t.ok(partPush.params[0].info.workOrderId, 'relocation carries a workOrderId (part-move gate)') + t.is(partPush.params[0].info.workOrderId, regPush.params[0].id, 'relocation references the created WO id') +}) + +test('handlers: createWorkOrder Type 2 (move) surfaces a failed relocation push instead of swallowing it', async (t) => { + const ctx = createMockCtxWithOrks([{ rpcPublicKey: 'k' }], async (_k, method, params) => { + if (method === 'pushAction') { + if (params.action === 'updateThing') return { id: null, errors: ['ERR_ORK_ACTION_CALLS_EMPTY'] } + return { id: 'a', errors: [] } + } + if (method === 'listThings') return [{ id: 'part-1', type: 'inventory-miner_part-psu', rack: 'psu-rack-1', info: { location: 'site.lab' } }] + return null + }) + ctx.authLib = mockAuthLib + ctx._workOrderRackId = RACK + await t.exception( + () => handlers.createWorkOrder(ctx, { + ...userMeta(), + body: { type: 2, deviceType: 'psu', deviceModel: 'P', deviceIdentifier: 'SN-1', info: { location: 'site.warehouse' } } + }), + /ERR_PART_MOVE_PUSH_FAILED/ + ) }) test('handlers: createWorkOrdersBatch Type 2 (move) relocates every part', async (t) => { @@ -109,8 +132,10 @@ test('handlers: createWorkOrdersBatch Type 2 (move) relocates every part', async } }) const partPushes = pushed.filter(p => p.action === 'updateThing') + const regPush = pushed.find(p => p.action === 'registerThing') t.is(partPushes.length, 2, 'one relocation per device') t.is(partPushes[0].params[0].info.location, 'site.miner-room') + t.ok(partPushes.every(p => p.params[0].info.workOrderId === regPush.params[0].id), 'every relocation references the created WO id') }) test('handlers: createWorkOrder merges info.notes, info.remarks, info.site, info.location into thing info', async (t) => { diff --git a/workers/lib/server/handlers/work.orders.handlers.js b/workers/lib/server/handlers/work.orders.handlers.js index 65170e3..3dd6cc0 100644 --- a/workers/lib/server/handlers/work.orders.handlers.js +++ b/workers/lib/server/handlers/work.orders.handlers.js @@ -1,5 +1,6 @@ 'use strict' +const { randomUUID } = require('crypto') const { parseJsonQueryParam, flattenRpcResults, escapeRegex, listThingsWithCount } = require('../../utils') const { WORK_ORDER_THING_TYPE, @@ -9,7 +10,7 @@ const { SPARE_PART_INITIAL_LOCATION } = require('../../constants') const { renderWorkOrderCsv, renderRmaCsv } = require('../lib/work.order.export') -const { submitWorkOrderAction, getWorkOrderRackId } = require('../lib/work.orders') +const { submitWorkOrderAction, getWorkOrderRackId, assertActionApplied } = require('../lib/work.orders') async function _resolvePartByIdentifier (ctx, identifier) { const results = await ctx.dataProxy.requestData('listThings', { @@ -35,6 +36,7 @@ async function createWorkOrder (ctx, req) { } const voter = req._info.user.metadata.email + const woId = randomUUID() const { info: extraInfo, ...body } = req.body const info = { ...body, ...extraInfo, createdBy: voter, createdAt: Date.now() } @@ -85,10 +87,14 @@ async function createWorkOrder (ctx, req) { user: voter }] // Move WOs auto-close, so the relocation has to happen here or it never will. - if (info.location != null) await submitWorkOrderAction(ctx, req, 'updateThing', { id: part.id, info: { location: info.location } }, part.rack) + // The part rack rejects a location change that omits workOrderId (ERR_PART_MOVE_REQUIRES_WO). + if (info.location != null) { + const partResults = await submitWorkOrderAction(ctx, req, 'updateThing', { id: part.id, info: { location: info.location, workOrderId: woId } }, part.rack) + assertActionApplied(partResults, 'ERR_PART_MOVE_PUSH_FAILED') + } } - return submitWorkOrderAction(ctx, req, 'registerThing', { info }) + return submitWorkOrderAction(ctx, req, 'registerThing', { id: woId, info }) } function _buildPartsMove (type, part, device, info, voter, ts) { @@ -126,6 +132,7 @@ async function createWorkOrdersBatch (ctx, req) { } const voter = req._info.user.metadata.email + const woId = randomUUID() const ts = Date.now() const [summary] = devices @@ -153,13 +160,15 @@ async function createWorkOrdersBatch (ctx, req) { const move = _buildPartsMove(type, part, device, info, voter, ts) if (move) partsMoves.push(move) // Move WOs auto-close, so relocate each part here or it never happens. + // The part rack rejects a location change that omits workOrderId (ERR_PART_MOVE_REQUIRES_WO). if (type === WORK_ORDER_TYPES.MOVE && info.location != null) { - await submitWorkOrderAction(ctx, req, 'updateThing', { id: part.id, info: { location: info.location } }, part.rack) + const partResults = await submitWorkOrderAction(ctx, req, 'updateThing', { id: part.id, info: { location: info.location, workOrderId: woId } }, part.rack) + assertActionApplied(partResults, 'ERR_PART_MOVE_PUSH_FAILED') } } info.partsMoves = partsMoves - return submitWorkOrderAction(ctx, req, 'registerThing', { info }) + return submitWorkOrderAction(ctx, req, 'registerThing', { id: woId, info }) } async function updateWorkOrder (ctx, req) { diff --git a/workers/lib/server/lib/work.orders.js b/workers/lib/server/lib/work.orders.js index 291139b..e05f742 100644 --- a/workers/lib/server/lib/work.orders.js +++ b/workers/lib/server/lib/work.orders.js @@ -30,4 +30,13 @@ async function submitWorkOrderAction (ctx, req, action, paramObj, rackId) { }) } -module.exports = { getWorkOrderRackId, submitWorkOrderAction } +function assertActionApplied (results, errCode) { + const errors = (results || []).flatMap(r => r?.errors || []) + if (errors.length) { + const err = new Error(`${errCode}:${errors.join(',')}`) + err.statusCode = 502 + throw err + } +} + +module.exports = { getWorkOrderRackId, submitWorkOrderAction, assertActionApplied }