import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtemp, mkdir, readFile, realpath, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import test from "node:test"; import { aggregateMetrics, checkStagedFile, checkStagedPresentationCssFile, compareWithBaseline, countEffects, countLines, countMemoization, countModuleMutableGlobals, countProviderBranches, countRenderMirrorRefs, countStoreCreations, countSwallowedCatches, isScannedSourceFile, isScannedPresentationCssFile, isComponentModule, isTimerForbiddenFile, measureFileMetrics, parseStagedAddedLines, stagedDiffArgs } from "./check-agent-gui-degradation.mjs"; import { createIsolatedGitEnvironment } from "./git-environment.mjs"; const scriptPath = resolve( dirname(fileURLToPath(import.meta.url)), "check-agent-gui-degradation.mjs" ); test("counts effects and memoization hooks", () => { const source = [ "useEffect(() => {}, []);", "useLayoutEffect(() => {}, []);", "const value = useMemo(() => 1, []);", "const callback = useCallback(() => 2, []);", "export default memo(Component);" ].join("\n"); assert.equal(countEffects(source), 2); assert.equal(countMemoization(source), 3); }); test("counts render-time ref mirrors but ignores imperative refs", () => { const source = [ "const usageProjectionRef = useRef(null);", "const inputRef = useRef(input);", "const handleHandoffRef = useRef(null);", "const elementRef = useRef(null);", "const timeoutRef = useRef(null);", "const searchInputRef = useRef(null);", "const dragStateRef = useRef(null);", "const handledSequenceRef = useRef(null);" ].join("\n"); assert.equal(countRenderMirrorRefs(source), 3); }); test("counts provider behavior branches with the wide ruleset", () => { const source = [ 'if (provider === "codex") {}', 'if (data.provider !== "claude-code") {}', "switch (provider) {", ' case "opencode":', ' case "cursor":', " break;", "}", '// case "not-a-provider":', 'if (kind === "codex") {}' ].join("\n"); assert.equal(countProviderBranches(source), 4); }); test("detects swallowed catch blocks including comment-only bodies", () => { const source = [ "try { run(); } catch {}", "try { run(); } catch (error) {", " // ignored on purpose", "}", "try { run(); } catch (error) {", " report(error);", "}", "promise.catch((error) => report(error));" ].join("\n"); assert.equal(countSwallowedCatches(source), 2); }); test("ignores catch keyword inside strings", () => { const source = 'const label = "catch {} nothing";'; assert.equal(countSwallowedCatches(source), 0); }); test("counts store creations only in files importing valtio or zustand", () => { const valtioSource = [ 'import { proxy } from "valtio/vanilla";', "const state = proxy({ open: false });" ].join("\n"); const zustandSource = [ 'import { create } from "zustand";', "const useStore = create()(() => ({}));" ].join("\n"); const plainSource = "const state = proxy({ open: false });"; assert.equal(countStoreCreations(valtioSource), 1); assert.equal(countStoreCreations(zustandSource), 1); assert.equal(countStoreCreations(plainSource), 0); }); test("counts module-level mutable globals at column zero only", () => { const source = [ "let mutableSlot = null;", "export let sharedFlag = false;", "function scoped() {", " let local = 1;", " return local;", "}", "const stable = 2;" ].join("\n"); assert.equal(countModuleMutableGlobals(source), 2); }); test("counts trailing line without final newline", () => { assert.equal(countLines("a\nb\nc"), 3); assert.equal(countLines("a\nb\nc\n"), 3); assert.equal(countLines(""), 0); }); test("skips test, generated, and dist files from scanning", () => { assert.equal( isScannedSourceFile("packages/agent/gui/agent-gui/Component.tsx"), true ); assert.equal( isScannedSourceFile("packages/agent/gui/agent-gui/Component.spec.tsx"), false ); assert.equal( isScannedSourceFile("packages/agent/gui/dist/Component.tsx"), false ); assert.equal(isScannedSourceFile("packages/agent/gui/types.d.ts"), false); assert.equal(isScannedSourceFile("packages/agent/gui/README.md"), false); assert.equal( isScannedPresentationCssFile( "packages/agent/gui/app/renderer/agentactivity.css" ), true ); assert.equal( isScannedPresentationCssFile("apps/desktop/src/renderer/app.css"), false ); }); test("separates component modules from TSX read hooks", () => { assert.equal( isComponentModule("packages/agent/gui/agent-gui/View.tsx"), true ); assert.equal( isComponentModule("packages/agent/gui/agent-gui/useDetailModel.tsx"), false ); }); test("marks engine, reducer, and selector paths as timer-forbidden", () => { assert.equal( isTimerForbiddenFile("packages/agent/activity-core/src/engine/loop.ts"), true ); assert.equal( isTimerForbiddenFile("packages/agent/activity-core/src/queue.reducer.ts"), true ); assert.equal( isTimerForbiddenFile("packages/agent/activity-core/src/selectors.ts"), true ); assert.equal( isTimerForbiddenFile("packages/agent/gui/agent-gui/Component.tsx"), false ); }); test("routes provider branches of identity-exempt files to the identity bucket", () => { const source = 'const icon = provider === "codex" ? codexIcon : defaultIcon;'; const exemptPath = "packages/agent/gui/agentGuiSessionProviderIconUrls.ts"; const exempt = measureFileMetrics(exemptPath, source, [exemptPath]); assert.equal(exempt.providerBranches, 0); assert.equal(exempt.identityProviderBranches, 1); const regular = measureFileMetrics(exemptPath, source, []); assert.equal(regular.providerBranches, 1); assert.equal(regular.identityProviderBranches, 0); }); test("aggregates effects and applies component memo and line budgets", () => { const longFile = measureFileMetrics( "packages/agent/gui/long.ts", `${"line\n".repeat(801)}`, [] ); const shortFile = measureFileMetrics( "packages/agent/gui/short.tsx", `${"useMemo(() => 1, []);\n".repeat(6)}useEffect(() => {}, []);\n`, [] ); const metrics = aggregateMetrics({ "packages/agent/gui/long.ts": longFile, "packages/agent/gui/short.tsx": shortFile }); assert.deepEqual(metrics.fileLines, { "packages/agent/gui/long.ts": 801 }); assert.equal(metrics.effectCount, 1); assert.deepEqual(metrics.componentMemoOverages, { "packages/agent/gui/short.tsx": 6 }); }); test("flags increases as regressions and decreases as unlocked improvements", () => { const { improvements, regressions } = compareWithBaseline( { fileLines: { "a.ts": 900, "c.ts": 850 }, providerBranches: 10, setTimeoutCount: 2 }, { fileLines: { "a.ts": 850, "b.ts": 900 }, providerBranches: 12, setTimeoutCount: 2 } ); assert.equal(regressions.length, 2); assert.ok(regressions.some((entry) => /a\.ts 850 -> 900/.test(entry))); assert.ok( regressions.some((entry) => /c\.ts.*not in the baseline/.test(entry)) ); assert.equal(improvements.length, 2); assert.ok( improvements.some((entry) => /providerBranches: 12 -> 10/.test(entry)) ); assert.ok(improvements.some((entry) => /b\.ts 900 -> gone/.test(entry))); }); test("treats new over-limit files as regressions even without baseline entry", () => { const { regressions } = compareWithBaseline( { fileLines: { "new.ts": 900 } }, { fileLines: {} } ); assert.equal(regressions.length, 1); assert.match(regressions[0], /not in the baseline/); }); test("parses added lines from a unified zero-context diff", () => { const diff = [ "diff --git a/packages/agent/gui/x.ts b/packages/agent/gui/x.ts", "--- a/packages/agent/gui/x.ts", "+++ b/packages/agent/gui/x.ts", "@@ -10,0 +11,2 @@ context", "+const added = 1;", "+const alsoAdded = 2;", "@@ -20 +23 @@ context", "-const removed = 3;", "+const replaced = 4;" ].join("\n"); const addedLinesByFile = parseStagedAddedLines(diff); assert.deepEqual(addedLinesByFile.get("packages/agent/gui/x.ts"), [ { content: "const added = 1;", line: 11 }, { content: "const alsoAdded = 2;", line: 12 }, { content: "const replaced = 4;", line: 23 } ]); }); test("staged diff compares a merge index against the incoming parent", () => { assert.deepEqual(stagedDiffArgs(null).slice(0, 3), [ "diff", "--cached", "-U0" ]); assert.deepEqual(stagedDiffArgs("merge-head").slice(0, 4), [ "diff", "--cached", "merge-head", "-U0" ]); }); test("staged check flags uncommented timers and accepts timing-comment timers", () => { const stagedContent = [ "// timing: batch daemon events into one frame", "setTimeout(flush, 33);", "setTimeout(retry, 100);" ].join("\n"); const violations = checkStagedFile({ addedLines: [ { content: "setTimeout(flush, 33);", line: 2 }, { content: "setTimeout(retry, 100);", line: 3 } ], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/helper.ts", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-sync-timer"); assert.equal(violations[0].line, 3); }); test("staged check rejects timers in timer-forbidden paths despite comments", () => { const stagedContent = [ "// timing: not acceptable here", "setTimeout(step, 5);" ].join("\n"); const violations = checkStagedFile({ addedLines: [{ content: "setTimeout(step, 5);", line: 2 }], identityExemptFiles: [], relativePath: "packages/agent/activity-core/src/engine/loop.ts", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-sync-timer"); }); test("staged check requires lifecycle reasons for presentation schedulers", () => { const stagedContent = [ "// presentation-work: one visible-frame commit while dragging", "requestAnimationFrame(commitVisibleDrag);", "const observer = new ResizeObserver(measureEverything);" ].join("\n"); const violations = checkStagedFile({ addedLines: [ { content: "requestAnimationFrame(commitVisibleDrag);", line: 2 }, { content: "const observer = new ResizeObserver(measureEverything);", line: 3 } ], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/View.tsx", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "presentation-scheduler-lifecycle"); assert.equal(violations[0].line, 3); }); test("staged check requires lifecycle reasons for inline compositor hints", () => { const line = 'const style = { willChange: "transform" };'; const violations = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/View.tsx", stagedContent: line }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "inline-compositor-lifecycle"); }); test("staged CSS requires exact reasons and rejects transition all", () => { const stagedContent = [ ".viewer {", " will-change: transform;", " transition: all 180ms ease;", "}" ].join("\n"); const withoutReason = checkStagedPresentationCssFile({ addedLines: [ { content: " will-change: transform;", line: 2 }, { content: " transition: all 180ms ease;", line: 3 } ], presentationHintReasons: {}, relativePath: "packages/agent/gui/view.css", stagedContent }); assert.deepEqual(withoutReason.map(({ rule }) => rule).sort(), [ "no-transition-all", "presentation-hint-lifecycle" ]); const fingerprint = "packages/agent/gui/view.css | .viewer | will-change: transform"; const withReason = checkStagedPresentationCssFile({ addedLines: [{ content: " will-change: transform;", line: 2 }], presentationHintReasons: { [fingerprint]: "Viewer exists only while its modal is open" }, relativePath: "packages/agent/gui/view.css", stagedContent: ".viewer {\n will-change: transform;\n}" }); assert.deepEqual(withReason, []); }); test("staged check flags new provider branches outside identity-exempt files", () => { const line = 'if (provider === "codex") { enablePlanMode(); }'; const flagged = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/behavior.ts", stagedContent: line }); assert.equal(flagged.length, 1); assert.equal(flagged[0].rule, "provider-branch-freeze"); const exempt = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [ "packages/agent/gui/agentGuiSessionProviderIconUrls.ts" ], relativePath: "packages/agent/gui/agentGuiSessionProviderIconUrls.ts", stagedContent: line }); assert.equal(exempt.length, 0); }); test("staged check flags store creation in component files", () => { const stagedContent = [ 'import { proxy } from "valtio/vanilla";', "const railState = proxy({ hovered: null });" ].join("\n"); const violations = checkStagedFile({ addedLines: [ { content: "const railState = proxy({ hovered: null });", line: 2 } ], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/View.tsx", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-store-in-view"); }); test("staged check rejects component cache overages with layer guidance", () => { const stagedContent = "useMemo(() => 1, []);\n".repeat(6); const violations = checkStagedFile({ addedLines: [{ content: "useMemo(() => 1, []);", line: 6 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/View.tsx", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "component-memo-budget"); assert.match(violations[0].message, /engine selectors\/read hooks/); }); test("staged check rejects render ref mirrors with engine guidance", () => { const line = "const usageProjectionRef = useRef(null);"; const violations = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/View.tsx", stagedContent: line }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-render-ref-mirror"); assert.match(violations[0].message, /engine\/controller/); }); test("staged check flags direct useSyncExternalStore outside the binding file", () => { const line = "const value = useSyncExternalStore(subscribe, getSnapshot);"; const flagged = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/Component.tsx", stagedContent: line }); assert.equal(flagged.length, 1); assert.equal(flagged[0].rule, "single-subscription-binding"); const binding = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/shared/engine/useEngineSelector.ts", stagedContent: line }); assert.equal(binding.length, 0); }); test("staged check flags swallowed catches intersecting added lines", () => { const stagedContent = ["try {", " run();", "} catch {", "}"].join("\n"); const violations = checkStagedFile({ addedLines: [{ content: "} catch {", line: 3 }], identityExemptFiles: [], relativePath: "packages/agent/gui/agent-gui/helper.ts", stagedContent }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-swallowed-catch"); }); test("staged check flags new module-level mutable globals", () => { const line = "let activeRuntimeSlot = null;"; const violations = checkStagedFile({ addedLines: [{ content: line, line: 1 }], identityExemptFiles: [], relativePath: "packages/agent/gui/runtimeRegistry.ts", stagedContent: line }); assert.equal(violations.length, 1); assert.equal(violations[0].rule, "no-module-mutable-global"); }); test("full mode generates a baseline and then detects regressions", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); const sourcePath = join( workspaceRoot, "packages/agent/gui/agent-gui/sample.ts" ); await mkdir(dirname(sourcePath), { recursive: true }); await writeFile(sourcePath, 'if (provider === "codex") {}\n'); const baselinePath = join(workspaceRoot, "baseline/agent-gui.json"); const update = runScript(workspaceRoot, baselinePath, ["--update-baseline"]); assert.equal(update.status, 0, update.stderr || update.stdout); const baseline = JSON.parse(await readFile(baselinePath, "utf8")); assert.equal(baseline.metrics.providerBranches, 1); const pass = runScript(workspaceRoot, baselinePath, []); assert.equal(pass.status, 0, pass.stderr || pass.stdout); assert.match(pass.stdout, /degradation check passed/); await writeFile( sourcePath, 'if (provider === "codex") {}\nif (provider === "cursor") {}\n' ); const regression = runScript(workspaceRoot, baselinePath, []); assert.notEqual(regression.status, 0); assert.match(regression.stderr, /providerBranches: 1 -> 2/); await writeFile(sourcePath, "const clean = 1;\n"); const improvement = runScript(workspaceRoot, baselinePath, []); assert.notEqual(improvement.status, 0); assert.match(improvement.stderr, /--update-baseline/); }); test("full mode refuses to baseline presentation hints without lifecycle reasons", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); const stylesheetPath = join( workspaceRoot, "packages/agent/gui/app/renderer/agentactivity.css" ); await mkdir(dirname(stylesheetPath), { recursive: true }); await writeFile( stylesheetPath, ` .viewer { will-change: transform; } .agent-gui-node__layout[data-agent-gui-visible="false"] :where(*, *::before, *::after) { animation-play-state: paused !important; } .agent-gui-node__layout[data-agent-gui-visible="false"] { content-visibility: hidden; } .agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-track, .agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-item { animation: none; will-change: auto; } ` ); const baselinePath = join(workspaceRoot, "baseline/agent-gui.json"); await mkdir(dirname(baselinePath), { recursive: true }); await writeFile( baselinePath, `${JSON.stringify({ identityExemptFiles: [], metrics: {}, presentationHintReasons: {} })}\n` ); const missingReason = runScript(workspaceRoot, baselinePath, [ "--update-baseline" ]); assert.notEqual(missingReason.status, 0); assert.match(missingReason.stderr, /missing reason/); await writeFile( baselinePath, `${JSON.stringify({ identityExemptFiles: [], metrics: {}, presentationHintReasons: { "packages/agent/gui/app/renderer/agentactivity.css | .viewer | will-change: transform": "Viewer is mounted only while its modal is open" } })}\n` ); const update = runScript(workspaceRoot, baselinePath, ["--update-baseline"]); assert.equal(update.status, 0, update.stderr || update.stdout); const pass = runScript(workspaceRoot, baselinePath, []); assert.equal(pass.status, 0, pass.stderr || pass.stdout); }); test("staged mode flags violations on staged added lines end to end", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); runFixtureGit(workspaceRoot, ["init", "--quiet"]); await assertFixtureGitRoot(workspaceRoot); const sourcePath = join( workspaceRoot, "packages/agent/gui/agent-gui/helper.ts" ); await mkdir(dirname(sourcePath), { recursive: true }); await writeFile(sourcePath, "export const before = 1;\n"); runFixtureGit(workspaceRoot, ["add", "."]); runFixtureGit(workspaceRoot, [ "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "--quiet", "-m", "init" ]); await writeFile( sourcePath, "export const before = 1;\nsetTimeout(retry, 100);\n" ); runFixtureGit(workspaceRoot, ["add", "."]); const baselinePath = join(workspaceRoot, "baseline/agent-gui.json"); const result = runScript(workspaceRoot, baselinePath, ["--staged"]); assert.notEqual(result.status, 0); assert.match(result.stderr, /no-sync-timer/); await writeFile( sourcePath, "export const before = 1;\n// timing: retry with visible reason\nsetTimeout(retry, 100);\n" ); runFixtureGit(workspaceRoot, ["add", "."]); const pass = runScript(workspaceRoot, baselinePath, ["--staged"]); assert.equal(pass.status, 0, pass.stderr || pass.stdout); assert.doesNotMatch(pass.stderr, /fatal:/u); }); test("staged mode accepts a CSS hint reason only after the baseline is staged", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); runFixtureGit(workspaceRoot, ["init", "--quiet"]); await assertFixtureGitRoot(workspaceRoot); const stylesheetPath = join( workspaceRoot, "packages/agent/gui/app/renderer/agentactivity.css" ); const baselinePath = join( workspaceRoot, "tools/degradation-baseline/agent-gui.json" ); await mkdir(dirname(stylesheetPath), { recursive: true }); await mkdir(dirname(baselinePath), { recursive: true }); const requiredCss = ` .agent-gui-node__layout[data-agent-gui-visible="false"] :where(*, *::before, *::after) { animation-play-state: paused !important; } .agent-gui-node__layout[data-agent-gui-visible="false"] { content-visibility: hidden; } .agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-track, .agent-gui-node__layout[data-agent-gui-active="false"] .agent-gui-node__composer-prompt-tip-item { animation: none; will-change: auto; } `; await writeFile(stylesheetPath, requiredCss); await writeFile( baselinePath, `${JSON.stringify({ identityExemptFiles: [], metrics: {}, presentationHintReasons: {} })}\n` ); runFixtureGit(workspaceRoot, ["add", "."]); runFixtureGit(workspaceRoot, [ "-c", "user.email=test@example.com", "-c", "user.name=Test", "commit", "--quiet", "-m", "init" ]); await writeFile( stylesheetPath, `${requiredCss}\n.viewer {\n will-change: transform;\n}\n` ); await writeFile( baselinePath, `${JSON.stringify({ identityExemptFiles: [], metrics: {}, presentationHintReasons: { "packages/agent/gui/app/renderer/agentactivity.css | .viewer | will-change: transform": "Viewer is mounted only while its modal is open" } })}\n` ); runFixtureGit(workspaceRoot, ["add", stylesheetPath]); const unstagedReason = runScript(workspaceRoot, baselinePath, ["--staged"]); assert.notEqual(unstagedReason.status, 0); assert.match(unstagedReason.stderr, /presentation-hint-lifecycle/); runFixtureGit(workspaceRoot, ["add", baselinePath]); const stagedReason = runScript(workspaceRoot, baselinePath, ["--staged"]); assert.equal( stagedReason.status, 0, stagedReason.stderr || stagedReason.stdout ); }); test("full mode fails with instructions when the baseline is missing", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); await mkdir(join(workspaceRoot, "packages/agent/gui"), { recursive: true }); const baselinePath = join(workspaceRoot, "baseline/agent-gui.json"); const result = runScript(workspaceRoot, baselinePath, []); assert.notEqual(result.status, 0); assert.match(result.stderr, /baseline is missing/); }); test("full mode rejects identity exemptions whose files no longer exist", async () => { const workspaceRoot = await mkdtemp(join(tmpdir(), "agent-gui-degradation-")); const baselinePath = join(workspaceRoot, "baseline/agent-gui.json"); await mkdir(dirname(baselinePath), { recursive: true }); await writeFile( baselinePath, `${JSON.stringify({ identityExemptFiles: ["packages/agent/gui/providerTargets.ts"], metrics: {} })}\n` ); const result = runScript(workspaceRoot, baselinePath, []); assert.notEqual(result.status, 0); assert.match(result.stderr, /stale identity exemptions/); assert.match(result.stderr, /providerTargets\.ts/); }); function runScript(workspaceRoot, baselinePath, args) { return spawnSync(process.execPath, [scriptPath, ...args], { encoding: "utf8", env: { ...createIsolatedGitEnvironment(workspaceRoot), TUTTI_AGENT_GUI_DEGRADATION_BASELINE: baselinePath, TUTTI_WORKSPACE_ROOT: workspaceRoot } }); } function runFixtureGit(workspaceRoot, args) { const result = spawnSync("git", args, { cwd: workspaceRoot, encoding: "utf8", env: createIsolatedGitEnvironment(workspaceRoot) }); assert.equal(result.status, 0, result.stderr || result.stdout); return result; } async function assertFixtureGitRoot(workspaceRoot) { const result = runFixtureGit(workspaceRoot, [ "rev-parse", "--absolute-git-dir" ]); assert.equal( await realpath(result.stdout.trim()), await realpath(join(workspaceRoot, ".git")) ); }