Python · 26431 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_derive_verification_commands_adds_semantic_html_toc_check(tmp_path: Path) -> None:
147 chapters = tmp_path / "chapters"
148 chapters.mkdir()
149 (chapters / "01-introduction.html").write_text(
150 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
151 )
152 index = tmp_path / "index.html"
153 index.write_text(
154 "\n".join(
155 [
156 '<ul class="chapter-list">',
157 ' <li><a href="chapters/01-introduction.html">Chapter 1: Introduction to Fortran</a></li>',
158 "</ul>",
159 ]
160 )
161 )
162
163 dod = create_definition_of_done(
164 "Update index.html so the table of contents links and hrefs are correct."
165 )
166 dod.acceptance_criteria = [
167 "All table of contents links in index.html point to existing chapter files.",
168 "All link texts match the actual chapter titles.",
169 ]
170 dod.touched_files = [str(index)]
171
172 commands = derive_verification_commands(
173 dod,
174 project_root=tmp_path,
175 task_statement=dod.task_statement,
176 )
177
178 assert any(command.startswith("python3 - <<'PY'") for command in commands)
179 assert not any(command == f"test -f {index}" for command in commands)
180
181
182 def test_derive_verification_commands_avoids_repo_defaults_for_external_artifacts(
183 tmp_path: Path,
184 ) -> None:
185 (tmp_path / "pyproject.toml").write_text("[project]\nname='loader'\n")
186 (tmp_path / "package.json").write_text("{}\n")
187 external_root = tmp_path.parent / "external-guide"
188 external_root.mkdir(exist_ok=True)
189 external_index = external_root / "index.html"
190 external_index.write_text("<html></html>\n")
191
192 dod = create_definition_of_done("Create an external nginx guide.")
193 dod.task_size = "standard"
194 dod.touched_files = [str(external_index)]
195
196 commands = derive_verification_commands(
197 dod,
198 project_root=tmp_path,
199 task_statement=dod.task_statement,
200 )
201
202 assert commands == [f"test -f {external_index}"]
203
204
205 def test_derive_verification_commands_adds_generic_local_html_link_check(
206 tmp_path: Path,
207 ) -> None:
208 docs = tmp_path / "docs"
209 docs.mkdir()
210 index = docs / "index.html"
211 index.write_text('<a href="chapters/01-intro.html">Intro</a>\n')
212
213 dod = create_definition_of_done("Create a small multi-page HTML guide.")
214 dod.touched_files = [str(index)]
215
216 commands = derive_verification_commands(
217 dod,
218 project_root=tmp_path,
219 task_statement=dod.task_statement,
220 supplement_existing=True,
221 )
222
223 assert any("Missing local HTML links:" in command for command in commands)
224
225
226 def test_derive_verification_commands_adds_planned_artifact_existence_checks(
227 tmp_path: Path,
228 ) -> None:
229 implementation_plan = tmp_path / "implementation.md"
230 implementation_plan.write_text(
231 "\n".join(
232 [
233 "# Implementation Plan",
234 "",
235 "## File Changes",
236 "- `docs/index.html`",
237 "- `docs/chapters/01-intro.html`",
238 "- `docs/chapters/02-installation.html`",
239 "- `docs/chapters/`",
240 ]
241 )
242 )
243
244 dod = create_definition_of_done("Create a multi-page HTML guide.")
245 dod.implementation_plan = str(implementation_plan)
246
247 commands = derive_verification_commands(
248 dod,
249 project_root=tmp_path,
250 task_statement=dod.task_statement,
251 supplement_existing=True,
252 )
253
254 assert f"test -f {tmp_path / 'docs/index.html'}" in commands
255 assert f"test -f {tmp_path / 'docs/chapters/01-intro.html'}" in commands
256 assert f"test -f {tmp_path / 'docs/chapters/02-installation.html'}" in commands
257 assert f"test -d {tmp_path / 'docs/chapters'}" in commands
258
259
260 def test_derive_verification_commands_adds_html_guide_quality_check_for_thorough_guides(
261 tmp_path: Path,
262 ) -> None:
263 docs = tmp_path / "docs"
264 chapters = docs / "chapters"
265 chapters.mkdir(parents=True)
266 implementation_plan = tmp_path / "implementation.md"
267 implementation_plan.write_text(
268 "\n".join(
269 [
270 "# Implementation Plan",
271 "",
272 "## File Changes",
273 f"- `{docs / 'index.html'}`",
274 f"- `{chapters / '01-introduction.html'}`",
275 f"- `{chapters / '02-installation.html'}`",
276 f"- `{chapters / '03-configuration.html'}`",
277 f"- `{chapters / '04-troubleshooting.html'}`",
278 "",
279 ]
280 )
281 )
282
283 dod = create_definition_of_done(
284 "Create an equally thorough multi-page HTML guide with chapter files."
285 )
286 dod.implementation_plan = str(implementation_plan)
287
288 commands = derive_verification_commands(
289 dod,
290 project_root=tmp_path,
291 task_statement=dod.task_statement,
292 supplement_existing=True,
293 )
294
295 assert any("HTML guide content quality issues:" in command for command in commands)
296
297
298 def test_derive_verification_commands_uses_reference_guide_depth_floor(
299 tmp_path: Path,
300 ) -> None:
301 reference = tmp_path / "reference"
302 reference_chapters = reference / "chapters"
303 reference_chapters.mkdir(parents=True)
304 (reference / "index.html").write_text("<h1>Reference</h1>" + "<p>" + "i" * 1600 + "</p>")
305 for index in range(1, 5):
306 (reference_chapters / f"0{index}-topic.html").write_text(
307 "<h1>Reference Chapter</h1>"
308 + "".join(f"<h2>Section {section}</h2><p>{'x' * 300}</p>" for section in range(10))
309 )
310
311 guide = tmp_path / "guide"
312 chapters = guide / "chapters"
313 chapters.mkdir(parents=True)
314 (guide / "index.html").write_text("<h1>Guide</h1>" + "<p>" + "i" * 1000 + "</p>")
315 (chapters / "01-introduction.html").write_text(
316 "<h1>Intro</h1>"
317 + "".join(f"<h2>Section {section}</h2><p>{'x' * 110}</p>" for section in range(10))
318 )
319 for index in range(2, 5):
320 (chapters / f"0{index}-topic.html").write_text(
321 "<h1>Topic</h1>"
322 + "".join(f"<h2>Section {section}</h2><p>{'x' * 220}</p>" for section in range(10))
323 )
324
325 implementation_plan = tmp_path / "implementation.md"
326 implementation_plan.write_text(
327 "\n".join(
328 [
329 "# Implementation Plan",
330 "",
331 "## File Changes",
332 f"- `{guide / 'index.html'}`",
333 f"- `{chapters / '01-introduction.html'}`",
334 f"- `{chapters / '02-topic.html'}`",
335 f"- `{chapters / '03-topic.html'}`",
336 f"- `{chapters / '04-topic.html'}`",
337 "",
338 ]
339 )
340 )
341
342 dod = create_definition_of_done(
343 f"Create an equally thorough HTML guide modeled on {reference} at {guide}."
344 )
345 dod.implementation_plan = str(implementation_plan)
346
347 commands = derive_verification_commands(
348 dod,
349 project_root=tmp_path,
350 task_statement=dod.task_statement,
351 supplement_existing=True,
352 )
353 quality_command = next(
354 command for command in commands if "HTML guide content quality issues:" in command
355 )
356
357 result = subprocess.run(
358 quality_command,
359 shell=True,
360 cwd=tmp_path,
361 capture_output=True,
362 text=True,
363 check=False,
364 )
365
366 assert result.returncode == 1
367 assert "01-introduction.html: thin content" in result.stdout
368 assert "expected at least 15" in result.stdout
369
370
371 def test_derive_verification_commands_flags_insufficient_pages_for_broad_thorough_guide(
372 tmp_path: Path,
373 ) -> None:
374 guide = tmp_path / "guide"
375 chapters = guide / "chapters"
376 chapters.mkdir(parents=True)
377 (guide / "index.html").write_text("<html></html>\n")
378 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
379
380 implementation_plan = tmp_path / "implementation.md"
381 implementation_plan.write_text(
382 "\n".join(
383 [
384 "# Implementation Plan",
385 "",
386 "## File Changes",
387 f"- `{guide / 'index.html'}`",
388 f"- `{chapters}/` (directory for chapter files)",
389 "",
390 "## Execution Order",
391 "- Create chapter files with appropriate content",
392 ]
393 )
394 )
395
396 dod = create_definition_of_done(
397 "Create an equally thorough multi-page HTML guide with chapter files."
398 )
399 dod.implementation_plan = str(implementation_plan)
400
401 commands = derive_verification_commands(
402 dod,
403 project_root=tmp_path,
404 task_statement=dod.task_statement,
405 supplement_existing=True,
406 )
407
408 assert any("insufficient HTML page count" in command for command in commands)
409
410
411 def test_sanitize_verification_commands_splits_concatenated_ls_and_directory_test(
412 tmp_path: Path,
413 ) -> None:
414 guide = tmp_path / "guides" / "nginx"
415 chapters = guide / "chapters"
416 chapters.mkdir(parents=True)
417 index = guide / "index.html"
418 index.write_text("<html></html>\n")
419 implementation_plan = tmp_path / "implementation.md"
420 implementation_plan.write_text(
421 "\n".join(
422 [
423 "# Implementation Plan",
424 "",
425 "## File Changes",
426 f"- `{index}`",
427 f"- `{chapters}/`",
428 "",
429 ]
430 )
431 )
432 dod = create_definition_of_done("Create a multi-page HTML guide.")
433 dod.implementation_plan = str(implementation_plan)
434
435 commands = sanitize_verification_commands(
436 [
437 f"ls -la {guide}/ ls -la {chapters}/",
438 f"test -f {chapters}/",
439 ],
440 dod=dod,
441 project_root=tmp_path,
442 )
443
444 assert commands == [
445 f"ls -la {guide}/",
446 f"ls -la {chapters}/",
447 f"test -d {chapters}/",
448 ]
449
450
451 def test_collect_planned_artifact_targets_ignores_prose_path_fragments_in_refreshed_plan(
452 tmp_path: Path,
453 ) -> None:
454 implementation_plan = tmp_path / "implementation.md"
455 touched_index = tmp_path / "external" / "guides" / "nginx" / "index.html"
456 touched_index.parent.mkdir(parents=True)
457 touched_index.write_text("<html></html>\n")
458 implementation_plan.write_text(
459 "\n".join(
460 [
461 "# Implementation Plan",
462 "",
463 "## File Changes",
464 "- Created main index.html file with proper structure and navigation",
465 "- Created the nginx guide directory structure (chapters/)",
466 "- Created the first chapter file (01-introduction.html) with appropriate content",
467 "",
468 "## Confirmed Progress",
469 f"- Already touched during execution: `{touched_index}`.",
470 ]
471 )
472 )
473
474 dod = create_definition_of_done("Create an external nginx guide.")
475 dod.implementation_plan = str(implementation_plan)
476
477 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
478
479 assert (tmp_path / "chapters", True) not in targets
480 assert (tmp_path / "01-introduction.html", False) not in targets
481 assert targets == [(touched_index, False)]
482
483
484 def test_collect_planned_artifact_targets_resolves_nested_file_changes_relative_to_parent_directory(
485 tmp_path: Path,
486 ) -> None:
487 implementation_plan = tmp_path / "implementation.md"
488 implementation_plan.write_text(
489 "\n".join(
490 [
491 "# Implementation Plan",
492 "",
493 "## File Changes",
494 f"- `{tmp_path / 'guide' / 'index.html'}`",
495 f"- Create chapter files in `{tmp_path / 'guide' / 'chapters'}/`:",
496 " - `00-introduction.html`",
497 " - `01-installation.html`",
498 " - `02-configuration.html`",
499 "",
500 ]
501 )
502 )
503
504 dod = create_definition_of_done("Create a multi-page guide.")
505 dod.implementation_plan = str(implementation_plan)
506
507 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
508
509 assert targets == [
510 (tmp_path / "guide" / "index.html", False),
511 (tmp_path / "guide" / "chapters", True),
512 (tmp_path / "guide" / "chapters" / "00-introduction.html", False),
513 (tmp_path / "guide" / "chapters" / "01-installation.html", False),
514 (tmp_path / "guide" / "chapters" / "02-configuration.html", False),
515 ]
516
517
518 def test_collect_planned_artifact_targets_ignores_read_only_reference_paths(
519 tmp_path: Path,
520 ) -> None:
521 implementation_plan = tmp_path / "implementation.md"
522 implementation_plan.write_text(
523 "\n".join(
524 [
525 "# Implementation Plan",
526 "",
527 "## File Changes",
528 f"- `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'index.html'}`",
529 f"- `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'chapters'}/`",
530 "- Read `~/Loader/guides/fortran/index.html`",
531 "- Read files in `~/Loader/guides/fortran/chapters/`",
532 "",
533 ]
534 )
535 )
536
537 dod = create_definition_of_done("Create an nginx guide from a Fortran reference.")
538 dod.implementation_plan = str(implementation_plan)
539
540 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
541
542 assert targets == [
543 (tmp_path / "Loader" / "guides" / "nginx" / "index.html", False),
544 (tmp_path / "Loader" / "guides" / "nginx" / "chapters", True),
545 ]
546
547
548 def test_collect_planned_artifact_targets_ignores_nested_read_only_reference_paths(
549 tmp_path: Path,
550 ) -> None:
551 implementation_plan = tmp_path / "implementation.md"
552 implementation_plan.write_text(
553 "\n".join(
554 [
555 "# Implementation Plan",
556 "",
557 "## File Changes",
558 "1. Create directory structure for nginx guide:",
559 f" - `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'index.html'}`",
560 f" - `{tmp_path / 'Loader' / 'guides' / 'nginx' / 'chapters'}/`",
561 "2. Analyze existing fortran guide structure to understand the format:",
562 " - `~/Loader/guides/fortran/`",
563 " - `~/Loader/guides/fortran/chapters/`",
564 "3. Create nginx guide content following the same structure and cadence as the fortran guide",
565 "",
566 ]
567 )
568 )
569
570 dod = create_definition_of_done("Create an nginx guide from a Fortran reference.")
571 dod.implementation_plan = str(implementation_plan)
572
573 targets = collect_planned_artifact_targets(dod, project_root=tmp_path)
574
575 assert targets == [
576 (tmp_path / "Loader" / "guides" / "nginx" / "index.html", False),
577 (tmp_path / "Loader" / "guides" / "nginx" / "chapters", True),
578 ]
579
580
581 def test_all_planned_artifacts_exist_requires_file_contents_for_planned_output_directory(
582 tmp_path: Path,
583 ) -> None:
584 implementation_plan = tmp_path / "implementation.md"
585 implementation_plan.write_text(
586 "\n".join(
587 [
588 "# Implementation Plan",
589 "",
590 "## File Changes",
591 f"- `{tmp_path / 'guide' / 'index.html'}`",
592 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
593 "",
594 "## Execution Order",
595 "- Create chapter files with appropriate content",
596 ]
597 )
598 )
599
600 guide_root = tmp_path / "guide"
601 chapters = guide_root / "chapters"
602 guide_root.mkdir()
603 chapters.mkdir()
604 (guide_root / "index.html").write_text("<html></html>\n")
605
606 dod = create_definition_of_done("Create a multi-file guide with chapters.")
607 dod.implementation_plan = str(implementation_plan)
608 dod.completed_items = ["Create chapter files with appropriate content"]
609
610 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
611
612
613 def test_all_planned_artifacts_exist_stays_false_for_substantive_guide_with_only_one_chapter(
614 tmp_path: Path,
615 ) -> None:
616 implementation_plan = tmp_path / "implementation.md"
617 implementation_plan.write_text(
618 "\n".join(
619 [
620 "# Implementation Plan",
621 "",
622 "## File Changes",
623 f"- `{tmp_path / 'guide' / 'index.html'}`",
624 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
625 "",
626 "## Execution Order",
627 "- Create chapter files with appropriate content",
628 ]
629 )
630 )
631
632 guide_root = tmp_path / "guide"
633 chapters = guide_root / "chapters"
634 chapters.mkdir(parents=True)
635 (guide_root / "index.html").write_text("<html></html>\n")
636 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
637
638 dod = create_definition_of_done("Create an equally thorough guide with chapters.")
639 dod.implementation_plan = str(implementation_plan)
640 dod.completed_items = ["Create chapter files with appropriate content"]
641 dod.touched_files = [
642 str(guide_root / "index.html"),
643 str(chapters / "01-introduction.html"),
644 ]
645
646 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
647
648
649 def test_all_planned_artifacts_exist_respects_nested_file_change_entries(
650 tmp_path: Path,
651 ) -> None:
652 implementation_plan = tmp_path / "implementation.md"
653 implementation_plan.write_text(
654 "\n".join(
655 [
656 "# Implementation Plan",
657 "",
658 "## File Changes",
659 f"- `{tmp_path / 'guide' / 'index.html'}`",
660 f"- Create chapter files in `{tmp_path / 'guide' / 'chapters'}/`:",
661 " - `00-introduction.html`",
662 " - `01-installation.html`",
663 "",
664 ]
665 )
666 )
667
668 guide = tmp_path / "guide"
669 chapters = guide / "chapters"
670 chapters.mkdir(parents=True)
671 (guide / "index.html").write_text("<html></html>\n")
672 (chapters / "00-introduction.html").write_text("<html></html>\n")
673
674 dod = create_definition_of_done("Create a multi-page guide.")
675 dod.implementation_plan = str(implementation_plan)
676
677 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
678
679 (chapters / "01-installation.html").write_text("<h1>Installation</h1>\n")
680
681 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is True
682
683
684 def test_all_planned_artifact_outputs_stay_false_while_root_declares_missing_html_outputs(
685 tmp_path: Path,
686 ) -> None:
687 implementation_plan = tmp_path / "implementation.md"
688 implementation_plan.write_text(
689 "\n".join(
690 [
691 "# Implementation Plan",
692 "",
693 "## File Changes",
694 f"- `{tmp_path / 'guide' / 'index.html'}`",
695 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
696 "",
697 "## Execution Order",
698 "- Create chapter files with appropriate content",
699 ]
700 )
701 )
702
703 guide_root = tmp_path / "guide"
704 chapters = guide_root / "chapters"
705 guide_root.mkdir()
706 chapters.mkdir()
707 index = guide_root / "index.html"
708 index.write_text(
709 '<a href="chapters/01-introduction.html">Intro</a>\n'
710 '<a href="chapters/02-setup.html">Setup</a>\n'
711 )
712 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
713
714 dod = create_definition_of_done("Create a multi-file guide with chapters.")
715 dod.implementation_plan = str(implementation_plan)
716 dod.touched_files = [str(index), str(chapters / "01-introduction.html")]
717 dod.completed_items = ["Create chapter files with appropriate content"]
718
719 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is False
720 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is False
721
722 (chapters / "02-setup.html").write_text("<h1>Setup</h1>\n")
723
724 assert all_planned_artifacts_exist(dod, project_root=tmp_path) is True
725 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is True
726
727
728 def test_collect_missing_declared_html_outputs_accepts_root_html_file_target(
729 tmp_path: Path,
730 ) -> None:
731 implementation_plan = tmp_path / "implementation.md"
732 implementation_plan.write_text(
733 "\n".join(
734 [
735 "# Implementation Plan",
736 "",
737 "## File Changes",
738 f"- `{tmp_path / 'guide' / 'index.html'}`",
739 f"- `{tmp_path / 'guide' / 'chapters'}/` (directory for chapter files)",
740 ]
741 )
742 )
743
744 guide_root = tmp_path / "guide"
745 chapters = guide_root / "chapters"
746 chapters.mkdir(parents=True)
747 index = guide_root / "index.html"
748 index.write_text(
749 '<a href="chapters/01-introduction.html">Intro</a>\n'
750 '<a href="chapters/02-setup.html">Setup</a>\n'
751 )
752 (chapters / "01-introduction.html").write_text("<h1>Intro</h1>\n")
753
754 dod = create_definition_of_done("Create a multi-file guide with chapters.")
755 dod.implementation_plan = str(implementation_plan)
756 dod.touched_files = [str(index), str(chapters / "01-introduction.html")]
757
758 assert all_planned_artifact_outputs_exist(dod, project_root=tmp_path) is False
759
760
761 def test_build_verification_summary_keeps_concrete_missing_link_details() -> None:
762 summary = build_verification_summary(
763 [
764 VerificationEvidence(
765 command="python3 - <<'PY' ... PY",
766 passed=False,
767 stderr=(
768 "Missing links:\n"
769 "chapters/05-control-structures.html -> missing\n"
770 "chapters/06-input-output.html -> missing\n"
771 ),
772 )
773 ]
774 )
775
776 assert "Missing links:" in summary
777 assert "chapters/05-control-structures.html -> missing" in summary
778 assert "chapters/06-input-output.html -> missing" in summary