claude-dev-env 2.4.0 → 2.7.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 (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,248 @@
1
+ """Behavioral tests for the baseline_import_isolation parts module.
2
+
3
+ The import-root probe runs a real interpreter subprocess and the leak plugin is
4
+ written, imported, and called the way pytest calls it, so each test measures the
5
+ mechanism the live gate depends on rather than a stand-in for it.
6
+ """
7
+
8
+ import importlib.util
9
+ import json
10
+ import os
11
+ import sys
12
+ import types
13
+ from pathlib import Path
14
+
15
+ import pytest
16
+ from pr_loop_shared_constants.code_rules_gate_constants import (
17
+ BASELINE_LEAK_PLUGIN_MODULE_NAME,
18
+ BASELINE_LEAK_REPORT_ENV_VAR,
19
+ BASELINE_PRIMARY_ROOT_ENV_VAR,
20
+ PYTEST_PLUGINS_ENV_VAR,
21
+ PYTHONPATH_ENV_VAR,
22
+ )
23
+
24
+ from code_rules_gate_parts import baseline_import_isolation
25
+
26
+ UNREACHABLE_PROBE_TIMEOUT_SECONDS = 0.001
27
+
28
+
29
+ def baseline_environment_for(
30
+ tmp_path: Path,
31
+ staged_environment: dict[str, str],
32
+ all_import_roots: list[Path],
33
+ ) -> dict[str, str]:
34
+ """Build a baseline environment against a /repo and /baseline pair under *tmp_path*."""
35
+ return baseline_import_isolation.baseline_pytest_environment(
36
+ staged_environment,
37
+ tmp_path / "repo",
38
+ tmp_path / "baseline",
39
+ all_import_roots,
40
+ tmp_path / "plugin",
41
+ tmp_path / "report.json",
42
+ )
43
+
44
+
45
+ def test_rebased_into_baseline_moves_a_repository_path_into_the_worktree(
46
+ tmp_path: Path,
47
+ ) -> None:
48
+ rebased_path = baseline_import_isolation.rebased_into_baseline(
49
+ tmp_path / "repo" / "packages", tmp_path / "repo", tmp_path / "baseline"
50
+ )
51
+
52
+ assert rebased_path == tmp_path / "baseline" / "packages"
53
+
54
+
55
+ def test_rebased_into_baseline_leaves_a_path_outside_the_repository_unclaimed(
56
+ tmp_path: Path,
57
+ ) -> None:
58
+ rebased_path = baseline_import_isolation.rebased_into_baseline(
59
+ tmp_path / "elsewhere" / "library", tmp_path / "repo", tmp_path / "baseline"
60
+ )
61
+
62
+ assert rebased_path is None
63
+
64
+
65
+ def test_baseline_pytest_environment_leads_with_the_rebased_import_roots(
66
+ tmp_path: Path,
67
+ ) -> None:
68
+ baseline_environment = baseline_environment_for(
69
+ tmp_path, {}, [tmp_path / "repo" / "source", tmp_path / "elsewhere"]
70
+ )
71
+
72
+ all_entries = baseline_environment[PYTHONPATH_ENV_VAR].split(os.pathsep)
73
+ assert all_entries[0] == str(tmp_path / "baseline" / "source")
74
+ assert str(tmp_path / "elsewhere") not in all_entries
75
+
76
+
77
+ def test_baseline_pytest_environment_rebases_a_repository_pythonpath_entry(
78
+ tmp_path: Path,
79
+ ) -> None:
80
+ staged_environment = {PYTHONPATH_ENV_VAR: str(tmp_path / "repo" / "packages")}
81
+
82
+ baseline_environment = baseline_environment_for(tmp_path, staged_environment, [])
83
+
84
+ all_entries = baseline_environment[PYTHONPATH_ENV_VAR].split(os.pathsep)
85
+ assert str(tmp_path / "baseline" / "packages") in all_entries
86
+ assert str(tmp_path / "repo" / "packages") not in all_entries
87
+
88
+
89
+ def test_baseline_pytest_environment_keeps_an_outside_pythonpath_entry(
90
+ tmp_path: Path,
91
+ ) -> None:
92
+ staged_environment = {PYTHONPATH_ENV_VAR: str(tmp_path / "vendor")}
93
+
94
+ baseline_environment = baseline_environment_for(tmp_path, staged_environment, [])
95
+
96
+ assert str(tmp_path / "vendor") in baseline_environment[PYTHONPATH_ENV_VAR].split(
97
+ os.pathsep
98
+ )
99
+
100
+
101
+ def test_baseline_pytest_environment_adds_the_leak_plugin_to_an_existing_list(
102
+ tmp_path: Path,
103
+ ) -> None:
104
+ staged_environment = {PYTEST_PLUGINS_ENV_VAR: "project_plugin"}
105
+
106
+ baseline_environment = baseline_environment_for(tmp_path, staged_environment, [])
107
+
108
+ assert baseline_environment[PYTEST_PLUGINS_ENV_VAR] == (
109
+ f"{BASELINE_LEAK_PLUGIN_MODULE_NAME},project_plugin"
110
+ )
111
+
112
+
113
+ def test_discover_import_roots_reports_a_pythonpath_directory(tmp_path: Path) -> None:
114
+ library_directory = tmp_path / "library"
115
+ library_directory.mkdir()
116
+ probe_environment = dict(os.environ)
117
+ probe_environment[PYTHONPATH_ENV_VAR] = str(library_directory)
118
+
119
+ all_import_roots = baseline_import_isolation.discover_import_roots(
120
+ sys.executable, probe_environment, tmp_path
121
+ )
122
+
123
+ assert library_directory.resolve() in all_import_roots
124
+
125
+
126
+ def test_discover_import_roots_gives_up_on_a_probe_that_runs_out_of_time(
127
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
128
+ ) -> None:
129
+ monkeypatch.setattr(
130
+ baseline_import_isolation,
131
+ "BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS",
132
+ UNREACHABLE_PROBE_TIMEOUT_SECONDS,
133
+ )
134
+
135
+ all_import_roots = baseline_import_isolation.discover_import_roots(
136
+ sys.executable, dict(os.environ), tmp_path
137
+ )
138
+
139
+ assert all_import_roots == []
140
+ assert "import-root probe" in capsys.readouterr().err
141
+
142
+
143
+ def test_discover_import_roots_gives_up_on_an_interpreter_that_cannot_start(
144
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
145
+ ) -> None:
146
+ all_import_roots = baseline_import_isolation.discover_import_roots(
147
+ str(tmp_path / "no_such_interpreter"), dict(os.environ), tmp_path
148
+ )
149
+
150
+ assert all_import_roots == []
151
+ assert "import-root probe" in capsys.readouterr().err
152
+
153
+
154
+ def test_discover_import_roots_reports_an_editable_install_package_root(
155
+ tmp_path: Path,
156
+ ) -> None:
157
+ project_root = tmp_path / "project"
158
+ (project_root / "widget").mkdir(parents=True)
159
+ finder_directory = tmp_path / "finder"
160
+ finder_directory.mkdir()
161
+ (finder_directory / "sitecustomize.py").write_text(
162
+ "import sys\n"
163
+ f"MAPPING = {{'widget': r'{project_root / 'widget'}'}}\n\n\n"
164
+ "class EditableFinder:\n"
165
+ " @classmethod\n"
166
+ " def find_spec(cls, fullname, path=None, target=None):\n"
167
+ " return None\n\n\n"
168
+ "sys.meta_path.append(EditableFinder)\n",
169
+ encoding="utf-8",
170
+ )
171
+ probe_environment = dict(os.environ)
172
+ probe_environment[PYTHONPATH_ENV_VAR] = str(finder_directory)
173
+
174
+ all_import_roots = baseline_import_isolation.discover_import_roots(
175
+ sys.executable, probe_environment, tmp_path
176
+ )
177
+
178
+ assert project_root.resolve() in all_import_roots
179
+
180
+
181
+ def test_modules_imported_from_primary_tree_is_unknown_without_a_report(
182
+ tmp_path: Path,
183
+ ) -> None:
184
+ assert (
185
+ baseline_import_isolation.modules_imported_from_primary_tree(
186
+ tmp_path / "missing.json"
187
+ )
188
+ is None
189
+ )
190
+
191
+
192
+ def test_modules_imported_from_primary_tree_reads_the_reported_paths(
193
+ tmp_path: Path,
194
+ ) -> None:
195
+ report_path = tmp_path / "report.json"
196
+ report_path.write_text(json.dumps([str(tmp_path / "repo" / "foo.py")]), encoding="utf-8")
197
+
198
+ assert baseline_import_isolation.modules_imported_from_primary_tree(report_path) == [
199
+ tmp_path / "repo" / "foo.py"
200
+ ]
201
+
202
+
203
+ def load_installed_plugin(plugin_directory: Path) -> types.ModuleType:
204
+ """Import the plugin module ``install_leak_plugin`` wrote into *plugin_directory*."""
205
+ plugin_path = plugin_directory / f"{BASELINE_LEAK_PLUGIN_MODULE_NAME}.py"
206
+ module_specification = importlib.util.spec_from_file_location(
207
+ BASELINE_LEAK_PLUGIN_MODULE_NAME, plugin_path
208
+ )
209
+ assert module_specification is not None and module_specification.loader is not None
210
+ plugin_module = importlib.util.module_from_spec(module_specification)
211
+ module_specification.loader.exec_module(plugin_module)
212
+ return plugin_module
213
+
214
+
215
+ def test_installed_plugin_reports_a_module_loaded_from_the_primary_tree(
216
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
217
+ ) -> None:
218
+ plugin_directory = tmp_path / "plugin"
219
+ baseline_import_isolation.install_leak_plugin(plugin_directory)
220
+ plugin_module = load_installed_plugin(plugin_directory)
221
+ primary_root = tmp_path / "repo"
222
+ leaked_module = types.ModuleType("leaked_widget")
223
+ leaked_module.__file__ = str(primary_root / "widget" / "__init__.py")
224
+ monkeypatch.setitem(sys.modules, "leaked_widget", leaked_module)
225
+ report_path = tmp_path / "report.json"
226
+ monkeypatch.setenv(BASELINE_PRIMARY_ROOT_ENV_VAR, str(primary_root))
227
+ monkeypatch.setenv(BASELINE_LEAK_REPORT_ENV_VAR, str(report_path))
228
+
229
+ plugin_module.pytest_sessionfinish(None, 0)
230
+
231
+ assert baseline_import_isolation.modules_imported_from_primary_tree(report_path) == [
232
+ primary_root / "widget" / "__init__.py"
233
+ ]
234
+
235
+
236
+ def test_installed_plugin_reports_nothing_when_no_module_came_from_the_primary_tree(
237
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
238
+ ) -> None:
239
+ plugin_directory = tmp_path / "plugin"
240
+ baseline_import_isolation.install_leak_plugin(plugin_directory)
241
+ plugin_module = load_installed_plugin(plugin_directory)
242
+ report_path = tmp_path / "report.json"
243
+ monkeypatch.setenv(BASELINE_PRIMARY_ROOT_ENV_VAR, str(tmp_path / "repo"))
244
+ monkeypatch.setenv(BASELINE_LEAK_REPORT_ENV_VAR, str(report_path))
245
+
246
+ plugin_module.pytest_sessionfinish(None, 0)
247
+
248
+ assert baseline_import_isolation.modules_imported_from_primary_tree(report_path) == []
@@ -0,0 +1,309 @@
1
+ """Behavioral tests for the staged_test_regression parts module.
2
+
3
+ Every test drives a real git repository and a real pytest subprocess — no
4
+ mocked git state, no mocked pytest run — the same shape the live commit gate
5
+ exercises.
6
+ """
7
+
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ from code_rules_gate_parts import baseline_import_isolation, staged_test_regression
13
+ from code_rules_gate_parts.tests._repo_test_helpers import (
14
+ init_repository,
15
+ repository_root_without_pytest_config,
16
+ repository_with_root_pytest_config,
17
+ run_git,
18
+ write_and_stage,
19
+ write_commit_and_stage_change,
20
+ )
21
+
22
+ UNREACHABLE_PROBE_TIMEOUT_SECONDS = 0.001
23
+
24
+ CONSUMER_TEST_TEXT = (
25
+ "import foo\n\n\ndef test_consumer_reads_head_value() -> None:\n"
26
+ " assert foo.VALUE == 'clean', foo.__file__\n"
27
+ )
28
+
29
+ CONSUMER_TEST_TEXT_WITH_STAGED_EDIT = CONSUMER_TEST_TEXT + (
30
+ "\n\ndef test_consumer_extra() -> None:\n assert True\n"
31
+ )
32
+
33
+ IMPORT_HOOK_SITECUSTOMIZE_TEMPLATE = (
34
+ "import sys\n"
35
+ "from importlib.machinery import PathFinder\n\n"
36
+ "PINNED_ROOT = r'{pinned_root}'\n\n\n"
37
+ "class PinnedFinder(PathFinder):\n"
38
+ " @classmethod\n"
39
+ " def find_spec(cls, fullname, path=None, target=None):\n"
40
+ " if fullname.split('.')[0] != 'foo':\n"
41
+ " return None\n"
42
+ " return super().find_spec(fullname, [PINNED_ROOT], target)\n\n\n"
43
+ "sys.meta_path.insert(0, PinnedFinder)\n"
44
+ )
45
+
46
+
47
+ def stage_a_change_that_breaks_a_consumer(repository_root: Path) -> None:
48
+ """Commit a passing consumer of ``foo``, then stage a ``foo`` edit that breaks it.
49
+
50
+ The consumer imports ``foo`` by name, so which copy of ``foo`` the baseline
51
+ run loads decides whether the gate sees a regression at all.
52
+ """
53
+ write_and_stage(repository_root, "packages/foo/__init__.py", "VALUE = 'clean'\n")
54
+ write_and_stage(repository_root, "pkg_a/test_alpha.py", CONSUMER_TEST_TEXT)
55
+ run_git(repository_root, "commit", "--no-verify", "-m", "seed consumer")
56
+ write_and_stage(repository_root, "packages/foo/__init__.py", "VALUE = 'dirty'\n")
57
+ write_and_stage(
58
+ repository_root, "pkg_a/test_alpha.py", CONSUMER_TEST_TEXT_WITH_STAGED_EDIT
59
+ )
60
+
61
+
62
+ def test_pythonpath_route_into_the_working_tree_does_not_hide_a_regression(
63
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
64
+ ) -> None:
65
+ repository_root = repository_with_root_pytest_config(tmp_path)
66
+ stage_a_change_that_breaks_a_consumer(repository_root)
67
+ monkeypatch.setenv("PYTHONPATH", str(repository_root / "packages"))
68
+
69
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
70
+
71
+
72
+ def test_a_probe_that_runs_out_of_time_still_reaches_a_gate_decision(
73
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
74
+ ) -> None:
75
+ repository_root = repository_with_root_pytest_config(tmp_path)
76
+ write_commit_and_stage_change(
77
+ repository_root,
78
+ "pkg_a/test_alpha.py",
79
+ (
80
+ "def test_already_fails() -> None:\n assert False\n"
81
+ "def test_passes() -> None:\n assert True\n"
82
+ ),
83
+ (
84
+ "def test_already_fails() -> None:\n assert False\n"
85
+ "def test_passes() -> None:\n assert 1 == 1\n"
86
+ ),
87
+ )
88
+ monkeypatch.setattr(
89
+ baseline_import_isolation,
90
+ "BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS",
91
+ UNREACHABLE_PROBE_TIMEOUT_SECONDS,
92
+ )
93
+
94
+ exit_code = staged_test_regression.run_staged_test_files(repository_root)
95
+
96
+ assert exit_code == 0
97
+ assert "import-root probe" in capsys.readouterr().err
98
+
99
+
100
+ def test_import_hook_route_into_the_working_tree_is_reported_and_blocks(
101
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
102
+ ) -> None:
103
+ repository_root = repository_with_root_pytest_config(tmp_path)
104
+ stage_a_change_that_breaks_a_consumer(repository_root)
105
+ import_hook_directory = tmp_path / "import_hook"
106
+ import_hook_directory.mkdir()
107
+ (import_hook_directory / "sitecustomize.py").write_text(
108
+ IMPORT_HOOK_SITECUSTOMIZE_TEMPLATE.format(pinned_root=repository_root / "packages"),
109
+ encoding="utf-8",
110
+ )
111
+ monkeypatch.setenv("PYTHONPATH", str(import_hook_directory))
112
+
113
+ exit_code = staged_test_regression.run_staged_test_files(repository_root)
114
+
115
+ assert exit_code != 0
116
+ assert "imported 1 module(s) from your working tree" in capsys.readouterr().err
117
+
118
+
119
+ def test_pre_existing_failure_does_not_block_an_unrelated_staged_change(
120
+ tmp_path: Path,
121
+ ) -> None:
122
+ repository_root = repository_with_root_pytest_config(tmp_path)
123
+ write_commit_and_stage_change(
124
+ repository_root,
125
+ "pkg_a/test_alpha.py",
126
+ (
127
+ "def test_already_fails() -> None:\n assert False\n"
128
+ "def test_passes() -> None:\n assert True\n"
129
+ ),
130
+ (
131
+ "def test_already_fails() -> None:\n assert False\n"
132
+ "def test_passes() -> None:\n assert 1 == 1\n"
133
+ ),
134
+ )
135
+
136
+ assert staged_test_regression.run_staged_test_files(repository_root) == 0
137
+
138
+
139
+ def test_staged_change_that_newly_breaks_a_passing_test_blocks(tmp_path: Path) -> None:
140
+ repository_root = repository_with_root_pytest_config(tmp_path)
141
+ write_commit_and_stage_change(
142
+ repository_root,
143
+ "pkg_a/test_alpha.py",
144
+ "def test_alpha_passes() -> None:\n assert True\n",
145
+ "def test_alpha_passes() -> None:\n assert False\n",
146
+ )
147
+
148
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
149
+
150
+
151
+ def test_brand_new_failing_test_with_no_baseline_blocks(tmp_path: Path) -> None:
152
+ repository_root = repository_with_root_pytest_config(tmp_path)
153
+ write_and_stage(
154
+ repository_root,
155
+ "pkg_a/test_alpha.py",
156
+ "def test_alpha_fails() -> None:\n assert False\n",
157
+ )
158
+
159
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
160
+
161
+
162
+ def test_mixed_group_blocks_only_on_the_newly_broken_test(tmp_path: Path) -> None:
163
+ repository_root = repository_with_root_pytest_config(tmp_path)
164
+ write_commit_and_stage_change(
165
+ repository_root,
166
+ "pkg_a/test_alpha.py",
167
+ (
168
+ "def test_already_red() -> None:\n assert False\n"
169
+ "def test_stays_green() -> None:\n assert True\n"
170
+ ),
171
+ (
172
+ "def test_already_red() -> None:\n assert False\n"
173
+ "def test_stays_green() -> None:\n assert False\n"
174
+ ),
175
+ )
176
+
177
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
178
+
179
+
180
+ def test_regression_gate_leaves_the_staged_index_and_worktree_list_alone(
181
+ tmp_path: Path,
182
+ ) -> None:
183
+ repository_root = repository_with_root_pytest_config(tmp_path)
184
+ write_commit_and_stage_change(
185
+ repository_root,
186
+ "pkg_a/test_alpha.py",
187
+ (
188
+ "def test_already_fails() -> None:\n assert False\n"
189
+ "def test_passes() -> None:\n assert True\n"
190
+ ),
191
+ (
192
+ "def test_already_fails() -> None:\n assert False\n"
193
+ "def test_passes() -> None:\n assert 1 == 1\n"
194
+ ),
195
+ )
196
+
197
+ staged_test_regression.run_staged_test_files(repository_root)
198
+
199
+ staged_content = run_git(
200
+ repository_root, "show", ":pkg_a/test_alpha.py"
201
+ ).stdout.decode("utf-8")
202
+ assert "assert 1 == 1" in staged_content
203
+ status = run_git(repository_root, "status", "--porcelain").stdout.decode("utf-8")
204
+ assert "M pkg_a/test_alpha.py" in status
205
+ worktree_list = run_git(
206
+ repository_root, "worktree", "list", "--porcelain"
207
+ ).stdout.decode("utf-8")
208
+ assert worktree_list.count("worktree ") == 1
209
+ assert "code_rules_gate_baseline_" not in worktree_list
210
+
211
+
212
+ def test_run_staged_test_files_returns_zero_when_nothing_staged(tmp_path: Path) -> None:
213
+ repository_root = tmp_path / "repo"
214
+ repository_root.mkdir()
215
+ init_repository(repository_root)
216
+
217
+ assert staged_test_regression.run_staged_test_files(repository_root) == 0
218
+
219
+
220
+ def test_run_staged_test_files_returns_zero_when_only_multiple_confests_staged(
221
+ tmp_path: Path,
222
+ ) -> None:
223
+ repository_root = repository_with_root_pytest_config(tmp_path)
224
+ write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
225
+ write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
226
+ write_and_stage(repository_root, "pkg_c/tests/conftest.py", "import pytest\n")
227
+
228
+ assert staged_test_regression.run_staged_test_files(repository_root) == 0
229
+
230
+
231
+ def test_run_staged_test_files_passes_when_confests_stage_with_passing_test(
232
+ tmp_path: Path,
233
+ ) -> None:
234
+ repository_root = repository_with_root_pytest_config(tmp_path)
235
+ write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
236
+ write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
237
+ write_and_stage(
238
+ repository_root,
239
+ "pkg_a/test_alpha.py",
240
+ "def test_alpha_passes() -> None:\n assert True\n",
241
+ )
242
+
243
+ assert staged_test_regression.run_staged_test_files(repository_root) == 0
244
+
245
+
246
+ def test_run_staged_test_files_fails_when_real_test_fails_alongside_confests(
247
+ tmp_path: Path,
248
+ ) -> None:
249
+ repository_root = repository_with_root_pytest_config(tmp_path)
250
+ write_and_stage(repository_root, "pkg_a/conftest.py", "import pytest\n")
251
+ write_and_stage(repository_root, "pkg_b/conftest.py", "import pytest\n")
252
+ write_and_stage(
253
+ repository_root,
254
+ "pkg_a/test_alpha.py",
255
+ "def test_alpha_fails() -> None:\n assert False\n",
256
+ )
257
+
258
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
259
+
260
+
261
+ def test_no_head_baseline_blocks_on_any_staged_failure(tmp_path: Path) -> None:
262
+ repository_root = tmp_path / "repo"
263
+ repository_root.mkdir()
264
+ run_git(repository_root, "init", "--initial-branch=main")
265
+ run_git(repository_root, "config", "user.email", "test@example.com")
266
+ run_git(repository_root, "config", "user.name", "Test")
267
+ run_git(repository_root, "config", "commit.gpgsign", "false")
268
+ write_and_stage(repository_root, "pytest.ini", "[pytest]\n")
269
+ write_and_stage(
270
+ repository_root,
271
+ "test_alpha.py",
272
+ "def test_alpha_fails() -> None:\n assert False\n",
273
+ )
274
+
275
+ assert staged_test_regression.run_staged_test_files(repository_root) != 0
276
+
277
+
278
+ def _stage_package_owning_a_bare_config(
279
+ repository_root: Path, package_name: str, marker_value: str
280
+ ) -> None:
281
+ """Stage a config-less top-level package whose tests import a bare ``config``.
282
+
283
+ Two such packages staged together reproduce the measured collision: one
284
+ pytest session binds ``config`` to whichever package imports first, so the
285
+ other package's test reads the wrong marker.
286
+ """
287
+ write_and_stage(
288
+ repository_root,
289
+ f"{package_name}/config/__init__.py",
290
+ f'PACKAGE_MARKER = "{marker_value}"\n',
291
+ )
292
+ write_and_stage(
293
+ repository_root,
294
+ f"{package_name}/test_{package_name}.py",
295
+ "from config import PACKAGE_MARKER\n\n\n"
296
+ f"def test_{package_name}_reads_its_own_config() -> None:\n"
297
+ f' assert PACKAGE_MARKER == "{marker_value}"\n',
298
+ )
299
+
300
+
301
+ def test_run_staged_test_files_passes_when_config_less_packages_share_a_module_name(
302
+ tmp_path: Path,
303
+ ) -> None:
304
+ repository_root = repository_root_without_pytest_config(tmp_path)
305
+ init_repository(repository_root)
306
+ _stage_package_owning_a_bare_config(repository_root, "alpha_package", "alpha")
307
+ _stage_package_owning_a_bare_config(repository_root, "beta_package", "beta")
308
+
309
+ assert staged_test_regression.run_staged_test_files(repository_root) == 0