fix: project analyzer generated time

This commit is contained in:
AgentRun Codex
2026-07-12 22:24:00 +00:00
parent 5fd3a1f07d
commit 771a3596f1
8 changed files with 61 additions and 14 deletions
@@ -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 });
}
+2 -2
View File
@@ -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");