Python · 76211 bytes Raw Blame History
1 """Tests for response-repair helpers on RuntimeContext."""
2
3 from __future__ import annotations
4
5 import json
6 from pathlib import Path
7 from types import SimpleNamespace
8
9 import pytest
10
11 from loader.llm.base import Message, Role, ToolCall
12 from loader.runtime.context import RuntimeContext
13 from loader.runtime.dod import create_definition_of_done
14 from loader.runtime.path_display import display_runtime_path
15 from loader.runtime.permissions import (
16 PermissionMode,
17 build_permission_policy,
18 load_permission_rules,
19 )
20 from loader.runtime.recovery import RecoveryContext
21 from loader.runtime.repair import ResponseRepairer
22 from loader.tools.base import create_default_registry
23 from tests.helpers.runtime_harness import ScriptedBackend
24
25
26 class FakeSession:
27 def __init__(self) -> None:
28 self.messages = []
29
30 def append(self, message) -> None:
31 self.messages.append(message)
32
33
34 class FakeCodeFilter:
35 def reset(self) -> None:
36 return None
37
38
39 class FakeSafeguards:
40 def __init__(self) -> None:
41 self.action_tracker = object()
42 self.validator = object()
43 self.code_filter = FakeCodeFilter()
44
45 def filter_stream_chunk(self, content: str) -> str:
46 return content
47
48 def filter_complete_content(self, content: str) -> str:
49 return content
50
51 def should_steer(self) -> bool:
52 return False
53
54 def get_steering_message(self) -> str | None:
55 return None
56
57 def record_response(self, content: str) -> None:
58 return None
59
60 def detect_text_loop(self, content: str) -> tuple[bool, str]:
61 return False, ""
62
63 def detect_loop(self) -> tuple[bool, str]:
64 return False, ""
65
66
67 def build_context(
68 *,
69 temp_dir: Path,
70 use_react: bool,
71 ) -> RuntimeContext:
72 registry = create_default_registry(temp_dir)
73 registry.configure_workspace_root(temp_dir)
74 rule_status = load_permission_rules(temp_dir)
75 policy = build_permission_policy(
76 active_mode=PermissionMode.WORKSPACE_WRITE,
77 workspace_root=temp_dir,
78 tool_requirements=registry.get_tool_requirements(),
79 rules=rule_status.rules,
80 )
81 session = FakeSession()
82 return RuntimeContext(
83 project_root=temp_dir,
84 backend=ScriptedBackend(),
85 registry=registry,
86 session=session, # type: ignore[arg-type]
87 config=SimpleNamespace(force_react=use_react),
88 capability_profile=SimpleNamespace(supports_native_tools=not use_react), # type: ignore[arg-type]
89 project_context=None,
90 permission_policy=policy,
91 permission_config_status=rule_status,
92 workflow_mode="execute",
93 safeguards=FakeSafeguards(),
94 )
95
96
97 def test_response_repairer_uses_runtime_parser_for_bracket_tool_fallback(
98 temp_dir: Path,
99 ) -> None:
100 context = build_context(
101 temp_dir=temp_dir,
102 use_react=False,
103 )
104 repairer = ResponseRepairer(context)
105
106 analysis = repairer.analyze_response(
107 content="I need clarification.",
108 response_content='[calls askuserquestion tool with: question="Which path?"]',
109 tool_calls=[],
110 extracted_iterations=0,
111 max_extracted_iterations=3,
112 )
113
114 assert analysis.tool_calls == [
115 ToolCall(
116 id="call_0",
117 name="AskUserQuestion",
118 arguments={"question": "Which path?"},
119 )
120 ]
121 assert analysis.tool_source == "raw_text"
122 assert analysis.clear_stream is True
123
124
125 def test_response_repairer_recovers_todowrite_from_runtime_registry(
126 temp_dir: Path,
127 ) -> None:
128 context = build_context(
129 temp_dir=temp_dir,
130 use_react=False,
131 )
132 repairer = ResponseRepairer(context)
133
134 analysis = repairer.analyze_response(
135 content="I'll track the work first.",
136 response_content=json.dumps(
137 {
138 "name": "TodoWrite",
139 "arguments": {
140 "todos": [
141 {
142 "content": "Run tests",
143 "active_form": "Running tests",
144 "status": "in_progress",
145 }
146 ]
147 },
148 }
149 ),
150 tool_calls=[],
151 extracted_iterations=0,
152 max_extracted_iterations=3,
153 )
154
155 assert analysis.tool_source == "raw_text"
156 assert analysis.clear_stream is True
157 assert analysis.tool_calls == [
158 ToolCall(
159 id="call_0",
160 name="TodoWrite",
161 arguments={
162 "todos": [
163 {
164 "content": "Run tests",
165 "active_form": "Running tests",
166 "status": "in_progress",
167 }
168 ]
169 },
170 )
171 ]
172
173
174 def test_response_repairer_fails_honestly_when_raw_tool_budget_is_exhausted(
175 temp_dir: Path,
176 ) -> None:
177 context = build_context(
178 temp_dir=temp_dir,
179 use_react=False,
180 )
181 repairer = ResponseRepairer(context)
182
183 analysis = repairer.analyze_response(
184 content=json.dumps(
185 {
186 "name": "read",
187 "arguments": {"file_path": "README.md"},
188 }
189 ),
190 response_content=json.dumps(
191 {
192 "name": "read",
193 "arguments": {"file_path": "README.md"},
194 }
195 ),
196 tool_calls=[],
197 extracted_iterations=3,
198 max_extracted_iterations=3,
199 )
200
201 assert analysis.should_stop is True
202 assert analysis.final_response == (
203 "I couldn't safely continue because the model kept emitting raw-text "
204 "tool calls instead of proper tool invocations. Please try again or "
205 "switch to a different backend/model."
206 )
207 assert analysis.failure == "raw-text tool recovery budget exhausted"
208 assert "Let me know if you'd like me to continue" not in analysis.final_response
209
210
211 def test_empty_response_retry_message_surfaces_missing_planned_artifacts_and_working_note(
212 temp_dir: Path,
213 ) -> None:
214 context = build_context(
215 temp_dir=temp_dir,
216 use_react=False,
217 )
218 repairer = ResponseRepairer(context)
219 implementation_plan = temp_dir / "implementation.md"
220 implementation_plan.write_text(
221 "\n".join(
222 [
223 "# Implementation Plan",
224 "",
225 "## File Changes",
226 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
227 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
228 "",
229 ]
230 )
231 )
232 first_artifact = temp_dir / "guides" / "nginx" / "index.html"
233 first_artifact.parent.mkdir(parents=True)
234 first_artifact.write_text("<html></html>\n")
235
236 dod = create_definition_of_done("Create a multi-file nginx guide.")
237 dod.implementation_plan = str(implementation_plan)
238 dod.touched_files.append(str(first_artifact))
239 dod.completed_items.append("Create the main index.html file")
240 dod.pending_items.append("Create each chapter file in sequence")
241
242 context.session.append(
243 SimpleNamespace(
244 role="tool",
245 content=(
246 "Observation [notepad_write_working]: Result: "
247 "- [2026-04-21T19:17:34Z] Creating fifth chapter file: Advanced configurations"
248 ),
249 )
250 )
251
252 decision = repairer.handle_empty_response(
253 task="Create a multi-file nginx guide.",
254 original_task=None,
255 empty_retry_count=1,
256 max_empty_retries=2,
257 dod=dod,
258 )
259
260 assert decision.should_continue is True
261 assert decision.retry_message is not None
262 assert "Latest working note: Creating fifth chapter file: Advanced configurations" in decision.retry_message
263 assert "Confirmed touched files: `index.html`" in decision.retry_message
264 assert "Confirmed completed work: Create the main index.html file" in decision.retry_message
265 assert "Next pending item: Create each chapter file in sequence" in decision.retry_message
266 assert "Continue from the confirmed progress below instead of restarting." in decision.retry_message
267
268
269 def test_empty_response_retry_mentions_write_can_create_missing_parent_directories(
270 temp_dir: Path,
271 ) -> None:
272 context = build_context(
273 temp_dir=temp_dir,
274 use_react=False,
275 )
276 repairer = ResponseRepairer(context)
277
278 guide_root = temp_dir / "guides" / "nginx"
279 index_path = guide_root / "index.html"
280
281 implementation_plan = temp_dir / "implementation.md"
282 implementation_plan.write_text(
283 "\n".join(
284 [
285 "# Implementation Plan",
286 "",
287 "## File Changes",
288 f"- `{index_path}`",
289 "",
290 ]
291 )
292 )
293
294 dod = create_definition_of_done("Create a multi-file nginx guide.")
295 dod.implementation_plan = str(implementation_plan)
296 dod.pending_items.extend(
297 [
298 "Create nginx guide directory structure",
299 "Write main index.html for nginx guide",
300 ]
301 )
302
303 decision = repairer.handle_empty_response(
304 task="Create a multi-file nginx guide.",
305 original_task=None,
306 empty_retry_count=1,
307 max_empty_retries=2,
308 dod=dod,
309 )
310
311 assert decision.should_continue is True
312 assert decision.retry_message is not None
313 assert (
314 "Resume with this exact next step: create `index.html`."
315 in decision.retry_message
316 )
317 assert (
318 "Prefer one `write` call for "
319 f"`{display_runtime_path(index_path)}` before any more reference reads."
320 in decision.retry_message
321 )
322 assert (
323 "The `write` tool can create that file's parent directories automatically, so do the write in one step instead of stopping for a separate mkdir."
324 in decision.retry_message
325 )
326 assert (
327 f'Emit this tool shape now: `write(file_path="{display_runtime_path(index_path)}", content="...")`.'
328 in decision.retry_message
329 )
330 assert (
331 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
332 in decision.retry_message
333 )
334 assert "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message
335
336
337 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
338 temp_dir: Path,
339 ) -> None:
340 context = build_context(
341 temp_dir=temp_dir,
342 use_react=False,
343 )
344 repairer = ResponseRepairer(context)
345
346 guide_root = temp_dir / "guides" / "nginx"
347 chapters_path = guide_root / "chapters"
348 index_path = guide_root / "index.html"
349
350 implementation_plan = temp_dir / "implementation.md"
351 implementation_plan.write_text(
352 "\n".join(
353 [
354 "# Implementation Plan",
355 "",
356 "## File Changes",
357 f"- `{chapters_path}/`",
358 f"- `{index_path}`",
359 "",
360 ]
361 )
362 )
363
364 dod = create_definition_of_done("Create a multi-file nginx guide.")
365 dod.implementation_plan = str(implementation_plan)
366 dod.pending_items.extend(
367 [
368 "Create the nginx directory structure",
369 "Create the main index.html file for nginx guide",
370 ]
371 )
372
373 decision = repairer.handle_empty_response(
374 task="Create a multi-file nginx guide.",
375 original_task=None,
376 empty_retry_count=1,
377 max_empty_retries=2,
378 dod=dod,
379 )
380
381 assert decision.should_continue is True
382 assert decision.retry_message is not None
383 assert (
384 "Resume with this exact next step: continue `Create the nginx directory structure` "
385 "by creating `chapters/`."
386 in decision.retry_message
387 )
388 assert (
389 "Prefer one concrete directory-creation step for "
390 f"`{display_runtime_path(chapters_path)}` before more research."
391 in decision.retry_message
392 )
393 expected_command = f"mkdir -p {display_runtime_path(chapters_path)}"
394 assert (
395 f'Emit this tool shape now: `bash(command="{expected_command}")`.'
396 in decision.retry_message
397 )
398 assert f'write(file_path="{display_runtime_path(chapters_path)}"' not in decision.retry_message
399
400
401 def test_empty_response_retry_uses_home_relative_path_for_home_artifacts(
402 temp_dir: Path,
403 monkeypatch: pytest.MonkeyPatch,
404 ) -> None:
405 monkeypatch.setenv("HOME", str(temp_dir.resolve(strict=False)))
406 context = build_context(
407 temp_dir=temp_dir,
408 use_react=False,
409 )
410 repairer = ResponseRepairer(context)
411
412 guide_root = temp_dir / "Loader" / "guides" / "nginx"
413 index_path = guide_root / "index.html"
414
415 implementation_plan = temp_dir / "implementation.md"
416 implementation_plan.write_text(
417 "\n".join(
418 [
419 "# Implementation Plan",
420 "",
421 "## File Changes",
422 f"- `{index_path}`",
423 "",
424 ]
425 )
426 )
427
428 dod = create_definition_of_done("Create a multi-file nginx guide.")
429 dod.implementation_plan = str(implementation_plan)
430 dod.pending_items.extend(
431 [
432 "Create nginx guide directory structure",
433 "Write main index.html for nginx guide",
434 ]
435 )
436
437 decision = repairer.handle_empty_response(
438 task="Create a multi-file nginx guide.",
439 original_task=None,
440 empty_retry_count=1,
441 max_empty_retries=2,
442 dod=dod,
443 )
444
445 assert decision.should_continue is True
446 assert decision.retry_message is not None
447 assert "`~/Loader/guides/nginx/index.html`" in decision.retry_message
448 assert (
449 'Emit this tool shape now: `write(file_path="~/Loader/guides/nginx/index.html", content="...")`.'
450 in decision.retry_message
451 )
452 assert (
453 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
454 in decision.retry_message
455 )
456
457
458 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
459 temp_dir: Path,
460 ) -> None:
461 context = build_context(
462 temp_dir=temp_dir,
463 use_react=False,
464 )
465 repairer = ResponseRepairer(context)
466
467 guide_root = temp_dir / "guides" / "nginx"
468 chapters = guide_root / "chapters"
469 chapters.mkdir(parents=True)
470 index_path = guide_root / "index.html"
471 first_chapter = chapters / "01-introduction.html"
472 second_chapter = chapters / "02-installation.html"
473 index_path.write_text("<html></html>\n")
474 first_chapter.write_text("<h1>Intro</h1>\n")
475
476 implementation_plan = temp_dir / "implementation.md"
477 implementation_plan.write_text(
478 "\n".join(
479 [
480 "# Implementation Plan",
481 "",
482 "## File Changes",
483 f"- `{index_path}`",
484 f"- `{first_chapter}`",
485 f"- `{second_chapter}`",
486 "",
487 ]
488 )
489 )
490
491 dod = create_definition_of_done("Create a multi-file nginx guide.")
492 dod.implementation_plan = str(implementation_plan)
493 dod.touched_files.extend([str(index_path), str(first_chapter)])
494 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
495
496 context.recovery_context = RecoveryContext(
497 original_tool="write",
498 original_args={"file_path": "", "content": "<html></html>\n"},
499 )
500 context.recovery_context.add_attempt(
501 "write",
502 {"file_path": "", "content": "<html></html>\n"},
503 "Empty file path",
504 )
505
506 decision = repairer.handle_empty_response(
507 task="Create a multi-file nginx guide.",
508 original_task=None,
509 empty_retry_count=1,
510 max_empty_retries=2,
511 dod=dod,
512 )
513
514 assert decision.should_continue is True
515 assert decision.retry_message is not None
516 assert (
517 "Last tool failure: resend `write` for "
518 f"`{display_runtime_path(second_chapter)}` with a valid `file_path` and real `content`."
519 in decision.retry_message
520 )
521 assert "Do not leave `file_path` empty" in decision.retry_message
522 assert (
523 f'Emit this tool shape now: `write(file_path="{display_runtime_path(second_chapter)}", content="...")`.'
524 in decision.retry_message
525 )
526
527
528 def test_empty_response_retry_respects_discovery_first_pending_step(
529 temp_dir: Path,
530 ) -> None:
531 context = build_context(
532 temp_dir=temp_dir,
533 use_react=False,
534 )
535 repairer = ResponseRepairer(context)
536
537 implementation_plan = temp_dir / "implementation.md"
538 implementation_plan.write_text(
539 "\n".join(
540 [
541 "# Implementation Plan",
542 "",
543 "## File Changes",
544 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
545 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
546 "",
547 ]
548 )
549 )
550
551 dod = create_definition_of_done("Create a multi-file nginx guide.")
552 dod.implementation_plan = str(implementation_plan)
553 dod.pending_items.extend(
554 [
555 "First, examine the existing fortran guide structure and content to understand the format",
556 "Create the nginx directory structure",
557 "Develop the main index.html file for the nginx guide",
558 ]
559 )
560
561 context.session.append(
562 SimpleNamespace(
563 role="tool",
564 content=(
565 "Observation [notepad_write_working]: Result: "
566 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
567 ),
568 )
569 )
570
571 decision = repairer.handle_empty_response(
572 task="Create a multi-file nginx guide.",
573 original_task=None,
574 empty_retry_count=1,
575 max_empty_retries=2,
576 dod=dod,
577 )
578
579 assert decision.should_continue is True
580 assert decision.retry_message is not None
581 assert (
582 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
583 in decision.retry_message
584 )
585 assert "one concrete evidence-gathering tool call" in decision.retry_message
586 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
587
588
589 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
590 temp_dir: Path,
591 ) -> None:
592 context = build_context(
593 temp_dir=temp_dir,
594 use_react=False,
595 )
596 repairer = ResponseRepairer(context)
597
598 guide_root = temp_dir / "guides" / "nginx"
599 chapters = guide_root / "chapters"
600 chapters.mkdir(parents=True)
601 index_path = guide_root / "index.html"
602 chapter_one = chapters / "01-getting-started.html"
603 chapter_two = chapters / "02-installation.html"
604 chapter_three = chapters / "03-first-website.html"
605 chapter_four = chapters / "04-configuration-basics.html"
606 index_path.write_text("<html></html>\n")
607 chapter_one.write_text("<h1>One</h1>\n")
608 chapter_two.write_text("<h1>Two</h1>\n")
609 chapter_three.write_text("<h1>Three</h1>\n")
610
611 implementation_plan = temp_dir / "implementation.md"
612 implementation_plan.write_text(
613 "\n".join(
614 [
615 "# Implementation Plan",
616 "",
617 "## File Changes",
618 f"- `{guide_root}/`",
619 f"- `{chapters}/`",
620 f"- `{index_path}`",
621 f"- `{chapter_one}`",
622 f"- `{chapter_two}`",
623 f"- `{chapter_three}`",
624 f"- `{chapter_four}`",
625 "",
626 ]
627 )
628 )
629
630 dod = create_definition_of_done("Create a multi-file nginx guide.")
631 dod.implementation_plan = str(implementation_plan)
632 dod.touched_files.extend(
633 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
634 )
635 dod.completed_items.extend(
636 [
637 "Create the directory structure for the new nginx guide",
638 "Create the main index.html file with proper structure",
639 ]
640 )
641 dod.pending_items.append("Create each chapter file in sequence")
642
643 decision = repairer.handle_empty_response(
644 task="Create a multi-file nginx guide.",
645 original_task=None,
646 empty_retry_count=3,
647 max_empty_retries=2,
648 dod=dod,
649 )
650
651 assert decision.should_continue is True
652 assert decision.retry_message is not None
653 assert "retry 3/4" in decision.retry_message
654 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
655
656
657 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
658 temp_dir: Path,
659 ) -> None:
660 context = build_context(
661 temp_dir=temp_dir,
662 use_react=False,
663 )
664 repairer = ResponseRepairer(context)
665
666 implementation_plan = temp_dir / "implementation.md"
667 implementation_plan.write_text(
668 "\n".join(
669 [
670 "# Implementation Plan",
671 "",
672 "## File Changes",
673 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
674 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
675 "",
676 ]
677 )
678 )
679
680 dod = create_definition_of_done("Create a multi-file nginx guide.")
681 dod.implementation_plan = str(implementation_plan)
682 dod.pending_items.append("Develop the main index.html file for the nginx guide")
683
684 decision = repairer.handle_empty_response(
685 task="Create a multi-file nginx guide.",
686 original_task=None,
687 empty_retry_count=3,
688 max_empty_retries=2,
689 dod=dod,
690 )
691
692 assert decision.should_continue is True
693 assert decision.retry_message is not None
694 assert "retry 3/4" in decision.retry_message
695 assert "Next missing planned artifact: `index.html`" in decision.retry_message
696 assert (
697 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
698 "by creating `index.html`."
699 in decision.retry_message
700 )
701
702
703 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
704 temp_dir: Path,
705 ) -> None:
706 context = build_context(
707 temp_dir=temp_dir,
708 use_react=False,
709 )
710 repairer = ResponseRepairer(context)
711
712 guide_root = temp_dir / "guides" / "nginx"
713 chapters = guide_root / "chapters"
714 guide_root.mkdir(parents=True)
715 chapters.mkdir()
716 index_path = guide_root / "index.html"
717 index_path.write_text("<html></html>\n")
718
719 implementation_plan = temp_dir / "implementation.md"
720 implementation_plan.write_text(
721 "\n".join(
722 [
723 "# Implementation Plan",
724 "",
725 "## File Changes",
726 f"- `{chapters}/`",
727 f"- `{index_path}`",
728 "",
729 ]
730 )
731 )
732
733 dod = create_definition_of_done("Create a multi-file nginx guide.")
734 dod.implementation_plan = str(implementation_plan)
735 dod.touched_files.append(str(index_path))
736 dod.completed_items.extend(
737 [
738 "Create the new nginx guide directory structure",
739 "Develop the main index.html file with proper structure",
740 ]
741 )
742 dod.pending_items.append("Create 01-introduction.html")
743
744 decision = repairer.handle_empty_response(
745 task="Create a multi-file nginx guide.",
746 original_task=None,
747 empty_retry_count=5,
748 max_empty_retries=2,
749 dod=dod,
750 )
751
752 assert decision.should_continue is True
753 assert decision.retry_message is not None
754 assert "retry 5/6" in decision.retry_message
755 assert "Continue `Create 01-introduction.html` by creating `01-introduction.html`." in decision.retry_message
756 assert 'Emit this tool shape now: `write(file_path="' in decision.retry_message
757 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
758
759
760 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
761 temp_dir: Path,
762 ) -> None:
763 context = build_context(
764 temp_dir=temp_dir,
765 use_react=False,
766 )
767 context.session.messages.append(
768 SimpleNamespace(
769 content=(
770 "Observation [notepad_write_working]: Result: "
771 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
772 )
773 )
774 )
775 repairer = ResponseRepairer(context)
776
777 guide_root = temp_dir / "guides" / "nginx"
778 chapters = guide_root / "chapters"
779 chapters.mkdir(parents=True)
780 index_path = guide_root / "index.html"
781 chapter_one = chapters / "01-getting-started.html"
782 chapter_two = chapters / "02-installation.html"
783 chapter_three = chapters / "03-first-website.html"
784 chapter_four = chapters / "04-configuration-basics.html"
785 chapter_five = chapters / "05-advanced-features.html"
786 index_path.write_text("<html></html>\n")
787 chapter_one.write_text("<h1>One</h1>\n")
788 chapter_two.write_text("<h1>Two</h1>\n")
789 chapter_three.write_text("<h1>Three</h1>\n")
790 chapter_four.write_text("<h1>Four</h1>\n")
791
792 implementation_plan = temp_dir / "implementation.md"
793 implementation_plan.write_text(
794 "\n".join(
795 [
796 "# Implementation Plan",
797 "",
798 "## File Changes",
799 f"- `{guide_root}/`",
800 f"- `{chapters}/`",
801 f"- `{index_path}`",
802 f"- `{chapter_one}`",
803 f"- `{chapter_two}`",
804 f"- `{chapter_three}`",
805 f"- `{chapter_four}`",
806 f"- `{chapter_five}`",
807 "",
808 ]
809 )
810 )
811
812 dod = create_definition_of_done("Create a multi-file nginx guide.")
813 dod.implementation_plan = str(implementation_plan)
814 dod.touched_files.extend(
815 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
816 )
817 dod.completed_items.extend(
818 [
819 "Create the directory structure for the new nginx guide",
820 "Create the main index.html file with proper structure",
821 ]
822 )
823 dod.pending_items.append("Create each chapter file in sequence")
824
825 decision = repairer.handle_empty_response(
826 task="Create a multi-file nginx guide.",
827 original_task=None,
828 empty_retry_count=3,
829 max_empty_retries=2,
830 dod=dod,
831 )
832
833 assert decision.should_continue is True
834 assert decision.retry_message is not None
835 assert "Continue from the exact next step below." in decision.retry_message
836 assert "Latest working note:" not in decision.retry_message
837 assert "Confirmed completed work:" not in decision.retry_message
838 assert "Next pending item:" not in decision.retry_message
839
840
841 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
842 temp_dir: Path,
843 ) -> None:
844 context = build_context(
845 temp_dir=temp_dir,
846 use_react=False,
847 )
848 repairer = ResponseRepairer(context)
849
850 guide_root = temp_dir / "guides" / "nginx"
851 chapters = guide_root / "chapters"
852 chapters.mkdir(parents=True)
853 index_path = guide_root / "index.html"
854 index_path.write_text("<html></html>\n")
855
856 implementation_plan = temp_dir / "implementation.md"
857 implementation_plan.write_text(
858 "\n".join(
859 [
860 "# Implementation Plan",
861 "",
862 "## File Changes",
863 f"- `{guide_root}/`",
864 f"- `{chapters}/`",
865 f"- `{index_path}`",
866 f"- `{chapters / '02-installation.html'}`",
867 "",
868 ]
869 )
870 )
871
872 dod = create_definition_of_done("Create a multi-file nginx guide.")
873 dod.implementation_plan = str(implementation_plan)
874 dod.touched_files.append(str(index_path))
875 dod.pending_items.append("Write the introduction chapter")
876
877 decision = repairer.handle_empty_response(
878 task="Create a multi-file nginx guide.",
879 original_task=None,
880 empty_retry_count=1,
881 max_empty_retries=2,
882 dod=dod,
883 )
884
885 assert decision.should_continue is True
886 assert decision.retry_message is not None
887 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
888 assert (
889 "Resume with this exact next step: continue `Write the introduction chapter` "
890 "by creating the next output file under `chapters/`."
891 in decision.retry_message
892 )
893 assert (
894 "Prefer one concrete `write` call for a file inside "
895 f"`{display_runtime_path(chapters)}` before more research."
896 in decision.retry_message
897 )
898
899
900 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
901 temp_dir: Path,
902 ) -> None:
903 context = build_context(
904 temp_dir=temp_dir,
905 use_react=False,
906 )
907 repairer = ResponseRepairer(context)
908
909 guide_root = temp_dir / "guides" / "nginx"
910 chapters = guide_root / "chapters"
911 guide_root.mkdir(parents=True)
912 chapters.mkdir()
913 chapter_one = chapters / "01-introduction.html"
914 index_path = guide_root / "index.html"
915
916 implementation_plan = temp_dir / "implementation.md"
917 implementation_plan.write_text(
918 "\n".join(
919 [
920 "# Implementation Plan",
921 "",
922 "## File Changes",
923 f"- `{guide_root}/`",
924 f"- `{index_path}`",
925 f"- `{chapters}/`",
926 f"- `{chapter_one}`",
927 "",
928 ]
929 )
930 )
931
932 dod = create_definition_of_done("Create a multi-file nginx guide.")
933 dod.implementation_plan = str(implementation_plan)
934 dod.completed_items.extend(
935 [
936 "First, examine the existing Fortran guide structure to understand the format and depth",
937 "Create the new nginx guide directory structure",
938 ]
939 )
940 dod.pending_items.append("Develop the main index.html file with proper structure")
941
942 decision = repairer.handle_empty_response(
943 task="Create a multi-file nginx guide.",
944 original_task=None,
945 empty_retry_count=2,
946 max_empty_retries=2,
947 dod=dod,
948 )
949
950 assert decision.should_continue is True
951 assert decision.retry_message is not None
952 assert (
953 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
954 in decision.retry_message
955 )
956 assert "Next missing planned artifact: `index.html`" in decision.retry_message
957 assert "Prefer one `write(content=...)` call" in decision.retry_message
958 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
959
960
961 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
962 temp_dir: Path,
963 ) -> None:
964 context = build_context(
965 temp_dir=temp_dir,
966 use_react=False,
967 )
968 repairer = ResponseRepairer(context)
969
970 guide_root = temp_dir / "guides" / "nginx"
971 chapters = guide_root / "chapters"
972 guide_root.mkdir(parents=True)
973 chapters.mkdir()
974 index_path = guide_root / "index.html"
975 chapter_one = chapters / "01-introduction.html"
976
977 implementation_plan = temp_dir / "implementation.md"
978 implementation_plan.write_text(
979 "\n".join(
980 [
981 "# Implementation Plan",
982 "",
983 "## File Changes",
984 f"- `{guide_root}/`",
985 f"- `{chapters}/`",
986 f"- `{index_path}`",
987 f"- `{chapter_one}`",
988 "",
989 ]
990 )
991 )
992
993 dod = create_definition_of_done("Create a multi-file nginx guide.")
994 dod.implementation_plan = str(implementation_plan)
995 dod.completed_items.extend(
996 [
997 "First, examine the existing Fortran guide structure to understand the format and depth",
998 "Create the new nginx guide directory structure",
999 ]
1000 )
1001 dod.pending_items.append("Develop the main index.html file with proper structure")
1002
1003 decision = repairer.handle_empty_response(
1004 task="Create a multi-file nginx guide.",
1005 original_task=None,
1006 empty_retry_count=4,
1007 max_empty_retries=4,
1008 dod=dod,
1009 )
1010
1011 assert decision.should_continue is True
1012 assert decision.retry_message is not None
1013 assert "Next missing planned artifact: `index.html`" in decision.retry_message
1014 assert (
1015 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
1016 "by creating `index.html`."
1017 in decision.retry_message
1018 )
1019 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
1020 assert "Remaining planned artifacts:" not in decision.retry_message
1021 assert (
1022 "Next observed output pattern under `chapters/`: `01-introduction.html`"
1023 not in decision.retry_message
1024 )
1025
1026
1027 def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_step(
1028 temp_dir: Path,
1029 ) -> None:
1030 context = build_context(
1031 temp_dir=temp_dir,
1032 use_react=False,
1033 )
1034 repairer = ResponseRepairer(context)
1035
1036 guide_root = temp_dir / "guides" / "nginx"
1037 chapters = guide_root / "chapters"
1038 chapters.mkdir(parents=True)
1039 index_path = guide_root / "index.html"
1040 index_path.write_text(
1041 "\n".join(
1042 [
1043 "<html>",
1044 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1045 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1046 "</html>",
1047 ]
1048 )
1049 + "\n"
1050 )
1051
1052 implementation_plan = temp_dir / "implementation.md"
1053 implementation_plan.write_text(
1054 "\n".join(
1055 [
1056 "# Implementation Plan",
1057 "",
1058 "## File Changes",
1059 f"- `{guide_root}/`",
1060 f"- `{chapters}/`",
1061 f"- `{index_path}`",
1062 "",
1063 ]
1064 )
1065 )
1066
1067 dod = create_definition_of_done("Create a multi-file nginx guide.")
1068 dod.implementation_plan = str(implementation_plan)
1069 dod.touched_files.append(str(index_path))
1070 dod.completed_items.append("Develop the main index.html file with proper structure")
1071 dod.pending_items.append("Create chapter files with content and structure")
1072
1073 decision = repairer.handle_empty_response(
1074 task="Create a multi-file nginx guide.",
1075 original_task=None,
1076 empty_retry_count=3,
1077 max_empty_retries=4,
1078 dod=dod,
1079 )
1080
1081 assert decision.should_continue is True
1082 assert decision.retry_message is not None
1083 assert "Next missing planned artifact:" not in decision.retry_message
1084 assert (
1085 "Continue `Create chapter files with content and structure` by creating `01-introduction.html`."
1086 in decision.retry_message
1087 )
1088 assert (
1089 'Emit this tool shape now: `write(file_path="'
1090 in decision.retry_message
1091 )
1092 assert (
1093 "Write a compact but real initial version of this file now, then refine or expand it in later edits."
1094 in decision.retry_message
1095 )
1096 assert "No narration, no TodoWrite, no rereads, and no empty response" in decision.retry_message
1097 assert "Follow the same full-payload one-file-at-a-time write pattern" not in decision.retry_message
1098 assert "Remaining planned artifacts:" not in decision.retry_message
1099 assert "Next pending item:" not in decision.retry_message
1100
1101
1102 def test_empty_response_retry_keeps_concrete_second_chapter_for_aggregate_chapter_step(
1103 temp_dir: Path,
1104 ) -> None:
1105 context = build_context(
1106 temp_dir=temp_dir,
1107 use_react=False,
1108 )
1109 repairer = ResponseRepairer(context)
1110
1111 guide_root = temp_dir / "guides" / "nginx"
1112 chapters = guide_root / "chapters"
1113 chapters.mkdir(parents=True)
1114 index_path = guide_root / "index.html"
1115 chapter_one = chapters / "01-introduction.html"
1116 chapter_two = chapters / "02-installation.html"
1117 index_path.write_text(
1118 "\n".join(
1119 [
1120 "<html>",
1121 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1122 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1123 "</html>",
1124 ]
1125 )
1126 + "\n"
1127 )
1128 chapter_one.write_text("<h1>Introduction</h1>\n")
1129
1130 implementation_plan = temp_dir / "implementation.md"
1131 implementation_plan.write_text(
1132 "\n".join(
1133 [
1134 "# Implementation Plan",
1135 "",
1136 "## File Changes",
1137 f"- `{guide_root}/`",
1138 f"- `{chapters}/`",
1139 f"- `{index_path}`",
1140 "",
1141 ]
1142 )
1143 )
1144
1145 dod = create_definition_of_done("Create a multi-file nginx guide.")
1146 dod.implementation_plan = str(implementation_plan)
1147 dod.touched_files.extend([str(index_path), str(chapter_one)])
1148 dod.completed_items.extend(
1149 [
1150 "Develop the main index.html file with proper structure",
1151 "Create first chapter file (01-introduction.html)",
1152 ]
1153 )
1154 dod.pending_items.append("Create chapter files following the established pattern")
1155
1156 decision = repairer.handle_empty_response(
1157 task="Create a multi-file nginx guide.",
1158 original_task=None,
1159 empty_retry_count=1,
1160 max_empty_retries=2,
1161 dod=dod,
1162 )
1163
1164 assert decision.should_continue is True
1165 assert decision.retry_message is not None
1166 assert "Next pending item:" not in decision.retry_message
1167 assert (
1168 "Resume with this exact next step: continue `Create chapter files following the established pattern` "
1169 "by creating `02-installation.html`."
1170 in decision.retry_message
1171 )
1172 assert (
1173 "It is the next concrete output needed to continue `Create chapter files following the established pattern`."
1174 in decision.retry_message
1175 )
1176 assert f"`{display_runtime_path(chapter_two)}`" in decision.retry_message
1177 assert "Follow the same full-payload one-file-at-a-time write pattern" in decision.retry_message
1178
1179
1180 def test_empty_response_retry_reuses_known_reference_structure_for_first_substantive_file(
1181 temp_dir: Path,
1182 ) -> None:
1183 context = build_context(
1184 temp_dir=temp_dir,
1185 use_react=False,
1186 )
1187 repairer = ResponseRepairer(context)
1188
1189 guide_root = temp_dir / "guides" / "nginx"
1190 chapters = guide_root / "chapters"
1191 chapters.mkdir(parents=True)
1192 index_path = guide_root / "index.html"
1193 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1194 reference_chapter.parent.mkdir(parents=True)
1195 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1196 index_path.write_text(
1197 "\n".join(
1198 [
1199 "<html>",
1200 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1201 "</html>",
1202 ]
1203 )
1204 + "\n"
1205 )
1206 context.session.append(
1207 Message(
1208 role=Role.ASSISTANT,
1209 content="",
1210 tool_calls=[
1211 ToolCall(
1212 id="call_ref",
1213 name="read",
1214 arguments={"file_path": str(reference_chapter)},
1215 )
1216 ],
1217 )
1218 )
1219
1220 implementation_plan = temp_dir / "implementation.md"
1221 implementation_plan.write_text(
1222 "\n".join(
1223 [
1224 "# Implementation Plan",
1225 "",
1226 "## File Changes",
1227 f"- `{guide_root}/`",
1228 f"- `{chapters}/`",
1229 f"- `{index_path}`",
1230 "",
1231 ]
1232 )
1233 )
1234
1235 dod = create_definition_of_done("Create a multi-file nginx guide.")
1236 dod.implementation_plan = str(implementation_plan)
1237 dod.touched_files.append(str(index_path))
1238 dod.completed_items.append("Develop the main index.html file with proper structure")
1239 dod.pending_items.append("Create chapter files following the established pattern")
1240
1241 decision = repairer.handle_empty_response(
1242 task="Create a multi-file nginx guide.",
1243 original_task=None,
1244 empty_retry_count=1,
1245 max_empty_retries=2,
1246 dod=dod,
1247 )
1248
1249 assert decision.should_continue is True
1250 assert decision.retry_message is not None
1251 assert (
1252 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1253 "as the starting pattern for this new file, then adapt the content to the current target."
1254 in decision.retry_message
1255 )
1256 assert (
1257 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1258 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1259 "and chapter body content."
1260 in decision.retry_message
1261 )
1262 assert (
1263 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1264 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1265 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1266 in decision.retry_message
1267 )
1268
1269
1270 def test_compact_first_substantive_retry_reuses_known_reference_structure(
1271 temp_dir: Path,
1272 ) -> None:
1273 context = build_context(
1274 temp_dir=temp_dir,
1275 use_react=False,
1276 )
1277 repairer = ResponseRepairer(context)
1278
1279 guide_root = temp_dir / "guides" / "nginx"
1280 chapters = guide_root / "chapters"
1281 chapters.mkdir(parents=True)
1282 index_path = guide_root / "index.html"
1283 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1284 reference_chapter.parent.mkdir(parents=True)
1285 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1286 index_path.write_text(
1287 "\n".join(
1288 [
1289 "<html>",
1290 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1291 "</html>",
1292 ]
1293 )
1294 + "\n"
1295 )
1296 context.session.append(
1297 Message(
1298 role=Role.ASSISTANT,
1299 content="",
1300 tool_calls=[
1301 ToolCall(
1302 id="call_ref",
1303 name="read",
1304 arguments={"file_path": str(reference_chapter)},
1305 )
1306 ],
1307 )
1308 )
1309
1310 implementation_plan = temp_dir / "implementation.md"
1311 implementation_plan.write_text(
1312 "\n".join(
1313 [
1314 "# Implementation Plan",
1315 "",
1316 "## File Changes",
1317 f"- `{guide_root}/`",
1318 f"- `{chapters}/`",
1319 f"- `{index_path}`",
1320 "",
1321 ]
1322 )
1323 )
1324
1325 dod = create_definition_of_done("Create a multi-file nginx guide.")
1326 dod.implementation_plan = str(implementation_plan)
1327 dod.touched_files.append(str(index_path))
1328 dod.completed_items.append("Develop the main index.html file with proper structure")
1329 dod.pending_items.append("Create chapter files following the established pattern")
1330
1331 decision = repairer.handle_empty_response(
1332 task="Create a multi-file nginx guide.",
1333 original_task=None,
1334 empty_retry_count=3,
1335 max_empty_retries=4,
1336 dod=dod,
1337 )
1338
1339 assert decision.should_continue is True
1340 assert decision.retry_message is not None
1341 assert (
1342 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1343 "as the starting pattern for this new file, then adapt the content to the current target."
1344 in decision.retry_message
1345 )
1346 assert (
1347 f"Reference cues from `{display_runtime_path(reference_chapter)}`: "
1348 "<h1>Chapter 1: Introduction to Fortran</h1>"
1349 in decision.retry_message
1350 )
1351 assert (
1352 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1353 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1354 "and chapter body content."
1355 in decision.retry_message
1356 )
1357 assert (
1358 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1359 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1360 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1361 in decision.retry_message
1362 )
1363
1364
1365 def test_late_first_substantive_retry_trims_context_to_core_write_cues(
1366 temp_dir: Path,
1367 ) -> None:
1368 context = build_context(
1369 temp_dir=temp_dir,
1370 use_react=False,
1371 )
1372 repairer = ResponseRepairer(context)
1373
1374 guide_root = temp_dir / "guides" / "nginx"
1375 chapters = guide_root / "chapters"
1376 chapters.mkdir(parents=True)
1377 index_path = guide_root / "index.html"
1378 reference_chapter = temp_dir / "guides" / "fortran" / "chapters" / "01-introduction.html"
1379 reference_chapter.parent.mkdir(parents=True)
1380 reference_chapter.write_text("<h1>Chapter 1: Introduction to Fortran</h1>\n")
1381 index_path.write_text(
1382 "\n".join(
1383 [
1384 "<html>",
1385 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1386 "</html>",
1387 ]
1388 )
1389 + "\n"
1390 )
1391 context.session.append(
1392 Message(
1393 role=Role.ASSISTANT,
1394 content="",
1395 tool_calls=[
1396 ToolCall(
1397 id="call_ref",
1398 name="read",
1399 arguments={"file_path": str(reference_chapter)},
1400 )
1401 ],
1402 )
1403 )
1404
1405 implementation_plan = temp_dir / "implementation.md"
1406 implementation_plan.write_text(
1407 "\n".join(
1408 [
1409 "# Implementation Plan",
1410 "",
1411 "## File Changes",
1412 f"- `{guide_root}/`",
1413 f"- `{chapters}/`",
1414 f"- `{index_path}`",
1415 "",
1416 ]
1417 )
1418 )
1419
1420 dod = create_definition_of_done("Create a multi-file nginx guide.")
1421 dod.implementation_plan = str(implementation_plan)
1422 dod.touched_files.append(str(index_path))
1423 dod.completed_items.append("Develop the main index.html file with proper structure")
1424 dod.pending_items.append("Create chapter files following the established pattern")
1425
1426 decision = repairer.handle_empty_response(
1427 task="Create a multi-file nginx guide.",
1428 original_task=None,
1429 empty_retry_count=6,
1430 max_empty_retries=6,
1431 dod=dod,
1432 )
1433
1434 assert decision.should_continue is True
1435 assert decision.retry_message is not None
1436 assert (
1437 f"Reuse the existing `{display_runtime_path(index_path)}` head/style/container pattern "
1438 "for this chapter so the guide stays visually consistent; only adapt the title, heading, "
1439 "and chapter body content."
1440 in decision.retry_message
1441 )
1442 assert (
1443 "If you get stuck, start with `<title>Chapter 1: Introduction to Nginx</title>`, "
1444 "`<h1>Chapter 1: Introduction to Nginx</h1>`, one introductory paragraph, a couple "
1445 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1446 in decision.retry_message
1447 )
1448 assert (
1449 "If blanking continues, use this minimal HTML starter as the `content` value "
1450 "and adapt it:"
1451 in decision.retry_message
1452 )
1453 assert "<title>Chapter 1: Introduction to Nginx</title>" in decision.retry_message
1454 assert '<p><a href="../index.html">← Back to Main Guide Index</a></p>' in decision.retry_message
1455 assert (
1456 f"You already read `{display_runtime_path(reference_chapter)}`; reuse its overall structure "
1457 "as the starting pattern for this new file, then adapt the content to the current target."
1458 not in decision.retry_message
1459 )
1460 assert "Reference cues from" not in decision.retry_message
1461 assert "Write a compact but real initial version of this file now" not in decision.retry_message
1462
1463
1464 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
1465 temp_dir: Path,
1466 ) -> None:
1467 context = build_context(
1468 temp_dir=temp_dir,
1469 use_react=False,
1470 )
1471 repairer = ResponseRepairer(context)
1472
1473 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
1474 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
1475 nginx_root.mkdir(parents=True)
1476 fortran_root.mkdir(parents=True)
1477 reference_index = fortran_root / "index.html"
1478 reference_index.write_text("<html>fortran</html>\n")
1479 output_index = nginx_root / "index.html"
1480
1481 implementation_plan = temp_dir / "implementation.md"
1482 implementation_plan.write_text(
1483 "\n".join(
1484 [
1485 "# Implementation Plan",
1486 "",
1487 "## File Changes",
1488 f"- `{output_index}`",
1489 f"- `{nginx_root / 'chapters'}/`",
1490 f"- `{reference_index}`",
1491 "",
1492 ]
1493 )
1494 )
1495
1496 dod = create_definition_of_done("Create a multi-file nginx guide.")
1497 dod.implementation_plan = str(implementation_plan)
1498 dod.touched_files.append(str(reference_index))
1499 dod.completed_items.append(
1500 "First, examine the existing Fortran guide structure and content"
1501 )
1502 dod.pending_items.append("Develop the nginx index.html file")
1503
1504 decision = repairer.handle_empty_response(
1505 task="Create a multi-file nginx guide.",
1506 original_task=None,
1507 empty_retry_count=2,
1508 max_empty_retries=2,
1509 dod=dod,
1510 )
1511
1512 assert decision.should_continue is True
1513 assert decision.retry_message is not None
1514 assert (
1515 "Prefer one `write(content=...)` call for "
1516 f"`{display_runtime_path(output_index)}` before more research."
1517 in decision.retry_message
1518 )
1519 assert str(reference_index) not in decision.retry_message
1520
1521
1522 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
1523 temp_dir: Path,
1524 ) -> None:
1525 context = build_context(
1526 temp_dir=temp_dir,
1527 use_react=False,
1528 )
1529 repairer = ResponseRepairer(context)
1530
1531 guide_root = temp_dir / "guides" / "nginx"
1532 chapters = guide_root / "chapters"
1533 chapters.mkdir(parents=True)
1534 index_path = guide_root / "index.html"
1535 index_path.write_text(
1536 "\n".join(
1537 [
1538 "<html>",
1539 '<a href="chapters/introduction.html">Introduction</a>',
1540 '<a href="chapters/installation.html">Installation</a>',
1541 "</html>",
1542 ]
1543 )
1544 + "\n"
1545 )
1546
1547 implementation_plan = temp_dir / "implementation.md"
1548 implementation_plan.write_text(
1549 "\n".join(
1550 [
1551 "# Implementation Plan",
1552 "",
1553 "## File Changes",
1554 f"- `{guide_root}/`",
1555 f"- `{chapters}/`",
1556 f"- `{index_path}`",
1557 f"- `{chapters / '02-installation.html'}`",
1558 "",
1559 ]
1560 )
1561 )
1562
1563 dod = create_definition_of_done("Create a multi-file nginx guide.")
1564 dod.implementation_plan = str(implementation_plan)
1565 dod.touched_files.append(str(index_path))
1566 dod.pending_items.append("Write the introduction chapter")
1567
1568 decision = repairer.handle_empty_response(
1569 task="Create a multi-file nginx guide.",
1570 original_task=None,
1571 empty_retry_count=1,
1572 max_empty_retries=2,
1573 dod=dod,
1574 )
1575
1576 assert decision.should_continue is True
1577 assert decision.retry_message is not None
1578 assert "Next missing planned artifact: `introduction.html`" in decision.retry_message
1579 assert (
1580 "Resume with this exact next step: continue `Write the introduction chapter` "
1581 "by creating `introduction.html`."
1582 in decision.retry_message
1583 )
1584 assert "Next declared output under `chapters/`" not in decision.retry_message
1585 assert (
1586 f"Prefer one `write(content=...)` call for `{(chapters / 'introduction.html').resolve(strict=False)}` "
1587 "before more research."
1588 in decision.retry_message
1589 )
1590
1591
1592 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
1593 temp_dir: Path,
1594 ) -> None:
1595 context = build_context(
1596 temp_dir=temp_dir,
1597 use_react=False,
1598 )
1599 repairer = ResponseRepairer(context)
1600
1601 guide_root = temp_dir / "guides" / "nginx"
1602 chapters = guide_root / "chapters"
1603 chapters.mkdir(parents=True)
1604 index_path = guide_root / "index.html"
1605 chapter_one = chapters / "01-introduction.html"
1606 index_path.write_text("<html></html>\n")
1607 chapter_one.write_text("<html></html>\n")
1608
1609 implementation_plan = temp_dir / "implementation.md"
1610 implementation_plan.write_text(
1611 "\n".join(
1612 [
1613 "# Implementation Plan",
1614 "",
1615 "## File Changes",
1616 f"- `{guide_root}/`",
1617 f"- `{chapters}/`",
1618 f"- `{index_path}`",
1619 f"- `{chapters / '02-installation.html'}`",
1620 "",
1621 ]
1622 )
1623 )
1624
1625 dod = create_definition_of_done("Create a multi-file nginx guide.")
1626 dod.implementation_plan = str(implementation_plan)
1627 dod.touched_files.extend([str(index_path), str(chapter_one)])
1628 dod.completed_items.extend(
1629 [
1630 "Create index.html for nginx guide",
1631 "Create first chapter file (01-introduction.html)",
1632 ]
1633 )
1634 dod.pending_items.append("Create second chapter file (02-installation.html)")
1635
1636 decision = repairer.handle_empty_response(
1637 task="Create a multi-file nginx guide.",
1638 original_task=None,
1639 empty_retry_count=2,
1640 max_empty_retries=2,
1641 dod=dod,
1642 )
1643
1644 assert decision.should_continue is True
1645 assert decision.retry_message is not None
1646 assert (
1647 "Resume with this exact next step: continue `Create second chapter file "
1648 "(02-installation.html)` by creating `02-installation.html`."
1649 in decision.retry_message
1650 )
1651 assert (
1652 "Prefer one `write(content=...)` call for "
1653 f"`{display_runtime_path(chapters / '02-installation.html')}` "
1654 "before more research."
1655 in decision.retry_message
1656 )
1657 assert "Do not return another working note or empty response" in decision.retry_message
1658
1659
1660 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
1661 temp_dir: Path,
1662 ) -> None:
1663 context = build_context(
1664 temp_dir=temp_dir,
1665 use_react=False,
1666 )
1667 repairer = ResponseRepairer(context)
1668
1669 guide_root = temp_dir / "guides" / "nginx"
1670 chapters = guide_root / "chapters"
1671 chapters.mkdir(parents=True)
1672 index_path = guide_root / "index.html"
1673 chapter_one = chapters / "01-introduction.html"
1674 index_path.write_text(
1675 "\n".join(
1676 [
1677 "<html>",
1678 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
1679 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1680 "</html>",
1681 ]
1682 )
1683 + "\n"
1684 )
1685 chapter_one.write_text("<html></html>\n")
1686
1687 implementation_plan = temp_dir / "implementation.md"
1688 implementation_plan.write_text(
1689 "\n".join(
1690 [
1691 "# Implementation Plan",
1692 "",
1693 "## File Changes",
1694 f"- `{guide_root}/`",
1695 f"- `{chapters}/`",
1696 f"- `{index_path}`",
1697 f"- `{chapters / '02-installation.html'}`",
1698 "",
1699 ]
1700 )
1701 )
1702
1703 dod = create_definition_of_done("Create a multi-file nginx guide.")
1704 dod.implementation_plan = str(implementation_plan)
1705 dod.touched_files.extend([str(index_path), str(chapter_one)])
1706 dod.completed_items.extend(
1707 [
1708 "Create index.html for nginx guide",
1709 "Create Chapter 1: Introduction to NGINX Tool",
1710 ]
1711 )
1712 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
1713
1714 decision = repairer.handle_empty_response(
1715 task="Create a multi-file nginx guide.",
1716 original_task=None,
1717 empty_retry_count=2,
1718 max_empty_retries=2,
1719 dod=dod,
1720 )
1721
1722 assert decision.should_continue is True
1723 assert decision.retry_message is not None
1724 assert (
1725 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
1726 "by creating `02-installation.html`."
1727 in decision.retry_message
1728 )
1729 assert (
1730 "Prefer one `write(content=...)` call for "
1731 f"`{display_runtime_path(chapters / '02-installation.html')}` "
1732 "before more research."
1733 in decision.retry_message
1734 )
1735 assert (
1736 f'Emit this tool shape now: `write(file_path="{display_runtime_path(chapters / "02-installation.html")}", content="...")`.'
1737 in decision.retry_message
1738 )
1739 assert (
1740 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
1741 "so it matches the current guide structure."
1742 in decision.retry_message
1743 )
1744
1745
1746 def test_late_chapter_retry_reuses_existing_sibling_html_structure(
1747 temp_dir: Path,
1748 ) -> None:
1749 context = build_context(
1750 temp_dir=temp_dir,
1751 use_react=False,
1752 )
1753 repairer = ResponseRepairer(context)
1754
1755 guide_root = temp_dir / "guides" / "nginx"
1756 chapters = guide_root / "chapters"
1757 chapters.mkdir(parents=True)
1758 index_path = guide_root / "index.html"
1759 chapter_one = chapters / "01-introduction.html"
1760 chapter_two = chapters / "02-installation.html"
1761 chapter_three = chapters / "03-basic-configuration.html"
1762 index_path.write_text(
1763 "\n".join(
1764 [
1765 "<html>",
1766 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
1767 '<a href="chapters/02-installation.html">Chapter 2: Installation</a>',
1768 '<a href="chapters/03-basic-configuration.html">Chapter 3: Basic Configuration</a>',
1769 "</html>",
1770 ]
1771 )
1772 + "\n"
1773 )
1774 chapter_one.write_text("<html><body><h1>Chapter 1</h1></body></html>\n")
1775 chapter_two.write_text("<html><body><h1>Chapter 2</h1></body></html>\n")
1776
1777 implementation_plan = temp_dir / "implementation.md"
1778 implementation_plan.write_text(
1779 "\n".join(
1780 [
1781 "# Implementation Plan",
1782 "",
1783 "## File Changes",
1784 f"- `{guide_root}/`",
1785 f"- `{chapters}/`",
1786 f"- `{index_path}`",
1787 f"- `{chapter_one}`",
1788 f"- `{chapter_two}`",
1789 f"- `{chapter_three}`",
1790 "",
1791 ]
1792 )
1793 )
1794
1795 dod = create_definition_of_done("Create a multi-file nginx guide.")
1796 dod.implementation_plan = str(implementation_plan)
1797 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
1798 dod.completed_items.extend(
1799 [
1800 "Create index.html for nginx guide",
1801 "Create first chapter file (01-introduction.html)",
1802 "Create second chapter file (02-installation.html)",
1803 ]
1804 )
1805 dod.pending_items.append("Create third chapter file (03-basic-configuration.html)")
1806
1807 decision = repairer.handle_empty_response(
1808 task="Create a multi-file nginx guide.",
1809 original_task=None,
1810 empty_retry_count=4,
1811 max_empty_retries=2,
1812 dod=dod,
1813 )
1814
1815 assert decision.should_continue is True
1816 assert decision.retry_message is not None
1817 assert (
1818 "Reuse the overall structure and navigation pattern from "
1819 f"`{display_runtime_path(chapter_two)}` as the starting pattern for "
1820 "`Chapter 3: Basic Configuration`; adapt the title, heading, and body content "
1821 "to the new chapter."
1822 in decision.retry_message
1823 )
1824 assert (
1825 "If you get stuck, start with `<title>Chapter 3: Basic Configuration</title>`, "
1826 "`<h1>Chapter 3: Basic Configuration</h1>`, one introductory paragraph, a couple "
1827 "of `<h2>` sections with short body text, and a back link to `../index.html`."
1828 in decision.retry_message
1829 )
1830
1831
1832 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
1833 temp_dir: Path,
1834 ) -> None:
1835 context = build_context(
1836 temp_dir=temp_dir,
1837 use_react=False,
1838 )
1839 repairer = ResponseRepairer(context)
1840
1841 guide_root = temp_dir / "guides" / "nginx"
1842 chapters = guide_root / "chapters"
1843 chapters.mkdir(parents=True)
1844 chapter_one = chapters / "01-introduction.html"
1845 chapter_one.write_text("<html></html>\n")
1846
1847 implementation_plan = temp_dir / "implementation.md"
1848 implementation_plan.write_text(
1849 "\n".join(
1850 [
1851 "# Implementation Plan",
1852 "",
1853 "## File Changes",
1854 f"- `{guide_root}/`",
1855 f"- `{chapters}/`",
1856 f"- `{guide_root / 'index.html'}`",
1857 f"- `{chapters / '01-introduction.html'}`",
1858 "",
1859 ]
1860 )
1861 )
1862
1863 dod = create_definition_of_done("Create a multi-file nginx guide.")
1864 dod.implementation_plan = str(implementation_plan)
1865 dod.touched_files.append(str(chapter_one))
1866 dod.completed_items.append("Create first chapter file (01-introduction.html)")
1867 dod.pending_items.append("Develop the main index.html file for the nginx guide")
1868
1869 recovery_context = RecoveryContext(
1870 original_tool="write",
1871 original_args={
1872 "file_path": "~/Loader/guides/nginx/index.html",
1873 "content_chars": 1354,
1874 "content_lines": 30,
1875 },
1876 )
1877 recovery_context.add_attempt(
1878 "write",
1879 {
1880 "file_path": "~/Loader/guides/nginx/index.html",
1881 "content_chars": 1354,
1882 "content_lines": 30,
1883 },
1884 "WriteTool.execute() missing 1 required positional argument: 'content'",
1885 )
1886 context.recovery_context = recovery_context
1887
1888 decision = repairer.handle_empty_response(
1889 task="Create a multi-file nginx guide.",
1890 original_task=None,
1891 empty_retry_count=2,
1892 max_empty_retries=2,
1893 dod=dod,
1894 )
1895
1896 assert decision.should_continue is True
1897 assert decision.retry_message is not None
1898 assert "resend `write`" in decision.retry_message
1899 assert "content_chars" in decision.retry_message
1900 assert "index.html" in decision.retry_message
1901
1902
1903 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
1904 temp_dir: Path,
1905 ) -> None:
1906 context = build_context(
1907 temp_dir=temp_dir,
1908 use_react=False,
1909 )
1910 repairer = ResponseRepairer(context)
1911
1912 guide_root = temp_dir / "guides" / "nginx"
1913 chapters = guide_root / "chapters"
1914 chapters.mkdir(parents=True)
1915 index_path = guide_root / "index.html"
1916 chapter_one = chapters / "01-introduction.html"
1917 index_path.write_text(
1918 "\n".join(
1919 [
1920 "<html>",
1921 '<a href="chapters/01-introduction.html">Introduction</a>',
1922 '<a href="chapters/02-installation.html">Installation</a>',
1923 "</html>",
1924 ]
1925 )
1926 + "\n"
1927 )
1928 chapter_one.write_text("<html></html>\n")
1929
1930 implementation_plan = temp_dir / "implementation.md"
1931 implementation_plan.write_text(
1932 "\n".join(
1933 [
1934 "# Implementation Plan",
1935 "",
1936 "## File Changes",
1937 f"- `{guide_root}/`",
1938 f"- `{chapters}/`",
1939 f"- `{index_path}`",
1940 f"- `{chapters / '02-installation.html'}`",
1941 "",
1942 ]
1943 )
1944 )
1945
1946 dod = create_definition_of_done("Create a multi-file nginx guide.")
1947 dod.implementation_plan = str(implementation_plan)
1948 dod.touched_files.extend([str(index_path), str(chapter_one)])
1949 dod.completed_items.extend(
1950 [
1951 "Create index.html for nginx guide",
1952 "Create first chapter file (01-introduction.html)",
1953 ]
1954 )
1955 dod.pending_items.append("Create second chapter file (02-installation.html)")
1956
1957 decision = repairer.handle_empty_response(
1958 task="Create a multi-file nginx guide.",
1959 original_task=None,
1960 empty_retry_count=1,
1961 max_empty_retries=2,
1962 dod=dod,
1963 )
1964
1965 assert decision.should_continue is True
1966 assert decision.retry_message is not None
1967 assert "Continue from the exact next step below." in decision.retry_message
1968 assert "Confirmed completed work:" not in decision.retry_message
1969 assert "Next pending item:" not in decision.retry_message
1970 assert (
1971 "Resume with this exact next step: continue `Create second chapter file "
1972 "(02-installation.html)` by creating `02-installation.html`."
1973 in decision.retry_message
1974 )
1975
1976
1977 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
1978 temp_dir: Path,
1979 ) -> None:
1980 context = build_context(
1981 temp_dir=temp_dir,
1982 use_react=False,
1983 )
1984 repairer = ResponseRepairer(context)
1985
1986 guide_root = temp_dir / "guides" / "nginx"
1987 chapters = guide_root / "chapters"
1988 chapters.mkdir(parents=True)
1989 index_path = guide_root / "index.html"
1990 chapter_one = chapters / "01-introduction.html"
1991 index_path.write_text("<html></html>\n")
1992 chapter_one.write_text("<html></html>\n")
1993
1994 implementation_plan = temp_dir / "implementation.md"
1995 implementation_plan.write_text(
1996 "\n".join(
1997 [
1998 "# Implementation Plan",
1999 "",
2000 "## File Changes",
2001 f"- `{guide_root}/`",
2002 f"- `{chapters}/`",
2003 f"- `{index_path}`",
2004 f"- `{chapter_one}`",
2005 f"- `{chapters / '02-installation.html'}`",
2006 "",
2007 ]
2008 )
2009 )
2010
2011 dod = create_definition_of_done("Create a multi-file nginx guide.")
2012 dod.implementation_plan = str(implementation_plan)
2013 dod.touched_files.extend([str(index_path), str(chapter_one)])
2014 dod.completed_items.extend(
2015 [
2016 "Develop the main index.html file for the nginx guide",
2017 "Create first chapter file (01-introduction.html)",
2018 ]
2019 )
2020 dod.pending_items.extend(
2021 [
2022 "Create the nginx directory structure",
2023 "Create second chapter file (02-installation.html)",
2024 ]
2025 )
2026
2027 decision = repairer.handle_empty_response(
2028 task="Create a multi-file nginx guide.",
2029 original_task=None,
2030 empty_retry_count=1,
2031 max_empty_retries=2,
2032 dod=dod,
2033 )
2034
2035 assert decision.should_continue is True
2036 assert decision.retry_message is not None
2037 assert "Create the nginx directory structure" not in decision.retry_message
2038 assert "02-installation.html" in decision.retry_message
2039
2040
2041 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
2042 temp_dir: Path,
2043 ) -> None:
2044 context = build_context(
2045 temp_dir=temp_dir,
2046 use_react=False,
2047 )
2048 repairer = ResponseRepairer(context)
2049
2050 guide_root = temp_dir / "guides" / "nginx"
2051 chapters = guide_root / "chapters"
2052 chapters.mkdir(parents=True)
2053 index_path = guide_root / "index.html"
2054 chapter_one = chapters / "01-getting-started.html"
2055 chapter_two = chapters / "02-installation.html"
2056 chapter_three = chapters / "03-first-website.html"
2057 chapter_four = chapters / "04-configuration-basics.html"
2058 index_path.write_text("<html></html>\n")
2059 chapter_one.write_text("<h1>One</h1>\n")
2060 chapter_two.write_text("<h1>Two</h1>\n")
2061 chapter_three.write_text("<h1>Three</h1>\n")
2062
2063 implementation_plan = temp_dir / "implementation.md"
2064 implementation_plan.write_text(
2065 "\n".join(
2066 [
2067 "# Implementation Plan",
2068 "",
2069 "## File Changes",
2070 f"- `{guide_root}/`",
2071 f"- `{chapters}/`",
2072 f"- `{index_path}`",
2073 f"- `{chapter_one}`",
2074 f"- `{chapter_two}`",
2075 f"- `{chapter_three}`",
2076 f"- `{chapter_four}`",
2077 "",
2078 ]
2079 )
2080 )
2081
2082 dod = create_definition_of_done("Create a multi-file nginx guide.")
2083 dod.implementation_plan = str(implementation_plan)
2084 dod.touched_files.extend(
2085 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
2086 )
2087 dod.completed_items.extend(
2088 [
2089 "Create the directory structure for the new nginx guide",
2090 "Create the main index.html file with proper structure",
2091 ]
2092 )
2093 dod.pending_items.append("Create each chapter file in sequence")
2094
2095 decision = repairer.handle_empty_response(
2096 task="Create a multi-file nginx guide.",
2097 original_task=None,
2098 empty_retry_count=5,
2099 max_empty_retries=2,
2100 dod=dod,
2101 )
2102
2103 assert decision.should_continue is False
2104 assert decision.final_response is not None
2105 assert "retrying 4 times" in decision.final_response
2106
2107
2108 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
2109 temp_dir: Path,
2110 ) -> None:
2111 context = build_context(
2112 temp_dir=temp_dir,
2113 use_react=False,
2114 )
2115 repairer = ResponseRepairer(context)
2116
2117 guide_root = temp_dir / "guides" / "nginx"
2118 chapters = guide_root / "chapters"
2119 chapters.mkdir(parents=True)
2120 implementation_plan = temp_dir / "implementation.md"
2121 implementation_plan.write_text(
2122 "\n".join(
2123 [
2124 "# Implementation Plan",
2125 "",
2126 "## File Changes",
2127 f"- `{guide_root / 'index.html'}`",
2128 f"- `{chapters / '01-getting-started.html'}`",
2129 f"- `{chapters / '02-installation.html'}`",
2130 "",
2131 ]
2132 )
2133 )
2134
2135 dod = create_definition_of_done("Create a multi-file nginx guide.")
2136 dod.implementation_plan = str(implementation_plan)
2137 dod.touched_files.extend(
2138 [
2139 str(guide_root / "index.html"),
2140 str(chapters / "01-getting-started.html"),
2141 ]
2142 )
2143 dod.completed_items.extend(
2144 [
2145 "Create the directory structure for the new nginx guide",
2146 "Create the main index.html file with proper structure",
2147 ]
2148 )
2149 dod.pending_items.append("Create each chapter file in sequence")
2150
2151 decision = repairer.handle_empty_response(
2152 task="Create a multi-file nginx guide.",
2153 original_task=None,
2154 empty_retry_count=1,
2155 max_empty_retries=2,
2156 dod=dod,
2157 )
2158
2159 assert decision.retry_message is not None
2160 assert "Continue from the exact next step below." in decision.retry_message
2161 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
2162
2163
2164 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
2165 temp_dir: Path,
2166 ) -> None:
2167 context = build_context(
2168 temp_dir=temp_dir,
2169 use_react=False,
2170 )
2171 repairer = ResponseRepairer(context)
2172
2173 guide_root = temp_dir / "guides" / "nginx"
2174 chapters = guide_root / "chapters"
2175 chapters.mkdir(parents=True)
2176 index_path = guide_root / "index.html"
2177 chapter_one = chapters / "01-getting-started.html"
2178 chapter_two = chapters / "02-installation.html"
2179 chapter_three = chapters / "03-first-website.html"
2180 index_path.write_text("<html></html>\n")
2181 chapter_one.write_text("<h1>One</h1>\n")
2182 chapter_two.write_text("<h1>Two</h1>\n")
2183
2184 implementation_plan = temp_dir / "implementation.md"
2185 implementation_plan.write_text(
2186 "\n".join(
2187 [
2188 "# Implementation Plan",
2189 "",
2190 "## File Changes",
2191 f"- `{guide_root}/`",
2192 f"- `{chapters}/`",
2193 f"- `{index_path}`",
2194 f"- `{chapter_one}`",
2195 f"- `{chapter_two}`",
2196 f"- `{chapter_three}`",
2197 "",
2198 ]
2199 )
2200 )
2201
2202 dod = create_definition_of_done("Create a multi-file nginx guide.")
2203 dod.implementation_plan = str(implementation_plan)
2204 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
2205 dod.completed_items.extend(
2206 [
2207 "Create the main index.html file with proper structure",
2208 "Link all chapters together properly",
2209 ]
2210 )
2211 dod.pending_items.append("Create each chapter file in sequence")
2212
2213 decision = repairer.handle_empty_response(
2214 task="Create a multi-file nginx guide.",
2215 original_task=None,
2216 empty_retry_count=1,
2217 max_empty_retries=2,
2218 dod=dod,
2219 )
2220
2221 assert decision.retry_message is not None
2222 assert "Link all chapters together properly" not in decision.retry_message
2223 assert "Continue from the exact next step below." in decision.retry_message
2224 assert "Resume with this exact next step:" in decision.retry_message
2225
2226
2227 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
2228 temp_dir: Path,
2229 ) -> None:
2230 context = build_context(
2231 temp_dir=temp_dir,
2232 use_react=False,
2233 )
2234 repairer = ResponseRepairer(context)
2235
2236 reference_chapters = temp_dir / "fortran" / "chapters"
2237 reference_chapters.mkdir(parents=True)
2238 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
2239
2240 guide_root = temp_dir / "guides" / "nginx"
2241 chapters = guide_root / "chapters"
2242 chapters.mkdir(parents=True)
2243 index_path = guide_root / "index.html"
2244 index_path.write_text("<html></html>\n")
2245
2246 implementation_plan = temp_dir / "implementation.md"
2247 implementation_plan.write_text(
2248 "\n".join(
2249 [
2250 "# Implementation Plan",
2251 "",
2252 "## File Changes",
2253 f"- `{guide_root}/`",
2254 f"- `{chapters}/`",
2255 f"- `{index_path}`",
2256 "",
2257 ]
2258 )
2259 )
2260
2261 dod = create_definition_of_done("Create a multi-file nginx guide.")
2262 dod.implementation_plan = str(implementation_plan)
2263 dod.touched_files.append(str(index_path))
2264 dod.pending_items.append("Write the introduction chapter")
2265 context.session.append(
2266 Message(
2267 role=Role.ASSISTANT,
2268 content="",
2269 tool_calls=[
2270 ToolCall(
2271 id="read-ref-1",
2272 name="read",
2273 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
2274 )
2275 ],
2276 )
2277 )
2278
2279 decision = repairer.handle_empty_response(
2280 task="Create a multi-file nginx guide.",
2281 original_task=None,
2282 empty_retry_count=1,
2283 max_empty_retries=2,
2284 dod=dod,
2285 )
2286
2287 assert decision.should_continue is True
2288 assert decision.retry_message is not None
2289 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
2290 assert (
2291 "Resume with this exact next step: continue `Write the introduction chapter` "
2292 "by creating `01-introduction.html`."
2293 in decision.retry_message
2294 )
2295 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
2296 assert (
2297 "It mirrors the observed filename pattern from another `chapters/` directory "
2298 "you already inspected."
2299 in decision.retry_message
2300 )