Files

85 lines
6.2 KiB
JavaScript
Raw Permalink Normal View History

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