test(fleet): verify booking quote source linkage in TEST
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
# 后端脚本管理与全量 HTTP 验收索引
|
||||
|
||||
`fleet-booking-quotes-test-env.mjs` 是外接预订选用销售报价的独立 TEST HTTP 验收入口,使用显式 `node --env-file=<用户指定目录的test-acceptance.env>` 和 `--ready-commit=a6fd0ba3`,执行前须由总控核对实际部署版本与健康。只读公共仓城市/时区,创建自身全新隔离客户、销售报价及未分配资源的车次/询价草稿;验证候选日期/币种/计价、零价与缺价、手填价格、陈旧版本拒绝、刷新重选及来源快照,不创建库存、不发车、不证明承运成功或实收完成。请求前持久留证、敏感字段脱敏、未知结果停止且禁止自动重跑,独立登录并退出;保留自己的计划/报价/草稿,不删除资源。当前固定R2标识,独占报告 `reports/fleet-booking-quotes-live-20261003-r2/`。离线合同 `fleet-booking-quotes-test-env-contract.test.mjs`,运行前仍须总控放行对应健康版本。
|
||||
|
||||
`deploy/scripts/run-ci-maven.sh` 是 Linux Jenkins 的受限 Maven 构建入口,不部署、不访问业务数据库、不安装依赖。总控及单服务 Job 共用 `firstleg-test-maven` 锁;同一构建账号另用 `$HOME/.cache/chongchong-ci/maven.lock` 文件锁,已有本机持锁任务时失败退出,不删除锁文件或杀其他进程。入口禁止 `-T/--threads`,固定 Maven 堆768MiB、元空间256MiB、直接内存128MiB,启用根POM `ci-bounded`:单个测试JVM、堆1GiB、元空间384MiB、直接内存128MiB,保留JaCoCo迟绑定;OOM快速退出,父进程丢失检测、停止时kill、单测试JVM20分钟上限。独立于Jenkins连接的本机GNU timeout在45分钟后TERM、30秒后KILL进程组,流水线另设50分钟超时;缺少flock/timeout/mvn直接失败。构建输出及Surefire报告沿现有Jenkins归档;失败先确认原进程退出再重跑,不自动清理缓存。该预算不等于总RSS硬限额,代理本身和JVM原生内存仍需保留余量;节点执行器应设为1、保持systemd control-group清理,并在真实Linux节点验证峰值。`ci-maven-resource-contract.test.mjs` 检查双入口接线、参数拒绝/失败传播和子JVM配置,Shell替身检查不代替Linux实际锁与信号验收。
|
||||
|
||||
`client-address-isolation-test-env.mjs` 是客户地址提交隔离的 TEST 定向负向入口,默认 `--plan` 零网络;执行要求显式 TEST 配置及 `--execute --environment=TEST --run-id=<新批次> --customer-profile=<同目录test-ui-*.env>`。分别登录两个既有客户,从来源客户读取仍有效的取货/私人地址、从目标客户读取实际仓库和渠道;向目标客户发两次单单与两次单行批量 JSON 创建请求,必须精准返回“客户地址不存在或已停用”,普通必填错误不算通过。每个载荷只含一种外客户地址,成功返回订单或未知结果立即停止,报告保存幂等键/载荷摘要/意外订单标识供定位,不自动重试、删除或恢复;结束退出两个独立会话。独占报告 `reports/client-address-isolation-<run-id>/`,不记录凭据、地址明文或账号。该项不等于 XLSX 解析拒绝、数据库零漂移或真实页面验证;对应 `.test.mjs` 检查精准错误分类与探针载荷。
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { warehouseQuoteContext, verifyBookingQuotes } from './fleet-booking-quotes-test-env.mjs'
|
||||
|
||||
test('warehouse date uses authoritative timezone and rejects ambiguous or customer-owned warehouse', () => {
|
||||
const detail = { warehouse: { id: '92', tenantId: '0', status: 'ACTIVE', warehouseType: 'OVERSEAS', timezone: 'America/Los_Angeles' }, addresses: [{ warehouseId: '92', tenantId: '0', city: 'LA' }] }
|
||||
assert.equal(warehouseQuoteContext(detail, '2026-10-04T01:00:00Z').day, '2026-10-03')
|
||||
assert.equal(warehouseQuoteContext({ ...detail, warehouse: { ...detail.warehouse, timezone: 'UTC-8' } }, '2026-10-04T01:00:00Z').day, '2026-10-03')
|
||||
assert.throws(() => warehouseQuoteContext({ ...detail, warehouse: { ...detail.warehouse, tenantId: '1' } }, '2026-10-04T01:00:00Z'))
|
||||
assert.throws(() => warehouseQuoteContext({ ...detail, addresses: [...detail.addresses, { ...detail.addresses[0], city: 'SF' }] }, '2026-10-04T01:00:00Z'))
|
||||
})
|
||||
function harness(fault) {
|
||||
const quotes = [], bookings = [], replay = new Map(), calls = []
|
||||
let trip = { tripNo: 'TR-OWN', recordVersion: 0 }, expired = false
|
||||
const context = { marker: 'TEST-CURRENT', createdResources: { merchantQualificationFixture: { marker: 'TEST-CURRENT', tenantId: '9' } }, async request(method, path, body) {
|
||||
calls.push({ method, path, body: structuredClone(body) }); const url = new URL(path, 'http://offline.invalid'), p = url.pathname
|
||||
const response = (data, code = '200') => ({ http: 200, body: { code, data: structuredClone(data) }, requestId: 'request', traceId: 'trace' })
|
||||
if (p.endsWith('/quote-candidates')) {
|
||||
const basis = url.searchParams.get('priceBasis'), currency = url.searchParams.get('currency')
|
||||
return response({ records: quotes.filter(q => !expired && q.details.validFrom === '2026-10-04' && q.details.currency === currency && q.details[basis === 'PALLET' ? 'looseDeliveryPrice' : 'fullDeliveryPrice'] != null).map(q => ({ quoteNo: q.quoteNo, recordVersion: q.recordVersion, quotedUnitPrice: q.details[basis === 'PALLET' ? 'looseDeliveryPrice' : 'fullDeliveryPrice'], priceItem: basis === 'PALLET' ? 'LOOSE_DELIVERY' : 'FULL_DELIVERY', effectiveOn: '2026-10-04' })) })
|
||||
}
|
||||
if (p.endsWith('/quotes') && method === 'POST') { const q = { quoteNo: `FQ-${quotes.length}`, recordVersion: 0, details: body.details }; quotes.push(q); return response(q) }
|
||||
if (p.includes('/quotes/FQ-')) { const q = quotes.find(q => p.endsWith(q.quoteNo)); q.details = body.details; q.recordVersion++; return response(q) }
|
||||
if (p.endsWith('/trips') && method === 'POST') return response({ trip })
|
||||
if (p.endsWith('/plan')) { expired = true; trip.recordVersion++; return response({ trip }) }
|
||||
if (p.endsWith('/confirm')) return response(null, fault === 'accepts-stale' ? '200' : 'A0409')
|
||||
if (p.endsWith('/bookings') && method === 'GET') return response({ tripRecordVersion: trip.recordVersion, records: bookings })
|
||||
if (p.endsWith('/bookings') && replay.has(body.idempotencyKey)) return response({ ...replay.get(body.idempotencyKey), replayed: true })
|
||||
const q = body.quoteRef ? quotes.find(q => q.quoteNo === body.quoteRef.quoteNo) : null
|
||||
if (q && q.recordVersion !== body.quoteRef.recordVersion) return response(null, 'A0409')
|
||||
let b
|
||||
if (method === 'POST') { b = { bookingNo: `FB-${bookings.length}`, status: 'DRAFT', recordVersion: 0 }; bookings.push(b) }
|
||||
else { b = bookings.find(b => p.endsWith(b.bookingNo)); b.recordVersion++ }
|
||||
Object.assign(b, { cargo: body.cargo, agreedAmount: body.cargo.unitPrice * body.cargo.palletSpaces, quoteSource: q ? { quoteNo: q.quoteNo, recordVersion: q.recordVersion, quotedUnitPrice: q.details.looseDeliveryPrice } : null })
|
||||
trip.recordVersion++
|
||||
const result = { tripRecordVersion: trip.recordVersion, booking: b, replayed: false }
|
||||
if (method === 'POST') replay.set(body.idempotencyKey, structuredClone(result))
|
||||
return response(result)
|
||||
} }
|
||||
return { context, calls }
|
||||
}
|
||||
test('independent draft fixture covers source version, date, currency, manual pricing without inventory or departure writes', async () => {
|
||||
const h = harness(), proof = await verifyBookingQuotes(h.context, { warehouseId: '92', originCity: 'la', day: '2026-10-04' }, '2026-10-04T12:00:00Z')
|
||||
assert.equal(proof.status, 'VERIFIED_DRAFT_SOURCE_LINKAGE'); assert.ok(proof.checks.every(c => c.passed))
|
||||
assert.ok(h.calls.every(c => !/inventory|\/depart$|\/assignment$|\/receipt-confirmation$/.test(c.path)))
|
||||
await assert.rejects(verifyBookingQuotes(h.context, {}, ''), /no restart/)
|
||||
})
|
||||
test('unexpected acceptance of stale confirmation cannot be reported as successful', async () => {
|
||||
const h = harness('accepts-stale')
|
||||
await assert.rejects(verifyBookingQuotes(h.context, { warehouseId: '92', originCity: 'la', day: '2026-10-04' }, '2026-10-04T12:00:00Z'), /A0409/)
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
import { readFile, mkdir, open } from 'node:fs/promises'
|
||||
import { resolve, dirname } 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 } from './fleet-quotes-test-env.mjs'
|
||||
import { run as prepareCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs'
|
||||
|
||||
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
||||
const MARKER = 'FLEET-BOOKING-QUOTE-20261003-R2'
|
||||
const TRIPS = '/api/platform/v1/last-leg/fleet/trips', QUOTES = '/api/platform/v1/last-leg/fleet/quotes'
|
||||
export function warehouseQuoteContext(detail, planned) {
|
||||
const w = detail?.warehouse
|
||||
if (!w?.id || String(w.tenantId) !== '0' || w.status !== 'ACTIVE' || !['OVERSEAS', 'OVERSEAS_PARTNER'].includes(w.warehouseType) || !w.timezone) throw Error('Warehouse quote context unavailable')
|
||||
const cities = [...new Set((detail.addresses || []).filter(a => String(a.warehouseId) === String(w.id) && String(a.tenantId) === '0' && a.city?.trim()).map(a => a.city.trim().toLowerCase()))]
|
||||
if (cities.length !== 1) throw Error('Warehouse quote city ambiguous')
|
||||
const offset = /^(?:UTC)?([+-])(\d{1,2})(?::(\d{2}))?$/.exec(w.timezone)
|
||||
if (offset && (Number(offset[2]) > 18 || Number(offset[3] || 0) > 59 || Number(offset[2]) === 18 && Number(offset[3] || 0) !== 0)) throw Error('Warehouse offset invalid')
|
||||
const day = offset ? new Date(Date.parse(planned) + (offset[1] === '-' ? -1 : 1) * (Number(offset[2]) * 60 + Number(offset[3] || 0)) * 60000).toISOString().slice(0, 10)
|
||||
: new Intl.DateTimeFormat('en-CA', { timeZone: w.timezone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date(planned))
|
||||
return { warehouseId: String(w.id), originCity: cities[0], timezone: w.timezone, day }
|
||||
}
|
||||
export async function verifyBookingQuotes(context, warehouse, planned) {
|
||||
if (context.createdResources.bookingQuoteAcceptance || context.createdResources.merchantQualificationFixture?.marker !== context.marker) throw Error('Fresh own fixture required; no restart')
|
||||
const proof = context.createdResources.bookingQuoteAcceptance = { marker: context.marker, status: 'CHECKING', warehouse, quotes: [], bookings: [], checks: [], confirmationSuccess: 'NOT_TESTED_REQUIRES_LOAD_PLAN', receiptAndDeparture: 'NOT_TESTED' }
|
||||
const check = (name, pass) => { proof.checks.push({ name, passed: Boolean(pass) }); if (!pass) throw Error(name) }
|
||||
const request = async (method, path, body, expected = '200') => {
|
||||
const r = await context.request(method, path, body)
|
||||
check(`${method} ${path.split('?')[0]} => ${expected}`, r.http === 200 && String(r.body?.code) === expected && r.requestId && r.traceId)
|
||||
return r.body.data
|
||||
}
|
||||
const customerId = String(context.createdResources.merchantQualificationFixture.tenantId)
|
||||
const destination = 'QA-BOOKING', stops = [{ name: '本轮报价测试起运仓' }, { name: destination, destinationKey: `FBA:${destination}` }]
|
||||
let trip = (await request('POST', TRIPS, { warehouseId: warehouse.warehouseId, plannedDepartureAt: planned, expectedDriverEndAt: new Date(Date.parse(planned) + 3600000).toISOString(), stops, remark: context.marker, idempotencyKey: `${context.marker}-trip` })).trip
|
||||
proof.tripNo = trip.tripNo
|
||||
const path = `${TRIPS}/${trip.tripNo}/bookings`
|
||||
const detail = { kind: 'SALE', partyId: customerId, originCity: warehouse.originCity, destinationCode: destination, distanceMiles: 10, currency: 'USD', selfPickupPrice: null, fullDeliveryPrice: 25, looseDeliveryPrice: 10, validFrom: warehouse.day, validTo: warehouse.day, remark: context.marker }
|
||||
const createQuote = async (suffix, change) => { const quote = await request('POST', QUOTES, { idempotencyKey: `${context.marker}-${suffix}`, details: { ...detail, ...change } }); proof.quotes.push(quote.quoteNo); return quote }
|
||||
const valid = await createQuote('valid', {})
|
||||
const expiredDay = new Date(Date.parse(`${warehouse.day}T00:00:00Z`) - 86400000).toISOString().slice(0, 10)
|
||||
await createQuote('expired', { distanceMiles: 11, validFrom: expiredDay, validTo: expiredDay })
|
||||
const cny = await createQuote('cny', { distanceMiles: 12, currency: 'CNY', fullDeliveryPrice: 35, looseDeliveryPrice: null })
|
||||
const zero = await createQuote('zero', { distanceMiles: 13, fullDeliveryPrice: null, looseDeliveryPrice: 0 })
|
||||
const candidates = (currency, priceBasis) => request('GET', `${path}/quote-candidates?${new URLSearchParams({ customerId, destinationKey: `FBA:${destination}`, currency, priceBasis, page: '1', size: '100' })}`)
|
||||
const pallet = await candidates('USD', 'PALLET'), order = await candidates('USD', 'ORDER'), cnyPallet = await candidates('CNY', 'PALLET'), cnyOrder = await candidates('CNY', 'ORDER')
|
||||
check('USD pallet candidate excludes expired/currency mismatch but preserves explicit zero', pallet.records.length === 2 && pallet.records.some(q => q.quoteNo === zero.quoteNo && Number(q.quotedUnitPrice) === 0) && pallet.records.some(q => q.quoteNo === valid.quoteNo))
|
||||
check('ORDER selects full-delivery instead of loose-delivery price', order.records.length === 1 && order.records[0].quoteNo === valid.quoteNo && order.records[0].priceItem === 'FULL_DELIVERY' && Number(order.records[0].quotedUnitPrice) === 25)
|
||||
check('Missing CNY pallet price is not zero', cnyPallet.records.length === 0 && cnyOrder.records.length === 1 && cnyOrder.records[0].quoteNo === cny.quoteNo)
|
||||
check('Server date equals authoritative warehouse local planned day', pallet.records.every(q => q.effectiveOn === warehouse.day))
|
||||
const cargo = { destinationKey: `FBA:${destination}`, cargoName: '报价联动测试', packageCount: 2, palletSpaces: 2, weightKg: 10, volumeCbm: 1, priceBasis: 'PALLET', unitPrice: 7, currency: 'USD', contactName: null, contactPhone: null, remark: context.marker }
|
||||
const reference = { quoteNo: valid.quoteNo, recordVersion: valid.recordVersion, priceItem: 'LOOSE_DELIVERY' }
|
||||
const firstBody = { tripRecordVersion: trip.recordVersion, customerId, cargo, quoteRef: reference, idempotencyKey: `${context.marker}-booking` }
|
||||
const first = await request('POST', path, firstBody); trip.recordVersion = first.tripRecordVersion; proof.bookings.push(first.booking.bookingNo)
|
||||
check('Source price frozen separately from negotiated amount', Number(first.booking.quoteSource.quotedUnitPrice) === 10 && Number(first.booking.cargo.unitPrice) === 7 && Number(first.booking.agreedAmount) === 14 && String(first.booking.quoteSource.recordVersion) === String(valid.recordVersion))
|
||||
const manual = await request('POST', path, { tripRecordVersion: trip.recordVersion, customerId, cargo: { ...cargo, unitPrice: 0 }, quoteRef: null, idempotencyKey: `${context.marker}-manual` }); trip.recordVersion = manual.tripRecordVersion; proof.bookings.push(manual.booking.bookingNo)
|
||||
check('Manual zero-price draft never gains a mandatory source', manual.booking.quoteSource == null && Number(manual.booking.agreedAmount) === 0)
|
||||
const changed = await request('PUT', `${QUOTES}/${valid.quoteNo}`, { recordVersion: valid.recordVersion, details: { ...detail, looseDeliveryPrice: 12 } })
|
||||
const replay = await request('POST', path, firstBody)
|
||||
check('Exact create replay retains original source snapshot after quote edit', replay.replayed === true && Number(replay.booking.quoteSource.quotedUnitPrice) === 10)
|
||||
await request('POST', path, { ...firstBody, tripRecordVersion: trip.recordVersion, idempotencyKey: `${context.marker}-stale` }, 'A0409')
|
||||
await request('POST', `${path}/${first.booking.bookingNo}/confirm`, { tripRecordVersion: trip.recordVersion, recordVersion: first.booking.recordVersion, idempotencyKey: `${context.marker}-stale-confirm` }, 'A0409')
|
||||
const afterReject = await request('GET', path)
|
||||
check('Stale source rejection does not mutate booking or parent version', String(afterReject.tripRecordVersion) === String(trip.recordVersion) && afterReject.records.find(b => b.bookingNo === first.booking.bookingNo)?.status === 'DRAFT')
|
||||
const refreshed = await candidates('USD', 'PALLET'), current = refreshed.records.find(q => q.quoteNo === valid.quoteNo)
|
||||
check('Explicit refresh exposes new source version and rate', String(current?.recordVersion) === String(changed.recordVersion) && Number(current?.quotedUnitPrice) === 12)
|
||||
const updated = await request('PUT', `${path}/${first.booking.bookingNo}`, { tripRecordVersion: trip.recordVersion, recordVersion: first.booking.recordVersion, cargo, quoteRef: { ...reference, recordVersion: current.recordVersion } }); trip.recordVersion = updated.tripRecordVersion
|
||||
check('Explicit reselection refreshes source while preserving negotiated price', Number(updated.booking.quoteSource.quotedUnitPrice) === 12 && Number(updated.booking.agreedAmount) === 14)
|
||||
const later = new Date(Date.parse(planned) + 2 * 86400000).toISOString()
|
||||
trip = (await request('PUT', `${TRIPS}/${trip.tripNo}/plan`, { recordVersion: trip.recordVersion, plannedDepartureAt: later, expectedDriverEndAt: new Date(Date.parse(later) + 3600000).toISOString(), stops, remark: context.marker, reason: '仅本轮报价有效日期验证' })).trip
|
||||
check('Plan date outside validity removes candidates', (await candidates('USD', 'PALLET')).records.length === 0)
|
||||
await request('POST', `${path}/${first.booking.bookingNo}/confirm`, { tripRecordVersion: trip.recordVersion, recordVersion: updated.booking.recordVersion, idempotencyKey: `${context.marker}-expired-confirm` }, 'A0409')
|
||||
proof.finalBookings = await request('GET', path); proof.status = 'VERIFIED_DRAFT_SOURCE_LINKAGE'; proof.cleanup = 'RETAINED_OWN_PLANS_QUOTES_AND_DRAFTS_NO_RESOURCE_ASSIGNMENT'
|
||||
return proof
|
||||
}
|
||||
export async function execute(args = process.argv.slice(2), execArgs = process.execArgv) {
|
||||
const envFile = requireTestProfile(args, execArgs), env = parseEnv(await readFile(envFile, 'utf8'))
|
||||
if (env.CHONGCHONG_ENV !== 'test' || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw 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 Error('Invalid TEST API')
|
||||
// R1 stopped before creating any business fixture because its offset parser rejected valid Java ZoneId UTC-8.
|
||||
// Preserve that evidence. This corrected run has a distinct claim, marker and request-key namespace.
|
||||
const directory = resolve(root, 'reports/fleet-booking-quotes-live-20261003-r2')
|
||||
await mkdir(directory, { recursive: true })
|
||||
const context = new AcceptanceRun({ environment: 'test', gateway: base.origin, username: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD, customerName: `选价测试客户-${MARKER}`, quiet: true })
|
||||
context.marker = MARKER
|
||||
const journal = await installQuoteJournal(context, directory, { secrets: [env.CHONGCHONG_ADMIN_PASSWORD, env.CHONGCHONG_ADMIN_USERNAME], persist: async (name, data) => {
|
||||
const file = await open(resolve(directory, name), 'wx', 0o600)
|
||||
try { await file.writeFile(JSON.stringify({ ...data, marker: MARKER, scope: 'BOOKING_QUOTE_ONLY', deployedCommit: '4ff55ba3e08c7fe0c6e04832fa95ebb680c76b8c' }, null, 2)); await file.sync() } finally { await file.close() }
|
||||
} })
|
||||
let error = null, loggedOut = false
|
||||
try {
|
||||
const login = await context.request('POST', '/api/auth/login', { loginId: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD }, '')
|
||||
if (login.http !== 200 || String(login.body?.code) !== '200' || !login.body.data?.accessToken || !login.body.data?.refreshToken) throw Error('Login failed')
|
||||
context.token = login.body.data.accessToken; context.refreshToken = login.body.data.refreshToken
|
||||
const options = await context.request('GET', '/api/platform/v1/last-leg/fleet/drivers/warehouse-options?region=OVERSEAS')
|
||||
if (String(options.body?.code) !== '200' || !Array.isArray(options.body.data)) throw Error('Warehouse directory failed')
|
||||
const planned = new Date(Date.now() + 86400000).toISOString(); let warehouse
|
||||
for (const option of options.body.data.slice(0, 20)) {
|
||||
const detail = await context.request('GET', `/api/platform/v1/warehouse-admin/${encodeURIComponent(option.id)}?tenantId=0`)
|
||||
if (String(detail.body?.code) !== '200') throw Error('Warehouse details failed')
|
||||
try { warehouse = warehouseQuoteContext(detail.body.data, planned); break } catch { /* Read next real active warehouse; never write missing master data. */ }
|
||||
}
|
||||
if (!warehouse) throw Error('No active warehouse has unambiguous city/timezone; no business fixtures created')
|
||||
context.createdResources.quoteWarehouse = warehouse
|
||||
await context.executeModule('merchant-qualification-fixture', () => prepareCustomer(context, { publishEnvironment: false }))
|
||||
if (context.steps.some(s => !s.passed)) throw Error('Fresh customer fixture failed')
|
||||
await verifyBookingQuotes(context, warehouse, planned)
|
||||
} catch (cause) { error = cause.message; if (context.createdResources.bookingQuoteAcceptance) context.createdResources.bookingQuoteAcceptance.status = 'FAILED' }
|
||||
finally {
|
||||
try { if (context.token) { const r = await context.request('POST', '/api/auth/logout', { refreshToken: context.refreshToken }); loggedOut = r.http === 200 && String(r.body?.code) === '200' } } catch { error ||= 'Logout outcome unknown' }
|
||||
context.token = ''; context.refreshToken = ''
|
||||
}
|
||||
const report = { success: !error && loggedOut && !journal.unknown(), error, loggedOut, createdResources: context.createdResources, steps: context.steps, ui: 'NOT_TESTED', operations: 'NOT_TESTED' }
|
||||
await journal.checkpoint('final', report)
|
||||
return { success: report.success, directory }
|
||||
}
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) execute().then(r => { console.log(JSON.stringify(r)); process.exitCode = r.success ? 0 : 1 }).catch(() => { console.error('Booking quote run blocked; preserve checkpoints, no automatic rerun.'); process.exitCode = 1 })
|
||||
Reference in New Issue
Block a user