claude-dev-env 2.21.0 → 2.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,8 @@
1
1
  import json
2
+ import subprocess
2
3
  import sys
3
4
  from pathlib import Path
5
+ from typing import Any
4
6
 
5
7
  import pytest
6
8
  import tomllib
@@ -442,6 +444,69 @@ def _apply_source_agent(source_root: Path, target_root: Path, description: str)
442
444
  return target_root / "Luna.toml"
443
445
 
444
446
 
447
+ def _write_codex_hook_source(source_root: Path) -> None:
448
+ package_root = Path(__file__).parents[2]
449
+ source_hooks = source_root / "hooks"
450
+ source_hooks.mkdir(parents=True, exist_ok=True)
451
+ (source_hooks / "hooks.json").write_bytes(
452
+ (package_root / "hooks" / "hooks.json").read_bytes()
453
+ )
454
+ for each_relative_path in materializer.codex_hook_dependency_manifest:
455
+ source_path = source_root / each_relative_path
456
+ source_path.parent.mkdir(parents=True, exist_ok=True)
457
+ source_path.write_bytes((package_root / each_relative_path).read_bytes())
458
+
459
+
460
+ def test_build_plan_rejects_reparse_hook_dependencies(
461
+ tmp_path: Path,
462
+ ) -> None:
463
+ source = tmp_path / "source"
464
+ _write_codex_hook_source(source)
465
+ dependency_path = source / materializer.codex_enforcer_script_relative_path
466
+ outside_path = tmp_path / "outside.py"
467
+ outside_path.write_text("secret", encoding="utf-8")
468
+ try:
469
+ dependency_path.unlink()
470
+ dependency_path.symlink_to(outside_path)
471
+ except OSError as error:
472
+ pytest.skip(f"symlinks unavailable: {error}")
473
+
474
+ with pytest.raises(MaterializerError, match="reparse point"):
475
+ build_plan(MaterializerConfig(source, tmp_path / "target"))
476
+
477
+
478
+ def test_publish_plan_preserves_modified_enforcer_hook_as_conflict(
479
+ tmp_path: Path,
480
+ ) -> None:
481
+ source = tmp_path / "source"
482
+ target = tmp_path / "target"
483
+ _write_codex_hook_source(source)
484
+ target.mkdir()
485
+ modified_command = f'python3 "{target / materializer.codex_enforcer_script_relative_path}" --custom'
486
+ existing_manifest = {
487
+ "hooks": {
488
+ "PreToolUse": [
489
+ {
490
+ "matcher": materializer.codex_hook_matcher,
491
+ "hooks": [
492
+ {"type": "command", "command": modified_command, "timeout": 5}
493
+ ],
494
+ }
495
+ ]
496
+ }
497
+ }
498
+ hooks_path = target / materializer.codex_hook_manifest_target_path
499
+ hooks_path.write_text(json.dumps(existing_manifest), encoding="utf-8")
500
+ config = MaterializerConfig(source, target, should_apply=True)
501
+
502
+ planned, report = build_plan(config)
503
+ publication = publish_plan(config, planned, report)
504
+
505
+ assert publication.conflicted == 1
506
+ assert publication.details["conflicted"] == ["hooks.json"]
507
+ assert json.loads(hooks_path.read_text(encoding="utf-8")) == existing_manifest
508
+
509
+
445
510
  def test_apply_with_a_missing_source_root_keeps_managed_files_and_fails(
446
511
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
447
512
  ) -> None:
@@ -712,3 +777,249 @@ def test_frontmatter_rejects_non_string_name() -> None:
712
777
  "---\nname: 1\ndescription: y\n---\n",
713
778
  "bad.md",
714
779
  )
