Python · 25102 bytes Raw Blame History
1 """Tests for runtime-owned safeguard services."""
2
3 from __future__ import annotations
4
5 import tempfile
6 from pathlib import Path
7
8 import loader.agent.safeguards as agent_safeguards
9 from loader.agent.safeguards import RuntimeSafeguards as AgentRuntimeSafeguards
10 from loader.runtime.safeguard_services import (
11 ActionTracker,
12 PreActionValidator,
13 ValidationResult,
14 )
15 from loader.runtime.safeguards import RuntimeSafeguards
16 from loader.runtime.semantic_rules.html_toc import (
17 build_html_toc_edit_call_template,
18 build_html_toc_replacement_block,
19 format_html_inventory_entry,
20 task_targets_html_toc,
21 validate_html_toc,
22 )
23
24
25 def test_action_tracker_detects_duplicate_write_after_recording(tmp_path) -> None:
26 tracker = ActionTracker()
27 file_path = tmp_path / "notes.txt"
28 arguments = {"file_path": str(file_path), "content": "alpha\n"}
29
30 assert tracker.check_tool_call("write", arguments) == (False, "")
31
32 tracker.record_tool_call("write", arguments)
33
34 is_duplicate, reason = tracker.check_tool_call("write", arguments)
35
36 assert is_duplicate is True
37 assert str(file_path) in reason
38
39
40 def test_task_targets_html_toc_requires_explicit_repair_intent() -> None:
41 prompt = (
42 "Have a look at ~/Loader/guides/fortran and chapters/ within. Get a feel "
43 "for the structure and cadence of the guide. We are going to make an all "
44 "new equally thorough guide on how to use the nginx tool. It will live in "
45 "~/Loader/guides/nginx/index.html and ~/Loader/guides/nginx/chapters/."
46 )
47
48 assert task_targets_html_toc(prompt) is False
49
50
51 def test_build_html_toc_replacement_block_uses_verified_inventory(tmp_path) -> None:
52 chapters = tmp_path / "chapters"
53 chapters.mkdir()
54 (chapters / "01-introduction.html").write_text(
55 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
56 )
57 (chapters / "02-setup.html").write_text(
58 "<h1>Chapter 2: Setting Up Your Environment</h1>\n"
59 )
60 index_path = tmp_path / "index.html"
61 index_path.write_text(
62 "<h2>Table of Contents</h2>\n"
63 "<ul class=\"chapter-list\">\n"
64 " <li><a href=\"chapters/01-old.html\">Chapter 1: Old</a></li>\n"
65 "</ul>\n"
66 )
67
68 replacement = build_html_toc_replacement_block(index_path)
69
70 assert replacement is not None
71 assert "<h2>Table of Contents</h2>" in replacement
72 assert '<li><a href="chapters/01-introduction.html">Chapter 1: Introduction to Fortran</a></li>' in replacement
73 assert '<li><a href="chapters/02-setup.html">Chapter 2: Setting Up Your Environment</a></li>' in replacement
74
75
76 def test_build_html_toc_edit_call_template_uses_current_and_replacement_blocks(tmp_path) -> None:
77 chapters = tmp_path / "chapters"
78 chapters.mkdir()
79 (chapters / "01-introduction.html").write_text(
80 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
81 )
82 index_path = tmp_path / "index.html"
83 index_path.write_text(
84 "<h2>Table of Contents</h2>\n"
85 '<ul class="chapter-list">\n'
86 ' <li><a href="chapters/01-old.html">Chapter 1: Old</a></li>\n'
87 "</ul>\n"
88 )
89
90 template = build_html_toc_edit_call_template(index_path)
91
92 assert template is not None
93 assert template.startswith("edit(")
94 assert f'file_path="{index_path}"' in template
95 assert 'old_string="""' in template
96 assert 'new_string="""' in template
97 assert '<li><a href="chapters/01-introduction.html">Chapter 1: Introduction to Fortran</a></li>' in template
98
99
100 def test_validate_html_toc_reports_missing_and_mismatched_links(tmp_path) -> None:
101 chapters = tmp_path / "chapters"
102 chapters.mkdir()
103 (chapters / "01-introduction.html").write_text(
104 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
105 )
106 index_path = tmp_path / "index.html"
107 index_path.write_text(
108 '<ul class="chapter-list">\n'
109 ' <li><a href="chapters/01-introduction.html">Chapter 1: Wrong Title</a></li>\n'
110 ' <li><a href="chapters/02-missing.html">Chapter 2: Missing</a></li>\n'
111 "</ul>\n"
112 )
113
114 result = validate_html_toc(index_path)
115
116 assert result is not None
117 assert result.valid is False
118 assert result.link_count == 2
119 assert result.missing == ("chapters/02-missing.html -> missing",)
120 assert (
121 result.mismatched
122 == (
123 "chapters/01-introduction.html -> Chapter 1: Wrong Title != Chapter 1: Introduction to Fortran",
124 )
125 )
126
127
128 def test_validate_html_toc_reports_success(tmp_path) -> None:
129 chapters = tmp_path / "chapters"
130 chapters.mkdir()
131 (chapters / "01-introduction.html").write_text(
132 "<h1>Chapter 1: Introduction to Fortran</h1>\n"
133 )
134 (chapters / "02-setup.html").write_text(
135 "<h1>Chapter 2: Setting Up Your Environment</h1>\n"
136 )
137 index_path = tmp_path / "index.html"
138 index_path.write_text(
139 '<ul class="chapter-list">\n'
140 ' <li><a href="chapters/01-introduction.html">Chapter 1: Introduction to Fortran</a></li>\n'
141 ' <li><a href="chapters/02-setup.html">Chapter 2: Setting Up Your Environment</a></li>\n'
142 "</ul>\n"
143 )
144
145 result = validate_html_toc(index_path)
146
147 assert result is not None
148 assert result.valid is True
149 assert result.link_count == 2
150 assert result.missing == ()
151 assert result.mismatched == ()
152
153
154 def test_action_tracker_preserves_loop_description_format() -> None:
155 tracker = ActionTracker()
156
157 tracker.record_tool_call("read", {"file_path": "a.txt"})
158 tracker.record_tool_call("grep", {"pattern": "alpha"})
159 tracker.record_tool_call("read", {"file_path": "b.txt"})
160 tracker.record_tool_call("grep", {"pattern": "beta"})
161
162 is_loop, description = tracker.detect_loop()
163
164 assert is_loop is True
165 assert description == "Repeating pattern detected (2x): read → grep"
166
167
168 def test_action_tracker_blocks_repeated_bash_observation_without_changes() -> None:
169 tracker = ActionTracker()
170 arguments = {"command": "ls -la ~/Loader/guides/fortran/chapters/"}
171
172 tracker.record_tool_call("bash", arguments)
173
174 is_duplicate, reason = tracker.check_tool_call("bash", arguments)
175
176 assert is_duplicate is True
177 assert "read-only shell probe" in reason
178
179
180 def test_action_tracker_allows_repeated_bash_observation_after_mutation() -> None:
181 tracker = ActionTracker()
182 bash_args = {"command": "ls -la ~/Loader/guides/fortran/chapters/"}
183 patch_args = {
184 "file_path": "index.html",
185 "hunks": [
186 {
187 "old_start": 1,
188 "old_lines": 1,
189 "new_start": 1,
190 "new_lines": 1,
191 "lines": ["-old", "+new"],
192 }
193 ],
194 }
195
196 tracker.record_tool_call("bash", bash_args)
197 tracker.record_tool_call("patch", patch_args)
198
199 assert tracker.check_tool_call("bash", bash_args) == (False, "")
200
201
202 def test_action_tracker_blocks_repeated_read_without_changes(tmp_path) -> None:
203 tracker = ActionTracker()
204 file_path = tmp_path / "index.html"
205 arguments = {"file_path": str(file_path)}
206
207 tracker.record_tool_call("read", arguments)
208
209 is_duplicate, reason = tracker.check_tool_call("read", arguments)
210
211 assert is_duplicate is True
212 assert str(file_path) in reason
213
214
215 def test_action_tracker_allows_one_interleaved_reread_without_changes(tmp_path) -> None:
216 tracker = ActionTracker()
217 index_path = tmp_path / "index.html"
218 chapter_path = tmp_path / "chapter-1.html"
219
220 tracker.record_tool_call("read", {"file_path": str(index_path)})
221 tracker.record_tool_call("read", {"file_path": str(chapter_path)})
222
223 assert tracker.check_tool_call("read", {"file_path": str(index_path)}) == (False, "")
224
225
226 def test_action_tracker_allows_reading_a_different_slice_of_the_same_file(tmp_path) -> None:
227 tracker = ActionTracker()
228 index_path = tmp_path / "index.html"
229
230 tracker.record_tool_call("read", {"file_path": str(index_path)})
231
232 assert tracker.check_tool_call(
233 "read",
234 {"file_path": str(index_path), "offset": 1, "limit": 50},
235 ) == (False, "")
236
237
238 def test_action_tracker_blocks_fourth_interleaved_reread_without_changes(tmp_path) -> None:
239 tracker = ActionTracker()
240 index_path = tmp_path / "index.html"
241 chapter_a = tmp_path / "chapter-1.html"
242 chapter_b = tmp_path / "chapter-2.html"
243 chapter_c = tmp_path / "chapter-3.html"
244
245 tracker.record_tool_call("read", {"file_path": str(index_path)})
246 tracker.record_tool_call("read", {"file_path": str(chapter_a)})
247 tracker.record_tool_call("read", {"file_path": str(index_path)})
248 tracker.record_tool_call("read", {"file_path": str(chapter_b)})
249 tracker.record_tool_call("read", {"file_path": str(index_path)})
250 tracker.record_tool_call("read", {"file_path": str(chapter_c)})
251
252 is_duplicate, reason = tracker.check_tool_call("read", {"file_path": str(index_path)})
253
254 assert is_duplicate is True
255 assert str(index_path) in reason
256
257
258 def test_action_tracker_allows_one_target_index_reread_after_chapter_discovery(tmp_path) -> None:
259 tracker = ActionTracker()
260 index_path = tmp_path / "index.html"
261 chapters = tmp_path / "chapters"
262 chapter_a = chapters / "01-introduction.html"
263 chapter_b = chapters / "02-setup.html"
264 chapter_c = chapters / "03-basics.html"
265
266 tracker.record_tool_call("read", {"file_path": str(index_path)})
267 tracker.record_tool_call("read", {"file_path": str(chapter_a)})
268 tracker.record_tool_call("read", {"file_path": str(chapter_b)})
269 tracker.record_tool_call("read", {"file_path": str(chapter_c)})
270
271 is_duplicate, reason = tracker.check_tool_call("read", {"file_path": str(index_path)})
272
273 assert is_duplicate is False
274 assert reason == ""
275
276
277 def test_action_tracker_blocks_second_target_index_reread_after_chapter_discovery(tmp_path) -> None:
278 tracker = ActionTracker()
279 index_path = tmp_path / "index.html"
280 chapters = tmp_path / "chapters"
281
282 tracker.record_tool_call("read", {"file_path": str(index_path)})
283 tracker.record_tool_call("read", {"file_path": str(chapters / "01-introduction.html")})
284 tracker.record_tool_call("read", {"file_path": str(chapters / "02-setup.html")})
285 tracker.record_tool_call("read", {"file_path": str(chapters / "03-basics.html")})
286 tracker.record_tool_call("read", {"file_path": str(index_path)})
287
288 is_duplicate, reason = tracker.check_tool_call("read", {"file_path": str(index_path)})
289
290 assert is_duplicate is True
291 assert "reuse the earlier read result instead of rereading" in reason
292
293
294 def test_action_tracker_blocks_repeated_chapter_directory_search_once_titles_are_known(
295 tmp_path,
296 ) -> None:
297 tracker = ActionTracker()
298 chapters = tmp_path / "chapters"
299 search_args = {"pattern": "*.html", "path": str(chapters)}
300
301 tracker.record_tool_call("glob", search_args)
302 tracker.record_tool_call("glob", search_args)
303
304 is_duplicate, reason = tracker.check_tool_call("glob", search_args)
305
306 assert is_duplicate is True
307 assert "reuse the earlier search result instead of rerunning it" in reason
308
309
310 def test_action_tracker_allows_repeated_read_after_mutation(tmp_path) -> None:
311 tracker = ActionTracker()
312 file_path = tmp_path / "index.html"
313 read_args = {"file_path": str(file_path)}
314 edit_args = {
315 "file_path": str(file_path),
316 "old_string": "old",
317 "new_string": "new",
318 }
319
320 tracker.record_tool_call("read", read_args)
321 tracker.record_tool_call("edit", edit_args)
322
323 assert tracker.check_tool_call("read", read_args) == (False, "")
324
325
326 def test_pre_action_validator_blocks_patch_without_hunks() -> None:
327 validator = PreActionValidator()
328
329 result = validator.validate(
330 "patch",
331 {"file_path": "notes.txt", "hunks": []},
332 )
333
334 assert result == ValidationResult(
335 valid=False,
336 reason="Patch hunks are missing",
337 suggestion="Provide structured patch hunks or a unified diff patch string",
338 severity="error",
339 )
340
341
342 def test_pre_action_validator_allows_patch_string_without_hunks() -> None:
343 validator = PreActionValidator()
344
345 result = validator.validate(
346 "patch",
347 {
348 "file_path": "notes.txt",
349 "patch": "--- a/notes.txt\n+++ b/notes.txt\n@@ -1,1 +1,1 @@\n-old\n+new\n",
350 },
351 )
352
353 assert result == ValidationResult(valid=True)
354
355
356 def test_pre_action_validator_blocks_shell_text_rewrite_for_html_target() -> None:
357 validator = PreActionValidator()
358
359 result = validator.validate(
360 "bash",
361 {
362 "command": (
363 "cd /tmp/fortran-qwen-recovery-check && "
364 "sed -i '1,3c\\<li>updated</li>' index.html"
365 )
366 },
367 )
368
369 assert result.valid is False
370 assert result.reason == (
371 "Shell-based text rewrites are brittle and bypass Loader's safer file tools"
372 )
373 assert "edit/patch/write" in result.suggestion
374 assert "index.html" in result.suggestion
375
376
377 def test_pre_action_validator_allows_non_mutating_sed_probe() -> None:
378 validator = PreActionValidator()
379
380 result = validator.validate(
381 "bash",
382 {"command": "sed -n '1,20p' index.html"},
383 )
384
385 assert result == ValidationResult(valid=True)
386
387
388 def test_pre_action_validator_blocks_index_edit_with_missing_chapter_href(tmp_path) -> None:
389 validator = PreActionValidator()
390 index = tmp_path / "index.html"
391 chapters = tmp_path / "chapters"
392 chapters.mkdir()
393 (chapters / "05-input-output.html").write_text(
394 "<h1>Chapter 5: Input and Output</h1>\n"
395 )
396
397 result = validator.validate(
398 "edit",
399 {
400 "file_path": str(index),
401 "old_string": '<li><a href="chapters/05-input-output.html">Chapter 5: Input and Output</a></li>',
402 "new_string": '<li><a href="chapters/05-control-structures.html">Chapter 5: Control Structures</a></li>',
403 },
404 )
405
406 assert result.valid is False
407 assert result.reason == "Edited HTML links point to files that do not exist"
408 assert "chapters/05-control-structures.html" in result.suggestion
409
410
411 def test_pre_action_validator_blocks_index_edit_with_title_mismatch(tmp_path) -> None:
412 validator = PreActionValidator()
413 index = tmp_path / "index.html"
414 chapters = tmp_path / "chapters"
415 chapters.mkdir()
416 (chapters / "12-troubleshooting-tips.html").write_text(
417 "<h1>Chapter 12: Troubleshooting and Tips</h1>\n"
418 )
419
420 result = validator.validate(
421 "edit",
422 {
423 "file_path": str(index),
424 "old_string": '<li><a href="chapters/12-troubleshooting-tips.html">Chapter 12: Troubleshooting and Tips</a></li>',
425 "new_string": '<li><a href="chapters/12-troubleshooting-tips.html">Chapter 12: Troubleshooting Tips</a></li>',
426 },
427 )
428
429 assert result.valid is True
430
431
432 def test_pre_action_validator_allows_chapter_write_with_future_target_declared_by_index(
433 tmp_path: Path,
434 ) -> None:
435 validator = PreActionValidator()
436 guide = tmp_path / "guide"
437 chapters = guide / "chapters"
438 chapters.mkdir(parents=True)
439 (guide / "index.html").write_text(
440 "\n".join(
441 [
442 '<a href="chapters/introduction.html">Introduction</a>',
443 '<a href="chapters/installation.html">Installation</a>',
444 "",
445 ]
446 )
447 )
448
449 result = validator.validate(
450 "write",
451 {
452 "file_path": str(chapters / "introduction.html"),
453 "content": '<a href="installation.html">Next</a>\n',
454 },
455 )
456
457 assert result.valid is True
458
459
460 def test_pre_action_validator_blocks_undeclared_chapter_file_creation_after_root_seed(
461 tmp_path: Path,
462 ) -> None:
463 validator = PreActionValidator()
464 guide = tmp_path / "guide"
465 chapters = guide / "chapters"
466 chapters.mkdir(parents=True)
467 (guide / "index.html").write_text(
468 "\n".join(
469 [
470 '<a href="chapters/01-introduction.html">Introduction</a>',
471 '<a href="chapters/02-installation.html">Installation</a>',
472 '<a href="chapters/03-configuration.html">Configuration</a>',
473 "",
474 ]
475 )
476 )
477 (chapters / "01-introduction.html").write_text("<html></html>\n")
478 (chapters / "02-installation.html").write_text("<html></html>\n")
479
480 result = validator.validate(
481 "write",
482 {
483 "file_path": str(chapters / "09-monitoring.html"),
484 "content": "<html></html>\n",
485 },
486 )
487
488 assert result.valid is False
489 assert result.reason == "HTML file creation falls outside the current declared artifact set"
490 assert "09-monitoring.html" in result.suggestion
491 assert str((guide / "index.html").resolve(strict=False)) in result.suggestion
492
493
494 def test_pre_action_validator_allows_declared_missing_chapter_file_creation(
495 tmp_path: Path,
496 ) -> None:
497 validator = PreActionValidator()
498 guide = tmp_path / "guide"
499 chapters = guide / "chapters"
500 chapters.mkdir(parents=True)
501 (guide / "index.html").write_text(
502 "\n".join(
503 [
504 '<a href="chapters/01-introduction.html">Introduction</a>',
505 '<a href="chapters/02-installation.html">Installation</a>',
506 '<a href="chapters/03-configuration.html">Configuration</a>',
507 "",
508 ]
509 )
510 )
511 (chapters / "01-introduction.html").write_text("<html></html>\n")
512
513 result = validator.validate(
514 "write",
515 {
516 "file_path": str(chapters / "02-installation.html"),
517 "content": "<html></html>\n",
518 },
519 )
520
521 assert result.valid is True
522
523
524 def test_pre_action_validator_blocks_chapter_write_with_undeclared_missing_sibling(
525 tmp_path: Path,
526 ) -> None:
527 validator = PreActionValidator()
528 guide = tmp_path / "guide"
529 chapters = guide / "chapters"
530 chapters.mkdir(parents=True)
531 (guide / "index.html").write_text(
532 "\n".join(
533 [
534 '<a href="chapters/introduction.html">Introduction</a>',
535 '<a href="chapters/installation.html">Installation</a>',
536 '<a href="chapters/configuration.html">Configuration</a>',
537 '<a href="chapters/usage.html">Usage</a>',
538 '<a href="chapters/troubleshooting.html">Troubleshooting</a>',
539 "",
540 ]
541 )
542 )
543 (chapters / "introduction.html").write_text('<a href="installation.html">Next</a>\n')
544 (chapters / "installation.html").write_text('<a href="configuration.html">Next</a>\n')
545 (chapters / "configuration.html").write_text('<a href="usage.html">Next</a>\n')
546
547 result = validator.validate(
548 "write",
549 {
550 "file_path": str(chapters / "usage.html"),
551 "content": '<a href="advanced.html">Next</a>\n',
552 },
553 )
554
555 assert result.valid is False
556 assert (
557 result.reason
558 == "HTML page introduces new local targets outside the current declared artifact set"
559 )
560 assert "advanced.html" in result.suggestion
561
562
563 def test_pre_action_validator_blocks_chapter_write_with_existing_but_undeclared_sibling(
564 tmp_path: Path,
565 ) -> None:
566 validator = PreActionValidator()
567 guide = tmp_path / "guide"
568 chapters = guide / "chapters"
569 chapters.mkdir(parents=True)
570 (guide / "index.html").write_text(
571 "\n".join(
572 [
573 '<a href="chapters/01-introduction.html">Introduction</a>',
574 '<a href="chapters/02-installation.html">Installation</a>',
575 '<a href="chapters/03-basic-configuration.html">Basic Configuration</a>',
576 '<a href="chapters/04-advanced-configuration.html">Advanced Configuration</a>',
577 "",
578 ]
579 )
580 )
581 (chapters / "01-introduction.html").write_text('<a href="02-installation.html">Next</a>\n')
582 (chapters / "02-installation.html").write_text(
583 '<a href="03-basic-configuration.html">Next</a>\n'
584 )
585 (chapters / "04-locations-and-servers.html").write_text(
586 '<a href="05-static-content.html">Next</a>\n'
587 )
588
589 result = validator.validate(
590 "write",
591 {
592 "file_path": str(chapters / "03-basic-configuration.html"),
593 "content": '<a href="04-locations-and-servers.html">Next</a>\n',
594 },
595 )
596
597 assert result.valid is False
598 assert (
599 result.reason
600 == "HTML page introduces new local targets outside the current declared artifact set"
601 )
602 assert "04-locations-and-servers.html" in result.suggestion
603 assert "04-advanced-configuration.html" in result.suggestion
604
605
606 def test_pre_action_validator_does_not_suggest_unrelated_declared_html_target(
607 tmp_path: Path,
608 ) -> None:
609 validator = PreActionValidator()
610 guide = tmp_path / "guide"
611 chapters = guide / "chapters"
612 chapters.mkdir(parents=True)
613 (guide / "index.html").write_text(
614 "\n".join(
615 [
616 '<a href="chapters/introduction.html">Introduction</a>',
617 '<a href="chapters/installation.html">Installation</a>',
618 '<a href="chapters/configuration.html">Configuration</a>',
619 '<a href="chapters/basic-usage.html">Basic Usage</a>',
620 '<a href="chapters/advanced-topics.html">Advanced Topics</a>',
621 "",
622 ]
623 )
624 )
625
626 result = validator.validate(
627 "write",
628 {
629 "file_path": str(chapters / "introduction.html"),
630 "content": '<a href="troubleshooting.html">Troubleshooting</a>\n',
631 },
632 )
633
634 assert result.valid is False
635 assert "troubleshooting.html" in result.suggestion
636 assert "Closest declared local targets include:" not in result.suggestion
637
638
639 def test_pre_action_validator_allows_chapter_write_with_root_declared_sibling_and_index_link(
640 tmp_path: Path,
641 ) -> None:
642 validator = PreActionValidator()
643 guide = tmp_path / "guide"
644 chapters = guide / "chapters"
645 chapters.mkdir(parents=True)
646 (guide / "index.html").write_text(
647 "\n".join(
648 [
649 '<a href="chapters/01-introduction.html">Introduction</a>',
650 '<a href="chapters/02-installation.html">Installation</a>',
651 '<a href="chapters/03-basic-configuration.html">Basic Configuration</a>',
652 "",
653 ]
654 )
655 )
656
657 result = validator.validate(
658 "write",
659 {
660 "file_path": str(chapters / "02-installation.html"),
661 "content": (
662 '<a href="../index.html">Back to guide</a>\n'
663 '<a href="03-basic-configuration.html">Next</a>\n'
664 ),
665 },
666 )
667
668 assert result.valid is True
669
670
671 def test_pre_action_validator_blocks_missing_numbered_read_with_existing_sibling(
672 tmp_path: Path,
673 ) -> None:
674 validator = PreActionValidator()
675 chapters = tmp_path / "chapters"
676 chapters.mkdir()
677 (chapters / "01-getting-started.html").write_text("<h1>Getting Started</h1>\n")
678
679 result = validator.validate(
680 "read",
681 {"file_path": str(chapters / "01-introduction.html")},
682 )
683
684 assert result.valid is False
685 assert result.reason == "Read target conflicts with an existing numbered sibling"
686 assert "01-getting-started.html" in result.suggestion
687
688
689 def test_pre_action_validator_blocks_new_numbered_sibling_drift(tmp_path) -> None:
690 validator = PreActionValidator()
691 chapters = tmp_path / "chapters"
692 chapters.mkdir()
693 (chapters / "01-getting-started.html").write_text("<h1>Getting Started</h1>\n")
694
695 result = validator.validate(
696 "write",
697 {
698 "file_path": str(chapters / "01-intro.html"),
699 "content": "<h1>Intro</h1>\n",
700 },
701 )
702
703 assert result.valid is False
704 assert result.reason == "New file conflicts with an existing numbered sibling"
705 assert "01-getting-started.html" in result.suggestion
706
707
708 def test_format_html_inventory_entry_handles_tmp_alias_paths() -> None:
709 root = Path(tempfile.mkdtemp(dir="/tmp"))
710 chapters = root / "chapters"
711 chapters.mkdir()
712 candidate = chapters / "05-input-output.html"
713 candidate.write_text("<h1>Chapter 5: Input and Output</h1>\n")
714
715 entry = format_html_inventory_entry(root, candidate.resolve(strict=False))
716
717 assert entry == "chapters/05-input-output.html = Chapter 5: Input and Output"
718
719
720 def test_runtime_safeguards_wrap_runtime_owned_services() -> None:
721 safeguards = RuntimeSafeguards()
722
723 assert isinstance(safeguards.action_tracker, ActionTracker)
724 assert isinstance(safeguards.validator, PreActionValidator)
725
726
727 def test_agent_safeguards_reexport_runtime_safeguards() -> None:
728 assert AgentRuntimeSafeguards is RuntimeSafeguards
729
730
731 def test_agent_safeguards_exports_curated_compatibility_surface() -> None:
732 assert agent_safeguards.__all__ == [
733 "ActionTracker",
734 "CodeBlockFilter",
735 "FilterResult",
736 "PatternDetector",
737 "PatternMatch",
738 "PreActionValidator",
739 "RuntimeSafeguards",
740 "ValidationResult",
741 ]