import assert from "node:assert/strict"; import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { spawnSync } from "node:child_process"; import { test } from "bun:test"; import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect"; function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } test("performance-summary labels LoAF-only evidence when CPU profile capture is pending", async () => { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-")); await mkdir(join(stateDir, "analysis"), { recursive: true }); await mkdir(join(stateDir, "commands", "pending"), { recursive: true }); await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-perf-test", status: "failed" }) + "\n"); await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "failed", updatedAt: "2026-07-02T12:08:06Z" }) + "\n"); await writeFile(join(stateDir, "performance-events.jsonl"), [ JSON.stringify({ type: "performance-event", ts: "2026-07-02T12:07:00Z", performance: { kind: "long-animation-frame", duration: 2083.8 }, }), ].join("\n") + "\n"); await writeFile(join(stateDir, "commands", "pending", "cmd-perf.json"), JSON.stringify({ id: "cmd-perf", type: "performanceCapture", createdAt: "2026-07-02T12:08:00Z", }) + "\n"); await writeFile(join(stateDir, "analysis", "report.json"), JSON.stringify({ manifest: { status: "failed" }, heartbeat: { status: "failed", updatedAt: "2026-07-02T12:08:06Z" }, frontendPerformance: { summary: { eventCount: 1, longTaskCount: 0, longAnimationFrameCount: 1, eventLoopGapCount: 0, captureCount: 0, scriptHotspotCount: 1, maxLongAnimationFrameMs: 2083.8, longAnimationFrameRedMs: 200, cpuProfileStatus: "missing", attributionMode: "loaf-only-no-cpu-profile", noCpuProfile: true, loafOnly: true, valuesRedacted: true, }, scriptHotspots: [{ sourceFunctionName: "Response.json.then", sourceURL: "https://hwlab.example.test/app.js", sourceFile: "app.js", sourceCharPosition: 123456, sourceMapStatus: "missing", totalDurationMs: 2108, count: 2, valuesRedacted: true, }], profileHotspots: [], profileStacks: [], captures: [], longAnimationFrames: [{ ts: "2026-07-02T12:07:00Z", kind: "long-animation-frame", durationMs: 2083.8, sampleSeq: 42, pageRole: "control", scriptCount: 1 }], }, findings: [{ id: "frontend-performance-loaf-only-no-cpu-profile", severity: "amber", summary: "frontend performance evidence is LoAF/LongTask/event-loop only because no completed CPU profile capture is present", count: 1, }, { id: "tool-pending-commands-unconsumed", severity: "red", summary: "web-probe observe has pending/processing control commands that were not consumed by the runner", count: 1, }], }) + "\n"); const script = nodeWebObserveCollectViewNodeScript({ maxFiles: 100, view: "performance-summary", traceId: null, sampleSeq: null, timestamp: null, turn: null, commandId: null, windowMs: null, }); const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], { cwd: join(import.meta.dir, "../../.."), encoding: "utf8", }); assert.equal(result.status, 0, result.stderr || result.stdout); const output = JSON.parse(result.stdout); const text = String(output.renderedText ?? ""); const debug = JSON.stringify({ summary: output.summary, evidenceMode: output.evidenceMode, text }, null, 2); assert.equal(output.evidenceMode.attributionMode, "loaf-only-no-cpu-profile", debug); assert.equal(output.evidenceMode.cpuProfileStatus, "pending-command-no-cpu-profile"); assert.equal(output.evidenceMode.pendingPerformanceCapture, true); assert.match(text, /LoAF-only \/ no CPU profile/u); assert.match(text, /pending performanceCapture: cmd-perf\(pending\)/u); assert.match(text, /runner state: not-running\/failed/u); assert.match(text, /no CPU profile hotspots; no completed performanceCapture artifact/u); assert.match(text, /sourceMap=missing/u); assert.match(text, /char=123456/u); }, 20_000); test("performance-summary renders CPU profile window misses and source attribution context", async () => { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-performance-window-")); await mkdir(join(stateDir, "analysis"), { recursive: true }); await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-perf-window-test", status: "completed" }) + "\n"); await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T12:08:06Z" }) + "\n"); await writeFile(join(stateDir, "performance-events.jsonl"), ""); await writeFile(join(stateDir, "analysis", "report.json"), JSON.stringify({ manifest: { status: "completed" }, heartbeat: { status: "completed", updatedAt: "2026-07-02T12:08:06Z" }, frontendPerformance: { summary: { eventCount: 2, longTaskCount: 0, longAnimationFrameCount: 1, eventLoopGapCount: 1, captureCount: 1, profileHotspotCount: 1, scriptHotspotCount: 1, windowCorrelationCount: 1, cpuProfileWindowCoveredCount: 0, cpuProfileWindowOverlappedCount: 0, cpuProfileWindowMissedCount: 1, cpuProfileWindowMissingCount: 0, maxLongAnimationFrameMs: 1811, maxEventLoopGapMs: 4490, longAnimationFrameRedMs: 200, eventLoopGapRedMs: 1000, cpuProfileStatus: "captured-hotspots-outside-performance-window", attributionMode: "cpu-profile-missed-performance-window", sourceMapStatus: "missing", valuesRedacted: true, }, performanceWindows: [{ kind: "long-animation-frame", ts: "2026-07-02T12:00:00Z", startAt: "2026-07-02T12:00:00.000Z", endAt: "2026-07-02T12:00:01.811Z", durationMs: 1811, sampleSeq: 7, pageRole: "control", pageId: "page-1", cpuProfileCoverageStatus: "missed", coverageReason: "nearest performanceCapture started after this frontend performance window ended", capture: { captureId: "perf-late", commandId: "cmd-late", startAt: "2026-07-02T12:00:10.000Z", endAt: "2026-07-02T12:00:18.158Z", durationMs: 8158, windowDistanceMs: 8189, profileSampleCount: 812, valuesRedacted: true, }, topLoafScript: { sourceFunctionName: "Response.json.then", sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js", sourceFile: "CodeWorkbenchView.js", sourceCharPosition: 456789, lineNumber: 91, sourceMapStatus: "missing", totalDurationMs: 1811, count: 5, valuesRedacted: true, }, relatedSamples: [{ ts: "2026-07-02T12:00:01Z", sampleSeq: 7, pageRole: "control", activeSessionId: "ses_test", traceIds: ["trc_test"], valuesRedacted: true, }], relatedNetwork: [{ ts: "2026-07-02T12:00:00.500Z", sampleSeq: 7, phase: "response", method: "GET", status: 200, urlPath: "/v1/workbench/sessions/ses_test/turns", bodyByteCount: 1048576, traceId: "trc_test", valuesRedacted: true, }], valuesRedacted: true, }], scriptHotspots: [{ sourceFunctionName: "Response.json.then", sourceURL: "https://hwlab.example.test/assets/CodeWorkbenchView.js", sourceFile: "CodeWorkbenchView.js", sourceCharPosition: 456789, lineNumber: 91, sourceMapStatus: "missing", totalDurationMs: 1811, count: 5, valuesRedacted: true, }], profileHotspots: [{ functionName: "(program)", sourceFile: "CodeWorkbenchView.js", sourceMapStatus: "missing", selfTimeMs: 120, totalTimeMs: 200, captureCount: 1, valuesRedacted: true, }], profileStacks: [], captures: [{ captureId: "perf-late", commandId: "cmd-late", startAt: "2026-07-02T12:00:10.000Z", endAt: "2026-07-02T12:00:18.158Z", durationMs: 8158, profileSampleCount: 812, valuesRedacted: true, }], longAnimationFrames: [{ ts: "2026-07-02T12:00:00Z", startAt: "2026-07-02T12:00:00.000Z", endAt: "2026-07-02T12:00:01.811Z", kind: "long-animation-frame", durationMs: 1811, sampleSeq: 7, pageRole: "control", scriptCount: 1 }], sourceAttribution: { sourceMapStatus: "missing", note: "No source-map artifact is loaded by this analyzer; function/file/line fields are raw browser URLs or CPU profile callFrame locations.", sourceFiles: [{ sourceFile: "CodeWorkbenchView.js", count: 2, sourceMapStatus: "missing", valuesRedacted: true }], valuesRedacted: true, }, }, findings: [], }) + "\n"); const script = nodeWebObserveCollectViewNodeScript({ maxFiles: 100, view: "performance-summary", traceId: null, sampleSeq: null, timestamp: null, turn: null, commandId: null, windowMs: null, }); const result = spawnSync("bash", ["-lc", `state_dir=${shellQuote(stateDir)}\n${script}`], { cwd: join(import.meta.dir, "../../.."), encoding: "utf8", }); assert.equal(result.status, 0, result.stderr || result.stdout); const output = JSON.parse(result.stdout); const text = String(output.renderedText ?? ""); const debug = JSON.stringify({ summary: output.summary, evidenceMode: output.evidenceMode, text }, null, 2); assert.equal(output.summary.cpuProfileWindowMissedCount, 1); assert.equal(output.evidenceMode.attributionMode, "cpu-profile-missed-performance-window", debug); assert.equal(output.evidenceMode.cpuProfileMissedPerformanceWindows, true, debug); assert.match(text, /window correlation=1 covered=0 overlapped=0 missed=1 missing=0 sourceMap=missing/u); assert.match(text, /evidence attribution=cpu-profile-missed-performance-window/u); assert.match(text, /CPU profile artifacts exist but missed severe performance windows; do not cite CPU profile hotspots as same-window evidence/u); assert.doesNotMatch(text, /hotspot rows may be used as CPU profile evidence/u); assert.match(text, /cpuWindow=missed/u); assert.match(text, /nearest performanceCapture started after/u); assert.match(text, /CodeWorkbenchView\.js/u); assert.match(text, /bytes=1048576/u); assert.match(text, /session=ses_test trace=trc_test/u); }, 20_000);