@@ -3945,6 +3945,162 @@ async def test_tool_batch_runner_todowrite_after_outputs_exist_but_links_missing |
| 3945 | 3945 | assert "Move to verification or final confirmation using the files already on disk." in message |
| 3946 | 3946 | |
| 3947 | 3947 | |
| 3948 | +@pytest.mark.asyncio |
| 3949 | +async def test_tool_batch_runner_todowrite_drops_unplanned_expansion_after_outputs_exist( |
| 3950 | + temp_dir: Path, |
| 3951 | +) -> None: |
| 3952 | + async def assess_confidence( |
| 3953 | + tool_name: str, |
| 3954 | + tool_args: dict, |
| 3955 | + context: str, |
| 3956 | + ) -> ConfidenceAssessment: |
| 3957 | + raise AssertionError("Confidence scoring should not run for this scenario") |
| 3958 | + |
| 3959 | + async def verify_action( |
| 3960 | + tool_name: str, |
| 3961 | + tool_args: dict, |
| 3962 | + result: str, |
| 3963 | + expected: str = "", |
| 3964 | + ) -> ActionVerification: |
| 3965 | + raise AssertionError("Verification should not run for this scenario") |
| 3966 | + |
| 3967 | + guide_root = temp_dir / "guides" / "nginx" |
| 3968 | + chapters = guide_root / "chapters" |
| 3969 | + guide_root.mkdir(parents=True) |
| 3970 | + chapters.mkdir() |
| 3971 | + index_path = guide_root / "index.html" |
| 3972 | + chapter_one = chapters / "01-introduction.html" |
| 3973 | + chapter_two = chapters / "02-installation.html" |
| 3974 | + index_path.write_text( |
| 3975 | + "\n".join( |
| 3976 | + [ |
| 3977 | + '<a href="chapters/01-introduction.html">Intro</a>', |
| 3978 | + '<a href="chapters/02-installation.html">Install</a>', |
| 3979 | + '<a href="../index.html">Back</a>', |
| 3980 | + "", |
| 3981 | + ] |
| 3982 | + ) |
| 3983 | + ) |
| 3984 | + chapter_one.write_text("<html></html>\n") |
| 3985 | + chapter_two.write_text("<html></html>\n") |
| 3986 | + |
| 3987 | + implementation_plan = temp_dir / "implementation.md" |
| 3988 | + implementation_plan.write_text( |
| 3989 | + "\n".join( |
| 3990 | + [ |
| 3991 | + "# Implementation Plan", |
| 3992 | + "", |
| 3993 | + "## File Changes", |
| 3994 | + f"- `{guide_root}/`", |
| 3995 | + f"- `{chapters}/`", |
| 3996 | + f"- `{index_path}`", |
| 3997 | + f"- `{chapter_one}`", |
| 3998 | + f"- `{chapter_two}`", |
| 3999 | + "", |
| 4000 | + ] |
| 4001 | + ) |
| 4002 | + ) |
| 4003 | + |
| 4004 | + context = build_context( |
| 4005 | + temp_dir=temp_dir, |
| 4006 | + messages=[], |
| 4007 | + safeguards=FakeSafeguards(), |
| 4008 | + assess_confidence=assess_confidence, |
| 4009 | + verify_action=verify_action, |
| 4010 | + auto_recover=False, |
| 4011 | + ) |
| 4012 | + queued_messages: list[str] = [] |
| 4013 | + context.queue_steering_message_callback = queued_messages.append |
| 4014 | + runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir)) |
| 4015 | + dod = create_definition_of_done("Create a multi-file nginx guide.") |
| 4016 | + dod.implementation_plan = str(implementation_plan) |
| 4017 | + dod.verification_commands = [f"ls -la {guide_root}"] |
| 4018 | + |
| 4019 | + tool_call = ToolCall( |
| 4020 | + id="todo-post-build-expansion", |
| 4021 | + name="TodoWrite", |
| 4022 | + arguments={ |
| 4023 | + "todos": [ |
| 4024 | + { |
| 4025 | + "content": "Create index.html for nginx guide", |
| 4026 | + "activeForm": "Creating index.html", |
| 4027 | + "status": "in_progress", |
| 4028 | + }, |
| 4029 | + { |
| 4030 | + "content": "Create chapter 01-introduction.html", |
| 4031 | + "activeForm": "Creating chapter 01-introduction.html", |
| 4032 | + "status": "completed", |
| 4033 | + }, |
| 4034 | + { |
| 4035 | + "content": "Create chapter 02-installation.html", |
| 4036 | + "activeForm": "Creating chapter 02-installation.html", |
| 4037 | + "status": "completed", |
| 4038 | + }, |
| 4039 | + { |
| 4040 | + "content": "Create chapter 08-troubleshooting.html", |
| 4041 | + "activeForm": "Creating chapter 08-troubleshooting.html", |
| 4042 | + "status": "pending", |
| 4043 | + }, |
| 4044 | + ] |
| 4045 | + }, |
| 4046 | + ) |
| 4047 | + executor = FakeExecutor( |
| 4048 | + [ |
| 4049 | + tool_outcome( |
| 4050 | + tool_call=tool_call, |
| 4051 | + output="Todos updated", |
| 4052 | + is_error=False, |
| 4053 | + metadata={ |
| 4054 | + "new_todos": [ |
| 4055 | + { |
| 4056 | + "content": "Create index.html for nginx guide", |
| 4057 | + "active_form": "Creating index.html", |
| 4058 | + "status": "in_progress", |
| 4059 | + }, |
| 4060 | + { |
| 4061 | + "content": "Create chapter 01-introduction.html", |
| 4062 | + "active_form": "Creating chapter 01-introduction.html", |
| 4063 | + "status": "completed", |
| 4064 | + }, |
| 4065 | + { |
| 4066 | + "content": "Create chapter 02-installation.html", |
| 4067 | + "active_form": "Creating chapter 02-installation.html", |
| 4068 | + "status": "completed", |
| 4069 | + }, |
| 4070 | + { |
| 4071 | + "content": "Create chapter 08-troubleshooting.html", |
| 4072 | + "active_form": "Creating chapter 08-troubleshooting.html", |
| 4073 | + "status": "pending", |
| 4074 | + }, |
| 4075 | + ] |
| 4076 | + }, |
| 4077 | + ) |
| 4078 | + ] |
| 4079 | + ) |
| 4080 | + |
| 4081 | + summary = TurnSummary(final_response="") |
| 4082 | + await runner.execute_batch( |
| 4083 | + tool_calls=[tool_call], |
| 4084 | + tool_source="assistant", |
| 4085 | + pending_tool_calls_seen=set(), |
| 4086 | + emit=_noop_emit, |
| 4087 | + summary=summary, |
| 4088 | + dod=dod, |
| 4089 | + executor=executor, # type: ignore[arg-type] |
| 4090 | + on_confirmation=None, |
| 4091 | + on_user_question=None, |
| 4092 | + emit_confirmation=None, |
| 4093 | + consecutive_errors=0, |
| 4094 | + ) |
| 4095 | + |
| 4096 | + assert queued_messages |
| 4097 | + message = queued_messages[-1] |
| 4098 | + assert "Todo tracking is updated. All explicitly planned artifacts now exist on disk." in message |
| 4099 | + assert "Repair or verify the current files instead of expanding the artifact set." in message |
| 4100 | + assert "Move to verification or final confirmation using the files already on disk." in message |
| 4101 | + assert "08-troubleshooting.html" not in message |
| 4102 | + |
| 4103 | + |
| 3948 | 4104 | @pytest.mark.asyncio |
| 3949 | 4105 | async def test_tool_batch_runner_todowrite_with_existing_output_roots_requeues_next_mutation( |
| 3950 | 4106 | temp_dir: Path, |