fix: project analyzer generated time
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -1488,7 +1488,7 @@ function compactAnalyzeBrowserProcess(value: unknown): Record<string, unknown> |
|
||||
};
|
||||
}
|
||||
|
||||
function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number): Record<string, unknown> {
|
||||
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<string, unknown> {
|
||||
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",
|
||||
|
||||
@@ -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<string, string>): Promise<Record<string, any>> {
|
||||
async function runObserveAnalyzer(files: Record<string, string>): Promise<{ report: Record<string, any>; stdout: Record<string, unknown> }> {
|
||||
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<string, string>): Promise<Record
|
||||
encoding: "utf8",
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr || result.stdout);
|
||||
return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8"));
|
||||
return {
|
||||
report: JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8")),
|
||||
stdout: JSON.parse(result.stdout),
|
||||
};
|
||||
}
|
||||
|
||||
test("observe analyzer stdout carries generatedAt into terminal ingest", async () => {
|
||||
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<string, unknown>) => 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<string, unknown>) => item.id === "workbench-automatic-recovery-fanout-authority");
|
||||
|
||||
assert.equal(finding, undefined);
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -2220,7 +2220,7 @@ export function compactWebObserveAnalyzeAnalysisForRaw(analysis: Record<string,
|
||||
reportJsonPath: stringOrNullValue(analysis.reportJsonPath) ?? stringOrNullValue(recordValue(analysis.analyzer).reportJsonPath),
|
||||
reportJsonSha256: stringOrNullValue(analysis.reportJsonSha256) ?? stringOrNullValue(recordValue(analysis.analyzer).reportJsonSha256),
|
||||
reportJsonBytes: numberOrNullValue(analysis.reportJsonBytes) ?? numberOrNullValue(recordValue(analysis.analyzer).reportJsonBytes),
|
||||
reportUpdatedAt: stringOrNullValue(analysis.reportUpdatedAt) ?? stringOrNullValue(analysis.updatedAt) ?? stringOrNullValue(analysis.analyzedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).reportUpdatedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).updatedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).analyzedAt),
|
||||
reportUpdatedAt: stringOrNullValue(analysis.reportUpdatedAt) ?? stringOrNullValue(analysis.updatedAt) ?? stringOrNullValue(analysis.analyzedAt) ?? stringOrNullValue(analysis.generatedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).reportUpdatedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).updatedAt) ?? stringOrNullValue(recordValue(analysis.analyzer).analyzedAt),
|
||||
reportMdPath: stringOrNullValue(analysis.reportMdPath),
|
||||
reportMdSha256: stringOrNullValue(analysis.reportMdSha256),
|
||||
analyzer: compactWebObserveAnalyzeAnalyzerForRaw(analysis.analyzer),
|
||||
@@ -2489,7 +2489,7 @@ export function recoverWebObserveAnalyzeFromArtifacts(options: NodeWebProbeObser
|
||||
"const archiveSampleMetrics = objectOrNull(archiveSummary.sampleMetrics) || objectOrNull(source.sampleMetrics?.summary) || objectOrNull(srcMetrics.summary) || {};",
|
||||
"const slowApis = arr(source.pagePerformanceSlowApi).length > 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");
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export class MonitorIngestFailedError extends Error {
|
||||
export function buildMonitorTerminalIngest(identity: MonitorTerminalIdentity, report: Record<string, unknown>, artifacts: readonly MonitorTerminalArtifact[], locatorOwner: Pick<MonitorArtifactLocator, "ownerNode" | "ownerLane" | "ownerPvc" | "stateDir">): MonitorCentralIngest {
|
||||
const immutableReport = JSON.parse(monitorCentralStableJson(report)) as Record<string, unknown>;
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user