From 023a9953a5440bbe5d45d566410b0165a920d94a Mon Sep 17 00:00:00 2001 From: Joseph Mearman Date: Tue, 27 Oct 2026 14:32:46 +0000 Subject: [PATCH] fix: stop two tests depending on how fast a busy machine runs The watchdog tests that wait for a sampler's own outcome gave it the production deadline, so a child process that was slow to start made the watchdog report that the sampler did not finish instead of how it ended. They now use the file's existing guard against a sampler that never ends. The activity width sweep resolved the statusline settings and rebuilt the palette for every row at every width of every session. The settings and palette are now built once per case and the render context once per session and width, shared by the five rows. Fixes #618 --- src/frontdoor/watchdog.test.ts | 8 ++++---- src/statusline/elements/activityFit.test.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/frontdoor/watchdog.test.ts b/src/frontdoor/watchdog.test.ts index de46b54a..48cf651e 100644 --- a/src/frontdoor/watchdog.test.ts +++ b/src/frontdoor/watchdog.test.ts @@ -36,7 +36,7 @@ const BOUNDS_TO_WAIT_FOR_SILENCE = 4; const BOUNDS_TO_BLOCK = 3; /** The wait for the main thread to pause for the tests that do not read its frames: none, so a stall is reported as soon as it is seen. */ const NO_PAUSE_WAIT_MS = 0; -/** The wait for the test that does read them: a guard against a pause that never comes, not an expectation of how long one takes, so a loaded machine cannot make it flaky. */ +/** The wait for the test that does read them, and the deadline a sampler is given by the tests that wait for what the sampler itself does (it exits, or it cannot start): a guard against a pause or a sampler that never ends, not an expectation of how long one takes, so a loaded machine cannot make it flaky. Starting a child process is slow enough on a loaded machine to pass any fixed figure, and the watchdog would then report that the sampler did not finish instead of how it ended. The tests of the deadline itself set their own. */ const PAUSE_WAIT_GUARD_MS = 60_000; /** The processor count of the fixed load readings the thread tests judge by, so the machine running the tests (which may itself be loaded) never decides whether a stall is reported. */ @@ -315,7 +315,7 @@ describe("startThreadWatchdog", () => { fs.writeFileSync(path.join(samples, old), "older"); fs.utimesSync(path.join(samples, old), new Date(0), new Date(Number(old.slice(0, 1)) * MS_PER_SECOND)); } - const nativeSample = { command: process.execPath, args: ["-e", "require('node:fs').writeFileSync(process.argv[1], 'sampled ' + process.argv[2] + 's')", "{file}", "{seconds}"], directory: samples, seconds: 1, overheadMs: PROBE_TIMEOUT_MS, keep: 2 }; + const nativeSample = { command: process.execPath, args: ["-e", "require('node:fs').writeFileSync(process.argv[1], 'sampled ' + process.argv[2] + 's')", "{file}", "{seconds}"], directory: samples, seconds: 1, overheadMs: PAUSE_WAIT_GUARD_MS, keep: 2 }; const log = await stallWith({ nativeSample }); await vi.waitFor( () => { @@ -340,7 +340,7 @@ describe("startThreadWatchdog", () => { it("says the sampler wrote nothing when it fails, passing on what it said, rather than naming a file that is not there", async () => { const samples = path.join(directory, "stall-samples"); - const nativeSample = { command: process.execPath, args: ["-e", "process.stderr.write('cannot examine process'); process.exit(1)"], directory: samples, seconds: 1, overheadMs: PROBE_TIMEOUT_MS, keep: 1 }; + const nativeSample = { command: process.execPath, args: ["-e", "process.stderr.write('cannot examine process'); process.exit(1)"], directory: samples, seconds: 1, overheadMs: PAUSE_WAIT_GUARD_MS, keep: 1 }; await stallWith({ nativeSample }); await vi.waitFor( () => { @@ -382,7 +382,7 @@ describe("startThreadWatchdog", () => { }); it("says so when the native sampler cannot be run, and still ends the stall", async () => { - const nativeSample = { command: path.join(directory, "no-such-sampler"), args: [], directory: path.join(directory, "stall-samples"), seconds: 1, overheadMs: PROBE_TIMEOUT_MS, keep: 1 }; + const nativeSample = { command: path.join(directory, "no-such-sampler"), args: [], directory: path.join(directory, "stall-samples"), seconds: 1, overheadMs: PAUSE_WAIT_GUARD_MS, keep: 1 }; const log = await stallWith({ nativeSample }); await vi.waitFor( () => { diff --git a/src/statusline/elements/activityFit.test.ts b/src/statusline/elements/activityFit.test.ts index c0610724..5563e8af 100644 --- a/src/statusline/elements/activityFit.test.ts +++ b/src/statusline/elements/activityFit.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it } from "vitest"; -import { resolveStatusline, type StatuslineSettings } from "../../config/statusline"; +import { resolveStatusline, type ResolvedStatusline, type StatuslineSettings } from "../../config/statusline"; import { visibleLength } from "../ansi"; import { STATUSLINE_LANGUAGES } from "../messages/languages"; -import { createPalette } from "../palette"; +import { createPalette, type Palette } from "../palette"; import { fakeRenderContext } from "../testing"; import { EMPTY_ACTIVITY_FACTS, type ActivityFacts, type AgentFact, type NamesFact, type ToolCallFact } from "../transcript/activity"; import type { RenderContext, RowOutput } from "./context"; @@ -71,11 +71,11 @@ function session(seed: number): ActivityFacts { }; } -function contextOf(activity: ActivityFacts, width: number, settings: StatuslineSettings): RenderContext { - const resolved = resolveStatusline(settings); +/** The context a row draws from, for `activity` at `width`. The resolved settings and the palette are the caller's, built once per settings choice: they depend on neither the session nor the width, and resolving them parses the whole settings schema. */ +function contextOf(activity: ActivityFacts, width: number, resolved: ResolvedStatusline, palette: Palette): RenderContext { return fakeRenderContext({ settings: resolved, - palette: createPalette(resolved.colours, true), + palette, width, nowMs: NOW_MS, transcript: () => ({ @@ -104,12 +104,15 @@ describe("the activity rows at every width", () => { for (const language of STATUSLINE_LANGUAGES) { for (const [choice, settings] of SETTINGS) { it(`never draw a line wider than the width, in ${language} with ${choice}`, () => { + const resolved = resolveStatusline({ ...settings, language }); + const palette = createPalette(resolved.colours, true); const overflowing: string[] = []; for (let index = 0; index < SESSIONS; index += 1) { const activity = session(SEED + index); for (let width = MIN_WIDTH; width <= MAX_WIDTH; width += 1) { + const context = contextOf(activity, width, resolved, palette); for (const [name, row] of Object.entries(ROWS)) { - for (const line of linesOf(row(contextOf(activity, width, { ...settings, language })))) { + for (const line of linesOf(row(context))) { if (visibleLength(line) > width) { overflowing.push(`${name} session ${String(index)} width ${String(width)}: ${String(visibleLength(line))} columns`); } @@ -123,7 +126,9 @@ describe("the activity rows at every width", () => { } it("draw something for the sessions the test generates, so the widths it checks are not all empty rows", () => { - const drawn = Object.values(ROWS).map((row) => Array.from({ length: SESSIONS }, (_, index) => linesOf(row(contextOf(session(SEED + index), MAX_WIDTH, {}))).length).reduce((total, count) => total + count, 0)); + const resolved = resolveStatusline({}); + const palette = createPalette(resolved.colours, true); + const drawn = Object.values(ROWS).map((row) => Array.from({ length: SESSIONS }, (_, index) => linesOf(row(contextOf(session(SEED + index), MAX_WIDTH, resolved, palette))).length).reduce((total, count) => total + count, 0)); expect(drawn.every((count) => count > 0)).toBe(true); }); });