diff --git a/test/gstack-gbrain-source-wireup.test.ts b/test/gstack-gbrain-source-wireup.test.ts index d7a30b768..b9995b678 100644 --- a/test/gstack-gbrain-source-wireup.test.ts +++ b/test/gstack-gbrain-source-wireup.test.ts @@ -20,6 +20,18 @@ const ROOT = path.resolve(import.meta.dir, '..'); const BIN_DIR = path.join(ROOT, 'bin'); const WIREUP_BIN = path.join(BIN_DIR, 'gstack-gbrain-source-wireup'); +// Hermetic PATH base (#2255). The missing-gbrain fixtures must not see a +// user-installed gbrain on the host (e.g. macOS /opt/homebrew/bin), or the +// "missing" case exits 0 instead of 2. Base is root-owned OS dirs only +// (/usr/bin:/bin:/usr/sbin:/sbin — where git/python3/jq/coreutils resolve on +// macOS and Linux) plus BUN_ONLY_DIR, so no user-installed gbrain can be +// present. The scratch dir holds only a bun symlink, mirroring +// gbrain-detect-install.test.ts so spawned children can resolve bun on CI +// regardless of install dir. +const BUN_ONLY_DIR = fs.mkdtempSync(path.join(os.tmpdir(), 'wireup-bun-only-')); +fs.symlinkSync(process.execPath, path.join(BUN_ONLY_DIR, 'bun')); +const HERMETIC_PATH = `/usr/bin:/bin:/usr/sbin:/sbin:${BUN_ONLY_DIR}`; + let tmpHome: string; let gstackHome: string; let worktreeDir: string; @@ -113,7 +125,7 @@ function run( opts: { env?: Record } = {} ) { const env = { - PATH: `${fakeBinDir}:${process.env.PATH || '/usr/bin:/bin:/opt/homebrew/bin'}`, + PATH: `${fakeBinDir}:${HERMETIC_PATH}`, HOME: tmpHome, GSTACK_HOME: gstackHome, GSTACK_BRAIN_WORKTREE: worktreeDir, @@ -229,14 +241,52 @@ describe('gstack-gbrain-source-wireup — wireup mode', () => { test('--strict + gbrain missing on PATH: exits 2', () => { setupGstackRepo('git@github.com:user/gstack-brain-user.git'); - // Don't make a fake gbrain — fakeBinDir is empty. Keep system dirs on PATH - // so basic commands (git, awk, sed, etc.) work; only `gbrain` is absent. - const r = run(['--strict'], { - env: { PATH: `${fakeBinDir}:/usr/bin:/bin:/opt/homebrew/bin` }, - }); + // Don't make a fake gbrain — fakeBinDir is empty. run() applies the + // hermetic PATH base; only `gbrain` is absent. + const r = run(['--strict']); expect(r.status).toBe(2); }); + test('--strict + gbrain present in controlled dir: exits 0 (positive control)', () => { + setupGstackRepo('git@github.com:user/gstack-brain-user.git'); + // Positive control for hermeticity: a gbrain stub in the test-controlled + // fakeBinDir (first on the hermetic PATH) IS found, so --strict proceeds + // (exit 0). This proves the fixture CAN supply gbrain when present; the + // determinism test below proves the host cannot leak one in (#2255). + makeFakeGbrain({}); + const r = run(['--strict'], { env: { GSTACK_BRAIN_NO_SYNC: '1' } }); + expect(r.status).toBe(0); + expect(gbrainCalls().some((c) => c.startsWith('gbrain sources add'))).toBe(true); + }); + + test('--strict + gbrain present in a host-like dir: still exits 2 (determinism)', () => { + setupGstackRepo('git@github.com:user/gstack-brain-user.git'); + // Determinism check (#2255, plan TS2): plant a real-looking gbrain stub in + // a dir that the OLD fixture would have leaked via process.env.PATH or the + // hardcoded /opt/homebrew/bin list. The root-owned-only hermetic base + // excludes user-writable dirs, so the child never sees the stub and the + // missing case stays deterministic across dev machines. This test fails on + // the unpatched fixture (stub found -> exit 0) and passes on the fixed one. + const hostLikeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wireup-host-like-')); + fs.writeFileSync( + path.join(hostLikeDir, 'gbrain'), + '#!/bin/bash\necho "gbrain 0.18.2"\n', + { mode: 0o755 } + ); + // No env PATH override: run() applies the hermetic base. The stub exists + // only in a user-writable dir the base excludes. + const r = run(['--strict']); + expect(r.status).toBe(2); + // Sanity: the stub IS visible to a shell using the host-like PATH, so this + // test would catch the old leak if the base ever regressed. + const check = spawnSync('bash', ['-c', `command -v gbrain && gbrain --version`], { + env: { PATH: `${hostLikeDir}:${process.env.PATH || '/usr/bin:/bin'}` }, + encoding: 'utf-8', + }); + expect(check.status).toBe(0); + expect(check.stdout).toContain('gbrain 0.18.2'); + }); + test('source-id derived from origin URL', () => { setupGstackRepo('git@github.com:user/gstack-brain-alice.git'); makeFakeGbrain({}); @@ -388,9 +438,7 @@ describe('gstack-gbrain-source-wireup — uninstall mode', () => { expect(fs.existsSync(worktreeDir)).toBe(true); // Now remove the fake gbrain so uninstall sees gbrain missing fs.rmSync(path.join(fakeBinDir, 'gbrain'), { force: true }); - const r = run(['--uninstall'], { - env: { PATH: `${fakeBinDir}:/usr/bin:/bin:/opt/homebrew/bin` }, - }); + const r = run(['--uninstall']); expect(r.status).toBe(0); // best-effort, never fails on gbrain absence expect(fs.existsSync(worktreeDir)).toBe(false); // worktree still cleaned up });