test(fba): preserve verified fleet and fulfillment acceptance workflows
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import { run } from './phase4-overseas-master-data.mjs'
|
||||
import { run, vehicleCapacityMatches } from './phase4-overseas-master-data.mjs'
|
||||
|
||||
test('vehicle capacity evidence rejects absent, zero and guessed dimensions', () => {
|
||||
const expected = { maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 }
|
||||
assert.equal(vehicleCapacityMatches(expected, expected), true)
|
||||
assert.equal(vehicleCapacityMatches({ maxPalletSpaces: '30', maxLoadKg: '18000.00', maxVolumeCbm: '90.0' }, expected), true)
|
||||
for (const key of Object.keys(expected)) for (const value of [undefined, null, '', 0, Infinity, 'unknown', 123]) {
|
||||
assert.equal(vehicleCapacityMatches({ ...expected, [key]: value }, expected), false, `${key}: ${value}`)
|
||||
}
|
||||
})
|
||||
|
||||
const source = readFileSync(new URL('./phase4-overseas-master-data.mjs', import.meta.url), 'utf8')
|
||||
const controllerSource = [
|
||||
@@ -28,12 +37,13 @@ function vehicleWire(state) {
|
||||
return { ...state.vehicle, mileageKm: String(state.vehicle.mileageKm), recordVersion: String(state.vehicle.recordVersion) }
|
||||
}
|
||||
|
||||
function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) {
|
||||
function fakeContext({ softAssertions = false, refreshEmpty = false, careLeak = false, processingMarksPaid = false } = {}) {
|
||||
const calls = []
|
||||
const coverageGaps = []
|
||||
const steps = []
|
||||
const marker = 'ADMIN-API-20260821123456789'
|
||||
const state = { warehouse: null, quote: null, exchange: null, vehicle: null, optionReads: 0, role: null, user: null }
|
||||
const careRows = new Map(), careHistory = new Map(), uploadKeys = new Map()
|
||||
const context = {
|
||||
marker, calls, coverageGaps, steps, createdResources: {},
|
||||
async findAvailablePhone() { return '13800138000' },
|
||||
@@ -62,6 +72,37 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) {
|
||||
calls.push({ gateway: 'overseas', method, path, body, extraHeaders, tokenKind: token === 'deny-overseas-token' ? 'denied' : 'admin' })
|
||||
assert.notEqual(method, 'DELETE')
|
||||
if (token === 'deny-overseas-token') return forbidden()
|
||||
if (path.includes('/fleet/vehicles/')) {
|
||||
const url = new URL(path, 'https://fixture.invalid'), parts = url.pathname.split('/records'), owner = parts[0].split('/').at(-1)
|
||||
const suffix = parts[1], record = careRows.get(suffix.split('/')[1])
|
||||
if (record && owner !== record.vehicleId && !careLeak) return { http: 200, body: { code: 'A0404' } }
|
||||
if (method === 'POST' && !suffix) {
|
||||
const prior = [...careRows.values()].find(value => value.key === body.idempotencyKey && value.vehicleId === owner)
|
||||
if (prior) return ok(structuredClone(prior))
|
||||
const created = { recordNo: `VR-${careRows.size + 1}`, vehicleId: owner, details: structuredClone(body.details), files: [], recordVersion: 0, key: body.idempotencyKey,
|
||||
paymentStatus: ['REPAIR', 'RESCUE'].includes(body.details.recordType) ? 'UNLINKED' : null }
|
||||
careRows.set(created.recordNo, created); careHistory.set(created.recordNo, [{ snapshot: structuredClone(created) }]); return ok(structuredClone(created))
|
||||
}
|
||||
if (method === 'GET' && !suffix) {
|
||||
const rows = [...careRows.values()].filter(value => value.vehicleId === owner && (!url.searchParams.get('recordType') || value.details.recordType === url.searchParams.get('recordType')))
|
||||
return ok({ records: structuredClone(rows), total: rows.length })
|
||||
}
|
||||
assert.ok(record)
|
||||
if (method === 'GET' && suffix.endsWith('/history')) return ok({ records: structuredClone(careHistory.get(record.recordNo)) })
|
||||
if (method === 'GET') return ok(structuredClone(record))
|
||||
if (method === 'PUT') {
|
||||
if (Number(body.recordVersion) !== record.recordVersion) return conflict()
|
||||
record.details = structuredClone(body.details); record.recordVersion++
|
||||
if (processingMarksPaid && record.details.incident?.processingStatus === 'COMPLETED') record.paymentStatus = 'PAID'
|
||||
careHistory.get(record.recordNo).unshift({ reason: body.reason, snapshot: structuredClone(record) }); return ok(structuredClone(record))
|
||||
}
|
||||
}
|
||||
if (method === 'POST' && path === `${OVERSEAS_BASE}/vehicles` && body.plateNumber.startsWith('CARE-')) {
|
||||
state.spareVehicle = { ...body, id: '930000000000000002', recordVersion: 0 }; return ok({ ...state.spareVehicle })
|
||||
}
|
||||
if (method === 'PATCH' && path === `${OVERSEAS_BASE}/vehicles/930000000000000002/status`) {
|
||||
state.spareVehicle.status = body.status; state.spareVehicle.recordVersion++; return ok({ ...state.spareVehicle })
|
||||
}
|
||||
if (method === 'GET' && path === `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`) {
|
||||
state.optionReads += 1
|
||||
if (!state.warehouse) return ok([])
|
||||
@@ -194,14 +235,25 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) {
|
||||
return handler.apply(this,args);
|
||||
},
|
||||
|
||||
async requestMultipart(method, path, file) {
|
||||
calls.push({ gateway: 'overseas', method, path })
|
||||
const url = new URL(path, 'https://fixture.invalid'), number = url.pathname.split('/records/')[1].split('/')[0]
|
||||
const record = careRows.get(number), key = url.searchParams.get('idempotencyKey')
|
||||
assert.equal(file.type, 'image/png')
|
||||
if (uploadKeys.has(key)) return ok(structuredClone(record))
|
||||
assert.equal(Number(url.searchParams.get('recordVersion')), record.recordVersion)
|
||||
record.recordVersion++; record.files.push({ fileId: '990000000000000001' }); uploadKeys.set(key, true)
|
||||
return ok(structuredClone(record))
|
||||
},
|
||||
async requestBinary(method, path) {
|
||||
calls.push({ gateway: 'overseas-binary', method, path })
|
||||
assert.equal(method, 'GET')
|
||||
if (path.includes('/fleet/vehicles/')) return { http: 200, contentType: 'image/png', bytes: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') }
|
||||
assert.match(path, /\/(?:overseas-quotes|exchange-rates|vehicles)\/export\?/)
|
||||
let csv = ''
|
||||
if (path.includes('/overseas-quotes/')) csv = `报价ID,报价名称,合作仓编码,合作仓名称,报价方,币种,生效开始,生效结束,版本号,状态,更新时间\r\n${state.quote.summary.id},${state.quote.summary.name},${state.quote.summary.warehouseCode},${state.quote.summary.warehouseName},${state.quote.summary.providerName},USD,2098-01-01,2098-12-31,V1,ACTIVE,2026-08-21\r\n`
|
||||
else if (path.includes('/exchange-rates/')) csv = `月份,基准币种,报价币种,月平均汇率,状态,更新人,更新时间\r\n${state.exchange.month},USD,CNY,${state.exchange.usdToCnyAverage},${state.exchange.status},admin,2026-08-21\r\n`
|
||||
else csv = `车辆ID,车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,更新时间\r\n${state.vehicle.id},${state.vehicle.plateNumber},${state.vehicle.model},18.75,101,Carrier,123,OVERSEAS,OWNED,${state.vehicle.status},2026-08-21\r\n`
|
||||
else csv = `车牌号,车型,油耗(L/100km),当前公里码(km),司机或外派单位,联系电话,区域,车辆归属,状态,最大托位,最大载重(kg),可用容积(m³),更新时间\r\n${state.vehicle.plateNumber},${state.vehicle.model},18.75,101,Carrier,123,OVERSEAS,OWNED,${state.vehicle.status},${state.vehicle.maxPalletSpaces},${state.vehicle.maxLoadKg},${state.vehicle.maxVolumeCbm},2026-08-21\r\n`
|
||||
return { method, path, http: 200, body: null, bytes: new TextEncoder().encode(`\uFEFF${csv}`),
|
||||
contentType: 'text/csv;charset=UTF-8', requestId: 'req-csv' }
|
||||
},
|
||||
@@ -209,7 +261,7 @@ function fakeContext({ softAssertions = false, refreshEmpty = false } = {}) {
|
||||
return { context, state }
|
||||
}
|
||||
|
||||
test('静态锁定真实双Gateway生命周期、CSV与安全边界', () => {
|
||||
test('静态锁定统一Gateway主数据生命周期、CSV与安全边界', () => {
|
||||
assert.match(source, /context\.request\('POST', DOMESTIC_BASE/)
|
||||
assert.match(source, /warehouseType: 'OVERSEAS_PARTNER'/)
|
||||
assert.match(source, /context\.request\('POST'/)
|
||||
@@ -233,6 +285,22 @@ test('静态锁定真实双Gateway生命周期、CSV与安全边界', () => {
|
||||
assert.doesNotMatch(source, /Idempotency-Key/)
|
||||
})
|
||||
|
||||
test('API dropping updated vehicle capacity fails even with a correct CSV and cleans up the actor', async () => {
|
||||
const { context } = fakeContext({ softAssertions: true })
|
||||
const request = context.request.bind(context)
|
||||
context.request = async (...args) => {
|
||||
const result = await request(...args)
|
||||
if (args[0] === 'PUT' && /\/vehicles\/\d+$/.test(args[1]) && result.body?.code === '200') {
|
||||
return { ...result, body: { ...result.body, data: { ...result.body.data, maxLoadKg: 18000 } } }
|
||||
}
|
||||
return result
|
||||
}
|
||||
await run(context)
|
||||
assert.equal(context.steps.find(step => step.name === '车辆编辑保持三项实际容量')?.passed, false)
|
||||
assert.equal(context.steps.find(step => step.name === '导出marker车辆CSV表头与marker内容')?.passed, true)
|
||||
assert.equal(context.createdResources.phase4OverseasMasterData.vehicle.finalStatus, 'INACTIVE')
|
||||
})
|
||||
|
||||
test('marker资源完成创建、详情、编辑、冲突、状态、CSV并全部停用', async () => {
|
||||
const { context, state } = fakeContext()
|
||||
await run(context)
|
||||
@@ -253,9 +321,11 @@ test('marker资源完成创建、详情、编辑、冲突、状态、CSV并全
|
||||
assert.equal(state.role.status, 'DISABLED')
|
||||
assert.equal(state.user.status, 'DISABLED')
|
||||
assert.equal(state.optionReads, 4)
|
||||
assert.equal(context.calls.filter((call) => call.gateway === 'overseas-binary').length, 3)
|
||||
assert.equal(context.calls.filter((call) => call.gateway === 'overseas-binary' && call.path.includes('/export?')).length, 3)
|
||||
assert.equal(resource.vehicle.care.status, 'VERIFIED')
|
||||
assert.equal(state.spareVehicle.status, 'INACTIVE')
|
||||
assert.equal(context.calls.filter((call) => call.method === 'DELETE').length, 0)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'PUT').length, 9)
|
||||
assert.equal(context.calls.filter((call) => call.method === 'PUT' && !call.path.includes('/fleet/vehicles/')).length, 9)
|
||||
assert.equal(context.calls.filter((call) => call.tokenKind === 'denied').length, 1)
|
||||
assert.equal(context.coverageGaps.length, 0)
|
||||
})
|
||||
@@ -273,6 +343,24 @@ test('创建前空候选不形成假PASS,刷新后本轮合作仓进入报价
|
||||
assert.equal(context.coverageGaps.length, 0)
|
||||
})
|
||||
|
||||
test('运维记录跨车泄露会阻断并保留证据,仍停用隔离用测试车辆', async () => {
|
||||
const { context, state } = fakeContext({ softAssertions: true, careLeak: true })
|
||||
await run(context)
|
||||
const care = context.createdResources.phase4OverseasMasterData.vehicle.care
|
||||
assert.equal(care.status, 'CHECKING')
|
||||
assert.equal(care.records.length, 6)
|
||||
assert.equal(care.otherVehicleStatus, 'INACTIVE')
|
||||
assert.equal(state.spareVehicle.status, 'INACTIVE')
|
||||
assert.ok(context.steps.some(step => step.name === '其他车辆不能读取或修改本车记录和照片' && !step.passed && step.deploymentBlocker))
|
||||
})
|
||||
|
||||
test('事件处理完成被错误映射为已付款时,真实验收编排必须阻断', async () => {
|
||||
const { context } = fakeContext({ softAssertions: true, processingMarksPaid: true })
|
||||
await run(context)
|
||||
assert.equal(context.createdResources.phase4OverseasMasterData.vehicle.care.status, 'CHECKING')
|
||||
assert.ok(context.steps.some(step => step.name === '处理完成保持独立的费用及付款事实' && !step.passed && step.deploymentBlocker))
|
||||
})
|
||||
|
||||
test('refresh后仍为空时报价前置明确deployment blocker且不产生coverageGap假PASS', async () => {
|
||||
const { context, state } = fakeContext({ softAssertions: true, refreshEmpty: true })
|
||||
await run(context)
|
||||
|
||||
Reference in New Issue
Block a user