Files
baiyue-firstleg/scripts/admin-test-env-modules/phase4-overseas-master-data-contract.test.mjs
T

466 lines
27 KiB
JavaScript
Raw Normal View History

2026-10-08 17:07:57 +08:00
import { exportWorkbook } from './download-center-fixture.mjs'
2026-08-21 10:38:28 +08:00
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { run, vehicleCapacityMatches } from './phase4-overseas-master-data.mjs'
import { run as runCare } from './fleet-vehicle-care.mjs'
2026-10-21 15:53:33 +08:00
test('平台用户手机号已存在只换号重试,未知创建结果不得重发', async () => {
for (const collision of [true, false]) {
const { context } = fakeContext({ softAssertions: true })
const original = context.request.bind(context)
const phones = []
let sequence = 0
context.findAvailablePhone = async () => `1380000${String(++sequence).padStart(4, '0')}`
context.request = async (method, path, body, ...rest) => {
if (method === 'POST' && path === '/api/platform/v1/iam/users') {
phones.push(body.phone)
if (phones.length === 1) return collision
? {http:200,body:{code:'A0409',message:'手机号已存在'}}
: {http:500,body:{code:'S0500'}}
}
return original(method,path,body,...rest)
}
await run(context)
assert.equal(phones.length, collision ? 2 : 1)
if (collision) assert.notEqual(phones[0],phones[1])
}
})
test('运维专项接受NON_NULL真实响应但仍完成全部六类记录和两车停用', async () => {
const { context, state } = fakeContext()
const request = context.request.bind(context)
context.request = async (...args) => JSON.parse(JSON.stringify(await request(...args), (_key, value) => value === null ? undefined : value))
await runCare(context)
assert.equal(context.createdResources.fleetVehicleCare.status, 'VERIFIED')
assert.equal(context.createdResources.fleetVehicleCare.care.records.length, 6)
assert.equal(context.createdResources.fleetVehicleCare.cleanup, 'INACTIVE')
assert.equal(state.vehicle.status, 'INACTIVE')
assert.equal(state.spareVehicle.status, 'INACTIVE')
})
test('省略实际维修项目仍失败,失败后停用主车辆并保留报告', async () => {
const { context, state } = fakeContext({ softAssertions: true })
const request = context.request.bind(context)
context.request = async (...args) => {
const response = await request(...args)
if (response.body?.data?.details?.recordType === 'REPAIR') delete response.body.data.details.incident.item
return response
}
await assert.rejects(runCare(context), /事件项目地点和分类完整回显/)
assert.equal(context.createdResources.fleetVehicleCare.status, 'FAILED')
assert.equal(context.createdResources.fleetVehicleCare.cleanup, 'INACTIVE')
assert.equal(state.vehicle.status, 'INACTIVE')
})
test('运维专项停用失败不得标记整个运行通过', async () => {
const { context } = fakeContext()
const request = context.request.bind(context)
context.request = async (...args) => args[0] === 'PATCH' && args[1].endsWith('/930000000000000001/status')
? { http: 503, body: { code: 'S0503' } } : request(...args)
await assert.rejects(runCare(context))
assert.equal(context.createdResources.fleetVehicleCare.status, 'FAILED')
assert.equal(context.createdResources.fleetVehicleCare.cleanup, 'FAILED')
})
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}`)
}
})
2026-08-21 10:38:28 +08:00
const source = readFileSync(new URL('./phase4-overseas-master-data.mjs', import.meta.url), 'utf8')
2026-08-24 19:58:35 +08:00
const controllerSource = [
'../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteController.java',
'../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateController.java',
2026-09-01 11:02:10 +08:00
'../../lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleController.java',
2026-08-24 19:58:35 +08:00
].map((path) => readFileSync(new URL(path, import.meta.url), 'utf8')).join('\n')
2026-08-21 10:38:28 +08:00
const DOMESTIC_BASE = '/api/platform/v1/warehouse-admin'
const OVERSEAS_BASE = '/api/platform/v1/last-leg/master-data'
const ok = (data) => ({ http: 200, body: { code: '200', message: 'success', data }, requestId: 'req-p4', traceId: 'trace-p4' })
const conflict = () => ({ http: 200, body: { code: 'A0409', message: 'record version conflict', data: null }, requestId: 'req-conflict' })
2026-08-24 19:58:35 +08:00
const forbidden = () => ({ http: 403, body: { code: 'A0403', message: 'Access denied', data: null }, requestId: 'req-forbidden' })
2026-08-21 10:38:28 +08:00
function detailQuote(state) {
return {
summary: { ...state.summary },
warehouseFees: state.warehouseFees.map((item) => ({ ...item })),
transitPrices: state.transitPrices.map((item) => ({ ...item })),
createdBy: 'admin', createdAt: '2026-08-21T00:00:00',
}
}
function vehicleWire(state) {
return { ...state.vehicle, mileageKm: String(state.vehicle.mileageKm), recordVersion: String(state.vehicle.recordVersion) }
}
function fakeContext({ softAssertions = false, refreshEmpty = false, careLeak = false, processingMarksPaid = false } = {}) {
2026-08-21 10:38:28 +08:00
const calls = []
const coverageGaps = []
2026-08-24 19:58:35 +08:00
const steps = []
2026-08-21 10:38:28 +08:00
const marker = 'ADMIN-API-20260821123456789'
2026-08-24 19:58:35 +08:00
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()
2026-08-21 10:38:28 +08:00
const context = {
2026-08-24 19:58:35 +08:00
marker, calls, coverageGaps, steps, createdResources: {},
async findAvailablePhone() { return '13800138000' },
2026-08-21 10:38:28 +08:00
apiSuccess(_module, name, response) {
assert.equal(response?.http, 200, `${name}: HTTP`)
assert.equal(String(response?.body?.code), '200', `${name}: code`)
return response.body.data
},
2026-08-24 19:58:35 +08:00
assert(_module, name, _response, condition, evidence, options = {}) {
const passed = Boolean(condition)
steps.push({ name, passed, deploymentBlocker: Boolean(options.deploymentBlocker && !passed), evidence })
if (!passed && !softAssertions) assert.equal(passed, true, `${name}: ${evidence}`)
return passed
2026-08-21 10:38:28 +08:00
},
expectConflict(_module, name, response) {
assert.equal(response.http, 200, `${name}: HTTP`)
assert.ok(['A0409', '409'].includes(String(response.body?.code)), `${name}: code=${response.body?.code}`)
return true
},
addCoverageGap(module, scenario, name, reason, category, metadata = {}) {
coverageGaps.push({ module, scenario, name, reason, category, ...metadata })
},
async request(...args) {
2026-10-08 17:07:57 +08:00
if (args[1].startsWith('/api/platform/v1/files/export-tasks')) {
calls.push({ gateway: 'domestic', method: args[0], path: args[1], body: args[2] })
return ok({ id: args[2]?.exportType || args[1].split('/').at(-1), status: args[0] === 'POST' ? 'PENDING' : 'SUCCEEDED' })
}
const route = /\/(?:last-leg|overseas-warehouse-resources)(?:\/|\?|$)/.test(String(args[1]));
const handler = route ? async function(method,path,body,token = 'admin-overseas-token',extraHeaders = {}) {
2026-08-24 19:58:35 +08:00
calls.push({ gateway: 'overseas', method, path, body, extraHeaders, tokenKind: token === 'deny-overseas-token' ? 'denied' : 'admin' })
2026-08-21 10:38:28 +08:00
assert.notEqual(method, 'DELETE')
2026-08-24 19:58:35 +08:00
if (token === 'deny-overseas-token') return forbidden()
if (path.includes('/fleet/vehicles/')) {
2026-10-08 17:07:57 +08:00
if (method === 'POST' && path.endsWith('/records/finance-status')) {
return ok(body.recordNos.map(recordNo => ({ recordNo, status: processingMarksPaid ? 'PAID' : 'UNLINKED',
sourceVersion: null, submittedRecordVersion: null, finance: null })))
}
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,
2026-10-08 17:07:57 +08:00
paymentStatus: 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++
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 })
}
2026-08-21 10:38:28 +08:00
if (method === 'GET' && path === `${OVERSEAS_BASE}/overseas-quotes/warehouse-options`) {
2026-08-24 19:58:35 +08:00
state.optionReads += 1
if (!state.warehouse) return ok([])
return ok([{ warehouseId: '910000000000000001', warehouseCode: state.warehouse.warehouseCode,
warehouseName: state.warehouse.warehouseName, countryCode: 'US' }])
}
if (method === 'GET' && path === `${OVERSEAS_BASE}/overseas-quotes/warehouse-options?refresh=true`) {
state.optionReads += 1
if (refreshEmpty) return ok([])
2026-08-21 10:38:28 +08:00
return ok([{ warehouseId: '910000000000000001', warehouseCode: state.warehouse.warehouseCode,
warehouseName: state.warehouse.warehouseName, countryCode: 'US' }])
}
if (method === 'POST' && path === `${OVERSEAS_BASE}/overseas-quotes`) {
state.quote = {
summary: { id: '920000000000000001', ...body, warehouseCode: state.warehouse.warehouseCode,
warehouseName: state.warehouse.warehouseName, countryCode: 'US', status: 'INACTIVE', recordVersion: 0 },
warehouseFees: body.warehouseFees, transitPrices: body.transitPrices || [],
}
return ok(detailQuote(state.quote))
}
2026-08-24 19:58:35 +08:00
if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/overseas-quotes?page=1&size=20&keyword=`)) {
return ok({ records: state.quote ? [{ ...state.quote.summary }] : [], page: 1, size: 20,
total: state.quote ? 1 : 0, pages: state.quote ? 1 : 0 })
}
2026-08-21 10:38:28 +08:00
if (path === `${OVERSEAS_BASE}/overseas-quotes/920000000000000001`) {
if (method === 'GET') return ok(detailQuote(state.quote))
if (method === 'PUT') {
if (Number(body.recordVersion) !== state.quote.summary.recordVersion) return conflict()
state.quote.summary = { ...state.quote.summary, name: body.name, providerName: body.providerName,
currency: body.currency, effectiveStart: body.effectiveStart, effectiveEnd: body.effectiveEnd,
remark: body.remark, recordVersion: state.quote.summary.recordVersion + 1 }
state.quote.warehouseFees = body.warehouseFees
state.quote.transitPrices = body.transitPrices || []
return ok(detailQuote(state.quote))
}
}
if (method === 'PATCH' && path === `${OVERSEAS_BASE}/overseas-quotes/920000000000000001/status`) {
assert.equal(Number(body.recordVersion), state.quote.summary.recordVersion)
state.quote.summary.status = body.status
state.quote.summary.recordVersion += 1
return ok(detailQuote(state.quote))
}
if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/exchange-rates?page=1&size=20&monthFrom=`)) {
2026-08-24 19:58:35 +08:00
const matches = state.exchange && path.includes(`monthFrom=${state.exchange.month}`)
&& (!path.includes('status=') || path.includes(`status=${state.exchange.status}`))
return ok({ records: matches ? [{ ...state.exchange }] : [], page: 1, size: 20,
total: matches ? 1 : 0, pages: matches ? 1 : 0 })
2026-08-21 10:38:28 +08:00
}
if (method === 'POST' && path === `${OVERSEAS_BASE}/exchange-rates`) {
state.exchange = { ...body, baseCurrency: 'USD', quoteCurrency: 'CNY', status: 'ACTIVE', recordVersion: 0 }
return ok({ ...state.exchange })
}
if (state.exchange && path === `${OVERSEAS_BASE}/exchange-rates/${state.exchange.month}`) {
if (method === 'GET') return ok({ ...state.exchange })
if (method === 'PUT') {
if (Number(body.recordVersion) !== state.exchange.recordVersion) return conflict()
state.exchange = { ...state.exchange, usdToCnyAverage: body.usdToCnyAverage,
remark: body.remark, recordVersion: state.exchange.recordVersion + 1 }
return ok({ ...state.exchange })
}
}
if (state.exchange && method === 'PATCH' && path === `${OVERSEAS_BASE}/exchange-rates/${state.exchange.month}/status`) {
assert.equal(Number(body.recordVersion), state.exchange.recordVersion)
state.exchange = { ...state.exchange, status: body.status, remark: body.remark ?? state.exchange.remark,
recordVersion: state.exchange.recordVersion + 1 }
return ok({ ...state.exchange })
}
if (method === 'POST' && path === `${OVERSEAS_BASE}/vehicles`) {
state.vehicle = { id: '930000000000000001', ...body, recordVersion: 0 }
return ok(vehicleWire(state))
}
if (path === `${OVERSEAS_BASE}/vehicles/930000000000000001`) {
if (method === 'GET') return ok(vehicleWire(state))
if (method === 'PUT') {
if (Number(body.recordVersion) !== state.vehicle.recordVersion) return conflict()
state.vehicle = { ...state.vehicle, ...body, recordVersion: state.vehicle.recordVersion + 1,
status: state.vehicle.status }
return ok(vehicleWire(state))
}
}
if (method === 'PATCH' && path === `${OVERSEAS_BASE}/vehicles/930000000000000001/status`) {
assert.equal(Number(body.recordVersion), state.vehicle.recordVersion)
state.vehicle = { ...state.vehicle, status: body.status, recordVersion: state.vehicle.recordVersion + 1 }
return ok(vehicleWire(state))
}
2026-08-24 19:58:35 +08:00
if (method === 'GET' && path.startsWith(`${OVERSEAS_BASE}/vehicles?page=1&size=20&keyword=`)) {
return ok({ records: state.vehicle ? [vehicleWire(state)] : [], page: 1, size: 20,
total: state.vehicle ? 1 : 0, pages: state.vehicle ? 1 : 0 })
}
2026-08-21 10:38:28 +08:00
assert.fail(`unexpected overseas call ${method} ${path}`)
} : async function(method,path,body) {
calls.push({ gateway: 'domestic', method, path, body })
assert.notEqual(method, 'DELETE')
if (method === 'POST' && path === DOMESTIC_BASE) {
state.warehouse = { ...body, id: '910000000000000001' }
return ok(state.warehouse.id)
}
if (method === 'GET' && path === `${DOMESTIC_BASE}/910000000000000001?tenantId=0`) {
return ok({ warehouse: { ...state.warehouse }, addresses: [state.warehouse.address], capabilities: state.warehouse.capabilities })
}
if (method === 'PUT' && path === `${DOMESTIC_BASE}/910000000000000001?tenantId=0`) {
state.warehouse = { ...body, id: state.warehouse.id }
return ok(null)
}
if (method === 'PATCH' && path === `${DOMESTIC_BASE}/910000000000000001/status?tenantId=0&status=INACTIVE`) {
state.warehouse.status = 'INACTIVE'
return ok(null)
}
if (method === 'POST' && path === '/api/platform/v1/iam/roles') {
state.role = { id: '940000000000000001', ...body, status: 'ACTIVE' }
return ok({ ...state.role })
}
if (method === 'POST' && path === '/api/platform/v1/iam/users') {
state.user = { membershipId: '950000000000000001', ...body, status: 'ACTIVE' }
return ok({ ...state.user })
}
if (method === 'POST' && path === '/api/auth/login') {
return ok({ accessToken: 'deny-overseas-token', expiresIn: 900 })
}
if (method === 'PUT' && path === '/api/platform/v1/iam/users/950000000000000001/status') {
state.user.status = body.status
return ok(null)
}
if (method === 'PUT' && path === '/api/platform/v1/iam/roles/940000000000000001/status') {
state.role.status = body.status
return ok(null)
}
assert.fail(`unexpected domestic call ${method} ${path}`)
};
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) {
2026-08-21 10:38:28 +08:00
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') }
2026-10-08 17:07:57 +08:00
assert.match(path, /files\/export-tasks\/[^/]+\/download$/)
let headers, values
if (path.includes('OVERSEAS_QUOTE_LIST')) {
headers = ['报价名称', '合作仓编码', '合作仓名称', '报价方']
values = [state.quote.summary.name, state.quote.summary.warehouseCode, state.quote.summary.warehouseName, state.quote.summary.providerName]
} else if (path.includes('MONTHLY_EXCHANGE_RATE_LIST')) {
headers = ['月份', '月平均汇率', '状态']
values = [state.exchange.month, state.exchange.usdToCnyAverage, state.exchange.status]
} else {
headers = ['车牌号', '最大托位', '最大载重(kg)', '可用容积(m³)', '状态']
values = [state.vehicle.plateNumber, state.vehicle.maxPalletSpaces, state.vehicle.maxLoadKg, state.vehicle.maxVolumeCbm, state.vehicle.status]
}
return { method, path, http: 200, bytes: exportWorkbook(headers, values),
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }
2026-08-21 10:38:28 +08:00
},
}
return { context, state }
}
test('静态锁定统一Gateway主数据生命周期、CSV与安全边界', () => {
2026-08-21 10:38:28 +08:00
assert.match(source, /context\.request\('POST', DOMESTIC_BASE/)
assert.match(source, /warehouseType: 'OVERSEAS_PARTNER'/)
assert.match(source, /context\.request\('POST'/)
assert.match(source, /context\.request\('PUT'/)
assert.match(source, /context\.request\('PATCH'/)
2026-10-08 17:07:57 +08:00
assert.match(source, /verifyDownloadCenterExport\(context/)
2026-08-21 10:38:28 +08:00
assert.match(source, /context\.expectConflict/)
2026-08-24 19:58:35 +08:00
assert.match(source, /warehouse-options\?refresh=true/)
assert.match(source, /空候选不负缓存30分钟/)
assert.match(source, /30分钟正缓存与刷新快照一致/)
assert.match(source, /无quote:list权限访问海外报价被拒绝/)
2026-10-08 17:07:57 +08:00
assert.match(source, /verifyDownloadCenterExport/)
2026-08-21 10:38:28 +08:00
assert.match(source, /finally\s*\{/)
assert.match(source, /context\.createdResources\.phase4OverseasMasterData/)
2026-08-24 19:58:35 +08:00
assert.doesNotMatch(source, /context\.addCoverageGap/)
2026-08-21 10:38:28 +08:00
assert.match(source, /2090 \+ Math\.floor/)
assert.doesNotMatch(source, /\/internal\/|overseas-partners|https?:\/\/|fetch\(|process\.env|request(?:Overseas)?\('DELETE'/)
2026-10-08 17:07:57 +08:00
for (const endpoint of ['overseas-quotes/warehouse-options', 'OVERSEAS_QUOTE_LIST',
'MONTHLY_EXCHANGE_RATE_LIST', 'VEHICLE_LIST']) assert.ok(source.includes(endpoint))
2026-08-24 19:58:35 +08:00
assert.doesNotMatch(controllerSource, /Idempotency-Key|IdempotencyKey/)
assert.doesNotMatch(source, /Idempotency-Key/)
2026-08-21 10:38:28 +08:00
})
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)
2026-10-08 17:07:57 +08:00
assert.equal(context.steps.find(step => step.name === 'VEHICLE_LIST表头与marker内容')?.passed, true)
assert.equal(context.createdResources.phase4OverseasMasterData.vehicle.finalStatus, 'INACTIVE')
})
2026-08-21 10:38:28 +08:00
test('marker资源完成创建、详情、编辑、冲突、状态、CSV并全部停用', async () => {
const { context, state } = fakeContext()
await run(context)
const resource = context.createdResources.phase4OverseasMasterData
assert.equal(resource.partnerWarehouse.id, '910000000000000001')
assert.equal(resource.partnerWarehouse.finalStatus, 'INACTIVE')
assert.equal(resource.quote.id, '920000000000000001')
assert.equal(resource.quote.finalStatus, 'INACTIVE')
assert.match(resource.exchangeRate.month, /^209\d-(0[1-9]|1[0-2])$/)
assert.equal(resource.exchangeRate.finalStatus, 'INACTIVE')
assert.equal(resource.vehicle.id, '930000000000000001')
assert.equal(resource.vehicle.finalStatus, 'INACTIVE')
assert.equal(typeof resource.vehicle.recordVersion, 'number')
assert.equal(state.warehouse.status, 'INACTIVE')
assert.equal(state.quote.summary.status, 'INACTIVE')
assert.equal(state.exchange.status, 'INACTIVE')
assert.equal(state.vehicle.status, 'INACTIVE')
2026-08-24 19:58:35 +08:00
assert.equal(state.role.status, 'DISABLED')
assert.equal(state.user.status, 'DISABLED')
assert.equal(state.optionReads, 4)
2026-10-08 17:07:57 +08:00
assert.equal(context.calls.filter((call) => call.gateway === 'overseas-binary' && call.path.includes('/export-tasks/') && call.path.endsWith('/download')).length, 3)
assert.equal(resource.vehicle.care.status, 'VERIFIED')
assert.equal(state.spareVehicle.status, 'INACTIVE')
2026-08-21 10:38:28 +08:00
assert.equal(context.calls.filter((call) => call.method === 'DELETE').length, 0)
assert.equal(context.calls.filter((call) => call.method === 'PUT' && !call.path.includes('/fleet/vehicles/')).length, 9)
2026-08-24 19:58:35 +08:00
assert.equal(context.calls.filter((call) => call.tokenKind === 'denied').length, 1)
assert.equal(context.coverageGaps.length, 0)
2026-08-21 10:38:28 +08:00
})
2026-08-24 19:58:35 +08:00
test('创建前空候选不形成假PASS,刷新后本轮合作仓进入报价完整生命周期', async () => {
const { context } = fakeContext()
await run(context)
const optionCalls = context.calls.filter((call) => call.path.includes('warehouse-options'))
assert.equal(optionCalls.length, 4)
assert.equal(optionCalls[0].path.endsWith('warehouse-options'), true)
assert.equal(optionCalls[1].path.endsWith('warehouse-options'), true)
assert.equal(optionCalls[2].path.endsWith('warehouse-options?refresh=true'), true)
assert.equal(optionCalls[3].path.endsWith('warehouse-options'), true)
assert.equal(context.createdResources.phase4OverseasMasterData.quote.id, '920000000000000001')
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')
2026-10-08 17:07:57 +08:00
assert.ok(context.steps.some(step => step.name === '处理完成未自动提交财务或标记付款' && !step.passed && step.deploymentBlocker))
})
test('UNLINKED付款响应夹带财务单据或提交版本仍阻断,不忽略独立查询证据', async () => {
for (const facts of [{ submittedRecordVersion: 1 }, { finance: { costNo: 'COST-UNEXPECTED' } }]) {
const { context } = fakeContext({ softAssertions: true })
const request = context.request.bind(context)
context.request = async (...args) => {
const response = await request(...args)
if (args[1].endsWith('/records/finance-status')) Object.assign(response.body.data[0], facts)
return response
}
await run(context)
assert.equal(context.createdResources.phase4OverseasMasterData.vehicle.care.status, 'CHECKING')
assert.ok(context.steps.some(step => step.name === '处理完成未自动提交财务或标记付款' && !step.passed && step.deploymentBlocker))
}
})
2026-08-24 19:58:35 +08:00
test('refresh后仍为空时报价前置明确deployment blocker且不产生coverageGap假PASS', async () => {
const { context, state } = fakeContext({ softAssertions: true, refreshEmpty: true })
await run(context)
assert.equal(state.quote, null)
2026-08-24 19:58:35 +08:00
assert.equal(state.warehouse.status, 'INACTIVE')
2026-08-21 10:38:28 +08:00
assert.equal(state.exchange.status, 'INACTIVE')
assert.equal(state.vehicle.status, 'INACTIVE')
2026-08-24 19:58:35 +08:00
assert.equal(context.coverageGaps.length, 0)
assert.equal(context.steps.some((step) => step.name === '刷新后新建合作仓立即可见'
&& !step.passed && step.deploymentBlocker), true)
assert.equal(context.steps.some((step) => step.name === '海外报价生命周期执行'
&& !step.passed && step.deploymentBlocker), true)
2026-08-21 10:38:28 +08:00
})