| 1 | from __future__ import annotations |
| 2 | |
| 3 | from pathlib import Path |
| 4 | |
| 5 | from loader.llm.base import Message, Role |
| 6 | from loader.runtime.repair_focus import extract_active_repair_context |
| 7 | |
| 8 | |
| 9 | def test_extract_active_repair_context_parses_write_next_step_target( |
| 10 | tmp_path: Path, |
| 11 | ) -> None: |
| 12 | repair_target = tmp_path / "guides" / "nginx" / "chapters" / "02-configuration.html" |
| 13 | context = extract_active_repair_context( |
| 14 | [ |
| 15 | Message( |
| 16 | role=Role.ASSISTANT, |
| 17 | content=( |
| 18 | "Repair focus:\n" |
| 19 | f"- Continue the declared output set by creating missing planned artifact `{repair_target}`.\n" |
| 20 | f"- Immediate next step: write `{repair_target}`.\n" |
| 21 | "- Do not rewrite existing aggregate files to match the partial artifact set while these declared outputs are still missing.\n" |
| 22 | ), |
| 23 | ) |
| 24 | ] |
| 25 | ) |
| 26 | |
| 27 | assert context is not None |
| 28 | assert context.artifact_path == str(repair_target.resolve(strict=False)) |
| 29 | assert str(repair_target.resolve(strict=False)) in context.allowed_paths |
| 30 | |
| 31 | |
| 32 | def test_extract_active_repair_context_keeps_immediate_target_first( |
| 33 | tmp_path: Path, |
| 34 | ) -> None: |
| 35 | index_path = tmp_path / "guides" / "nginx" / "index.html" |
| 36 | chapter_path = tmp_path / "guides" / "nginx" / "chapters" / "02-installation.html" |
| 37 | |
| 38 | context = extract_active_repair_context( |
| 39 | [ |
| 40 | Message( |
| 41 | role=Role.USER, |
| 42 | content=( |
| 43 | "Repair focus:\n" |
| 44 | f"- Improve `{chapter_path}`: thin content (526 text chars, expected at least 1758).\n" |
| 45 | f"- Immediate next step: edit `{index_path}`.\n" |
| 46 | f"- Improve `{index_path}`: insufficient structured content (9 blocks, expected at least 12).\n" |
| 47 | ), |
| 48 | ) |
| 49 | ] |
| 50 | ) |
| 51 | |
| 52 | assert context is not None |
| 53 | assert context.artifact_path == str(index_path.resolve(strict=False)) |
| 54 | assert context.allowed_paths[0] == str(index_path.resolve(strict=False)) |
| 55 | assert str(chapter_path.resolve(strict=False)) in context.allowed_paths |
| 56 | |
| 57 | |
| 58 | def test_extract_active_repair_context_uses_latest_quality_handoff( |
| 59 | tmp_path: Path, |
| 60 | ) -> None: |
| 61 | first = tmp_path / "guide" / "chapters" / "01-introduction.html" |
| 62 | second = tmp_path / "guide" / "chapters" / "02-installation.html" |
| 63 | |
| 64 | context = extract_active_repair_context( |
| 65 | [ |
| 66 | Message( |
| 67 | role=Role.USER, |
| 68 | content=( |
| 69 | "Repair focus:\n" |
| 70 | f"- Improve `{first}`: thin content (400 text chars, expected at least 1758).\n" |
| 71 | f"- Immediate next step: edit `{first}`.\n" |
| 72 | ), |
| 73 | ), |
| 74 | Message( |
| 75 | role=Role.USER, |
| 76 | content=( |
| 77 | "The active HTML content-quality repair target was updated.\n\n" |
| 78 | "Repair focus:\n" |
| 79 | f"- Improve `{second}`: insufficient structured content (6 blocks, expected at least 18).\n" |
| 80 | f"- Immediate next step: edit `{second}`.\n" |
| 81 | "- Continue with one concrete edit, patch, or write call.\n" |
| 82 | ), |
| 83 | ), |
| 84 | ] |
| 85 | ) |
| 86 | |
| 87 | assert context is not None |
| 88 | assert context.artifact_path == str(second.resolve(strict=False)) |
| 89 | assert context.allowed_paths[0] == str(second.resolve(strict=False)) |