@@ -904,6 +904,67 @@ async def test_active_repair_scope_hook_blocks_local_rereads_outside_concrete_re |
| 904 | 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 | 968 | @pytest.mark.asyncio |
| 908 | 969 | async def test_active_repair_scope_hook_blocks_repair_audit_loop_after_repeated_source_reads( |
| 909 | 970 | temp_dir: Path, |