feat(fleet): attach scoped evidence to measured trip records
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
|
||||
`fleet-quotes-test-env.mjs` 是独立报价TEST验收入口:显式使用 `node --env-file=<用户指定目录的test-acceptance.env> scripts/fleet-quotes-test-env.mjs --ready-commit=a6fd0ba3`,仅在总控确认匹配服务健康后执行。使用独立登录和本轮客户/供应商/报价,不借其他任务会话;固定marker为`FLEET-QUOTE-20261003`,报告目录`reports/fleet-quote-live-20261003`独占。登录、客户前置和业务请求均由脱敏持久化日志包裹,发送前fsync原请求/幂等键与文件样本引用;结果未知停止业务,保留原样本,拒绝自动重跑/续跑/覆盖。结束退出独立会话,不执行SQL、部署或库存操作。该入口只证明报价范围,运营统计仍须本轮真实DEPART及独立预期,见模块README。
|
||||
|
||||
`fleet-company-cost-test-env.mjs` 是公司车辆成本独立 TEST 验收入口,复用 Admin HTTP 客户端但不展开其他业务模块或复用其会话/样本。须在脚本前显式 `node --env-file=<用户指定目录的test-acceptance.env>`,并在总控确认服务就绪后传入 `--ready-commit=a6fd0ba3`。固定 marker 为 `FLEET-COST-20261003`,报告为 `reports/fleet-cost-live-20261003/api-report.json`;既有报告拒绝覆盖重跑。只创建本任务供应商、车辆、维修/救援、公司费用与合成付款凭证;不操作库存、不创建客户或客户费用、不修改共享汇率。客户费用池只读检查本 marker 来源未误入;不借用其他客户进行成本明细或账户查询,不保存其他客户数据。请求前落盘原内容及幂等键,失败保留证据;结束退出本任务会话、停用本任务车辆,不删除数据。真实执行仍须本轮 TEST 授权及服务就绪;离线检查见 `admin-test-env-modules/fleet-company-cost-acceptance-contract.test.mjs`。
|
||||
`fleet-company-cost-test-env.mjs` 是公司车辆成本独立 TEST 验收入口,复用 Admin HTTP 客户端但不展开其他业务模块或复用其会话/样本。须在脚本前显式 `node --env-file=<用户指定目录的test-acceptance.env>`,并在总控确认服务就绪后传入 `--ready-commit=8700fc34238bc33339351c73c15f3af8de8ce517`。固定 marker 为 `FLEET-COST-20261003`,报告为 `reports/fleet-cost-live-20261003/api-report.json`;既有报告拒绝覆盖重跑。只创建本任务供应商、车辆、维修/救援、公司费用与合成付款凭证;不操作库存、不创建客户或客户费用、不修改共享汇率。客户费用池只读检查本 marker 来源未误入;不借用其他客户进行成本明细或账户查询,不保存其他客户数据。请求前落盘原内容及幂等键,失败保留证据;结束退出本任务会话、停用本任务车辆,不删除数据。真实执行仍须本轮 TEST 授权及服务就绪;离线检查见 `admin-test-env-modules/fleet-company-cost-acceptance-contract.test.mjs`。
|
||||
|
||||
`fee-void-recovery-test-env.mjs` 是费用作废确认响应丢失的 TEST 专项入口,默认 `--plan` 零网络。执行须显式传入 `--execute --environment TEST --run-id <新批次> --customer-id <测试客户ID> --control-customer-id <另一测试客户ID> --warehouse-id <海外仓ID>`,外部 TEST/Jenkins 配置及 `FBA_CORE_JENKINS_AGENT` 提供凭据与取证节点。创建一条新仓储服务应收,经提交审核和原发布确认后,对该来源版本的 VOIDED 事件注入 ACK 丢失;原事件 DEAD 后受保护重试,核对最终作废、同命令重放、另一客户费用与完整库存不变。不写业务SQL,不使用旧费用作废;故障窗口会临时重建同镜像 Lastleg,禁止并行发布或其他共享环境写入。报告在 `reports/fee-void-<新批次>/`,请求前保存意图,失败保留样本,不自动重跑或覆盖;finally 恢复原服务、停止代理并退出会话,恢复未证明则失败。`fee-void-recovery-test-env.test.mjs` 是离线编排与失败清理检查,不能代替真实环境结果。
|
||||
|
||||
|
||||
@@ -123,6 +123,15 @@ test('independent runner requires explicit service readiness and only the specif
|
||||
assert.throws(() => requireReady([`--ready-commit=${READY_COMMIT}`], [`--env-file=${env.replace('test-acceptance', 'dev-acceptance')}`]), /授权目录/)
|
||||
assert.ok(requireReady([`--ready-commit=${READY_COMMIT}`], [`--env-file=${env}`]).endsWith('test-acceptance.env'))
|
||||
})
|
||||
|
||||
test('readiness gate accepts only the full production anchor, not old anchors, shorthand or another commit', () => {
|
||||
const profile = ['--env-file=C:/Users/BY-OY/.codex/env/chongchong-global/test-acceptance.env']
|
||||
assert.equal(READY_COMMIT, '8700fc34238bc33339351c73c15f3af8de8ce517')
|
||||
for (const commit of ['a6fd0ba3', 'cb6c54ff', '8700fc34', 'd05ff1cd', `${READY_COMMIT}0`]) {
|
||||
assert.throws(() => requireReady([`--ready-commit=${commit}`], profile), /就绪/)
|
||||
}
|
||||
assert.ok(requireReady([`--ready-commit=${READY_COMMIT}`], profile).endsWith('test-acceptance.env'))
|
||||
})
|
||||
test('company cost verifier exercises CNY/USD, void/rebuild, payment replay and customer isolation without direct transport', async () => {
|
||||
const { context, calls, vehicles, records, costs } = fixture()
|
||||
await run(context, options)
|
||||
|
||||
@@ -8,7 +8,7 @@ function harness(fault) {
|
||||
const marker = 'FLEET-CURRENT-RUN', tripNo = 'TR-CURRENT', departedAt = '2026-10-03T01:00:00Z'
|
||||
const rows = new Map(), histories = new Map(), commands = new Map(), measuredKeys = new Map(), calls = []
|
||||
let next = 0, supplierStatus = 'ACTIVE'
|
||||
let measured = { tripNo, recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null } }
|
||||
let measured = { tripNo, recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null, evidenceFiles: [] } }
|
||||
const measuredHistory = []
|
||||
const expected = { physicalPallets: 1, palletSpaces: 2, ownPackages: 11, externalPackages: 4, externalAgreedAmounts: [{ currency: 'USD', amount: 0 }], costs: [] }
|
||||
const response = (data, code = '200', http = 200) => ({ http, body: { code, data: copy(data) }, requestId: 'offline-request', traceId: 'offline-trace' })
|
||||
@@ -75,7 +75,9 @@ function harness(fault) {
|
||||
const prior = measuredKeys.get(body.idempotencyKey)
|
||||
if (prior) { if (fault === 'measured replay adds history') measuredHistory.push(copy(measured)); return response(prior) }
|
||||
if (String(body.recordVersion) !== measured.recordVersion) return response(null, 'A0409')
|
||||
measured = { tripNo, recordVersion: String(Number(measured.recordVersion) + 1), details: copy(body.details) }
|
||||
assert.deepEqual(body.details.evidenceFileIds, []); assert.equal(Object.hasOwn(body.details, 'evidenceFiles'), false)
|
||||
const { evidenceFileIds, ...values } = body.details
|
||||
measured = { tripNo, recordVersion: String(Number(measured.recordVersion) + 1), details: { ...copy(values), evidenceFiles: [] } }
|
||||
measuredKeys.set(body.idempotencyKey, copy(measured)); measuredHistory.unshift(copy(measured)); return response(measured)
|
||||
}
|
||||
if (p === '/api/platform/v1/last-leg/fleet/operations') {
|
||||
|
||||
@@ -285,12 +285,12 @@ async function execute(context, { scope, writeMeasured = false, idempotencyNames
|
||||
const recordPath = `${OPERATIONS}/${encode(fixture.tripNo)}/record`
|
||||
const initial = await command('GET', recordPath, undefined, '核对本轮车次尚无实测记录')
|
||||
check('禁止覆盖已有实测历史样本', null, id(initial.recordVersion) === '0' && initial.details.actualDistanceMiles == null && initial.details.actualFuelLitres == null)
|
||||
const measured = { recordVersion: initial.recordVersion, idempotencyKey: key('measured-zero'), details: { actualDistanceMiles: 0, actualFuelLitres: null, remark: context.marker }, reason: null }
|
||||
const measured = { recordVersion: initial.recordVersion, idempotencyKey: key('measured-zero'), details: { actualDistanceMiles: 0, actualFuelLitres: null, remark: context.marker, evidenceFileIds: [] }, reason: null }
|
||||
const first = await command('PUT', recordPath, measured, '登记明确零里程和未知油耗')
|
||||
check('实测明确零值不变成未填写', null, first.details.actualDistanceMiles != null && Number(first.details.actualDistanceMiles) === 0 && first.details.actualFuelLitres == null)
|
||||
const firstAgain = await command('PUT', recordPath, measured, '重放实测保存')
|
||||
check('实测重放保留原版本', null, id(firstAgain.recordVersion) === id(first.recordVersion))
|
||||
const corrected = await command('PUT', recordPath, { recordVersion: first.recordVersion, idempotencyKey: key('measured-correct'), details: { actualDistanceMiles: 25.5, actualFuelLitres: 8.2, remark: context.marker }, reason: '核对受控实测' }, '更正实测里程油耗')
|
||||
const corrected = await command('PUT', recordPath, { recordVersion: first.recordVersion, idempotencyKey: key('measured-correct'), details: { actualDistanceMiles: 25.5, actualFuelLitres: 8.2, remark: context.marker, evidenceFileIds: [] }, reason: '核对受控实测' }, '更正实测里程油耗')
|
||||
await reject('PUT', recordPath, { ...measured, idempotencyKey: key('measured-stale') }, 'A0409', '旧实测版本拒绝')
|
||||
const measuredHistory = await command('GET', `${recordPath}/history?page=1&size=20`, undefined, '读取实测更正历史')
|
||||
check('实测重放与冲突没有追加历史', null, Number(measuredHistory.total) === 2 && version(corrected.recordVersion) === version(first.recordVersion) + 1n)
|
||||
|
||||
@@ -6,7 +6,10 @@ import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
|
||||
import { run as verifyCompanyCost } from './admin-test-env-modules/fleet-company-cost-acceptance.mjs'
|
||||
|
||||
export const MARKER = 'FLEET-COST-20261003'
|
||||
export const READY_COMMIT = 'a6fd0ba3'
|
||||
// Production source anchor, not a deployment-health assertion. The coordinator must independently
|
||||
// verify the deployed commit (including any descendant) and service readiness before passing this exact anchor.
|
||||
// This gate intentionally does not infer ancestry, compare deployed source, or perform network checks.
|
||||
export const READY_COMMIT = '8700fc34238bc33339351c73c15f3af8de8ce517'
|
||||
export function requireReady(args, execArgs) {
|
||||
if (!args.includes(`--ready-commit=${READY_COMMIT}`)) throw new Error('等待总控确认目标服务就绪后,才可显式传入本轮ready-commit')
|
||||
return requireTestProfile(execArgs)
|
||||
|
||||
@@ -15,6 +15,21 @@ const http = (path, source, target, contexts, access, idempotency, note) => ({
|
||||
})
|
||||
|
||||
export const SYNCHRONOUS_CLIENTS = Object.freeze([
|
||||
feign('finance/src/main/java/com/baiyue/finance/fleet/VehicleCostFileClient.java',
|
||||
'finance-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED',
|
||||
'公司车辆付款凭证按平台及费用ID校验;上传创建不可变对象且无远端幂等键,读取可重复,禁止传输层自动重试上传'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceClient.java',
|
||||
'lastleg-service', 'finance-service', ['REQUEST'], 'READ', 'SAFE_READ',
|
||||
'按维修/救援来源批量只读查询公司费用状态;POST为查询,不触发入账或付款'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareSupplierClient.java',
|
||||
'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ',
|
||||
'维修/救援登记读取并校验权威供应商状态,不创建或修改供应商'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteSupplierClient.java',
|
||||
'lastleg-service', 'warehouse-service', ['REQUEST'], 'READ', 'SAFE_READ',
|
||||
'采购报价读取启用卡车供应商及服务范围;内部令牌保护,只读供应商主档'),
|
||||
http('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGateway.java',
|
||||
'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED',
|
||||
'出车实测凭证按平台/车次独立归属;上传无远端幂等键,当前/历史版本引用经Lastleg校验后读取,不自动重试上传'),
|
||||
feign('lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java',
|
||||
'lastleg-service', 'finance-service', ['REQUEST'], 'READ', 'SAFE_READ',
|
||||
'原运费覆盖只读:按实际客户与头程入仓ID批量读取Finance已接受来源;目标端只查本库,无下游调用;不提供费用写入或资金命令'),
|
||||
|
||||
@@ -102,7 +102,7 @@ test('all Feign clients have reviewed context, access and idempotency metadata',
|
||||
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'FEIGN')
|
||||
.map(({ path, source, target }) => ({ path, source, target }))
|
||||
.sort((left, right) => left.path.localeCompare(right.path))
|
||||
assert.equal(reviewed.length, 66)
|
||||
assert.equal(reviewed.length, 70)
|
||||
assert.deepEqual(await discoverFeignEdges(), reviewed)
|
||||
})
|
||||
|
||||
@@ -118,7 +118,7 @@ test('all raw HTTP transports have reviewed context, access and idempotency meta
|
||||
}
|
||||
const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP')
|
||||
.map(client => client.path).sort()
|
||||
assert.equal(reviewed.length, 24)
|
||||
assert.equal(reviewed.length, 25)
|
||||
assert.deepEqual(discovered.sort(), reviewed)
|
||||
})
|
||||
|
||||
@@ -210,8 +210,11 @@ test('unified authority checks remain infrastructure edges and direct events rem
|
||||
assert.ok(graphEdges('BACKGROUND').includes('lastleg-service->finance-service'))
|
||||
const financeReads = SYNCHRONOUS_CLIENTS.filter(client => client.source === 'lastleg-service'
|
||||
&& client.target === 'finance-service' && client.contexts.includes('REQUEST'))
|
||||
assert.deepEqual(financeReads.map(client => [client.path, client.access, client.idempotency]), [[
|
||||
assert.deepEqual(financeReads.map(client => [client.path, client.access, client.idempotency])
|
||||
.sort((left, right) => left[0].localeCompare(right[0])), [[
|
||||
'lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java', 'READ', 'SAFE_READ'
|
||||
], [
|
||||
'lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceClient.java', 'READ', 'SAFE_READ'
|
||||
]])
|
||||
assert.ok(!SYNCHRONOUS_CLIENTS.some(client => ['event-bridge-service','gateway-overseas'].includes(client.source) || ['event-bridge-service','gateway-overseas'].includes(client.target)))
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user