@@ -3506,12 +3506,150 @@ async def test_tool_batch_runner_todowrite_with_existing_output_roots_requeues_n |
| 3506 | 3506 | assert "Todo tracking is updated. A declared output artifact is still missing." in message |
| 3507 | 3507 | assert "Continue with the next pending item: `Write the introduction chapter`." in message |
| 3508 | 3508 | assert "Resume by creating `01-introduction.html` now." in message |
| 3509 | | - assert "It is the next missing declared output under `chapters/`." in message |
| 3510 | 3509 | assert "Prefer one `write` call for `" in message |
| 3511 | 3510 | assert "01-introduction.html` instead of more rereads." in message |
| 3512 | 3511 | assert "Do not spend the next turn on TodoWrite alone" in message |
| 3513 | 3512 | |
| 3514 | 3513 | |
| 3514 | +@pytest.mark.asyncio |
| 3515 | +async def test_tool_batch_runner_todowrite_prefers_pending_index_over_empty_output_directory( |
| 3516 | + temp_dir: Path, |
| 3517 | +) -> None: |
| 3518 | + async def assess_confidence( |
| 3519 | + tool_name: str, |
| 3520 | + tool_args: dict, |
| 3521 | + context: str, |
| 3522 | + ) -> ConfidenceAssessment: |
| 3523 | + raise AssertionError("Confidence scoring should not run in this scenario") |
| 3524 | + |
| 3525 | + async def verify_action( |
| 3526 | + tool_name: str, |
| 3527 | + tool_args: dict, |
| 3528 | + result: str, |
| 3529 | + expected: str = "", |
| 3530 | + ) -> ActionVerification: |
| 3531 | + raise AssertionError("Verification should not run in this scenario") |
| 3532 | + |
| 3533 | + guide_root = temp_dir / "Loader" / "guides" / "nginx" |
| 3534 | + chapters = guide_root / "chapters" |
| 3535 | + chapters.mkdir(parents=True) |
| 3536 | + index_path = guide_root / "index.html" |
| 3537 | + implementation_plan = temp_dir / "implementation.md" |
| 3538 | + implementation_plan.write_text( |
| 3539 | + "\n".join( |
| 3540 | + [ |
| 3541 | + "# Implementation Plan", |
| 3542 | + "", |
| 3543 | + "## File Changes", |
| 3544 | + f"- `{chapters}/`", |
| 3545 | + f"- `{index_path}`", |
| 3546 | + "", |
| 3547 | + ] |
| 3548 | + ) |
| 3549 | + ) |
| 3550 | + |
| 3551 | + dod = create_definition_of_done("Create a multi-file nginx guide.") |
| 3552 | + dod.implementation_plan = str(implementation_plan) |
| 3553 | + sync_todos_to_definition_of_done( |
| 3554 | + dod, |
| 3555 | + [ |
| 3556 | + { |
| 3557 | + "content": "Examine the existing Fortran guide structure to understand the format and depth", |
| 3558 | + "active_form": "Examining the existing Fortran guide structure", |
| 3559 | + "status": "completed", |
| 3560 | + }, |
| 3561 | + { |
| 3562 | + "content": "Create the new nginx guide directory structure", |
| 3563 | + "active_form": "Creating the new nginx guide directory structure", |
| 3564 | + "status": "completed", |
| 3565 | + }, |
| 3566 | + { |
| 3567 | + "content": "Create a new index.html for the nginx guide", |
| 3568 | + "active_form": "Creating a new index.html for the nginx guide", |
| 3569 | + "status": "pending", |
| 3570 | + }, |
| 3571 | + { |
| 3572 | + "content": "Create the first chapter for the nginx guide", |
| 3573 | + "active_form": "Creating the first chapter for the nginx guide", |
| 3574 | + "status": "pending", |
| 3575 | + }, |
| 3576 | + ], |
| 3577 | + project_root=temp_dir, |
| 3578 | + ) |
| 3579 | + |
| 3580 | + queued_messages: list[str] = [] |
| 3581 | + context = build_context( |
| 3582 | + temp_dir=temp_dir, |
| 3583 | + messages=[], |
| 3584 | + safeguards=FakeSafeguards(), |
| 3585 | + assess_confidence=assess_confidence, |
| 3586 | + verify_action=verify_action, |
| 3587 | + auto_recover=False, |
| 3588 | + ) |
| 3589 | + context.queue_steering_message_callback = queued_messages.append |
| 3590 | + runner = ToolBatchRunner(context, DefinitionOfDoneStore(temp_dir)) |
| 3591 | + |
| 3592 | + todos = [ |
| 3593 | + { |
| 3594 | + "content": "Examine the existing Fortran guide structure to understand the format and depth", |
| 3595 | + "active_form": "Examining the existing Fortran guide structure", |
| 3596 | + "status": "completed", |
| 3597 | + }, |
| 3598 | + { |
| 3599 | + "content": "Create the new nginx guide directory structure", |
| 3600 | + "active_form": "Creating the new nginx guide directory structure", |
| 3601 | + "status": "completed", |
| 3602 | + }, |
| 3603 | + { |
| 3604 | + "content": "Create a new index.html for the nginx guide", |
| 3605 | + "active_form": "Creating a new index.html for the nginx guide", |
| 3606 | + "status": "pending", |
| 3607 | + }, |
| 3608 | + { |
| 3609 | + "content": "Create the first chapter for the nginx guide", |
| 3610 | + "active_form": "Creating the first chapter for the nginx guide", |
| 3611 | + "status": "pending", |
| 3612 | + }, |
| 3613 | + ] |
| 3614 | + tool_call = ToolCall( |
| 3615 | + id="todo-index-before-chapter", |
| 3616 | + name="TodoWrite", |
| 3617 | + arguments={"todos": todos}, |
| 3618 | + ) |
| 3619 | + executor = FakeExecutor( |
| 3620 | + [ |
| 3621 | + tool_outcome( |
| 3622 | + tool_call=tool_call, |
| 3623 | + output="Todos updated", |
| 3624 | + is_error=False, |
| 3625 | + metadata={"new_todos": todos}, |
| 3626 | + ) |
| 3627 | + ] |
| 3628 | + ) |
| 3629 | + |
| 3630 | + summary = TurnSummary(final_response="") |
| 3631 | + await runner.execute_batch( |
| 3632 | + tool_calls=[tool_call], |
| 3633 | + tool_source="assistant", |
| 3634 | + pending_tool_calls_seen=set(), |
| 3635 | + emit=_noop_emit, |
| 3636 | + summary=summary, |
| 3637 | + dod=dod, |
| 3638 | + executor=executor, # type: ignore[arg-type] |
| 3639 | + on_confirmation=None, |
| 3640 | + on_user_question=None, |
| 3641 | + emit_confirmation=None, |
| 3642 | + consecutive_errors=0, |
| 3643 | + ) |
| 3644 | + |
| 3645 | + assert queued_messages |
| 3646 | + message = queued_messages[-1] |
| 3647 | + assert "Continue with the next pending item: `Create a new index.html for the nginx guide`." in message |
| 3648 | + assert "Resume by creating `index.html` now." in message |
| 3649 | + assert f"Prefer one `write` call for `{index_path.resolve(strict=False)}`" in message |
| 3650 | + assert "01-introduction.html" not in message |
| 3651 | + |
| 3652 | + |
| 3515 | 3653 | @pytest.mark.asyncio |
| 3516 | 3654 | async def test_tool_batch_runner_todowrite_with_declared_child_targets_names_next_missing_file( |
| 3517 | 3655 | temp_dir: Path, |
@@ -3635,7 +3773,6 @@ async def test_tool_batch_runner_todowrite_with_declared_child_targets_names_nex |
| 3635 | 3773 | assert "Todo tracking is updated. A declared output artifact is still missing." in message |
| 3636 | 3774 | assert "Continue with the next pending item: `Write the introduction chapter`." in message |
| 3637 | 3775 | assert "Resume by creating `introduction.html` now." in message |
| 3638 | | - assert "It is the next missing declared output under `chapters/`." in message |
| 3639 | 3776 | assert "Prefer one `write` call for `" in message |
| 3640 | 3777 | assert "introduction.html` instead of more rereads." in message |
| 3641 | 3778 | assert "Do not spend the next turn on TodoWrite alone" in message |