This commit is contained in:
2026-10-08 17:07:57 +08:00
parent 16d2d9111a
commit c1ddb658bd
111 changed files with 2914 additions and 941 deletions
@@ -1,6 +1,8 @@
import { exportWorkbook } from './download-center-fixture.mjs'
import { parseXlsx } from './finance-downloads.mjs'
import assert from 'node:assert/strict'
import test from 'node:test'
import { run, runQuoteSession, verifyOperations, requireCurrentFixture, readCsv, fixtureContract } from './fleet-quotes-operations.mjs'
import { run, runQuoteSession, verifyOperations, requireCurrentFixture, fixtureContract } from './fleet-quotes-operations.mjs'
import { AcceptanceRun } from '../admin-test-env-acceptance.mjs'
const copy = value => structuredClone(value)
@@ -10,6 +12,7 @@ function harness(fault) {
let next = 0, supplierStatus = 'ACTIVE'
let measured = { tripNo, recordVersion: '0', details: { actualDistanceMiles: null, actualFuelLitres: null, remark: null, evidenceFiles: [] } }
const measuredHistory = []
const exportTasks = new Map()
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' })
function snapshot(row, action) { histories.get(row.quoteNo).unshift({ action, recordVersion: row.recordVersion, snapshot: copy(row) }) }
@@ -31,6 +34,16 @@ function harness(fault) {
apiSuccess(_module, label, result) { assert.equal(result.http, 200, label); assert.equal(result.body.code, '200', label); return result.body.data },
async request(method, path, body) {
calls.push({ method, path, body: copy(body) }); const u = new URL(path, 'http://offline.invalid'), p = u.pathname
if (p === '/api/platform/v1/files/export-tasks' && method === 'POST') {
const taskId = String(exportTasks.size + 1)
exportTasks.set(taskId, body)
return response({ id: taskId, status: 'PENDING' })
}
if (p.startsWith('/api/platform/v1/files/export-tasks/')) {
const task = exportTasks.get(p.split('/').at(-1))
const failed = task.parameters.kind === 'INVALID' || (task.parameters.from && Date.parse(task.parameters.from) > Date.parse(task.parameters.to))
return response({ id: p.split('/').at(-1), status: failed ? 'FAILED' : 'SUCCEEDED', failureMessage: failed ? '筛选参数无效' : null })
}
if (p === '/api/platform/v1/last-leg/fleet/trips/TR-CURRENT') return response({ trip: { tripNo, warehouseId: '92', departedAt, status: 'IN_TRANSIT', remark: fault === 'historical trip' ? 'OLD' : marker } })
if (p === '/api/platform/v1/suppliers' && method === 'POST') {
for (const field of ['name', 'region', 'legalRepresentative', 'contactName', 'phone', 'settlementPeriod']) assert.ok(body[field], field)
@@ -90,11 +103,11 @@ function harness(fault) {
},
async requestBinary(method, path) {
calls.push({ method, path }); const u = new URL(path, 'http://offline.invalid')
const error = response(null, 'A0400')
if (u.pathname.includes('/quotes/') && u.searchParams.get('size') === '0') { const invalid = response(null, 'A0422'); return { ...invalid, contentType: 'application/json', bytes: Buffer.from(JSON.stringify(invalid.body)) } }
if (fault === 'json export masquerades as csv' || (u.searchParams.has('from') && Date.parse(u.searchParams.get('from')) > Date.parse(u.searchParams.get('to')))) return { ...error, contentType: fault ? 'text/csv' : 'application/json', bytes: Buffer.from(JSON.stringify(error.body)) }
const text = u.pathname.includes('/quotes/') ? `\uFEFF报价号,金额\r\n${[...rows.keys()].join('\r\n')}` : `\uFEFF车次号,实际里程(mile)\r\n${tripNo},25.5`
return { http: 200, body: null, bytes: Buffer.from(text), contentType: 'text/csv;charset=UTF-8', requestId: 'offline-request', traceId: 'offline-trace' }
assert.match(u.pathname, /files\/export-tasks\/[^/]+\/download$/)
if (fault === 'json export masquerades as csv') return { http: 200, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', bytes: Buffer.from('{"code":"A0400"}') }
const task = exportTasks.get(u.pathname.split('/').at(-2))
const bytes = task.exportType === 'FLEET_QUOTE_LIST' ? exportWorkbook(['报价号'], [[...rows.keys()].join(' / ')]) : exportWorkbook(['车次号', '实际里程(mile)'], [tripNo, '25.5'])
return { http: 200, body: null, bytes, contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', requestId: 'offline-request', traceId: 'offline-trace' }
},
}
return { context, rows, calls, supplierStatus: () => supplierStatus }
@@ -127,7 +140,7 @@ test('missing or stale fixture fails before any HTTP and cannot be treated as sk
test('proof cannot be overwritten on rerun and CSV detector rejects misleading success content', async () => {
const { context } = harness(); await run(context); const proof = context.createdResources.fleetQuoteAcceptance
await assert.rejects(run(context), /Existing proof/); assert.equal(context.createdResources.fleetQuoteAcceptance, proof)
assert.throws(() => readCsv({ http: 200, contentType: 'text/csv', bytes: Buffer.from('{"code":"A0403"}') }, '报价号'), /not a valid CSV/)
assert.throws(() => parseXlsx(Buffer.from('{"code":"A0403"}')), /ZIP/)
assert.throws(() => requireCurrentFixture({ marker: 'x' }), /current-run/)
})
test('unknown expected money is rejected before HTTP instead of comparing as zero', async () => {
@@ -141,10 +154,10 @@ test('quote acceptance is independent of DEPART and never requests operations, i
assert.equal(context.createdResources.fleetQuoteAcceptance.operationsStatus, 'NOT_TESTED')
assert.ok(calls.every(call => !/operations|inventory|finance|\/trips\//.test(call.path)))
})
test('operations defaults to read-only and never creates customer, supplier or quote records', async () => {
test('operations only reads business data and creates download tasks, never customer, supplier or quote records', async () => {
const { context, calls, rows } = harness(); delete context.createdResources.merchantQualificationFixture
await verifyOperations(context)
assert.ok(calls.every(call => call.method === 'GET')); assert.equal(rows.size, 0)
assert.ok(calls.every(call => call.method === 'GET' || (call.method === 'POST' && call.path === '/api/platform/v1/files/export-tasks' && call.body.exportType === 'TRIP_OPERATIONS_LIST'))); assert.equal(rows.size, 0)
assert.equal(context.createdResources.fleetOperationsAcceptance.measuredStatus, 'NOT_TESTED')
})
test('measured writes require an explicit independent idempotency namespace', async () => {