test(fleet): verify operations from immutable current-run evidence

This commit is contained in:
2026-10-03 15:00:12 +08:00
parent 780d37e1d2
commit dd52bfd043
4 changed files with 221 additions and 0 deletions
+2
View File
@@ -1,5 +1,7 @@
# 后端脚本管理与全量 HTTP 验收索引
`fleet-operations-test-env.mjs` 是车次运营统计独立只读TEST入口。显式环境文件及`--ready-commit=a6fd0ba3`之外,必须提供`--fixture=reports/<C批次>/handover-fixture.json --fixture-sha256=<文件SHA256> --marker=<该轮marker> --run-id=<独立批次>`;校验本轮真实DEPART和已核平车次成本,不能借历史车次或从统计响应反推期望。独立登录,只读该车次/历史/运营统计及导出,禁止造数、业务写入和里程更正;登录/退出为唯一POST。报告`reports/fleet-operations-<run-id>/`独占且请求前落盘,原币成本CNY100.03与USD13.07分开;失败保留证据、禁止覆盖重跑,结束退出独立会话。来源断言和权限由正式入口及`fleet-operations-test-env-contract.test.mjs`共同约束,离线测试不证明真实统计已通过。
`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`。
@@ -1,4 +1,66 @@
import { createHash } from 'node:crypto'
import { expectedAllocations } from './fleet-trip-cost-acceptance.mjs'
/** C's signed-by-hash handover artifact is the only producer accepted by the standalone reader. */
export function operationsFixtureFromHandover(bytes, { sha256, marker, runId }) {
const fail = () => { throw new Error('Current C fixture, original DEPART or verified independent cost shares do not match') }
if (!/^[a-f0-9]{64}$/.test(sha256 || '') || createHash('sha256').update(bytes).digest('hex') !== sha256 || !marker || !runId) fail()
const f = JSON.parse(bytes.toString('utf8')), history = f.immutableDeparture, snapshot = history?.snapshot, load = snapshot?.confirmedLoad, c = f.costs
if (f.marker !== marker || f.runId !== runId || f.status !== 'DEPART_AND_HANDOVER_VERIFIED' || !f.handoverNo
|| !f.tripNo || !f.warehouseId || history?.action !== 'DEPART' || snapshot?.trip?.tripNo !== f.tripNo
|| String(snapshot.trip.warehouseId) !== String(f.warehouseId) || !snapshot.trip.remark?.includes(marker)
|| f.departure?.tripNo !== f.tripNo || f.departure.status !== 'IN_TRANSIT' || !Number.isFinite(Date.parse(f.departure.departedAt))
|| snapshot.trip.departedAt !== f.departure.departedAt || !load?.manifestHash
|| c?.status !== 'VERIFIED' || c.marker !== marker || c.tripNo !== f.tripNo
|| c.expected?.finalCny !== '100.03' || c.expected?.usd !== '13.07' || c.expected?.allocationMethod !== 'SAME_ROUTE_PALLET_SPACES') fail()
const positive = value => Number.isSafeInteger(Number(value)) && Number(value) > 0
if (!Array.isArray(load.pallets) || !load.pallets.length || !Array.isArray(load.cargo) || !load.cargo.length || !Array.isArray(c.shares) || !c.shares.length) fail()
const pallets = new Map()
for (const p of load.pallets) { if (!p.warehousePalletId || pallets.has(String(p.warehousePalletId)) || !positive(p.palletSpaces)) fail(); pallets.set(String(p.warehousePalletId), Number(p.palletSpaces)) }
const seen = new Set(), shipped = new Map()
for (const row of load.cargo) {
const item = `${row.dispatchNo}:${row.dispatchItemId}`, key = `${row.dispatchNo}:${row.customerId}:${row.warehousePalletId}`
if (!row.dispatchNo || !row.dispatchItemId || !row.customerId || seen.has(item) || !pallets.has(String(row.warehousePalletId)) || !positive(row.packages)) fail()
seen.add(item); shipped.set(key, (shipped.get(key) || 0) + Number(row.packages))
}
const bookings = new Map(), external = new Map(), money = new Map()
for (const b of snapshot.externalBookings || []) {
if (['DRAFT', 'CANCELLED'].includes(b.status)) continue
if (b.status !== 'BOUND' || !b.bookingNo || bookings.has(b.bookingNo) || !b.dispatchNo || !b.receipt?.source?.pallets?.length || !['CNY', 'USD'].includes(b.cargo?.currency)) fail()
bookings.set(b.bookingNo, b)
let count = 0
for (const p of b.receipt.source.pallets) { if (!positive(p.packages)) fail(); const key = `${b.dispatchNo}:${b.customerId}:${p.warehousePalletId}`; external.set(key, (external.get(key) || 0) + Number(p.packages)); count += Number(p.packages) }
if (count !== Number(b.receipt.source.receivedPackages)) fail()
const amount = number(b.agreedAmount); money.set(b.cargo.currency, (money.get(b.cargo.currency) || 0) + Math.round(amount * 100))
}
for (const [key, value] of external) if (value > (shipped.get(key) || 0)) fail()
const sharePallets = new Set(), businesses = new Set()
for (const share of c.shares) {
if (!share.businessNo || businesses.has(share.businessNo) || !['OWN', 'EXTERNAL'].includes(share.sourceType) || !share.warehousePalletIds?.length) fail()
businesses.add(share.businessNo)
let spaces = 0
for (const id of share.warehousePalletIds) {
if (!pallets.has(String(id)) || sharePallets.has(String(id)) || !load.cargo.some(row => row.dispatchNo === share.dispatchNo && String(row.warehousePalletId) === String(id))) fail()
sharePallets.add(String(id)); spaces += pallets.get(String(id))
}
if (spaces !== share.palletSpaces) fail()
if (share.sourceType === 'EXTERNAL') {
const b = bookings.get(share.businessNo)
if (!b || b.dispatchNo !== share.dispatchNo || b.receipt.source.pallets.some(p => !share.warehousePalletIds.map(String).includes(String(p.warehousePalletId)))) fail()
} else if (share.businessNo !== share.dispatchNo || [...bookings.values()].some(b => b.dispatchNo === share.dispatchNo)) fail()
}
if (sharePallets.size !== pallets.size || !c.shares.some(s => s.sourceType === 'OWN') || !c.shares.some(s => s.sourceType === 'EXTERNAL')) fail()
const total = [...shipped.values()].reduce((a, b) => a + b, 0), ext = [...external.values()].reduce((a, b) => a + b, 0)
if (total !== Number(f.departure.shippedPackages) || ext <= 0 || ext >= total) fail()
const costs = [['CNY', '100.03'], ['USD', '13.07']].map(([currency, amount]) => {
const allocations = expectedAllocations(amount, c.shares)
const sum = type => Number(allocations.filter(row => row.sourceType === type).reduce((n, row) => n + BigInt(row.cents), 0n)) / 100
return { currency, directCost: Number(amount), ownCost: sum('OWN'), externalCost: sum('EXTERNAL') }
})
return { marker, stage: 'CURRENT_RUN_DEPARTED', tripNo: f.tripNo, warehouseId: String(f.warehouseId), departedAt: f.departure.departedAt, departureVerified: true,
immutableDeparture: history, expected: { physicalPallets: pallets.size, palletSpaces: [...pallets.values()].reduce((a, b) => a + b, 0), ownPackages: total - ext, externalPackages: ext,
externalAgreedAmounts: [...money].map(([currency, amount]) => ({ currency, amount: amount / 100 })), costs } }
}
const MODULE = 'fleet-quotes-operations'
const QUOTES = '/api/platform/v1/last-leg/fleet/quotes'
@@ -10,6 +72,7 @@ const encode = encodeURIComponent
const query = values => new URLSearchParams(Object.entries(values).filter(([, value]) => value != null).map(([key, value]) => [key, String(value)])).toString()
const version = value => BigInt(id(value))
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b)
const canonical = value => Array.isArray(value) ? value.map(canonical) : value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, canonical(value[key])])) : value
function number(value) {
if (!['number', 'string'].includes(typeof value) || !/^\d+(?:\.\d{1,2})?$/.test(String(value)) || !Number.isFinite(Number(value))) throw new Error('Missing or invalid numeric fact; never convert unknown to zero')
return Number(value)
@@ -133,6 +196,11 @@ async function execute(context, { scope, writeMeasured = false, idempotencyNames
let fixture, from, to, report
if (scope === 'OPERATIONS_ONLY') {
fixture = requireCurrentFixture(context)
if (fixture.immutableDeparture) {
const history = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}/history?page=1&size=200`, undefined, '核对输入报告的原DEPART不可变快照')
const departures = history.records?.filter(row => row.action === 'DEPART') || []
check('当前历史完整且原DEPART与SHA绑定报告一致', null, Number(history.total) === history.records?.length && departures.length === 1 && same(canonical(departures[0]), canonical(fixture.immutableDeparture)))
}
proof.tripNo = fixture.tripNo
const trip = await command('GET', `/api/platform/v1/last-leg/fleet/trips/${encode(fixture.tripNo)}`, undefined, '核对本轮真实发车车次')
check('车次属于当前场景并有真实发车时间', null, trip.trip?.tripNo === fixture.tripNo && id(trip.trip.warehouseId) === id(fixture.warehouseId)
@@ -0,0 +1,67 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { operationsFixtureFromHandover } from './admin-test-env-modules/fleet-quotes-operations.mjs'
import { parseOperationsArguments, restrictOperationsTransport, runOperationsSession } from './fleet-operations-test-env.mjs'
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
function sample() {
const marker = 'CURRENT', tripNo = 'TR-CURRENT', departedAt = '2026-10-03T01:00:00Z'
return { marker, runId: 'current-run', tripNo, warehouseId: '92', status: 'DEPART_AND_HANDOVER_VERIFIED', handoverNo: 'HO-CURRENT',
departure: { tripNo, status: 'IN_TRANSIT', departedAt, shippedPackages: 14 },
immutableDeparture: { action: 'DEPART', recordVersion: '5', snapshot: {
trip: { tripNo, warehouseId: '92', departedAt, remark: marker }, confirmedLoad: { manifestHash: 'original-load',
pallets: [{ warehousePalletId: '1', palletSpaces: 1 }, { warehousePalletId: '2', palletSpaces: 1 }],
cargo: [{ dispatchNo: 'DP-OWN', dispatchItemId: '1', customerId: '9', warehousePalletId: '1', packages: 10 }, { dispatchNo: 'DP-EXT', dispatchItemId: '2', customerId: '9', warehousePalletId: '2', packages: 4 }] },
externalBookings: [{ bookingNo: 'FB-EXT', status: 'BOUND', dispatchNo: 'DP-EXT', customerId: '9', agreedAmount: '30.00', cargo: { currency: 'CNY' }, receipt: { source: { receivedPackages: 4, pallets: [{ warehousePalletId: '2', packages: 4 }] } } }] } },
costs: { marker, tripNo, status: 'VERIFIED', expected: { finalCny: '100.03', usd: '13.07', allocationMethod: 'SAME_ROUTE_PALLET_SPACES' }, shares: [
{ businessNo: 'DP-OWN', sourceType: 'OWN', dispatchNo: 'DP-OWN', palletSpaces: 1, warehousePalletIds: ['1'] },
{ businessNo: 'FB-EXT', sourceType: 'EXTERNAL', dispatchNo: 'DP-EXT', palletSpaces: 1, warehousePalletIds: ['2'] }] } }
}
function convert(value, overrides = {}) {
const bytes = Buffer.from(JSON.stringify(value))
return operationsFixtureFromHandover(bytes, { sha256: createHash('sha256').update(bytes).digest('hex'), marker: 'CURRENT', runId: 'current-run', ...overrides })
}
test('adapts actual C artifact with independent physical cargo and exact cent currency shares', () => {
const f = convert(sample())
assert.deepEqual(f.expected, { physicalPallets: 2, palletSpaces: 2, ownPackages: 10, externalPackages: 4,
externalAgreedAmounts: [{ currency: 'CNY', amount: 30 }], costs: [
{ currency: 'CNY', directCost: 100.03, ownCost: 50.02, externalCost: 50.01 },
{ currency: 'USD', directCost: 13.07, ownCost: 6.54, externalCost: 6.53 }] })
assert.equal(f.stage, 'CURRENT_RUN_DEPARTED'); assert.equal(f.departureVerified, true)
})
for (const [name, mutate] of [
['stale marker', f => { f.marker = 'OLD' }], ['stale run', f => { f.runId = 'old' }],
['unverified costs', f => { f.costs.status = 'CHECKING' }], ['wrong currency amount', f => { f.costs.expected.usd = '0' }],
['not departed', f => { f.departure.status = 'PLANNED' }], ['replaced history', f => { f.immutableDeparture.action = 'HANDOVER' }],
['duplicate physical pallet', f => { f.immutableDeparture.snapshot.confirmedLoad.pallets.push(f.immutableDeparture.snapshot.confirmedLoad.pallets[0]) }],
['external over-attribution', f => { f.immutableDeparture.snapshot.externalBookings[0].receipt.source.pallets[0].packages = 99 }],
['wrong shares', f => { f.costs.shares[1].warehousePalletIds = ['1'] }], ['wrong departure total', f => { f.departure.shippedPackages = 15 }],
]) test(`rejects ${name} before any HTTP`, () => { const f = sample(); mutate(f); assert.throws(() => convert(f)) })
test('rejects mismatched file hash even when its contents look valid', () => assert.throws(() => convert(sample(), { sha256: '0'.repeat(64) })))
test('requires explicit report/hash/current marker/run and TEST readiness; denies write and resume flags', () => {
const args = ['--ready-commit=a6fd0ba3', '--fixture=reports/fleet-current-run/handover-fixture.json', `--fixture-sha256=${'a'.repeat(64)}`, '--marker=CURRENT', '--run-id=current-run']
const runtime = ['--env-file=C:/Users/BY-OY/.codex/env/chongchong-global/test-acceptance.env']
assert.equal(parseOperationsArguments(args, runtime).runId, 'current-run')
for (const bad of [args.slice(1), [...args, '--write-measured=true'], [...args, '--resume=true'], args.map(a => a.startsWith('--fixture=') ? '--fixture=../old/handover-fixture.json' : a)]) assert.throws(() => parseOperationsArguments(bad, runtime))
})
test('transport permits only current-trip/statistics reads plus independent session login/logout', async () => {
const context = new AcceptanceRun({ environment: 'test' }), calls = []
for (const name of ['request', 'requestWithBody', 'requestBinary']) context[name] = async (...args) => { calls.push(args); return {} }
restrictOperationsTransport(context, 'TR-CURRENT')
await context.request('GET', '/api/platform/v1/last-leg/fleet/trips/TR-CURRENT')
for (const [method, path] of [['PUT', '/api/platform/v1/last-leg/fleet/operations/TR-CURRENT'], ['POST', '/api/platform/v1/last-leg/fleet/trips'], ['GET', '/api/platform/v1/last-leg/fleet/trips/TR-OLD'], ['GET', '/api/platform/v1/iam/tenants']]) assert.throws(() => context.request(method, path))
assert.equal(calls.length, 1)
})
test('new login and logout surround a rejected live immutable snapshot without business writes', async () => {
const context = new AcceptanceRun({ environment: 'test', username: 'offline', password: 'offline', quiet: true }), calls = []
context.request = async (method, path) => {
calls.push([method, path])
return { http: 200, body: { code: '200', data: path.endsWith('/login') ? { accessToken: 'offline-access', refreshToken: 'offline-refresh' } : path.includes('/history') ? { total: 0, records: [] } : null }, requestId: 'offline', traceId: 'offline' }
}
const report = await runOperationsSession(context, convert(sample()))
assert.equal(report.success, false); assert.equal(report.loggedOut, true); assert.equal(report.measuredStatus, 'NOT_TESTED')
assert.deepEqual(calls.map(c => c[0]), ['POST', 'GET', 'POST'])
assert.equal(calls[0][1], '/api/auth/login'); assert.equal(calls.at(-1)[1], '/api/auth/logout')
assert.equal(context.token, '')
})
+84
View File
@@ -0,0 +1,84 @@
import { readFile, mkdir, open } from 'node:fs/promises'
import { resolve, dirname, relative, isAbsolute } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { parseEnv } from 'node:util'
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
import { requireTestProfile, installQuoteJournal, READY_COMMIT } from './fleet-quotes-test-env.mjs'
import { operationsFixtureFromHandover, verifyOperations } from './admin-test-env-modules/fleet-quotes-operations.mjs'
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
export function parseOperationsArguments(args, execArgs) {
const allowed = ['ready-commit', 'fixture', 'fixture-sha256', 'marker', 'run-id'], values = {}
for (const arg of args) {
const match = /^--([^=]+)=(.+)$/.exec(arg)
if (!match || !allowed.includes(match[1]) || values[match[1]]) throw new Error('Explicit unique operations arguments required; no resume or write mode')
values[match[1]] = match[2]
}
if (allowed.some(name => !values[name]) || !/^[a-f0-9]{64}$/.test(values['fixture-sha256']) || !/^[A-Za-z0-9_-]{1,80}$/.test(values.marker) || !/^[A-Za-z0-9_-]{1,48}$/.test(values['run-id'])) throw new Error('Missing current-run identity or SHA256')
const envFile = requireTestProfile([`--ready-commit=${values['ready-commit']}`], execArgs)
const fixturePath = resolve(root, values.fixture), rel = relative(resolve(root, 'reports'), fixturePath)
if (isAbsolute(rel) || rel.startsWith('..') || !/^[^/\\]+[/\\]handover-fixture\.json$/.test(rel)) throw new Error('Only an explicit C report under this TEST repository reports directory is accepted')
return { envFile, fixturePath, sha256: values['fixture-sha256'], marker: values.marker, runId: values['run-id'] }
}
export async function runOperationsSession(context, fixture) {
if (context.options?.environment !== 'test' || context.token || context.refreshToken || Object.keys(context.createdResources).length) throw new Error('Fresh independent TEST session required')
context.marker = fixture.marker
context.context.fleetQuotesOperationsFixture = fixture
const successful = r => r?.http === 200 && String(r.body?.code) === '200'
let failure = null, loggedOut = false
try {
const login = await context.request('POST', '/api/auth/login', { loginId: context.options.username, password: context.options.password }, '')
if (!successful(login) || !login.body.data?.accessToken || !login.body.data?.refreshToken) throw new Error('Independent TEST login failed')
context.token = login.body.data.accessToken; context.refreshToken = login.body.data.refreshToken
await verifyOperations(context, { writeMeasured: false })
} catch (error) { failure = error.message }
finally {
try {
if (context.token) loggedOut = successful(await context.request('POST', '/api/auth/logout', { refreshToken: context.refreshToken }))
} catch { failure ||= 'Logout outcome unknown' }
context.token = ''; context.refreshToken = ''
}
return { marker: fixture.marker, scope: 'OPERATIONS_ONLY', success: !failure && loggedOut && context.steps.every(s => s.passed), failure, loggedOut,
measuredStatus: 'NOT_TESTED', financeStatus: 'NOT_TESTED', steps: context.steps, createdResources: context.createdResources }
}
export function restrictOperationsTransport(context, tripNo) {
for (const name of ['request', 'requestWithBody', 'requestBinary']) {
const original = context[name].bind(context)
context[name] = (method, path, ...args) => {
const trip = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripNo)}`
const read = method === 'GET' && (path === trip || path.startsWith(`${trip}/history?`) || /^\/api\/platform\/v1\/last-leg\/fleet\/operations(?:\/export)?\?/.test(path))
const auth = name === 'request' && method === 'POST' && ['/api/auth/login', '/api/auth/logout'].includes(path)
if (!read && !auth) throw new Error('Operations reader forbids business writes and unrelated reads')
return original(method, path, ...args)
}
}
}
export async function execute(args = process.argv.slice(2), execArgs = process.execArgv) {
const config = parseOperationsArguments(args, execArgs)
if (!root.replaceAll('\\', '/').endsWith('/chongchong-global/baiyue-firstleg')) throw new Error('Registered TEST workspace required')
const fixture = operationsFixtureFromHandover(await readFile(config.fixturePath), config)
const env = parseEnv(await readFile(config.envFile, 'utf8')), base = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
if (env.CHONGCHONG_ENV !== 'test' || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD || !['http:', 'https:'].includes(base.protocol)
|| base.pathname.replace(/\/$/, '') !== '/api' || base.username || base.password || base.search || base.hash) throw new Error('Explicit unified TEST configuration required')
const directory = resolve(root, 'reports', `fleet-operations-${config.runId}`)
await mkdir(directory, { recursive: true })
const context = new AcceptanceRun({ environment: 'test', gateway: base.origin, username: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD, quiet: true })
restrictOperationsTransport(context, fixture.tripNo)
const journal = await installQuoteJournal(context, directory, { secrets: [env.CHONGCHONG_ADMIN_USERNAME, env.CHONGCHONG_ADMIN_PASSWORD], persist: async (name, evidence) => {
const file = await open(resolve(directory, name), 'wx', 0o600)
try {
await file.writeFile(JSON.stringify({ ...evidence, marker: config.marker, scope: 'OPERATIONS_ONLY', expectedCommit: READY_COMMIT,
input: { fixturePath: config.fixturePath, sha256: config.sha256, runId: config.runId }, expected: fixture.expected }, null, 2)); await file.sync()
} finally { await file.close() }
} })
const report = await runOperationsSession(context, fixture)
await journal.checkpoint('final', report)
return { success: report.success && !journal.unknown(), directory }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
execute().then(result => { console.log(JSON.stringify(result)); process.exitCode = result.success ? 0 : 1 })
.catch(() => { console.error('运营只读验收未启动或中止,请核对本轮C报告、SHA及持久检查点;未自动重跑。'); process.exitCode = 1 })
}