Python · 53050 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 from loader.llm.base import Message, Role, ToolCall
10 from loader.runtime.context import RuntimeContext
11 from loader.runtime.dod import create_definition_of_done
12 from loader.runtime.permissions import (
13 PermissionMode,
14 build_permission_policy,
15 load_permission_rules,
16 )
17 from loader.runtime.recovery import RecoveryContext
18 from loader.runtime.repair import ResponseRepairer
19 from loader.tools.base import create_default_registry
20 from tests.helpers.runtime_harness import ScriptedBackend
21
22
23 class FakeSession:
24 def __init__(self) -> None:
25 self.messages = []
26
27 def append(self, message) -> None:
28 self.messages.append(message)
29
30
31 class FakeCodeFilter:
32 def reset(self) -> None:
33 return None
34
35
36 class FakeSafeguards:
37 def __init__(self) -> None:
38 self.action_tracker = object()
39 self.validator = object()
40 self.code_filter = FakeCodeFilter()
41
42 def filter_stream_chunk(self, content: str) -> str:
43 return content
44
45 def filter_complete_content(self, content: str) -> str:
46 return content
47
48 def should_steer(self) -> bool:
49 return False
50
51 def get_steering_message(self) -> str | None:
52 return None
53
54 def record_response(self, content: str) -> None:
55 return None
56
57 def detect_text_loop(self, content: str) -> tuple[bool, str]:
58 return False, ""
59
60 def detect_loop(self) -> tuple[bool, str]:
61 return False, ""
62
63
64 def build_context(
65 *,
66 temp_dir: Path,
67 use_react: bool,
68 ) -> RuntimeContext:
69 registry = create_default_registry(temp_dir)
70 registry.configure_workspace_root(temp_dir)
71 rule_status = load_permission_rules(temp_dir)
72 policy = build_permission_policy(
73 active_mode=PermissionMode.WORKSPACE_WRITE,
74 workspace_root=temp_dir,
75 tool_requirements=registry.get_tool_requirements(),
76 rules=rule_status.rules,
77 )
78 session = FakeSession()
79 return RuntimeContext(
80 project_root=temp_dir,
81 backend=ScriptedBackend(),
82 registry=registry,
83 session=session, # type: ignore[arg-type]
84 config=SimpleNamespace(force_react=use_react),
85 capability_profile=SimpleNamespace(supports_native_tools=not use_react), # type: ignore[arg-type]
86 project_context=None,
87 permission_policy=policy,
88 permission_config_status=rule_status,
89 workflow_mode="execute",
90 safeguards=FakeSafeguards(),
91 )
92
93
94 def test_response_repairer_uses_runtime_parser_for_bracket_tool_fallback(
95 temp_dir: Path,
96 ) -> None:
97 context = build_context(
98 temp_dir=temp_dir,
99 use_react=False,
100 )
101 repairer = ResponseRepairer(context)
102
103 analysis = repairer.analyze_response(
104 content="I need clarification.",
105 response_content='[calls askuserquestion tool with: question="Which path?"]',
106 tool_calls=[],
107 extracted_iterations=0,
108 max_extracted_iterations=3,
109 )
110
111 assert analysis.tool_calls == [
112 ToolCall(
113 id="call_0",
114 name="AskUserQuestion",
115 arguments={"question": "Which path?"},
116 )
117 ]
118 assert analysis.tool_source == "raw_text"
119 assert analysis.clear_stream is True
120
121
122 def test_response_repairer_recovers_todowrite_from_runtime_registry(
123 temp_dir: Path,
124 ) -> None:
125 context = build_context(
126 temp_dir=temp_dir,
127 use_react=False,
128 )
129 repairer = ResponseRepairer(context)
130
131 analysis = repairer.analyze_response(
132 content="I'll track the work first.",
133 response_content=json.dumps(
134 {
135 "name": "TodoWrite",
136 "arguments": {
137 "todos": [
138 {
139 "content": "Run tests",
140 "active_form": "Running tests",
141 "status": "in_progress",
142 }
143 ]
144 },
145 }
146 ),
147 tool_calls=[],
148 extracted_iterations=0,
149 max_extracted_iterations=3,
150 )
151
152 assert analysis.tool_source == "raw_text"
153 assert analysis.clear_stream is True
154 assert analysis.tool_calls == [
155 ToolCall(
156 id="call_0",
157 name="TodoWrite",
158 arguments={
159 "todos": [
160 {
161 "content": "Run tests",
162 "active_form": "Running tests",
163 "status": "in_progress",
164 }
165 ]
166 },
167 )
168 ]
169
170
171 def test_response_repairer_fails_honestly_when_raw_tool_budget_is_exhausted(
172 temp_dir: Path,
173 ) -> None:
174 context = build_context(
175 temp_dir=temp_dir,
176 use_react=False,
177 )
178 repairer = ResponseRepairer(context)
179
180 analysis = repairer.analyze_response(
181 content=json.dumps(
182 {
183 "name": "read",
184 "arguments": {"file_path": "README.md"},
185 }
186 ),
187 response_content=json.dumps(
188 {
189 "name": "read",
190 "arguments": {"file_path": "README.md"},
191 }
192 ),
193 tool_calls=[],
194 extracted_iterations=3,
195 max_extracted_iterations=3,
196 )
197
198 assert analysis.should_stop is True
199 assert analysis.final_response == (
200 "I couldn't safely continue because the model kept emitting raw-text "
201 "tool calls instead of proper tool invocations. Please try again or "
202 "switch to a different backend/model."
203 )
204 assert analysis.failure == "raw-text tool recovery budget exhausted"
205 assert "Let me know if you'd like me to continue" not in analysis.final_response
206
207
208 def test_empty_response_retry_message_surfaces_missing_planned_artifacts_and_working_note(
209 temp_dir: Path,
210 ) -> None:
211 context = build_context(
212 temp_dir=temp_dir,
213 use_react=False,
214 )
215 repairer = ResponseRepairer(context)
216 implementation_plan = temp_dir / "implementation.md"
217 implementation_plan.write_text(
218 "\n".join(
219 [
220 "# Implementation Plan",
221 "",
222 "## File Changes",
223 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
224 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
225 "",
226 ]
227 )
228 )
229 first_artifact = temp_dir / "guides" / "nginx" / "index.html"
230 first_artifact.parent.mkdir(parents=True)
231 first_artifact.write_text("<html></html>\n")
232
233 dod = create_definition_of_done("Create a multi-file nginx guide.")
234 dod.implementation_plan = str(implementation_plan)
235 dod.touched_files.append(str(first_artifact))
236 dod.completed_items.append("Create the main index.html file")
237 dod.pending_items.append("Create each chapter file in sequence")
238
239 context.session.append(
240 SimpleNamespace(
241 role="tool",
242 content=(
243 "Observation [notepad_write_working]: Result: "
244 "- [2026-04-21T19:17:34Z] Creating fifth chapter file: Advanced configurations"
245 ),
246 )
247 )
248
249 decision = repairer.handle_empty_response(
250 task="Create a multi-file nginx guide.",
251 original_task=None,
252 empty_retry_count=1,
253 max_empty_retries=2,
254 dod=dod,
255 )
256
257 assert decision.should_continue is True
258 assert decision.retry_message is not None
259 assert "Latest working note: Creating fifth chapter file: Advanced configurations" in decision.retry_message
260 assert "Confirmed touched files: `index.html`" in decision.retry_message
261 assert "Confirmed completed work: Create the main index.html file" in decision.retry_message
262 assert "Next pending item: Create each chapter file in sequence" in decision.retry_message
263 assert "Continue from the confirmed progress below instead of restarting." in decision.retry_message
264
265
266 def test_empty_response_retry_mentions_write_can_create_missing_parent_directories(
267 temp_dir: Path,
268 ) -> None:
269 context = build_context(
270 temp_dir=temp_dir,
271 use_react=False,
272 )
273 repairer = ResponseRepairer(context)
274
275 guide_root = temp_dir / "guides" / "nginx"
276 index_path = guide_root / "index.html"
277
278 implementation_plan = temp_dir / "implementation.md"
279 implementation_plan.write_text(
280 "\n".join(
281 [
282 "# Implementation Plan",
283 "",
284 "## File Changes",
285 f"- `{index_path}`",
286 "",
287 ]
288 )
289 )
290
291 dod = create_definition_of_done("Create a multi-file nginx guide.")
292 dod.implementation_plan = str(implementation_plan)
293 dod.pending_items.extend(
294 [
295 "Create nginx guide directory structure",
296 "Write main index.html for nginx guide",
297 ]
298 )
299
300 decision = repairer.handle_empty_response(
301 task="Create a multi-file nginx guide.",
302 original_task=None,
303 empty_retry_count=1,
304 max_empty_retries=2,
305 dod=dod,
306 )
307
308 assert decision.should_continue is True
309 assert decision.retry_message is not None
310 assert (
311 "Resume with this exact next step: continue `Write main index.html for nginx guide` "
312 "by creating `index.html`."
313 in decision.retry_message
314 )
315 assert (
316 f"Prefer one `write(content=...)` call for `{index_path}` before more research."
317 in decision.retry_message
318 )
319 assert (
320 f'Emit this tool shape now: `write(file_path="{index_path.resolve(strict=False)}", content="...")`.'
321 in decision.retry_message
322 )
323 assert (
324 "Do not restart discovery unless one specific missing fact blocks that file write."
325 in decision.retry_message
326 )
327
328
329 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
330 temp_dir: Path,
331 ) -> None:
332 context = build_context(
333 temp_dir=temp_dir,
334 use_react=False,
335 )
336 repairer = ResponseRepairer(context)
337
338 guide_root = temp_dir / "guides" / "nginx"
339 chapters = guide_root / "chapters"
340 chapters.mkdir(parents=True)
341 index_path = guide_root / "index.html"
342 first_chapter = chapters / "01-introduction.html"
343 second_chapter = chapters / "02-installation.html"
344 index_path.write_text("<html></html>\n")
345 first_chapter.write_text("<h1>Intro</h1>\n")
346
347 implementation_plan = temp_dir / "implementation.md"
348 implementation_plan.write_text(
349 "\n".join(
350 [
351 "# Implementation Plan",
352 "",
353 "## File Changes",
354 f"- `{index_path}`",
355 f"- `{first_chapter}`",
356 f"- `{second_chapter}`",
357 "",
358 ]
359 )
360 )
361
362 dod = create_definition_of_done("Create a multi-file nginx guide.")
363 dod.implementation_plan = str(implementation_plan)
364 dod.touched_files.extend([str(index_path), str(first_chapter)])
365 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
366
367 context.recovery_context = RecoveryContext(
368 original_tool="write",
369 original_args={"file_path": "", "content": "<html></html>\n"},
370 )
371 context.recovery_context.add_attempt(
372 "write",
373 {"file_path": "", "content": "<html></html>\n"},
374 "Empty file path",
375 )
376
377 decision = repairer.handle_empty_response(
378 task="Create a multi-file nginx guide.",
379 original_task=None,
380 empty_retry_count=1,
381 max_empty_retries=2,
382 dod=dod,
383 )
384
385 assert decision.should_continue is True
386 assert decision.retry_message is not None
387 assert (
388 f"Last tool failure: resend `write` for `{second_chapter}` with a valid `file_path` and real `content`."
389 in decision.retry_message
390 )
391 assert "Do not leave `file_path` empty" in decision.retry_message
392 assert (
393 f'Emit this tool shape now: `write(file_path="{second_chapter.resolve(strict=False)}", content="...")`.'
394 in decision.retry_message
395 )
396
397
398 def test_empty_response_retry_respects_discovery_first_pending_step(
399 temp_dir: Path,
400 ) -> None:
401 context = build_context(
402 temp_dir=temp_dir,
403 use_react=False,
404 )
405 repairer = ResponseRepairer(context)
406
407 implementation_plan = temp_dir / "implementation.md"
408 implementation_plan.write_text(
409 "\n".join(
410 [
411 "# Implementation Plan",
412 "",
413 "## File Changes",
414 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
415 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
416 "",
417 ]
418 )
419 )
420
421 dod = create_definition_of_done("Create a multi-file nginx guide.")
422 dod.implementation_plan = str(implementation_plan)
423 dod.pending_items.extend(
424 [
425 "First, examine the existing fortran guide structure and content to understand the format",
426 "Create the nginx directory structure",
427 "Develop the main index.html file for the nginx guide",
428 ]
429 )
430
431 context.session.append(
432 SimpleNamespace(
433 role="tool",
434 content=(
435 "Observation [notepad_write_working]: Result: "
436 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
437 ),
438 )
439 )
440
441 decision = repairer.handle_empty_response(
442 task="Create a multi-file nginx guide.",
443 original_task=None,
444 empty_retry_count=1,
445 max_empty_retries=2,
446 dod=dod,
447 )
448
449 assert decision.should_continue is True
450 assert decision.retry_message is not None
451 assert (
452 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
453 in decision.retry_message
454 )
455 assert "one concrete evidence-gathering tool call" in decision.retry_message
456 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
457
458
459 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
460 temp_dir: Path,
461 ) -> None:
462 context = build_context(
463 temp_dir=temp_dir,
464 use_react=False,
465 )
466 repairer = ResponseRepairer(context)
467
468 guide_root = temp_dir / "guides" / "nginx"
469 chapters = guide_root / "chapters"
470 chapters.mkdir(parents=True)
471 index_path = guide_root / "index.html"
472 chapter_one = chapters / "01-getting-started.html"
473 chapter_two = chapters / "02-installation.html"
474 chapter_three = chapters / "03-first-website.html"
475 chapter_four = chapters / "04-configuration-basics.html"
476 index_path.write_text("<html></html>\n")
477 chapter_one.write_text("<h1>One</h1>\n")
478 chapter_two.write_text("<h1>Two</h1>\n")
479 chapter_three.write_text("<h1>Three</h1>\n")
480
481 implementation_plan = temp_dir / "implementation.md"
482 implementation_plan.write_text(
483 "\n".join(
484 [
485 "# Implementation Plan",
486 "",
487 "## File Changes",
488 f"- `{guide_root}/`",
489 f"- `{chapters}/`",
490 f"- `{index_path}`",
491 f"- `{chapter_one}`",
492 f"- `{chapter_two}`",
493 f"- `{chapter_three}`",
494 f"- `{chapter_four}`",
495 "",
496 ]
497 )
498 )
499
500 dod = create_definition_of_done("Create a multi-file nginx guide.")
501 dod.implementation_plan = str(implementation_plan)
502 dod.touched_files.extend(
503 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
504 )
505 dod.completed_items.extend(
506 [
507 "Create the directory structure for the new nginx guide",
508 "Create the main index.html file with proper structure",
509 ]
510 )
511 dod.pending_items.append("Create each chapter file in sequence")
512
513 decision = repairer.handle_empty_response(
514 task="Create a multi-file nginx guide.",
515 original_task=None,
516 empty_retry_count=3,
517 max_empty_retries=2,
518 dod=dod,
519 )
520
521 assert decision.should_continue is True
522 assert decision.retry_message is not None
523 assert "retry 3/4" in decision.retry_message
524 assert "Follow the same one-file-at-a-time mutation pattern" in decision.retry_message
525
526
527 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
528 temp_dir: Path,
529 ) -> None:
530 context = build_context(
531 temp_dir=temp_dir,
532 use_react=False,
533 )
534 repairer = ResponseRepairer(context)
535
536 implementation_plan = temp_dir / "implementation.md"
537 implementation_plan.write_text(
538 "\n".join(
539 [
540 "# Implementation Plan",
541 "",
542 "## File Changes",
543 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
544 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
545 "",
546 ]
547 )
548 )
549
550 dod = create_definition_of_done("Create a multi-file nginx guide.")
551 dod.implementation_plan = str(implementation_plan)
552 dod.pending_items.append("Develop the main index.html file for the nginx guide")
553
554 decision = repairer.handle_empty_response(
555 task="Create a multi-file nginx guide.",
556 original_task=None,
557 empty_retry_count=3,
558 max_empty_retries=2,
559 dod=dod,
560 )
561
562 assert decision.should_continue is True
563 assert decision.retry_message is not None
564 assert "retry 3/4" in decision.retry_message
565 assert "Next missing planned artifact: `index.html`" in decision.retry_message
566 assert (
567 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
568 "by creating `index.html`."
569 in decision.retry_message
570 )
571
572
573 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
574 temp_dir: Path,
575 ) -> None:
576 context = build_context(
577 temp_dir=temp_dir,
578 use_react=False,
579 )
580 repairer = ResponseRepairer(context)
581
582 guide_root = temp_dir / "guides" / "nginx"
583 chapters = guide_root / "chapters"
584 guide_root.mkdir(parents=True)
585 chapters.mkdir()
586 index_path = guide_root / "index.html"
587 index_path.write_text("<html></html>\n")
588
589 implementation_plan = temp_dir / "implementation.md"
590 implementation_plan.write_text(
591 "\n".join(
592 [
593 "# Implementation Plan",
594 "",
595 "## File Changes",
596 f"- `{chapters}/`",
597 f"- `{index_path}`",
598 "",
599 ]
600 )
601 )
602
603 dod = create_definition_of_done("Create a multi-file nginx guide.")
604 dod.implementation_plan = str(implementation_plan)
605 dod.touched_files.append(str(index_path))
606 dod.completed_items.extend(
607 [
608 "Create the new nginx guide directory structure",
609 "Develop the main index.html file with proper structure",
610 ]
611 )
612 dod.pending_items.append("Create 01-introduction.html")
613
614 decision = repairer.handle_empty_response(
615 task="Create a multi-file nginx guide.",
616 original_task=None,
617 empty_retry_count=5,
618 max_empty_retries=2,
619 dod=dod,
620 )
621
622 assert decision.should_continue is True
623 assert decision.retry_message is not None
624 assert "retry 5/6" in decision.retry_message
625 assert "01-introduction.html" in decision.retry_message
626
627
628 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
629 temp_dir: Path,
630 ) -> None:
631 context = build_context(
632 temp_dir=temp_dir,
633 use_react=False,
634 )
635 context.session.messages.append(
636 SimpleNamespace(
637 content=(
638 "Observation [notepad_write_working]: Result: "
639 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
640 )
641 )
642 )
643 repairer = ResponseRepairer(context)
644
645 guide_root = temp_dir / "guides" / "nginx"
646 chapters = guide_root / "chapters"
647 chapters.mkdir(parents=True)
648 index_path = guide_root / "index.html"
649 chapter_one = chapters / "01-getting-started.html"
650 chapter_two = chapters / "02-installation.html"
651 chapter_three = chapters / "03-first-website.html"
652 chapter_four = chapters / "04-configuration-basics.html"
653 chapter_five = chapters / "05-advanced-features.html"
654 index_path.write_text("<html></html>\n")
655 chapter_one.write_text("<h1>One</h1>\n")
656 chapter_two.write_text("<h1>Two</h1>\n")
657 chapter_three.write_text("<h1>Three</h1>\n")
658 chapter_four.write_text("<h1>Four</h1>\n")
659
660 implementation_plan = temp_dir / "implementation.md"
661 implementation_plan.write_text(
662 "\n".join(
663 [
664 "# Implementation Plan",
665 "",
666 "## File Changes",
667 f"- `{guide_root}/`",
668 f"- `{chapters}/`",
669 f"- `{index_path}`",
670 f"- `{chapter_one}`",
671 f"- `{chapter_two}`",
672 f"- `{chapter_three}`",
673 f"- `{chapter_four}`",
674 f"- `{chapter_five}`",
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.touched_files.extend(
683 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
684 )
685 dod.completed_items.extend(
686 [
687 "Create the directory structure for the new nginx guide",
688 "Create the main index.html file with proper structure",
689 ]
690 )
691 dod.pending_items.append("Create each chapter file in sequence")
692
693 decision = repairer.handle_empty_response(
694 task="Create a multi-file nginx guide.",
695 original_task=None,
696 empty_retry_count=3,
697 max_empty_retries=2,
698 dod=dod,
699 )
700
701 assert decision.should_continue is True
702 assert decision.retry_message is not None
703 assert "Continue from the exact next step below." in decision.retry_message
704 assert "Latest working note:" not in decision.retry_message
705 assert "Confirmed completed work:" not in decision.retry_message
706 assert "Next pending item:" not in decision.retry_message
707
708
709 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
710 temp_dir: Path,
711 ) -> None:
712 context = build_context(
713 temp_dir=temp_dir,
714 use_react=False,
715 )
716 repairer = ResponseRepairer(context)
717
718 guide_root = temp_dir / "guides" / "nginx"
719 chapters = guide_root / "chapters"
720 chapters.mkdir(parents=True)
721 index_path = guide_root / "index.html"
722 index_path.write_text("<html></html>\n")
723
724 implementation_plan = temp_dir / "implementation.md"
725 implementation_plan.write_text(
726 "\n".join(
727 [
728 "# Implementation Plan",
729 "",
730 "## File Changes",
731 f"- `{guide_root}/`",
732 f"- `{chapters}/`",
733 f"- `{index_path}`",
734 f"- `{chapters / '02-installation.html'}`",
735 "",
736 ]
737 )
738 )
739
740 dod = create_definition_of_done("Create a multi-file nginx guide.")
741 dod.implementation_plan = str(implementation_plan)
742 dod.touched_files.append(str(index_path))
743 dod.pending_items.append("Write the introduction chapter")
744
745 decision = repairer.handle_empty_response(
746 task="Create a multi-file nginx guide.",
747 original_task=None,
748 empty_retry_count=1,
749 max_empty_retries=2,
750 dod=dod,
751 )
752
753 assert decision.should_continue is True
754 assert decision.retry_message is not None
755 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
756 assert (
757 "Resume with this exact next step: continue `Write the introduction chapter` "
758 "by creating the next output file under `chapters/`."
759 in decision.retry_message
760 )
761 assert (
762 f"Prefer one concrete `write` call for a file inside `{chapters}` before more research."
763 in decision.retry_message
764 )
765
766
767 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
768 temp_dir: Path,
769 ) -> None:
770 context = build_context(
771 temp_dir=temp_dir,
772 use_react=False,
773 )
774 repairer = ResponseRepairer(context)
775
776 guide_root = temp_dir / "guides" / "nginx"
777 chapters = guide_root / "chapters"
778 guide_root.mkdir(parents=True)
779 chapters.mkdir()
780 chapter_one = chapters / "01-introduction.html"
781 index_path = guide_root / "index.html"
782
783 implementation_plan = temp_dir / "implementation.md"
784 implementation_plan.write_text(
785 "\n".join(
786 [
787 "# Implementation Plan",
788 "",
789 "## File Changes",
790 f"- `{guide_root}/`",
791 f"- `{index_path}`",
792 f"- `{chapters}/`",
793 f"- `{chapter_one}`",
794 "",
795 ]
796 )
797 )
798
799 dod = create_definition_of_done("Create a multi-file nginx guide.")
800 dod.implementation_plan = str(implementation_plan)
801 dod.completed_items.extend(
802 [
803 "First, examine the existing Fortran guide structure to understand the format and depth",
804 "Create the new nginx guide directory structure",
805 ]
806 )
807 dod.pending_items.append("Develop the main index.html file with proper structure")
808
809 decision = repairer.handle_empty_response(
810 task="Create a multi-file nginx guide.",
811 original_task=None,
812 empty_retry_count=2,
813 max_empty_retries=2,
814 dod=dod,
815 )
816
817 assert decision.should_continue is True
818 assert decision.retry_message is not None
819 assert (
820 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
821 in decision.retry_message
822 )
823 assert "Next missing planned artifact: `index.html`" in decision.retry_message
824 assert "Prefer one `write(content=...)` call" in decision.retry_message
825 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
826
827
828 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
829 temp_dir: Path,
830 ) -> None:
831 context = build_context(
832 temp_dir=temp_dir,
833 use_react=False,
834 )
835 repairer = ResponseRepairer(context)
836
837 guide_root = temp_dir / "guides" / "nginx"
838 chapters = guide_root / "chapters"
839 guide_root.mkdir(parents=True)
840 chapters.mkdir()
841 index_path = guide_root / "index.html"
842 chapter_one = chapters / "01-introduction.html"
843
844 implementation_plan = temp_dir / "implementation.md"
845 implementation_plan.write_text(
846 "\n".join(
847 [
848 "# Implementation Plan",
849 "",
850 "## File Changes",
851 f"- `{guide_root}/`",
852 f"- `{chapters}/`",
853 f"- `{index_path}`",
854 f"- `{chapter_one}`",
855 "",
856 ]
857 )
858 )
859
860 dod = create_definition_of_done("Create a multi-file nginx guide.")
861 dod.implementation_plan = str(implementation_plan)
862 dod.completed_items.extend(
863 [
864 "First, examine the existing Fortran guide structure to understand the format and depth",
865 "Create the new nginx guide directory structure",
866 ]
867 )
868 dod.pending_items.append("Develop the main index.html file with proper structure")
869
870 decision = repairer.handle_empty_response(
871 task="Create a multi-file nginx guide.",
872 original_task=None,
873 empty_retry_count=4,
874 max_empty_retries=4,
875 dod=dod,
876 )
877
878 assert decision.should_continue is True
879 assert decision.retry_message is not None
880 assert "Next missing planned artifact: `index.html`" in decision.retry_message
881 assert (
882 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
883 "by creating `index.html`."
884 in decision.retry_message
885 )
886 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
887
888
889 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
890 temp_dir: Path,
891 ) -> None:
892 context = build_context(
893 temp_dir=temp_dir,
894 use_react=False,
895 )
896 repairer = ResponseRepairer(context)
897
898 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
899 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
900 nginx_root.mkdir(parents=True)
901 fortran_root.mkdir(parents=True)
902 reference_index = fortran_root / "index.html"
903 reference_index.write_text("<html>fortran</html>\n")
904 output_index = nginx_root / "index.html"
905
906 implementation_plan = temp_dir / "implementation.md"
907 implementation_plan.write_text(
908 "\n".join(
909 [
910 "# Implementation Plan",
911 "",
912 "## File Changes",
913 f"- `{output_index}`",
914 f"- `{nginx_root / 'chapters'}/`",
915 f"- `{reference_index}`",
916 "",
917 ]
918 )
919 )
920
921 dod = create_definition_of_done("Create a multi-file nginx guide.")
922 dod.implementation_plan = str(implementation_plan)
923 dod.touched_files.append(str(reference_index))
924 dod.completed_items.append(
925 "First, examine the existing Fortran guide structure and content"
926 )
927 dod.pending_items.append("Develop the nginx index.html file")
928
929 decision = repairer.handle_empty_response(
930 task="Create a multi-file nginx guide.",
931 original_task=None,
932 empty_retry_count=2,
933 max_empty_retries=2,
934 dod=dod,
935 )
936
937 assert decision.should_continue is True
938 assert decision.retry_message is not None
939 assert (
940 f"Prefer one `write(content=...)` call for `{output_index}` before more research."
941 in decision.retry_message
942 )
943 assert str(reference_index) not in decision.retry_message
944
945
946 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
947 temp_dir: Path,
948 ) -> None:
949 context = build_context(
950 temp_dir=temp_dir,
951 use_react=False,
952 )
953 repairer = ResponseRepairer(context)
954
955 guide_root = temp_dir / "guides" / "nginx"
956 chapters = guide_root / "chapters"
957 chapters.mkdir(parents=True)
958 index_path = guide_root / "index.html"
959 index_path.write_text(
960 "\n".join(
961 [
962 "<html>",
963 '<a href="chapters/introduction.html">Introduction</a>',
964 '<a href="chapters/installation.html">Installation</a>',
965 "</html>",
966 ]
967 )
968 + "\n"
969 )
970
971 implementation_plan = temp_dir / "implementation.md"
972 implementation_plan.write_text(
973 "\n".join(
974 [
975 "# Implementation Plan",
976 "",
977 "## File Changes",
978 f"- `{guide_root}/`",
979 f"- `{chapters}/`",
980 f"- `{index_path}`",
981 f"- `{chapters / '02-installation.html'}`",
982 "",
983 ]
984 )
985 )
986
987 dod = create_definition_of_done("Create a multi-file nginx guide.")
988 dod.implementation_plan = str(implementation_plan)
989 dod.touched_files.append(str(index_path))
990 dod.pending_items.append("Write the introduction chapter")
991
992 decision = repairer.handle_empty_response(
993 task="Create a multi-file nginx guide.",
994 original_task=None,
995 empty_retry_count=1,
996 max_empty_retries=2,
997 dod=dod,
998 )
999
1000 assert decision.should_continue is True
1001 assert decision.retry_message is not None
1002 assert "Next missing planned artifact: `introduction.html`" in decision.retry_message
1003 assert "Next declared output under `chapters/`: `introduction.html`" in decision.retry_message
1004 assert (
1005 "Resume with this exact next step: continue `Write the introduction chapter` "
1006 "by creating `introduction.html`."
1007 in decision.retry_message
1008 )
1009 assert (
1010 f"Prefer one `write(content=...)` call for `{(chapters / 'introduction.html').resolve(strict=False)}` "
1011 "before more research."
1012 in decision.retry_message
1013 )
1014
1015
1016 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
1017 temp_dir: Path,
1018 ) -> None:
1019 context = build_context(
1020 temp_dir=temp_dir,
1021 use_react=False,
1022 )
1023 repairer = ResponseRepairer(context)
1024
1025 guide_root = temp_dir / "guides" / "nginx"
1026 chapters = guide_root / "chapters"
1027 chapters.mkdir(parents=True)
1028 index_path = guide_root / "index.html"
1029 chapter_one = chapters / "01-introduction.html"
1030 index_path.write_text("<html></html>\n")
1031 chapter_one.write_text("<html></html>\n")
1032
1033 implementation_plan = temp_dir / "implementation.md"
1034 implementation_plan.write_text(
1035 "\n".join(
1036 [
1037 "# Implementation Plan",
1038 "",
1039 "## File Changes",
1040 f"- `{guide_root}/`",
1041 f"- `{chapters}/`",
1042 f"- `{index_path}`",
1043 f"- `{chapters / '02-installation.html'}`",
1044 "",
1045 ]
1046 )
1047 )
1048
1049 dod = create_definition_of_done("Create a multi-file nginx guide.")
1050 dod.implementation_plan = str(implementation_plan)
1051 dod.touched_files.extend([str(index_path), str(chapter_one)])
1052 dod.completed_items.extend(
1053 [
1054 "Create index.html for nginx guide",
1055 "Create first chapter file (01-introduction.html)",
1056 ]
1057 )
1058 dod.pending_items.append("Create second chapter file (02-installation.html)")
1059
1060 decision = repairer.handle_empty_response(
1061 task="Create a multi-file nginx guide.",
1062 original_task=None,
1063 empty_retry_count=2,
1064 max_empty_retries=2,
1065 dod=dod,
1066 )
1067
1068 assert decision.should_continue is True
1069 assert decision.retry_message is not None
1070 assert (
1071 "Resume with this exact next step: continue `Create second chapter file "
1072 "(02-installation.html)` by creating `02-installation.html`."
1073 in decision.retry_message
1074 )
1075 assert (
1076 f"Prefer one `write(content=...)` call for `{chapters / '02-installation.html'}` "
1077 "before more research."
1078 in decision.retry_message
1079 )
1080 assert "Do not return another working note or empty response" in decision.retry_message
1081
1082
1083 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
1084 temp_dir: Path,
1085 ) -> None:
1086 context = build_context(
1087 temp_dir=temp_dir,
1088 use_react=False,
1089 )
1090 repairer = ResponseRepairer(context)
1091
1092 guide_root = temp_dir / "guides" / "nginx"
1093 chapters = guide_root / "chapters"
1094 chapters.mkdir(parents=True)
1095 index_path = guide_root / "index.html"
1096 chapter_one = chapters / "01-introduction.html"
1097 index_path.write_text(
1098 "\n".join(
1099 [
1100 "<html>",
1101 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
1102 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1103 "</html>",
1104 ]
1105 )
1106 + "\n"
1107 )
1108 chapter_one.write_text("<html></html>\n")
1109
1110 implementation_plan = temp_dir / "implementation.md"
1111 implementation_plan.write_text(
1112 "\n".join(
1113 [
1114 "# Implementation Plan",
1115 "",
1116 "## File Changes",
1117 f"- `{guide_root}/`",
1118 f"- `{chapters}/`",
1119 f"- `{index_path}`",
1120 f"- `{chapters / '02-installation.html'}`",
1121 "",
1122 ]
1123 )
1124 )
1125
1126 dod = create_definition_of_done("Create a multi-file nginx guide.")
1127 dod.implementation_plan = str(implementation_plan)
1128 dod.touched_files.extend([str(index_path), str(chapter_one)])
1129 dod.completed_items.extend(
1130 [
1131 "Create index.html for nginx guide",
1132 "Create Chapter 1: Introduction to NGINX Tool",
1133 ]
1134 )
1135 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
1136
1137 decision = repairer.handle_empty_response(
1138 task="Create a multi-file nginx guide.",
1139 original_task=None,
1140 empty_retry_count=2,
1141 max_empty_retries=2,
1142 dod=dod,
1143 )
1144
1145 assert decision.should_continue is True
1146 assert decision.retry_message is not None
1147 assert (
1148 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
1149 "by creating `02-installation.html`."
1150 in decision.retry_message
1151 )
1152 assert (
1153 f"Prefer one `write(content=...)` call for `{(chapters / '02-installation.html').resolve(strict=False)}` "
1154 "before more research."
1155 in decision.retry_message
1156 )
1157 assert (
1158 f'Emit this tool shape now: `write(file_path="{(chapters / "02-installation.html").resolve(strict=False)}", content="...")`.'
1159 in decision.retry_message
1160 )
1161 assert (
1162 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
1163 "so it matches the current guide structure."
1164 in decision.retry_message
1165 )
1166
1167
1168 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
1169 temp_dir: Path,
1170 ) -> None:
1171 context = build_context(
1172 temp_dir=temp_dir,
1173 use_react=False,
1174 )
1175 repairer = ResponseRepairer(context)
1176
1177 guide_root = temp_dir / "guides" / "nginx"
1178 chapters = guide_root / "chapters"
1179 chapters.mkdir(parents=True)
1180 chapter_one = chapters / "01-introduction.html"
1181 chapter_one.write_text("<html></html>\n")
1182
1183 implementation_plan = temp_dir / "implementation.md"
1184 implementation_plan.write_text(
1185 "\n".join(
1186 [
1187 "# Implementation Plan",
1188 "",
1189 "## File Changes",
1190 f"- `{guide_root}/`",
1191 f"- `{chapters}/`",
1192 f"- `{guide_root / 'index.html'}`",
1193 f"- `{chapters / '01-introduction.html'}`",
1194 "",
1195 ]
1196 )
1197 )
1198
1199 dod = create_definition_of_done("Create a multi-file nginx guide.")
1200 dod.implementation_plan = str(implementation_plan)
1201 dod.touched_files.append(str(chapter_one))
1202 dod.completed_items.append("Create first chapter file (01-introduction.html)")
1203 dod.pending_items.append("Develop the main index.html file for the nginx guide")
1204
1205 recovery_context = RecoveryContext(
1206 original_tool="write",
1207 original_args={
1208 "file_path": "~/Loader/guides/nginx/index.html",
1209 "content_chars": 1354,
1210 "content_lines": 30,
1211 },
1212 )
1213 recovery_context.add_attempt(
1214 "write",
1215 {
1216 "file_path": "~/Loader/guides/nginx/index.html",
1217 "content_chars": 1354,
1218 "content_lines": 30,
1219 },
1220 "WriteTool.execute() missing 1 required positional argument: 'content'",
1221 )
1222 context.recovery_context = recovery_context
1223
1224 decision = repairer.handle_empty_response(
1225 task="Create a multi-file nginx guide.",
1226 original_task=None,
1227 empty_retry_count=2,
1228 max_empty_retries=2,
1229 dod=dod,
1230 )
1231
1232 assert decision.should_continue is True
1233 assert decision.retry_message is not None
1234 assert "resend `write`" in decision.retry_message
1235 assert "content_chars" in decision.retry_message
1236 assert "index.html" in decision.retry_message
1237
1238
1239 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
1240 temp_dir: Path,
1241 ) -> None:
1242 context = build_context(
1243 temp_dir=temp_dir,
1244 use_react=False,
1245 )
1246 repairer = ResponseRepairer(context)
1247
1248 guide_root = temp_dir / "guides" / "nginx"
1249 chapters = guide_root / "chapters"
1250 chapters.mkdir(parents=True)
1251 index_path = guide_root / "index.html"
1252 chapter_one = chapters / "01-introduction.html"
1253 index_path.write_text(
1254 "\n".join(
1255 [
1256 "<html>",
1257 '<a href="chapters/01-introduction.html">Introduction</a>',
1258 '<a href="chapters/02-installation.html">Installation</a>',
1259 "</html>",
1260 ]
1261 )
1262 + "\n"
1263 )
1264 chapter_one.write_text("<html></html>\n")
1265
1266 implementation_plan = temp_dir / "implementation.md"
1267 implementation_plan.write_text(
1268 "\n".join(
1269 [
1270 "# Implementation Plan",
1271 "",
1272 "## File Changes",
1273 f"- `{guide_root}/`",
1274 f"- `{chapters}/`",
1275 f"- `{index_path}`",
1276 f"- `{chapters / '02-installation.html'}`",
1277 "",
1278 ]
1279 )
1280 )
1281
1282 dod = create_definition_of_done("Create a multi-file nginx guide.")
1283 dod.implementation_plan = str(implementation_plan)
1284 dod.touched_files.extend([str(index_path), str(chapter_one)])
1285 dod.completed_items.extend(
1286 [
1287 "Create index.html for nginx guide",
1288 "Create first chapter file (01-introduction.html)",
1289 ]
1290 )
1291 dod.pending_items.append("Create second chapter file (02-installation.html)")
1292
1293 decision = repairer.handle_empty_response(
1294 task="Create a multi-file nginx guide.",
1295 original_task=None,
1296 empty_retry_count=1,
1297 max_empty_retries=2,
1298 dod=dod,
1299 )
1300
1301 assert decision.should_continue is True
1302 assert decision.retry_message is not None
1303 assert "Continue from the exact next step below." in decision.retry_message
1304 assert "Confirmed completed work:" not in decision.retry_message
1305 assert "Next pending item:" not in decision.retry_message
1306 assert (
1307 "Resume with this exact next step: continue `Create second chapter file "
1308 "(02-installation.html)` by creating `02-installation.html`."
1309 in decision.retry_message
1310 )
1311
1312
1313 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
1314 temp_dir: Path,
1315 ) -> None:
1316 context = build_context(
1317 temp_dir=temp_dir,
1318 use_react=False,
1319 )
1320 repairer = ResponseRepairer(context)
1321
1322 guide_root = temp_dir / "guides" / "nginx"
1323 chapters = guide_root / "chapters"
1324 chapters.mkdir(parents=True)
1325 index_path = guide_root / "index.html"
1326 chapter_one = chapters / "01-introduction.html"
1327 index_path.write_text("<html></html>\n")
1328 chapter_one.write_text("<html></html>\n")
1329
1330 implementation_plan = temp_dir / "implementation.md"
1331 implementation_plan.write_text(
1332 "\n".join(
1333 [
1334 "# Implementation Plan",
1335 "",
1336 "## File Changes",
1337 f"- `{guide_root}/`",
1338 f"- `{chapters}/`",
1339 f"- `{index_path}`",
1340 f"- `{chapter_one}`",
1341 f"- `{chapters / '02-installation.html'}`",
1342 "",
1343 ]
1344 )
1345 )
1346
1347 dod = create_definition_of_done("Create a multi-file nginx guide.")
1348 dod.implementation_plan = str(implementation_plan)
1349 dod.touched_files.extend([str(index_path), str(chapter_one)])
1350 dod.completed_items.extend(
1351 [
1352 "Develop the main index.html file for the nginx guide",
1353 "Create first chapter file (01-introduction.html)",
1354 ]
1355 )
1356 dod.pending_items.extend(
1357 [
1358 "Create the nginx directory structure",
1359 "Create second chapter file (02-installation.html)",
1360 ]
1361 )
1362
1363 decision = repairer.handle_empty_response(
1364 task="Create a multi-file nginx guide.",
1365 original_task=None,
1366 empty_retry_count=1,
1367 max_empty_retries=2,
1368 dod=dod,
1369 )
1370
1371 assert decision.should_continue is True
1372 assert decision.retry_message is not None
1373 assert "Create the nginx directory structure" not in decision.retry_message
1374 assert "02-installation.html" in decision.retry_message
1375
1376
1377 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
1378 temp_dir: Path,
1379 ) -> None:
1380 context = build_context(
1381 temp_dir=temp_dir,
1382 use_react=False,
1383 )
1384 repairer = ResponseRepairer(context)
1385
1386 guide_root = temp_dir / "guides" / "nginx"
1387 chapters = guide_root / "chapters"
1388 chapters.mkdir(parents=True)
1389 index_path = guide_root / "index.html"
1390 chapter_one = chapters / "01-getting-started.html"
1391 chapter_two = chapters / "02-installation.html"
1392 chapter_three = chapters / "03-first-website.html"
1393 chapter_four = chapters / "04-configuration-basics.html"
1394 index_path.write_text("<html></html>\n")
1395 chapter_one.write_text("<h1>One</h1>\n")
1396 chapter_two.write_text("<h1>Two</h1>\n")
1397 chapter_three.write_text("<h1>Three</h1>\n")
1398
1399 implementation_plan = temp_dir / "implementation.md"
1400 implementation_plan.write_text(
1401 "\n".join(
1402 [
1403 "# Implementation Plan",
1404 "",
1405 "## File Changes",
1406 f"- `{guide_root}/`",
1407 f"- `{chapters}/`",
1408 f"- `{index_path}`",
1409 f"- `{chapter_one}`",
1410 f"- `{chapter_two}`",
1411 f"- `{chapter_three}`",
1412 f"- `{chapter_four}`",
1413 "",
1414 ]
1415 )
1416 )
1417
1418 dod = create_definition_of_done("Create a multi-file nginx guide.")
1419 dod.implementation_plan = str(implementation_plan)
1420 dod.touched_files.extend(
1421 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
1422 )
1423 dod.completed_items.extend(
1424 [
1425 "Create the directory structure for the new nginx guide",
1426 "Create the main index.html file with proper structure",
1427 ]
1428 )
1429 dod.pending_items.append("Create each chapter file in sequence")
1430
1431 decision = repairer.handle_empty_response(
1432 task="Create a multi-file nginx guide.",
1433 original_task=None,
1434 empty_retry_count=5,
1435 max_empty_retries=2,
1436 dod=dod,
1437 )
1438
1439 assert decision.should_continue is False
1440 assert decision.final_response is not None
1441 assert "retrying 4 times" in decision.final_response
1442
1443
1444 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
1445 temp_dir: Path,
1446 ) -> None:
1447 context = build_context(
1448 temp_dir=temp_dir,
1449 use_react=False,
1450 )
1451 repairer = ResponseRepairer(context)
1452
1453 guide_root = temp_dir / "guides" / "nginx"
1454 chapters = guide_root / "chapters"
1455 chapters.mkdir(parents=True)
1456 implementation_plan = temp_dir / "implementation.md"
1457 implementation_plan.write_text(
1458 "\n".join(
1459 [
1460 "# Implementation Plan",
1461 "",
1462 "## File Changes",
1463 f"- `{guide_root / 'index.html'}`",
1464 f"- `{chapters / '01-getting-started.html'}`",
1465 f"- `{chapters / '02-installation.html'}`",
1466 "",
1467 ]
1468 )
1469 )
1470
1471 dod = create_definition_of_done("Create a multi-file nginx guide.")
1472 dod.implementation_plan = str(implementation_plan)
1473 dod.touched_files.extend(
1474 [
1475 str(guide_root / "index.html"),
1476 str(chapters / "01-getting-started.html"),
1477 ]
1478 )
1479 dod.completed_items.extend(
1480 [
1481 "Create the directory structure for the new nginx guide",
1482 "Create the main index.html file with proper structure",
1483 ]
1484 )
1485 dod.pending_items.append("Create each chapter file in sequence")
1486
1487 decision = repairer.handle_empty_response(
1488 task="Create a multi-file nginx guide.",
1489 original_task=None,
1490 empty_retry_count=1,
1491 max_empty_retries=2,
1492 dod=dod,
1493 )
1494
1495 assert decision.retry_message is not None
1496 assert "Continue from the exact next step below." in decision.retry_message
1497 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
1498
1499
1500 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
1501 temp_dir: Path,
1502 ) -> None:
1503 context = build_context(
1504 temp_dir=temp_dir,
1505 use_react=False,
1506 )
1507 repairer = ResponseRepairer(context)
1508
1509 guide_root = temp_dir / "guides" / "nginx"
1510 chapters = guide_root / "chapters"
1511 chapters.mkdir(parents=True)
1512 index_path = guide_root / "index.html"
1513 chapter_one = chapters / "01-getting-started.html"
1514 chapter_two = chapters / "02-installation.html"
1515 chapter_three = chapters / "03-first-website.html"
1516 index_path.write_text("<html></html>\n")
1517 chapter_one.write_text("<h1>One</h1>\n")
1518 chapter_two.write_text("<h1>Two</h1>\n")
1519
1520 implementation_plan = temp_dir / "implementation.md"
1521 implementation_plan.write_text(
1522 "\n".join(
1523 [
1524 "# Implementation Plan",
1525 "",
1526 "## File Changes",
1527 f"- `{guide_root}/`",
1528 f"- `{chapters}/`",
1529 f"- `{index_path}`",
1530 f"- `{chapter_one}`",
1531 f"- `{chapter_two}`",
1532 f"- `{chapter_three}`",
1533 "",
1534 ]
1535 )
1536 )
1537
1538 dod = create_definition_of_done("Create a multi-file nginx guide.")
1539 dod.implementation_plan = str(implementation_plan)
1540 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
1541 dod.completed_items.extend(
1542 [
1543 "Create the main index.html file with proper structure",
1544 "Link all chapters together properly",
1545 ]
1546 )
1547 dod.pending_items.append("Create each chapter file in sequence")
1548
1549 decision = repairer.handle_empty_response(
1550 task="Create a multi-file nginx guide.",
1551 original_task=None,
1552 empty_retry_count=1,
1553 max_empty_retries=2,
1554 dod=dod,
1555 )
1556
1557 assert decision.retry_message is not None
1558 assert "Link all chapters together properly" not in decision.retry_message
1559 assert "Continue from the exact next step below." in decision.retry_message
1560 assert "Resume with this exact next step:" in decision.retry_message
1561
1562
1563 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
1564 temp_dir: Path,
1565 ) -> None:
1566 context = build_context(
1567 temp_dir=temp_dir,
1568 use_react=False,
1569 )
1570 repairer = ResponseRepairer(context)
1571
1572 reference_chapters = temp_dir / "fortran" / "chapters"
1573 reference_chapters.mkdir(parents=True)
1574 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
1575
1576 guide_root = temp_dir / "guides" / "nginx"
1577 chapters = guide_root / "chapters"
1578 chapters.mkdir(parents=True)
1579 index_path = guide_root / "index.html"
1580 index_path.write_text("<html></html>\n")
1581
1582 implementation_plan = temp_dir / "implementation.md"
1583 implementation_plan.write_text(
1584 "\n".join(
1585 [
1586 "# Implementation Plan",
1587 "",
1588 "## File Changes",
1589 f"- `{guide_root}/`",
1590 f"- `{chapters}/`",
1591 f"- `{index_path}`",
1592 "",
1593 ]
1594 )
1595 )
1596
1597 dod = create_definition_of_done("Create a multi-file nginx guide.")
1598 dod.implementation_plan = str(implementation_plan)
1599 dod.touched_files.append(str(index_path))
1600 dod.pending_items.append("Write the introduction chapter")
1601 context.session.append(
1602 Message(
1603 role=Role.ASSISTANT,
1604 content="",
1605 tool_calls=[
1606 ToolCall(
1607 id="read-ref-1",
1608 name="read",
1609 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
1610 )
1611 ],
1612 )
1613 )
1614
1615 decision = repairer.handle_empty_response(
1616 task="Create a multi-file nginx guide.",
1617 original_task=None,
1618 empty_retry_count=1,
1619 max_empty_retries=2,
1620 dod=dod,
1621 )
1622
1623 assert decision.should_continue is True
1624 assert decision.retry_message is not None
1625 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
1626 assert "Next observed output pattern under `chapters/`: `01-introduction.html`" in decision.retry_message
1627 assert (
1628 "Resume with this exact next step: continue `Write the introduction chapter` "
1629 "by creating `01-introduction.html`."
1630 in decision.retry_message
1631 )
1632 assert (
1633 "It mirrors the observed filename pattern from another `chapters/` directory "
1634 "you already inspected."
1635 in decision.retry_message
1636 )