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 { nodeWebObserveAnalyzerSource } from "../hwlab-node-web-observe-analyzer-source"; const alertThresholds = { sameOriginApiSlowMs: 60000, partialApiSlowMs: 60000, longLivedStreamOpenSlowMs: 60000, visibleLoadingSlowMs: 60000, turnTimingSampleSlackSeconds: 60, turnElapsedSevereTimeoutSeconds: 3600, domEvaluateTimeoutRedCount: 99, domEvaluateTimeoutRedWindowMs: 60000, screenshotTimeoutRedCount: 99, pageErrorRedCount: 99, longTaskRedMs: 50, longAnimationFrameRedMs: 50, eventLoopGapRedMs: 50, 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, }, }; 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 writeBaseState(options: { withSourceMap: boolean; captureEndAt: string }): Promise { const stateDir = await mkdtemp(join(tmpdir(), "unidesk-web-observe-analyzer-performance-")); await mkdir(join(stateDir, "analysis"), { recursive: true }); const profilePath = join(stateDir, "profile.cpuprofile"); const sourceMapPath = join(stateDir, "app.js.map"); await writeFile(join(stateDir, "manifest.json"), JSON.stringify({ jobId: "webobs-performance-test", status: "completed" }) + "\n"); await writeFile(join(stateDir, "heartbeat.json"), JSON.stringify({ status: "completed", updatedAt: "2026-07-02T00:00:03.000Z" }) + "\n"); await writeFile(profilePath, JSON.stringify({ startTime: 0, endTime: 2000000, nodes: [ { id: 1, callFrame: { functionName: "(root)", url: "", lineNumber: -1, columnNumber: -1 }, children: [2] }, { id: 2, callFrame: { functionName: "hotParse", url: "https://hwlab.example.test/assets/app.js", scriptId: "7", lineNumber: 0, columnNumber: 10 } }, ], samples: [2, 2, 2], timeDeltas: [600000, 600000, 800000], }) + "\n"); if (options.withSourceMap) { await writeFile(sourceMapPath, JSON.stringify({ version: 3, file: "app.js", sources: ["src/api/client.ts"], names: ["responseJson"], mappings: "UAKEA", }) + "\n"); } await writeJsonl(join(stateDir, "samples.jsonl"), []); await writeJsonl(join(stateDir, "network.jsonl"), []); await writeJsonl(join(stateDir, "control.jsonl"), []); await writeJsonl(join(stateDir, "console.jsonl"), []); await writeJsonl(join(stateDir, "errors.jsonl"), []); await writeJsonl(join(stateDir, "browser-process.jsonl"), []); await writeJsonl(join(stateDir, "artifacts.jsonl"), options.withSourceMap ? [{ ts: "2026-07-02T00:00:00.100Z", kind: "source-map", path: sourceMapPath, url: "https://hwlab.example.test/assets/app.js.map", }] : []); await writeJsonl(join(stateDir, "performance-events.jsonl"), [ { type: "performance-event", ts: "2026-07-02T00:00:01.080Z", sampleSeq: 11, pageRole: "control", pageId: "page-1", performance: { kind: "long-animation-frame", name: "long-animation-frame", duration: 80, blockingDuration: 80, timeOrigin: Date.parse("2026-07-02T00:00:00.000Z"), startTime: 1000, path: "/workbench", url: "https://hwlab.example.test/workbench", scripts: [{ invoker: "then", invokerType: "promise", sourceURL: "https://hwlab.example.test/assets/app.js", sourceFunctionName: "Response.json.then", sourceCharPosition: 10, lineNumber: 0, columnNumber: 10, duration: 80, }], }, }, { type: "performance-capture-completed", ts: options.captureEndAt, commandId: "cmd-profile", captureId: "cap-profile", pageRole: "control", pageId: "page-1", artifact: { kind: "performance-cpu-profile", captureId: "cap-profile", commandId: "cmd-profile", path: profilePath, durationMs: 2000 }, summary: { durationMs: 2000, sampleCount: 3, totalTimeMs: 2000, topFunctions: [{ functionName: "hotParse", url: "https://hwlab.example.test/assets/app.js", scriptId: "7", lineNumber: 0, columnNumber: 10, selfTimeMs: 2000, totalTimeMs: 2000, sampleCount: 3, }], }, }, ]); return stateDir; } test("observe analyzer maps LoAF and same-window CPU samples through loaded source map", async () => { const stateDir = await writeBaseState({ withSourceMap: true, captureEndAt: "2026-07-02T00:00:02.500Z" }); const report = await runAnalyzer(stateDir); const frontendPerformance = report.frontendPerformance as Record; const summary = frontendPerformance.summary as Record; assert.equal(summary.sourceMapStatus, "mapped"); assert.equal(summary.cpuProfileWindowCoveredCount, 1); const scriptHotspot = (frontendPerformance.scriptHotspots as Array>)[0]; assert.equal(scriptHotspot.sourceMapStatus, "mapped"); assert.equal(scriptHotspot.sourceFile, "src/api/client.ts"); assert.equal(scriptHotspot.sourceLine, 6); assert.equal(scriptHotspot.lineNumber, 0); assert.equal(scriptHotspot.sourceCharPosition, 10); const windowRow = (frontendPerformance.performanceWindows as Array>)[0]; assert.equal(windowRow.cpuProfileCoverageStatus, "covered"); const evidence = windowRow.profileEvidence as Record; assert.equal(evidence.timelineStatus, "window-samples"); assert.equal(evidence.evidenceScope, "same-window-cpu-samples"); assert.equal(evidence.sampleCount, 1); assert.equal(evidence.totalTimeMs, 80); const topFunction = (evidence.topFunctions as Array>)[0]; assert.equal(topFunction.functionName, "hotParse"); assert.equal(topFunction.sourceMapStatus, "mapped"); assert.equal(topFunction.sourceFile, "src/api/client.ts"); assert.equal(topFunction.sourceLine, 6); assert.equal(topFunction.selfTimeMs, 80); }, 20_000); test("observe analyzer preserves raw attribution when source map is missing and marks missed CPU timeline windows", async () => { const stateDir = await writeBaseState({ withSourceMap: false, captureEndAt: "2026-07-02T00:00:05.000Z" }); const report = await runAnalyzer(stateDir); const frontendPerformance = report.frontendPerformance as Record; const summary = frontendPerformance.summary as Record; assert.equal(summary.sourceMapStatus, "missing"); assert.equal(summary.cpuProfileWindowMissedCount, 1); assert.equal(summary.attributionMode, "cpu-profile-missed-performance-window"); const scriptHotspot = (frontendPerformance.scriptHotspots as Array>)[0]; assert.equal(scriptHotspot.sourceMapStatus, "missing"); assert.equal(scriptHotspot.sourceFile, "app.js"); assert.equal(scriptHotspot.sourceLine, 0); const windowRow = (frontendPerformance.performanceWindows as Array>)[0]; assert.equal(windowRow.cpuProfileCoverageStatus, "missed"); assert.equal(windowRow.profileEvidence, null); const capture = windowRow.capture as Record; assert.equal(capture.profileTimelineStatus, "loaded"); assert.equal(capture.windowDistanceMs, 1920); }, 20_000);