123 lines
11 KiB
JavaScript
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
|
|
}
|