// tests/lib/no-claude-code-launch.test.mjs // Nothing Voyage publishes may start Claude Code. The published tree is every // tracked file (the catalog pins a release tag, so the tag's tree is what a // user installs). A hit is any line that carries a way to start a Claude Code // session or model run: // cli the `claude` CLI with a flag or a prompt (-p, --print, --bg, // --resume, --version, "prompt", $(...)); a user opening their // own session is the PM way and carries no command line // plugin-eval `claude plugin eval` (it runs one child session per case) // spawn spawn/exec/execFile/fork of the `claude` binary from code // sdk the Agent SDK, the claude-code npm package, the claude-code // GitHub Action // bypass permission-bypass flags, which only exist for launched sessions // Plugin management (`claude plugin install|validate`, `marketplace add`) // starts no session and is not a hit. // // Classes: (a) starts Claude Code, (b) tells a user or a session to start it, // (c) historical mention (changelog, measurement records, recorded fixtures), // (operator-instrument) a documented tool a PERSON runs, kept by operator // decision 2026-10-02: the fixed test set run through Anthropic's plugin eval // tool; Voyage starts nothing. // (a) and (b) must be zero. A (c) or operator-instrument file stands only in // EXEMPT, with its reason and its EXACT hit count: a new hit in an exempt file // fails, and so does an entry that no longer hits. // // When this test fails, remove the launch (work that needs another session // becomes an order plus a starter the user runs in a session they open); do // NOT widen EXEMPT to hide it. import { test } from 'node:test'; import { strict as assert } from 'node:assert'; import { execFileSync } from 'node:child_process'; import { mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; const HERE = dirname(fileURLToPath(import.meta.url)); const ROOT = join(HERE, '..', '..'); const SELF = 'tests/lib/no-claude-code-launch.test.mjs'; const PATTERNS = [ ['cli', /(^|[^A-Za-z0-9_./@-])claude[ \t]+(-{1,2}[A-Za-z]|["'$])/], ['plugin-eval', /(^|[^A-Za-z0-9_./@-])claude[ \t]+plugin[ \t]+eval\b/], ['spawn', /\b(spawn|spawnSync|exec|execSync|execFile|execFileSync|fork)\(\s*['"`]claude\b/], ['sdk', /claude-agent-sdk|claude_agent_sdk|@anthropic-ai\/claude-code|claude-code-sdk|claude_code_sdk|claude-code-action/], ['bypass', /bypassPermissions|--dangerously-skip-permissions/], ]; // path -> [class, exact hit count, reason] const EXEMPT = new Map([ [SELF, ['self', 17, "the gate's own pattern table and known-positive fixture"]], ['CHANGELOG.md', ['c', 15, 'release history; never rewritten']], ['docs/T1-cc26-delegated-orchestration.md', ['c', 4, 'measurement record (T1/CC-26)']], ['docs/T1-synthesis-poc-results.md', ['c', 1, 'measurement record (synthesis PoC)']], ['docs/cc-upgrade-2.1.181-decision-matrix.md', ['c', 3, 'decision record (CC 2.1.181 upgrade)']], ['docs/deep-research-engine-research.md', ['c', 6, 'research record (deep-research engine)']], ['docs/operations.md', ['c', 1, 'Path A decision record: why cache-first was rejected']], ['docs/spike-pretooluse-subagent-reach.md', ['c', 2, 'spike record (PreToolUse reach)']], ['docs/voyage-vs-cc-balance-analysis.md', ['c', 1, 'decision record (balance analysis)']], ['scripts/synthesis-measure.mjs', ['c', 2, 'measurement script; says why a child-session instrument was not used, launches nothing']], ['tests/fixtures/research-deep-research-adapted.md', ['c', 1, 'recorded research fixture']], ['tests/fixtures/synthesis/digest.json', ['c', 1, 'recorded synthesis fixture']], ['tests/fixtures/synthesis/exploration/architecture-mapper.md', ['c', 3, 'recorded exploration fixture']], ['tests/fixtures/synthesis/exploration/dependency-tracer.md', ['c', 2, 'recorded exploration fixture']], ['docs/proevesett.md', ['operator-instrument', 3, "the operator's own eval run through Anthropic's `claude plugin eval`; Voyage starts nothing - kept by operator decision 2026-10-02"]], ['.gitignore', ['operator-instrument', 1, 'ignores the output of that same operator-run eval']], ['hooks/scripts/post-approval-log.mjs', ['mode-read', 3, "reads the session's permission_mode from hook input so an approval is never logged when no person was asked; starts nothing - operator decision 2026-10-06"]], ['tests/hooks/guards-review-regressions.test.mjs', ['mode-read', 1, 'feeds that same permission_mode value to the hook under test; starts nothing']], ]); function trackedFiles(root) { const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' }); return out.split('\0').filter(Boolean); } // One "path:line:pattern" entry per hit line (first matching pattern wins). function scan(root, files) { const hits = []; for (const rel of files) { const buf = readFileSync(join(root, rel)); if (buf.includes(0)) continue; // binary buf.toString('utf-8').split('\n').forEach((line, i) => { const p = PATTERNS.find(([, re]) => re.test(line)); if (p) hits.push(`${rel}:${i + 1}:${p[0]}`); }); } return hits; } test('scan finds every launch form and passes the look-alikes (known-positive + known-negative)', () => { const dir = mkdtempSync(join(tmpdir(), 'cc-launch-')); try { const positives = [ 'claude -p "/trekexecute --session 1 plan.md"', ' GIT_OPTIONAL_LOCKS=0 claude --print "x"', 'run `claude --bg` in a pane', 'claude "$(cat session-1.md)"', 'claude --resume', 'claude plugin eval . --runs 1', "const child = spawn('claude', argv);", 'execFileSync("claude", ["--version"])', "import { query } from '@anthropic-ai/claude-agent-sdk';", 'npx @anthropic-ai/claude-code', ' --permission-mode bypassPermissions \\', 'claude --dangerously-skip-permissions', 'uses: anthropics/claude-code-action@v1', ]; const negatives = [ 'Claude Code runs the command inline.', 'model: claude-opus-5-5', 'paths under .claude/projects/ and ${CLAUDE_PLUGIN_ROOT}', 'claude plugin install voyage@ktg-plugin-marketplace', 'claude plugin validate', 'Open a Claude Code session in the worktree and run `/trekexecute --session 1 plan.md`.', 'const bin = "claude";', 'the claude binary is not on PATH', ]; writeFileSync(join(dir, 'pos.md'), positives.join('\n') + '\n'); writeFileSync(join(dir, 'neg.md'), negatives.join('\n') + '\n'); const pos = scan(dir, ['pos.md']); assert.equal(pos.length, positives.length, `every positive must hit:\n${pos.join('\n')}`); assert.deepEqual(scan(dir, ['neg.md']), []); assert.deepEqual(new Set(pos.map(h => h.split(':')[2])), new Set(PATTERNS.map(([k]) => k)), 'the known-positive fixture must exercise every pattern'); } finally { rmSync(dir, { recursive: true, force: true }); } }); test('no tracked file outside EXEMPT starts Claude Code or tells anyone to', () => { const files = trackedFiles(ROOT); assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`); const hits = scan(ROOT, files.filter(f => !EXEMPT.has(f))); assert.deepEqual(hits, [], `${hits.length} launch hit(s) in (a)/(b):\n${hits.join('\n')}`); }); test('every EXEMPT entry is tracked, has a reason, and hits exactly its recorded count', () => { const files = new Set(trackedFiles(ROOT)); for (const [rel, [cls, count, reason]] of EXEMPT) { assert.ok(files.has(rel), `EXEMPT names untracked ${rel}`); assert.ok(['c', 'self', 'operator-instrument', 'mode-read'].includes(cls), `${rel}: unknown class ${cls}`); assert.ok(reason && reason.length > 10, `${rel}: an exemption needs a reason`); const n = scan(ROOT, [rel]).length; assert.equal(n, count, `${rel}: ${n} hit(s), EXEMPT records ${count} - a new hit is a launch, a lost one is a stale entry`); } });