import io import json from scripts.hooks import pre_tool_use, stop_validation def edit_payload(root, path, *, session_id="session-1", turn_id="turn-1"): return { "session_id": session_id, "turn_id": turn_id, "cwd": str(root), "tool_name": "apply_patch", "tool_input": { "patch": ( "*** Begin Patch\n" f"*** Update File: {path}\n" "@@\n" "-old\n" "+new\n" "*** End Patch\n" ) }, } def stop_payload(root, *, session_id="session-1", turn_id="turn-1"): return { "session_id": session_id, "turn_id": turn_id, "cwd": str(root), "hook_event_name": "Stop", "stop_hook_active": False, } def run_pre_tool_use(monkeypatch, payload): monkeypatch.setattr( pre_tool_use.sys, "stdin", io.StringIO(json.dumps(payload)), ) assert pre_tool_use.main() == 0 def run_stop(monkeypatch, payload, root, validation_calls): monkeypatch.setattr( stop_validation.sys, "stdin", io.StringIO(json.dumps(payload)), ) monkeypatch.setattr( stop_validation, "_project_root", lambda *args, **kwargs: root, ) def capture_validation(*args, **kwargs): validation_calls.append(root) return True, "" monkeypatch.setattr(stop_validation, "run_validations", capture_validation) assert stop_validation.main() == 0 def test_stop_skips_validation_after_markdown_only_edit(tmp_path, monkeypatch): run_pre_tool_use(monkeypatch, edit_payload(tmp_path, "docs/guide.md")) validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [] def test_stop_validates_after_non_document_edit(tmp_path, monkeypatch): run_pre_tool_use(monkeypatch, edit_payload(tmp_path, "scripts/tool.py")) validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [tmp_path] def test_stop_validates_after_mixed_document_and_code_edits(tmp_path, monkeypatch): run_pre_tool_use(monkeypatch, edit_payload(tmp_path, "scripts/tool.py")) run_pre_tool_use(monkeypatch, edit_payload(tmp_path, "docs/guide.md")) validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [tmp_path] def test_stop_validates_when_turn_scope_is_missing(tmp_path, monkeypatch): validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [tmp_path] def test_document_scope_is_isolated_by_turn(tmp_path, monkeypatch): run_pre_tool_use( monkeypatch, edit_payload(tmp_path, "docs/guide.md", turn_id="document-turn"), ) validation_calls = [] run_stop( monkeypatch, stop_payload(tmp_path, turn_id="different-turn"), tmp_path, validation_calls, ) assert validation_calls == [tmp_path] def test_stop_validates_when_turn_scope_is_corrupt(tmp_path, monkeypatch): run_pre_tool_use(monkeypatch, edit_payload(tmp_path, "docs/guide.md")) state_files = list((tmp_path / ".harness" / "hook-state").glob("*.json")) assert len(state_files) == 1 state_files[0].write_text("not json", encoding="utf-8") validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [tmp_path] def test_later_document_edit_does_not_repair_malformed_scope( tmp_path, monkeypatch ): payload = edit_payload(tmp_path, "docs/guide.md") run_pre_tool_use(monkeypatch, payload) state_files = list((tmp_path / ".harness" / "hook-state").glob("*.json")) assert len(state_files) == 1 state_files[0].write_text('{"version": 1}', encoding="utf-8") run_pre_tool_use(monkeypatch, payload) validation_calls = [] run_stop(monkeypatch, stop_payload(tmp_path), tmp_path, validation_calls) assert validation_calls == [tmp_path]