claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -1,903 +1,974 @@
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
- branch_surface_manifest = store_module.branch_surface_manifest
32
- manifest_sha256 = store_module.manifest_sha256
33
- workflow_verdict_covers_surface = store_module.workflow_verdict_covers_surface
34
- minted_verdict_covers_surface = store_module.minted_verdict_covers_surface
35
- write_verdict = store_module.write_verdict
36
- worktree_path_for_branch = store_module.worktree_path_for_branch
37
- empty_surface_hash = store_module.empty_surface_hash
38
-
39
- constants_spec = importlib.util.spec_from_file_location(
40
- "verified_commit_constants",
41
- _HOOK_DIR / "config" / "verified_commit_constants.py",
42
- )
43
- assert constants_spec is not None
44
- assert constants_spec.loader is not None
45
- constants_module = importlib.util.module_from_spec(constants_spec)
46
- constants_spec.loader.exec_module(constants_module)
47
- CORRECTIVE_MESSAGE = constants_module.CORRECTIVE_MESSAGE
48
- EMPTY_SURFACE_GUARD_MESSAGE = constants_module.EMPTY_SURFACE_GUARD_MESSAGE
49
- BRANCH_WORKTREE_ABSENT_MESSAGE = constants_module.BRANCH_WORKTREE_ABSENT_MESSAGE
50
-
51
- PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
52
- TEST_SOURCE = "def test_add() -> None:\n assert 1 + 1 == 2\n"
53
-
54
-
55
- def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
56
- subprocess.run(
57
- ["git", "-C", str(repo_dir), *git_arguments],
58
- check=True,
59
- capture_output=True,
60
- text=True,
61
- )
62
-
63
-
64
- def _make_repo_on_branch(
65
- tmp_path: pathlib.Path, branch_name: str
66
- ) -> pathlib.Path:
67
- origin_dir = tmp_path / "origin.git"
68
- work_dir = tmp_path / "work"
69
- work_dir.mkdir()
70
- subprocess.run(
71
- ["git", "init", "--bare", f"--initial-branch={branch_name}", str(origin_dir)],
72
- check=True,
73
- capture_output=True,
74
- text=True,
75
- )
76
- empty_hooks_dir = tmp_path / "nohooks"
77
- empty_hooks_dir.mkdir()
78
- _run_git(work_dir, "init", f"--initial-branch={branch_name}")
79
- _run_git(work_dir, "config", "user.email", "tests@example.com")
80
- _run_git(work_dir, "config", "user.name", "Verdict Store Tests")
81
- _run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
82
- (work_dir / "src").mkdir()
83
- (work_dir / "tests").mkdir()
84
- (work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
85
- (work_dir / "tests" / "test_app.py").write_text(TEST_SOURCE, encoding="utf-8")
86
- (work_dir / "README.md").write_text("# Fixture repo\n", encoding="utf-8")
87
- _run_git(work_dir, "add", "-A")
88
- _run_git(work_dir, "commit", "-m", "base")
89
- _run_git(work_dir, "remote", "add", "origin", str(origin_dir))
90
- _run_git(work_dir, "push", "-u", "origin", branch_name)
91
- return work_dir
92
-
93
-
94
- def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
95
- return _make_repo_on_branch(tmp_path, "main")
96
-
97
-
98
- def _exemption_for(work_dir: pathlib.Path) -> bool:
99
- merge_base_sha = resolve_merge_base(str(work_dir))
100
- assert merge_base_sha is not None
101
- return is_verification_exempt_diff(str(work_dir), merge_base_sha)
102
-
103
-
104
- def test_production_change_is_gated(tmp_path: pathlib.Path) -> None:
105
- work_dir = _make_repo_with_origin(tmp_path)
106
- (work_dir / "src" / "app.py").write_text(
107
- "def add(left: int, right: int) -> int:\n return left - right\n",
108
- encoding="utf-8",
109
- )
110
- assert _exemption_for(work_dir) is False
111
-
112
-
113
- def test_docs_only_change_is_exempt(tmp_path: pathlib.Path) -> None:
114
- work_dir = _make_repo_with_origin(tmp_path)
115
- (work_dir / "README.md").write_text(
116
- "# Fixture repo\n\nUpdated.\n", encoding="utf-8"
117
- )
118
- assert _exemption_for(work_dir) is True
119
-
120
-
121
- def test_docstring_only_python_change_is_exempt(tmp_path: pathlib.Path) -> None:
122
- work_dir = _make_repo_with_origin(tmp_path)
123
- (work_dir / "src" / "app.py").write_text(
124
- 'def add(left: int, right: int) -> int:\n """Add two integers."""\n'
125
- " return left + right\n",
126
- encoding="utf-8",
127
- )
128
- assert _exemption_for(work_dir) is True
129
-
130
-
131
- def test_modified_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
132
- work_dir = _make_repo_with_origin(tmp_path)
133
- (work_dir / "tests" / "test_app.py").write_text(
134
- TEST_SOURCE + "\n\ndef test_add_zero() -> None:\n assert 0 + 0 == 0\n",
135
- encoding="utf-8",
136
- )
137
- assert _exemption_for(work_dir) is True
138
-
139
-
140
- def test_untracked_test_prefix_file_is_exempt(tmp_path: pathlib.Path) -> None:
141
- work_dir = _make_repo_with_origin(tmp_path)
142
- (work_dir / "tests" / "test_extra.py").write_text(TEST_SOURCE, encoding="utf-8")
143
- assert _exemption_for(work_dir) is True
144
-
145
-
146
- def test_untracked_test_suffix_file_is_exempt(tmp_path: pathlib.Path) -> None:
147
- work_dir = _make_repo_with_origin(tmp_path)
148
- (work_dir / "tests" / "app_test.py").write_text(TEST_SOURCE, encoding="utf-8")
149
- assert _exemption_for(work_dir) is True
150
-
151
-
152
- def test_modified_conftest_is_exempt(tmp_path: pathlib.Path) -> None:
153
- work_dir = _make_repo_with_origin(tmp_path)
154
- (work_dir / "tests" / "conftest.py").write_text(
155
- "import pytest\n\n\n@pytest.fixture\ndef sample() -> int:\n return 3\n",
156
- encoding="utf-8",
157
- )
158
- assert _exemption_for(work_dir) is True
159
-
160
-
161
- def test_deleted_test_file_is_exempt(tmp_path: pathlib.Path) -> None:
162
- work_dir = _make_repo_with_origin(tmp_path)
163
- (work_dir / "tests" / "test_app.py").unlink()
164
- assert _exemption_for(work_dir) is True
165
-
166
-
167
- def test_mixed_test_and_production_change_is_gated(tmp_path: pathlib.Path) -> None:
168
- work_dir = _make_repo_with_origin(tmp_path)
169
- (work_dir / "tests" / "test_app.py").write_text(
170
- TEST_SOURCE + "\n", encoding="utf-8"
171
- )
172
- (work_dir / "src" / "app.py").write_text(
173
- "def add(left: int, right: int) -> int:\n return left * right\n",
174
- encoding="utf-8",
175
- )
176
- assert _exemption_for(work_dir) is False
177
-
178
-
179
- def test_untracked_production_file_is_gated(tmp_path: pathlib.Path) -> None:
180
- work_dir = _make_repo_with_origin(tmp_path)
181
- (work_dir / "src" / "extra.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
182
- assert _exemption_for(work_dir) is False
183
-
184
-
185
- def test_production_file_named_like_test_outside_python_is_gated(
186
- tmp_path: pathlib.Path,
187
- ) -> None:
188
- work_dir = _make_repo_with_origin(tmp_path)
189
- (work_dir / "src" / "test_data.json").write_text("{}", encoding="utf-8")
190
- assert _exemption_for(work_dir) is False
191
-
192
-
193
- def test_comment_only_change_in_non_python_file_is_gated(
194
- tmp_path: pathlib.Path,
195
- ) -> None:
196
- work_dir = _make_repo_with_origin(tmp_path)
197
- shell_script_path = work_dir / "src" / "deploy.sh"
198
- shell_script_path.write_text("# build the project\nmake build\n", encoding="utf-8")
199
- _run_git(work_dir, "add", "-A")
200
- _run_git(work_dir, "commit", "-m", "add deploy script")
201
- shell_script_path.write_text(
202
- "# build the release artifact\nmake build\n", encoding="utf-8"
203
- )
204
- assert _exemption_for(work_dir) is False
205
-
206
-
207
- def test_corrective_message_scopes_comment_exemption_to_python() -> None:
208
- lowered_message = CORRECTIVE_MESSAGE.lower()
209
- assert "comment" in lowered_message
210
- assert "python" in lowered_message
211
- assert "comment-, and test-only surfaces are exempt" not in lowered_message
212
-
213
-
214
- def test_corrective_message_names_worktree_surface_keying() -> None:
215
- lowered_message = CORRECTIVE_MESSAGE.lower()
216
- assert "work tree" in lowered_message
217
- assert "byte-identical" in lowered_message
218
-
219
-
220
- def test_corrective_message_gives_cross_worktree_remedy() -> None:
221
- lowered_message = CORRECTIVE_MESSAGE.lower()
222
- assert "run this command from the work tree" in lowered_message
223
- assert "verify this surface here" in lowered_message
224
-
225
-
226
- def test_untracked_claude_production_hook_is_gated(tmp_path: pathlib.Path) -> None:
227
- work_dir = _make_repo_with_origin(tmp_path)
228
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
229
- new_hook_dir.mkdir(parents=True)
230
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
231
- assert _exemption_for(work_dir) is False
232
-
233
-
234
- def test_untracked_claude_production_hook_is_in_surface_manifest(
235
- tmp_path: pathlib.Path,
236
- ) -> None:
237
- work_dir = _make_repo_with_origin(tmp_path)
238
- new_hook_dir = work_dir / ".claude" / "hooks" / "blocking"
239
- new_hook_dir.mkdir(parents=True)
240
- (new_hook_dir / "evil_new_hook.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
241
- merge_base_sha = resolve_merge_base(str(work_dir))
242
- assert merge_base_sha is not None
243
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
244
- assert surface_manifest_text is not None
245
- assert ".claude/hooks/blocking/evil_new_hook.py" in surface_manifest_text
246
-
247
-
248
- def test_untracked_claude_worktree_scratch_copy_stays_filtered(
249
- tmp_path: pathlib.Path,
250
- ) -> None:
251
- work_dir = _make_repo_with_origin(tmp_path)
252
- scratch_dir = work_dir / ".claude" / "worktrees" / "feature" / "src"
253
- scratch_dir.mkdir(parents=True)
254
- (scratch_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
255
- assert _exemption_for(work_dir) is True
256
- merge_base_sha = resolve_merge_base(str(work_dir))
257
- assert merge_base_sha is not None
258
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
259
- assert surface_manifest_text == ""
260
-
261
-
262
- def _git_output(work_dir: pathlib.Path, *git_arguments: str) -> str:
263
- completed_process = subprocess.run(
264
- ["git", "-C", str(work_dir), *git_arguments],
265
- check=True,
266
- capture_output=True,
267
- text=True,
268
- )
269
- return completed_process.stdout.strip()
270
-
271
-
272
- def test_resolve_merge_base_finds_nonstandard_default_branch(
273
- tmp_path: pathlib.Path,
274
- ) -> None:
275
- work_dir = _make_repo_on_branch(tmp_path, "develop")
276
- subprocess.run(
277
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
278
- check=True,
279
- capture_output=True,
280
- text=True,
281
- )
282
- expected_merge_base = _git_output(
283
- work_dir, "merge-base", "HEAD", "origin/develop"
284
- )
285
- assert resolve_merge_base(str(work_dir)) == expected_merge_base
286
-
287
-
288
- def test_production_change_is_gated_on_nonstandard_default_branch(
289
- tmp_path: pathlib.Path,
290
- ) -> None:
291
- work_dir = _make_repo_on_branch(tmp_path, "develop")
292
- subprocess.run(
293
- ["git", "-C", str(work_dir), "remote", "set-head", "origin", "--delete"],
294
- check=True,
295
- capture_output=True,
296
- text=True,
297
- )
298
- (work_dir / "src" / "app.py").write_text(
299
- "def add(left: int, right: int) -> int:\n return left - right\n",
300
- encoding="utf-8",
301
- )
302
- assert _exemption_for(work_dir) is False
303
-
304
-
305
- MATCHING_MANIFEST_SHA256 = "a" * 64
306
- OTHER_MANIFEST_SHA256 = "b" * 64
307
- VERIFIER_AGENT_TYPE = "code-verifier"
308
-
309
-
310
- def _verdict_transcript_text(is_all_pass: bool, bound_manifest_sha256: str) -> str:
311
- verdict_record = {
312
- "all_pass": is_all_pass,
313
- "findings": [],
314
- "manifest_sha256": bound_manifest_sha256,
315
- }
316
- assistant_text = (
317
- "Verification complete.\n\n```verdict\n"
318
- + json.dumps(verdict_record)
319
- + "\n```\n"
320
- )
321
- assistant_entry = {
322
- "type": "assistant",
323
- "message": {"content": [{"type": "text", "text": assistant_text}]},
324
- }
325
- return json.dumps(assistant_entry) + "\n"
326
-
327
-
328
- def _write_agent_transcript(
329
- subagents_dir: pathlib.Path,
330
- agent_id: str,
331
- agent_type: str,
332
- transcript_text: str,
333
- should_write_sidecar: bool,
334
- ) -> None:
335
- workflow_dir = subagents_dir / "workflows" / "wf_x"
336
- workflow_dir.mkdir(parents=True, exist_ok=True)
337
- (workflow_dir / f"agent-{agent_id}.jsonl").write_text(
338
- transcript_text, encoding="utf-8"
339
- )
340
- if should_write_sidecar:
341
- (workflow_dir / f"agent-{agent_id}.meta.json").write_text(
342
- json.dumps({"agentType": agent_type}), encoding="utf-8"
343
- )
344
-
345
-
346
- def _session_transcript_path(tmp_path: pathlib.Path, session_id: str) -> pathlib.Path:
347
- session_root = tmp_path / "projects" / "demo"
348
- session_root.mkdir(parents=True)
349
- transcript_path = session_root / f"{session_id}.jsonl"
350
- transcript_path.write_text("", encoding="utf-8")
351
- return transcript_path
352
-
353
-
354
- def test_workflow_verdict_covers_surface_true_for_matching_passing_verifier(
355
- tmp_path: pathlib.Path,
356
- ) -> None:
357
- transcript_path = _session_transcript_path(tmp_path, "sess1")
358
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
359
- _write_agent_transcript(
360
- subagents_dir,
361
- "01",
362
- VERIFIER_AGENT_TYPE,
363
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
364
- should_write_sidecar=True,
365
- )
366
- assert (
367
- workflow_verdict_covers_surface(
368
- str(transcript_path), MATCHING_MANIFEST_SHA256
369
- )
370
- is True
371
- )
372
-
373
-
374
- def test_workflow_verdict_covers_surface_false_for_nonmatching_hash(
375
- tmp_path: pathlib.Path,
376
- ) -> None:
377
- transcript_path = _session_transcript_path(tmp_path, "sess1")
378
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
379
- _write_agent_transcript(
380
- subagents_dir,
381
- "01",
382
- VERIFIER_AGENT_TYPE,
383
- _verdict_transcript_text(True, OTHER_MANIFEST_SHA256),
384
- should_write_sidecar=True,
385
- )
386
- assert (
387
- workflow_verdict_covers_surface(
388
- str(transcript_path), MATCHING_MANIFEST_SHA256
389
- )
390
- is False
391
- )
392
-
393
-
394
- def test_workflow_verdict_covers_surface_false_for_all_pass_false(
395
- tmp_path: pathlib.Path,
396
- ) -> None:
397
- transcript_path = _session_transcript_path(tmp_path, "sess1")
398
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
399
- _write_agent_transcript(
400
- subagents_dir,
401
- "01",
402
- VERIFIER_AGENT_TYPE,
403
- _verdict_transcript_text(False, MATCHING_MANIFEST_SHA256),
404
- should_write_sidecar=True,
405
- )
406
- assert (
407
- workflow_verdict_covers_surface(
408
- str(transcript_path), MATCHING_MANIFEST_SHA256
409
- )
410
- is False
411
- )
412
-
413
-
414
- def test_workflow_verdict_covers_surface_false_for_non_verifier_sidecar(
415
- tmp_path: pathlib.Path,
416
- ) -> None:
417
- transcript_path = _session_transcript_path(tmp_path, "sess1")
418
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
419
- _write_agent_transcript(
420
- subagents_dir,
421
- "01",
422
- "clean-coder",
423
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
424
- should_write_sidecar=True,
425
- )
426
- assert (
427
- workflow_verdict_covers_surface(
428
- str(transcript_path), MATCHING_MANIFEST_SHA256
429
- )
430
- is False
431
- )
432
-
433
-
434
- def test_workflow_verdict_covers_surface_false_for_missing_sidecar_with_verdict(
435
- tmp_path: pathlib.Path,
436
- ) -> None:
437
- transcript_path = _session_transcript_path(tmp_path, "sess1")
438
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
439
- _write_agent_transcript(
440
- subagents_dir,
441
- "01",
442
- VERIFIER_AGENT_TYPE,
443
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
444
- should_write_sidecar=False,
445
- )
446
- assert (
447
- workflow_verdict_covers_surface(
448
- str(transcript_path), MATCHING_MANIFEST_SHA256
449
- )
450
- is False
451
- )
452
-
453
-
454
- def test_workflow_verdict_covers_surface_false_for_corrupt_sidecar_with_verdict(
455
- tmp_path: pathlib.Path,
456
- ) -> None:
457
- transcript_path = _session_transcript_path(tmp_path, "sess1")
458
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
459
- _write_agent_transcript(
460
- subagents_dir,
461
- "01",
462
- VERIFIER_AGENT_TYPE,
463
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
464
- should_write_sidecar=False,
465
- )
466
- corrupt_sidecar = (
467
- subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
468
- )
469
- corrupt_sidecar.write_text("{not valid json", encoding="utf-8")
470
- assert (
471
- workflow_verdict_covers_surface(
472
- str(transcript_path), MATCHING_MANIFEST_SHA256
473
- )
474
- is False
475
- )
476
-
477
-
478
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_sidecar(
479
- tmp_path: pathlib.Path,
480
- ) -> None:
481
- transcript_path = _session_transcript_path(tmp_path, "sess1")
482
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
483
- _write_agent_transcript(
484
- subagents_dir,
485
- "01",
486
- VERIFIER_AGENT_TYPE,
487
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
488
- should_write_sidecar=False,
489
- )
490
- invalid_utf8_sidecar = subagents_dir / "workflows" / "wf_x" / "agent-01.meta.json"
491
- invalid_utf8_sidecar.write_bytes(b'{"agentType": "\xff\xfe bad"}')
492
- assert (
493
- workflow_verdict_covers_surface(
494
- str(transcript_path), MATCHING_MANIFEST_SHA256
495
- )
496
- is False
497
- )
498
-
499
-
500
- def test_workflow_verdict_covers_surface_false_for_invalid_utf8_transcript(
501
- tmp_path: pathlib.Path,
502
- ) -> None:
503
- transcript_path = _session_transcript_path(tmp_path, "sess1")
504
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
505
- workflow_dir = subagents_dir / "workflows" / "wf_x"
506
- workflow_dir.mkdir(parents=True, exist_ok=True)
507
- (workflow_dir / "agent-01.jsonl").write_bytes(b'{"type": "assistant"}\xff\xfe\n')
508
- (workflow_dir / "agent-01.meta.json").write_text(
509
- json.dumps({"agentType": VERIFIER_AGENT_TYPE}), encoding="utf-8"
510
- )
511
- assert (
512
- workflow_verdict_covers_surface(
513
- str(transcript_path), MATCHING_MANIFEST_SHA256
514
- )
515
- is False
516
- )
517
-
518
-
519
- def test_workflow_verdict_covers_surface_false_for_missing_subagents_dir(
520
- tmp_path: pathlib.Path,
521
- ) -> None:
522
- transcript_path = _session_transcript_path(tmp_path, "sess1")
523
- assert (
524
- workflow_verdict_covers_surface(
525
- str(transcript_path), MATCHING_MANIFEST_SHA256
526
- )
527
- is False
528
- )
529
-
530
-
531
- def test_workflow_verdict_covers_surface_true_when_transcript_is_under_subagents(
532
- tmp_path: pathlib.Path,
533
- ) -> None:
534
- subagents_dir = tmp_path / "projects" / "demo" / "sess1" / "subagents"
535
- _write_agent_transcript(
536
- subagents_dir,
537
- "01",
538
- VERIFIER_AGENT_TYPE,
539
- _verdict_transcript_text(True, MATCHING_MANIFEST_SHA256),
540
- should_write_sidecar=True,
541
- )
542
- caller_transcript_path = (
543
- subagents_dir / "workflows" / "wf_x" / "agent-00.jsonl"
544
- )
545
- caller_transcript_path.write_text("", encoding="utf-8")
546
- assert (
547
- workflow_verdict_covers_surface(
548
- str(caller_transcript_path), MATCHING_MANIFEST_SHA256
549
- )
550
- is True
551
- )
552
-
553
-
554
- def test_manifest_hash_cli_prints_live_surface_hash(tmp_path: pathlib.Path) -> None:
555
- work_dir = _make_repo_with_origin(tmp_path)
556
- (work_dir / "src" / "app.py").write_text(
557
- "def add(left: int, right: int) -> int:\n return left - right\n",
558
- encoding="utf-8",
559
- )
560
- merge_base_sha = resolve_merge_base(str(work_dir))
561
- assert merge_base_sha is not None
562
- surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
563
- assert surface_manifest_text is not None
564
- expected_hash = manifest_sha256(surface_manifest_text)
565
- completed_process = subprocess.run(
566
- [
567
- sys.executable,
568
- str(_HOOK_DIR / "verification_verdict_store.py"),
569
- "--manifest-hash",
570
- str(work_dir),
571
- ],
572
- check=True,
573
- capture_output=True,
574
- text=True,
575
- )
576
- assert completed_process.stdout.strip() == expected_hash
577
-
578
-
579
- def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
580
- home_text = str(fake_home)
581
- monkeypatch.setenv("HOME", home_text)
582
- monkeypatch.setenv("USERPROFILE", home_text)
583
- monkeypatch.delenv("HOMEDRIVE", raising=False)
584
- monkeypatch.delenv("HOMEPATH", raising=False)
585
-
586
-
587
- def test_minted_verdict_covers_surface_matches_other_worktree_by_hash(
588
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
589
- ) -> None:
590
- fake_home = tmp_path / "home"
591
- fake_home.mkdir()
592
- _isolate_home(monkeypatch, fake_home)
593
- write_verdict(
594
- str(tmp_path / "other" / "worktree"),
595
- MATCHING_MANIFEST_SHA256,
596
- True,
597
- [],
598
- "agent-x",
599
- )
600
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is True
601
-
602
-
603
- def test_minted_verdict_covers_surface_false_for_other_hash(
604
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
605
- ) -> None:
606
- fake_home = tmp_path / "home"
607
- fake_home.mkdir()
608
- _isolate_home(monkeypatch, fake_home)
609
- write_verdict(
610
- str(tmp_path / "other" / "worktree"),
611
- OTHER_MANIFEST_SHA256,
612
- True,
613
- [],
614
- "agent-x",
615
- )
616
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
617
-
618
-
619
- def test_minted_verdict_covers_surface_false_for_failing_verdict(
620
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
621
- ) -> None:
622
- fake_home = tmp_path / "home"
623
- fake_home.mkdir()
624
- _isolate_home(monkeypatch, fake_home)
625
- write_verdict(
626
- str(tmp_path / "other" / "worktree"),
627
- MATCHING_MANIFEST_SHA256,
628
- False,
629
- [{"severity": "P0", "summary": "boom"}],
630
- "agent-x",
631
- )
632
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
633
-
634
-
635
- def test_minted_verdict_covers_surface_false_when_directory_absent(
636
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
637
- ) -> None:
638
- fake_home = tmp_path / "home"
639
- fake_home.mkdir()
640
- _isolate_home(monkeypatch, fake_home)
641
- assert minted_verdict_covers_surface(MATCHING_MANIFEST_SHA256) is False
642
-
643
-
644
- def _make_repo_with_branch_worktree(
645
- tmp_path: pathlib.Path, branch_name: str
646
- ) -> tuple[pathlib.Path, pathlib.Path]:
647
- """Create a repo with a branch checked out in a separate worktree.
648
-
649
- Returns:
650
- A tuple of (main worktree path, branch worktree path).
651
- """
652
- empty_hooks_dir = tmp_path / "nohooks"
653
- empty_hooks_dir.mkdir()
654
-
655
- main_dir = tmp_path / "main"
656
- main_dir.mkdir()
657
- _run_git(main_dir, "init", "--initial-branch=main")
658
- _run_git(main_dir, "config", "user.email", "tests@example.com")
659
- _run_git(main_dir, "config", "user.name", "Worktree Tests")
660
- _run_git(main_dir, "config", "core.hooksPath", str(empty_hooks_dir))
661
- (main_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
662
- _run_git(main_dir, "add", "-A")
663
- _run_git(main_dir, "commit", "-m", "base")
664
-
665
- origin_dir = tmp_path / "origin.git"
666
- subprocess.run(
667
- ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
668
- check=True,
669
- capture_output=True,
670
- text=True,
671
- )
672
- _run_git(main_dir, "remote", "add", "origin", str(origin_dir))
673
- _run_git(main_dir, "push", "-u", "origin", "main")
674
-
675
- _run_git(main_dir, "branch", branch_name)
676
-
677
- branch_worktree_dir = tmp_path / "branch-worktree"
678
- _run_git(main_dir, "worktree", "add", str(branch_worktree_dir), branch_name)
679
-
680
- return main_dir, branch_worktree_dir
681
-
682
-
683
- def test_worktree_path_for_branch_returns_path_when_branch_present(
684
- tmp_path: pathlib.Path,
685
- ) -> None:
686
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
687
- tmp_path, "feature-x"
688
- )
689
- resolved_path = worktree_path_for_branch(str(branch_worktree_dir), "feature-x")
690
- assert resolved_path is not None
691
- assert pathlib.Path(resolved_path).resolve() == branch_worktree_dir.resolve()
692
-
693
-
694
- def test_worktree_path_for_branch_returns_none_when_branch_absent(
695
- tmp_path: pathlib.Path,
696
- ) -> None:
697
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
698
- tmp_path, "feature-x"
699
- )
700
- resolved_path = worktree_path_for_branch(str(main_dir), "branch-never-checked-out")
701
- assert resolved_path is None
702
-
703
-
704
- def test_empty_surface_hash_equals_hash_of_empty_string() -> None:
705
- assert empty_surface_hash() == manifest_sha256("")
706
-
707
-
708
- def test_manifest_hash_cli_empty_surface_writes_guard_message_to_stderr(
709
- tmp_path: pathlib.Path,
710
- ) -> None:
711
- work_dir = _make_repo_with_origin(tmp_path)
712
- completed_process = subprocess.run(
713
- [
714
- sys.executable,
715
- str(_HOOK_DIR / "verification_verdict_store.py"),
716
- "--manifest-hash",
717
- str(work_dir),
718
- ],
719
- capture_output=True,
720
- text=True,
721
- )
722
- assert completed_process.returncode != 0
723
- assert completed_process.stdout.strip() == ""
724
- lowered_stderr = completed_process.stderr.lower()
725
- assert "wrong work tree" in lowered_stderr or "empty" in lowered_stderr
726
-
727
-
728
- def test_manifest_hash_cli_empty_surface_prints_nothing_on_stdout(
729
- tmp_path: pathlib.Path,
730
- ) -> None:
731
- work_dir = _make_repo_with_origin(tmp_path)
732
- completed_process = subprocess.run(
733
- [
734
- sys.executable,
735
- str(_HOOK_DIR / "verification_verdict_store.py"),
736
- "--manifest-hash",
737
- str(work_dir),
738
- ],
739
- capture_output=True,
740
- text=True,
741
- )
742
- assert completed_process.stdout.strip() == ""
743
-
744
-
745
- def test_manifest_hash_for_branch_cli_prints_same_hash_as_explicit_dir(
746
- tmp_path: pathlib.Path,
747
- ) -> None:
748
- _main_dir, branch_worktree_dir = _make_repo_with_branch_worktree(
749
- tmp_path, "feature-branch"
750
- )
751
- (branch_worktree_dir / "app.py").write_text(
752
- "def add(left: int, right: int) -> int:\n return left - right\n",
753
- encoding="utf-8",
754
- )
755
- direct_process = subprocess.run(
756
- [
757
- sys.executable,
758
- str(_HOOK_DIR / "verification_verdict_store.py"),
759
- "--manifest-hash",
760
- str(branch_worktree_dir),
761
- ],
762
- capture_output=True,
763
- text=True,
764
- check=True,
765
- )
766
- branch_process = subprocess.run(
767
- [
768
- sys.executable,
769
- str(_HOOK_DIR / "verification_verdict_store.py"),
770
- "--manifest-hash-for-branch",
771
- "feature-branch",
772
- ],
773
- capture_output=True,
774
- text=True,
775
- check=True,
776
- cwd=str(branch_worktree_dir),
777
- )
778
- assert direct_process.stdout.strip() == branch_process.stdout.strip()
779
- assert direct_process.stdout.strip() != ""
780
-
781
-
782
- def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
783
- tmp_path: pathlib.Path,
784
- ) -> None:
785
- main_dir, _branch_worktree_dir = _make_repo_with_branch_worktree(
786
- tmp_path, "feature-branch"
787
- )
788
- completed_process = subprocess.run(
789
- [
790
- sys.executable,
791
- str(_HOOK_DIR / "verification_verdict_store.py"),
792
- "--manifest-hash-for-branch",
793
- "branch-never-checked-out",
794
- ],
795
- capture_output=True,
796
- text=True,
797
- cwd=str(main_dir),
798
- )
799
- assert completed_process.returncode != 0
800
- assert completed_process.stdout.strip() == ""
801
-
802
-
803
- def test_store_imports_under_foreign_config_shadow(
804
- run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
805
- ) -> None:
806
- completed_probe = run_under_config_shadow(
807
- "import verification_verdict_store\nprint('IMPORT_OK')\n"
808
- )
809
- assert completed_probe.returncode == 0, completed_probe.stderr
810
- assert "IMPORT_OK" in completed_probe.stdout
811
-
812
-
813
- def _ledger_path(fake_home: pathlib.Path) -> pathlib.Path:
814
- return (
815
- fake_home
816
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
817
- / constants_module.LOGS_DIRECTORY_NAME
818
- / constants_module.VERIFIER_VERDICTS_JSONL_FILENAME
819
- )
820
-
821
-
822
- def _read_ledger_rows(fake_home: pathlib.Path) -> list[dict]:
823
- ledger_file = _ledger_path(fake_home)
824
- all_rows: list[dict] = []
825
- for each_line in ledger_file.read_text(encoding="utf-8").splitlines():
826
- if each_line.strip():
827
- all_rows.append(json.loads(each_line))
828
- return all_rows
829
-
830
-
831
- def test_write_verdict_appends_failing_row_that_survives_later_clean_mint(
832
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
833
- ) -> None:
834
- """A failing mint's check names stay readable after a later clean mint."""
835
- fake_home = tmp_path / "home"
836
- fake_home.mkdir()
837
- _isolate_home(monkeypatch, fake_home)
838
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
839
- work_dir = _make_repo_with_origin(tmp_path)
840
- failing_findings = [
841
- {"check": "gates.import", "detail": "ImportError: boom"},
842
- {"check": "task.item-2", "detail": "missing hunk"},
843
- ]
844
- write_verdict(
845
- str(work_dir),
846
- MATCHING_MANIFEST_SHA256,
847
- False,
848
- failing_findings,
849
- "agent-fail",
850
- )
851
- write_verdict(
852
- str(work_dir),
853
- MATCHING_MANIFEST_SHA256,
854
- True,
855
- [],
856
- "agent-pass",
857
- )
858
- all_rows = _read_ledger_rows(fake_home)
859
- assert len(all_rows) == 2
860
- failing_row = all_rows[0]
861
- clean_row = all_rows[1]
862
- assert failing_row[constants_module.LEDGER_KEY_ALL_PASS] is False
863
- assert failing_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 2
864
- assert failing_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == [
865
- "gates.import",
866
- "task.item-2",
867
- ]
868
- assert failing_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-fail"
869
- assert failing_row[constants_module.LEDGER_KEY_MANIFEST_HASH] == MATCHING_MANIFEST_SHA256
870
- assert failing_row[constants_module.LEDGER_KEY_REPO_ROOT] == str(work_dir)
871
- assert failing_row[constants_module.LEDGER_KEY_BRANCH] == "main"
872
- assert clean_row[constants_module.LEDGER_KEY_ALL_PASS] is True
873
- assert clean_row[constants_module.LEDGER_KEY_FINDING_COUNT] == 0
874
- assert clean_row[constants_module.LEDGER_KEY_FINDING_CHECK_NAMES] == []
875
- assert clean_row[constants_module.LEDGER_KEY_MINTED_FROM_AGENT_ID] == "agent-pass"
876
-
877
-
878
- def test_write_verdict_returns_path_when_ledger_append_fails(
879
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
880
- ) -> None:
881
- """A ledger IO failure never changes the mint path or verdict file write."""
882
- fake_home = tmp_path / "home"
883
- fake_home.mkdir()
884
- _isolate_home(monkeypatch, fake_home)
885
- monkeypatch.setattr(pathlib.Path, "home", classmethod(lambda cls: fake_home))
886
- logs_as_file = (
887
- fake_home
888
- / constants_module.CLAUDE_HOME_DIRECTORY_NAME
889
- / constants_module.LOGS_DIRECTORY_NAME
890
- )
891
- logs_as_file.parent.mkdir(parents=True, exist_ok=True)
892
- logs_as_file.write_text("not-a-directory", encoding="utf-8")
893
- work_dir = _make_repo_with_origin(tmp_path)
894
- verdict_file = write_verdict(
895
- str(work_dir),
896
- MATCHING_MANIFEST_SHA256,
897
- True,
898
- [],
899
- "agent-x",
900
- )
901
- assert verdict_file.is_file()
902
- verdict_record = json.loads(verdict_file.read_text(encoding="utf-8"))
903
- assert verdict_record[constants_module.VERDICT_KEY_ALL_PASS] is True
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))