import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import { validateRollbackRequest, validateRollbackScope, resolveRollbackScope, rollbackSql, buildRollbackRemoteScript, fullRollbackSnapshot, assertRollbackBaseline, assertTargetTransferUpdate, assertMidTransactionObservation, assertRestoredSnapshot, runTripRollback, requestRollbackDeparture, assertDatabaseLockConflict, createDepartureRollbackVerifier } from './fleet-trip-rollback-test-env.mjs' const sqlSource = fs.readFileSync(new URL('../lastleg-service/src/main/resources/db/lastleg.sql', import.meta.url), 'utf8') // Read actual CREATE columns (including multiple columns on a line), respecting quoted comments and nested type parentheses. function ddlColumns(table) { const body = sqlSource.match(new RegExp(`CREATE TABLE(?: IF NOT EXISTS)? ${table} \\(([\\s\\S]*?)\\) ENGINE=`, 'i'))?.[1] assert.ok(body, `actual schema table ${table}`) const parts = []; let start = 0, depth = 0, quoted = false for (let i = 0; i < body.length; i++) { if (body[i] === "'" && body[i - 1] !== '\\') quoted = !quoted if (!quoted) { if (body[i] === '(') depth++ if (body[i] === ')') depth-- if (body[i] === ',' && depth === 0) { parts.push(body.slice(start, i)); start = i + 1 } } } parts.push(body.slice(start)) const primary = body.match(/PRIMARY KEY\s*\(([^)]+)\)/i)[1].split(',').map(c => c.trim()) return parts.map(part => part.trim().match(/^([a-z][a-z0-9_]*)\s+((?:BIGINT|INT|VARCHAR|CHAR|TEXT|LONGTEXT|MEDIUMTEXT|DATETIME|TIMESTAMP|DATE|TIME|JSON|DECIMAL|TINYINT|SMALLINT|BOOLEAN|BIT|DOUBLE|FLOAT)(?:\([^)]*\))?)/i)) .filter(Boolean).map((match, i) => ({ collection: 'column', table, column: match[1], ordinal: i + 1, type: match[2].toLowerCase(), key: primary.includes(match[1]) ? 'PRI' : '' })) } const tableNames = ['ll_dispatch_order', 'll_dispatch_history', 'll_dispatch_item', 'll_dispatch_command', 'll_dispatch_pallet_lock', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard', 'll_dispatch_item_source', 'll_fleet_trip', 'll_fleet_trip_dispatch', 'll_fleet_trip_history', 'll_fleet_pool_order_link', 'll_outbound_order', 'll_delivery_order', 'll_order', 'll_loading_order_status_history', 'll_warehouse_transfer_order', 'll_dispatch_pda_task', 'll_dispatch_pda_pick', 'll_dispatch_pda_pick_order', 'll_inventory_balance', 'll_inventory_position', 'll_inventory_ledger'] const columns = [...tableNames].sort().flatMap(ddlColumns) const request = { marker: 'OWN_RUN', tripNo: 'TR-OWN', dispatchNo: 'DS-OWN', warehouseId: '51', commandKey: 'ORIGINAL-DEPART', commandVersion: '11', expectedImage: 'sha256:' + 'a'.repeat(64), // Reverse input and numeric ordering deliberately differ from Java string ordering. members: [{ customerId: '99', incomingNo: 'WT-99', dispatchItemId: '302', palletId: '402', positionId: '502', balanceId: '602', outboundNo: 'OB-99', deliveryNo: 'DL-99' }, { customerId: '101', incomingNo: 'WT-101', dispatchItemId: '301', palletId: '401', positionId: '501', balanceId: '601', outboundNo: 'OB-101', deliveryNo: 'DL-101' }] } function resolution() { return [{ collection: 'root', id: '201', dispatchNo: request.dispatchNo, warehouseId: '51', status: 'ACTIVE', stage: 'AFTER_LOADING', version: '11' }, { collection: 'trip', id: '211', tripNo: request.tripNo, warehouseId: '51', status: 'PLANNED' }, { collection: 'binding', dispatchId: '201', dispatchNo: request.dispatchNo, tripId: '211', tripNo: request.tripNo }, { collection: 'task', id: '221', dispatchId: '201', warehouseId: '51' }, ...columns, ...request.members.flatMap(m => { const suffix = m.customerId === '101' ? '1' : '2' return [{ collection: 'item', id: m.dispatchItemId, dispatchId: '201', customerId: m.customerId, palletId: m.palletId, orderId: '70' + suffix, sourceType: null, sourceNo: null, incomingNo: m.incomingNo, packages: '1' }, { collection: 'transfer', id: '80' + suffix, customerId: m.customerId, incomingNo: m.incomingNo, orderId: '70' + suffix, warehouseId: '51', needRelabel: '0', relabelNo: null, outboundAt: null }, { collection: 'outbound', id: '90' + suffix, customerId: m.customerId, dispatchId: '201', outboundNo: m.outboundNo, status: 'WAIT_OUTBOUND' }, { collection: 'delivery', id: '100' + suffix, customerId: m.customerId, dispatchId: '201', deliveryNo: m.deliveryNo, outboundNo: m.outboundNo, status: 'WAIT_DELIVERY' }] })] } const scope = resolveRollbackScope(request, resolution()) const row = (table, values) => { const actual = columns.filter(c => c.table === table).map(c => c.column) for (const key of Object.keys(values)) assert.ok(actual.includes(key), `real ${table}.${key}`) return { ...Object.fromEntries(actual.map(c => [c, null])), ...values } } function baseline() { const value = Object.fromEntries(tableNames.map(table => [table, []])) value.ll_dispatch_order.push(row('ll_dispatch_order', { id: '201', dispatch_no: request.dispatchNo, trip_id: '211', overseas_warehouse_id: '51', status: 'ACTIVE', stage: 'AFTER_LOADING', record_version: '11', shipped_package_count: '0' })) value.ll_fleet_trip.push(row('ll_fleet_trip', { id: '211', trip_no: request.tripNo, status: 'PLANNED', record_version: '7' })) value.ll_fleet_trip_dispatch.push(row('ll_fleet_trip_dispatch', { dispatch_id: '201', dispatch_no: request.dispatchNo, trip_id: '211', trip_no: request.tripNo })) value.ll_dispatch_pda_task.push(row('ll_dispatch_pda_task', { id: '221', dispatch_order_id: '201' })) for (const [index, m] of scope.members.entries()) { const suffix = String(index + 1) value.ll_dispatch_item.push(row('ll_dispatch_item', { id: m.dispatchItemId, dispatch_order_id: '201', customer_id: m.customerId })) value.ll_outbound_order.push(row('ll_outbound_order', { id: m.outboundId, customer_id: m.customerId, dispatch_order_id: '201', outbound_no: m.outboundNo, status: 'WAIT_OUTBOUND' })) value.ll_delivery_order.push(row('ll_delivery_order', { id: m.deliveryId, customer_id: m.customerId, dispatch_order_id: '201', delivery_no: m.deliveryNo, status: 'WAIT_DELIVERY', version: '0' })) value.ll_order.push(row('ll_order', { id: m.orderId, customer_id: m.customerId, picked_package_count: '1', shipped_package_count: '0' })) value.ll_warehouse_transfer_order.push(row('ll_warehouse_transfer_order', { id: m.transferId, customer_id: m.customerId, lastleg_order_id: m.orderId, transfer_no: m.incomingNo, status: 'RECEIVED' })) value.ll_dispatch_pallet_lock.push(row('ll_dispatch_pallet_lock', { id: '110' + suffix, physical_lock_id: '170' + suffix, customer_id: m.customerId, dispatch_order_id: '201', warehouse_pallet_id: m.palletId, status: 'ACTIVE', active_key: 'OWN-' + suffix })) value.ll_dispatch_physical_pallet_lock.push(row('ll_dispatch_physical_pallet_lock', { id: '170' + suffix, warehouse_id: '51', warehouse_pallet_id: m.palletId, active_warehouse_pallet_id: m.palletId, dispatch_order_id: scope.dispatchId, status: 'ACTIVE' })) value.ll_physical_pallet_guard.push(row('ll_physical_pallet_guard', { warehouse_pallet_id: m.palletId, created_at: `2026-10-19 11:00:00.00${suffix}` })) value.ll_dispatch_pda_pick.push(row('ll_dispatch_pda_pick', { id: '120' + suffix, customer_id: m.customerId, task_id: '221', dispatch_item_id: m.dispatchItemId, inventory_position_id: m.positionId, packages: '1' })) value.ll_dispatch_pda_pick_order.push(row('ll_dispatch_pda_pick_order', { id: '130' + suffix, customer_id: m.customerId, pick_id: '120' + suffix, receipt_id: '140' + suffix, order_id: m.orderId, packages: '1' })) value.ll_inventory_balance.push(row('ll_inventory_balance', { id: m.balanceId, customer_id: m.customerId, overseas_warehouse_id: '51', available_packages: '0' })) value.ll_inventory_position.push(row('ll_inventory_position', { id: m.positionId, balance_id: m.balanceId, customer_id: m.customerId, overseas_warehouse_id: '51', available_packages: '0' })) value.ll_inventory_ledger.push(row('ll_inventory_ledger', { id: '150' + suffix, customer_id: m.customerId, overseas_warehouse_id: '51', business_type: 'SHIP', business_order_no: request.dispatchNo, balance_id: m.balanceId, position_id: m.positionId, delta_packages: '-1' })) } return value } function snapshotRows(snapshot) { return [...structuredClone(columns), ...tableNames.flatMap(table => [{ collection: 'coverage', table, count: snapshot[table].length }, ...snapshot[table].map(value => ({ collection: 'row', table, row: structuredClone(value) }))])] } const updateSql = "UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=?,updated_at=? WHERE customer_id=? AND lastleg_order_id=? AND outbound_at IS NULL" const lease = () => ({ collection: 'lease', connectionId: '11', transactionId: '111', lockId: '802', mode: 'X,REC_NOT_GAP', ageSeconds: 1 }) function midRows() { const dirty = baseline() for (const [index, m] of scope.members.entries()) { Object.assign(dirty.ll_outbound_order[index], { status: 'OUTBOUNDED' }) Object.assign(dirty.ll_delivery_order[index], { status: 'DELIVERING', dispatched_at: '2026-10-19 12:00:00.000', version: '1' }) Object.assign(dirty.ll_order[index], { picked_package_count: '0', shipped_package_count: '1', outbound_order_no: m.outboundNo }) dirty.ll_loading_order_status_history.push(row('ll_loading_order_status_history', { id: '160' + (index + 1), customer_id: m.customerId, order_id: m.orderId, event_id: 'DISPATCH_DEPART:' + scope.dispatchNo })) } Object.assign(dirty.ll_warehouse_transfer_order[0], { status: 'OUTBOUNDED', outbound_at: '2026-10-19 12:00:00.000' }) return [lease(), { collection: 'wait', requester: '12', blocker: '11', requestingTransactionId: '112', blockingTransactionId: '111', lockId: '802', blockingLockId: '802', objectName: 'll_warehouse_transfer_order', indexName: 'PRIMARY', requestStatus: 'WAITING', blockingStatus: 'GRANTED', ownsRoot: 1, statement: updateSql, sessionLockWaitSeconds: '50' }, ...snapshotRows(dirty)] } const conflict = () => ({ http: 200, body: { code: 'A0409', message: '资源正在处理中,请稍后重试' }, requestId: 'request', traceId: 'trace' }) test('business-only request resolves one native root and real customer children in Java string order', () => { assert.equal(scope.dispatchId, '201'); assert.equal(scope.members[0].customerId, '101'); assert.equal(scope.members[1].customerId, '99') assert.equal(scope.members[1].transferId, '802'); assert.equal(request.members[0].customerId, '99') assert.doesNotThrow(() => validateRollbackScope(scope)) const sql = rollbackSql(request, 'RESOLVE') assert.match(sql, /ll_dispatch_order WHERE dispatch_no=/) assert.match(sql, /'incomingNo',CAST\(inbound_code AS CHAR\)/) assert.doesNotMatch(sql, /FOR UPDATE|firstDispatchId|secondDispatchId/) }) for (const [name, mutate] of [ ['two roots', rows => rows.push({ ...rows[0], id: '999' })], ['two Fleet bindings', rows => rows.push({ ...rows.find(r => r.collection === 'binding'), dispatchId: '999' })], ['wrong root version', rows => { rows[0].version = '12' }], ['invented ordinary source alias', rows => { rows.find(r => r.collection === 'item').sourceType = 'WAREHOUSE_TRANSFER' }], ['relabel source', rows => { rows.find(r => r.collection === 'transfer').needRelabel = '1' }], ['wrong incoming', rows => { rows.find(r => r.collection === 'transfer').incomingNo = 'BORROWED' }], ['foreign child', rows => { rows.find(r => r.collection === 'delivery').dispatchId = '999' }], ['missing private ID', rows => { rows.find(r => r.collection === 'transfer').id = undefined }], ['truncated schema', rows => rows.splice(rows.findIndex(r => r.collection === 'column' && r.table === 'll_order'), 1)], ]) test(`native resolution blocks ${name}`, () => { const rows = resolution(); mutate(rows); assert.throws(() => resolveRollbackScope(request, rows)) }) test('scope rejects injection, repeated ownership, reversed sort and unpinned image', () => { for (const change of [{ marker: "OWN';DELETE" }, { expectedImage: 'latest' }, { dispatchId: '1 OR 1=1' }, { members: [...scope.members].reverse() }]) assert.throws(() => validateRollbackScope({ ...scope, ...change })) const bad = structuredClone(request); bad.members[1].customerId = bad.members[0].customerId assert.throws(() => validateRollbackRequest(bad)) }) test('lease is only the second actual transfer PK, fixed read-only lease and automatic rollback', () => { const sql = rollbackSql(scope, 'LEASE') assert.match(sql, /FROM ll_warehouse_transfer_order WHERE id=802 AND customer_id=/) assert.match(sql, /lastleg_order_id=702 AND transfer_no=/) assert.match(sql, /outbound_at IS NULL FOR UPDATE;.*SLEEP\(65\)\);ROLLBACK;/) assert.doesNotMatch(sql, /\b(?:INSERT|DELETE|ALTER|DROP|TRUNCATE|REPLACE|KILL|SET GLOBAL)\b|ll_dispatch_pallet_lock/) assert.equal((sql.match(/\bUPDATE\b/g) || []).length, 1) assert.throws(() => rollbackSql(scope, 'CUSTOM_SQL')) }) test('wait proof joins exact granted/blocking PK, real transaction SQL, root ownership and requester session variable', () => { const sql = rollbackSql(scope, 'OBSERVE') for (const text of ['READ UNCOMMITTED', 'w.BLOCKING_ENGINE_LOCK_ID', "bl.LOCK_DATA=l.LOCK_DATA", "l.LOCK_DATA='802'", 'r.trx_query', 'v.THREAD_ID=th.THREAD_ID', 'th.PROCESSLIST_ID=r.trx_mysql_thread_id', "v.VARIABLE_NAME='innodb_lock_wait_timeout'", "dl.LOCK_DATA='201'"]) assert.ok(sql.includes(text), text) assert.doesNotMatch(sql, /@@GLOBAL|ll_dispatch_pda_operation|DEPARTED/) assert.match(rollbackSql(scope, 'RESTORED'), /REPEATABLE READ;\nSTART TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;/) assert.doesNotMatch(rollbackSql(scope, 'RESTORED'), /READ UNCOMMITTED/) }) test('generated resolution and snapshot projections use current native schema columns', () => { const sql = rollbackSql(request, 'RESOLVE') + '\n' + rollbackSql(scope, 'BASELINE') for (const query of sql.split(';')) { const match = query.match(/SELECT JSON_OBJECT\([\s\S]*? FROM (ll_[a-z_]+) WHERE/) if (!match) continue const actual = new Set(ddlColumns(match[1]).map(c => c.column)) for (const column of query.slice(0, match.index + match[0].indexOf(' FROM ')).matchAll(/CAST\(`?([a-z_]+)`? AS CHAR/g)) assert.ok(actual.has(column[1]), `${match[1]}.${column[1]}`) } assert.ok(columns.filter(c => c.table === 'll_order').length > 50) assert.doesNotThrow(() => assertRollbackBaseline(fullRollbackSnapshot(snapshotRows(baseline()), scope), scope)) }) test('remote envelope pins TEST container, healthy image, DB identity and preflights observation permissions', () => { function decode(script) { const encoded = script.match(/decoder.decode\(\[((?:'[A-Za-z0-9+/=]*',?)+)\]\.join\(''\)\)/)[1]; return Buffer.from([...encoded.matchAll(/'([^']*)'/g)].map(m => m[1]).join(''), 'base64').toString() } const python = decode(decode(buildRollbackRemoteScript(scope, 'OBSERVE', 'test-agent'))) for (const text of ['firstleg-test-lastleg', 'firstleg-test', "e.get('DEPLOY_ENV')!='test'", "get('Status')!='healthy'", scope.expectedImage, 'MYSQL_PWD', 'variables_by_thread']) assert.ok(python.includes(text), text) assert.ok(buildRollbackRemoteScript({}, 'RUNTIME', 'test-agent')) assert.throws(() => buildRollbackRemoteScript(scope, 'LEASE', "agent');evil()")) }) test('full-field baseline cannot accept missing columns, rows, coverage or changed schema', () => { for (const mutate of [ rows => { delete rows.find(r => r.collection === 'row').row.updated_at }, rows => rows.splice(rows.findIndex(r => r.collection === 'row'), 1), rows => rows.splice(rows.findIndex(r => r.collection === 'coverage'), 1), rows => { rows.find(r => r.collection === 'column').type = 'changed' }, rows => rows.push({ ...rows.find(r => r.collection === 'coverage') }), ]) { const rows = snapshotRows(baseline()); mutate(rows); assert.throws(() => fullRollbackSnapshot(rows, scope)) } }) for (const [name, mutate] of [ ['no committed SHIP', b => { b.ll_inventory_ledger = [] }], ['wrong scan source', b => { b.ll_dispatch_pda_pick_order[0].order_id = '999' }], ['prior departure command', b => { b.ll_dispatch_command.push(row('ll_dispatch_command', { id: '999', action: 'DEPART' })) }], ['source contribution on ordinary transfer', b => { b.ll_dispatch_item_source.push(row('ll_dispatch_item_source', { id: '991', dispatch_order_id: '201', dispatch_item_id: scope.members[0].dispatchItemId })) }], ['ordinary receipt also claims relabel contribution', b => { b.ll_dispatch_pda_pick_order[0].dispatch_item_source_id = '991' }], ['ordinary receipt also claims native return carton', b => { b.ll_dispatch_pda_pick_order[0].return_carton_id = '991' }], ['missing physical lock', b => { b.ll_dispatch_physical_pallet_lock.pop() }], ['missing permanent physical guard', b => { b.ll_physical_pallet_guard.pop() }], ['guard for a foreign pallet', b => { b.ll_physical_pallet_guard[0].warehouse_pallet_id = '991' }], ['extra foreign permanent guard', b => { b.ll_physical_pallet_guard.push(row('ll_physical_pallet_guard', { warehouse_pallet_id: '991', created_at: '2026-10-19 11:00:00.001' })) }], ['guard without original created timestamp', b => { b.ll_physical_pallet_guard[0].created_at = null }], ['guard with invalid creation timestamp', b => { b.ll_physical_pallet_guard[0].created_at = 'NOT-A-CREATION-TIME' }], ['logical claim points to other physical pallet', b => { b.ll_dispatch_pallet_lock[0].physical_lock_id = b.ll_dispatch_physical_pallet_lock[1].id }], ['logical claim on other main', b => { b.ll_dispatch_pallet_lock[0].dispatch_order_id = '991' }], ['physical claim on other main', b => { b.ll_dispatch_physical_pallet_lock[0].dispatch_order_id = '991' }], ['physical claim on other warehouse', b => { b.ll_dispatch_physical_pallet_lock[0].warehouse_id = '991' }], ['physical claim for other pallet', b => { b.ll_dispatch_physical_pallet_lock[0].warehouse_pallet_id = '991' }], ['physical active key mismatch', b => { b.ll_dispatch_physical_pallet_lock[0].active_warehouse_pallet_id = '991' }], ['physical claim released', b => { b.ll_dispatch_physical_pallet_lock[0].status = 'RELEASED'; b.ll_dispatch_physical_pallet_lock[0].active_warehouse_pallet_id = null }], ['physical draft ownership', b => { b.ll_dispatch_physical_pallet_lock[0].draft_token = 'OTHER-DRAFT'; b.ll_dispatch_physical_pallet_lock[0].draft_container_id = '991' }], ['pool fixture', b => { b.ll_fleet_pool_order_link.push(row('ll_fleet_pool_order_link', { id: '999' })) }], ]) test(`baseline rejects ${name}`, () => { const value = baseline(); mutate(value); assert.throws(() => assertRollbackBaseline(value, scope)) }) test('late dirty observer proves first completed transfer, both source writes, unchanged native roots and prior SHIP', () => { const proof = assertMidTransactionObservation(midRows(), scope, baseline()) assert.equal(proof.dirtyObservationOnly, true); assert.equal(proof.firstCustomerId, '101'); assert.equal(proof.sessionLockWaitSeconds, '50') assert.doesNotThrow(() => assertTargetTransferUpdate(updateSql.replaceAll('=', ' = '), scope)) assert.doesNotThrow(() => assertTargetTransferUpdate(updateSql.replace('customer_id=?', "customer_id='99'").replace('lastleg_order_id=?', 'lastleg_order_id=702'), scope)) }) for (const [name, mutate] of [ ['foreign blocker', r => { r[1].blocker = '77' }], ['missing wait', r => r.splice(1, 1)], ['multiple waits', r => r.push({ ...r[1], requester: '13' })], ['wrong target row', r => { r[1].lockId = '801' }], ['wrong blocking row', r => { r[1].blockingLockId = '801' }], ['wrong blocking transaction', r => { r[1].blockingTransactionId = '113' }], ['unrelated transaction', r => { r[1].ownsRoot = 0 }], ['unavailable session variable', r => { delete r[1].sessionLockWaitSeconds }], ['global timeout stand-in', r => { delete r[1].sessionLockWaitSeconds; r[1].globalLockWaitSeconds = '50' }], ['session timeout beyond lease', r => { r[1].sessionLockWaitSeconds = '120' }], ['unavailable requesting SQL', r => { r[1].statement = null }], ['early child lock', r => { r[1].statement = 'SELECT * FROM ll_outbound_order WHERE dispatch_order_id=201 FOR UPDATE' }], ['different transfer UPDATE', r => { r[1].statement = updateSql.replace('lastleg_order_id=?', 'lastleg_order_id=701') }], ['first outbound not written', r => { r.find(x => x.table === 'll_outbound_order' && x.row?.id === '901').row.status = 'WAIT_OUTBOUND' }], ['first order not written', r => { r.find(x => x.table === 'll_order' && x.row?.id === '701').row.shipped_package_count = '0' }], ['no first source history', r => { r.find(x => x.table === 'll_loading_order_status_history' && x.row?.order_id === '701').row.event_id = 'OTHER' }], ['first transfer not written', r => { r.find(x => x.table === 'll_warehouse_transfer_order' && x.row?.id === '801').row.outbound_at = null }], ['second transfer already changed', r => { r.find(x => x.table === 'll_warehouse_transfer_order' && x.row?.id === '802').row.status = 'OUTBOUNDED' }], ['root changed too soon', r => { r.find(x => x.table === 'll_dispatch_order' && x.row).row.stage = 'DELIVERING' }], ['physical lock released before late source update', r => { r.find(x => x.table === 'll_dispatch_physical_pallet_lock' && x.row).row.status = 'RELEASED' }], ['permanent guard deleted in departure transaction', r => { r.splice(r.findIndex(x => x.table === 'll_physical_pallet_guard' && x.row), 1); r.find(x => x.collection === 'coverage' && x.table === 'll_physical_pallet_guard').count-- }], ['permanent guard recreated in departure transaction', r => { r.find(x => x.table === 'll_physical_pallet_guard' && x.row).row.created_at = '2026-10-19 12:00:00.999' }], ['permanent guard key replaced in departure transaction', r => { r.find(x => x.table === 'll_physical_pallet_guard' && x.row).row.warehouse_pallet_id = '991' }], ['source contribution added before late source update', r => { const value = row('ll_dispatch_item_source', { id: '991', dispatch_order_id: '201' }); r.find(x => x.collection === 'coverage' && x.table === 'll_dispatch_item_source').count = 1; r.push({ collection: 'row', table: 'll_dispatch_item_source', row: value }) }], ['prior SHIP changed', r => { r.find(x => x.table === 'll_inventory_ledger' && x.row).row.delta_packages = '0' }], ]) test(`rejects insufficient middle evidence: ${name}`, () => { const value = midRows(); mutate(value); assert.throws(() => assertMidTransactionObservation(value, scope, baseline())) }) test('rollback compares every full field, not just business status or inventory totals', () => { const before = baseline(), after = structuredClone(before) const proof = assertRestoredSnapshot(before, after) assert.equal(proof.beforeSha256, proof.afterSha256); assert.equal(proof.committedRead, true) for (const table of tableNames.filter(t => after[t].length)) { const changed = structuredClone(after); changed[table][0][Object.keys(changed[table][0]).at(-1)] = 'CHANGED' assert.throws(() => assertRestoredSnapshot(before, changed), table) } }) test('database conflict requires exact handler result and trace; timeout or precondition rejection fails', () => { assert.doesNotThrow(() => assertDatabaseLockConflict(conflict())) for (const wrong of [{ http: 0 }, { http: 500 }, { body: { code: 'S0500', message: 'error' } }, { body: { code: 'A0409', message: '请完成拣货' } }, { traceId: null }]) assert.throws(() => assertDatabaseLockConflict({ ...conflict(), ...wrong })) }) test('single-attempt request keeps original key/version/body on the 60-second channel', async () => { const body = { recordVersion: '11', idempotencyKey: 'ORIGINAL' }, calls = [] const context = { request: () => { throw Error('30_SECOND_CHANNEL') }, requestWithBody: async (...args) => { calls.push(args); return { http: 0, body: null } } } assert.equal((await requestRollbackDeparture(context, '/owned/depart', body, 'owned-token')).http, 0) assert.deepEqual(calls, [['POST', '/owned/depart', body, 'owned-token']]); assert.equal(calls[0][2], body) }) test('plan has no network; non-TEST execution fails before any fetch', async () => { const fetch = globalThis.fetch; globalThis.fetch = () => { throw Error('NETWORK_FORBIDDEN') } try { const plan = await runTripRollback({ execute: false }, { env: {} }) assert.equal(plan.businessSqlWrites, false); assert.equal(plan.deployment, false); assert.match(plan.fault, /One native dispatch/) await assert.rejects(runTripRollback({ execute: true, environment: 'TEST' }, { env: { CHONGCHONG_ENV: 'prod' } }), /TEST/) } finally { globalThis.fetch = fetch } }) test('offline verifier preserves original command and requires all phases before caller can depart/sign', async () => { const original = process.cwd(), temp = fs.mkdtempSync(join(tmpdir(), 'fleet-rollback-offline-')) try { process.chdir(temp); fs.mkdirSync('reports/fleet-cross-offline', { recursive: true }) const calls = [], ready = { trip: { dispatches: [{ dispatchNo: request.dispatchNo }] }, dispatch: { dispatchNo: request.dispatchNo }, task: { recordVersion: '11' } } const fixture = { ...request, pdaToken: 'synthetic-offline-token' }, body = { recordVersion: '11', idempotencyKey: request.commandKey, remark: request.marker } const runtime = { imageId: request.expectedImage, rows: [{ collection: 'databaseRuntime', serverUuid: 'offline', databaseName: 'offline', performanceSchema: 1 }] } const read = async (_env, _scope, mode) => { calls.push(mode) if (mode === 'RESOLVE') return { rows: resolution() } if (mode === 'BASELINE' || mode === 'RESTORED') return { rows: snapshotRows(baseline()) } if (mode === 'LEASE') return { rows: [{ collection: 'held', lockId: '802', connectionId: '11', customerId: '99' }, { released: true }] } if (mode === 'LOCKS') return { rows: [lease()] } if (mode === 'OBSERVE') return { rows: midRows() } if (mode === 'RELEASED') return { rows: [] } throw Error('UNEXPECTED_MODE') } const context = { marker: request.marker, requestWithBody: async (...args) => { calls.push(args); return conflict() } } const proof = await createDepartureRollbackVerifier({}, { runId: 'offline' }, runtime, { read, wait: () => { throw Error('NO_REAL_WAIT') } }) ({ context, fixture, ready, departPath: '/owned/depart', departBody: body, snapshot: async () => ready }) assert.equal(proof.verified, true); assert.equal(proof.consistentSnapshot.fullFields, true) assert.deepEqual(calls.filter(Array.isArray), [['POST', '/owned/depart', body, fixture.pdaToken]]) assert.deepEqual(calls.filter(x => typeof x === 'string'), ['RESOLVE', 'BASELINE', 'LEASE', 'LOCKS', 'OBSERVE', 'RELEASED', 'RESTORED']) assert.deepEqual(proof.command.body, body) const saved = JSON.parse(fs.readFileSync('reports/fleet-cross-offline/rollback.json', 'utf8')) assert.equal(saved.verified, true); assert.ok(saved.finishedAt) const caller = fs.readFileSync(new URL('./admin-test-env-modules/fleet-cross-customer-acceptance.mjs', import.meta.url), 'utf8') assert.ok(caller.indexOf('await verifyDepartureRollback(') < caller.indexOf("const departed = await request('POST', departPath, departBody")) assert.match(caller, /Platform exact replay of PDA native departure/); assert.match(caller, /This customer independently signed/) } finally { process.chdir(original) assert.ok(resolve(temp).startsWith(resolve(tmpdir()) + '\\') || resolve(temp).startsWith(resolve(tmpdir()) + '/')) fs.rmSync(temp, { recursive: true, force: true }) } }) test('23 full snapshots include batch-only permanent guards, empty ordinary contributions and both customer-free physical owners from actual DDL', () => { const full = fullRollbackSnapshot(snapshotRows(baseline()), scope) assert.equal(tableNames.length, 23); assert.equal(Object.keys(full).length, 23) assert.equal(snapshotRows(baseline()).filter(row => row.collection === 'coverage').length, 23) assert.equal(full.ll_dispatch_item_source.length, 0) assert.deepEqual(full.ll_physical_pallet_guard.map(row => row.warehouse_pallet_id).sort(), ['401', '402']) assert.deepEqual(ddlColumns('ll_physical_pallet_guard').map(column => [column.column, column.type, column.key]), [['warehouse_pallet_id', 'bigint', 'PRI'], ['created_at', 'datetime(3)', '']]) assert.deepEqual(Object.keys(full.ll_physical_pallet_guard[0]).sort(), ['created_at', 'warehouse_pallet_id']) assert.equal(full.ll_dispatch_physical_pallet_lock.length, 2) assert.deepEqual(Object.keys(full.ll_dispatch_physical_pallet_lock[0]).sort(), ddlColumns('ll_dispatch_physical_pallet_lock').map(column => column.column).sort()) assert.equal(Object.hasOwn(full.ll_dispatch_physical_pallet_lock[0], 'customer_id'), false) assert.equal(ddlColumns('ll_dispatch_pallet_lock').some(column => column.column === 'active_warehouse_pallet_id'), false) assert.ok(ddlColumns('ll_dispatch_pallet_lock').some(column => column.column === 'physical_lock_id')) assert.ok(ddlColumns('ll_dispatch_pda_pick_order').some(column => column.column === 'dispatch_item_source_id')) assert.doesNotThrow(() => assertRollbackBaseline(full, scope)) const sql = rollbackSql(scope, 'BASELINE') assert.match(sql, /FROM ll_dispatch_physical_pallet_lock WHERE dispatch_order_id=201 OR warehouse_pallet_id IN \(401,402\)/) assert.match(sql, /FROM ll_dispatch_item_source WHERE dispatch_order_id=201 OR dispatch_item_id IN \(301,302\)/) for (const mode of ['BASELINE', 'OBSERVE', 'RESTORED']) { const guardQueries = rollbackSql(scope, mode).split(';').filter(query => query.includes('FROM ll_physical_pallet_guard')) assert.equal(guardQueries.length, 2) for (const query of guardQueries) assert.ok(query.endsWith('FROM ll_physical_pallet_guard WHERE warehouse_pallet_id IN (401,402)')) assert.ok(guardQueries.some(query => query.includes('COUNT(*)'))) assert.ok(guardQueries.some(query => query.includes("'created_at',CAST(`created_at` AS CHAR CHARACTER SET utf8mb4)"))) } assert.match(sql, /'dispatch_item_source_id',CAST\(`dispatch_item_source_id` AS CHAR CHARACTER SET utf8mb4\)/) }) test('missing source-table zero coverage or partial new-table schema cannot masquerade as a full rollback snapshot', () => { for (const table of ['ll_dispatch_item_source', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard']) { const rows = snapshotRows(baseline()).filter(row => !(row.collection === 'coverage' && row.table === table)) assert.throws(() => fullRollbackSnapshot(rows, scope), /EXACT_COVERAGE_REQUIRED/) const resolvedRows = resolution().filter(row => !(row.collection === 'column' && row.table === table)) assert.throws(() => resolveRollbackScope(request, resolvedRows), /COMPLETE_ACTUAL_SCHEMA_REQUIRED/) } const changed = snapshotRows(baseline()) const physical = changed.find(row => row.table === 'll_dispatch_physical_pallet_lock' && row.row) delete physical.row.released_at assert.throws(() => fullRollbackSnapshot(changed, scope), /COMPLETE_FULL_FIELD_SNAPSHOT_REQUIRED/) }) test('permanent guard schema cannot introduce customer, owner, state or TTL or change the original PK/time types', () => { for (const mutate of [ rows => { rows.find(row => row.table === 'll_physical_pallet_guard' && row.column === 'warehouse_pallet_id').key = '' }, rows => { rows.find(row => row.table === 'll_physical_pallet_guard' && row.column === 'created_at').type = 'datetime' }, ...['customer_id', 'owner', 'state', 'expires_at'].map(column => rows => rows.push({ collection: 'column', table: 'll_physical_pallet_guard', column, ordinal: 3, type: 'varchar(128)', key: '' })), ]) { const rows = resolution(); mutate(rows) assert.throws(() => resolveRollbackScope(request, rows)) } }) for (const fault of ['delete', 'recreate', 'created timestamp', 'key replacement']) test(`committed rollback rejects permanent guard ${fault}`, () => { const before = baseline(), restored = structuredClone(before) if (fault === 'delete') restored.ll_physical_pallet_guard.pop() if (fault === 'recreate') { const old = restored.ll_physical_pallet_guard.shift(); restored.ll_physical_pallet_guard.push({ ...old, created_at: '2026-10-19 12:00:00.123' }) } if (fault === 'created timestamp') restored.ll_physical_pallet_guard[0].created_at = '2026-10-19 11:00:00.999' if (fault === 'key replacement') restored.ll_physical_pallet_guard[0].warehouse_pallet_id = '991' assert.throws(() => assertRestoredSnapshot(before, restored), /FULL_FIELD_COMMITTED_ROLLBACK_SNAPSHOT_CHANGED/) })