From dd52bfd04327d6224756e0c2f978d67f96b55042 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 3 Oct 2026 15:00:12 +0800 Subject: [PATCH] test(fleet): verify operations from immutable current-run evidence --- scripts/README.md | 2 + .../fleet-quotes-operations.mjs | 68 +++++++++++++++ ...leet-operations-test-env-contract.test.mjs | 67 +++++++++++++++ scripts/fleet-operations-test-env.mjs | 84 +++++++++++++++++++ 4 files changed, 221 insertions(+) create mode 100644 scripts/fleet-operations-test-env-contract.test.mjs create mode 100644 scripts/fleet-operations-test-env.mjs diff --git a/scripts/README.md b/scripts/README.md index d20604286..2ec1b1597 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -1,5 +1,7 @@ # 后端脚本管理与全量 HTTP 验收索引 +`fleet-operations-test-env.mjs` 是车次运营统计独立只读TEST入口。显式环境文件及`--ready-commit=a6fd0ba3`之外,必须提供`--fixture=reports//handover-fixture.json --fixture-sha256=<文件SHA256> --marker=<该轮marker> --run-id=<独立批次>`;校验本轮真实DEPART和已核平车次成本,不能借历史车次或从统计响应反推期望。独立登录,只读该车次/历史/运营统计及导出,禁止造数、业务写入和里程更正;登录/退出为唯一POST。报告`reports/fleet-operations-/`独占且请求前落盘,原币成本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`。 diff --git a/scripts/admin-test-env-modules/fleet-quotes-operations.mjs b/scripts/admin-test-env-modules/fleet-quotes-operations.mjs index f710c0ee3..fca72635d 100644 --- a/scripts/admin-test-env-modules/fleet-quotes-operations.mjs +++ b/scripts/admin-test-env-modules/fleet-quotes-operations.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) diff --git a/scripts/fleet-operations-test-env-contract.test.mjs b/scripts/fleet-operations-test-env-contract.test.mjs new file mode 100644 index 000000000..53858bf45 --- /dev/null +++ b/scripts/fleet-operations-test-env-contract.test.mjs @@ -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, '') +}) diff --git a/scripts/fleet-operations-test-env.mjs b/scripts/fleet-operations-test-env.mjs new file mode 100644 index 000000000..153402ce6 --- /dev/null +++ b/scripts/fleet-operations-test-env.mjs @@ -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 }) +}