Files
baiyue-firstleg/scripts/fba-remaining-nodes-acceptance.mjs
T

123 lines
11 KiB
JavaScript

// Internal evidence helper; invoked explicitly by the assigned audit, never runs on import.
import fs from 'node:fs'
import path from 'node:path'
import crypto from 'node:crypto'
export const digest = value => crypto.createHash('sha256').update(typeof value === 'string' ? value : JSON.stringify(value)).digest('hex')
export function selectSteps(report, predicate) {
return (report.steps || []).flatMap((step, index) => predicate(step) ? [{ pointer: `/steps/${index}`, ...step }] : [])
}
export function requirePassed(steps) {
return steps.length > 0 && steps.every(s => s.passed === true)
}
export function transferConclusion(detail) {
const shipped = Boolean(detail.actualShipAt)
const terminal = detail.status === 'RECEIVED'
const received = (detail.allocations || []).length > 0 && detail.allocations.every(a => a.status === 'RECEIVED' && a.destinationInventoryPositionId)
return { status: detail.status, orderStatus: detail.orderStatus, transferNo: detail.transferNo,
allocationCount: detail.allocations?.length || 0, packageCount: detail.packageCount,
sourceCount: detail.sourceOrders?.length || 0, actualShipAt: detail.actualShipAt || null,
completed: terminal && shipped && received, requiresRealOperation: !(terminal && shipped && received) }
}
export async function auditRemainingNodes({ root, output, env, live = false }) {
if (fs.existsSync(output)) throw Error('REPORT_EXISTS_NO_OVERWRITE')
const read = relative => JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8'))
const files = ['reports/node-completion-audit-root-20261004-r1.json',
'reports/admin-full-current-20261002-r4.json', 'reports/overseas-receiving-current-20261002-r1.json',
'reports/fba-core-f40-tax-fleet-20261003-r1/root_first.json',
'reports/f26-loaded-transit-verification-20261004-r2.json',
'reports/discard-review-entry-root-20261004-r1/final-evidence.json']
const reports = Object.fromEntries(files.map(f => [f, read(f)]))
const full = reports[files[1]]
const evidence = (file, predicate) => ({ file, sha256: digest(fs.readFileSync(path.join(root, file), 'utf8')),
steps: selectSteps(reports[file], predicate) })
const result = { checkedAt: new Date().toISOString(), goalComplete: false,
scope: 'Existing TEST evidence only; no business commands, SQL, fault injection, deployment or UI operation',
sourceFiles: files.map(file => ({ file, sha256: digest(fs.readFileSync(path.join(root, file), 'utf8')) })),
integrationRequired: { file: 'scripts/README.md', owner: '总控',
reason: 'Current write boundary excludes shared script index; documentation contract identifies this new helper as undocumented.',
text: '`scripts/fba-remaining-nodes-acceptance.mjs`:本轮FBA剩余节点内部证据助手,仅显式调用auditRemainingNodes;读取指定原报告并按JSON pointer/SHA-256保留证据。live=true仅使用明确TEST环境文件的独立Auth会话与串行GET,正常退出;不执行业务写命令、SQL、故障注入、部署或页面操作。报告reports/fba-remaining-nodes-20261004/,既有报告拒绝覆盖,失败保留且不自动重试。对应离线测试fba-remaining-nodes-acceptance.test.mjs。' },
nodes: [], requests: [], sessionClosed: !live }
result.nodes.push({ node: 'F23.2', conclusion: 'HTTP actual AT_ORIGIN_PORT supported; Admin save and two-end projection remain unverified',
category: '需真实操作', evidence: [evidence(files[3], s => /AT_ORIGIN_PORT/.test(s.name))],
next: 'Use a current not-yet-at-origin-port sample; freeze query version/history and customer projection, record actual node once through Admin, capture POST and both readbacks; preserve customs/container identity and do not rewrite a later actual node.' })
result.nodes.push({ node: 'F25', conclusion: 'Clearance/DO upload and replay, fleet/appointment/empty-return supported; PU, distinct actual pickup and mail lifecycle unverified',
category: '需真实操作', evidence: [evidence(files[1], s => s.module === 'phase5-terminal-drayage'),
evidence(files[1], s => s.module === 'phase3-container-documents' && /DELIVERY_ORDER|DESTINATION_CLEARANCE/.test(s.name))],
boundaries: { mailRequestsInOriginalReport: full.steps.filter(s => /terminal-drayages.*\/emails?|terminal-drayages.*pickup-email/.test(s.request?.path || '')).length,
pickupAssertion: 'Original drayage assertion compares actualPickupAt to unloadingCompletedAt; this does not establish a distinct pickup command/time.' },
next: 'Freeze pre-pickup container and actual node history; inspect current DO/PU contract, confirm distinct PICKED_UP before unloading; capture explicit email queue/recipient snapshot, SMTP acceptance separately, fleet-change ordered cancellation/request and failed-mail retry without assuming delivery.' })
result.nodes.push({ node: 'F27', conclusion: 'Normal conversion/resources/replay supported; resource-failure retry UI unverified',
category: '需真实操作', evidence: [evidence(files[1], s => s.module === 'phase4-tail-order-controls')],
explicitRetryEvidence: selectSteps(full, s => /资源.*重试|计划.*重试|重试.*资源/.test(s.name)),
next: 'First locate a real resourcePendingPlanCount>0 container; freeze plans, reservations, resource identities and original command key. Use original container conversion UI retry once, verify only failed resources finish and no new plans/reservations; healthy completed samples cannot prove recovery. Failure injection requires controller window.' })
result.nodes.push({ node: 'F33-transfer', conclusion: 'Original stock transfer approved; outbound/target receipt terminal not established',
category: '需真实操作', originalTransferNo: 'WT-WH-20261002-430594',
evidence: [evidence(files[1], s => (s.request?.path || '').includes('WT-WH-20261002-430594'))],
originalFixture: full.createdResources.phase4OverseasClientServices,
next: 'Continue this exact original transfer only after current source stock, holds, destination and allocations are frozen. Verify allocation, actual outbound and target receipt with per-position balances/unique movements and same-command replay. Incoming transfer fixtures are separate sources and cannot replace this ticket.' })
result.nodes.push({ node: 'F33-discard', conclusion: 'Rejection release supported; approval/decrement decision pending',
category: '缺用户输入', evidenceFiles: [files[5]], next: 'Await approval-time versus PDA actual-quantity decrement decision; no approval or decrement issued.' })
result.nodes.push({ node: 'F26', conclusion: 'Existing 68-check source/transit scope is already evidenced; no repeat run',
category: '可立即关闭指定分支', evidenceFiles: [files[4]], boundary: 'Only 11 FBA sources/13 actual cartons and pre-receipt mapping; not all event faults or UI.' })
result.nodes.push({ node: 'F24-external', conclusion: 'External carrier lifecycle lacks valid input',
category: '缺用户输入', next: 'Provide valid carrier TEST input; synthetic fixture or manual nodes cannot close supplier lifecycle.' })
const save = () => { fs.mkdirSync(path.dirname(output), { recursive: true }); fs.writeFileSync(output, JSON.stringify(result, null, 2)) }
save()
if (!live) return result
let session
const call = async (method, route, body, token = session?.accessToken) => {
if (method !== 'GET' && !['/auth/login', '/auth/logout'].includes(route)) throw Error('BUSINESS_WRITE_FORBIDDEN')
const entry = { method, path: route, phase: 'INTENT' }; result.requests.push(entry); save()
const response = await fetch(env.CHONGCHONG_ADMIN_API_BASE_URL.replace(/\/$/, '') + route, {
method, redirect: 'error', signal: AbortSignal.timeout(20000),
headers: { Accept: 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}),
...(body === undefined ? {} : { 'Content-Type': 'application/json' }) },
...(body === undefined ? {} : { body: JSON.stringify(body) }) })
const json = JSON.parse(await response.text())
Object.assign(entry, { phase: 'ACKNOWLEDGED', http: response.status, code: String(json.code),
requestId: response.headers.get('x-request-id'), traceId: response.headers.get('x-trace-id') }); save()
if (response.status !== 200 || String(json.code) !== '200') throw Error(`READ_FAILED_${response.status}_${json.code}`)
return json.data
}
try {
if (String(env.CHONGCHONG_ENV).toUpperCase() !== 'TEST' || !env.CHONGCHONG_ADMIN_API_BASE_URL || !env.CHONGCHONG_ADMIN_USERNAME || !env.CHONGCHONG_ADMIN_PASSWORD) throw Error('EXPLICIT_TEST_ENV_REQUIRED')
session = await call('POST', '/auth/login', { loginId: env.CHONGCHONG_ADMIN_USERNAME, password: env.CHONGCHONG_ADMIN_PASSWORD }, '')
const transferRoute = '/platform/v1/last-leg/warehouse-transfers/WT-WH-20261002-430594?customerId=2105875471489142785'
const before = await call('GET', transferRoute)
if (before.transferNo !== 'WT-WH-20261002-430594') throw Error('TRANSFER_SCOPE_MISMATCH')
result.nodes.find(n => n.node === 'F33-transfer').current = transferConclusion(before)
const container = full.createdResources.phase5TerminalDrayage.containerBookingId
const drayage = await call('GET', `/platform/v1/terminal-drayages/${container}`)
const history = await call('GET', `/platform/v1/terminal-drayages/${container}/history`)
const mailbox = await call('GET', `/platform/v1/terminal-drayages/${container}/emails`)
result.nodes.find(n => n.node === 'F25').current = {
containerNo: drayage.containerNo, status: drayage.status, actualPickupAt: drayage.actualPickupAt,
unloadingCompletedAt: drayage.unloadingCompletedAt, actualEmptyReturnAt: drayage.actualEmptyReturnAt,
pickupEqualsUnloading: drayage.actualPickupAt === drayage.unloadingCompletedAt,
historyActions: history.map(h => h.action), mailboxCount: mailbox.records?.length || 0,
mailStatuses: (mailbox.records || []).map(m => ({ status: m.status, kind: m.kind })) }
const overview = await call('GET', `/platform/v1/containers/overseas/${container}/overview`)
Object.assign(result.nodes.find(n => n.node === 'F25').current, {
pickupCodePresent: Boolean(overview.container?.pickupCode),
documents: (overview.documents || []).map(d => ({ documentType: d.documentType, version: d.version,
filePresent: Boolean(d.fileId), completed: d.completed === true })) })
const preview = await call('GET', `/platform/v1/last-leg/inbound-plans/container-preview?sourceContainerBookingId=${container}`)
result.nodes.find(n => n.node === 'F27').current = { containerNo: preview.containerNo,
totalOrderCount: preview.totalOrderCount, pendingOrderCount: preview.pendingOrderCount,
blockedOrderCount: preview.blockedOrderCount, totalPlannedPalletCount: preview.totalPlannedPalletCount,
plans: (preview.groups || []).flatMap(g => (g.orders || []).map(o => ({ planNo: o.existingPlanNo, planStatus: o.planStatus }))),
recoveryProven: false }
const after = await call('GET', transferRoute)
result.transferReadStable = digest(before) === digest(after)
result.liveReadSuccess = result.transferReadStable
} catch (error) { result.liveReadSuccess = false; result.failure = String(error.message).replace(/https?:\/\/\S+/g, '[redacted]') }
finally {
if (!session) result.sessionClosed = true
if (session) { try { await call('POST', '/auth/logout', { refreshToken: session.refreshToken }); result.sessionClosed = true }
catch { result.sessionClosed = false; result.liveReadSuccess = false } }
save()
}
return result
}