test(fleet): verify vehicle care lifecycle independently
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# Admin 测试环境 HTTP 模块
|
||||
|
||||
`fleet-vehicle-care.mjs` 是可选车辆运维专项,通过正式入口 `admin-test-env-acceptance.mjs --modules fleet-vehicle-care --output <报告路径>` 执行,不单独运行。仅用于获授权 TEST,凭据由调用者显式 `node --env-file=<指定环境文件>` 提供;复用总入口 gateway/username/password 参数。经正式 API 创建本轮专用车辆,再复用 `vehicle-care.mjs` 验证六类记录、版本、更正历史、照片幂等及跨车隔离;不依赖订单、库存、报价或历史样本。写入仅涉及测试车辆、运维记录和图片;成功/失败均保留报告 `createdResources.fleetVehicleCare` 和样本,finally 读取最新版本停用本轮主车辆,内部助手停用隔离车辆,不删除记录或文件。失败重跑创建新样本,不修改原失败证据;不执行 SQL 或部署。该场景不证明费用已经入账或付款。
|
||||
|
||||
`fleet-cross-customer-fixture.mjs` 导出 `createFleetCrossCustomerFixture`,由上述专用 TEST 入口显式调用。默认只返回准备契约;执行时重新核对指定实际收货仓和FBA目的仓,通过正式客户资质、普通转运来货、PDA一托一箱测量/绑定/实收、客户确认及预排接口生成两套独立本轮货物,再共用一个真实车次并各自转派送,停在尚未拣货。客户上下文独立且不覆盖外层客户环境,退出本函数创建的客户会话,PDA启停归外层入口;无外接预订循环依赖、业务SQL、部署或自动恢复。`fleet-cross-customer-fixture-contract.test.mjs` 只证明生成编排及拒绝边界,真实运行另行留证。
|
||||
|
||||
`fleet-cross-customer-acceptance.mjs` 是跨客户共享车次的内部后半链验收函数,不是独立入口。默认调用只返回 fixture 契约、零网络;显式 `execute:true` 需要同批次两个独立客户、同仓各一托一箱已实收并分别预排/转派送、关联同一合格车次且未拣货,以及调用方提供的完整库存只读取证器。逐客户审核和拣货,验证第二单未就绪时整车拒绝且全部快照不变,再验证统一发车、原键重放和分别签收。前置门禁证据与事务中途故障回滚严格区分,后者保持 `NOT_TESTED`。失败保留当前样本与原请求,不自动续跑、补造或清理。对应合同 `fleet-cross-customer-acceptance-contract.test.mjs` 仅证明编排与反例断言,不能代替真实运行。临时账号生命周期、最小来货生成及报告保存由外层显式编排负责。
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import { verifyVehicleCare } from './vehicle-care.mjs'
|
||||
|
||||
const MODULE = 'fleet-vehicle-care'
|
||||
const BASE = '/api/platform/v1/last-leg/master-data/vehicles'
|
||||
const OPTIONS = { scenario: 'vehicle-care-lifecycle', deploymentBlocker: true }
|
||||
|
||||
/** Fresh TEST fixtures only. No dispatch, inventory or financial writes. */
|
||||
export async function run(context) {
|
||||
const proof = { status: 'CHECKING', care: {}, cleanup: 'PENDING' }
|
||||
context.createdResources.fleetVehicleCare = proof
|
||||
async function command(method, path, body, label) {
|
||||
const response = await context.request(method, path, body)
|
||||
const data = context.apiSuccess(MODULE, label, response, '', OPTIONS)
|
||||
if (response?.http !== 200 || String(response?.body?.code) !== '200' || data == null) throw new Error(label)
|
||||
return data
|
||||
}
|
||||
let failure
|
||||
try {
|
||||
const vehicle = await command('POST', BASE, {
|
||||
plateNumber: `VC-${randomUUID().slice(0, 12)}`, model: 'Vehicle care TEST',
|
||||
fuelConsumptionPer100Km: 10, mileageKm: 100, region: 'OVERSEAS', ownership: 'OWNED',
|
||||
status: 'IN_SERVICE', remark: context.marker,
|
||||
}, '建立本轮车辆运维专用档案')
|
||||
proof.vehicleId = String(vehicle.id)
|
||||
proof.plateNumber = vehicle.plateNumber
|
||||
await verifyVehicleCare(context, MODULE, vehicle, proof.care)
|
||||
proof.status = 'VERIFIED'
|
||||
} catch (error) {
|
||||
failure = error
|
||||
proof.status = 'FAILED'
|
||||
proof.error = error instanceof Error ? error.message : String(error)
|
||||
} finally {
|
||||
if (proof.vehicleId) {
|
||||
try {
|
||||
const path = `${BASE}/${encodeURIComponent(proof.vehicleId)}`
|
||||
const current = await command('GET', path, undefined, '结束前读取本轮车辆当前版本')
|
||||
const ended = current.status === 'INACTIVE' ? current : await command('PATCH', `${path}/status`, {
|
||||
recordVersion: current.recordVersion, status: 'INACTIVE',
|
||||
}, '停用本轮运维专用车辆并保留证据')
|
||||
if (ended.status !== 'INACTIVE') throw new Error('本轮运维车辆未停用')
|
||||
proof.cleanup = 'INACTIVE'
|
||||
} catch (error) {
|
||||
proof.cleanup = 'FAILED'
|
||||
proof.cleanupError = error instanceof Error ? error.message : String(error)
|
||||
proof.status = 'FAILED'
|
||||
failure ??= error
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failure) throw failure
|
||||
}
|
||||
@@ -2,6 +2,43 @@ 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'
|
||||
|
||||
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 }
|
||||
|
||||
@@ -56,19 +56,23 @@ export async function verifyVehicleCare(context, module, vehicle, proof) {
|
||||
fuelQuantity: null, fuelUnit: null, verificationStatus: null, remark: context.marker,
|
||||
incident: { description: `${context.marker} 受控故障处置记录`, item: priced ? '更换轮胎' : null, place: priced ? '本轮测试作业点' : null,
|
||||
handler: null, processingStatus: 'PENDING', exceptionType: type === 'EXCEPTION' ? 'VEHICLE_BREAKDOWN' : null,
|
||||
reportedCost: priced ? 0 : null, currency: priced ? 'USD' : null } }
|
||||
reportedCost: type === 'REPAIR' ? 125.5 : priced ? 0 : null, currency: priced ? 'USD' : null } }
|
||||
const initial = { idempotencyKey: `${context.marker}:${type}`, details }
|
||||
let incident = await command('POST', base, initial, `创建本轮${type}记录`)
|
||||
proof.records.push({ recordNo: incident.recordNo, createRequest: initial })
|
||||
check('事件项目地点和分类完整回显', null, incident.vehicleId === id && incident.details?.incident?.item === details.incident.item
|
||||
&& incident.details.incident.place === details.incident.place && incident.details.incident.exceptionType === details.incident.exceptionType)
|
||||
check('事件项目地点和分类完整回显', null, incident.vehicleId === id && (incident.details?.incident?.item ?? null) === details.incident.item
|
||||
&& (incident.details.incident.place ?? null) === details.incident.place && (incident.details.incident.exceptionType ?? null) === details.incident.exceptionType)
|
||||
incident = await command('PUT', `${base}/${incident.recordNo}`, { recordVersion: Number(incident.recordVersion), reason: '受控任务实际处理完成',
|
||||
details: { ...details, incident: { ...details.incident, handler: '本轮测试处理人', processingStatus: 'COMPLETED' } } }, `完成本轮${type}处理记录`)
|
||||
check('处理完成保持独立的费用及付款事实', null, incident.details.incident.processingStatus === 'COMPLETED'
|
||||
&& (!priced || (Number(incident.details.incident.reportedCost) === 0 && incident.paymentStatus === 'UNLINKED')))
|
||||
&& (!priced || (Number(incident.details.incident.reportedCost) === details.incident.reportedCost && incident.paymentStatus === 'UNLINKED')))
|
||||
const refreshed = await command('GET', `${base}/${incident.recordNo}`, undefined, `重新读取本轮${type}记录`)
|
||||
check('处理记录重新读取保持全部现场字段', null, refreshed.details.incident.item === details.incident.item
|
||||
&& refreshed.details.incident.place === details.incident.place && refreshed.details.incident.handler === '本轮测试处理人')
|
||||
check('处理记录重新读取保持全部现场字段', null, (refreshed.details.incident.item ?? null) === details.incident.item
|
||||
&& (refreshed.details.incident.place ?? null) === details.incident.place && refreshed.details.incident.handler === '本轮测试处理人')
|
||||
const changes = await command('GET', `${base}/${incident.recordNo}/history?page=1&size=20`, undefined, `核对本轮${type}处理历史`)
|
||||
check('处理历史保留原待处理事实及完成结果', null, changes.records?.length === 2
|
||||
&& changes.records[0].snapshot?.details?.incident?.processingStatus === 'COMPLETED'
|
||||
&& changes.records[1].snapshot?.details?.incident?.processingStatus === 'PENDING')
|
||||
}
|
||||
let other
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user