From e6f6237d0e96e78ddef0bdcf7071cbc7f8ece6bf Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Fri, 4 Sep 2026 03:08:10 -0600 Subject: [PATCH 1/2] test(server): isolate the vitest suite from the host's ~/.codex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Codex token harvester resolves its rollout directory from CODEX_HOME and falls back to the host's real ~/.codex (src/agents/codex-sessions.ts:11). Any server test that harvests a codex agent then recursively walks that tree and reads the head of every rollout file. On this machine that is 5.7 GB / ~11.7k files, and the self-hosted CI runner is the same machine — so it is not a "works on CI" situation. PR #1041 patched the one test whose 30s budget it blew by swapping CODEX_HOME inside that test, but stopAgent harvests fire-and-forget (manager.ts:1173), so codex agents stopped by other tests still walked the real directory. The file was still spending ~24s on host I/O. Point CODEX_HOME at an empty directory for the whole server suite via vitest.config.ts `test.env`, and drop the now-redundant per-test override. apps/server/test/db/agent-manager.test.ts alone: 55.5s with one 30s timeout before, 7.5s and 129/129 after. Full server suite: 3130 passed in 40.4s. Co-Authored-By: Claude Opus 5 (1M context) --- apps/server/test/db/agent-manager.test.ts | 45 ++++++++--------------- apps/server/vitest.config.ts | 17 +++++++++ 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/apps/server/test/db/agent-manager.test.ts b/apps/server/test/db/agent-manager.test.ts index 218062bb8..43c169266 100644 --- a/apps/server/test/db/agent-manager.test.ts +++ b/apps/server/test/db/agent-manager.test.ts @@ -2431,38 +2431,23 @@ describe("AgentManager", () => { }); it("should skip session ownership logic for non-claude agents", async () => { - // Point the codex harvest at an empty temp dir: without this the - // harvester walks the real ~/.codex/sessions, and on a developer - // machine (or the self-hosted runner) that can be gigabytes of - // rollout files — enough to blow the test timeout. - const codexHome = await mkdtemp( - path.join(os.tmpdir(), "dispatch-codex-home-") - ); - const previousCodexHome = process.env.CODEX_HOME; - process.env.CODEX_HOME = codexHome; - try { - const agent = await manager.createAgent({ - name: "codex-agent", - type: "codex", - cwd: "/tmp", - useWorktree: false, - }); + const agent = await manager.createAgent({ + name: "codex-agent", + type: "codex", + cwd: "/tmp", + useWorktree: false, + }); - // Should not throw — codex agents don't use session ownership - await manager.harvestAgentTokens(agent); + // Should not throw — codex agents don't use session ownership + await manager.harvestAgentTokens(agent); - // No sessions exist under the isolated CODEX_HOME, so nothing is - // harvested. - const usage = await pool.query( - `SELECT COUNT(*)::int AS count FROM agent_token_usage WHERE agent_id = $1`, - [agent.id] - ); - expect(usage.rows[0].count).toBe(0); - } finally { - if (previousCodexHome === undefined) delete process.env.CODEX_HOME; - else process.env.CODEX_HOME = previousCodexHome; - await rm(codexHome, { recursive: true, force: true }); - } + // vitest.config.ts points CODEX_HOME at an empty directory for the whole + // suite, so no rollout files exist and nothing is harvested. + const usage = await pool.query( + `SELECT COUNT(*)::int AS count FROM agent_token_usage WHERE agent_id = $1`, + [agent.id] + ); + expect(usage.rows[0].count).toBe(0); }); }); diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts index 802baafae..0ac04763e 100644 --- a/apps/server/vitest.config.ts +++ b/apps/server/vitest.config.ts @@ -1,10 +1,27 @@ +import { mkdirSync } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { defineConfig } from "vitest/config"; +// The Codex token harvester resolves its rollout directory from `CODEX_HOME`, +// falling back to the host's real `~/.codex` (see src/agents/codex-sessions.ts). +// Any test that harvests a codex agent — including the fire-and-forget harvest +// `stopAgent` kicks off, which no per-test override can wrap — then recursively +// walks that directory and reads the head of every rollout file. On a machine +// that actually uses Codex that is gigabytes of I/O per run, and the self-hosted +// CI runner is such a machine. Point the whole suite at an empty directory so no +// test ever reads the host's CLI history. +const codexHome = path.join(os.tmpdir(), "dispatch-server-vitest-codex-home"); +mkdirSync(codexHome, { recursive: true }); + export default defineConfig({ test: { globals: true, testTimeout: 30_000, hookTimeout: 30_000, + env: { + CODEX_HOME: codexHome, + }, include: ["test/**/*.test.ts"], exclude: [ "**/node_modules/**", From 82dca38404d9f781429fe11be54e7cfd34e2746d Mon Sep 17 00:00:00 2001 From: Brad Harris Date: Fri, 4 Sep 2026 03:16:27 -0600 Subject: [PATCH 2/2] test(server): use mkdtempSync for the suite's isolated CODEX_HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review 1107 item 2612: a fixed name under os.tmpdir() is not guaranteed to be empty — mkdirSync(..., { recursive: true }) silently reuses an existing directory or follows an existing symlink, so a stale or planted `sessions` tree would put host-controlled data back in the harvester's path. mkdtempSync creates a fresh 0700 directory with a random suffix on every run, so it can never adopt an existing tree, and concurrent runs no longer share one. Removed on process exit. Verified: zero `dispatch-server-vitest-codex-*` directories in TMPDIR before and after a run; agent-manager.test.ts 129/129 in 8.1s; full server suite 3130 passed in 32.2s; pnpm run check clean. Co-Authored-By: Claude Opus 5 (1M context) --- apps/server/vitest.config.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/apps/server/vitest.config.ts b/apps/server/vitest.config.ts index 0ac04763e..88f6b6fd9 100644 --- a/apps/server/vitest.config.ts +++ b/apps/server/vitest.config.ts @@ -1,4 +1,4 @@ -import { mkdirSync } from "node:fs"; +import { mkdtempSync, rmSync } from "node:fs"; import os from "node:os"; import path from "node:path"; import { defineConfig } from "vitest/config"; @@ -11,8 +11,18 @@ import { defineConfig } from "vitest/config"; // that actually uses Codex that is gigabytes of I/O per run, and the self-hosted // CI runner is such a machine. Point the whole suite at an empty directory so no // test ever reads the host's CLI history. -const codexHome = path.join(os.tmpdir(), "dispatch-server-vitest-codex-home"); -mkdirSync(codexHome, { recursive: true }); +// +// `mkdtempSync` rather than a fixed name: it creates a fresh 0700 directory with +// a random suffix, so it can never reuse a stale tree, adopt a planted +// `sessions` directory, or follow a pre-existing symlink the way +// `mkdirSync(fixedName, { recursive: true })` silently would. Removed on exit; +// concurrent runs each get their own. +const codexHome = mkdtempSync( + path.join(os.tmpdir(), "dispatch-server-vitest-codex-") +); +process.on("exit", () => { + rmSync(codexHome, { recursive: true, force: true }); +}); export default defineConfig({ test: {