claude-dev-env 2.12.0 → 2.13.0

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.
Files changed (193) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  2. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  3. package/agents/CLAUDE.md +1 -2
  4. package/agents/pr-description-writer.md +11 -81
  5. package/bin/install.mjs +48 -9
  6. package/bin/install.prune.test.mjs +26 -0
  7. package/bin/install.settings-defaults.test.mjs +60 -33
  8. package/bin/install.test.mjs +2 -4
  9. package/commands/sr-loop.md +9 -5
  10. package/docs/CLAUDE.md +0 -1
  11. package/docs/references/CLAUDE.md +0 -1
  12. package/hooks/CLAUDE.md +1 -1
  13. package/hooks/blocking/CLAUDE.md +2 -27
  14. package/hooks/blocking/config/CLAUDE.md +1 -13
  15. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  16. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  17. package/hooks/blocking/stop_dispatcher.py +4 -4
  18. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  19. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  20. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  21. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  22. package/hooks/diagnostic/CLAUDE.md +3 -3
  23. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  24. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  25. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  26. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  28. package/hooks/git-hooks/pre_push.py +3 -85
  29. package/hooks/git-hooks/test_pre_push.py +0 -130
  30. package/hooks/hooks.json +0 -35
  31. package/hooks/hooks_constants/CLAUDE.md +0 -9
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  33. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  34. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  36. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  37. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  40. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  41. package/hooks/lifecycle/CLAUDE.md +1 -3
  42. package/package.json +1 -1
  43. package/rules/CLAUDE.md +0 -2
  44. package/rules/gh-cli-conventions.md +0 -1
  45. package/rules/git-workflow.md +1 -3
  46. package/scripts/CLAUDE.md +0 -1
  47. package/scripts/_code_review_test_support.py +0 -95
  48. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  49. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  50. package/scripts/invoke_code_review.py +8 -451
  51. package/scripts/test_invoke_code_review.py +5 -61
  52. package/scripts/test_invoke_code_review_cli.py +1 -45
  53. package/scripts/test_invoke_code_review_contract.py +1 -118
  54. package/settings.json +1 -10
  55. package/skills/CLAUDE.md +8 -0
  56. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  57. package/skills/autoconverge/reference/convergence.md +7 -7
  58. package/skills/autoconverge/reference/gotchas.md +3 -10
  59. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  60. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  61. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  62. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  63. package/skills/autoconverge/workflow/converge.mjs +201 -238
  64. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  65. package/skills/bugteam/CONSTRAINTS.md +1 -1
  66. package/skills/bugteam/SKILL.md +9 -0
  67. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  68. package/skills/bugteam/test_skill_additions.py +9 -0
  69. package/skills/comments/SKILL.md +65 -0
  70. package/skills/descriptions/SKILL.md +51 -0
  71. package/skills/emergencies/SKILL.md +42 -0
  72. package/skills/imagegen/SKILL.md +21 -0
  73. package/skills/imagegen/scripts/config/__init__.py +1 -0
  74. package/skills/imagegen/scripts/config/constants.py +28 -0
  75. package/skills/imagegen/scripts/imagegen.py +45 -0
  76. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  77. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  78. package/skills/orchestrator/SKILL.md +2 -4
  79. package/skills/plan-to-pr/SKILL.md +6 -7
  80. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  81. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  82. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  83. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  84. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  85. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  86. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  87. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  88. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  89. package/skills/pr-converge/SKILL.md +13 -16
  90. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  91. package/skills/pr-converge/reference/state-schema.md +1 -27
  92. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  93. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  94. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  95. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  96. package/skills/prototype/SKILL.md +4 -4
  97. package/skills/prototype/reference/honest-limitations.md +4 -4
  98. package/skills/prototype/reference/promotion-tasks.md +2 -2
  99. package/skills/prototype/workflows/promotion.md +4 -6
  100. package/skills/reviews/SKILL.md +85 -0
  101. package/skills/small-cl/SKILL.md +52 -0
  102. package/agents/code-verifier.md +0 -74
  103. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  104. package/docs/references/code-review-enforcement.md +0 -132
  105. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  106. package/hooks/blocking/code_review_gate_deny.py +0 -74
  107. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  108. package/hooks/blocking/code_review_push_gate.py +0 -145
  109. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  110. package/hooks/blocking/code_review_stamp_store.py +0 -233
  111. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  112. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  113. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  114. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  115. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  116. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  117. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  118. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  119. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  120. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  121. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  122. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  123. package/hooks/blocking/conftest.py +0 -123
  124. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  125. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  126. package/hooks/blocking/pr_description_body_audit.py +0 -148
  127. package/hooks/blocking/pr_description_command_parser.py +0 -237
  128. package/hooks/blocking/pr_description_enforcer.py +0 -227
  129. package/hooks/blocking/pr_description_pr_number.py +0 -155
  130. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  131. package/hooks/blocking/pr_description_readability.py +0 -364
  132. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  133. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  134. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  135. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  136. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  137. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  138. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  139. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  140. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  141. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  142. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  143. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  144. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  145. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  146. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  147. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  148. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  149. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  150. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  151. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  152. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  153. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  154. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  155. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  156. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  157. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  158. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  159. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  160. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  161. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  162. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  163. package/hooks/blocking/verification_verdict_store.py +0 -1039
  164. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  165. package/hooks/blocking/verified_commit_gate.py +0 -113
  166. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  167. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  168. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  169. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  170. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  171. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  172. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  173. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  174. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  175. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  176. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  177. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  178. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  179. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  180. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  181. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  182. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  183. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  184. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  185. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  186. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  187. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  188. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  189. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  190. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  191. package/rules/proof-of-work-pr-comments.md +0 -22
  192. package/rules/verified-commit-gate-skip.md +0 -4
  193. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,974 +0,0 @@
