@@ -1606,7 +1606,7 @@ async def test_late_reference_drift_hook_blocks_reference_reads_after_artifacts_ |
| 1606 | 1606 | |
| 1607 | 1607 | |
| 1608 | 1608 | @pytest.mark.asyncio |
| 1609 | | -async def test_late_reference_drift_hook_allows_verification_reference_reads_after_artifacts_exist( |
| 1609 | +async def test_late_reference_drift_hook_blocks_verification_reference_reads_after_artifacts_exist( |
| 1610 | 1610 | temp_dir: Path, |
| 1611 | 1611 | ) -> None: |
| 1612 | 1612 | registry = create_default_registry(temp_dir) |
@@ -1662,8 +1662,10 @@ async def test_late_reference_drift_hook_allows_verification_reference_reads_aft |
| 1662 | 1662 | ) |
| 1663 | 1663 | ) |
| 1664 | 1664 | |
| 1665 | | - assert result.decision == HookDecision.CONTINUE |
| 1666 | | - assert result.message is None |
| 1665 | + assert result.decision == HookDecision.DENY |
| 1666 | + assert result.terminal_state == "blocked" |
| 1667 | + assert result.message is not None |
| 1668 | + assert "completed artifact set scope" in result.message |
| 1667 | 1669 | |
| 1668 | 1670 | |
| 1669 | 1671 | @pytest.mark.asyncio |
@@ -1735,6 +1737,75 @@ async def test_late_reference_drift_hook_blocks_excessive_post_build_self_audits |
| 1735 | 1737 | assert "post-build audit loop" in blocked.message |
| 1736 | 1738 | |
| 1737 | 1739 | |
| 1740 | +@pytest.mark.asyncio |
| 1741 | +async def test_late_reference_drift_hook_blocks_excessive_post_build_self_audits_during_verification( |
| 1742 | + temp_dir: Path, |
| 1743 | +) -> None: |
| 1744 | + registry = create_default_registry(temp_dir) |
| 1745 | + policy = build_permission_policy( |
| 1746 | + active_mode=PermissionMode.WORKSPACE_WRITE, |
| 1747 | + workspace_root=temp_dir, |
| 1748 | + tool_requirements=registry.get_tool_requirements(), |
| 1749 | + ) |
| 1750 | + dod_store = DefinitionOfDoneStore(temp_dir) |
| 1751 | + dod = create_definition_of_done("Create a multi-file guide from a reference") |
| 1752 | + dod.status = "in_progress" |
| 1753 | + plan_path = temp_dir / "implementation.md" |
| 1754 | + plan_path.write_text( |
| 1755 | + "\n".join( |
| 1756 | + [ |
| 1757 | + "# Implementation Plan", |
| 1758 | + "", |
| 1759 | + "## File Changes", |
| 1760 | + f"- `{temp_dir / 'guide' / 'index.html'}`", |
| 1761 | + f"- `{temp_dir / 'guide' / 'chapters' / '01-getting-started.html'}`", |
| 1762 | + f"- `{temp_dir / 'guide' / 'chapters' / '02-installation.html'}`", |
| 1763 | + "", |
| 1764 | + ] |
| 1765 | + ) |
| 1766 | + ) |
| 1767 | + dod.implementation_plan = str(plan_path) |
| 1768 | + guide_dir = temp_dir / "guide" / "chapters" |
| 1769 | + guide_dir.mkdir(parents=True, exist_ok=True) |
| 1770 | + target = guide_dir / "02-installation.html" |
| 1771 | + (temp_dir / "guide" / "index.html").write_text("<h1>Nginx Guide</h1>\n") |
| 1772 | + (guide_dir / "01-getting-started.html").write_text("<h1>Getting Started</h1>\n") |
| 1773 | + target.write_text("<h1>Installation</h1>\n") |
| 1774 | + dod_path = dod_store.save(dod) |
| 1775 | + session = FakeSession(active_dod_path=str(dod_path), messages=[]) |
| 1776 | + hook = LateReferenceDriftHook( |
| 1777 | + dod_store=dod_store, |
| 1778 | + project_root=temp_dir, |
| 1779 | + session=session, |
| 1780 | + ) |
| 1781 | + |
| 1782 | + def make_context(index: int) -> HookContext: |
| 1783 | + return HookContext( |
| 1784 | + tool_call=ToolCall( |
| 1785 | + id=f"read-verify-{index}", |
| 1786 | + name="read", |
| 1787 | + arguments={"file_path": str(target)}, |
| 1788 | + ), |
| 1789 | + tool=registry.get("read"), |
| 1790 | + registry=registry, |
| 1791 | + permission_policy=policy, |
| 1792 | + source="verification", |
| 1793 | + ) |
| 1794 | + |
| 1795 | + for index in range(1, 5): |
| 1796 | + context = make_context(index) |
| 1797 | + result = await hook.pre_tool_use(context) |
| 1798 | + assert result.decision == HookDecision.CONTINUE |
| 1799 | + await hook.post_tool_use(context) |
| 1800 | + |
| 1801 | + blocked = await hook.pre_tool_use(make_context(5)) |
| 1802 | + |
| 1803 | + assert blocked.decision == HookDecision.DENY |
| 1804 | + assert blocked.terminal_state == "blocked" |
| 1805 | + assert blocked.message is not None |
| 1806 | + assert "post-build audit loop" in blocked.message |
| 1807 | + |
| 1808 | + |
| 1738 | 1809 | @pytest.mark.asyncio |
| 1739 | 1810 | async def test_late_reference_drift_hook_does_not_treat_empty_output_dir_as_complete_artifact_set( |
| 1740 | 1811 | temp_dir: Path, |