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 }) }