Python · 29031 bytes Raw Blame History
1 """Tests for definition-of-done state and persistence."""
2
3 import json
4 import subprocess
5 from pathlib import Path
6
7 from loader.llm.base import ToolCall
8 from loader.runtime.dod import (
9 DefinitionOfDoneStore,
10 VerificationEvidence,
11 all_planned_artifact_outputs_exist,
12 all_planned_artifacts_exist,
13 begin_new_verification_attempt,
14 build_verification_summary,
15 collect_planned_artifact_targets,
16 create_definition_of_done,
17 derive_verification_commands,
18 determine_task_size,
19 ensure_active_verification_attempt,
20 record_successful_tool_call,
21 sanitize_verification_commands,
22 )
23
24
25 def test_determine_task_size_boundaries() -> None:
26 assert determine_task_size(1, 10) == "small"
27 assert determine_task_size(3, 99) == "small"
28 assert determine_task_size(4, 99) == "standard"
29 assert determine_task_size(15, 499) == "standard"
30 assert determine_task_size(16, 499) == "large"
31 assert determine_task_size(15, 500) == "large"
32
33
34 def test_definition_of_done_round_trip(tmp_path: Path) -> None:
35 store = DefinitionOfDoneStore(tmp_path)
36 dod = create_definition_of_done(
37 "Create hello.py and verify it runs.",
38 retry_budget=2,
39 )
40 dod.status = "fixing"
41 dod.retry_count = 1
42 dod.verification_commands = ["python hello.py"]
43 dod.touched_files = [str(tmp_path / "hello.py")]
44 attempt = begin_new_verification_attempt(dod)
45 saved_path = store.save(dod)
46
47 reloaded = store.load(saved_path)
48
49 assert reloaded.task_statement == dod.task_statement
50 assert reloaded.status == "fixing"
51 assert reloaded.retry_count == 1
52 assert reloaded.verification_commands == ["python hello.py"]
53 assert reloaded.touched_files == [str(tmp_path / "hello.py")]
54 assert reloaded.active_verification_attempt_id == attempt.attempt_id
55 assert reloaded.active_verification_attempt_number == attempt.attempt_number
56
57
58 def test_ensure_active_verification_attempt_rehydrates_missing_active_attempt() -> None:
59 dod = create_definition_of_done("Verify the runtime output.")
60 dod.verification_attempt_counter = 2
61
62 attempt = ensure_active_verification_attempt(dod)
63
64 assert attempt.attempt_id == "verification-attempt-2"
65 assert attempt.attempt_number == 2
66 assert dod.active_verification_attempt_id == "verification-attempt-2"
67 assert dod.active_verification_attempt_number == 2
68
69
70 def test_verification_command_derivation_prefers_runtime_evidence(tmp_path: Path) -> None:
71 project_root = tmp_path
72 dod = create_definition_of_done("Create hello.py and make sure it runs.")
73 hello_path = project_root / "hello.py"
74 record_successful_tool_call(
75 dod,
76 ToolCall(
77 id="write-1",
78 name="write",
79 arguments={"file_path": str(hello_path), "content": "print('hi')\n"},
80 ),
81 )
82 record_successful_tool_call(
83 dod,
84 ToolCall(
85 id="bash-1",
86 name="bash",
87 arguments={"command": "python hello.py"},
88 ),
89 )
90
91 commands = derive_verification_commands(
92 dod,
93 project_root=project_root,
94 task_statement=dod.task_statement,
95 )
96
97 assert commands == ["python hello.py"]
98
99
100 def test_record_successful_tool_call_preserves_absolute_path_string(tmp_path: Path) -> None:
101 dod = create_definition_of_done("Create hello.py and verify it exists.")
102 absolute_path = tmp_path / "hello.py"
103
104 record_successful_tool_call(
105 dod,
106 ToolCall(
107 id="write-1",
108 name="write",
109 arguments={"file_path": str(absolute_path), "content": "print('hi')\n"},
110 ),
111 )
112
113 assert dod.touched_files == [str(absolute_path)]
114
115
116 def test_record_successful_tool_call_counts_json_string_patch_hunks(
117 tmp_path: Path,
118 ) -> None:
119 dod = create_definition_of_done("Patch generated HTML content.")
120 target = tmp_path / "chapter.html"
121 hunks = json.dumps(
122 [
123 {
124 "old_start": 10,
125 "old_lines": 2,
126 "new_start": 10,
127 "new_lines": 8,
128 "lines": ["-old", "-body", "+new", "+expanded"],
129 }
130 ]
131 )[:-1]
132
133 record_successful_tool_call(
134 dod,
135 ToolCall(
136 id="patch-1",
137 name="patch",
138 arguments={"file_path": str(target), "hunks": hunks},
139 ),
140 )
141
142 assert dod.touched_files == [str(target)]
143 assert dod.line_changes == 8
144
145
146 def test_record_successful_tool_call_counts_path_content_edit(tmp_path: Path) -> None:
147 dod = create_definition_of_done("Replace generated HTML content.")
148 target = tmp_path / "index.html"
149
150 record_successful_tool_call(
151 dod,
152 ToolCall(
153 id="edit-1",
154 name="edit",
155 arguments={
156 "path": str(target),
157 "content": "<h1>Guide</h1>\n<p>Expanded.</p>\n",
158 },
159 ),
160 )
161
162 assert dod.touched_files == [str(target)]
163 assert dod.line_changes == 3
164
165
166 def test_derive_verification_commands_adds_semantic_html_toc_check(tmp_path: Path) -> None:
167 chapters = tmp_path / "chapters"
168 chapters.mkdir()
169 (chapters / "01-introduction.html").write_text(
170 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
171 )
172 index = tmp_path / "index.html"
173 index.write_text(
174 "\n".join(
175 [
176 '<ul class="chapter-list">',
177 ' <li><a href="chapters/01-introduction.html">Chapter 1: Introduction to Fortran</a></li>',
178 "</ul>",
179 ]
180 )
181 )
182
183 dod = create_definition_of_done(
184 "Update index.html so the table of contents links and hrefs are correct."
185 )
186 dod.acceptance_criteria = [
187 "All table of contents links in index.html point to existing chapter files.",
188 "All link texts match the actual chapter titles.",
189 ]
190 dod.touched_files = [str(index)]
191
192 commands = derive_verification_commands(
193 dod,
194 project_root=tmp_path,
195 task_statement=dod.task_statement,
196 )
197
198 assert any(command.startswith("python3 - <<'PY'") for command in commands)
199 assert not any(command == f"test -f {index}" for command in commands)
200
201
202 def test_derive_verification_commands_avoids_repo_defaults_for_external_artifacts(
203 tmp_path: Path,
204 ) -> None:
205 (tmp_path / "pyproject.toml").write_text("[project]\nname='loader'\n")
206 (tmp_path / "package.json").write_text("{}\n")
207 external_root = tmp_path.parent / "external-guide"
208 external_root.mkdir(exist_ok=True)
209 external_index = external_root / "index.html"
210 external_index.write_text("<html></html>\n")
211
212 dod = create_definition_of_done("Create an external nginx guide.")
213 dod.task_size = "standard"
214 dod.touched_files = [str(external_index)]
215
216 commands = derive_verification_commands(
217 dod,
218 project_root=tmp_path,
219 task_statement=dod.task_statement,
220 )
221
222 assert commands == [f"test -f {external_index}"]
223
224
225 def test_derive_verification_commands_adds_generic_local_html_link_check(
226 tmp_path: Path,
227 ) -> None:
228 docs = tmp_path / "docs"
229 docs.mkdir()
230 index = docs / "index.html"
231 index.write_text('<a href="chapters/01-intro.html">Intro</a>\n')
232
233 dod = create_definition_of_done("Create a small multi-page HTML guide.")
234 dod.touched_files = [str(index)]
235
236 commands = derive_verification_commands(
237 dod,
238 project_root=tmp_path,
239 task_statement=dod.task_statement,
240 supplement_existing=True,
241 )
242
243 assert any("Missing local HTML links:" in command for command in commands)
244
245
246 def test_derive_verification_commands_adds_planned_artifact_existence_checks(
247 tmp_path: Path,
248 ) -> None:
249 implementation_plan = tmp_path / "implementation.md"
250 implementation_plan.write_text(
251 "\n".join(
252 [
253 "# Implementation Plan",
254 "",
255 "## File Changes",
256 "- `docs/index.html`",
257 "- `docs/chapters/01-intro.html`",
258 "- `docs/chapters/02-installation.html`",
259 "- `docs/chapters/`",
260 ]
261 )
262 )
263
264 dod = create_definition_of_done("Create a multi-page HTML guide.")
265 dod.implementation_plan = str(implementation_plan)
266
267 commands = derive_verification_commands(
268 dod,
269 project_root=tmp_path,
270 task_statement=dod.task_statement,
271 supplement_existing=True,
272 )
273
274 assert f"test -f {tmp_path / 'docs/index.html'}" in commands
275 assert f"test -f {tmp_path / 'docs/chapters/01-intro.html'}" in commands
276 assert f"test -f {tmp_path / 'docs/chapters/02-installation.html'}" in commands
277 assert f"test -d {tmp_path / 'docs/chapters'}" in commands
278
279
280 def test_derive_verification_commands_adds_html_guide_quality_check_for_thorough_guides(
281 tmp_path: Path,
282 ) -> None:
283 docs = tmp_path / "docs"
284 chapters = docs / "chapters"
285 chapters.mkdir(parents=True)
286 implementation_plan = tmp_path / "implementation.md"
287 implementation_plan.write_text(
288 "\n".join(
289 [
290 "# Implementation Plan",
291 "",
292 "## File Changes",
293 f"- `{docs / 'index.html'}`",
294 f"- `{chapters / '01-introduction.html'}`",
295 f"- `{chapters / '02-installation.html'}`",
296 f"- `{chapters / '03-configuration.html'}`",
297 f"- `{chapters / '04-troubleshooting.html'}`",
298 "",
299 ]
300 )
301 )
302
303 dod = create_definition_of_done(
304 "Create an equally thorough multi-page HTML guide with chapter files."
305 )
306 dod.implementation_plan = str(implementation_plan)
307
308 commands = derive_verification_commands(
309 dod,
310 project_root=tmp_path,
311 task_statement=dod.task_statement,
312 supplement_existing=True,
313 )
314
315 assert any("HTML guide content quality issues:" in command for command in commands)
316
317
318 def test_derive_verification_commands_uses_reference_guide_depth_floor(
319 tmp_path: Path,
320 ) -> None:
321 reference = tmp_path / "reference"
322 reference_chapters = reference / "chapters"
323 reference_chapters.mkdir(parents=True)
324 (reference / "index.html").write_text("<h1>Reference</h1>" + "<p>" + "i" * 1600 + "</p>")
325 for index in range(1, 5):
326 (reference_chapters / f"0{index}-topic.html").write_text(
327 "<h1>Reference Chapter</h1>"
328 + "".join(f"<h2>Section {section}</h2><p>{'x' * 300}</p>" for section in range(10))
329 )
330
331 guide = tmp_path / "guide"
332 chapters = guide / "chapters"
333 chapters.mkdir(parents=True)
334 (guide / "index.html").write_text("<h1>Guide</h1>" + "<p>" + "i" * 1000 + "</p>")
335 (chapters / "01-introduction.html").write_text(
336 "<h1>Intro</h1>"
337 + "".join(f"<h2>Section {section}</h2><p>{'x' * 110}</p>" for section in range(10))
338 )
339 for index in range(2, 5):
340 (chapters / f"0{index}-topic.html").write_text(
341 "<h1>Topic</h1>"
342 + "".join(f"<h2>Section {section}</h2><p>{'x' * 220}</p>" for section in range(10))
343 )
344
345 implementation_plan = tmp_path / "implementation.md"
346 implementation_plan.write_text(
347 "\n".join(
348 [
349 "# Implementation Plan",
350 "",
351 "## File Changes",
352 f"- `{guide / 'index.html'}`",
353 f"- `{chapters / '01-introduction.html'}`",
354 f"- `{chapters / '02-topic.html'}`",
355 f"- `{chapters / '03-topic.html'}`",
356 f"- `{chapters / '04-topic.html'}`",
357 "",
358 ]
359 )
360 )
361
362 dod = create_definition_of_done(
363 f"Create an equally thorough HTML guide modeled on {reference} at {guide}."
364 )
365 dod.implementation_plan = str(implementation_plan)
366
367 commands = derive_verification_commands(
368 dod,
369 project_root=tmp_path,
370 task_statement=dod.task_statement,
371 supplement_existing=True,
372 )
373 quality_command = next(
374 command for command in commands if "HTML guide content quality issues:" in command
375 )
376
377 result = subprocess.run(
378 quality_command,
379 shell=True,
380 cwd=tmp_path,
381 capture_output=True,
382 text=True,
383 check=False,
384 )
385
386 assert result.returncode == 1
387 assert "01-introduction.html: thin content" in result.stdout
388 assert "expected at least 15" in result.stdout
389
390
391 def test_html_guide_quality_check_flags_malformed_document_structure(
392 tmp_path: Path,
393 ) -> None:
394 def rich_doc(title: str) -> str:
395 body = "".join(
396 f"<h2>Section {index}</h2><p>{'x' * 180}</p><ul><li>{'y' * 90}</li></ul>"
397 for index in range(9)
398 )
399 return f"<!DOCTYPE html><html><body><h1>{title}</h1>{body}</body></html>\n"
400
401 guide = tmp_path / "guide"
402 chapters = guide / "chapters"
403 chapters.mkdir(parents=True)
404 index_path = guide / "index.html"
405 first = chapters / "01-introduction.html"
406 second = chapters / "02-installation.html"
407 third = chapters / "03-configuration.html"
408 index_path.write_text(rich_doc("Guide"))
409 first.write_text(rich_doc("Introduction"))
410 second.write_text(rich_doc("Installation").rstrip() + "\n</html>\n")
411 third.write_text(rich_doc("Configuration"))
412
413 implementation_plan = tmp_path / "implementation.md"
414 implementation_plan.write_text(
415 "\n".join(
416 [
417 "# Implementation Plan",
418 "",
419 "## File Changes",
420 f"- `{index_path}`",
421 f"- `{first}`",
422 f"- `{second}`",
423 f"- `{third}`",
424 "",
425 ]
426 )
427 )
428
429 dod = create_definition_of_done(
430 "Create an equally thorough multi-page HTML guide with chapter files."
431 )
432 dod.implementation_plan = str(implementation_plan)
433
434 commands = derive_verification_commands(
435 dod,
436 project_root=tmp_path,
437 task_statement=dod.task_statement,
438 supplement_existing=True,
439 )
440 quality_command = next(
441 command for command in commands if "HTML guide content quality issues:" in command
442 )
443 result = subprocess.run(
444 quality_command,
445 shell=True,
446 cwd=tmp_path,
447 capture_output=True,
448 text=True,
449 check=False,
450 )
451
452 assert result.returncode == 1
453 assert "02-installation.html: expected exactly one closing </html> tag" in result.stdout
454
455
456 def test_derive_verification_commands_flags_insufficient_pages_for_broad_thorough_guide(
457 tmp_path: Path,
458 ) -> None:
459 guide = tmp_path / "guide"
460 chapters = guide / "chapters"
461 chapters.mkdir(parents=True)
462 (guide / "index.html").write_text("<html></html>\n")
463 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
464
465 implementation_plan = tmp_path / "implementation.md"
466 implementation_plan.write_text(
467 "\n".join(
468 [
469 "# Implementation Plan",
470 "",
471 "## File Changes",
472 f"- `{guide / 'index.html'}`",
473 f"- `{chapters}/` (directory for chapter files)",
474 "",
475 "## Execution Order",
476 "- Create chapter files with appropriate content",
477 ]
478 )
479 )
480
481 dod = create_definition_of_done(
482 "Create an equally thorough multi-page HTML guide with chapter files."
483 )
484 dod.implementation_plan = str(implementation_plan)
485
486 commands = derive_verification_commands(
487 dod,
488 project_root=tmp_path,
489 task_statement=dod.task_statement,
490 supplement_existing=True,
491 )
492
493 assert any("insufficient HTML page count" in command for command in commands)
494
495
496 def test_sanitize_verification_commands_splits_concatenated_ls_and_directory_test(
497 tmp_path: Path,
498 ) -> None:
499 guide = tmp_path / "guides" / "nginx"
500 chapters = guide / "chapters"
501 chapters.mkdir(parents=True)
502 index = guide / "index.html"
503 index.write_text("<html></html>\n")
504 implementation_plan = tmp_path / "implementation.md"
505 implementation_plan.write_text(
506 "\n".join(
507 [
508 "# Implementation Plan",
509 "",
510 "## File Changes",
511 f"- `{index}`",
512 f"- `{chapters}/`",
513 "",
514 ]
515 )
516 )
517 dod = create_definition_of_done("Create a multi-page HTML guide.")
518 dod.implementation_plan = str(implementation_plan)
519
520 commands = sanitize_verification_commands(
521 [
522 f"ls -la {guide}/ ls -la {chapters}/",
523 f"test -f {chapters}/",
524 ],
525 dod=dod,
526 project_root=tmp_path,
527 )
528
529 assert commands == [
530 f"ls -la {guide}/",
531 f"ls -la {chapters}/",
532 f"test -d {chapters}/",
533 ]
534
535
536 def test_collect_planned_artifact_targets_ignores_prose_path_fragments_in_refreshed_plan(
537 tmp_path: Path,
538 ) -> None:
539 implementation_plan = tmp_path / "implementation.md"
540 touched_index = tmp_path / "external" / "guides" / "nginx" / "index.html"
541 touched_index.parent.mkdir(parents=True)
542 touched_index.write_text("<html></html>\n")
543 implementation_plan.write_text(
544 "\n".join(
545 [
546 "# Implementation Plan",
547 "",
548 "## File Changes",
549 "- Created main index.html file with proper structure and navigation",
550 "- Created the nginx guide directory structure (chapters/)",
551 "- Created the first chapter file (01-introduction.html) with appropriate content",
552 "",
553 "## Confirmed Progress",
554 f"- Already touched during execution: `{touched_index}`.",
555 ]
556 )
557 )
558
559 dod = create_definition_of_done("Create an external nginx guide.")
560 dod.implementation_plan = str(implementation_plan)
561
562 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
563
564 assert (tmp_path / "chapters", True) not in targets
565 assert (tmp_path / "01-introduction.html", False) not in targets
566 assert targets == [(touched_index, False)]
567
568
569 def test_collect_planned_artifact_targets_resolves_nested_file_changes_relative_to_parent_directory(
570 tmp_path: Path,
571 ) -> None:
572 implementation_plan = tmp_path / "implementation.md"
573 implementation_plan.write_text(
574 "\n".join(
575 [
576 "# Implementation Plan",
577 "",
578 "## File Changes",
579 f"- `{tmp_path / 'guide' / 'index.html'}`",
580 f"- Create chapter files in `{tmp_path / 'guide' / 'chapters'}/`:",
581 " - `00-introduction.html`",
582 " - `01-installation.html`",
583 " - `02-configuration.html`",
584 "",
585 ]
586 )
587 )
588
589 dod = create_definition_of_done("Create a multi-page guide.")
590 dod.implementation_plan = str(implementation_plan)
591
592 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
593
594 assert targets == [
595 (tmp_path / "guide" / "index.html", False),
596 (tmp_path / "guide" / "chapters", True),
597 (tmp_path / "guide" / "chapters" / "00-introduction.html", False),
598 (tmp_path / "guide" / "chapters" / "01-installation.html", False),
599 (tmp_path / "guide" / "chapters" / "02-configuration.html", False),
600 ]
601
602
603 def test_collect_planned_artifact_targets_ignores_read_only_reference_paths(
604 tmp_path: Path,
605 ) -> None:
606 implementation_plan = tmp_path / "implementation.md"
607 implementation_plan.write_text(
608 "\n".join(
609 [
610 "# Implementation Plan",
611 "",
612 "## File Changes",
613 f"- `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'index.html'}`",
614 f"- `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'chapters'}/`",
615 "- Read `~/Loader/guides/fortran/index.html`",
616 "- Read files in `~/Loader/guides/fortran/chapters/`",
617 "",
618 ]
619 )
620 )
621
622 dod = create_definition_of_done("Create an nginx guide from a Fortran reference.")
623 dod.implementation_plan = str(implementation_plan)
624
625 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
626
627 assert targets == [
628 (tmp_path / "Loader" / "guides" / "nginx" / "index.html", False),
629 (tmp_path / "Loader" / "guides" / "nginx" / "chapters", True),
630 ]
631
632
633 def test_collect_planned_artifact_targets_ignores_nested_read_only_reference_paths(
634 tmp_path: Path,
635 ) -> None:
636 implementation_plan = tmp_path / "implementation.md"
637 implementation_plan.write_text(
638 "\n".join(
639 [
640 "# Implementation Plan",
641 "",
642 "## File Changes",
643 "1. Create directory structure for nginx guide:",
644 f" - `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'index.html'}`",
645 f" - `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'chapters'}/`",
646 "2. Analyze existing fortran guide structure to understand the format:",
647 " - `~/Loader/guides/fortran/`",
648 " - `~/Loader/guides/fortran/chapters/`",
649 "3. Create nginx guide content following the same structure and cadence as the fortran guide",
650 "",
651 ]
652 )
653 )
654
655 dod = create_definition_of_done("Create an nginx guide from a Fortran reference.")
656 dod.implementation_plan = str(implementation_plan)
657
658 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
659
660 assert targets == [
661 (tmp_path / "Loader" / "guides" / "nginx" / "index.html", False),
662 (tmp_path / "Loader" / "guides" / "nginx" / "chapters", True),
663 ]
664
665
666 def test_all_planned_artifacts_exist_requires_file_contents_for_planned_output_directory(
667 tmp_path: Path,
668 ) -> None:
669 implementation_plan = tmp_path / "implementation.md"
670 implementation_plan.write_text(
671 "\n".join(
672 [
673 "# Implementation Plan",
674 "",
675 "## File Changes",
676 f"- `{tmp_path / 'guide' / 'index.html'}`",
677 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
678 "",
679 "## Execution Order",
680 "- Create chapter files with appropriate content",
681 ]
682 )
683 )
684
685 guide_root = tmp_path / "guide"
686 chapters = guide_root / "chapters"
687 guide_root.mkdir()
688 chapters.mkdir()
689 (guide_root / "index.html").write_text("<html></html>\n")
690
691 dod = create_definition_of_done("Create a multi-file guide with chapters.")
692 dod.implementation_plan = str(implementation_plan)
693 dod.completed_items = ["Create chapter files with appropriate content"]
694
695 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
696
697
698 def test_all_planned_artifacts_exist_stays_false_for_substantive_guide_with_only_one_chapter(
699 tmp_path: Path,
700 ) -> None:
701 implementation_plan = tmp_path / "implementation.md"
702 implementation_plan.write_text(
703 "\n".join(
704 [
705 "# Implementation Plan",
706 "",
707 "## File Changes",
708 f"- `{tmp_path / 'guide' / 'index.html'}`",
709 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
710 "",
711 "## Execution Order",
712 "- Create chapter files with appropriate content",
713 ]
714 )
715 )
716
717 guide_root = tmp_path / "guide"
718 chapters = guide_root / "chapters"
719 chapters.mkdir(parents=True)
720 (guide_root / "index.html").write_text("<html></html>\n")
721 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
722
723 dod = create_definition_of_done("Create an equally thorough guide with chapters.")
724 dod.implementation_plan = str(implementation_plan)
725 dod.completed_items = ["Create chapter files with appropriate content"]
726 dod.touched_files = [
727 str(guide_root / "index.html"),
728 str(chapters / "01-introduction.html"),
729 ]
730
731 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
732
733
734 def test_all_planned_artifacts_exist_respects_nested_file_change_entries(
735 tmp_path: Path,
736 ) -> None:
737 implementation_plan = tmp_path / "implementation.md"
738 implementation_plan.write_text(
739 "\n".join(
740 [
741 "# Implementation Plan",
742 "",
743 "## File Changes",
744 f"- `{tmp_path / 'guide' / 'index.html'}`",
745 f"- Create chapter files in `{tmp_path / 'guide' / 'chapters'}/`:",
746 " - `00-introduction.html`",
747 " - `01-installation.html`",
748 "",
749 ]
750 )
751 )
752
753 guide = tmp_path / "guide"
754 chapters = guide / "chapters"
755 chapters.mkdir(parents=True)
756 (guide / "index.html").write_text("<html></html>\n")
757 (chapters / "00-introduction.html").write_text("<html></html>\n")
758
759 dod = create_definition_of_done("Create a multi-page guide.")
760 dod.implementation_plan = str(implementation_plan)
761
762 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
763
764 (chapters / "01-installation.html").write_text("<h1>Installation</h1>\n")
765
766 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is True
767
768
769 def test_all_planned_artifact_outputs_stay_false_while_root_declares_missing_html_outputs(
770 tmp_path: Path,
771 ) -> None:
772 implementation_plan = tmp_path / "implementation.md"
773 implementation_plan.write_text(
774 "\n".join(
775 [
776 "# Implementation Plan",
777 "",
778 "## File Changes",
779 f"- `{tmp_path / 'guide' / 'index.html'}`",
780 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
781 "",
782 "## Execution Order",
783 "- Create chapter files with appropriate content",
784 ]
785 )
786 )
787
788 guide_root = tmp_path / "guide"
789 chapters = guide_root / "chapters"
790 guide_root.mkdir()
791 chapters.mkdir()
792 index = guide_root / "index.html"
793 index.write_text(
794 '<a href="chapters/01-introduction.html">Intro</a>\n'
795 '<a href="chapters/02-setup.html">Setup</a>\n'
796 )
797 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
798
799 dod = create_definition_of_done("Create a multi-file guide with chapters.")
800 dod.implementation_plan = str(implementation_plan)
801 dod.touched_files = [str(index), str(chapters / "01-introduction.html")]
802 dod.completed_items = ["Create chapter files with appropriate content"]
803
804 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
805 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is False
806
807 (chapters / "02-setup.html").write_text("<h1>Setup</h1>\n")
808
809 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is True
810 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is True
811
812
813 def test_collect_missing_declared_html_outputs_accepts_root_html_file_target(
814 tmp_path: Path,
815 ) -> None:
816 implementation_plan = tmp_path / "implementation.md"
817 implementation_plan.write_text(
818 "\n".join(
819 [
820 "# Implementation Plan",
821 "",
822 "## File Changes",
823 f"- `{tmp_path / 'guide' / 'index.html'}`",
824 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
825 ]
826 )
827 )
828
829 guide_root = tmp_path / "guide"
830 chapters = guide_root / "chapters"
831 chapters.mkdir(parents=True)
832 index = guide_root / "index.html"
833 index.write_text(
834 '<a href="chapters/01-introduction.html">Intro</a>\n'
835 '<a href="chapters/02-setup.html">Setup</a>\n'
836 )
837 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
838
839 dod = create_definition_of_done("Create a multi-file guide with chapters.")
840 dod.implementation_plan = str(implementation_plan)
841 dod.touched_files = [str(index), str(chapters / "01-introduction.html")]
842
843 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is False
844
845
846 def test_build_verification_summary_keeps_concrete_missing_link_details() -> None:
847 summary = build_verification_summary(
848 [
849 VerificationEvidence(
850 command="python3 - <<'PY' ... PY",
851 passed=False,
852 stderr=(
853 "Missing links:\n"
854 "chapters/05-control-structures.html -> missing\n"
855 "chapters/06-input-output.html -> missing\n"
856 ),
857 )
858 ]
859 )
860
861 assert "Missing links:" in summary
862 assert "chapters/05-control-structures.html -> missing" in summary
863 assert "chapters/06-input-output.html -> missing" in summary