diff --git a/tests/kit/provider-cli.test.mjs b/tests/kit/provider-cli.test.mjs index e43269b..f77619f 100644 --- a/tests/kit/provider-cli.test.mjs +++ b/tests/kit/provider-cli.test.mjs @@ -5,7 +5,7 @@ // real machine's kit.json or ~/.claude — src/lib/paths.mjs's configBase() // reads APPDATA (not XDG_CONFIG_HOME) on win32, so both must be set or the // sandbox is silently bypassed there. -import { test } from 'node:test'; +import { test, after } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { fileURLToPath } from 'node:url'; @@ -14,6 +14,22 @@ import os from 'node:os'; import path from 'node:path'; import { DUAL_ROLE_TIP, JUDGE_BIAS_TIP } from '../../src/lib/providers.mjs'; +// Tripwire (#137): a spawned `ak x host pick` whose cwd falls back to the test +// process's cwd writes PROJECT-scoped config (.claude/settings.local.json, +// .agentic-qe/llm-config.json) into the REAL repository the suite runs from — +// HOME sandboxing cannot catch that class of leak. Record the real cwd's state +// at module load and prove it byte-identical when the suite ends. +const REAL_PROJECT_FILES = ['.claude/settings.local.json', '.agentic-qe/llm-config.json'] + .map((rel) => path.resolve(process.cwd(), rel)); +const readOrNull = (f) => { try { return fs.readFileSync(f, 'utf8'); } catch { return null; } }; +const realProjectBefore = REAL_PROJECT_FILES.map(readOrNull); +after(() => { + REAL_PROJECT_FILES.forEach((f, i) => { + assert.equal(readOrNull(f), realProjectBefore[i], + `${f} was modified by this suite — a spawned ak command leaked out of the sandbox (cwd not anchored to the sandbox project?)`); + }); +}); + const BIN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../bin/agentic-kit.mjs'); function sandbox({ hosts }) { @@ -163,10 +179,13 @@ function pickSandbox({ return { home, project, binDir, catalog }; } -function akPick(args, { cwd, home, binDir, catalog }, { input, env = {} } = {}) { +function akPick(args, { cwd, project, home, binDir, catalog }, { input, env = {} } = {}) { return spawnSync(process.execPath, [BIN, ...args], { encoding: 'utf8', - cwd, + // Anchor at the sandbox project (#137): an undefined cwd inherits the test + // process's cwd — the real repository — and pick's PROJECT-scoped writes + // (settings.local.json, llm-config.json) would land there. + cwd: cwd ?? project, input, env: { ...process.env,