780
+
781
+
782
+ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
783
+ canonical_rule_path = Path(__file__).parents[2] / "rules" / "failure-blast-radius.md"
784
+ canonical_rule = canonical_rule_path.read_text(encoding="utf-8")
785
+
786
+ projected_instruction = materializer.render_codex_failure_blast_radius(canonical_rule)
787
+
788
+ assert projected_instruction.startswith("Failure handling for this run")
789
+ assert "Three real attempts, then park." in projected_instruction
790
+ assert "Report as: N of M complete" in projected_instruction
791
+
792
+
793
+ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
794
+ tmp_path: Path,
795
+ ) -> None:
796
+ canonical_rule_path = Path(__file__).parents[2] / "rules" / "failure-blast-radius.md"
797
+ source = tmp_path / "source"
798
+ target = tmp_path / "target"
799
+ rule_path = source / "rules" / "failure-blast-radius.md"
800
+ rule_path.parent.mkdir(parents=True)
801
+ rule_path.write_text(canonical_rule_path.read_text(encoding="utf-8"), encoding="utf-8")
802
+ config = MaterializerConfig(source, target, should_apply=True)
803
+
804
+ planned, report = build_plan(config, all_agents=[])
805
+ publish_plan(config, planned, report)
806
+
807
+ projected_path = target / "AGENTS.md"
808
+ first_projection = projected_path.read_text(encoding="utf-8")
809
+ manifest_record = load_manifest(config.manifest_path)["files"]["AGENTS.md"]
810
+ assert manifest_record["ownership"] == "codex-compat"
811
+ assert manifest_record["source"] == "rules/failure-blast-radius.md"
812
+
813
+ rule_path.write_text(
814
+ rule_path.read_text(encoding="utf-8").replace("Three real attempts, then park.", "Three tested attempts, then park."),
815
+ encoding="utf-8",
816
+ )
817
+ drifted_plan, _ = build_plan(config, all_agents=[])
818
+
819
+ drifted_projection = next(
820
+ each_file.content
821
+ for each_file in drifted_plan
822
+ if each_file.target_relative_path == "AGENTS.md"
823
+ )
824
+ assert drifted_projection != first_projection
825
+
826
+
827
+ def test_codex_hook_manifest_keeps_dispatcher_order_and_adds_enforcer() -> None:
828
+ hooks_path = Path(__file__).parents[2] / "hooks" / "hooks.json"
829
+ hooks_configuration = json.loads(hooks_path.read_text(encoding="utf-8"))
830
+ pre_tool_use_entries = hooks_configuration["hooks"]["PreToolUse"]
831
+ matcher_names = [each_entry.get("matcher") for each_entry in pre_tool_use_entries]
832
+
833
+ dispatcher_index = matcher_names.index("Write|Edit|MultiEdit")
834
+ enforcer_entries = [
835
+ each_entry
836
+ for each_entry in pre_tool_use_entries
837
+ if each_entry.get("matcher") == "apply_patch"
838
+ ]
839
+ assert len(enforcer_entries) == 1
840
+ assert matcher_names.index("apply_patch") > dispatcher_index
841
+ assert enforcer_entries[0]["hooks"][0]["command"].endswith(
842
+ "/hooks/blocking/code_rules_enforcer.py"
843
+ )
844
+
845
+
846
+ def _prepare_projection_fixture(
847
+ tmp_path: Path,
848
+ ) -> tuple[Path, Path, dict[str, Any], Path]:
849
+ source = tmp_path / "source"
850
+ target = tmp_path.parent / "codex-prod-target"
851
+ source_rule = source / "rules" / "failure-blast-radius.md"
852
+ source_hooks = source / "hooks" / "hooks.json"
853
+ source_rule.parent.mkdir(parents=True)
854
+ source_hooks.parent.mkdir(parents=True)
855
+ source_rule.write_text(
856
+ (Path(__file__).parents[2] / "rules" / "failure-blast-radius.md").read_text(
857
+ encoding="utf-8"
858
+ ),
859
+ encoding="utf-8",
860
+ )
861
+ source_hooks.write_text(
862
+ (Path(__file__).parents[2] / "hooks" / "hooks.json").read_text(encoding="utf-8"),
863
+ encoding="utf-8",
864
+ )
865
+ package_root = Path(__file__).parents[2]
866
+ for each_relative_path in materializer.codex_hook_dependency_manifest:
867
+ target_dependency = source / each_relative_path
868
+ target_dependency.parent.mkdir(parents=True, exist_ok=True)
869
+ target_dependency.write_bytes((package_root / each_relative_path).read_bytes())
870
+ target.mkdir()
871
+ existing_hooks = {
872
+ "hooks": {
873
+ "PreToolUse": [
874
+ {
875
+ "matcher": "Write|Edit",
876
+ "hooks": [{"type": "command", "command": "python existing.py"}],
877
+ },
878
+ {
879
+ "matcher": "apply_patch",
880
+ "hooks": [
881
+ {
882
+ "type": "command",
883
+ "command": (
884
+ f'python3 "{target / "hooks" / "blocking" / "code_rules_enforcer.py"}"'
885
+ ),
886
+ "timeout": 60,
887
+ }
888
+ ],
889
+ },
890
+ {
891
+ "matcher": "apply_patch",
892
+ "hooks": [
893
+ {"type": "command", "command": "python not_code_rules_enforcer.py"}
894
+ ],
895
+ },
896
+ ],
897
+ "Bash": [{"matcher": "", "hooks": []}],
898
+ }
899
+ }
900
+ (target / "hooks.json").write_text(json.dumps(existing_hooks), encoding="utf-8")
901
+ retired_owned_path = target / "hooks" / "obsolete.py"
902
+ retired_owned_path.parent.mkdir(parents=True)
903
+ retired_owned_path.write_text("retired owned hook\n", encoding="utf-8")
904
+ retired_manifest = {
905
+ "version": 1,
906
+ "files": {
907
+ "hooks/obsolete.py": {
908
+ "source": "hooks/obsolete.py",
909
+ "hash": materializer.hash_content(retired_owned_path.read_bytes()),
910
+ "ownership": "codex-compat",
911
+ }
912
+ },
913
+ }
914
+ (target / ".codex-compat-manifest.json").write_text(
915
+ json.dumps(retired_manifest), encoding="utf-8"
916
+ )
917
+ return source, target, existing_hooks, retired_owned_path
918
+
919
+
920
+ def _run_materializer_and_read_report(
921
+ source: Path,
922
+ target: Path,
923
+ capsys: pytest.CaptureFixture[str],
924
+ ) -> dict[str, Any]:
925
+ exit_code = materializer.main([str(source), str(target), "--apply"])
926
+ report = json.loads(capsys.readouterr().out)
927
+ assert exit_code == 0, report
928
+ assert report["errors"] == 0, report
929
+ return report
930
+
931
+
932
+ def _assert_projection_and_manifest(
933
+ target: Path,
934
+ existing_hooks: dict[str, Any],
935
+ retired_owned_path: Path,
936
+ ) -> None:
937
+ assert not retired_owned_path.exists()
938
+ projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
939
+ assert "Three real attempts, then park." in projected_instruction
940
+ manifest = json.loads((target / "hooks.json").read_text(encoding="utf-8"))
941
+ assert manifest["hooks"]["PreToolUse"][0] == existing_hooks["hooks"]["PreToolUse"][0]
942
+ apply_patch_entries = [
943
+ each_entry
944
+ for each_entry in manifest["hooks"]["PreToolUse"]
945
+ if each_entry["matcher"] == "apply_patch"
946
+ ]
947
+ assert len(apply_patch_entries) == 1
948
+ apply_patch_commands = [
949
+ each_hook["command"] for each_hook in apply_patch_entries[0]["hooks"]
950
+ ]
951
+ assert apply_patch_commands[0] == "python not_code_rules_enforcer.py"
952
+ managed_command = (
953
+ f'python3 "{target / "hooks" / "blocking" / "code_rules_enforcer.py"}"'
954
+ )
955
+ assert managed_command in apply_patch_commands
956
+ manifest_record = load_manifest(target / ".codex-compat-manifest.json")["files"]
957
+ assert manifest_record["AGENTS.md"]["ownership"] == "codex-compat"
958
+ assert manifest_record["hooks.json"]["source"] == "hooks/hooks.json"
959
+
960
+
961
+ def _assert_detached_enforcer_behavior(target: Path) -> None:
962
+ target_entrypoint = target / materializer.codex_enforcer_script_relative_path
963
+ allow_payload = {
964
+ "tool_name": "apply_patch",
965
+ "cwd": str(target),
966
+ "tool_input": {
967
+ "command": (
968
+ "*** Begin Patch\n"
969
+ "*** Add File: module.py\n"
970
+ "+for each_member in all_members:\n"
971
+ "+ raise AssetItemBlocked()\n"
972
+ "*** End Patch"
973
+ )
974
+ },
975
+ }
976
+ allow_run = subprocess.run(
977
+ [sys.executable, str(target_entrypoint)],
978
+ cwd=target,
979
+ input=json.dumps(allow_payload),
980
+ capture_output=True,
981
+ text=True,
982
+ check=False,
983
+ )
984
+ assert allow_run.returncode == 0
985
+ assert allow_run.stdout == ""
986
+
987
+ deny_payload = {
988
+ **allow_payload,
989
+ "tool_input": {
990
+ "command": allow_payload["tool_input"]["command"].replace(
991
+ "AssetItemBlocked", "RuntimeError"
992
+ )
993
+ },
994
+ }
995
+ deny_run = subprocess.run(
996
+ [sys.executable, str(target_entrypoint)],
997
+ cwd=target,
998
+ input=json.dumps(deny_payload),
999
+ capture_output=True,
1000
+ text=True,
1001
+ check=False,
1002
+ )
1003
+ assert deny_run.returncode == 0
1004
+ assert json.loads(deny_run.stdout)["hookSpecificOutput"]["permissionDecision"] == "deny"
1005
+ assert target_entrypoint.is_file()
1006
+
1007
+
1008
+ def test_exact_cli_publishes_instruction_and_additive_hook_projection(
1009
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
1010
+ ) -> None:
1011
+ source, target, existing_hooks, retired_owned_path = _prepare_projection_fixture(
1012
+ tmp_path
1013
+ )
1014
+
1015
+ first_report = _run_materializer_and_read_report(source, target, capsys)
1016
+ assert first_report["written"] > 0
1017
+ _assert_projection_and_manifest(target, existing_hooks, retired_owned_path)
1018
+
1019
+ hooks_before_second_run = (target / "hooks.json").read_bytes()
1020
+ second_report = _run_materializer_and_read_report(source, target, capsys)
1021
+ assert second_report["written"] == 0
1022
+ assert (target / "hooks.json").read_bytes() == hooks_before_second_run
1023
+
1024
+ source.rename(tmp_path / "source-detached")
1025
+ _assert_detached_enforcer_behavior(target)