import assert from "node:assert/strict"; import { mkdir, mkdtemp, readFile, 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 { withWebObserveCollectRendered } from "../hwlab-node-web-observe-render"; import { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source"; import { nodeWebObserveCollectViewNodeScript } from "../hwlab-node-web-observe-collect"; import { nodeWebObserveCollectNodeScript } from "./web-observe-scripts"; const alertThresholds = { sameOriginApiSlowMs: 60000, partialApiSlowMs: 60000, longLivedStreamOpenSlowMs: 60000, visibleLoadingSlowMs: 60000, turnTimingSampleSlackSeconds: 60, turnElapsedSevereTimeoutSeconds: 3600, domEvaluateTimeoutRedCount: 99, domEvaluateTimeoutRedWindowMs: 60000, screenshotTimeoutRedCount: 99, pageErrorRedCount: 99, longTaskRedMs: 1000, longAnimationFrameRedMs: 1000, eventLoopGapRedMs: 1000, browserProcessSampleIntervalMs: 1000, requestRateBucketMs: 10000, requestRateTotalRedPerMinute: 999999, requestRatePageRedPerMinute: 999999, requestRateApiPathRedPerMinute: 999999, browserTotalRssRedMb: 999999, browserProcessRssRedMb: 999999, browserRssGrowthRedMb: 999999, browserRssGrowthWindowMs: 60000, playwrightResponsivenessRedMs: 60000, playwrightResponsivenessTimeoutRedCount: 99, cdpMetricsTimeoutRedCount: 99, uncommandedStateChangeCommandWindowMs: 1000, scrollJumpCommandWindowMs: 1000, scrollJumpFromY: 999999, scrollJumpToY: 999999, sessionRailFallbackRatio: 0.5, }; const browserFreezePolicy = { enabled: true, blockerWindowMs: 60000, memory: { totalRssBlockerMb: 999999, processRssBlockerMb: 999999, growthBlockerMb: 999999 }, responsiveness: { latencyBlockerMs: 60000, eventBlockerCount: 99 }, cdp: { metricsTimeoutBlockerCount: 99 }, kill: { enabled: false, gracefulSignal: "SIGTERM", forceSignal: "SIGKILL", graceMs: 1000, pollIntervalMs: 100, exitCode: 124 }, }; function shellQuote(value: string): string { return `'${value.replace(/'/g, `'\\''`)}'`; } async function writeJsonl(path: string, rows: Array>): Promise { await writeFile(path, rows.map((row) => JSON.stringify(row)).join("\n") + (rows.length > 0 ? "\n" : "")); } async function runAnalyzer(stateDir: string): Promise> { const analyzerPath = join(stateDir, "analyze.mjs"); await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 }); const result = spawnSync("bun", [analyzerPath, stateDir], { cwd: join(import.meta.dir, "../../.."), env: { ...process.env, UNIDESK_WEB_OBSERVE_ANALYZE_TAIL_SAMPLES: "0", UNIDESK_WEB_OBSERVE_ALERT_THRESHOLDS_JSON: JSON.stringify(alertThresholds), UNIDESK_WEB_OBSERVE_BROWSER_FREEZE_POLICY_JSON: JSON.stringify(browserFreezePolicy), }, encoding: "utf8", }); assert.equal(result.status, 0, result.stderr || result.stdout); return JSON.parse(await readFile(join(stateDir, "analysis", "report.json"), "utf8")) as Record; } async function writeState(): Promise { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-web-performance-")); await mkdir(join(stateDir, "analysis"), { recursive: true }); await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-web-performance-test", status: "completed" }) + "\n"); await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T17:00:05.000Z" }) + "\n"); await writeJsonl(join(stateDir, "samples.jsonl"), []); await writeJsonl(join(stateDir, "console.jsonl"), []); await writeJsonl(join(stateDir, "errors.jsonl"), []); await writeJsonl(join(stateDir, "browser-process.jsonl"), []); await writeJsonl(join(stateDir, "performance-events.jsonl"), []); await writeJsonl(join(stateDir, "artifacts.jsonl"), []); await writeJsonl(join(stateDir, "control.jsonl"), [{ type: "sendPrompt", phase: "completed", commandId: "cmd-prompt", ts: "2026-07-02T17:00:00.000Z", }]); await writeJsonl(join(stateDir, "network.jsonl"), [ { type: "request", ts: "2026-07-02T17:00:01.000Z", pageRole: "control", pageId: "page-1", method: "POST", url: "https://hwlab.example.test/v1/web-performance", resourceType: "fetch", webPerformancePayload: { captureStatus: "captured", parseStatus: "parsed", schemaVersion: "hwlab-web-performance-v2", byteCount: 768, byteLimit: 65536, bodyHash: "sha256:payload", eventCount: 1, storedEventCount: 1, events: [{ ts: "2026-07-02T17:00:01.000Z", eventType: "runtime_diagnostic", diagnosticCode: "workbench_sse_flush", reason: "sse_flush", module: "workbench-runtime", traceId: "trc_webperf_fixture", sessionIdHash: "sha256:session", eventCount: 12, deliveredCount: 10, chunkCount: 3, flushDurationMs: 18.75, droppedCount: 2, maxItemsPerChunk: 5, maxChunkMs: 7.5, replacedByKey: 3, valuesRedacted: true, }], valuesRedacted: true, }, }, { type: "request", ts: "2026-07-02T17:00:02.000Z", method: "POST", url: "https://hwlab.example.test/v1/web-performance", webPerformancePayload: { captureStatus: "captured", parseStatus: "invalid-json", byteCount: 12, byteLimit: 65536, bodyHash: "sha256:invalid", eventCount: null, events: [], valuesRedacted: true, }, }, { type: "request", ts: "2026-07-02T17:00:03.000Z", method: "POST", url: "https://hwlab.example.test/v1/web-performance", webPerformancePayload: { captureStatus: "skipped-over-limit", parseStatus: "not-parsed-over-limit", byteCount: 70000, byteLimit: 65536, bodyHash: "sha256:large", eventCount: null, events: [], valuesRedacted: true, }, }, ]); return stateDir; } test("generated observe analyzer source compiles as a single ESM bundle", async () => { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-compile-")); const analyzerPath = join(stateDir, "observer-analyzer.mjs"); await writeFile(analyzerPath, nodeWebObserveAnalyzerSource(), { mode: 0o700 }); const result = spawnSync("node", ["--check", analyzerPath], { encoding: "utf8" }); assert.equal(result.status, 0, result.stderr || result.stdout); }, 20_000); test("file collect exposes requested and resolved artifact when analysis report falls back", async () => { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-file-fallback-")); await mkdir(join(stateDir, "analysis"), { recursive: true }); await writeFile(join(stateDir, "analysis", "analyzer-stdout.json"), JSON.stringify({ ok: false, command: "web-probe-observe analyze", error: "fixture analyzer failed before report", valuesRedacted: true, }) + "\n"); const script = nodeWebObserveCollectNodeScript(100, "analysis/report.json", null, 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) as Record; const file = output.file as Record; assert.equal(output.requestedFile, "analysis/report.json"); assert.equal(output.resolvedFile, "analysis/analyzer-stdout.json"); assert.equal(output.fallbackReason, "requested-file-missing"); assert.equal(file.requestedRelative, "analysis/report.json"); assert.equal(file.resolvedRelative, "analysis/analyzer-stdout.json"); const rendered = withWebObserveCollectRendered({ ok: true, status: "collected", command: "web-probe observe collect", id: "webobs-fallback-test", node: "JD01", lane: "v03", view: "files", requestedFile: "analysis/report.json", collect: output, valuesRedacted: true, }); assert.match(rendered.renderedText, /File resolution/u); assert.match(rendered.renderedText, /analysis\/report\.json/u); assert.match(rendered.renderedText, /analysis\/analyzer-stdout\.json/u); assert.match(rendered.renderedText, /requested-file-missing/u); }, 20_000); test("analyzer and performance-summary include bounded web-performance runtime diagnostics", async () => { const stateDir = await writeState(); const report = await runAnalyzer(stateDir); const runtimeAlerts = report.runtimeAlerts as Record; const summary = runtimeAlerts.summary as Record; assert.equal(summary.webPerformancePayloadRequestCount, 3); assert.equal(summary.webPerformancePayloadParsedCount, 1); assert.equal(summary.webPerformancePayloadParseIssueCount, 2); assert.equal(summary.webPerformanceRuntimeDiagnosticCount, 1); const groups = runtimeAlerts.webPerformanceRuntimeDiagnosticsByCode as Array>; assert.equal(groups[0].diagnosticCode, "workbench_sse_flush"); assert.equal(groups[0].reason, "sse_flush"); assert.equal(groups[0].module, "workbench-runtime"); assert.equal(groups[0].eventCount, 12); assert.equal(groups[0].deliveredCount, 10); assert.equal(groups[0].chunkCount, 3); assert.equal(groups[0].maxFlushDurationMs, 18.75); assert.equal(groups[0].replacedByKeyCount, 3); const reportText = JSON.stringify(report); assert.match(reportText, /workbench_sse_flush/u); assert.doesNotMatch(reportText, /rawBody|super-secret|ses_unredacted/u); 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 renderedText = String(output.renderedText ?? ""); assert.match(renderedText, /Web performance runtime diagnostics/u); assert.match(renderedText, /payloads=3 parsed=1 parseIssues=2 events=1 groups=1/u); assert.match(renderedText, /workbench_sse_flush reason=sse_flush module=workbench-runtime/u); assert.match(renderedText, /eventCount=12 delivered=10 chunks=3 flushMax=18.75ms dropped=2/u); assert.match(renderedText, /payload states=invalid-json:1, not-parsed-over-limit:1, parsed:1/u); }, 20_000);