diff --git a/config/vitest.ci.config.ts b/config/vitest.ci.config.ts index 0305838c..e0490680 100644 --- a/config/vitest.ci.config.ts +++ b/config/vitest.ci.config.ts @@ -23,6 +23,13 @@ export default defineConfig({ include: ['test/**/*.test.ts'], exclude: [...configDefaults.exclude, ...NON_CI_TEST_GLOBS], setupFiles: ['./test/setup.ts'], + // SAFETY: force every worker's data dir away from prod `~/.codeman`. Route + // tests (e.g. session-routes-workspace-hooks) write remote-hosts.json into + // `getDataDir()`; without this a bare run clobbers the production host + // registry (found 2026-08-29). `/tmp` is fine here — the tree is throwaway. + env: { + CODEMAN_DATA_DIR: '/tmp/codeman-vitest-data', + }, fileParallelism: false, testTimeout: 30000, teardownTimeout: 60000, diff --git a/config/vitest.config.ts b/config/vitest.config.ts index 578e6c4a..7b1317d4 100644 --- a/config/vitest.config.ts +++ b/config/vitest.config.ts @@ -21,6 +21,13 @@ export default defineConfig({ environment: 'node', include: ['test/**/*.test.ts'], setupFiles: ['./test/setup.ts'], + // SAFETY: force every worker's data dir away from prod `~/.codeman`. Route + // tests (e.g. session-routes-workspace-hooks) write remote-hosts.json into + // `getDataDir()`; without this a bare run clobbers the production host + // registry (found 2026-08-29). `/tmp` is fine here — the tree is throwaway. + env: { + CODEMAN_DATA_DIR: '/tmp/codeman-vitest-data', + }, // Run test files sequentially to respect mux session limits // Individual tests within files still run in parallel where safe fileParallelism: false, diff --git a/test/routes/session-routes-workspace-hooks.test.ts b/test/routes/session-routes-workspace-hooks.test.ts index 4957226d..39c4763e 100644 --- a/test/routes/session-routes-workspace-hooks.test.ts +++ b/test/routes/session-routes-workspace-hooks.test.ts @@ -168,11 +168,22 @@ describe('POST /api/sessions workspace hooks', () => { // `user@host:session` — locally a RELATIVE path, so a mkdir would create it // as a junk directory under the server cwd. statusLineTelemetry rides along: // applyStatusLineConfig mkdirs the same way and used to run for remote attaches. - await mkdir(getDataDir(), { recursive: true }); - await writeFile( - join(getDataDir(), 'remote-hosts.json'), - JSON.stringify([{ id: 'h1', label: 'box', host: '10.0.0.5', username: 'dev' }]) - ); + // SAFETY (2026-08-29): `getDataDir()` is call-time so stub the env to a + // throwaway dir for this write — otherwise this test overwrites the PROD + // `~/.codeman/remote-hosts.json` with the fixture below, wiping every + // user-defined remote host (caught live: a full-suite run emptied the + // launch-case dropdown and broke remote session creation). + const fixtureDataDir = join(tmpdir(), `codeman-hook-fixture-${process.pid}`); + vi.stubEnv('CODEMAN_DATA_DIR', fixtureDataDir); + try { + await mkdir(getDataDir(), { recursive: true }); + await writeFile( + join(getDataDir(), 'remote-hosts.json'), + JSON.stringify([{ id: 'h1', label: 'box', host: '10.0.0.5', username: 'dev' }]) + ); + } finally { + vi.unstubAllEnvs(); + } const res = await createSession({ name: 'hooks-remote', diff --git a/test/setup.ts b/test/setup.ts index edd11f51..e5135457 100644 --- a/test/setup.ts +++ b/test/setup.ts @@ -18,6 +18,7 @@ const originalHome = process.env.HOME; const originalUserProfile = process.env.USERPROFILE; const originalVitest = process.env.VITEST; const originalPlaywrightBrowsersPath = process.env.PLAYWRIGHT_BROWSERS_PATH; +const originalCodemanDataDir = process.env.CODEMAN_DATA_DIR; const testHome = mkdtempSync(join(tmpdir(), 'codeman-vitest-')); if (originalPlaywrightBrowsersPath === undefined && originalHome) { @@ -32,6 +33,16 @@ process.env.HOME = testHome; process.env.USERPROFILE = testHome; process.env.VITEST = 'true'; +// SAFETY: `getDataDir()` resolves via `homedir()` → `~/.codeman`. +// Overriding HOME above is NOT enough: on Linux `os.homedir()` reads /etc/passwd, +// not $HOME, so without this a route test that writes `remote-hosts.json` (or +// any state file) into `getDataDir()` silently clobbers the PRODUCTION +// `~/.codeman` tree (found 2026-08-29: `session-routes-workspace-hooks.test.ts` +// overwrote prod `remote-hosts.json` with an `h1/box/10.0.0.5` fixture during a +// bare full-suite run, wiping every user-defined remote host and emptying the +// launch case dropdown). Point every test at a throwaway data dir instead. +process.env.CODEMAN_DATA_DIR = join(tmpdir(), `codeman-vitest-data-${process.pid}`); + delete process.env.CODEMAN_PASSWORD; delete process.env.CODEMAN_USERNAME; // Gesture availability changes renderIndexHtml output (injects the @@ -64,7 +75,11 @@ afterAll(async () => { if (originalPlaywrightBrowsersPath === undefined) delete process.env.PLAYWRIGHT_BROWSERS_PATH; else process.env.PLAYWRIGHT_BROWSERS_PATH = originalPlaywrightBrowsersPath; + if (originalCodemanDataDir === undefined) delete process.env.CODEMAN_DATA_DIR; + else process.env.CODEMAN_DATA_DIR = originalCodemanDataDir; + rmSync(testHome, { recursive: true, force: true }); + rmSync(process.env.CODEMAN_DATA_DIR ?? '', { recursive: true, force: true }); }); // afterAll never fires for a fully-skipped test file (no tests execute), which