Files
codegraph/__tests__/test-home-sandbox.test.ts
Colby Mchenry 7bbc1fb7f6 fix(tests): the test suite no longer writes to the developer's real home dir (#2275) (#2295)
Running the suite on a machine with Claude Code configured could wire the
CodeGraph prompt hook into the developer's own ~/.claude/settings.json:
every upgrade test whose fake upgrade succeeded ran the real post-upgrade
prompt-hook self-heal, which detected the real global Claude profile and
wrote it. Daemon suites likewise left records in the real
~/.codegraph/daemons. On Windows a HOME-only redirect would not have
helped, since os.homedir() reads USERPROFILE there.

- The prompt-hook writer is now injected through UpgradeDeps
  (wirePromptHook); the CLI passes the real defaultWirePromptHook and the
  upgrade tests a recorder. New tests pin that a successful upgrade calls
  the recorder and that the real writer never touches a configured profile.
- A vitest setupFiles entry gives every engine test file a throwaway home:
  HOME/USERPROFILE (plus HOMEDRIVE/HOMEPATH/APPDATA/LOCALAPPDATA on
  Windows), XDG_CONFIG_HOME, and GIT_CONFIG_GLOBAL (seeded with a dummy
  identity) point into it, and CLAUDE_CONFIG_DIR/CODEX_HOME/COPILOT_HOME/
  HERMES_HOME are cleared. Spawned CLI/MCP children inherit it. A sandbox
  test asserts os.homedir(), the Claude profile, the daemon registry, a
  child process and git's global config all resolve inside it.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:18:46 +00:00

66 lines
2.6 KiB
TypeScript

/**
* The suite's home-dir sandbox (`setup-home-sandbox.ts`, #2275) is in force:
* nothing a test runs — in-process or spawned — resolves the developer's real
* home, Claude profile, daemon registry or git identity.
*/
import { describe, it, expect } from 'vitest';
import { execFileSync } from 'child_process';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { claudeTarget } from '../src/installer/targets/claude';
import { getRegistryDir } from '../src/mcp/daemon-registry';
const tmpRoot = fs.realpathSync(os.tmpdir());
const under = (p: string, dir: string): boolean => path.resolve(p).startsWith(dir + path.sep);
/** The sandbox home — asserted to be one, so the checks below can't pass on a real home. */
function sandboxHome(): string {
const home = os.homedir();
expect(under(home, tmpRoot), home).toBe(true);
return home;
}
describe('test home sandbox', () => {
it('os.homedir() is a throwaway dir under the temp root, not the real home', () => {
const home = sandboxHome();
expect(path.basename(home)).toMatch(/^codegraph-test-home-/);
// The account's profile dir, which ignores HOME / USERPROFILE.
expect(home).not.toBe(os.userInfo().homedir);
});
it('the global Claude profile and the daemon registry resolve inside it', () => {
const home = sandboxHome();
expect(process.env.CLAUDE_CONFIG_DIR).toBeUndefined();
expect(under(claudeTarget.detect('global').configPath, home)).toBe(true);
expect(under(getRegistryDir(), home)).toBe(true);
});
it('a spawned child sees the same home', () => {
const childHome = execFileSync(process.execPath, ['-p', 'require("os").homedir()'], {
encoding: 'utf-8',
windowsHide: true,
}).trim();
expect(childHome).toBe(sandboxHome());
});
it("git's global config is the seeded sandbox file", () => {
const gitConfig = process.env.GIT_CONFIG_GLOBAL ?? '';
expect(under(gitConfig, sandboxHome())).toBe(true);
const name = execFileSync('git', ['config', '--global', '--get', 'user.name'], {
encoding: 'utf-8',
windowsHide: true,
}).trim();
expect(name).toBe('CodeGraph Test');
});
// os.homedir() reads USERPROFILE there, and git falls back to
// HOMEDRIVE+HOMEPATH — a HOME-only sandbox would not hold.
it.runIf(process.platform === 'win32')('covers the Windows home variables', () => {
const home = sandboxHome();
expect(process.env.USERPROFILE).toBe(home);
expect(`${process.env.HOMEDRIVE}${process.env.HOMEPATH}`).toBe(home);
expect(under(process.env.APPDATA ?? '', home)).toBe(true);
expect(under(process.env.LOCALAPPDATA ?? '', home)).toBe(true);
});
});