Python · 57543 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: create `index.html`."
312 in decision.retry_message
313 )
314 assert (
315 f"Prefer one `write` call for `{index_path}` before any more reference reads."
316 in decision.retry_message
317 )
318 assert (
319 "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."
320 in decision.retry_message
321 )
322 assert (
323 f'Emit this tool shape now: `write(file_path="{index_path.resolve(strict=False)}", content="...")`.'
324 in decision.retry_message
325 )
326 assert "Do not restart discovery unless one specific missing fact blocks this step." in decision.retry_message
327
328
329 def test_empty_response_retry_uses_directory_creation_for_setup_targets(
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_path = guide_root / "chapters"
340 index_path = guide_root / "index.html"
341
342 implementation_plan = temp_dir / "implementation.md"
343 implementation_plan.write_text(
344 "\n".join(
345 [
346 "# Implementation Plan",
347 "",
348 "## File Changes",
349 f"- `{chapters_path}/`",
350 f"- `{index_path}`",
351 "",
352 ]
353 )
354 )
355
356 dod = create_definition_of_done("Create a multi-file nginx guide.")
357 dod.implementation_plan = str(implementation_plan)
358 dod.pending_items.extend(
359 [
360 "Create the nginx directory structure",
361 "Create the main index.html file for nginx guide",
362 ]
363 )
364
365 decision = repairer.handle_empty_response(
366 task="Create a multi-file nginx guide.",
367 original_task=None,
368 empty_retry_count=1,
369 max_empty_retries=2,
370 dod=dod,
371 )
372
373 assert decision.should_continue is True
374 assert decision.retry_message is not None
375 assert (
376 "Resume with this exact next step: continue `Create the nginx directory structure` "
377 "by creating `chapters/`."
378 in decision.retry_message
379 )
380 assert (
381 f"Prefer one concrete directory-creation step for `{chapters_path}` before more research."
382 in decision.retry_message
383 )
384 expected_command = f"mkdir -p {chapters_path.resolve(strict=False)}"
385 assert (
386 f'Emit this tool shape now: `bash(command="{expected_command}")`.'
387 in decision.retry_message
388 )
389 assert f'write(file_path="{chapters_path.resolve(strict=False)}"' not in decision.retry_message
390
391
392 def test_empty_response_retry_recovers_blocked_empty_file_path_to_concrete_target(
393 temp_dir: Path,
394 ) -> None:
395 context = build_context(
396 temp_dir=temp_dir,
397 use_react=False,
398 )
399 repairer = ResponseRepairer(context)
400
401 guide_root = temp_dir / "guides" / "nginx"
402 chapters = guide_root / "chapters"
403 chapters.mkdir(parents=True)
404 index_path = guide_root / "index.html"
405 first_chapter = chapters / "01-introduction.html"
406 second_chapter = chapters / "02-installation.html"
407 index_path.write_text("<html></html>\n")
408 first_chapter.write_text("<h1>Intro</h1>\n")
409
410 implementation_plan = temp_dir / "implementation.md"
411 implementation_plan.write_text(
412 "\n".join(
413 [
414 "# Implementation Plan",
415 "",
416 "## File Changes",
417 f"- `{index_path}`",
418 f"- `{first_chapter}`",
419 f"- `{second_chapter}`",
420 "",
421 ]
422 )
423 )
424
425 dod = create_definition_of_done("Create a multi-file nginx guide.")
426 dod.implementation_plan = str(implementation_plan)
427 dod.touched_files.extend([str(index_path), str(first_chapter)])
428 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
429
430 context.recovery_context = RecoveryContext(
431 original_tool="write",
432 original_args={"file_path": "", "content": "<html></html>\n"},
433 )
434 context.recovery_context.add_attempt(
435 "write",
436 {"file_path": "", "content": "<html></html>\n"},
437 "Empty file path",
438 )
439
440 decision = repairer.handle_empty_response(
441 task="Create a multi-file nginx guide.",
442 original_task=None,
443 empty_retry_count=1,
444 max_empty_retries=2,
445 dod=dod,
446 )
447
448 assert decision.should_continue is True
449 assert decision.retry_message is not None
450 assert (
451 f"Last tool failure: resend `write` for `{second_chapter}` with a valid `file_path` and real `content`."
452 in decision.retry_message
453 )
454 assert "Do not leave `file_path` empty" in decision.retry_message
455 assert (
456 f'Emit this tool shape now: `write(file_path="{second_chapter.resolve(strict=False)}", content="...")`.'
457 in decision.retry_message
458 )
459
460
461 def test_empty_response_retry_respects_discovery_first_pending_step(
462 temp_dir: Path,
463 ) -> None:
464 context = build_context(
465 temp_dir=temp_dir,
466 use_react=False,
467 )
468 repairer = ResponseRepairer(context)
469
470 implementation_plan = temp_dir / "implementation.md"
471 implementation_plan.write_text(
472 "\n".join(
473 [
474 "# Implementation Plan",
475 "",
476 "## File Changes",
477 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
478 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
479 "",
480 ]
481 )
482 )
483
484 dod = create_definition_of_done("Create a multi-file nginx guide.")
485 dod.implementation_plan = str(implementation_plan)
486 dod.pending_items.extend(
487 [
488 "First, examine the existing fortran guide structure and content to understand the format",
489 "Create the nginx directory structure",
490 "Develop the main index.html file for the nginx guide",
491 ]
492 )
493
494 context.session.append(
495 SimpleNamespace(
496 role="tool",
497 content=(
498 "Observation [notepad_write_working]: Result: "
499 "- [2026-04-22T22:42:18Z] Analyzing the fortran guide structure before creating nginx guide"
500 ),
501 )
502 )
503
504 decision = repairer.handle_empty_response(
505 task="Create a multi-file nginx guide.",
506 original_task=None,
507 empty_retry_count=1,
508 max_empty_retries=2,
509 dod=dod,
510 )
511
512 assert decision.should_continue is True
513 assert decision.retry_message is not None
514 assert (
515 "Resume with this exact next step: advance `First, examine the existing fortran guide structure and content to understand the format`."
516 in decision.retry_message
517 )
518 assert "one concrete evidence-gathering tool call" in decision.retry_message
519 assert "Resume with this exact next step: create `index.html`." not in decision.retry_message
520
521
522 def test_empty_response_retry_budget_extends_for_late_stage_multi_artifact_progress(
523 temp_dir: Path,
524 ) -> None:
525 context = build_context(
526 temp_dir=temp_dir,
527 use_react=False,
528 )
529 repairer = ResponseRepairer(context)
530
531 guide_root = temp_dir / "guides" / "nginx"
532 chapters = guide_root / "chapters"
533 chapters.mkdir(parents=True)
534 index_path = guide_root / "index.html"
535 chapter_one = chapters / "01-getting-started.html"
536 chapter_two = chapters / "02-installation.html"
537 chapter_three = chapters / "03-first-website.html"
538 chapter_four = chapters / "04-configuration-basics.html"
539 index_path.write_text("<html></html>\n")
540 chapter_one.write_text("<h1>One</h1>\n")
541 chapter_two.write_text("<h1>Two</h1>\n")
542 chapter_three.write_text("<h1>Three</h1>\n")
543
544 implementation_plan = temp_dir / "implementation.md"
545 implementation_plan.write_text(
546 "\n".join(
547 [
548 "# Implementation Plan",
549 "",
550 "## File Changes",
551 f"- `{guide_root}/`",
552 f"- `{chapters}/`",
553 f"- `{index_path}`",
554 f"- `{chapter_one}`",
555 f"- `{chapter_two}`",
556 f"- `{chapter_three}`",
557 f"- `{chapter_four}`",
558 "",
559 ]
560 )
561 )
562
563 dod = create_definition_of_done("Create a multi-file nginx guide.")
564 dod.implementation_plan = str(implementation_plan)
565 dod.touched_files.extend(
566 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
567 )
568 dod.completed_items.extend(
569 [
570 "Create the directory structure for the new nginx guide",
571 "Create the main index.html file with proper structure",
572 ]
573 )
574 dod.pending_items.append("Create each chapter file in sequence")
575
576 decision = repairer.handle_empty_response(
577 task="Create a multi-file nginx guide.",
578 original_task=None,
579 empty_retry_count=3,
580 max_empty_retries=2,
581 dod=dod,
582 )
583
584 assert decision.should_continue is True
585 assert decision.retry_message is not None
586 assert "retry 3/4" in decision.retry_message
587 assert "Follow the same one-file-at-a-time mutation pattern" in decision.retry_message
588
589
590 def test_empty_response_retry_budget_extends_when_concrete_next_output_is_known(
591 temp_dir: Path,
592 ) -> None:
593 context = build_context(
594 temp_dir=temp_dir,
595 use_react=False,
596 )
597 repairer = ResponseRepairer(context)
598
599 implementation_plan = temp_dir / "implementation.md"
600 implementation_plan.write_text(
601 "\n".join(
602 [
603 "# Implementation Plan",
604 "",
605 "## File Changes",
606 f"- `{temp_dir / 'guides' / 'nginx' / 'index.html'}`",
607 f"- `{temp_dir / 'guides' / 'nginx' / 'chapters'}`",
608 "",
609 ]
610 )
611 )
612
613 dod = create_definition_of_done("Create a multi-file nginx guide.")
614 dod.implementation_plan = str(implementation_plan)
615 dod.pending_items.append("Develop the main index.html file for the nginx guide")
616
617 decision = repairer.handle_empty_response(
618 task="Create a multi-file nginx guide.",
619 original_task=None,
620 empty_retry_count=3,
621 max_empty_retries=2,
622 dod=dod,
623 )
624
625 assert decision.should_continue is True
626 assert decision.retry_message is not None
627 assert "retry 3/4" in decision.retry_message
628 assert "Next missing planned artifact: `index.html`" in decision.retry_message
629 assert (
630 "Resume with this exact next step: continue `Develop the main index.html file for the nginx guide` "
631 "by creating `index.html`."
632 in decision.retry_message
633 )
634
635
636 def test_empty_response_retry_budget_extends_further_after_first_output_file_exists(
637 temp_dir: Path,
638 ) -> None:
639 context = build_context(
640 temp_dir=temp_dir,
641 use_react=False,
642 )
643 repairer = ResponseRepairer(context)
644
645 guide_root = temp_dir / "guides" / "nginx"
646 chapters = guide_root / "chapters"
647 guide_root.mkdir(parents=True)
648 chapters.mkdir()
649 index_path = guide_root / "index.html"
650 index_path.write_text("<html></html>\n")
651
652 implementation_plan = temp_dir / "implementation.md"
653 implementation_plan.write_text(
654 "\n".join(
655 [
656 "# Implementation Plan",
657 "",
658 "## File Changes",
659 f"- `{chapters}/`",
660 f"- `{index_path}`",
661 "",
662 ]
663 )
664 )
665
666 dod = create_definition_of_done("Create a multi-file nginx guide.")
667 dod.implementation_plan = str(implementation_plan)
668 dod.touched_files.append(str(index_path))
669 dod.completed_items.extend(
670 [
671 "Create the new nginx guide directory structure",
672 "Develop the main index.html file with proper structure",
673 ]
674 )
675 dod.pending_items.append("Create 01-introduction.html")
676
677 decision = repairer.handle_empty_response(
678 task="Create a multi-file nginx guide.",
679 original_task=None,
680 empty_retry_count=5,
681 max_empty_retries=2,
682 dod=dod,
683 )
684
685 assert decision.should_continue is True
686 assert decision.retry_message is not None
687 assert "retry 5/6" in decision.retry_message
688 assert "01-introduction.html" in decision.retry_message
689
690
691 def test_empty_response_retry_uses_compact_prompt_after_substantial_progress(
692 temp_dir: Path,
693 ) -> None:
694 context = build_context(
695 temp_dir=temp_dir,
696 use_react=False,
697 )
698 context.session.messages.append(
699 SimpleNamespace(
700 content=(
701 "Observation [notepad_write_working]: Result: "
702 "- [2026-04-23T19:00:00Z] Creating fifth chapter file: Advanced features"
703 )
704 )
705 )
706 repairer = ResponseRepairer(context)
707
708 guide_root = temp_dir / "guides" / "nginx"
709 chapters = guide_root / "chapters"
710 chapters.mkdir(parents=True)
711 index_path = guide_root / "index.html"
712 chapter_one = chapters / "01-getting-started.html"
713 chapter_two = chapters / "02-installation.html"
714 chapter_three = chapters / "03-first-website.html"
715 chapter_four = chapters / "04-configuration-basics.html"
716 chapter_five = chapters / "05-advanced-features.html"
717 index_path.write_text("<html></html>\n")
718 chapter_one.write_text("<h1>One</h1>\n")
719 chapter_two.write_text("<h1>Two</h1>\n")
720 chapter_three.write_text("<h1>Three</h1>\n")
721 chapter_four.write_text("<h1>Four</h1>\n")
722
723 implementation_plan = temp_dir / "implementation.md"
724 implementation_plan.write_text(
725 "\n".join(
726 [
727 "# Implementation Plan",
728 "",
729 "## File Changes",
730 f"- `{guide_root}/`",
731 f"- `{chapters}/`",
732 f"- `{index_path}`",
733 f"- `{chapter_one}`",
734 f"- `{chapter_two}`",
735 f"- `{chapter_three}`",
736 f"- `{chapter_four}`",
737 f"- `{chapter_five}`",
738 "",
739 ]
740 )
741 )
742
743 dod = create_definition_of_done("Create a multi-file nginx guide.")
744 dod.implementation_plan = str(implementation_plan)
745 dod.touched_files.extend(
746 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
747 )
748 dod.completed_items.extend(
749 [
750 "Create the directory structure for the new nginx guide",
751 "Create the main index.html file with proper structure",
752 ]
753 )
754 dod.pending_items.append("Create each chapter file in sequence")
755
756 decision = repairer.handle_empty_response(
757 task="Create a multi-file nginx guide.",
758 original_task=None,
759 empty_retry_count=3,
760 max_empty_retries=2,
761 dod=dod,
762 )
763
764 assert decision.should_continue is True
765 assert decision.retry_message is not None
766 assert "Continue from the exact next step below." in decision.retry_message
767 assert "Latest working note:" not in decision.retry_message
768 assert "Confirmed completed work:" not in decision.retry_message
769 assert "Next pending item:" not in decision.retry_message
770
771
772 def test_empty_response_retry_points_at_next_output_file_when_planned_directory_is_empty(
773 temp_dir: Path,
774 ) -> None:
775 context = build_context(
776 temp_dir=temp_dir,
777 use_react=False,
778 )
779 repairer = ResponseRepairer(context)
780
781 guide_root = temp_dir / "guides" / "nginx"
782 chapters = guide_root / "chapters"
783 chapters.mkdir(parents=True)
784 index_path = guide_root / "index.html"
785 index_path.write_text("<html></html>\n")
786
787 implementation_plan = temp_dir / "implementation.md"
788 implementation_plan.write_text(
789 "\n".join(
790 [
791 "# Implementation Plan",
792 "",
793 "## File Changes",
794 f"- `{guide_root}/`",
795 f"- `{chapters}/`",
796 f"- `{index_path}`",
797 f"- `{chapters / '02-installation.html'}`",
798 "",
799 ]
800 )
801 )
802
803 dod = create_definition_of_done("Create a multi-file nginx guide.")
804 dod.implementation_plan = str(implementation_plan)
805 dod.touched_files.append(str(index_path))
806 dod.pending_items.append("Write the introduction chapter")
807
808 decision = repairer.handle_empty_response(
809 task="Create a multi-file nginx guide.",
810 original_task=None,
811 empty_retry_count=1,
812 max_empty_retries=2,
813 dod=dod,
814 )
815
816 assert decision.should_continue is True
817 assert decision.retry_message is not None
818 assert "Next missing planned artifact: `chapters/`" in decision.retry_message
819 assert (
820 "Resume with this exact next step: continue `Write the introduction chapter` "
821 "by creating the next output file under `chapters/`."
822 in decision.retry_message
823 )
824 assert (
825 f"Prefer one concrete `write` call for a file inside `{chapters}` before more research."
826 in decision.retry_message
827 )
828
829
830 def test_empty_response_retry_treats_develop_index_step_as_mutation_work(
831 temp_dir: Path,
832 ) -> None:
833 context = build_context(
834 temp_dir=temp_dir,
835 use_react=False,
836 )
837 repairer = ResponseRepairer(context)
838
839 guide_root = temp_dir / "guides" / "nginx"
840 chapters = guide_root / "chapters"
841 guide_root.mkdir(parents=True)
842 chapters.mkdir()
843 chapter_one = chapters / "01-introduction.html"
844 index_path = guide_root / "index.html"
845
846 implementation_plan = temp_dir / "implementation.md"
847 implementation_plan.write_text(
848 "\n".join(
849 [
850 "# Implementation Plan",
851 "",
852 "## File Changes",
853 f"- `{guide_root}/`",
854 f"- `{index_path}`",
855 f"- `{chapters}/`",
856 f"- `{chapter_one}`",
857 "",
858 ]
859 )
860 )
861
862 dod = create_definition_of_done("Create a multi-file nginx guide.")
863 dod.implementation_plan = str(implementation_plan)
864 dod.completed_items.extend(
865 [
866 "First, examine the existing Fortran guide structure to understand the format and depth",
867 "Create the new nginx guide directory structure",
868 ]
869 )
870 dod.pending_items.append("Develop the main index.html file with proper structure")
871
872 decision = repairer.handle_empty_response(
873 task="Create a multi-file nginx guide.",
874 original_task=None,
875 empty_retry_count=2,
876 max_empty_retries=2,
877 dod=dod,
878 )
879
880 assert decision.should_continue is True
881 assert decision.retry_message is not None
882 assert (
883 "Resume with this exact next step: continue `Develop the main index.html file with proper structure`"
884 in decision.retry_message
885 )
886 assert "Next missing planned artifact: `index.html`" in decision.retry_message
887 assert "Prefer one `write(content=...)` call" in decision.retry_message
888 assert "Make the next response one concrete evidence-gathering tool call" not in decision.retry_message
889
890
891 def test_empty_response_retry_prefers_pending_index_over_broad_directory_headline(
892 temp_dir: Path,
893 ) -> None:
894 context = build_context(
895 temp_dir=temp_dir,
896 use_react=False,
897 )
898 repairer = ResponseRepairer(context)
899
900 guide_root = temp_dir / "guides" / "nginx"
901 chapters = guide_root / "chapters"
902 guide_root.mkdir(parents=True)
903 chapters.mkdir()
904 index_path = guide_root / "index.html"
905 chapter_one = chapters / "01-introduction.html"
906
907 implementation_plan = temp_dir / "implementation.md"
908 implementation_plan.write_text(
909 "\n".join(
910 [
911 "# Implementation Plan",
912 "",
913 "## File Changes",
914 f"- `{guide_root}/`",
915 f"- `{chapters}/`",
916 f"- `{index_path}`",
917 f"- `{chapter_one}`",
918 "",
919 ]
920 )
921 )
922
923 dod = create_definition_of_done("Create a multi-file nginx guide.")
924 dod.implementation_plan = str(implementation_plan)
925 dod.completed_items.extend(
926 [
927 "First, examine the existing Fortran guide structure to understand the format and depth",
928 "Create the new nginx guide directory structure",
929 ]
930 )
931 dod.pending_items.append("Develop the main index.html file with proper structure")
932
933 decision = repairer.handle_empty_response(
934 task="Create a multi-file nginx guide.",
935 original_task=None,
936 empty_retry_count=4,
937 max_empty_retries=4,
938 dod=dod,
939 )
940
941 assert decision.should_continue is True
942 assert decision.retry_message is not None
943 assert "Next missing planned artifact: `index.html`" in decision.retry_message
944 assert (
945 "Resume with this exact next step: continue `Develop the main index.html file with proper structure` "
946 "by creating `index.html`."
947 in decision.retry_message
948 )
949 assert "Next missing planned artifact: `chapters/`" not in decision.retry_message
950 assert (
951 "Next observed output pattern under `chapters/`: `01-introduction.html`"
952 not in decision.retry_message
953 )
954
955
956 def test_empty_response_retry_uses_concrete_file_language_for_aggregate_chapter_step(
957 temp_dir: Path,
958 ) -> None:
959 context = build_context(
960 temp_dir=temp_dir,
961 use_react=False,
962 )
963 repairer = ResponseRepairer(context)
964
965 guide_root = temp_dir / "guides" / "nginx"
966 chapters = guide_root / "chapters"
967 chapters.mkdir(parents=True)
968 index_path = guide_root / "index.html"
969 index_path.write_text(
970 "\n".join(
971 [
972 "<html>",
973 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to Nginx</a>',
974 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
975 "</html>",
976 ]
977 )
978 + "\n"
979 )
980
981 implementation_plan = temp_dir / "implementation.md"
982 implementation_plan.write_text(
983 "\n".join(
984 [
985 "# Implementation Plan",
986 "",
987 "## File Changes",
988 f"- `{guide_root}/`",
989 f"- `{chapters}/`",
990 f"- `{index_path}`",
991 "",
992 ]
993 )
994 )
995
996 dod = create_definition_of_done("Create a multi-file nginx guide.")
997 dod.implementation_plan = str(implementation_plan)
998 dod.touched_files.append(str(index_path))
999 dod.completed_items.append("Develop the main index.html file with proper structure")
1000 dod.pending_items.append("Create chapter files with content and structure")
1001
1002 decision = repairer.handle_empty_response(
1003 task="Create a multi-file nginx guide.",
1004 original_task=None,
1005 empty_retry_count=3,
1006 max_empty_retries=4,
1007 dod=dod,
1008 )
1009
1010 assert decision.should_continue is True
1011 assert decision.retry_message is not None
1012 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
1013 assert (
1014 "Resume with this exact next step: create `01-introduction.html`."
1015 in decision.retry_message
1016 )
1017 assert (
1018 "It is the next concrete output needed to continue `Create chapter files with content and structure`."
1019 in decision.retry_message
1020 )
1021 assert (
1022 "continue `Create chapter files with content and structure` by creating `01-introduction.html`."
1023 not in decision.retry_message
1024 )
1025
1026
1027 def test_empty_response_retry_prefers_output_index_over_reference_index_with_same_name(
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 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
1037 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
1038 nginx_root.mkdir(parents=True)
1039 fortran_root.mkdir(parents=True)
1040 reference_index = fortran_root / "index.html"
1041 reference_index.write_text("<html>fortran</html>\n")
1042 output_index = nginx_root / "index.html"
1043
1044 implementation_plan = temp_dir / "implementation.md"
1045 implementation_plan.write_text(
1046 "\n".join(
1047 [
1048 "# Implementation Plan",
1049 "",
1050 "## File Changes",
1051 f"- `{output_index}`",
1052 f"- `{nginx_root / 'chapters'}/`",
1053 f"- `{reference_index}`",
1054 "",
1055 ]
1056 )
1057 )
1058
1059 dod = create_definition_of_done("Create a multi-file nginx guide.")
1060 dod.implementation_plan = str(implementation_plan)
1061 dod.touched_files.append(str(reference_index))
1062 dod.completed_items.append(
1063 "First, examine the existing Fortran guide structure and content"
1064 )
1065 dod.pending_items.append("Develop the nginx index.html file")
1066
1067 decision = repairer.handle_empty_response(
1068 task="Create a multi-file nginx guide.",
1069 original_task=None,
1070 empty_retry_count=2,
1071 max_empty_retries=2,
1072 dod=dod,
1073 )
1074
1075 assert decision.should_continue is True
1076 assert decision.retry_message is not None
1077 assert (
1078 f"Prefer one `write(content=...)` call for `{output_index}` before more research."
1079 in decision.retry_message
1080 )
1081 assert str(reference_index) not in decision.retry_message
1082
1083
1084 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
1085 temp_dir: Path,
1086 ) -> None:
1087 context = build_context(
1088 temp_dir=temp_dir,
1089 use_react=False,
1090 )
1091 repairer = ResponseRepairer(context)
1092
1093 guide_root = temp_dir / "guides" / "nginx"
1094 chapters = guide_root / "chapters"
1095 chapters.mkdir(parents=True)
1096 index_path = guide_root / "index.html"
1097 index_path.write_text(
1098 "\n".join(
1099 [
1100 "<html>",
1101 '<a href="chapters/introduction.html">Introduction</a>',
1102 '<a href="chapters/installation.html">Installation</a>',
1103 "</html>",
1104 ]
1105 )
1106 + "\n"
1107 )
1108
1109 implementation_plan = temp_dir / "implementation.md"
1110 implementation_plan.write_text(
1111 "\n".join(
1112 [
1113 "# Implementation Plan",
1114 "",
1115 "## File Changes",
1116 f"- `{guide_root}/`",
1117 f"- `{chapters}/`",
1118 f"- `{index_path}`",
1119 f"- `{chapters / '02-installation.html'}`",
1120 "",
1121 ]
1122 )
1123 )
1124
1125 dod = create_definition_of_done("Create a multi-file nginx guide.")
1126 dod.implementation_plan = str(implementation_plan)
1127 dod.touched_files.append(str(index_path))
1128 dod.pending_items.append("Write the introduction chapter")
1129
1130 decision = repairer.handle_empty_response(
1131 task="Create a multi-file nginx guide.",
1132 original_task=None,
1133 empty_retry_count=1,
1134 max_empty_retries=2,
1135 dod=dod,
1136 )
1137
1138 assert decision.should_continue is True
1139 assert decision.retry_message is not None
1140 assert "Next missing planned artifact: `introduction.html`" in decision.retry_message
1141 assert (
1142 "Resume with this exact next step: continue `Write the introduction chapter` "
1143 "by creating `introduction.html`."
1144 in decision.retry_message
1145 )
1146 assert "Next declared output under `chapters/`" not in decision.retry_message
1147 assert (
1148 f"Prefer one `write(content=...)` call for `{(chapters / 'introduction.html').resolve(strict=False)}` "
1149 "before more research."
1150 in decision.retry_message
1151 )
1152
1153
1154 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
1155 temp_dir: Path,
1156 ) -> None:
1157 context = build_context(
1158 temp_dir=temp_dir,
1159 use_react=False,
1160 )
1161 repairer = ResponseRepairer(context)
1162
1163 guide_root = temp_dir / "guides" / "nginx"
1164 chapters = guide_root / "chapters"
1165 chapters.mkdir(parents=True)
1166 index_path = guide_root / "index.html"
1167 chapter_one = chapters / "01-introduction.html"
1168 index_path.write_text("<html></html>\n")
1169 chapter_one.write_text("<html></html>\n")
1170
1171 implementation_plan = temp_dir / "implementation.md"
1172 implementation_plan.write_text(
1173 "\n".join(
1174 [
1175 "# Implementation Plan",
1176 "",
1177 "## File Changes",
1178 f"- `{guide_root}/`",
1179 f"- `{chapters}/`",
1180 f"- `{index_path}`",
1181 f"- `{chapters / '02-installation.html'}`",
1182 "",
1183 ]
1184 )
1185 )
1186
1187 dod = create_definition_of_done("Create a multi-file nginx guide.")
1188 dod.implementation_plan = str(implementation_plan)
1189 dod.touched_files.extend([str(index_path), str(chapter_one)])
1190 dod.completed_items.extend(
1191 [
1192 "Create index.html for nginx guide",
1193 "Create first chapter file (01-introduction.html)",
1194 ]
1195 )
1196 dod.pending_items.append("Create second chapter file (02-installation.html)")
1197
1198 decision = repairer.handle_empty_response(
1199 task="Create a multi-file nginx guide.",
1200 original_task=None,
1201 empty_retry_count=2,
1202 max_empty_retries=2,
1203 dod=dod,
1204 )
1205
1206 assert decision.should_continue is True
1207 assert decision.retry_message is not None
1208 assert (
1209 "Resume with this exact next step: continue `Create second chapter file "
1210 "(02-installation.html)` by creating `02-installation.html`."
1211 in decision.retry_message
1212 )
1213 assert (
1214 f"Prefer one `write(content=...)` call for `{chapters / '02-installation.html'}` "
1215 "before more research."
1216 in decision.retry_message
1217 )
1218 assert "Do not return another working note or empty response" in decision.retry_message
1219
1220
1221 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
1222 temp_dir: Path,
1223 ) -> None:
1224 context = build_context(
1225 temp_dir=temp_dir,
1226 use_react=False,
1227 )
1228 repairer = ResponseRepairer(context)
1229
1230 guide_root = temp_dir / "guides" / "nginx"
1231 chapters = guide_root / "chapters"
1232 chapters.mkdir(parents=True)
1233 index_path = guide_root / "index.html"
1234 chapter_one = chapters / "01-introduction.html"
1235 index_path.write_text(
1236 "\n".join(
1237 [
1238 "<html>",
1239 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
1240 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1241 "</html>",
1242 ]
1243 )
1244 + "\n"
1245 )
1246 chapter_one.write_text("<html></html>\n")
1247
1248 implementation_plan = temp_dir / "implementation.md"
1249 implementation_plan.write_text(
1250 "\n".join(
1251 [
1252 "# Implementation Plan",
1253 "",
1254 "## File Changes",
1255 f"- `{guide_root}/`",
1256 f"- `{chapters}/`",
1257 f"- `{index_path}`",
1258 f"- `{chapters / '02-installation.html'}`",
1259 "",
1260 ]
1261 )
1262 )
1263
1264 dod = create_definition_of_done("Create a multi-file nginx guide.")
1265 dod.implementation_plan = str(implementation_plan)
1266 dod.touched_files.extend([str(index_path), str(chapter_one)])
1267 dod.completed_items.extend(
1268 [
1269 "Create index.html for nginx guide",
1270 "Create Chapter 1: Introduction to NGINX Tool",
1271 ]
1272 )
1273 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
1274
1275 decision = repairer.handle_empty_response(
1276 task="Create a multi-file nginx guide.",
1277 original_task=None,
1278 empty_retry_count=2,
1279 max_empty_retries=2,
1280 dod=dod,
1281 )
1282
1283 assert decision.should_continue is True
1284 assert decision.retry_message is not None
1285 assert (
1286 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
1287 "by creating `02-installation.html`."
1288 in decision.retry_message
1289 )
1290 assert (
1291 f"Prefer one `write(content=...)` call for `{(chapters / '02-installation.html').resolve(strict=False)}` "
1292 "before more research."
1293 in decision.retry_message
1294 )
1295 assert (
1296 f'Emit this tool shape now: `write(file_path="{(chapters / "02-installation.html").resolve(strict=False)}", content="...")`.'
1297 in decision.retry_message
1298 )
1299 assert (
1300 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
1301 "so it matches the current guide structure."
1302 in decision.retry_message
1303 )
1304
1305
1306 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
1307 temp_dir: Path,
1308 ) -> None:
1309 context = build_context(
1310 temp_dir=temp_dir,
1311 use_react=False,
1312 )
1313 repairer = ResponseRepairer(context)
1314
1315 guide_root = temp_dir / "guides" / "nginx"
1316 chapters = guide_root / "chapters"
1317 chapters.mkdir(parents=True)
1318 chapter_one = chapters / "01-introduction.html"
1319 chapter_one.write_text("<html></html>\n")
1320
1321 implementation_plan = temp_dir / "implementation.md"
1322 implementation_plan.write_text(
1323 "\n".join(
1324 [
1325 "# Implementation Plan",
1326 "",
1327 "## File Changes",
1328 f"- `{guide_root}/`",
1329 f"- `{chapters}/`",
1330 f"- `{guide_root / 'index.html'}`",
1331 f"- `{chapters / '01-introduction.html'}`",
1332 "",
1333 ]
1334 )
1335 )
1336
1337 dod = create_definition_of_done("Create a multi-file nginx guide.")
1338 dod.implementation_plan = str(implementation_plan)
1339 dod.touched_files.append(str(chapter_one))
1340 dod.completed_items.append("Create first chapter file (01-introduction.html)")
1341 dod.pending_items.append("Develop the main index.html file for the nginx guide")
1342
1343 recovery_context = RecoveryContext(
1344 original_tool="write",
1345 original_args={
1346 "file_path": "~/Loader/guides/nginx/index.html",
1347 "content_chars": 1354,
1348 "content_lines": 30,
1349 },
1350 )
1351 recovery_context.add_attempt(
1352 "write",
1353 {
1354 "file_path": "~/Loader/guides/nginx/index.html",
1355 "content_chars": 1354,
1356 "content_lines": 30,
1357 },
1358 "WriteTool.execute() missing 1 required positional argument: 'content'",
1359 )
1360 context.recovery_context = recovery_context
1361
1362 decision = repairer.handle_empty_response(
1363 task="Create a multi-file nginx guide.",
1364 original_task=None,
1365 empty_retry_count=2,
1366 max_empty_retries=2,
1367 dod=dod,
1368 )
1369
1370 assert decision.should_continue is True
1371 assert decision.retry_message is not None
1372 assert "resend `write`" in decision.retry_message
1373 assert "content_chars" in decision.retry_message
1374 assert "index.html" in decision.retry_message
1375
1376
1377 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
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-introduction.html"
1391 index_path.write_text(
1392 "\n".join(
1393 [
1394 "<html>",
1395 '<a href="chapters/01-introduction.html">Introduction</a>',
1396 '<a href="chapters/02-installation.html">Installation</a>',
1397 "</html>",
1398 ]
1399 )
1400 + "\n"
1401 )
1402 chapter_one.write_text("<html></html>\n")
1403
1404 implementation_plan = temp_dir / "implementation.md"
1405 implementation_plan.write_text(
1406 "\n".join(
1407 [
1408 "# Implementation Plan",
1409 "",
1410 "## File Changes",
1411 f"- `{guide_root}/`",
1412 f"- `{chapters}/`",
1413 f"- `{index_path}`",
1414 f"- `{chapters / '02-installation.html'}`",
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.extend([str(index_path), str(chapter_one)])
1423 dod.completed_items.extend(
1424 [
1425 "Create index.html for nginx guide",
1426 "Create first chapter file (01-introduction.html)",
1427 ]
1428 )
1429 dod.pending_items.append("Create second chapter file (02-installation.html)")
1430
1431 decision = repairer.handle_empty_response(
1432 task="Create a multi-file nginx guide.",
1433 original_task=None,
1434 empty_retry_count=1,
1435 max_empty_retries=2,
1436 dod=dod,
1437 )
1438
1439 assert decision.should_continue is True
1440 assert decision.retry_message is not None
1441 assert "Continue from the exact next step below." in decision.retry_message
1442 assert "Confirmed completed work:" not in decision.retry_message
1443 assert "Next pending item:" not in decision.retry_message
1444 assert (
1445 "Resume with this exact next step: continue `Create second chapter file "
1446 "(02-installation.html)` by creating `02-installation.html`."
1447 in decision.retry_message
1448 )
1449
1450
1451 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
1452 temp_dir: Path,
1453 ) -> None:
1454 context = build_context(
1455 temp_dir=temp_dir,
1456 use_react=False,
1457 )
1458 repairer = ResponseRepairer(context)
1459
1460 guide_root = temp_dir / "guides" / "nginx"
1461 chapters = guide_root / "chapters"
1462 chapters.mkdir(parents=True)
1463 index_path = guide_root / "index.html"
1464 chapter_one = chapters / "01-introduction.html"
1465 index_path.write_text("<html></html>\n")
1466 chapter_one.write_text("<html></html>\n")
1467
1468 implementation_plan = temp_dir / "implementation.md"
1469 implementation_plan.write_text(
1470 "\n".join(
1471 [
1472 "# Implementation Plan",
1473 "",
1474 "## File Changes",
1475 f"- `{guide_root}/`",
1476 f"- `{chapters}/`",
1477 f"- `{index_path}`",
1478 f"- `{chapter_one}`",
1479 f"- `{chapters / '02-installation.html'}`",
1480 "",
1481 ]
1482 )
1483 )
1484
1485 dod = create_definition_of_done("Create a multi-file nginx guide.")
1486 dod.implementation_plan = str(implementation_plan)
1487 dod.touched_files.extend([str(index_path), str(chapter_one)])
1488 dod.completed_items.extend(
1489 [
1490 "Develop the main index.html file for the nginx guide",
1491 "Create first chapter file (01-introduction.html)",
1492 ]
1493 )
1494 dod.pending_items.extend(
1495 [
1496 "Create the nginx directory structure",
1497 "Create second chapter file (02-installation.html)",
1498 ]
1499 )
1500
1501 decision = repairer.handle_empty_response(
1502 task="Create a multi-file nginx guide.",
1503 original_task=None,
1504 empty_retry_count=1,
1505 max_empty_retries=2,
1506 dod=dod,
1507 )
1508
1509 assert decision.should_continue is True
1510 assert decision.retry_message is not None
1511 assert "Create the nginx directory structure" not in decision.retry_message
1512 assert "02-installation.html" in decision.retry_message
1513
1514
1515 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
1516 temp_dir: Path,
1517 ) -> None:
1518 context = build_context(
1519 temp_dir=temp_dir,
1520 use_react=False,
1521 )
1522 repairer = ResponseRepairer(context)
1523
1524 guide_root = temp_dir / "guides" / "nginx"
1525 chapters = guide_root / "chapters"
1526 chapters.mkdir(parents=True)
1527 index_path = guide_root / "index.html"
1528 chapter_one = chapters / "01-getting-started.html"
1529 chapter_two = chapters / "02-installation.html"
1530 chapter_three = chapters / "03-first-website.html"
1531 chapter_four = chapters / "04-configuration-basics.html"
1532 index_path.write_text("<html></html>\n")
1533 chapter_one.write_text("<h1>One</h1>\n")
1534 chapter_two.write_text("<h1>Two</h1>\n")
1535 chapter_three.write_text("<h1>Three</h1>\n")
1536
1537 implementation_plan = temp_dir / "implementation.md"
1538 implementation_plan.write_text(
1539 "\n".join(
1540 [
1541 "# Implementation Plan",
1542 "",
1543 "## File Changes",
1544 f"- `{guide_root}/`",
1545 f"- `{chapters}/`",
1546 f"- `{index_path}`",
1547 f"- `{chapter_one}`",
1548 f"- `{chapter_two}`",
1549 f"- `{chapter_three}`",
1550 f"- `{chapter_four}`",
1551 "",
1552 ]
1553 )
1554 )
1555
1556 dod = create_definition_of_done("Create a multi-file nginx guide.")
1557 dod.implementation_plan = str(implementation_plan)
1558 dod.touched_files.extend(
1559 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
1560 )
1561 dod.completed_items.extend(
1562 [
1563 "Create the directory structure for the new nginx guide",
1564 "Create the main index.html file with proper structure",
1565 ]
1566 )
1567 dod.pending_items.append("Create each chapter file in sequence")
1568
1569 decision = repairer.handle_empty_response(
1570 task="Create a multi-file nginx guide.",
1571 original_task=None,
1572 empty_retry_count=5,
1573 max_empty_retries=2,
1574 dod=dod,
1575 )
1576
1577 assert decision.should_continue is False
1578 assert decision.final_response is not None
1579 assert "retrying 4 times" in decision.final_response
1580
1581
1582 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
1583 temp_dir: Path,
1584 ) -> None:
1585 context = build_context(
1586 temp_dir=temp_dir,
1587 use_react=False,
1588 )
1589 repairer = ResponseRepairer(context)
1590
1591 guide_root = temp_dir / "guides" / "nginx"
1592 chapters = guide_root / "chapters"
1593 chapters.mkdir(parents=True)
1594 implementation_plan = temp_dir / "implementation.md"
1595 implementation_plan.write_text(
1596 "\n".join(
1597 [
1598 "# Implementation Plan",
1599 "",
1600 "## File Changes",
1601 f"- `{guide_root / 'index.html'}`",
1602 f"- `{chapters / '01-getting-started.html'}`",
1603 f"- `{chapters / '02-installation.html'}`",
1604 "",
1605 ]
1606 )
1607 )
1608
1609 dod = create_definition_of_done("Create a multi-file nginx guide.")
1610 dod.implementation_plan = str(implementation_plan)
1611 dod.touched_files.extend(
1612 [
1613 str(guide_root / "index.html"),
1614 str(chapters / "01-getting-started.html"),
1615 ]
1616 )
1617 dod.completed_items.extend(
1618 [
1619 "Create the directory structure for the new nginx guide",
1620 "Create the main index.html file with proper structure",
1621 ]
1622 )
1623 dod.pending_items.append("Create each chapter file in sequence")
1624
1625 decision = repairer.handle_empty_response(
1626 task="Create a multi-file nginx guide.",
1627 original_task=None,
1628 empty_retry_count=1,
1629 max_empty_retries=2,
1630 dod=dod,
1631 )
1632
1633 assert decision.retry_message is not None
1634 assert "Continue from the exact next step below." in decision.retry_message
1635 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
1636
1637
1638 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
1639 temp_dir: Path,
1640 ) -> None:
1641 context = build_context(
1642 temp_dir=temp_dir,
1643 use_react=False,
1644 )
1645 repairer = ResponseRepairer(context)
1646
1647 guide_root = temp_dir / "guides" / "nginx"
1648 chapters = guide_root / "chapters"
1649 chapters.mkdir(parents=True)
1650 index_path = guide_root / "index.html"
1651 chapter_one = chapters / "01-getting-started.html"
1652 chapter_two = chapters / "02-installation.html"
1653 chapter_three = chapters / "03-first-website.html"
1654 index_path.write_text("<html></html>\n")
1655 chapter_one.write_text("<h1>One</h1>\n")
1656 chapter_two.write_text("<h1>Two</h1>\n")
1657
1658 implementation_plan = temp_dir / "implementation.md"
1659 implementation_plan.write_text(
1660 "\n".join(
1661 [
1662 "# Implementation Plan",
1663 "",
1664 "## File Changes",
1665 f"- `{guide_root}/`",
1666 f"- `{chapters}/`",
1667 f"- `{index_path}`",
1668 f"- `{chapter_one}`",
1669 f"- `{chapter_two}`",
1670 f"- `{chapter_three}`",
1671 "",
1672 ]
1673 )
1674 )
1675
1676 dod = create_definition_of_done("Create a multi-file nginx guide.")
1677 dod.implementation_plan = str(implementation_plan)
1678 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
1679 dod.completed_items.extend(
1680 [
1681 "Create the main index.html file with proper structure",
1682 "Link all chapters together properly",
1683 ]
1684 )
1685 dod.pending_items.append("Create each chapter file in sequence")
1686
1687 decision = repairer.handle_empty_response(
1688 task="Create a multi-file nginx guide.",
1689 original_task=None,
1690 empty_retry_count=1,
1691 max_empty_retries=2,
1692 dod=dod,
1693 )
1694
1695 assert decision.retry_message is not None
1696 assert "Link all chapters together properly" not in decision.retry_message
1697 assert "Continue from the exact next step below." in decision.retry_message
1698 assert "Resume with this exact next step:" in decision.retry_message
1699
1700
1701 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
1702 temp_dir: Path,
1703 ) -> None:
1704 context = build_context(
1705 temp_dir=temp_dir,
1706 use_react=False,
1707 )
1708 repairer = ResponseRepairer(context)
1709
1710 reference_chapters = temp_dir / "fortran" / "chapters"
1711 reference_chapters.mkdir(parents=True)
1712 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
1713
1714 guide_root = temp_dir / "guides" / "nginx"
1715 chapters = guide_root / "chapters"
1716 chapters.mkdir(parents=True)
1717 index_path = guide_root / "index.html"
1718 index_path.write_text("<html></html>\n")
1719
1720 implementation_plan = temp_dir / "implementation.md"
1721 implementation_plan.write_text(
1722 "\n".join(
1723 [
1724 "# Implementation Plan",
1725 "",
1726 "## File Changes",
1727 f"- `{guide_root}/`",
1728 f"- `{chapters}/`",
1729 f"- `{index_path}`",
1730 "",
1731 ]
1732 )
1733 )
1734
1735 dod = create_definition_of_done("Create a multi-file nginx guide.")
1736 dod.implementation_plan = str(implementation_plan)
1737 dod.touched_files.append(str(index_path))
1738 dod.pending_items.append("Write the introduction chapter")
1739 context.session.append(
1740 Message(
1741 role=Role.ASSISTANT,
1742 content="",
1743 tool_calls=[
1744 ToolCall(
1745 id="read-ref-1",
1746 name="read",
1747 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
1748 )
1749 ],
1750 )
1751 )
1752
1753 decision = repairer.handle_empty_response(
1754 task="Create a multi-file nginx guide.",
1755 original_task=None,
1756 empty_retry_count=1,
1757 max_empty_retries=2,
1758 dod=dod,
1759 )
1760
1761 assert decision.should_continue is True
1762 assert decision.retry_message is not None
1763 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
1764 assert (
1765 "Resume with this exact next step: continue `Write the introduction chapter` "
1766 "by creating `01-introduction.html`."
1767 in decision.retry_message
1768 )
1769 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
1770 assert (
1771 "It mirrors the observed filename pattern from another `chapters/` directory "
1772 "you already inspected."
1773 in decision.retry_message
1774 )