1
- """Tests for the mechanical commit-gate exemption in verification_verdict_store.
2
-
3
- Each test builds a real git repository with a real origin remote and asserts
4
- the exemption decision against the live work tree, exercising the same code
5
- path the verified_commit_gate hook runs.
6
- """
7
-
8
- import importlib.util
9
- import json
10
- import pathlib
11
- import subprocess
12
- import sys
13
- from collections.abc import Callable
14
-
15
- import pytest
16
-
17
- _HOOK_DIR = pathlib.Path(__file__).parent
18
- if str(_HOOK_DIR) not in sys.path:
19
- sys.path.insert(0, str(_HOOK_DIR))
20
-
21
- store_spec = importlib.util.spec_from_file_location(
22
- "verification_verdict_store",
23
- _HOOK_DIR / "verification_verdict_store.py",
24
- )
25
- assert store_spec is not None
26
- assert store_spec.loader is not None
27
- store_module = importlib.util.module_from_spec(store_spec)
28
- store_spec.loader.exec_module(store_module)
29
- is_verification_exempt_diff = store_module.is_verification_exempt_diff
30
- resolve_merge_base = store_module.resolve_merge_base
31
- untracked_file_paths = store_module.untracked_file_paths
32
- branch_surface_manifest = store_module.branch_surface_manifest
33
- manifest_sha256 = store_module.manifest_sha256
34
- workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
35
- minted_verdict_covers_surface = store_module.minted_verdict_covers_surface
36
- write_verdict = store_module.write_verdict
37
- worktree_path_for_branch = store_module.worktree_path_for_branch
38
- empty_surface_hash = store_module.empty_surface_hash
39
- root_key_for_repo = store_module.root_key_for_repo
40
- verdict_path_for_repo = store_module.verdict_path_for_repo
41
-
42
- constants_spec = importlib.util.spec_from_file_location(
43
- "verified_commit_constants",
44
- _HOOK_DIR / "config" / "verified_commit_constants.py",
45
- )
46
- assert constants_spec is not None
47
- assert constants_spec.loader is not None
48
- constants_module = importlib.util.module_from_spec(constants_spec)
49
- constants_spec.loader.exec_module(constants_module)
50
- CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
51
- EMPTY_SURFACE_GUARD_MESSAGE = constants_module.EMPTY_SURFACE_GUARD_MESSAGE
52
- BRANCH_WORKTREE_ABSENT_MESSAGE = constants_module.BRANCH_WORKTREE_ABSENT_MESSAGE
53
-
54
- PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
55
- TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
56
-
57
-
58
- def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
59
- subprocess.run(
60
- ["git", "-C", str(repo_dir), *git_arguments],
61
- check=True,
62
- capture_output=True,
63
- text=True,
64
- )
65
-
66
-
67
- def _make_repo_on_branch(
68
- tmp_path: pathlib.Path, branch_name: str
69
- ) -> pathlib.Path:
70
- origin_dir = tmp_path / "origin.git"
71
- work_dir = tmp_path / "work"
72
- work_dir.mkdir()
73
- subprocess.run(
74
- ["git", "init", "--bare", f"--initial-branch={branch_name}", str(origin_dir)],
75
- check=True,
76
- capture_output=True,
77
- text=True,
78
- )
79
- empty_hooks_dir = tmp_path / "nohooks"
80
- empty_hooks_dir.mkdir()
81
- _run_git(work_dir, "init", f"--initial-branch={branch_name}")
82
- _run_git(work_dir, "config", "user.email", "tests@example.com")
83
- _run_git(work_dir, "config", "user.name", "Verdict Store Tests")
84
- _run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
85
- (work_dir / "src").mkdir()
86
- (work_dir / "tests").mkdir()
87
- (work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
88
- (work_dir / "tests" / "test_app.py").write_text(TEST_SOURCE, encoding="utf-8")
89
- (work_dir / "README.md").write_text("# Fixture repo\n", encoding="utf-8")
90
- _run_git(work_dir, "add", "-A")
91
- _run_git(work_dir, "commit", "-m", "base")
92
- _run_git(work_dir, "remote", "add", "origin", str(origin_dir))
93
- _run_git(work_dir, "push", "-u", "origin", branch_name)
94
- return work_dir
95
-
96
-
97
- def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
98
- return _make_repo_on_branch(tmp_path, "main")
99
-
100
-
101
- def _exemption_for(work_dir: pathlib.Path) -> bool:
102
- merge_base_sha = resolve_merge_base(str(work_dir))
103
- assert merge_base_sha is not None
104
- return is_verification_exempt_diff(str(work_dir), merge_base_sha)
105
-
106
-
107
- def test_production_change_is_gated(tmp_path: pathlib.Path) -> None:
108
- work_dir = _make_repo_with_origin(tmp_path)
109
- (work_dir / "src" / "app.py").write_text(
110
- "def add(left: int, right: int) -> int:\n return left - right\n",
111
- encoding="utf-8",
112
- )
113
- assert _exemption_for(work_dir) is False
114
-
115
-
116
- def test_docs_only_change_is_exempt(tmp_path: pathlib.Path) -> None:
117
- work_dir = _make_repo_with_origin(tmp_path)
118
- (work_dir / "README.md").write_text(
119
- "# Fixture repo\n\nUpdated.\n", encoding="utf-8"
120
- )
121
- assert _exemption_for(work_dir) is True
122
-
123
-
124
- def test_docstring_only_python_change_is_exempt(tmp_path: pathlib.Path) -> None:
125
- work_dir = _make_repo_with_origin(tmp_path)
126
- (work_dir / "src" / "app.py").write_text(
127
- 'def add(left: int, right: int) -> int:\n """Add two integers."""\n'
128
- " return left + right\n",
129
- encoding="utf-8",
130
- )
131
- assert _exemption_for(work_dir) is True
132
-
133
-
134
- def test_modified_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
135
- work_dir = _make_repo_with_origin(tmp_path)
136
- (work_dir / "tests" / "test_app.py").write_text(
137
- TEST_SOURCE + "\n\ndef test_add_zero() -> None:\n assert 0 + 0 == 0\n",
138
- encoding="utf-8",
139
- )
140
- assert _exemption_for(work_dir) is True
141
-
142
-
143
- def test_untracked_test_prefix_file_is_exempt(tmp_path: pathlib.Path) -> None:
144
- work_dir = _make_repo_with_origin(tmp_path)
145
- (work_dir / "tests" / "test_extra.py").write_text(TEST_SOURCE, encoding="utf-8")
146
- assert _exemption_for(work_dir) is True
147
-
148
-
149
- def test_untracked_test_suffix_file_is_exempt(tmp_path: pathlib.Path) -> None:
150
- work_dir = _make_repo_with_origin(tmp_path)
151
- (work_dir / "tests" / "app_test.py").write_text(TEST_SOURCE, encoding="utf-8")
152
- assert _exemption_for(work_dir) is True
153
-
154
-
155
- def test_modified_conftest_is_exempt(tmp_path: pathlib.Path) -> None:
156
- work_dir = _make_repo_with_origin(tmp_path)
157
- (work_dir / "tests" / "conftest.py").write_text(
158
- "import pytest\n\n\n@pytest.fixture\ndef sample() -> int:\n return 3\n",
159
- encoding="utf-8",
160
- )
161
- assert _exemption_for(work_dir) is True
162
-
163
-
164
- def test_deleted_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
165
- work_dir = _make_repo_with_origin(tmp_path)
166
- (work_dir / "tests" / "test_app.py").unlink()
167
- assert _exemption_for(work_dir) is True
168
-
169
-
170
- def test_mixed_test_and_production_change_is_gated(tmp_path: pathlib.Path) -> None:
171
- work_dir = _make_repo_with_origin(tmp_path)
172
- (work_dir / "tests" / "test_app.py").write_text(
173
- TEST_SOURCE + "\n", encoding="utf-8"
174
- )
175
- (work_dir / "src" / "app.py").write_text(
176
- "def add(left: int, right: int) -> int:\n return left * right\n",
177
- encoding="utf-8",
178
- )
179
- assert _exemption_for(work_dir) is False
180
-
181
-
182
- def test_untracked_production_file_is_gated(tmp_path: pathlib.Path) -> None:
183
- work_dir = _make_repo_with_origin(tmp_path)
184
- (work_dir / "src" / "extra.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
185
- assert _exemption_for(work_dir) is False
186
-
187
-
188
- def test_production_file_named_like_test_outside_python_is_gated(
189
- tmp_path: pathlib.Path,
190
- ) -> None:
191
- work_dir = _make_repo_with_origin(tmp_path)
192
- (work_dir / "src" / "test_data.json").write_text("{}", encoding="utf-8")
193
- assert _exemption_for(work_dir) is False
194
-
195
-
196
- def test_comment_only_change_in_non_python_file_is_gated(
197
- tmp_path: pathlib.Path,
198
- ) -> None:
199
- work_dir = _make_repo_with_origin(tmp_path)
200
- shell_script_path = work_dir / "src" / "deploy.sh"
201
- shell_script_path.write_text("# build the project\nmake build\n", encoding="utf-8")
202
- _run_git(work_dir, "add", "-A")
203
- _run_git(work_dir, "commit", "-m", "add deploy script")
204
- shell_script_path.write_text(
205
- "# build the release artifact\nmake build\n", encoding="utf-8"
206
- )
207
- assert _exemption_for(work_dir) is False
208
-
209
-
210
- def test_corrective_message_scopes_comment_exemption_to_python() -> None:
211
- lowered_message = CORRECTIVE_MESSAGE.lower()
212
- assert "comment" in lowered_message
213
- assert "python" in lowered_message
214
- assert "comment-, and test-only surfaces are exempt" not in lowered_message
215
-
216
-
217
- def test_corrective_message_names_worktree_surface_keying() -> None:
218
- lowered_message = CORRECTIVE_MESSAGE.lower()
219
- assert "work tree" in lowered_message
220
- assert "byte-identical" in lowered_message
221
-
222
-
223
- def test_corrective_message_gives_cross_worktree_remedy() -> None:
224
- lowered_message = CORRECTIVE_MESSAGE.lower()
225
- assert "run this command from the work tree" in lowered_message
226
- assert "verify this surface here" in lowered_message
227
-
228
-
229
- def test_untracked_claude_production_hook_is_gated(tmp_path: pathlib.Path) -> None:
230
- work_dir = _make_repo_with_origin(tmp_path)
231
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
232
- new_hook_dir.mkdir(parents=True)
233
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
234
- assert _exemption_for(work_dir) is False
235
-
236
-
237
- def test_untracked_claude_production_hook_is_in_surface_manifest(
238
- tmp_path: pathlib.Path,
239
- ) -> None:
240
- work_dir = _make_repo_with_origin(tmp_path)
241
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
242
- new_hook_dir.mkdir(parents=True)
243
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
244
- merge_base_sha = resolve_merge_base(str(work_dir))
245
- assert merge_base_sha is not None
246
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
247
- assert surface_manifest_text is not None
248
- assert ".claude/hooks/blocking/evil_new_hook.py" in surface_manifest_text
249
-
250
-
251
- def test_untracked_claude_worktree_scratch_copy_stays_filtered(
252
- tmp_path: pathlib.Path,
253
- ) -> None:
254
- work_dir = _make_repo_with_origin(tmp_path)
255
- scratch_dir = work_dir / ".claude" / "worktrees" / "feature" / "src"
256
- scratch_dir.mkdir(parents=True)
257
- (scratch_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
258
- assert _exemption_for(work_dir) is True
259
- merge_base_sha = resolve_merge_base(str(work_dir))
260
- assert merge_base_sha is not None
261
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
262
- assert surface_manifest_text == ""
263
-
264
-
265
- def test_untracked_plugin_runtime_state_is_filtered_while_plain_file_is_kept(
266
- tmp_path: pathlib.Path,
267
- ) -> None:
268
- work_dir = _make_repo_with_origin(tmp_path)
269
- plugin_state_dir = (
270
- work_dir / ".claude-plugin-data" / "review-routing" / "v1" / "7366ca28"
271
- )
272
- plugin_state_dir.mkdir(parents=True)
273
- (plugin_state_dir / "decision.json").write_text("{}\n", encoding="utf-8")
274
- (work_dir / "scratch.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
275
- assert untracked_file_paths(str(work_dir)) == ["scratch.py"]
276
-
277
-
278
- @pytest.mark.parametrize(
279
- "tooling_state_relative_path",
280
- [
281
- ".claude/verification/verdict.json",
282
- ".claude/worktrees/feature/src/app.py",
283
- ".claude/daemon/state.json",
284
- ".claude/teams/roster.json",
285
- ".claude/sessions/session.json",
286
- ".cursor/worktrees/feature/src/app.py",
287
- ".claude-plugin-data/review-routing/v1/7366ca28/decision.json",
288
- ],
289
- )
290
- def test_untracked_tooling_state_path_stays_out_of_the_surface(
291
- tmp_path: pathlib.Path, tooling_state_relative_path: str
292
- ) -> None:
293
- work_dir = _make_repo_with_origin(tmp_path)
294
- tooling_state_file = work_dir / tooling_state_relative_path
295
- tooling_state_file.parent.mkdir(parents=True)
296
- tooling_state_file.write_text(PRODUCTION_SOURCE, encoding="utf-8")
297
- assert untracked_file_paths(str(work_dir)) == []
298
-
299
-
300
- @pytest.mark.parametrize(
301
- "lookalike_relative_path",
302
- [
303
- ".claude-plugin-database/settings.py",
304
- "src/.claude-plugin-data/nested_module.py",
305
- ],
306
- )
307
- def test_untracked_prefix_lookalike_stays_in_the_surface(
308
- tmp_path: pathlib.Path, lookalike_relative_path: str
309
- ) -> None:
310
- work_dir = _make_repo_with_origin(tmp_path)
311
- lookalike_file = work_dir / lookalike_relative_path
312
- lookalike_file.parent.mkdir(parents=True)
313
- lookalike_file.write_text(PRODUCTION_SOURCE, encoding="utf-8")
314
- kept_paths = untracked_file_paths(str(work_dir))
315
- assert kept_paths == [lookalike_relative_path.replace("\\", "/")]
316
-
317
-
318
- def _git_output(work_dir: pathlib.Path, *git_arguments: str) -> str:
319
- completed_process = subprocess.run(
320
- ["git", "-C", str(work_dir), *git_arguments],
321
- check=True,
322
- capture_output=True,
323
- text=True,
324
- )
325
- return completed_process.stdout.strip()
326
-
327
-
328
- def test_resolve_merge_base_finds_nonstandard_default_branch(
329
- tmp_path: pathlib.Path,
330
- ) -> None:
331
- work_dir = _make_repo_on_branch(tmp_path, "develop")
332
- subprocess.run(
333
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
334
- check=True,
335
- capture_output=True,
336
- text=True,
337
- )
338
- expected_merge_base = _git_output(
339
- work_dir, "merge-base", "HEAD", "origin/develop"
340
- )
341
- assert resolve_merge_base(str(work_dir)) == expected_merge_base
342
-
343
-
344
- def test_production_change_is_gated_on_nonstandard_default_branch(
345
- tmp_path: pathlib.Path,
346
- ) -> None:
347
- work_dir = _make_repo_on_branch(tmp_path, "develop")
348
- subprocess.run(
349
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
350
- check=True,
351
- capture_output=True,
352
- text=True,
353
- )
354
- (work_dir / "src" / "app.py").write_text(
355
- "def add(left: int, right: int) -> int:\n return left - right\n",
356
- encoding="utf-8",
357
- )
358
- assert _exemption_for(work_dir) is False
359
-
360
-
361
- MATCHING_MANIFEST_SHA256 = "a" * 64
362
- OTHER_MANIFEST_SHA256 = "b" * 64
363
- VERIFIER_AGENT_TYPE = "code-verifier"
364
-
365
-
366
- def _verdict_transcript_text(is_all_pass: bool, bound_manifest_sha256: str) -> str:
367
- verdict_record = {
368
- "all_pass": is_all_pass,
369
- "findings": [],
370
- "manifest_sha256": bound_manifest_sha256,
371
- }
372
- assistant_text = (
373
- "Verification complete.\n\n```verdict\n"
374
- + json.dumps(verdict_record)
375
- + "\n```\n"
376
- )
377
- assistant_entry = {
378
- "type": "assistant",
379
- "message": {"content": [{"type": "text", "text": assistant_text}]},
380
- }
381
- return json.dumps(assistant_entry) + "\n"
382
-
383
-
384
- def _write_agent_transcript(
385
- subagents_dir: pathlib.Path,
386
- agent_id: str,
387
- agent_type: str,
388
- transcript_text: str,
389
- should_write_sidecar: bool,
390
- ) -> None:
391
- workflow_dir = subagents_dir / "workflows" / "wf_x"
392
- workflow_dir.mkdir(parents=True, exist_ok=True)
393
- (workflow_dir / f"agent-{agent_id}.jsonl").write_text(
394
- transcript_text, encoding="utf-8"
395
- )
396
- if should_write_sidecar:
397
- (workflow_dir / f"agent-{agent_id}.meta.json").write_text(
398
- json.dumps({"agentType": agent_type}), encoding="utf-8"
399
- )
400
-
401
-
402
- def _session_transcript_path(tmp_path: pathlib.Path, session_id: str) -> pathlib.Path:
403
- session_root = tmp_path / "projects" / "demo"
404
- session_root.mkdir(parents=True)
405
- transcript_path = session_root / f"{session_id}.jsonl"
406
- transcript_path.write_text("", encoding="utf-8")
407
- return transcript_path
408
-
409
-
410
- def test_workflow_verdict_covers_surface_true_for_matching_passing_verifier(
411
- tmp_path: pathlib.Path,
412
- ) -> None:
413
- transcript_path = _session_transcript_path(tmp_path, "sess1")
414
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
415
- _write_agent_transcript(
416
- subagents_dir,
417
- "01",
418
- VERIFIER_AGENT_TYPE,
419
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
420
- should_write_sidecar=True,
421
- )
422
- assert (
423
- workflow_verdict_covers_surface(
424
- str(transcript_path), MATCHING_MANIFEST_SHA256
425
- )
426
- is True
427
- )
428
-
429
-
430
- def test_workflow_verdict_covers_surface_false_for_nonmatching_hash(
431
- tmp_path: pathlib.Path,
432
- ) -> None:
433
- transcript_path = _session_transcript_path(tmp_path, "sess1")
434
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
435
- _write_agent_transcript(
436
- subagents_dir,
437
- "01",
438
- VERIFIER_AGENT_TYPE,
439
- _verdict_transcript_text(True, OTHER_MANIFEST_SHA256),
440
- should_write_sidecar=True,
441
- )
442
- assert (
443
- workflow_verdict_covers_surface(
444
- str(transcript_path), MATCHING_MANIFEST_SHA256
445
- )
446
- is False
447
- )
448
-
449
-
450
- def test_workflow_verdict_covers_surface_false_for_all_pass_false(
451
- tmp_path: pathlib.Path,
452
- ) -> None:
453
- transcript_path = _session_transcript_path(tmp_path, "sess1")
454
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
455
- _write_agent_transcript(
456
- subagents_dir,
457
- "01",
458
- VERIFIER_AGENT_TYPE,
459
- _verdict_transcript_text(False, MATCHING_MANIFEST_SHA256),
460
- should_write_sidecar=True,
461
- )
462
- assert (
463
- workflow_verdict_covers_surface(
464
- str(transcript_path), MATCHING_MANIFEST_SHA256
465
- )
466
- is False
467
- )
468
-
469
-
470
- def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
471
- tmp_path: pathlib.Path,
472
- ) -> None:
473
- transcript_path = _session_transcript_path(tmp_path, "sess1")
474
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
475
- _write_agent_transcript(
476
- subagents_dir,
477
- "01",
478
- "clean-coder",
479
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
480
- should_write_sidecar=True,
481
- )
482
- assert (
483
- workflow_verdict_covers_surface(
484
- str(transcript_path), MATCHING_MANIFEST_SHA256
485
- )
486
- is False
487
- )
488
-
489
-
490
- def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
491
- tmp_path: pathlib.Path,
492
- ) -> None:
493
- transcript_path = _session_transcript_path(tmp_path, "sess1")
494
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
495
- _write_agent_transcript(
496
- subagents_dir,
497
- "01",
498
- VERIFIER_AGENT_TYPE,
499
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
500
- should_write_sidecar=False,
501
- )
502
- assert (
503
- workflow_verdict_covers_surface(
504
- str(transcript_path), MATCHING_MANIFEST_SHA256
505
- )
506
- is False
507
- )
508
-
509
-
510
- def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
511
- tmp_path: pathlib.Path,
512
- ) -> None:
513
- transcript_path = _session_transcript_path(tmp_path, "sess1")
514
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
515
- _write_agent_transcript(
516
- subagents_dir,
517
- "01",
518
- VERIFIER_AGENT_TYPE,
519
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
520
- should_write_sidecar=False,
521
- )
522
- corrupt_sidecar = (
523
- subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
524
- )
525
- corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
526
- assert (
527
- workflow_verdict_covers_surface(
528
- str(transcript_path), MATCHING_MANIFEST_SHA256
529
- )
530
- is False
531
- )
532
-
533
-
534
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
535
- tmp_path: pathlib.Path,
536
- ) -> None:
537
- transcript_path = _session_transcript_path(tmp_path, "sess1")
538
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
539
- _write_agent_transcript(
540
- subagents_dir,
541
- "01",
542
- VERIFIER_AGENT_TYPE,
543
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
544
- should_write_sidecar=False,
545
- )
546
- invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
547
- invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
548
- assert (
549
- workflow_verdict_covers_surface(
550
- str(transcript_path), MATCHING_MANIFEST_SHA256
551
- )
552
- is False
553
- )
554
-
555
-
556
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
557
- tmp_path: pathlib.Path,
558
- ) -> None:
559
- transcript_path = _session_transcript_path(tmp_path, "sess1")
560
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
561
- workflow_dir = subagents_dir / "workflows" / "wf_x"
562
- workflow_dir.mkdir(parents=True, exist_ok=True)
563
- (workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
564
- (workflow_dir / "agent-01.meta.json").write_text(
565
- json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
566
- )
567
- assert (
568
- workflow_verdict_covers_surface(
569
- str(transcript_path), MATCHING_MANIFEST_SHA256
570
- )
571
- is False
572
- )
573
-
574
-
575
- def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
576
- tmp_path: pathlib.Path,
577
- ) -> None:
578
- transcript_path = _session_transcript_path(tmp_path, "sess1")
579
- assert (
580
- workflow_verdict_covers_surface(
581
- str(transcript_path), MATCHING_MANIFEST_SHA256
582
- )
583
- is False
584
- )
585
-
586
-
587
- def test_workflow_verdict_covers_surface_true_when_transcript_is_under_subagents(
588
- tmp_path: pathlib.Path,
589
- ) -> None:
590
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
591
- _write_agent_transcript(
592
- subagents_dir,
593
- "01",
594
- VERIFIER_AGENT_TYPE,
595
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
596
- should_write_sidecar=True,
597
- )
598
- caller_transcript_path = (
599
- subagents_dir / "workflows" / "wf_x" / "agent-00.jsonl"
600
- )
601
- caller_transcript_path.write_text("", encoding="utf-8")
602
- assert (
603
- workflow_verdict_covers_surface(
604
- str(caller_transcript_path), MATCHING_MANIFEST_SHA256
605
- )
606
- is True
607
- )
608
-
609
-
610
- def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> None:
611
- work_dir = _make_repo_with_origin(tmp_path)
612
- (work_dir / "src" / "app.py").write_text(
613
- "def add(left: int, right: int) -> int:\n return left - right\n",
614
- encoding="utf-8",
615
- )
616
- merge_base_sha = resolve_merge_base(str(work_dir))
617
- assert merge_base_sha is not None
618
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
619
- assert surface_manifest_text is not None
620
- expected_hash = manifest_sha256(surface_manifest_text)
621
- completed_process = subprocess.run(
622
- [
623
- sys.executable,
624
- str(_HOOK_DIR / "verification_verdict_store.py"),
625
- "--manifest-hash",
626
- str(work_dir),
627
- ],
628
- check=True,
629
- capture_output=True,
630
- text=True,
631
- )
632
- assert completed_process.stdout.strip() == expected_hash
633
-
634
-
635
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
636
- home_text = str(fake_home)
637
- monkeypatch.setenv("HOME", home_text)
638
- monkeypatch.setenv("USERPROFILE", home_text)
639
- monkeypatch.delenv("HOMEDRIVE", raising=False)
640
- monkeypatch.delenv("HOMEPATH", raising=False)
641
-
642
-
643
- def test_minted_verdict_covers_surface_matches_other_worktree_by_hash(
644
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
645
- ) -> None:
646
- fake_home = tmp_path / "home"
647
- fake_home.mkdir()
648
- _isolate_home(monkeypatch, fake_home)
649
- write_verdict(
650
- str(tmp_path / "other" / "worktree"),
651
- MATCHING_MANIFEST_SHA256,
652
- True,
653
- [],
654
- "agent-x",
655
- )
656
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is True
657
-
658
-
659
- def test_minted_verdict_covers_surface_false_for_other_hash(
660
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
661
- ) -> None:
662
- fake_home = tmp_path / "home"
663
- fake_home.mkdir()
664
- _isolate_home(monkeypatch, fake_home)
665
- write_verdict(
666
- str(tmp_path / "other" / "worktree"),
667
- OTHER_MANIFEST_SHA256,
668
- True,
669
- [],
670
- "agent-x",
671
- )
672
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
673
-
674
-
675
- def test_minted_verdict_covers_surface_false_for_failing_verdict(
676
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
677
- ) -> None:
678
- fake_home = tmp_path / "home"
679
- fake_home.mkdir()
680
- _isolate_home(monkeypatch, fake_home)
681
- write_verdict(
682
- str(tmp_path / "other" / "worktree"),
683
- MATCHING_MANIFEST_SHA256,
684
- False,
685
- [{"severity": "P0", "summary": "boom"}],
686
- "agent-x",
687
- )
688
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
689
-
690
-
691
- def test_minted_verdict_covers_surface_false_when_directory_absent(
692
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
693
- ) -> None:
694
- fake_home = tmp_path / "home"
695
- fake_home.mkdir()
696
- _isolate_home(monkeypatch, fake_home)
697
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
698
-
699
-
700
- def _make_repo_with_branch_worktree(
701
- tmp_path: pathlib.Path, branch_name: str
702
- ) -> tuple[pathlib.Path, pathlib.Path]:
703
- """Create a repo with a branch checked out in a separate worktree.
704
-
705
- Returns:
706
- A tuple of (main worktree path, branch worktree path).
707
- """
708
- empty_hooks_dir = tmp_path / "nohooks"
709
- empty_hooks_dir.mkdir()
710
-
711
- main_dir = tmp_path / "main"
712
- main_dir.mkdir()
713
- _run_git(main_dir, "init", "--initial-branch=main")
714
- _run_git(main_dir, "config", "user.email", "tests@example.com")
715
- _run_git(main_dir, "config", "user.name", "Worktree Tests")
716
- _run_git(main_dir, "config", "core.hooksPath", str(empty_hooks_dir))
717
- (main_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
718
- _run_git(main_dir, "add", "-A")
719
- _run_git(main_dir, "commit", "-m", "base")
720
-
721
- origin_dir = tmp_path / "origin.git"
722
- subprocess.run(
723
- ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
724
- check=True,
725
- capture_output=True,
726
- text=True,
727
- )
728
- _run_git(main_dir, "remote", "add", "origin", str(origin_dir))
729
- _run_git(main_dir, "push", "-u", "origin", "main")
730
-
731
- _run_git(main_dir, "branch", branch_name)
732
-
733
- branch_worktree_dir = tmp_path / "branch-worktree"
734
- _run_git(main_dir, "worktree", "add", str(branch_worktree_dir), branch_name)
735
-
736
- return main_dir, branch_worktree_dir
737
-
738
-
739
- def test_worktree_path_for_branch_returns_path_when_branch_present(
740
- tmp_path: pathlib.Path,
741
- ) -> None:
742
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
743
- tmp_path, "feature-x"
744
- )
745
- resolved_path = worktree_path_for_branch(str(branch_worktree_dir), "feature-x")
746
- assert resolved_path is not None
747
- assert pathlib.Path(resolved_path).resolve() == branch_worktree_dir.resolve()
748
-
749
-
750
- def test_worktree_path_for_branch_returns_none_when_branch_absent(
751
- tmp_path: pathlib.Path,
752
- ) -> None:
753
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
754
- tmp_path, "feature-x"
755
- )
756
- resolved_path = worktree_path_for_branch(str(main_dir), "branch-never-checked-out")
757
- assert resolved_path is None
758
-
759
-
760
- def test_empty_surface_hash_equals_hash_of_empty_string() -> None:
761
- assert empty_surface_hash() == manifest_sha256("")
762
-
763
-
764
- def test_manifest_hash_cli_empty_surface_writes_guard_message_to_stderr(
765
- tmp_path: pathlib.Path,
766
- ) -> None:
767
- work_dir = _make_repo_with_origin(tmp_path)
768
- completed_process = subprocess.run(
769
- [
770
- sys.executable,
771
- str(_HOOK_DIR / "verification_verdict_store.py"),
772
- "--manifest-hash",
773
- str(work_dir),
774
- ],
775
- capture_output=True,
776
- text=True,
777
- )
778
- assert completed_process.returncode != 0
779
- assert completed_process.stdout.strip() == ""
780
- lowered_stderr = completed_process.stderr.lower()
781
- assert "wrong work tree" in lowered_stderr or "empty" in lowered_stderr
782
-
783
-
784
- def test_manifest_hash_cli_empty_surface_prints_nothing_on_stdout(
785
- tmp_path: pathlib.Path,
786
- ) -> None:
787
- work_dir = _make_repo_with_origin(tmp_path)
788
- completed_process = subprocess.run(
789
- [
790
- sys.executable,
791
- str(_HOOK_DIR / "verification_verdict_store.py"),
792
- "--manifest-hash",
793
- str(work_dir),
794
- ],
795
- capture_output=True,
796
- text=True,
797
- )
798
- assert completed_process.stdout.strip() == ""
799
-
800
-
801
- def test_manifest_hash_for_branch_cli_prints_same_hash_as_explicit_dir(
802
- tmp_path: pathlib.Path,
803
- ) -> None:
804
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
805
- tmp_path, "feature-branch"
806
- )
807
- (branch_worktree_dir / "app.py").write_text(
808
- "def add(left: int, right: int) -> int:\n return left - right\n",
809
- encoding="utf-8",
810
- )
811
- direct_process = subprocess.run(
812
- [
813
- sys.executable,
814
- str(_HOOK_DIR / "verification_verdict_store.py"),
815
- "--manifest-hash",
816
- str(branch_worktree_dir),
817
- ],
818
- capture_output=True,
819
- text=True,
820
- check=True,
821
- )
822
- branch_process = subprocess.run(
823
- [
824
- sys.executable,
825
- str(_HOOK_DIR / "verification_verdict_store.py"),
826
- "--manifest-hash-for-branch",
827
- "feature-branch",
828
- ],
829
- capture_output=True,
830
- text=True,
831
- check=True,
832
- cwd=str(branch_worktree_dir),
833
- )
834
- assert direct_process.stdout.strip() == branch_process.stdout.strip()
835
- assert direct_process.stdout.strip() != ""
836
-
837
-
838
- def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
839
- tmp_path: pathlib.Path,
840
- ) -> None:
841
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
842
- tmp_path, "feature-branch"
843
- )
844
- completed_process = subprocess.run(
845
- [
846
- sys.executable,
847
- str(_HOOK_DIR / "verification_verdict_store.py"),
848
- "--manifest-hash-for-branch",
849
- "branch-never-checked-out",
850
- ],
851
- capture_output=True,
852
- text=True,
853
- cwd=str(main_dir),
854
- )
855
- assert completed_process.returncode != 0
856
- assert completed_process.stdout.strip() == ""
857
-
858
-
859
- def test_store_imports_under_foreign_config_shadow(
860
- run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
861
- ) -> None:
862
- completed_probe = run_under_config_shadow(
863
- "import verification_verdict_store\nprint('IMPORT_OK')\n"
864
- )
865
- assert completed_probe.returncode == 0, completed_probe.stderr
866
- assert "IMPORT_OK" in completed_probe.stdout
867
-
868
-
869
- def _ledger_path(fake_home: pathlib.Path) -> pathlib.Path:
870
- return (
871
- fake_home
872
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
873
- / constants_module.LOGS_DIRECTORY_NAME
874
- / constants_module.VERIFIER_VERDICTS_JSONL_FILENAME
875
- )
876
-
877
-
878
- def _read_ledger_rows(fake_home: pathlib.Path) -> list[dict]:
879
- ledger_file = _ledger_path(fake_home)
880
- all_rows: list[dict] = []
881
- for each_line in ledger_file.read_text(encoding="utf-8").splitlines():
882
- if each_line.strip():
883
- all_rows.append(json.loads(each_line))
884
- return all_rows
885
-
886
-
887
- def test_write_verdict_appends_failing_row_that_survives_later_clean_mint(
888
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
889
- ) -> None:
890
- """A failing mint's check names stay readable after a later clean mint."""
891
- fake_home = tmp_path / "home"
892
- fake_home.mkdir()
893
- _isolate_home(monkeypatch, fake_home)
894
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
895
- work_dir = _make_repo_with_origin(tmp_path)
896
- failing_findings = [
897
- {"check": "gates.import", "detail": "ImportError: boom"},
898
- {"check": "task.item-2", "detail": "missing hunk"},
899
- ]
900
- write_verdict(
901
- str(work_dir),
902
- MATCHING_MANIFEST_SHA256,
903
- False,
904
- failing_findings,
905
- "agent-fail",
906
- )
907
- write_verdict(
908
- str(work_dir),
909
- MATCHING_MANIFEST_SHA256,
910
- True,
911
- [],
912
- "agent-pass",
913
- )
914
- all_rows = _read_ledger_rows(fake_home)
915
- assert len(all_rows) == 2
916
- failing_row = all_rows[0]
917
- clean_row = all_rows[1]
918
- assert failing_row[constants_module.LEDGER_KEY_ALL_PASS] is False
919
- assert failing_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 2
920
- assert failing_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == [
921
- "gates.import",
922
- "task.item-2",
923
- ]
924
- assert failing_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-fail"
925
- assert failing_row[constants_module.LEDGER_KEY_MANIFEST_HASH] == MATCHING_MANIFEST_SHA256
926
- assert failing_row[constants_module.LEDGER_KEY_REPO_ROOT] == str(work_dir)
927
- assert failing_row[constants_module.LEDGER_KEY_BRANCH] == "main"
928
- assert clean_row[constants_module.LEDGER_KEY_ALL_PASS] is True
929
- assert clean_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 0
930
- assert clean_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == []
931
- assert clean_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-pass"
932
-
933
-
934
- def test_write_verdict_returns_path_when_ledger_append_fails(
935
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
936
- ) -> None:
937
- """A ledger IO failure never changes the mint path or verdict file write."""
938
- fake_home = tmp_path / "home"
939
- fake_home.mkdir()
940
- _isolate_home(monkeypatch, fake_home)
941
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
942
- logs_as_file = (
943
- fake_home
944
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
945
- / constants_module.LOGS_DIRECTORY_NAME
946
- )
947
- logs_as_file.parent.mkdir(parents=True, exist_ok=True)
948
- logs_as_file.write_text("not-a-directory", encoding="utf-8")
949
- work_dir = _make_repo_with_origin(tmp_path)
950
- verdict_file = write_verdict(
951
- str(work_dir),
952
- MATCHING_MANIFEST_SHA256,
953
- True,
954
- [],
955
- "agent-x",
956
- )
957
- assert verdict_file.is_file()
958
- verdict_record = json.loads(verdict_file.read_text(encoding="utf-8"))
959
- assert verdict_record[constants_module.VERDICT_KEY_ALL_PASS] is True
960
-
961
-
962
- def test_root_key_for_repo_folds_subdirectory_and_case(tmp_path: pathlib.Path) -> None:
963
- work_dir = tmp_path / "Repo"
964
- subdirectory = work_dir / "src"
965
- subdirectory.mkdir(parents=True)
966
- root_key = root_key_for_repo(str(work_dir))
967
- assert root_key_for_repo(str(subdirectory.parent)) == root_key
968
- assert root_key_for_repo(str(work_dir).lower()) == root_key
969
-
970
-
971
- def test_verdict_path_for_repo_stem_is_the_shared_root_key(tmp_path: pathlib.Path) -> None:
972
- work_dir = tmp_path / "repo"
973
- work_dir.mkdir()
974
- assert verdict_path_for_repo(str(work_dir)).stem == root_key_for_repo(str(work_dir))