diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f9c6b3..7a36a7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -141,6 +141,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). #### MCP / indexing +- The prompt hook no longer injects unrelated projects when run from your home directory or a broader directory containing a stray workspace manifest. (#1454) + - Indexing now succeeds when Node.js's SQLite lacks FTS5, with search falling back to name and fuzzy matching; thanks @aniruddhaadak80. (#1532) - `codegraph_explore` now makes clear that suggested call counts are advisory, so agents keep exploring when an answer is incomplete; thanks @rongbc. (#1504, #1570) diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts index fc57bc2..95cd83a 100644 --- a/__tests__/frontload-hook.test.ts +++ b/__tests__/frontload-hook.test.ts @@ -8,11 +8,15 @@ * logic), since the end-to-end hook is validated by a live agent run, not a * unit test. */ -import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { planFrontload, findIndexedSubprojectRoots, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; +import { planFrontload, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; + +// Make the built-in exports configurable so HOME can point at a real temp +// fixture without changing the process environment or the user's home files. +vi.mock('os', async (importOriginal) => ({ ...await importOriginal() })); /** Make `dir` look indexed (isInitialized needs `.codegraph/codegraph.db`). */ function mkIndexed(dir: string): string { @@ -30,7 +34,10 @@ function mkWorkspaceRoot(dir: string): string { describe('planFrontload — front-load hook project resolution (#964)', () => { let tmp: string; beforeEach(() => { tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'cg-frontload-'))); }); - afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }); }); + afterEach(() => { + vi.restoreAllMocks(); + fs.rmSync(tmp, { recursive: true, force: true }); + }); it('cwd is itself indexed → front-load cwd (the common single-project case)', () => { mkIndexed(tmp); @@ -92,6 +99,29 @@ describe('planFrontload — front-load hook project resolution (#964)', () => { expect(plan.nudgeProjects).toEqual([]); }); + it.each([ + { root: 'home', manifest: 'package.json', children: 1 }, + { root: 'home', manifest: 'package.json', children: 2 }, + { root: 'home', manifest: 'WORKSPACE', children: 1 }, + { root: 'parent of home', manifest: 'package.json', children: 1 }, + ])('$root with stray $manifest and $children indexed children → no-op (#1454)', ({ root, manifest, children }) => { + const homeDir = root === 'home' ? tmp : path.join(tmp, 'user'); + fs.mkdirSync(homeDir, { recursive: true }); + vi.spyOn(os, 'homedir').mockReturnValue(homeDir); + if (manifest === 'package.json') mkWorkspaceRoot(tmp); + else fs.mkdirSync(path.join(tmp, manifest)); // Even a WORKSPACE directory opens the manifest gate. + mkIndexed(path.join(tmp, 'packages', 'api')); + if (children === 2) mkIndexed(path.join(tmp, 'packages', 'web')); + expect(unsafeIndexRootReason(tmp)).toBe(root === 'home' ? 'your home directory' : 'a parent of your home directory'); + + expect(planFrontload(tmp, 'how does authentication work end to end?')).toEqual({ + exploreRoot: null, + nudgeProjects: [], + viaSubScan: false, + }); + expect(findIndexedSubprojectRoots(tmp)).toEqual([]); + }); + it('nothing indexed anywhere → no-op', () => { mkWorkspaceRoot(tmp); fs.mkdirSync(path.join(tmp, 'packages', 'api'), { recursive: true }); diff --git a/src/directory.ts b/src/directory.ts index b419495..3ada4c8 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -213,6 +213,8 @@ export function findIndexedSubprojectRoots( root: string, opts: { maxDepth?: number; max?: number } = {}, ): string[] { + // A stray workspace manifest must not enable scanning home or broader roots (#1454). + if (unsafeIndexRootReason(root) !== null) return []; const maxDepth = opts.maxDepth ?? 4; const max = opts.max ?? 64; const out: string[] = [];