85 lines
6.2 KiB
JavaScript
85 lines
6.2 KiB
JavaScript
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 })
|
||
|
|
}
|