From 771a3596f1a5259e143496ecf44f06b30bb42f5a Mon Sep 17 00:00:00 2001 From: AgentRun Codex Date: Sun, 12 Jul 2026 22:24:00 +0000 Subject: [PATCH] fix: project analyzer generated time --- .../hwlab-node-web-observe-analyzer-source.ts | 1 + ...hwlab-node-web-sentinel-p5-observe.test.ts | 30 +++++++++++++++++-- .../src/hwlab-node-web-sentinel-p5-observe.ts | 6 ++-- .../web-observe-analyzer-triad.test.ts | 21 ++++++++++--- .../web-probe-observe-options.test.ts | 7 ++++- scripts/src/hwlab-node/web-probe-observe.ts | 4 +-- .../monitor-terminal-ingest-migration.test.ts | 4 ++- scripts/src/monitor-terminal-ingest.ts | 2 +- 8 files changed, 61 insertions(+), 14 deletions(-) diff --git a/scripts/src/hwlab-node-web-observe-analyzer-source.ts b/scripts/src/hwlab-node-web-observe-analyzer-source.ts index 2135a58e..d1919e5f 100644 --- a/scripts/src/hwlab-node-web-observe-analyzer-source.ts +++ b/scripts/src/hwlab-node-web-observe-analyzer-source.ts @@ -166,6 +166,7 @@ console.log(JSON.stringify({ ok: true, command: "web-probe-observe analyze", stateDir, + generatedAt: report.generatedAt, jsonlScope: report.jsonlScope, reportJsonPath, reportJsonSha256: jsonMeta.sha256, diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.test.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.test.ts index 8da2dddc..f06c27fa 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.test.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.test.ts @@ -1,11 +1,12 @@ import assert from "node:assert/strict"; -import { mkdtempSync, rmSync } from "node:fs"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { test } from "bun:test"; import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes"; -import { classifyQuickVerifyCleanupStep, quickVerifyCleanupFindings, sentinelServiceProxyInvocation } from "./hwlab-node-web-sentinel-p5-observe"; +import { classifyQuickVerifyCleanupStep, quickVerifyCleanupFindings, readAnalysisSummaryFromWorkspace, sentinelServiceProxyInvocation } from "./hwlab-node-web-sentinel-p5-observe"; import { createWebProbeSentinelService } from "./hwlab-node-web-sentinel-service"; test("sentinel service proxy executes its script through trans standard input", () => { @@ -15,6 +16,31 @@ test("sentinel service proxy executes its script through trans standard input", }); }); +test("workspace analysis fallback maps report generatedAt to reportUpdatedAt", () => { + const directory = mkdtempSync(join(tmpdir(), "unidesk-workspace-analysis-")); + try { + const stateDir = ".state/web-observe/webobs-fixture-20260712T000000Z"; + mkdirSync(join(directory, stateDir, "analysis"), { recursive: true }); + writeFileSync(join(directory, stateDir, "analysis", "report.json"), JSON.stringify({ generatedAt: "2026-07-12T00:00:00.000Z", findings: [] })); + const summary = readAnalysisSummaryFromWorkspace({} as never, stateDir, 5, (script) => { + const result = spawnSync("sh", { cwd: directory, input: script, encoding: "utf8" }); + return { + command: ["sh"], + cwd: directory, + exitCode: result.status, + stdout: result.stdout ?? "", + stderr: result.stderr ?? "", + signal: result.signal, + timedOut: false, + }; + }); + assert.equal(summary.ok, true, JSON.stringify(summary)); + assert.equal(summary.reportUpdatedAt, "2026-07-12T00:00:00.000Z", JSON.stringify(summary)); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + test("recorded sentinel run is available through the latest report index", () => { const stateRoot = mkdtempSync(join(tmpdir(), "unidesk-sentinel-index-")); const service = createWebProbeSentinelService({ diff --git a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts index d096393e..5ffdfdc5 100644 --- a/scripts/src/hwlab-node-web-sentinel-p5-observe.ts +++ b/scripts/src/hwlab-node-web-sentinel-p5-observe.ts @@ -1488,7 +1488,7 @@ function compactAnalyzeBrowserProcess(value: unknown): Record | }; } -function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number): Record { +export function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number, execute: (script: string) => CommandResult = (script) => runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 })): Record { if (!isSafeRelativeStateDir(stateDir)) return { ok: false, reason: "unsafe-state-dir", stateDir, valuesRedacted: true }; const waitMs = Math.max(0, Math.min(50_000, (Math.max(5, timeoutSeconds) * 1000) - 5000)); const script = [ @@ -1517,10 +1517,10 @@ function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: st "function pageMemory(page){const eff=rec(page.effectiveMemory); const heap=rec(page.heapUsage); const perf=rec(page.performance); const metrics=rec(perf.metrics); const heapUsed=num(eff.heapUsedMb)??mb(heap.usedSize); const jsHeap=num(eff.jsHeapUsedMb)??mb(metrics.JSHeapUsedSize); const memory=heapUsed??jsHeap; if(memory==null)return null; return {memoryMb:memory,heapUsedMb:heapUsed,jsHeapUsedMb:jsHeap,effectiveHeapUsedMb:num(eff.effectiveHeapUsedMb),effectiveJsHeapUsedMb:num(eff.effectiveJsHeapUsedMb),domNodes:num(eff.domNodes)??num(rec(page.domCounters).nodes)??num(metrics.Nodes)}; }", "function browserProcessPageSeries(){const p=path.join(stateDir,'browser-process.jsonl'); let lines=[]; try{lines=String(read(p)||'').split(/\\r?\\n/).filter(Boolean)}catch{} const map=new Map(); let firstTs=null; for(const line of lines){let row=null; try{row=JSON.parse(line)}catch{} if(!row||row.type!=='browser-process-sample')continue; const ts=row.ts||null; const tsMs=Date.parse(String(ts||'')); if(!ts||!Number.isFinite(tsMs))continue; for(const page of arr(row.pages)){const memory=pageMemory(rec(page)); if(!memory)continue; if(firstTs==null)firstTs=tsMs; const role=clip(page.pageRole||'page',32); const id=clip(page.pageId||`${role}-${page.pageEpoch??map.size}`,80); const key=`${role}:${id}`; const current=map.get(key)||{key,pageRole:role,pageId:id,label:`${role} ${String(id).slice(0,10)}`,url:clip(page.url,180),points:[],valuesRedacted:true}; current.points.push({ts,elapsedSeconds:Math.max(0,Math.round((tsMs-firstTs)/1000)),elapsedMinutes:Math.round(Math.max(0,(tsMs-firstTs)/60000)*100)/100,...memory,valuesRedacted:true}); map.set(key,current);} } return Array.from(map.values()).filter((item)=>item.points.length>0); }", "const browserPageSeries=browserProcessPageSeries();", - "console.log(JSON.stringify({ok:!!report,reason:report?null:(jsonBuf?'report-json-parse-failed':'report-json-missing'),reportReadWaitMs:waitMs,reportParseError:clip(reportParseError,220),reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportJsonBytes:jsonBuf.length,reportUpdatedAt:typeof report?.updatedAt==='string'?report.updatedAt:(typeof report?.analyzedAt==='string'?report.analyzedAt:null),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,browserProcess:{source:'analysis-browser-process-jsonl',unit:'MB',metric:'page-heap-used',pageCount:browserPageSeries.length,sampleCount:browserPageSeries.reduce((sum,item)=>sum+item.points.length,0),pageSeries:browserPageSeries,valuesRedacted:true},requestRate:compactRequestRate(report?.requestRate),valuesRedacted:true}));", + "console.log(JSON.stringify({ok:!!report,reason:report?null:(jsonBuf?'report-json-parse-failed':'report-json-missing'),reportReadWaitMs:waitMs,reportParseError:clip(reportParseError,220),reportOk:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportJsonBytes:jsonBuf.length,reportUpdatedAt:typeof report?.updatedAt==='string'?report.updatedAt:(typeof report?.analyzedAt==='string'?report.analyzedAt:(typeof report?.generatedAt==='string'?report.generatedAt:null)),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,browserProcess:{source:'analysis-browser-process-jsonl',unit:'MB',metric:'page-heap-used',pageCount:browserPageSeries.length,sampleCount:browserPageSeries.reduce((sum,item)=>sum+item.points.length,0),pageSeries:browserPageSeries,valuesRedacted:true},requestRate:compactRequestRate(report?.requestRate),valuesRedacted:true}));", "NODE", ].join("\n"); - const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); + const result = execute(script); const parsedResolution = resolveCliChildJsonCommandResult({ result, requestedStdoutType: "web-probe sentinel quick-verify analysis summary JSON", diff --git a/scripts/src/hwlab-node/web-observe-analyzer-triad.test.ts b/scripts/src/hwlab-node/web-observe-analyzer-triad.test.ts index 89882ada..e1c16953 100644 --- a/scripts/src/hwlab-node/web-observe-analyzer-triad.test.ts +++ b/scripts/src/hwlab-node/web-observe-analyzer-triad.test.ts @@ -6,6 +6,8 @@ import { spawnSync } from "node:child_process"; import { test } from "bun:test"; import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source"; +import { compactWebObserveAnalyzeAnalysisForRaw } from "./web-probe-observe"; +import { buildMonitorTerminalIngest } from "../monitor-terminal-ingest"; const alertThresholds = { sameOriginApiSlowMs: 60000, @@ -65,7 +67,7 @@ const browserFreezePolicy = { }, }; -async function runObserveAnalyzer(files: Record): Promise> { +async function runObserveAnalyzer(files: Record): Promise<{ report: Record; stdout: Record }> { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-")); const analyzerPath = join(stateDir, "analyze.mjs"); await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 }); @@ -83,9 +85,20 @@ async function runObserveAnalyzer(files: Record): Promise { + const output = await runObserveAnalyzer({ "samples.jsonl": "" }); + assert.equal(output.stdout.generatedAt, output.report.generatedAt); + const compact = compactWebObserveAnalyzeAnalysisForRaw(output.stdout); + const ingest = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "analyzer-stdout" }, compact, [{ relativePath: "analysis/report.json", kind: "report-json", sha256: output.stdout.reportJsonSha256 as string, sizeBytes: output.report ? 1 : 0 }], { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: output.stdout.stateDir as string }); + assert.equal(ingest.updatedAt, output.report.generatedAt); +}); + test("observe analyzer classifies stale completed session rail when a newer turn is running", async () => { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-")); const analyzerPath = join(stateDir, "analyze.mjs"); @@ -226,7 +239,7 @@ test("observe analyzer flags automatic recovery legacy fanout authority", async JSON.stringify({ ts: "2026-07-01T15:21:30.000Z", type: "response", method: "GET", status: 200, url: "https://hwlab.example.test/v1/workbench/sync?sessionId=ses_auto&traceId=trc_auto&since=7", bodySummary: { pathKind: "workbench-sync-replay", eventCount: 1, cursorOutboxSeq: 12, realtimeAuthority: "workbench-realtime-authority-v2", terminalSeal: true } }) ].join("\n") + "\n"; - const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network }); + const { report } = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network }); const finding = report.findings.find((item: Record) => item.id === "workbench-automatic-recovery-fanout-authority"); assert.equal(finding?.rootCause, "workbench_automatic_recovery_legacy_fanout"); @@ -280,7 +293,7 @@ test("observe analyzer allows explicit detail trace reads when sync authority ev }) ].join("\n") + "\n"; - const report = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network }); + const { report } = await runObserveAnalyzer({ "samples.jsonl": samples, "network.jsonl": network }); const finding = report.findings.find((item: Record) => item.id === "workbench-automatic-recovery-fanout-authority"); assert.equal(finding, undefined); diff --git a/scripts/src/hwlab-node/web-probe-observe-options.test.ts b/scripts/src/hwlab-node/web-probe-observe-options.test.ts index 7e89ca24..d281c524 100644 --- a/scripts/src/hwlab-node/web-probe-observe-options.test.ts +++ b/scripts/src/hwlab-node/web-probe-observe-options.test.ts @@ -30,7 +30,7 @@ test("artifact recovery helper projects report time without filesystem mtime", ( assert.equal(recovered?.reportUpdatedAt, "2026-07-12T00:00:00.000Z"); assert.match(recoveryScript, /const reportJson = objectOrNull\(readJson\(reportJsonPath\)\) \|\| \{\};/u); assert.equal((recoveryScript.match(/const reportJson =/gu) ?? []).length, 1); - assert.match(recoveryScript, /source\.reportUpdatedAt \?\? source\.updatedAt \?\? source\.analyzedAt \?\? reportJson\.reportUpdatedAt \?\? reportJson\.updatedAt \?\? reportJson\.analyzedAt/u); + assert.match(recoveryScript, /source\.reportUpdatedAt \?\? source\.updatedAt \?\? source\.analyzedAt \?\? reportJson\.reportUpdatedAt \?\? reportJson\.updatedAt \?\? reportJson\.analyzedAt \?\? source\.generatedAt \?\? reportJson\.generatedAt/u); assert.doesNotMatch(recoveryScript, /mtime/u); const marker = "UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT"; const start = recoveryScript.indexOf(`<<'${marker}'\n`) + marker.length + 5; @@ -41,6 +41,11 @@ test("artifact recovery helper projects report time without filesystem mtime", ( writeFileSync(programPath, recoveryScript.slice(start, end)); const syntax = spawnSync(process.execPath, ["--check", programPath], { encoding: "utf8" }); assert.equal(syntax.status, 0, syntax.stderr); + const reportJsonPath = join(directory, "report.json"); + writeFileSync(reportJsonPath, JSON.stringify({ generatedAt: "2026-07-12T00:00:00.000Z" })); + const executed = spawnSync(process.execPath, [programPath, directory, join(directory, "stdout.json"), join(directory, "stderr.log"), reportJsonPath, join(directory, "report.md"), "124", "true"], { encoding: "utf8" }); + assert.equal(executed.status, 0, executed.stderr); + assert.equal(JSON.parse(executed.stdout).reportUpdatedAt, "2026-07-12T00:00:00.000Z"); } finally { rmSync(directory, { recursive: true, force: true }); } diff --git a/scripts/src/hwlab-node/web-probe-observe.ts b/scripts/src/hwlab-node/web-probe-observe.ts index 379db79e..ce2864ef 100644 --- a/scripts/src/hwlab-node/web-probe-observe.ts +++ b/scripts/src/hwlab-node/web-probe-observe.ts @@ -2220,7 +2220,7 @@ export function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record 0 ? arr(source.pagePerformanceSlowApi) : arr(pagePerformance.sameOriginApiByPath).filter((item) => Number(item?.overBudgetCount ?? item?.overFiveSecondCount ?? 0) > 0);", "const compact = { ok: source.ok === true, command: source.command ?? 'web-probe-observe analyze', stateDir: source.stateDir ?? stateDir, jsonlScope: source.jsonlScope ?? null, alertThresholds: source.alertThresholds ?? null, counts: source.counts ?? null, archiveSummary: { ...archiveSummary, sampleMetrics: archiveSampleMetrics, pagePerformance: objectOrNull(archiveSummary.pagePerformance) || objectOrNull(pagePerformance.summary) || {}, runtimeAlerts: objectOrNull(archiveSummary.runtimeAlerts) || objectOrNull(runtimeAlerts.summary) || {}, browserProcess: objectOrNull(archiveSummary.browserProcess) || objectOrNull(browserProcess.summary) || {}, requestRate: objectOrNull(archiveSummary.requestRate) || requestRateSummary || {}, frontendPerformance: objectOrNull(archiveSummary.frontendPerformance) || frontendPerformanceSummary || {}, redFindings: arr(archiveSummary.redFindings).slice(0, 12).map(slimFinding) }, analysisWindow: source.analysisWindow ?? objectOrNull(recent.summary), sampleMetrics: compactMetrics(srcMetrics), pageProvenance: objectOrNull(source.pageProvenance?.summary) || source.pageProvenance || null, pagePerformance: objectOrNull(pagePerformance.summary) || pagePerformance, projectManagement: objectOrNull(source.projectManagement) || null, promptNetwork: objectOrNull(promptNetwork.summary) || promptNetwork, requestRate: requestRateSummary, requestRateCurve, requestRatePeaks, frontendPerformance: frontendPerformanceSummary, frontendPerformanceHotspots, runtimeAlerts: objectOrNull(runtimeAlerts.summary) || runtimeAlerts, browserProcess: objectOrNull(browserProcess.summary) || browserProcess, runnerErrors: arr(source.runnerErrors).slice(-8), commandFailures: arr(source.commandFailures).slice(-8), commandState: objectOrNull(source.commandState) || null, toolFindings: arr(source.toolFindings).slice(0, 8).map(slimFinding), httpErrorGroups: arr(source.httpErrorGroups ?? runtimeAlerts.networkHttpErrorsByPath).slice(0, 8).map(slimGroup), requestFailedGroups: arr(source.requestFailedGroups ?? runtimeAlerts.networkRequestFailedByPath).slice(0, 8).map(slimGroup), domDiagnosticGroups: arr(source.domDiagnosticGroups ?? runtimeAlerts.domDiagnosticsByText).slice(0, 5).map(slimGroup), domDiagnosticSamples: arr(source.domDiagnosticSamples ?? runtimeAlerts.domDiagnostics).slice(0, 8).map(slimGroup), consoleAlertGroups: arr(source.consoleAlertGroups ?? runtimeAlerts.consoleAlertsByPath).slice(0, 8).map(slimGroup), consoleAlertSamples: arr(source.consoleAlertSamples ?? runtimeAlerts.consoleAlerts).slice(0, 8).map(slimGroup), turnTimingRecentUpdateJumps: arr(source.turnTimingRecentUpdateJumps ?? srcMetrics.turnTimingRecentUpdateSawtoothJumps).slice(0, 8), turnTimingElapsedZeroResets: arr(source.turnTimingElapsedZeroResets ?? srcMetrics.turnTimingElapsedZeroResets).slice(0, 8), turnTimingTotalElapsedForwardJumps: arr(source.turnTimingTotalElapsedForwardJumps ?? srcMetrics.turnTimingTotalElapsedForwardJumps).slice(0, 8), pagePerformanceSlowApi: slowApis.slice(0, 8).map(slimSlowApi), archivePagePerformanceSlowApi: arr(source.archivePagePerformanceSlowApi).slice(0, 8).map(slimSlowApi), pagePerformancePartialApi: arr(source.pagePerformancePartialApi).slice(0, 8), pagePerformanceSseStreams: arr(source.pagePerformanceSseStreams).slice(0, 8), findings: arr(source.findings).slice(0, 12).map(slimFinding), archiveRedFindings: arr(source.archiveRedFindings ?? archiveSummary.redFindings).slice(0, 12).map(slimFinding), reportJsonPath: source.reportJsonPath ?? reportJsonPath, reportJsonSha256: source.reportJsonSha256 ?? sha256(reportJsonPath), reportMdPath: source.reportMdPath ?? reportMdPath, reportMdSha256: source.reportMdSha256 ?? sha256(reportMdPath), analyzer: { ...(objectOrNull(source.analyzer) || {}), recoveredFrom: 'analysis-artifact-after-transport-timeout', stdoutPath, stderrPath, stdoutBytes: statSize(stdoutPath), stderrBytes: statSize(stderrPath), reportJsonBytes: statSize(reportJsonPath), reportMdBytes: statSize(reportMdPath), transportExitCode: Number(transportExitRaw), transportTimedOut: transportTimedOutRaw === 'true', valuesRedacted: true }, valuesRedacted: true };", - "compact.reportUpdatedAt = source.reportUpdatedAt ?? source.updatedAt ?? source.analyzedAt ?? reportJson.reportUpdatedAt ?? reportJson.updatedAt ?? reportJson.analyzedAt ?? null;", + "compact.reportUpdatedAt = source.reportUpdatedAt ?? source.updatedAt ?? source.analyzedAt ?? reportJson.reportUpdatedAt ?? reportJson.updatedAt ?? reportJson.analyzedAt ?? source.generatedAt ?? reportJson.generatedAt ?? null;", "console.log(JSON.stringify(compact));", "UNIDESK_WEB_OBSERVE_RECOVER_ANALYZE_ARTIFACT", ].join("\n"); diff --git a/scripts/src/monitor-terminal-ingest-migration.test.ts b/scripts/src/monitor-terminal-ingest-migration.test.ts index 8a834366..4b31575f 100644 --- a/scripts/src/monitor-terminal-ingest-migration.test.ts +++ b/scripts/src/monitor-terminal-ingest-migration.test.ts @@ -37,8 +37,10 @@ test("terminal writer requires explicit mutually exclusive authority", () => { }); test("real compact analyze projects stable report time and artifact facts", () => { - const compact = compactWebObserveAnalyzeAnalysisForRaw({ updatedAt: "2026-07-12T00:00:00.000Z", analyzer: { reportJsonPath: "/state/run-1/analysis/report.json", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12 } }); + const compact = compactWebObserveAnalyzeAnalysisForRaw({ generatedAt: "2026-07-12T00:00:00.000Z", analyzer: { reportJsonPath: "/state/run-1/analysis/report.json", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12 } }); assert.deepEqual({ reportUpdatedAt: compact.reportUpdatedAt, reportJsonSha256: compact.reportJsonSha256, reportJsonBytes: compact.reportJsonBytes }, { reportUpdatedAt: "2026-07-12T00:00:00.000Z", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12 }); + const ingest = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "generated-at" }, compact, [{ relativePath: "analysis/report.json", kind: "report-json", sha256: `sha256:${"c".repeat(64)}`, sizeBytes: 12 }], { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: "/state/run-1" }); + assert.equal(ingest.updatedAt, "2026-07-12T00:00:00.000Z"); }); test("SQLite snapshot reconciles the 228/242/234 fixture shape without production assumptions", async () => { diff --git a/scripts/src/monitor-terminal-ingest.ts b/scripts/src/monitor-terminal-ingest.ts index 39104796..f8f4c703 100644 --- a/scripts/src/monitor-terminal-ingest.ts +++ b/scripts/src/monitor-terminal-ingest.ts @@ -16,7 +16,7 @@ export class MonitorIngestFailedError extends Error { export function buildMonitorTerminalIngest(identity: MonitorTerminalIdentity, report: Record, artifacts: readonly MonitorTerminalArtifact[], locatorOwner: Pick): MonitorCentralIngest { const immutableReport = JSON.parse(monitorCentralStableJson(report)) as Record; if (!identity.runId) throw new MonitorIngestFailedError({ reason: "run_id_missing", valuesRedacted: true }); - const updatedAt = stringValue(immutableReport.updatedAt) ?? stringValue(immutableReport.analyzedAt); + const updatedAt = stringValue(immutableReport.updatedAt) ?? stringValue(immutableReport.analyzedAt) ?? stringValue(immutableReport.reportUpdatedAt); if (!updatedAt) throw new MonitorIngestFailedError({ reason: "terminal_timestamp_missing", runId: identity.runId, valuesRedacted: true }); for (const artifact of artifacts) if (!/^sha256:[a-f0-9]{64}$/u.test(artifact.sha256) || !Number.isSafeInteger(artifact.sizeBytes) || artifact.sizeBytes < 1) throw new MonitorIngestFailedError({ reason: "terminal_artifact_unverified", runId: identity.runId, relativePath: artifact.relativePath, valuesRedacted: true }); return normalizeMonitorCentralIngest({