@@ -291,52 +291,6 @@ def test_bash_positional_non_code_passes(fake_home):
291291 assert rc == 0
292292
293293
294- # ---------- ollama-filter filter_bash bypass ----------
295-
296-
297- def _ollama (sub_tool : str , ** args ) -> dict :
298- return {
299- "hook_event_name" : "PreToolUse" ,
300- "tool_name" : "mcp__ollama-filter__ollama_filter_call" ,
301- "tool_input" : {"tool" : sub_tool , "args" : args },
302- }
303-
304-
305- @pytest .mark .parametrize ("cmd,lang" , [
306- ('grep -rn foo ~/x --include="*.scala"' , "scala" ),
307- ('rg --type python bar ~/x' , "python" ),
308- ('find ~/x -name "*.ts"' , "typescript" ),
309- ('grep -n foo /tmp/Foo.cs' , "csharp" ),
310- ])
311- def test_ollama_filter_bash_bypass_blocked (fake_home , cmd , lang ):
312- _write_availability (fake_home , {"lsps" : {
313- "scala" : {"tool" :"metals-direct" ,"binary" :"/x" ,"backend" :"metals-mcp" ,"workspace" :"/w" },
314- "python" : {"tool" :"claude-lsp" ,"plugin_installed" :True ,"binary_on_path" :True ,"binary_name" :"pyright-langserver" ,"workspace" :"/w" },
315- "typescript" : {"tool" :"claude-lsp" ,"plugin_installed" :True ,"binary_on_path" :True ,"binary_name" :"typescript-language-server" ,"workspace" :"/w" },
316- "csharp" : {"tool" :"claude-lsp" ,"plugin_installed" :True ,"binary_on_path" :True ,"binary_name" :"csharp-ls" ,"workspace" :"/w" },
317- }})
318- rc , _ , err = _run (_ollama ("filter_bash" , command = cmd ), fake_home )
319- assert rc == 2
320- assert lang in err
321- assert "ollama_filter_call" in err
322-
323-
324- @pytest .mark .parametrize ("sub_tool,args" , [
325- ("search_memory" , {"query" : "foo" }),
326- ("search_repo" , {"query" : "foo" , "repo_path" : "/x" }),
327- ("filter_read" , {"path" : "/tmp/Foo.scala" }),
328- ("filter_webfetch" , {"url" : "https://x/y" }),
329- ("filter_bash" , {"command" : "docker ps" }),
330- ("filter_bash" , {"command" : "git log --oneline" }),
331- ])
332- def test_ollama_filter_non_grep_passes (fake_home , sub_tool , args ):
333- _write_availability (fake_home , {"lsps" : {
334- "scala" : {"tool" :"metals-direct" ,"binary" :"/x" ,"backend" :"metals-mcp" ,"workspace" :"/w" },
335- }})
336- rc , _ , _ = _run (_ollama (sub_tool , ** args ), fake_home )
337- assert rc == 0
338-
339-
340294# ---------- non-bash non-grep ----------
341295
342296
@@ -363,5 +317,112 @@ def test_empty_command_passes(fake_home):
363317 assert rc == 0
364318
365319
320+ # ---------- _log_block telemetry ----------
321+
322+
323+ @pytest .fixture
324+ def hook_module ():
325+ import importlib .util
326+ spec = importlib .util .spec_from_file_location ("enforce_lsp_over_grep" , HOOK_PATH )
327+ mod = importlib .util .module_from_spec (spec )
328+ assert spec .loader is not None
329+ spec .loader .exec_module (mod )
330+ return mod
331+
332+
333+ def test_log_block_writes_jsonl_entry (hook_module , tmp_path , monkeypatch ):
334+ log_path = tmp_path / "log.jsonl"
335+ monkeypatch .setattr (hook_module , "METRICS_LOG" , log_path )
336+ hook_module ._log_block ({"session_id" : "abc" }, "Bash" , "grep x /a/b.ts" , "positional" )
337+ lines = log_path .read_text ().splitlines ()
338+ assert len (lines ) == 1
339+ entry = json .loads (lines [0 ])
340+ assert "ts" in entry
341+ assert entry ["session_id" ] == "abc"
342+ assert entry ["tool_name" ] == "Bash"
343+ assert "grep x" in entry ["pattern_excerpt" ]
344+ assert entry ["reason" ] == "positional"
345+
346+
347+ def test_log_block_redacts_secrets (hook_module , tmp_path , monkeypatch ):
348+ log_path = tmp_path / "log.jsonl"
349+ monkeypatch .setattr (hook_module , "METRICS_LOG" , log_path )
350+ hook_module ._log_block ({"session_id" : "s" }, "Bash" , "api_key=AKIA1234" , "x" )
351+ entry = json .loads (log_path .read_text ().splitlines ()[0 ])
352+ assert entry ["pattern_excerpt" ] == "[REDACTED]"
353+ # 45-char alphanumeric string
354+ log_path .write_text ("" )
355+ hook_module ._log_block ({"session_id" : "s" }, "Bash" , "A" * 45 , "x" )
356+ entry = json .loads (log_path .read_text ().splitlines ()[0 ])
357+ assert entry ["pattern_excerpt" ] == "[REDACTED]"
358+
359+
360+ def test_log_block_silent_pass_on_io_error (hook_module , monkeypatch ):
361+ monkeypatch .setattr (hook_module , "METRICS_LOG" , Path ("/nonexistent-root-dir/log.jsonl" ))
362+ # MUST NOT raise
363+ result = hook_module ._log_block ({"session_id" : "s" }, "Bash" , "grep x /a/b.ts" , "x" )
364+ assert result is None
365+
366+
367+ def test_log_block_rotates_when_oversized (hook_module , tmp_path , monkeypatch ):
368+ log_path = tmp_path / "lsp-grep-blocks.log"
369+ backup_path = tmp_path / "lsp-grep-blocks.log.1"
370+ # pre-fill with 260 KB of dummy content
371+ dummy = "x" * (260 * 1024 )
372+ log_path .write_text (dummy )
373+ monkeypatch .setattr (hook_module , "METRICS_LOG" , log_path )
374+ hook_module ._log_block ({"session_id" : "s" }, "Bash" , "grep x /a/b.ts" , "rot" )
375+ # .log now small + only has new entry
376+ new_contents = log_path .read_text ()
377+ assert len (new_contents ) < 1024 , f"expected small fresh log, got { len (new_contents )} bytes"
378+ assert '"reason": "rot"' in new_contents
379+ # .log.1 exists, holds the previous oversized content
380+ assert backup_path .exists ()
381+ assert backup_path .read_text () == dummy
382+
383+
384+ def test_log_block_overwrites_old_rotation (hook_module , tmp_path , monkeypatch ):
385+ log_path = tmp_path / "lsp-grep-blocks.log"
386+ backup_path = tmp_path / "lsp-grep-blocks.log.1"
387+ fresh = "y" * (260 * 1024 )
388+ stale = "STALE-OLD-BACKUP"
389+ log_path .write_text (fresh )
390+ backup_path .write_text (stale )
391+ monkeypatch .setattr (hook_module , "METRICS_LOG" , log_path )
392+ hook_module ._log_block ({"session_id" : "s" }, "Bash" , "grep x /a/b.ts" , "rot" )
393+ # .log.1 now holds fresh (what .log had pre-rotation); stale backup overwritten
394+ assert backup_path .read_text () == fresh
395+ assert "STALE" not in backup_path .read_text ()
396+
397+
398+ # ---------- backslash/single-quote escape in positional regex ----------
399+
400+
401+ def test_strip_quoted_handles_backslash_escape_inside_double_quote (hook_module ):
402+ cmd = r'''grep "it\"s a .ts" /a/b.md'''
403+ langs = hook_module .detect_langs (cmd )
404+ assert langs == set (), f"expected no lang detection, got { langs } "
405+
406+
407+ def test_strip_quoted_handles_single_quote_literal (hook_module ):
408+ cmd = '''grep 'a"b.ts' /a/b.md'''
409+ langs = hook_module .detect_langs (cmd )
410+ assert langs == set (), f"expected no lang detection, got { langs } "
411+
412+
413+ def test_strip_quoted_handles_ansi_c_dollar_single (hook_module ):
414+ # bash ANSI-C quoting: $'...' — `.ts` inside pattern, target is .md
415+ cmd = r"""grep $'foo.ts\n' /a/b.md"""
416+ langs = hook_module .detect_langs (cmd )
417+ assert langs == set (), f"expected no lang detection, got { langs } "
418+
419+
420+ def test_strip_quoted_handles_locale_dollar_double (hook_module ):
421+ # locale-translation quoting: $"..." — `.ts` inside pattern, target is .md
422+ cmd = '''grep $"x.ts" /a/b.md'''
423+ langs = hook_module .detect_langs (cmd )
424+ assert langs == set (), f"expected no lang detection, got { langs } "
425+
426+
366427if __name__ == "__main__" :
367428 sys .exit (pytest .main ([__file__ , "-v" ]))
0 commit comments