From 9faa6b76d2eee35455200de33cc202da990e6584 Mon Sep 17 00:00:00 2001 From: montjeffrey Date: Sat, 10 Oct 2026 23:38:47 -0400 Subject: [PATCH] test(gateguard): pin searches that never saw the target and Unicode line terminators Each of these is credited on main, reproduced against upstream 2d515e4f: - rg -t py / -T js / --type-not js, fd -e py, and the Grep tool's type: "py" credit an Edit of tokenizer.js. Type and extension values are skipped as plain value flags, so they never filter. - Glob **/tokenizer.py, rg tokenizer src/*.py, and git grep tokenizer -- '*.py' credit tokenizer.js. The Glob pattern is never a filter, and a glob operand scopes as its literal directory. - Glob **/tokenizer.js, find -name tokenizer.js, rg -g tokenizer.js and Grep glob: "tokenizer.js" credit tokenizer.js, although each search could only find the target itself. - An Edit of "// notefoo(1);" to "// notefoo(2);" passes as comment-only, although JavaScript ends the comment at U+2028 and foo(2) runs. Scenario 21-bypass-filter-forms adds ten must-deny steps and four controls. Four hook controls that asserted exact-name lookups still credit (find -name widget.py, Grep glob widget.py, Get-ChildItem -Filter widget.py) now assert the lookup is denied; each keeps its original purpose, that the exclusion parser leaves a positive include alone, through a wildcard include (widget.*). --- .../21-bypass-filter-forms.json | 1424 +++++++++++++++++ tests/hooks/gateguard-fact-force.test.js | 26 +- tests/lib/gateguard-code-lexer.test.js | 10 + tests/lib/gateguard-search-evidence.test.js | 74 + tests/lib/gateguard-search-filters.test.js | 40 + 5 files changed, 1569 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/gateguard-scenarios/21-bypass-filter-forms.json diff --git a/tests/fixtures/gateguard-scenarios/21-bypass-filter-forms.json b/tests/fixtures/gateguard-scenarios/21-bypass-filter-forms.json new file mode 100644 index 000000000..d4bdc73e1 --- /dev/null +++ b/tests/fixtures/gateguard-scenarios/21-bypass-filter-forms.json @@ -0,0 +1,1424 @@ +{ + "name": "bypass-filter-forms", + "description": "Searches whose type, extension or path pattern misses the target (rg -t/-T, fd -e, the Grep tool type, a Glob pattern, a shell glob operand, a git pathspec), and name lookups confined to the target itself; then four controls whose filters or patterns cover the target.", + "files": { + "src/lexer/scanner.js": "function scannerValue(x) {\n return x;\n}\n", + "src/lexer/reader.js": "function readerValue(x) {\n return x;\n}\n", + "src/lexer/cursor.js": "function cursorValue(x) {\n return x;\n}\n", + "src/lexer/buffer.js": "function bufferValue(x) {\n return x;\n}\n", + "src/lexer/emitter.js": "function emitterValue(x) {\n return x;\n}\n", + "src/lexer/matcher.js": "function matcherValue(x) {\n return x;\n}\n", + "src/lexer/tracker.js": "function trackerValue(x) {\n return x;\n}\n", + "src/lexer/splitter.js": "function splitterValue(x) {\n return x;\n}\n", + "src/lexer/joiner.js": "function joinerValue(x) {\n return x;\n}\n", + "src/lexer/folder.js": "function folderValue(x) {\n return x;\n}\n", + "src/lexer/decoder.js": "function decoderValue(x) {\n return x;\n}\n", + "src/lexer/encoder.js": "function encoderValue(x) {\n return x;\n}\n", + "src/lexer/printer.js": "function printerValue(x) {\n return x;\n}\n", + "src/lexer/lister.js": "function listerValue(x) {\n return x;\n}\n" + }, + "steps": [ + { + "id": "rg-type-excludes-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-001", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-002", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_1", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_001", + "name": "Bash", + "input": { + "command": "rg -n scanner -t py src" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-003", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_001", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-004", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_2", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_002", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/scanner.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/scanner.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_002" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "rg-type-not-names-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-005", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-006", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_3", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_003", + "name": "Bash", + "input": { + "command": "rg -n reader -T js src" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-007", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_003", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-008", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_4", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_004", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/reader.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/reader.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_004" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "fd-extension-misses-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-009", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-010", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_5", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_005", + "name": "Bash", + "input": { + "command": "fd -e py cursor src" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-011", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_005", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-012", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_6", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_006", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/cursor.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/cursor.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_006" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "grep-tool-type-misses-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-013", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-014", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_7", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_007", + "name": "Grep", + "input": { + "pattern": "buffer", + "type": "py" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-015", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_007", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-016", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_8", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_008", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/buffer.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/buffer.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_008" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "glob-pattern-misses-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-017", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-018", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_9", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_009", + "name": "Glob", + "input": { + "pattern": "**/emitter.py" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-019", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_009", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-020", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_10", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_010", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/emitter.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/emitter.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_010" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "shell-glob-operand-misses-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-021", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-022", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_11", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_011", + "name": "Bash", + "input": { + "command": "rg -n matcher src/lexer/*.py" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-023", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_011", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-024", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_12", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_012", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/matcher.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/matcher.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_012" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "git-pathspec-misses-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-025", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-026", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_13", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_013", + "name": "Bash", + "input": { + "command": "git grep -n tracker -- '*.py'" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-027", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_013", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-028", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_14", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_014", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/tracker.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/tracker.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_014" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "glob-lookup-of-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-029", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-030", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_15", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_015", + "name": "Glob", + "input": { + "pattern": "**/splitter.js" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-031", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_015", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-032", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_16", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_016", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/splitter.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/splitter.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_016" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "find-name-lookup-of-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-033", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-034", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_17", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_017", + "name": "Bash", + "input": { + "command": "find src -name joiner.js" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-035", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_017", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-036", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_18", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_018", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/joiner.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/joiner.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_018" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "grep-tool-glob-lookup-of-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-037", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-038", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_19", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_019", + "name": "Grep", + "input": { + "pattern": "folder", + "glob": "folder.js" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-039", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_019", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-040", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_20", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_020", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/folder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/folder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_020" + }, + "expect": "deny", + "mustDeny": true, + "redundant": false, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "rg-type-includes-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-041", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-042", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_21", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_021", + "name": "Bash", + "input": { + "command": "rg -n decoder -t js src" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-043", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_021", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-044", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_22", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_022", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/decoder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/decoder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_022" + }, + "expect": "allow", + "mustDeny": false, + "redundant": true, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "glob-pattern-lists-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-045", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-046", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_23", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_023", + "name": "Glob", + "input": { + "pattern": "src/**/encoder*" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-047", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_023", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-048", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_24", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_024", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/encoder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/encoder.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_024" + }, + "expect": "allow", + "mustDeny": false, + "redundant": true, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "git-pathspec-covers-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-049", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-050", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_25", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_025", + "name": "Bash", + "input": { + "command": "git grep -n printer -- 'src/*.js'" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-051", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_025", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-052", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_26", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_026", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/printer.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/printer.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_026" + }, + "expect": "allow", + "mustDeny": false, + "redundant": true, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + }, + { + "id": "shell-glob-operand-covers-target", + "transcript": [ + { + "type": "user", + "uuid": "bypass-filter-forms-053", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": "Round the lexer values to cents." + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-054", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_27", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_027", + "name": "Bash", + "input": { + "command": "rg -n lister src/*" + } + } + ] + } + }, + { + "type": "user", + "uuid": "bypass-filter-forms-055", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "promptId": "prompt-filter-forms-1", + "message": { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_bypassfilterforms_027", + "content": "ok" + } + ] + } + }, + { + "type": "assistant", + "uuid": "bypass-filter-forms-056", + "sessionId": "sess-bypass-filter-forms", + "isSidechain": false, + "message": { + "id": "msg_bypassfilterforms_28", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [ + { + "type": "tool_use", + "id": "toolu_bypassfilterforms_028", + "name": "Edit", + "input": { + "file_path": "{{root}}/src/lexer/lister.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + } + } + ] + } + } + ], + "payload": { + "session_id": "sess-bypass-filter-forms", + "transcript_path": "{{transcript}}", + "cwd": "{{root}}", + "permission_mode": "default", + "hook_event_name": "PreToolUse", + "tool_name": "Edit", + "tool_input": { + "file_path": "{{root}}/src/lexer/lister.js", + "old_string": " return x;", + "new_string": " return Math.round(x * 100) / 100;" + }, + "tool_use_id": "toolu_bypassfilterforms_028" + }, + "expect": "allow", + "mustDeny": false, + "redundant": true, + "relevantQuestions": [ + "local-callers", + "quote-instruction" + ] + } + ] +} diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 1968cd5df..211f74a2d 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -5669,14 +5669,25 @@ function runTests() { ['Grep widget glob *.{py,js} (braces are one glob)', 'Grep', { pattern: 'widget', glob: '*.{py,js}', path: creditRoot }], ['Grep widget glob !node_modules/**', 'Grep', { pattern: 'widget', glob: '!node_modules/**', path: creditRoot }], ['grep --exclude-dir=node_modules widget', 'Bash', { command: 'grep -r --exclude-dir=node_modules widget .' }], - ['find -name widget.py', 'Bash', { command: 'find . -name widget.py' }], - ['Grep TODO glob widget.py (positive glob names the stem)', 'Grep', { pattern: 'TODO', glob: 'widget.py', path: creditRoot }] + ["find -name 'widget.*'", 'Bash', { command: "find . -name 'widget.*'" }], + ['Grep TODO glob widget.* (positive glob names the stem)', 'Grep', { pattern: 'TODO', glob: 'widget.*', path: creditRoot }] ]; for (const [label, name, input] of exclusionCredited) { gateCase(`control still credits (${label})`, () => { assertCredited(creditEdit(exclusionTarget, exclusionTurn(name, input)), label); }); } + // A search confined to the target's own file name read the target rather than searched for it. + const lookupDenied = [ + ['find -name widget.py', 'Bash', { command: 'find . -name widget.py' }], + ['Grep TODO glob widget.py', 'Grep', { pattern: 'TODO', glob: 'widget.py', path: creditRoot }] + ]; + for (const [label, name, input] of lookupDenied) { + gateCase(`a lookup of the target by its own name never credits it (${label})`, () => { + assertNotCredited(creditEdit(exclusionTarget, exclusionTurn(name, input)), label); + assert.ok(!readState().fact_force_credited, `${label}: nothing credited`); + }); + } // --- Bracket classes in filter globs match exactly --- const bracketCases = [ @@ -5780,10 +5791,10 @@ function runTests() { }); } const psCredited = [ - 'Get-ChildItem -Path src -Recurse -Filter widget.py', - 'Get-ChildItem -Recurse -Filt:widget.py', + 'Get-ChildItem -Path src -Recurse -Filter widget.*', + 'Get-ChildItem -Recurse -Filt:widget.*', 'Get-ChildItem -Recurse -Include a.md, widget.py', - 'Get-ChildItem -Recurse -Exclude a.md, b.md -Filter widget.py', + 'Get-ChildItem -Recurse -Exclude a.md, b.md -Filter widget.*', 'Select-String -Pattern widget -Path src/* -Exclude a.md' ]; for (const command of psCredited) { @@ -5791,6 +5802,11 @@ function runTests() { assertCredited(creditEdit(exclusionTarget, exclusionTurn('PowerShell', { command })), command); }); } + for (const command of ['Get-ChildItem -Path src -Recurse -Filter widget.py', 'Get-ChildItem -Recurse -Filt:widget.py']) { + gateCase(`a PowerShell lookup of the target by its own name never credits it (${command})`, () => { + assertNotCredited(creditEdit(exclusionTarget, exclusionTurn('PowerShell', { command })), command); + }); + } gateCase('PowerShell include lists spread across tokens still filter the target', () => { const command = 'Get-ChildItem -Recurse -Include a.md, widget.md'; assertNotCredited(creditEdit(exclusionTarget, exclusionTurn('PowerShell', { command })), command); diff --git a/tests/lib/gateguard-code-lexer.test.js b/tests/lib/gateguard-code-lexer.test.js index 8b9e9de6b..65b1fa34b 100644 --- a/tests/lib/gateguard-code-lexer.test.js +++ b/tests/lib/gateguard-code-lexer.test.js @@ -35,6 +35,16 @@ test('a comment-only change to C-family code is trivial', () => { assert.strictEqual(isTrivialEdit('a(); // x', 'a(); // y', JS), true); }); +test('a Unicode line terminator makes an edit non-trivial', () => { + // JavaScript ends a line comment at U+2028 and U+2029, and C# also at U+0085. + for (const sep of ['\u2028', '\u2029', '\u0085']) { + const code = sep.codePointAt(0).toString(16); + assert.strictEqual(isTrivialEdit(`// note${sep}foo(1);`, `// note${sep}foo(2);`, JS), false, `U+${code} ends the comment`); + assert.strictEqual(isTrivialEdit(`a(); // x${sep}y`, `a(); // z${sep}y`, JS), false, `U+${code} anywhere`); + assert.strictEqual(isTrivialEdit(`x = 1 # a${sep}b`, `x = 1 # c${sep}b`, PY), false, `U+${code} in any language`); + } +}); + test('comment markers inside a string are code', () => { assert.strictEqual(isTrivialEdit('const s = "a // b";', 'const s = "a // c";', JS), false); assert.strictEqual(isTrivialEdit("const s = 'x /* y */ z';", "const s = 'x /* q */ z';", JS), false); diff --git a/tests/lib/gateguard-search-evidence.test.js b/tests/lib/gateguard-search-evidence.test.js index bf20ed82c..1f3e87f7d 100644 --- a/tests/lib/gateguard-search-evidence.test.js +++ b/tests/lib/gateguard-search-evidence.test.js @@ -157,6 +157,80 @@ test('stripped test stems keep the generic, length, word-boundary and scope rule assert.strictEqual(miss && miss.reason, 'generic-stem'); }); +const filterScan = searches => ({ turnId: null, searches, batchIds: new Map(), newestMessageId: null, shellCommands: [], reads: [] }); +const filterSearch = (name, input) => ({ name, input, callsAgo: 1, messageId: 'msg_1' }); +const label = ([name, input]) => `${name} ${JSON.stringify(input)}`; + +test('a type, extension or path pattern that misses the target stops its stem from crediting', () => { + const { findCreditingSearch, findClosestMiss } = loadHook(); + const data = { cwd: root }; + const target = `${root}/src/parser/tokenizer.js`; + const missing = [ + ['Bash', { command: 'rg -t py tokenizer src' }], + ['Bash', { command: 'rg -T js tokenizer src' }], + ['Bash', { command: 'rg --type-not js tokenizer src' }], + ['Bash', { command: 'fd -e py tokenizer src' }], + ['Grep', { pattern: 'tokenizer', type: 'py' }], + ['Glob', { pattern: '**/tokenizer.py' }], + ['Bash', { command: 'rg tokenizer src/*.py' }], + ['Bash', { command: 'grep -rn tokenizer src/**/*.py' }], + ['Bash', { command: "git grep tokenizer -- '*.py'" }], + ['Bash', { command: "git ls-files '*tokenizer.py'" }] + ]; + for (const search of missing) { + const scan = filterScan([filterSearch(...search)]); + assert.strictEqual(findCreditingSearch(scan, target, false, data), null, label(search)); + assert.strictEqual((findClosestMiss(scan, target, false, data) || {}).reason, 'excluded', label(search)); + } + const covering = [ + ['Bash', { command: 'rg -t js tokenizer src' }], + ['Bash', { command: 'fd -e js tokenizer src' }], + ['Grep', { pattern: 'tokenizer', type: 'js' }], + ['Glob', { pattern: '**/tokenizer*' }], + ['Glob', { pattern: 'src/**/*tokenizer*' }], + ['Bash', { command: 'rg tokenizer src/*' }], + ['Bash', { command: 'rg tokenizer lib/*.py src' }], + ['Bash', { command: "git grep tokenizer -- '*.js'" }], + ['Bash', { command: "git grep tokenizer -- 'src/*.js'" }] + ]; + for (const search of covering) { + assert.ok(findCreditingSearch(filterScan([filterSearch(...search)]), target, false, data), label(search)); + } +}); + +test('a search confined to the target file name is a lookup, not a search', () => { + const { findCreditingSearch, findClosestMiss } = loadHook(); + const data = { cwd: root }; + const target = `${root}/src/parser/tokenizer.js`; + const lookups = [ + ['Glob', { pattern: '**/tokenizer.js' }], + ['Glob', { pattern: 'src/**/tokenizer.js' }], + ['Grep', { pattern: 'tokenizer', glob: 'tokenizer.js' }], + ['Grep', { pattern: 'tokenizer', glob: '**/tokenizer.js' }], + ['Bash', { command: 'find src -name tokenizer.js' }], + ['Bash', { command: 'rg -g tokenizer.js tokenizer src' }], + ['Bash', { command: 'rg --files -g tokenizer.js src' }], + ['Bash', { command: 'rg tokenizer src/**/tokenizer.js' }] + ]; + for (const search of lookups) { + const scan = filterScan([filterSearch(...search)]); + assert.strictEqual(findCreditingSearch(scan, target, false, data), null, label(search)); + assert.strictEqual((findClosestMiss(scan, target, false, data) || {}).reason, 'lookup', label(search)); + } + const searches = [ + ['Glob', { pattern: '**/tokenizer.*' }], + ['Bash', { command: "find src -name 'tokenizer.*'" }], + ['Bash', { command: "rg -g tokenizer.js -g '*.ts' tokenizer src" }] + ]; + for (const search of searches) { + assert.ok(findCreditingSearch(filterScan([filterSearch(...search)]), target, false, data), label(search)); + } + const testTarget = `${root}/tests/parser/tokenizer.test.js`; + const lookupOfModule = filterScan([filterSearch('Glob', { pattern: '**/tokenizer.js' })]); + assert.strictEqual(findCreditingSearch(lookupOfModule, testTarget, false, data), null, 'a Glob of the module never listed its test'); + assert.strictEqual((findClosestMiss(lookupOfModule, testTarget, false, data) || {}).reason, 'excluded'); +}); + fs.rmSync(transcriptDir, { recursive: true, force: true }); console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); diff --git a/tests/lib/gateguard-search-filters.test.js b/tests/lib/gateguard-search-filters.test.js index 1222e6c22..14709817c 100644 --- a/tests/lib/gateguard-search-filters.test.js +++ b/tests/lib/gateguard-search-filters.test.js @@ -86,5 +86,45 @@ test('PowerShell -Exclude binds from an unambiguous prefix', () => { assert.deepStrictEqual(filter.globs, ['a.py', 'widget.py']); }); +const admitted = (item, rel) => filtersAdmitTarget(item, ctxFor(rel), null) && admits(item, rel); + +test('rg -t/--type selects ripgrep type globs instead of being skipped', () => { + for (const args of [['-t', 'py'], ['-tpy'], ['--type', 'py'], ['--type=py'], ['-it', 'py']]) { + const item = rgItem([...args, 'tokenizer', 'src']); + assert.strictEqual(admitted(item, 'src/tokenizer.js'), false, args.join(' ')); + assert.strictEqual(admitted(item, 'src/tokenizer.pyi'), true, args.join(' ')); + } + assert.strictEqual(admitted(rgItem(['-t', 'js', 'x']), 'src/tokenizer.mjs'), true); + assert.strictEqual(admitted(rgItem(['-t', 'py', '-t', 'js', 'x']), 'src/tokenizer.js'), true, 'types are alternatives'); +}); + +test('rg -T/--type-not excludes the type globs', () => { + for (const args of [['-T', 'js'], ['-Tjs'], ['--type-not', 'js'], ['--type-not=js']]) { + assert.strictEqual(admitted(rgItem([...args, 'x']), 'src/tokenizer.js'), false, args.join(' ')); + assert.strictEqual(admitted(rgItem([...args, 'x']), 'src/tokenizer.py'), true, args.join(' ')); + } +}); + +test('unknown or redefined rg types are unreadable filters', () => { + for (const args of [['-t', 'nosuchtype'], ['-T', 'nosuchtype'], ['--type-add', 'web:*.js', '-t', 'web'], ['--type-clear', 'js']]) { + assert.strictEqual(admitted(rgItem([...args, 'x']), 'src/tokenizer.js'), false, args.join(' ')); + } +}); + +test('fd -e/--extension is an include', () => { + const fdItem = args => ({ ...shellSearchFilters('fd', args), scopes: [['.']], pathIncludes: false }); + for (const args of [['-e', 'py'], ['-epy'], ['--extension', 'py'], ['--extension=.py']]) { + assert.strictEqual(admitted(fdItem([...args, 'tokenizer', 'src']), 'src/tokenizer.js'), false, args.join(' ')); + assert.strictEqual(admitted(fdItem([...args, 'tokenizer', 'src']), 'src/tokenizer.py'), true, args.join(' ')); + } +}); + +test('the Grep tool type parameter is an include, and an unknown type is unreadable', () => { + const grepItem = (glob, type) => ({ ...grepToolFilters(glob, type), scopes: [['.']], pathIncludes: true }); + assert.strictEqual(admitted(grepItem(undefined, 'py'), 'src/tokenizer.js'), false); + assert.strictEqual(admitted(grepItem(undefined, 'py'), 'src/tokenizer.py'), true); + assert.strictEqual(admitted(grepItem(undefined, 'nosuchtype'), 'src/tokenizer.js'), false); +}); + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);