Python · 55211 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_prefers_output_index_over_reference_index_with_same_name(
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 nginx_root = temp_dir / "Loader" / "guides" / "nginx"
966 fortran_root = temp_dir / "Loader" / "guides" / "fortran"
967 nginx_root.mkdir(parents=True)
968 fortran_root.mkdir(parents=True)
969 reference_index = fortran_root / "index.html"
970 reference_index.write_text("<html>fortran</html>\n")
971 output_index = nginx_root / "index.html"
972
973 implementation_plan = temp_dir / "implementation.md"
974 implementation_plan.write_text(
975 "\n".join(
976 [
977 "# Implementation Plan",
978 "",
979 "## File Changes",
980 f"- `{output_index}`",
981 f"- `{nginx_root / 'chapters'}/`",
982 f"- `{reference_index}`",
983 "",
984 ]
985 )
986 )
987
988 dod = create_definition_of_done("Create a multi-file nginx guide.")
989 dod.implementation_plan = str(implementation_plan)
990 dod.touched_files.append(str(reference_index))
991 dod.completed_items.append(
992 "First, examine the existing Fortran guide structure and content"
993 )
994 dod.pending_items.append("Develop the nginx index.html file")
995
996 decision = repairer.handle_empty_response(
997 task="Create a multi-file nginx guide.",
998 original_task=None,
999 empty_retry_count=2,
1000 max_empty_retries=2,
1001 dod=dod,
1002 )
1003
1004 assert decision.should_continue is True
1005 assert decision.retry_message is not None
1006 assert (
1007 f"Prefer one `write(content=...)` call for `{output_index}` before more research."
1008 in decision.retry_message
1009 )
1010 assert str(reference_index) not in decision.retry_message
1011
1012
1013 def test_empty_response_retry_points_at_declared_child_file_within_incomplete_output_directory(
1014 temp_dir: Path,
1015 ) -> None:
1016 context = build_context(
1017 temp_dir=temp_dir,
1018 use_react=False,
1019 )
1020 repairer = ResponseRepairer(context)
1021
1022 guide_root = temp_dir / "guides" / "nginx"
1023 chapters = guide_root / "chapters"
1024 chapters.mkdir(parents=True)
1025 index_path = guide_root / "index.html"
1026 index_path.write_text(
1027 "\n".join(
1028 [
1029 "<html>",
1030 '<a href="chapters/introduction.html">Introduction</a>',
1031 '<a href="chapters/installation.html">Installation</a>',
1032 "</html>",
1033 ]
1034 )
1035 + "\n"
1036 )
1037
1038 implementation_plan = temp_dir / "implementation.md"
1039 implementation_plan.write_text(
1040 "\n".join(
1041 [
1042 "# Implementation Plan",
1043 "",
1044 "## File Changes",
1045 f"- `{guide_root}/`",
1046 f"- `{chapters}/`",
1047 f"- `{index_path}`",
1048 f"- `{chapters / '02-installation.html'}`",
1049 "",
1050 ]
1051 )
1052 )
1053
1054 dod = create_definition_of_done("Create a multi-file nginx guide.")
1055 dod.implementation_plan = str(implementation_plan)
1056 dod.touched_files.append(str(index_path))
1057 dod.pending_items.append("Write the introduction chapter")
1058
1059 decision = repairer.handle_empty_response(
1060 task="Create a multi-file nginx guide.",
1061 original_task=None,
1062 empty_retry_count=1,
1063 max_empty_retries=2,
1064 dod=dod,
1065 )
1066
1067 assert decision.should_continue is True
1068 assert decision.retry_message is not None
1069 assert "Next missing planned artifact: `introduction.html`" in decision.retry_message
1070 assert (
1071 "Resume with this exact next step: continue `Write the introduction chapter` "
1072 "by creating `introduction.html`."
1073 in decision.retry_message
1074 )
1075 assert "Next declared output under `chapters/`" not in decision.retry_message
1076 assert (
1077 f"Prefer one `write(content=...)` call for `{(chapters / 'introduction.html').resolve(strict=False)}` "
1078 "before more research."
1079 in decision.retry_message
1080 )
1081
1082
1083 def test_empty_response_retry_infers_concrete_file_from_pending_todo_after_broad_artifacts_exist(
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("<html></html>\n")
1098 chapter_one.write_text("<html></html>\n")
1099
1100 implementation_plan = temp_dir / "implementation.md"
1101 implementation_plan.write_text(
1102 "\n".join(
1103 [
1104 "# Implementation Plan",
1105 "",
1106 "## File Changes",
1107 f"- `{guide_root}/`",
1108 f"- `{chapters}/`",
1109 f"- `{index_path}`",
1110 f"- `{chapters / '02-installation.html'}`",
1111 "",
1112 ]
1113 )
1114 )
1115
1116 dod = create_definition_of_done("Create a multi-file nginx guide.")
1117 dod.implementation_plan = str(implementation_plan)
1118 dod.touched_files.extend([str(index_path), str(chapter_one)])
1119 dod.completed_items.extend(
1120 [
1121 "Create index.html for nginx guide",
1122 "Create first chapter file (01-introduction.html)",
1123 ]
1124 )
1125 dod.pending_items.append("Create second chapter file (02-installation.html)")
1126
1127 decision = repairer.handle_empty_response(
1128 task="Create a multi-file nginx guide.",
1129 original_task=None,
1130 empty_retry_count=2,
1131 max_empty_retries=2,
1132 dod=dod,
1133 )
1134
1135 assert decision.should_continue is True
1136 assert decision.retry_message is not None
1137 assert (
1138 "Resume with this exact next step: continue `Create second chapter file "
1139 "(02-installation.html)` by creating `02-installation.html`."
1140 in decision.retry_message
1141 )
1142 assert (
1143 f"Prefer one `write(content=...)` call for `{chapters / '02-installation.html'}` "
1144 "before more research."
1145 in decision.retry_message
1146 )
1147 assert "Do not return another working note or empty response" in decision.retry_message
1148
1149
1150 def test_empty_response_retry_maps_title_style_todo_to_html_graph_target(
1151 temp_dir: Path,
1152 ) -> None:
1153 context = build_context(
1154 temp_dir=temp_dir,
1155 use_react=False,
1156 )
1157 repairer = ResponseRepairer(context)
1158
1159 guide_root = temp_dir / "guides" / "nginx"
1160 chapters = guide_root / "chapters"
1161 chapters.mkdir(parents=True)
1162 index_path = guide_root / "index.html"
1163 chapter_one = chapters / "01-introduction.html"
1164 index_path.write_text(
1165 "\n".join(
1166 [
1167 "<html>",
1168 '<a href="chapters/01-introduction.html">Chapter 1: Introduction to NGINX Tool</a>',
1169 '<a href="chapters/02-installation.html">Chapter 2: Installation and Setup</a>',
1170 "</html>",
1171 ]
1172 )
1173 + "\n"
1174 )
1175 chapter_one.write_text("<html></html>\n")
1176
1177 implementation_plan = temp_dir / "implementation.md"
1178 implementation_plan.write_text(
1179 "\n".join(
1180 [
1181 "# Implementation Plan",
1182 "",
1183 "## File Changes",
1184 f"- `{guide_root}/`",
1185 f"- `{chapters}/`",
1186 f"- `{index_path}`",
1187 f"- `{chapters / '02-installation.html'}`",
1188 "",
1189 ]
1190 )
1191 )
1192
1193 dod = create_definition_of_done("Create a multi-file nginx guide.")
1194 dod.implementation_plan = str(implementation_plan)
1195 dod.touched_files.extend([str(index_path), str(chapter_one)])
1196 dod.completed_items.extend(
1197 [
1198 "Create index.html for nginx guide",
1199 "Create Chapter 1: Introduction to NGINX Tool",
1200 ]
1201 )
1202 dod.pending_items.append("Creating Chapter 2: Installation and Setup")
1203
1204 decision = repairer.handle_empty_response(
1205 task="Create a multi-file nginx guide.",
1206 original_task=None,
1207 empty_retry_count=2,
1208 max_empty_retries=2,
1209 dod=dod,
1210 )
1211
1212 assert decision.should_continue is True
1213 assert decision.retry_message is not None
1214 assert (
1215 "Resume with this exact next step: continue `Creating Chapter 2: Installation and Setup` "
1216 "by creating `02-installation.html`."
1217 in decision.retry_message
1218 )
1219 assert (
1220 f"Prefer one `write(content=...)` call for `{(chapters / '02-installation.html').resolve(strict=False)}` "
1221 "before more research."
1222 in decision.retry_message
1223 )
1224 assert (
1225 f'Emit this tool shape now: `write(file_path="{(chapters / "02-installation.html").resolve(strict=False)}", content="...")`.'
1226 in decision.retry_message
1227 )
1228 assert (
1229 "Use the existing outline label `Chapter 2: Installation and Setup` for that file "
1230 "so it matches the current guide structure."
1231 in decision.retry_message
1232 )
1233
1234
1235 def test_empty_response_retry_reminds_model_to_resend_real_write_payload(
1236 temp_dir: Path,
1237 ) -> None:
1238 context = build_context(
1239 temp_dir=temp_dir,
1240 use_react=False,
1241 )
1242 repairer = ResponseRepairer(context)
1243
1244 guide_root = temp_dir / "guides" / "nginx"
1245 chapters = guide_root / "chapters"
1246 chapters.mkdir(parents=True)
1247 chapter_one = chapters / "01-introduction.html"
1248 chapter_one.write_text("<html></html>\n")
1249
1250 implementation_plan = temp_dir / "implementation.md"
1251 implementation_plan.write_text(
1252 "\n".join(
1253 [
1254 "# Implementation Plan",
1255 "",
1256 "## File Changes",
1257 f"- `{guide_root}/`",
1258 f"- `{chapters}/`",
1259 f"- `{guide_root / 'index.html'}`",
1260 f"- `{chapters / '01-introduction.html'}`",
1261 "",
1262 ]
1263 )
1264 )
1265
1266 dod = create_definition_of_done("Create a multi-file nginx guide.")
1267 dod.implementation_plan = str(implementation_plan)
1268 dod.touched_files.append(str(chapter_one))
1269 dod.completed_items.append("Create first chapter file (01-introduction.html)")
1270 dod.pending_items.append("Develop the main index.html file for the nginx guide")
1271
1272 recovery_context = RecoveryContext(
1273 original_tool="write",
1274 original_args={
1275 "file_path": "~/Loader/guides/nginx/index.html",
1276 "content_chars": 1354,
1277 "content_lines": 30,
1278 },
1279 )
1280 recovery_context.add_attempt(
1281 "write",
1282 {
1283 "file_path": "~/Loader/guides/nginx/index.html",
1284 "content_chars": 1354,
1285 "content_lines": 30,
1286 },
1287 "WriteTool.execute() missing 1 required positional argument: 'content'",
1288 )
1289 context.recovery_context = recovery_context
1290
1291 decision = repairer.handle_empty_response(
1292 task="Create a multi-file nginx guide.",
1293 original_task=None,
1294 empty_retry_count=2,
1295 max_empty_retries=2,
1296 dod=dod,
1297 )
1298
1299 assert decision.should_continue is True
1300 assert decision.retry_message is not None
1301 assert "resend `write`" in decision.retry_message
1302 assert "content_chars" in decision.retry_message
1303 assert "index.html" in decision.retry_message
1304
1305
1306 def test_empty_response_retry_uses_compact_prompt_after_early_progress_with_concrete_next_file(
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 index_path = guide_root / "index.html"
1319 chapter_one = chapters / "01-introduction.html"
1320 index_path.write_text(
1321 "\n".join(
1322 [
1323 "<html>",
1324 '<a href="chapters/01-introduction.html">Introduction</a>',
1325 '<a href="chapters/02-installation.html">Installation</a>',
1326 "</html>",
1327 ]
1328 )
1329 + "\n"
1330 )
1331 chapter_one.write_text("<html></html>\n")
1332
1333 implementation_plan = temp_dir / "implementation.md"
1334 implementation_plan.write_text(
1335 "\n".join(
1336 [
1337 "# Implementation Plan",
1338 "",
1339 "## File Changes",
1340 f"- `{guide_root}/`",
1341 f"- `{chapters}/`",
1342 f"- `{index_path}`",
1343 f"- `{chapters / '02-installation.html'}`",
1344 "",
1345 ]
1346 )
1347 )
1348
1349 dod = create_definition_of_done("Create a multi-file nginx guide.")
1350 dod.implementation_plan = str(implementation_plan)
1351 dod.touched_files.extend([str(index_path), str(chapter_one)])
1352 dod.completed_items.extend(
1353 [
1354 "Create index.html for nginx guide",
1355 "Create first chapter file (01-introduction.html)",
1356 ]
1357 )
1358 dod.pending_items.append("Create second chapter file (02-installation.html)")
1359
1360 decision = repairer.handle_empty_response(
1361 task="Create a multi-file nginx guide.",
1362 original_task=None,
1363 empty_retry_count=1,
1364 max_empty_retries=2,
1365 dod=dod,
1366 )
1367
1368 assert decision.should_continue is True
1369 assert decision.retry_message is not None
1370 assert "Continue from the exact next step below." in decision.retry_message
1371 assert "Confirmed completed work:" not in decision.retry_message
1372 assert "Next pending item:" not in decision.retry_message
1373 assert (
1374 "Resume with this exact next step: continue `Create second chapter file "
1375 "(02-installation.html)` by creating `02-installation.html`."
1376 in decision.retry_message
1377 )
1378
1379
1380 def test_empty_response_retry_ignores_stale_setup_todo_after_files_created(
1381 temp_dir: Path,
1382 ) -> None:
1383 context = build_context(
1384 temp_dir=temp_dir,
1385 use_react=False,
1386 )
1387 repairer = ResponseRepairer(context)
1388
1389 guide_root = temp_dir / "guides" / "nginx"
1390 chapters = guide_root / "chapters"
1391 chapters.mkdir(parents=True)
1392 index_path = guide_root / "index.html"
1393 chapter_one = chapters / "01-introduction.html"
1394 index_path.write_text("<html></html>\n")
1395 chapter_one.write_text("<html></html>\n")
1396
1397 implementation_plan = temp_dir / "implementation.md"
1398 implementation_plan.write_text(
1399 "\n".join(
1400 [
1401 "# Implementation Plan",
1402 "",
1403 "## File Changes",
1404 f"- `{guide_root}/`",
1405 f"- `{chapters}/`",
1406 f"- `{index_path}`",
1407 f"- `{chapter_one}`",
1408 f"- `{chapters / '02-installation.html'}`",
1409 "",
1410 ]
1411 )
1412 )
1413
1414 dod = create_definition_of_done("Create a multi-file nginx guide.")
1415 dod.implementation_plan = str(implementation_plan)
1416 dod.touched_files.extend([str(index_path), str(chapter_one)])
1417 dod.completed_items.extend(
1418 [
1419 "Develop the main index.html file for the nginx guide",
1420 "Create first chapter file (01-introduction.html)",
1421 ]
1422 )
1423 dod.pending_items.extend(
1424 [
1425 "Create the nginx directory structure",
1426 "Create second chapter file (02-installation.html)",
1427 ]
1428 )
1429
1430 decision = repairer.handle_empty_response(
1431 task="Create a multi-file nginx guide.",
1432 original_task=None,
1433 empty_retry_count=1,
1434 max_empty_retries=2,
1435 dod=dod,
1436 )
1437
1438 assert decision.should_continue is True
1439 assert decision.retry_message is not None
1440 assert "Create the nginx directory structure" not in decision.retry_message
1441 assert "02-installation.html" in decision.retry_message
1442
1443
1444 def test_empty_response_retry_fails_after_extended_late_stage_budget_is_exhausted(
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 index_path = guide_root / "index.html"
1457 chapter_one = chapters / "01-getting-started.html"
1458 chapter_two = chapters / "02-installation.html"
1459 chapter_three = chapters / "03-first-website.html"
1460 chapter_four = chapters / "04-configuration-basics.html"
1461 index_path.write_text("<html></html>\n")
1462 chapter_one.write_text("<h1>One</h1>\n")
1463 chapter_two.write_text("<h1>Two</h1>\n")
1464 chapter_three.write_text("<h1>Three</h1>\n")
1465
1466 implementation_plan = temp_dir / "implementation.md"
1467 implementation_plan.write_text(
1468 "\n".join(
1469 [
1470 "# Implementation Plan",
1471 "",
1472 "## File Changes",
1473 f"- `{guide_root}/`",
1474 f"- `{chapters}/`",
1475 f"- `{index_path}`",
1476 f"- `{chapter_one}`",
1477 f"- `{chapter_two}`",
1478 f"- `{chapter_three}`",
1479 f"- `{chapter_four}`",
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(
1488 [str(index_path), str(chapter_one), str(chapter_two), str(chapter_three)]
1489 )
1490 dod.completed_items.extend(
1491 [
1492 "Create the directory structure for the new nginx guide",
1493 "Create the main index.html file with proper structure",
1494 ]
1495 )
1496 dod.pending_items.append("Create each chapter file in sequence")
1497
1498 decision = repairer.handle_empty_response(
1499 task="Create a multi-file nginx guide.",
1500 original_task=None,
1501 empty_retry_count=5,
1502 max_empty_retries=2,
1503 dod=dod,
1504 )
1505
1506 assert decision.should_continue is False
1507 assert decision.final_response is not None
1508 assert "retrying 4 times" in decision.final_response
1509
1510
1511 def test_empty_response_retry_mentions_todowrite_when_progress_has_outpaced_tracking(
1512 temp_dir: Path,
1513 ) -> None:
1514 context = build_context(
1515 temp_dir=temp_dir,
1516 use_react=False,
1517 )
1518 repairer = ResponseRepairer(context)
1519
1520 guide_root = temp_dir / "guides" / "nginx"
1521 chapters = guide_root / "chapters"
1522 chapters.mkdir(parents=True)
1523 implementation_plan = temp_dir / "implementation.md"
1524 implementation_plan.write_text(
1525 "\n".join(
1526 [
1527 "# Implementation Plan",
1528 "",
1529 "## File Changes",
1530 f"- `{guide_root / 'index.html'}`",
1531 f"- `{chapters / '01-getting-started.html'}`",
1532 f"- `{chapters / '02-installation.html'}`",
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(
1541 [
1542 str(guide_root / "index.html"),
1543 str(chapters / "01-getting-started.html"),
1544 ]
1545 )
1546 dod.completed_items.extend(
1547 [
1548 "Create the directory structure for the new nginx guide",
1549 "Create the main index.html file with proper structure",
1550 ]
1551 )
1552 dod.pending_items.append("Create each chapter file in sequence")
1553
1554 decision = repairer.handle_empty_response(
1555 task="Create a multi-file nginx guide.",
1556 original_task=None,
1557 empty_retry_count=1,
1558 max_empty_retries=2,
1559 dod=dod,
1560 )
1561
1562 assert decision.retry_message is not None
1563 assert "Continue from the exact next step below." in decision.retry_message
1564 assert "refresh `TodoWrite` alongside the next concrete mutation" not in decision.retry_message
1565
1566
1567 def test_empty_response_retry_omits_stale_aggregate_completed_work_when_artifacts_missing(
1568 temp_dir: Path,
1569 ) -> None:
1570 context = build_context(
1571 temp_dir=temp_dir,
1572 use_react=False,
1573 )
1574 repairer = ResponseRepairer(context)
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 chapter_one = chapters / "01-getting-started.html"
1581 chapter_two = chapters / "02-installation.html"
1582 chapter_three = chapters / "03-first-website.html"
1583 index_path.write_text("<html></html>\n")
1584 chapter_one.write_text("<h1>One</h1>\n")
1585 chapter_two.write_text("<h1>Two</h1>\n")
1586
1587 implementation_plan = temp_dir / "implementation.md"
1588 implementation_plan.write_text(
1589 "\n".join(
1590 [
1591 "# Implementation Plan",
1592 "",
1593 "## File Changes",
1594 f"- `{guide_root}/`",
1595 f"- `{chapters}/`",
1596 f"- `{index_path}`",
1597 f"- `{chapter_one}`",
1598 f"- `{chapter_two}`",
1599 f"- `{chapter_three}`",
1600 "",
1601 ]
1602 )
1603 )
1604
1605 dod = create_definition_of_done("Create a multi-file nginx guide.")
1606 dod.implementation_plan = str(implementation_plan)
1607 dod.touched_files.extend([str(index_path), str(chapter_one), str(chapter_two)])
1608 dod.completed_items.extend(
1609 [
1610 "Create the main index.html file with proper structure",
1611 "Link all chapters together properly",
1612 ]
1613 )
1614 dod.pending_items.append("Create each chapter file in sequence")
1615
1616 decision = repairer.handle_empty_response(
1617 task="Create a multi-file nginx guide.",
1618 original_task=None,
1619 empty_retry_count=1,
1620 max_empty_retries=2,
1621 dod=dod,
1622 )
1623
1624 assert decision.retry_message is not None
1625 assert "Link all chapters together properly" not in decision.retry_message
1626 assert "Continue from the exact next step below." in decision.retry_message
1627 assert "Resume with this exact next step:" in decision.retry_message
1628
1629
1630 def test_empty_response_retry_names_next_file_from_observed_sibling_directory(
1631 temp_dir: Path,
1632 ) -> None:
1633 context = build_context(
1634 temp_dir=temp_dir,
1635 use_react=False,
1636 )
1637 repairer = ResponseRepairer(context)
1638
1639 reference_chapters = temp_dir / "fortran" / "chapters"
1640 reference_chapters.mkdir(parents=True)
1641 (reference_chapters / "01-introduction.html").write_text("<h1>Introduction</h1>\n")
1642
1643 guide_root = temp_dir / "guides" / "nginx"
1644 chapters = guide_root / "chapters"
1645 chapters.mkdir(parents=True)
1646 index_path = guide_root / "index.html"
1647 index_path.write_text("<html></html>\n")
1648
1649 implementation_plan = temp_dir / "implementation.md"
1650 implementation_plan.write_text(
1651 "\n".join(
1652 [
1653 "# Implementation Plan",
1654 "",
1655 "## File Changes",
1656 f"- `{guide_root}/`",
1657 f"- `{chapters}/`",
1658 f"- `{index_path}`",
1659 "",
1660 ]
1661 )
1662 )
1663
1664 dod = create_definition_of_done("Create a multi-file nginx guide.")
1665 dod.implementation_plan = str(implementation_plan)
1666 dod.touched_files.append(str(index_path))
1667 dod.pending_items.append("Write the introduction chapter")
1668 context.session.append(
1669 Message(
1670 role=Role.ASSISTANT,
1671 content="",
1672 tool_calls=[
1673 ToolCall(
1674 id="read-ref-1",
1675 name="read",
1676 arguments={"file_path": str(reference_chapters / "01-introduction.html")},
1677 )
1678 ],
1679 )
1680 )
1681
1682 decision = repairer.handle_empty_response(
1683 task="Create a multi-file nginx guide.",
1684 original_task=None,
1685 empty_retry_count=1,
1686 max_empty_retries=2,
1687 dod=dod,
1688 )
1689
1690 assert decision.should_continue is True
1691 assert decision.retry_message is not None
1692 assert "Next missing planned artifact: `01-introduction.html`" in decision.retry_message
1693 assert (
1694 "Resume with this exact next step: continue `Write the introduction chapter` "
1695 "by creating `01-introduction.html`."
1696 in decision.retry_message
1697 )
1698 assert "Next observed output pattern under `chapters/`" not in decision.retry_message
1699 assert (
1700 "It mirrors the observed filename pattern from another `chapters/` directory "
1701 "you already inspected."
1702 in decision.retry_message
1703 )