131 lines
8.3 KiB
JavaScript
131 lines
8.3 KiB
JavaScript
import { open, mkdir, readFile } from 'node:fs/promises'
|
|||
|
|
import { resolve, dirname } from 'node:path'
|
||
|
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||
|
|
import { parseEnv } from 'node:util'
|
||
|
|
import { createHash } from 'node:crypto'
|
||
|
|
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
|
||
|
|
import { runQuoteSession } from './admin-test-env-modules/fleet-quotes-operations.mjs'
|
||
|
|
|
||
|
|
export const READY_COMMIT = 'a6fd0ba3'
|
||
|
|
export const MARKER = 'FLEET-QUOTE-20261003'
|
||
|
|
const repository = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||
|
|
const outputDirectory = resolve(repository, 'reports/fleet-quote-live-20261003')
|
||
|
|
const sensitive = /password|token|authorization|cookie|secret|loginId|username/i
|
||
|
|
|
||
|
|
export function requireTestProfile(args, execArgs) {
|
||
|
|
if (args.length !== 1 || args[0] !== `--ready-commit=${READY_COMMIT}`) throw new Error('Explicit current ready-commit required; no resume or replay mode')
|
||
|
|
const files = []
|
||
|
|
for (let i = 0; i < execArgs.length; i++) {
|
||
|
|
if (execArgs[i].startsWith('--env-file=')) files.push(execArgs[i].slice(11))
|
||
|
|
else if (execArgs[i] === '--env-file') files.push(execArgs[++i])
|
||
|
|
else throw new Error('Only the explicit TEST env-file runtime option is allowed')
|
||
|
|
}
|
||
|
|
if (files.length !== 1 || !files[0] || !resolve(files[0]).replaceAll('\\', '/').endsWith('/.codex/env/chongchong-global/test-acceptance.env')) throw new Error('Explicit authorized TEST profile required')
|
||
|
|
return resolve(files[0])
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Append-only, fsync-before-send evidence. A fixed exclusive claim prohibits automatic reruns even after a crash. */
|
||
|
|
export async function installQuoteJournal(context, directory, { persist, secrets = [] } = {}) {
|
||
|
|
let sequence = 0, halted = false
|
||
|
|
const knownSecrets = new Set(secrets.filter(Boolean))
|
||
|
|
function discover(value) {
|
||
|
|
if (!value || typeof value !== 'object') return
|
||
|
|
for (const [key, child] of Object.entries(value)) {
|
||
|
|
if (sensitive.test(key) && typeof child === 'string' && child) knownSecrets.add(child)
|
||
|
|
else if (child && typeof child === 'object') discover(child)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
function clean(value) {
|
||
|
|
discover(value)
|
||
|
|
if (typeof value === 'string') {
|
||
|
|
let result = value
|
||
|
|
for (const secret of knownSecrets) result = result.split(secret).join('[REDACTED]')
|
||
|
|
return result
|
||
|
|
}
|
||
|
|
if (Array.isArray(value)) return value.map(clean)
|
||
|
|
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([key, child]) => [key, sensitive.test(key) ? '[REDACTED]' : clean(child)]))
|
||
|
|
return value
|
||
|
|
}
|
||
|
|
const save = persist || (async (name, value) => {
|
||
|
|
const file = await open(resolve(directory, name), 'wx', 0o600)
|
||
|
|
try { await file.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8'); await file.sync() } finally { await file.close() }
|
||
|
|
})
|
||
|
|
async function checkpoint(kind, value) {
|
||
|
|
const name = `${String(++sequence).padStart(5, '0')}-${kind}.json`
|
||
|
|
await save(name, clean({ marker: MARKER, at: new Date().toISOString(), ...value }))
|
||
|
|
return name
|
||
|
|
}
|
||
|
|
await checkpoint('claim', { expectedCommit: READY_COMMIT, scope: 'QUOTES_ONLY', status: 'CLAIMED', rerun: 'FORBIDDEN; reconcile original samples manually' })
|
||
|
|
async function bodyEvidence(body) {
|
||
|
|
if (!(body instanceof FormData)) return body === undefined ? { encoding: 'absent' } : { encoding: 'json', value: structuredClone(body) }
|
||
|
|
const parts = []
|
||
|
|
for (const [name, value] of body.entries()) {
|
||
|
|
if (typeof value === 'string') parts.push({ name, value })
|
||
|
|
else {
|
||
|
|
const bytes = Buffer.from(await value.arrayBuffer())
|
||
|
|
parts.push({ name, filename: value.name, contentType: value.type, size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex'), base64: bytes.toString('base64') })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return { encoding: 'multipart', parts }
|
||
|
|
}
|
||
|
|
for (const name of ['request', 'requestWithBody', 'requestBinary']) {
|
||
|
|
const original = context[name].bind(context)
|
||
|
|
context[name] = async (method, path, ...rest) => {
|
||
|
|
method = String(method).toUpperCase()
|
||
|
|
const logout = method === 'POST' && path === '/api/auth/logout'
|
||
|
|
if (halted && !logout) throw new Error('Unknown request outcome: halted; original sample retained, no retry or cleanup')
|
||
|
|
const body = name === 'requestBinary' ? undefined : rest[0]
|
||
|
|
discover(body)
|
||
|
|
const request = { method, path, transport: name, body: await bodyEvidence(body), headers: name === 'requestBinary' ? {} : rest[2] || {}, idempotencyKey: body?.idempotencyKey ?? null }
|
||
|
|
let sample
|
||
|
|
try { sample = await checkpoint('before', request) } catch (error) { halted = true; context.quoteRequestOutcomeUnknown = true; throw error }
|
||
|
|
let response
|
||
|
|
try { response = await original(method, path, ...rest) }
|
||
|
|
catch { response = { http: 0, body: null, requestId: null, traceId: null } }
|
||
|
|
discover(response?.body)
|
||
|
|
const code = String(response?.body?.code ?? '')
|
||
|
|
const write = !['GET', 'HEAD'].includes(method)
|
||
|
|
const known = response?.http === 200 && (code === '200' || /^A04\d+$/.test(code)) || response?.http >= 400 && response?.http < 500 && response?.http !== 408
|
||
|
|
const unknown = write && !known
|
||
|
|
if (unknown) { halted = true; context.quoteRequestOutcomeUnknown = true }
|
||
|
|
try {
|
||
|
|
await checkpoint('after', { sample, method, path, outcome: unknown ? 'UNKNOWN_STOP' : 'RECEIVED', http: response?.http, code, requestId: response?.requestId, traceId: response?.traceId,
|
||
|
|
// Only write responses belong to our isolated fixture. Do not persist unrelated directory records.
|
||
|
|
body: write ? response?.body : undefined, createdResources: context.createdResources, steps: context.steps })
|
||
|
|
} catch (error) { halted = true; context.quoteRequestOutcomeUnknown = true; throw error }
|
||
|
|
if (unknown) throw new Error('Unknown write outcome; reconcile persisted original request before further business actions')
|
||
|
|
return response
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const executeModule = context.executeModule.bind(context)
|
||
|
|
context.executeModule = async (name, handler) => {
|
||
|
|
await checkpoint('module-start', { module: name, createdResources: context.createdResources })
|
||
|
|
try { return await executeModule(name, handler) }
|
||
|
|
finally { await checkpoint('module-end', { module: name, createdResources: context.createdResources, steps: context.steps, modules: context.modules }) }
|
||
|
|
}
|
||
|
|
return { checkpoint, unknown: () => halted }
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function execute(args = process.argv.slice(2), execArgs = process.execArgv) {
|
||
|
|
const file = requireTestProfile(args, execArgs)
|
||
|
|
if (!repository.replaceAll('\\', '/').endsWith('/chongchong-global/baiyue-firstleg')) throw new Error('Only registered TEST workspace is allowed')
|
||
|
|
const env = parseEnv(await readFile(file, 'utf8'))
|
||
|
|
if (env.CHONGCHONG_ENV !== 'test' || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw new Error('TEST credentials missing')
|
||
|
|
const base = new URL(env.CHONGCHONG_ADMIN_API_BASE_URL)
|
||
|
|
if (!['http:', 'https:'].includes(base.protocol) || base.pathname.replace(/\/$/, '') !== '/api' || base.username || base.password || base.search || base.hash) throw new Error('Invalid unified TEST API base')
|
||
|
|
await mkdir(outputDirectory, { recursive: true })
|
||
|
|
const context = new AcceptanceRun({ environment: 'test', gateway: base.origin, username: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD, quiet: true, modules: [] })
|
||
|
|
const journal = await installQuoteJournal(context, outputDirectory, { secrets: [env.CHONGCHONG_ADMIN_USERNAME, env.CHONGCHONG_ADMIN_PASSWORD] })
|
||
|
|
let report
|
||
|
|
try { report = await runQuoteSession(context, { marker: MARKER }) }
|
||
|
|
catch { report = { success: false, fatal: 'Session interrupted; inspect durable request checkpoints', operationsStatus: 'NOT_TESTED' } }
|
||
|
|
finally { context.token = ''; context.refreshToken = '' }
|
||
|
|
const success = Boolean(report.success) && !journal.unknown()
|
||
|
|
await journal.checkpoint('final', { ...report, success, outcome: journal.unknown() ? 'UNKNOWN_STOP' : success ? 'VERIFIED' : 'FAILED', createdResources: context.createdResources })
|
||
|
|
return { outputDirectory, success, realExecution: true }
|
||
|
|
}
|
||
|
|
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('报价验收未启动或已中止;核对TEST配置、就绪标记和持久检查点,禁止自动重跑。'); process.exitCode = 1 })
|
||
|
|
}
|