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