Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions gate-engine/search-tool/__tests__/search-tool-hooks.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ function runGuard(command, env = {}) {
const out = execFileSync('node', [GUARD], {
input: JSON.stringify({ tool_input: { command } }),
env: { ...process.env, ...env },
// No guard.config.json here (fresh tmpdir) so scanRoots falls back to the
// DEFAULT (['src']), matching this file's `src/`-targeted fixtures — and
// isolates these hook-wiring tests from devkit's OWN dogfood scanRoots
// (["cli","gate-engine"]), which would otherwise read every `src/`/`.`
// target here as out-of-scope now that firstAdvisablePattern also scopes
// to scanRoots (PR review finding, sc-1359 follow-up).
cwd: stateDir,
}).toString();
return out ? JSON.parse(out) : null;
}
Expand All @@ -43,6 +50,12 @@ function runCounterRaw(command, toolName = 'Bash') {
return execFileSync('node', [COUNTER], {
input: JSON.stringify({ tool_name: toolName, tool_input: { command }, session_id: sessionId }),
env: { ...process.env, TMPDIR: stateDir },
// No guard.config.json here (fresh tmpdir) so scanRoots falls back to the
// DEFAULT (['src']), matching this file's `src/`-targeted fixtures — and
// isolates these hook-wiring tests from devkit's OWN dogfood scanRoots
// (["cli","gate-engine"]), which would otherwise read every `src/`
// target here as out-of-scope (sc-1359 #3).
cwd: stateDir,
}).toString();
}
const runCounter = (command, toolName = 'Bash') =>
Expand Down Expand Up @@ -85,6 +98,34 @@ describe('search-tool-guard (PreToolUse)', () => {
expect(guardFires(`cd "${CWD}" && rtk grep -rn "permission prompt rendering" src/`)).toBe(true);
});

it('FIRES on a conceptual grep invoked via unspaced $(...) command substitution (guard-review finding, round 6)', () => {
expect(guardFires(`result=$(grep -r "how does auth work" src/)`)).toBe(true);
});

it("advises on the RIGHT invocation's pattern in a compound command, not an excluded one's (guard-review finding, round 8)", () => {
// The node_modules invocation's pattern must never be attributed just because a LATER,
// unrelated invocation elsewhere in the command has a non-excluded target.
const advice = runGuard(
`grep -rn "auth flow logic" node_modules && grep -rn "the retry backoff path" src`,
)?.hookSpecificOutput?.additionalContext;
expect(advice).toContain('the retry backoff path');
expect(advice).not.toContain('auth flow logic');
});

it('stays quiet on a target outside the configured scanRoots (PR review finding)', () => {
// This fixture's isolated cwd has no guard.config.json, so scanRoots falls back to the
// DEFAULT (['src']) — "docs/" is out of scope under that default.
expect(guardFires(`grep -rn "how does the docs pipeline render" docs/`)).toBe(false);
});

it('in a compound command, skips the out-of-scanRoots invocation and advises on the in-scope one (PR review finding)', () => {
const advice = runGuard(
`grep -rn "how does the docs pipeline render" docs/ && grep -rn "the retry backoff path" src`,
)?.hookSpecificOutput?.additionalContext;
expect(advice).toContain('the retry backoff path');
expect(advice).not.toContain('how does the docs pipeline render');
});

it('steers toward the CONFIGURED search tool (not a hardcoded name)', () => {
const advice = runGuard(`grep -rn "auth flow" .`)?.hookSpecificOutput?.additionalContext;
expect(advice).toContain(SEARCH_TOOL);
Expand All @@ -100,6 +141,16 @@ describe('search-tool-guard (PreToolUse)', () => {
});
expect(out?.hookSpecificOutput?.permissionDecision).toBe('ask');
});

it('stays quiet on a node_modules target regardless of pattern shape (sc-1359 #3)', () => {
expect(guardFires(`grep -rn "where is permission handled" node_modules/foo/lib.js`)).toBe(
false,
);
});

it('stays quiet on a /tmp target regardless of pattern shape (sc-1359 #3)', () => {
expect(guardFires(`grep -oE "FAIL +[^ ]+\\.test\\.tsx?" /tmp/vitest-out.log`)).toBe(false);
});
});

describe('search-tool-counter (PostToolUse) — streak state machine', () => {
Expand Down Expand Up @@ -143,6 +194,17 @@ describe('search-tool-counter (PostToolUse) — streak state machine', () => {
expect(runCounter(`bun x 2>&1 | grep error`)).toBe(false);
});

it('an out-of-index target is a NO-OP on the streak — neither increments nor resets (sc-1359 #3)', () => {
expect(runCounter(`grep -rn "x" src/`)).toBe(false); // streak 1
expect(runCounter(`grep -rn "y" node_modules/foo`)).toBe(false); // no-op, streak stays 1
expect(runCounter(`grep -rn "z" src/`)).toBe(false); // streak 2 (not reset by the no-op)
// 3rd IN-SCOPE grep still escalates — the node_modules call above didn't count toward it.
const msg = JSON.parse(runCounterRaw(`grep -rn "w" src/`)).hookSpecificOutput.additionalContext;
expect(msg).toContain('3 consecutive');
// The excluded call must not appear in the recent-commands list either.
expect(msg).not.toContain('node_modules');
});

it('degrades gracefully on a corrupt state file (concurrency safety: no throw, treated as 0)', () => {
mkdirSync(join(stateDir, 'devkit-search-state'), { recursive: true });
writeFileSync(stateFile(), '{ this is not valid json');
Expand Down
Loading
Loading