From d3f36e92a5dae321009963a3d1dad8bf2261cf1e Mon Sep 17 00:00:00 2001 From: "paseo-bot[bot]" <266920839+paseo-bot[bot]@users.noreply.github.com> Date: Wed, 28 Oct 2026 21:25:31 +0000 Subject: [PATCH] test(server): isolate config responsiveness from test runner work (#8131) Measure config reads in a fresh process so unrelated runner callbacks cannot consume the config CPU budget. Keep the existing budget, result, async I/O and yield assertions, and prove isolation with a busy parent callback. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com> --- .../src/utils/paseo-config-file.test.ts | 56 ++++++++++++++++--- .../test-fixtures/config-responsiveness.ts | 14 +++++ 2 files changed, 61 insertions(+), 9 deletions(-) create mode 100644 packages/server/src/utils/test-fixtures/config-responsiveness.ts diff --git a/packages/server/src/utils/paseo-config-file.test.ts b/packages/server/src/utils/paseo-config-file.test.ts index 1eed6ec054..186a5259ed 100644 --- a/packages/server/src/utils/paseo-config-file.test.ts +++ b/packages/server/src/utils/paseo-config-file.test.ts @@ -3,7 +3,6 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { writeFile, truncate, rm } from "node:fs/promises"; -import { measureMainThreadWork } from "../test-utils/main-thread-work.js"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -21,6 +20,24 @@ import { writePaseoConfigForEdit, } from "./paseo-config-file.js"; +async function measureConfigRead(directory: string) { + const fixture = fileURLToPath( + new URL("./test-fixtures/config-responsiveness.ts", import.meta.url), + ); + const { stdout, stderr } = await promisify(execFile)( + process.execPath, + ["--import", "tsx", fixture, directory], + { timeout: 15_000 }, + ); + process.stderr.write(stderr); + return JSON.parse(stdout) as { + result: Awaited>; + blockingMs: number; + filesystemRequests: number; + yields: number; + }; +} + describe("paseo config file substrate", () => { let tempDir: string; @@ -67,6 +84,33 @@ describe("paseo config file substrate", () => { ); }); + it("excludes unrelated test-runner CPU from the config read budget", async () => { + await writeFile(join(tempDir, "paseo.json"), JSON.stringify({ worktree: { setup: "npm ci" } })); + const unrelatedWork = new Promise((resolve) => { + setImmediate(() => { + const threadCpuUsage: (previous?: NodeJS.CpuUsage) => NodeJS.CpuUsage = Reflect.get( + process, + "threadCpuUsage", + ); + const start = threadCpuUsage(); + let elapsed: NodeJS.CpuUsage; + do { + elapsed = threadCpuUsage(start); + } while (elapsed.user + elapsed.system < 60_000); + resolve(); + }); + }); + const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir); + await unrelatedWork; + expect(result).toEqual({ + ok: true, + config: { worktree: { setup: "npm ci" } }, + revision: await statPaseoConfigPath(tempDir), + }); + expect(filesystemRequests).toBeGreaterThan(0); + expect(blockingMs).toBeLessThan(20); + }); + it("keeps main-thread work below 20ms while admitting large configs", async () => { const config = { scripts: Object.fromEntries( @@ -75,10 +119,7 @@ describe("paseo config file substrate", () => { worktree: { setup: "npm ci" }, }; await writeFile(join(tempDir, "paseo.json"), JSON.stringify(config)); - const { result, blockingMs, filesystemRequests, yields } = await measureMainThreadWork( - "project config 7000 scripts main-thread work", - () => readPaseoConfigForEdit(tempDir), - ); + const { result, blockingMs, filesystemRequests, yields } = await measureConfigRead(tempDir); expect(result).toEqual({ ok: true, config, revision: await statPaseoConfigPath(tempDir) }); expect(blockingMs).toBeLessThan(20); expect(filesystemRequests).toBeGreaterThan(0); @@ -90,10 +131,7 @@ describe("paseo config file substrate", () => { join(tempDir, "paseo.json"), JSON.stringify({ extra: "x".repeat(32 * 1024 * 1024) }), ); - const { result, blockingMs, filesystemRequests } = await measureMainThreadWork( - "project config 32MiB main-thread work", - () => readPaseoConfigForEdit(tempDir), - ); + const { result, blockingMs, filesystemRequests } = await measureConfigRead(tempDir); expect(blockingMs).toBeLessThan(20); expect(filesystemRequests).toBeGreaterThan(0); expect(result).toEqual({ ok: false, error: { code: "invalid_project_config" } }); diff --git a/packages/server/src/utils/test-fixtures/config-responsiveness.ts b/packages/server/src/utils/test-fixtures/config-responsiveness.ts new file mode 100644 index 0000000000..656c5a2864 --- /dev/null +++ b/packages/server/src/utils/test-fixtures/config-responsiveness.ts @@ -0,0 +1,14 @@ +import assert from "node:assert/strict"; +import { setImmediate as nextTurn } from "node:timers/promises"; +import { measureMainThreadWork } from "../../test-utils/main-thread-work.js"; +import { readPaseoConfigForEdit } from "../paseo-config-file.js"; + +// Keep Vitest transforms, reporting, and unrelated callbacks outside the measured +// thread. Read the real file once, without warming the config snapshot or schema. +const directory = process.argv[2]; +assert.ok(directory, "Config directory is required"); +await nextTurn(); +const measured = await measureMainThreadWork("project config main-thread work", () => + readPaseoConfigForEdit(directory), +); +process.stdout.write(JSON.stringify(measured));