@@ -904,6 +904,67 @@ async def test_active_repair_scope_hook_blocks_local_rereads_outside_concrete_re |
| 904 | assert str(stylesheet) in result.message | 904 | assert str(stylesheet) in result.message |
| 905 | | 905 | |
| 906 | | 906 | |
| | 907 | +@pytest.mark.asyncio |
| | 908 | +async def test_active_repair_scope_hook_blocks_broad_glob_during_concrete_repair( |
| | 909 | + temp_dir: Path, |
| | 910 | +) -> None: |
| | 911 | + registry = create_default_registry(temp_dir) |
| | 912 | + policy = build_permission_policy( |
| | 913 | + active_mode=PermissionMode.WORKSPACE_WRITE, |
| | 914 | + workspace_root=temp_dir, |
| | 915 | + tool_requirements=registry.get_tool_requirements(), |
| | 916 | + ) |
| | 917 | + dod_store = DefinitionOfDoneStore(temp_dir) |
| | 918 | + dod = create_definition_of_done("Repair the generated guide") |
| | 919 | + dod.status = "fixing" |
| | 920 | + dod_path = dod_store.save(dod) |
| | 921 | + guide_root = temp_dir / "guide" |
| | 922 | + chapters = guide_root / "chapters" |
| | 923 | + chapters.mkdir(parents=True) |
| | 924 | + repair_target = guide_root / "index.html" |
| | 925 | + repair_target.write_text("<h1>Guide</h1>\n") |
| | 926 | + (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n") |
| | 927 | + session = FakeSession( |
| | 928 | + active_dod_path=str(dod_path), |
| | 929 | + messages=[ |
| | 930 | + Message( |
| | 931 | + role=Role.ASSISTANT, |
| | 932 | + content=( |
| | 933 | + "Repair focus:\n" |
| | 934 | + f"- Improve `{repair_target}`: insufficient structured content.\n" |
| | 935 | + f"- Immediate next step: edit `{repair_target}`.\n" |
| | 936 | + "- Do not reread unrelated reference materials or restart discovery while this concrete repair target is unresolved.\n" |
| | 937 | + ), |
| | 938 | + ) |
| | 939 | + ], |
| | 940 | + ) |
| | 941 | + hook = ActiveRepairScopeHook( |
| | 942 | + dod_store=dod_store, |
| | 943 | + project_root=temp_dir, |
| | 944 | + session=session, |
| | 945 | + ) |
| | 946 | + |
| | 947 | + result = await hook.pre_tool_use( |
| | 948 | + HookContext( |
| | 949 | + tool_call=ToolCall( |
| | 950 | + id="glob-1", |
| | 951 | + name="glob", |
| | 952 | + arguments={"path": str(guide_root), "pattern": "**/*.html"}, |
| | 953 | + ), |
| | 954 | + tool=registry.get("glob"), |
| | 955 | + registry=registry, |
| | 956 | + permission_policy=policy, |
| | 957 | + source="native", |
| | 958 | + ) |
| | 959 | + ) |
| | 960 | + |
| | 961 | + assert result.decision == HookDecision.DENY |
| | 962 | + assert result.terminal_state == "blocked" |
| | 963 | + assert result.message is not None |
| | 964 | + assert "active repair scope" in result.message |
| | 965 | + assert str(repair_target) in result.message |
| | 966 | + |
| | 967 | + |
| 907 | @pytest.mark.asyncio | 968 | @pytest.mark.asyncio |
| 908 | async def test_active_repair_scope_hook_blocks_repair_audit_loop_after_repeated_source_reads( | 969 | async def test_active_repair_scope_hook_blocks_repair_audit_loop_after_repeated_source_reads( |
| 909 | temp_dir: Path, | 970 | temp_dir: Path, |