claude-dev-env 4.3.0 → 7.1.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 (480) hide show
  1. package/.agents/agents/pr-description-writer.md +2 -2
  2. package/.agents/agents/test_agent_frontmatter.py +2 -2
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -3
  4. package/.agents/skills/e-code-review/SKILL.md +12 -12
  5. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
  6. package/.agents/skills/e-code-review/reference/fix.md +15 -17
  7. package/.agents/skills/e-code-review/reference/loop.md +35 -39
  8. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
  9. package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
  10. package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
  11. package/.agents/skills/issue-tracker/SKILL.md +5 -4
  12. package/.agents/skills/pull-request/SKILL.md +11 -13
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
  15. package/.agents/skills/pull-request/scripts/pull_request.py +35 -44
  16. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
  17. package/.agents/skills/pull-request/scripts/test_pull_request.py +35 -3
  18. package/.agents/skills/session-continuity/README.md +189 -0
  19. package/.agents/skills/session-continuity/SKILL.md +114 -0
  20. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  21. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  22. package/.agents/skills/skill-builder/SKILL.md +3 -196
  23. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  24. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  25. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  26. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  27. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  28. package/bin/cde.mjs +36 -8
  29. package/bin/cde.test.mjs +48 -0
  30. package/bin/ever-shipped-skills.mjs +2 -20
  31. package/bin/ever-shipped-skills.test.mjs +8 -0
  32. package/bin/expand_home_directory_tokens.mjs +4 -5
  33. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  34. package/bin/git_hooks_installer.mjs +22 -3
  35. package/bin/git_hooks_installer.test.mjs +159 -5
  36. package/bin/install-session-continuity.mjs +78 -0
  37. package/bin/install-session-continuity.test.mjs +42 -0
  38. package/bin/install.agents-home.test.mjs +15 -13
  39. package/bin/install.codex-rules.test.mjs +5 -2
  40. package/bin/install.cursor-rules.test.mjs +5 -2
  41. package/bin/install.mjs +506 -63
  42. package/bin/install.policy-lint.test.mjs +130 -0
  43. package/bin/install.profiles.test.mjs +10 -0
  44. package/bin/install.prune.test.mjs +286 -37
  45. package/bin/install.pstack-plugin.test.mjs +114 -0
  46. package/bin/install.test.mjs +537 -120
  47. package/bin/install.transaction.test.mjs +3 -3
  48. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  49. package/docs/CODE_RULES.md +1 -1
  50. package/hooks/advisory/pr_done_reminder.py +342 -0
  51. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  52. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  53. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  54. package/hooks/blocking/code_rules_paired_test.py +22 -15
  55. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  56. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  57. package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
  58. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  59. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  60. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  61. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  62. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  63. package/hooks/blocking/test_pre_tool_use_dispatcher.py +43 -342
  64. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  65. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  66. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  67. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  68. package/hooks/git-hooks/post_commit.py +33 -0
  69. package/hooks/git-hooks/pre_commit.py +18 -49
  70. package/hooks/git-hooks/pre_push.py +45 -541
  71. package/hooks/git-hooks/test_installed_policy_lint.py +208 -0
  72. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  73. package/hooks/git-hooks/test_native_policy_commit.py +120 -0
  74. package/hooks/git-hooks/test_post_commit.py +43 -0
  75. package/hooks/git-hooks/test_pre_commit.py +38 -148
  76. package/hooks/git-hooks/test_pre_push.py +48 -1631
  77. package/hooks/git-hooks/test_verification_notice.py +60 -0
  78. package/hooks/git-hooks/test_verification_start.py +173 -0
  79. package/hooks/git-hooks/verification_notice.py +192 -0
  80. package/hooks/git-hooks/verification_notice_context.py +253 -0
  81. package/hooks/git-hooks/verification_notice_state.py +237 -0
  82. package/hooks/git-hooks/verification_start.py +100 -0
  83. package/hooks/hooks.json +1 -120
  84. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  85. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
  86. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  87. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  88. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  89. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -93
  90. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  91. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
  92. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  93. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  94. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +46 -201
  95. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  96. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  97. package/hooks/validators/run_all_validators.py +5 -1
  98. package/hooks/validators/security_checks.py +57 -2
  99. package/hooks/validators/test_hook_timing_harness.py +2 -5
  100. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  101. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  102. package/hooks/validators/test_security_checks.py +57 -0
  103. package/package.json +2 -2
  104. package/rules/confirm-implementation-forks.md +2 -0
  105. package/rules/durable-post-artifacts.md +2 -1
  106. package/rules/git-workflow.md +18 -6
  107. package/rules/pstack-host-mapping.md +48 -0
  108. package/rules/vault-context.md +3 -5
  109. package/rules/verify-runtime-state.md +7 -0
  110. package/scripts/audit-hooks-cli.mjs +5 -0
  111. package/scripts/audit-hooks.mjs +72 -16
  112. package/scripts/automatic_advisory/__init__.py +1 -0
  113. package/scripts/automatic_advisory/checkout.py +51 -0
  114. package/scripts/automatic_advisory/cli.py +334 -0
  115. package/scripts/automatic_advisory/config/__init__.py +1 -0
  116. package/scripts/automatic_advisory/config/constants.py +93 -0
  117. package/scripts/automatic_advisory/config/timing.py +4 -0
  118. package/scripts/automatic_advisory/configuration.py +197 -0
  119. package/scripts/automatic_advisory/execution.py +350 -0
  120. package/scripts/automatic_advisory/git.py +69 -0
  121. package/scripts/automatic_advisory/model.py +107 -0
  122. package/scripts/automatic_advisory/process_host.py +32 -0
  123. package/scripts/automatic_advisory/publisher.py +84 -0
  124. package/scripts/automatic_advisory/runner.py +399 -0
  125. package/scripts/automatic_advisory/state.py +116 -0
  126. package/scripts/automatic_advisory/window_flags.py +41 -0
  127. package/scripts/automatic_advisory/windows_job.py +122 -0
  128. package/scripts/check.ps1 +0 -1
  129. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  130. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  131. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  132. package/scripts/codex_compat_materializer.py +99 -13
  133. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  134. package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +35 -43
  135. package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +10 -10
  136. package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +14 -1
  137. package/scripts/local_report_cli.py +184 -0
  138. package/scripts/local_report_core.py +247 -0
  139. package/scripts/local_report_publisher.py +12 -0
  140. package/scripts/local_report_validation.py +357 -0
  141. package/scripts/local_verification/__init__.py +23 -0
  142. package/scripts/local_verification/check_runner.py +364 -0
  143. package/scripts/local_verification/check_support.py +99 -0
  144. package/scripts/local_verification/cli.py +158 -0
  145. package/scripts/local_verification/command_runner.py +84 -0
  146. package/scripts/local_verification/config/__init__.py +1 -0
  147. package/scripts/local_verification/config/constants.py +89 -0
  148. package/scripts/local_verification/config/timing.py +1 -0
  149. package/scripts/local_verification/git_state.py +146 -0
  150. package/scripts/local_verification/manifest.py +224 -0
  151. package/scripts/local_verification/model.py +174 -0
  152. package/scripts/local_verification/runner.py +234 -0
  153. package/scripts/policy_lint/adapter_detectors.py +13 -6
  154. package/scripts/policy_lint/adapter_pairing.py +58 -3
  155. package/scripts/policy_lint/adapters.py +7 -4
  156. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  157. package/scripts/policy_lint/config/constants.py +5 -0
  158. package/scripts/policy_lint/engine.py +48 -4
  159. package/scripts/policy_lint/model.py +1 -0
  160. package/scripts/policy_lint/registry.py +1 -1
  161. package/scripts/policy_lint/selection.py +2 -1
  162. package/scripts/policy_lint/selection_git.py +2 -0
  163. package/scripts/pr_verification/__init__.py +1 -0
  164. package/scripts/pr_verification/branch_refs.py +114 -0
  165. package/scripts/pr_verification/config/__init__.py +1 -0
  166. package/scripts/pr_verification/config/constants.py +84 -0
  167. package/scripts/pr_verification/config/timing.py +5 -0
  168. package/scripts/pr_verification/github.py +137 -0
  169. package/scripts/pr_verification/github_api.py +376 -0
  170. package/scripts/pr_verification/github_labels.py +88 -0
  171. package/scripts/pr_verification/github_parsing.py +182 -0
  172. package/scripts/pr_verification/github_transport.py +91 -0
  173. package/scripts/pr_verification/lock.py +122 -0
  174. package/scripts/pr_verification/model.py +43 -0
  175. package/scripts/pr_verification/requirements.txt +1 -0
  176. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  177. package/scripts/shared_tree_paths.py +32 -3
  178. package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +97 -0
  179. package/scripts/test_shared_tree_paths.py +33 -0
  180. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  181. package/scripts/tests/test-audit-hooks.mjs +94 -2
  182. package/scripts/tests/test_automatic_advisory.py +376 -0
  183. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  184. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  185. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  186. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  187. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  188. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  189. package/scripts/tests/test_closed_pr_label.py +157 -0
  190. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  191. package/scripts/tests/test_github_labels.py +101 -0
  192. package/scripts/tests/test_github_transport.py +125 -0
  193. package/scripts/tests/test_local_report_publisher.py +396 -0
  194. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  195. package/scripts/tests/test_local_verification.py +348 -0
  196. package/scripts/tests/test_local_verification_cli.py +98 -0
  197. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  198. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  199. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  200. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  201. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  202. package/scripts/tests/test_policy_lint_selection.py +25 -2
  203. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  204. package/scripts/tests/test_pr_verification_github.py +392 -0
  205. package/scripts/tests/test_pr_verification_lock.py +168 -0
  206. package/scripts/tests/test_rules.py +3 -7
  207. package/scripts/tests/test_verification_notice.py +347 -0
  208. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  209. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  210. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  211. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  212. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  213. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  214. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  215. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  216. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  217. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  218. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  219. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  220. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  221. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  222. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  223. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  224. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  225. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  226. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  227. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  228. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  229. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  230. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  231. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  232. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  233. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  234. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  235. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  236. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  237. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  238. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  239. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  240. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  241. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  242. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  243. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  244. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  245. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  246. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  247. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  248. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  249. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  250. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  251. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  252. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  253. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  254. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  255. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  256. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  257. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  258. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  259. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  260. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  261. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  262. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  263. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  264. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  265. package/.agents/skills/autoconverge/SKILL.md +0 -431
  266. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  267. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  268. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  269. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  270. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  271. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  272. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  273. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  274. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  275. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  276. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  277. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  278. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  279. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  280. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  281. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  282. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  283. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  284. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  285. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  286. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  287. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  288. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  289. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  290. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  291. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  292. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  293. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  294. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  295. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  296. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  297. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  298. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  299. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  300. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  301. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  302. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  303. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  304. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  305. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  306. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  307. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  308. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  309. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  310. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  311. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  312. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  313. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  314. package/.agents/skills/hitl/SKILL.md +0 -45
  315. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  316. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  317. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  318. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  319. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  320. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  321. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  322. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  323. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  324. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  325. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  326. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  327. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  328. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  329. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  330. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  331. package/.agents/skills/prototype/SKILL.md +0 -85
  332. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  333. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  334. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  335. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  336. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  337. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  338. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  339. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  340. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  341. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  342. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  343. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  344. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  345. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  346. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  347. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  348. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  349. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  350. package/.agents/skills/rebase/AGENTS.md +0 -1
  351. package/.agents/skills/rebase/SKILL.md +0 -178
  352. package/.agents/skills/review-router/SKILL.md +0 -51
  353. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  354. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  355. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  356. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  357. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  358. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  359. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  360. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  361. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  362. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  363. package/.agents/skills/review-tier/SKILL.md +0 -56
  364. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  365. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  366. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  367. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  368. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  369. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  370. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  371. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  372. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  373. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  374. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  375. package/.agents/skills/session-log/AGENTS.md +0 -1
  376. package/.agents/skills/session-log/SKILL.md +0 -170
  377. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  378. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  379. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  380. package/.agents/skills/session-tidy/SKILL.md +0 -191
  381. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  382. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  383. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  384. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  385. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  386. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  387. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  388. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  389. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  390. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  391. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  392. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  393. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  394. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  395. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  396. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  397. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  398. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  399. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  400. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  401. package/.agents/skills/update/AGENTS.md +0 -1
  402. package/.agents/skills/update/SKILL.md +0 -190
  403. package/_shared/pr-loop/AGENTS.md +0 -1
  404. package/_shared/pr-loop/audit-contract.md +0 -183
  405. package/_shared/pr-loop/audit-reply-template.md +0 -151
  406. package/_shared/pr-loop/code-rules-gate.md +0 -65
  407. package/_shared/pr-loop/fix-protocol.md +0 -60
  408. package/_shared/pr-loop/gh-payloads.md +0 -75
  409. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  410. package/_shared/pr-loop/precatch-rubric.md +0 -66
  411. package/_shared/pr-loop/preflight-proposal.md +0 -56
  412. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  413. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  414. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  415. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  416. package/_shared/pr-loop/state-schema.md +0 -45
  417. package/_shared/pr-loop/worker-spawn.md +0 -189
  418. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  419. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  420. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  421. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  422. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  423. package/hooks/blocking/fable_spawn_gate.py +0 -187
  424. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  425. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  426. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  427. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  428. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  429. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  430. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  431. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  432. package/hooks/blocking/sensitive_file_protector.py +0 -167
  433. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  434. package/hooks/blocking/session_handoff_blocker.py +0 -182
  435. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  436. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  437. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  438. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  439. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  440. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  441. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  442. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  443. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  444. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  445. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  446. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  447. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  448. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  449. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  450. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  451. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  452. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  453. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  454. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  455. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  456. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  457. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  458. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  459. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  460. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  461. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  462. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  463. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  464. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  465. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  466. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  467. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  468. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  469. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  470. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  471. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  472. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  473. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  474. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  475. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  476. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  477. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  478. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
  479. /package/.agents/skills/{_shared/pr-loop → session-continuity}/AGENTS.md +0 -0
  480. /package/{_shared/pr-loop → .agents/skills/session-continuity}/CLAUDE.md +0 -0
@@ -1,1655 +1,72 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import io
4
- import subprocess
5
4
  import sys
6
- from collections.abc import Callable
7
5
  from pathlib import Path
8
6
 
9
- import pytest
10
-
11
- import git_hooks_constants
12
- import pre_push
13
-
14
-
15
- SCRIPT_DIRECTORY = Path(__file__).resolve().parent
16
-
17
- REAL_RUN_GIT_TEXT_COMMAND = pre_push.run_git_text_command
18
-
19
-
20
- ALL_ZEROS_OBJECT_NAME: str = "0" * 40
21
- NON_ZERO_LOCAL_SHA: str = "a" * 40
22
- NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
23
- NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
24
-
25
-
26
- GIT_RESOLVED_EXIT_CODE: int = 0
27
- RESOLVED_COMMIT_OBJECT_NAME: str = "c" * 40
28
- HOOK_INVOCATION_NAME: str = "pre-push"
29
- PUSHED_REMOTE_NAME: str = "upstream"
30
- PUSHED_REMOTE_URL: str = "https://example.invalid/owner/repository.git"
31
-
32
-
33
- LOCALE_INVALID_REFERENCE_LISTING_BYTES: bytes = b"origin/caf\xe9\norigin/main\n"
34
- EMPTY_COMMAND_STREAM_BYTES: bytes = b""
35
- STRICT_DECODER_NAME: str = "utf-8"
36
- UNDECODABLE_BYTE_START_INDEX: int = 10
37
- UNDECODABLE_BYTE_END_INDEX: int = 11
38
- UNDECODABLE_BYTE_REASON: str = "invalid continuation byte"
39
- TEXT_DECODING_KEYWORD: str = "text"
40
- GIT_MISSING_REFERENCE_EXIT_CODE: int = 1
41
- GIT_LAUNCH_FAILURE_DETAIL: str = "git executable not found"
42
- RESOLVED_REMOTE_MAIN_REFERENCE: str = "origin/main"
43
- UNRESOLVABLE_BASE_REPORT_MARKER: str = "no usable gate base"
44
- GIT_UNAVAILABLE_REPORT_MARKER: str = "could not run git"
45
- NEW_BRANCH_PUSH_STDIN: str = (
46
- f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
47
- )
48
-
49
-
50
- def _refuse_locale_decoding() -> UnicodeDecodeError:
51
- """Build the error a text-mode subprocess raises on undecodable output."""
52
- return UnicodeDecodeError(
53
- STRICT_DECODER_NAME,
54
- LOCALE_INVALID_REFERENCE_LISTING_BYTES,
55
- UNDECODABLE_BYTE_START_INDEX,
56
- UNDECODABLE_BYTE_END_INDEX,
57
- UNDECODABLE_BYTE_REASON,
58
- )
59
-
60
-
61
- def _completed_git_process(
62
- all_command_arguments: list[str], exit_code: int, standard_output_bytes: bytes
63
- ) -> subprocess.CompletedProcess[bytes]:
64
- """Build a completed git process carrying raw bytes on standard output."""
65
- return subprocess.CompletedProcess(
66
- all_command_arguments,
67
- exit_code,
68
- standard_output_bytes,
69
- EMPTY_COMMAND_STREAM_BYTES,
70
- )
71
-
72
-
73
- def _answer_git_with_locale_invalid_bytes(
74
- all_command_arguments: list[str],
75
- **all_keyword_arguments: object,
76
- ) -> subprocess.CompletedProcess[bytes]:
77
- """Stand in for a git whose output the process locale cannot decode.
78
-
79
- ::
80
-
81
- text=True -> UnicodeDecodeError bytes mode -> the raw listing
82
-
83
- Args:
84
- all_command_arguments: The full git command the seam assembled.
85
- all_keyword_arguments: The keyword arguments the seam passed.
86
-
87
- Returns:
88
- A completed process carrying the raw listing bytes.
89
- """
90
- if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
91
- raise _refuse_locale_decoding()
92
- if git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND in all_command_arguments:
93
- return _completed_git_process(
94
- all_command_arguments,
95
- GIT_RESOLVED_EXIT_CODE,
96
- LOCALE_INVALID_REFERENCE_LISTING_BYTES,
97
- )
98
- return _completed_git_process(
99
- all_command_arguments,
100
- GIT_MISSING_REFERENCE_EXIT_CODE,
101
- EMPTY_COMMAND_STREAM_BYTES,
102
- )
103
-
104
-
105
- def _write_gate_that_allows_every_push(
106
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
107
- ) -> None:
108
- """Point both gates at stubs that allow, with a new-branch push on stdin."""
109
- passing_gate_path = tmp_path / "code_rules_gate.py"
110
- passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
111
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
112
- monkeypatch.setattr(sys, "stdin", io.StringIO(NEW_BRANCH_PUSH_STDIN))
113
-
114
-
115
- def test_reference_listing_with_locale_invalid_bytes_resolves_a_base(
116
- monkeypatch: pytest.MonkeyPatch,
117
- ) -> None:
118
- monkeypatch.setattr(subprocess, "run", _answer_git_with_locale_invalid_bytes)
119
-
120
- resolved_reference = pre_push.resolve_usable_base_reference(
121
- git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
122
- git_hooks_constants.DEFAULT_REMOTE_NAME,
123
- REAL_RUN_GIT_TEXT_COMMAND,
124
- )
125
-
126
- assert resolved_reference == RESOLVED_REMOTE_MAIN_REFERENCE
127
-
128
-
129
- def test_run_git_reference_query_returns_replaced_text_on_locale_invalid_bytes(
130
- monkeypatch: pytest.MonkeyPatch,
131
- ) -> None:
132
- """text=True would raise; bytes mode returns the replacing-policy decode."""
133
- locale_invalid_reference_bytes = b"refs/remotes/origin/caf\xe9"
134
- expected_replaced_reference = locale_invalid_reference_bytes.decode(
135
- git_hooks_constants.GIT_OUTPUT_ENCODING_NAME,
136
- errors=git_hooks_constants.GIT_OUTPUT_DECODE_ERRORS_POLICY,
137
- )
138
-
139
- def answer_with_locale_invalid_reference(
140
- all_command_arguments: list[str], **all_keyword_arguments: object
141
- ) -> subprocess.CompletedProcess[bytes]:
142
- if all_keyword_arguments.get(TEXT_DECODING_KEYWORD):
143
- raise _refuse_locale_decoding()
144
- return _completed_git_process(
145
- all_command_arguments,
146
- GIT_RESOLVED_EXIT_CODE,
147
- locale_invalid_reference_bytes,
148
- )
149
-
150
- monkeypatch.setattr(subprocess, "run", answer_with_locale_invalid_reference)
151
-
152
- resolved_output = pre_push.run_git_reference_query(
153
- ("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/HEAD")
154
- )
155
-
156
- assert resolved_output == expected_replaced_reference
157
-
158
-
159
- def test_unresolvable_merge_base_message_describes_pending_validation() -> None:
160
- skip_message = git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE
161
- assert "CODE_RULES validation is pending" in skip_message
162
- assert "Restore shared history" in skip_message
163
-
164
-
165
- def test_run_git_text_command_raises_when_git_cannot_launch(
166
- monkeypatch: pytest.MonkeyPatch,
167
- ) -> None:
168
- def raising_subprocess_run(
169
- all_command_arguments: list[str], **all_keyword_arguments: object
170
- ) -> subprocess.CompletedProcess[bytes]:
171
- raise OSError(GIT_LAUNCH_FAILURE_DETAIL)
172
-
173
- monkeypatch.setattr(subprocess, "run", raising_subprocess_run)
174
-
175
- with pytest.raises(pre_push.GitCommandUnavailable):
176
- REAL_RUN_GIT_TEXT_COMMAND([git_hooks_constants.GIT_FOR_EACH_REF_SUBCOMMAND])
177
-
178
-
179
- def test_main_reports_infrastructure_failure_when_git_cannot_launch(
180
- tmp_path: Path,
181
- monkeypatch: pytest.MonkeyPatch,
182
- capsys: pytest.CaptureFixture[str],
183
- ) -> None:
184
- _write_gate_that_allows_every_push(tmp_path, monkeypatch)
185
-
186
- def unavailable_run_git_text_command(
187
- all_command_arguments: list[str],
188
- ) -> tuple[int, str]:
189
- raise pre_push.GitCommandUnavailable(GIT_LAUNCH_FAILURE_DETAIL)
190
-
191
- monkeypatch.setattr(
192
- pre_push, "run_git_text_command", unavailable_run_git_text_command
193
- )
194
-
195
- exit_code = pre_push.main()
196
-
197
- captured_streams = capsys.readouterr()
198
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
199
- assert GIT_UNAVAILABLE_REPORT_MARKER in captured_streams.err
200
- assert GIT_LAUNCH_FAILURE_DETAIL in captured_streams.err
201
- assert UNRESOLVABLE_BASE_REPORT_MARKER not in captured_streams.err
202
-
203
-
204
- def test_main_reports_a_missing_default_branch_as_absence(
205
- tmp_path: Path,
206
- monkeypatch: pytest.MonkeyPatch,
207
- capsys: pytest.CaptureFixture[str],
208
- ) -> None:
209
- _write_gate_that_allows_every_push(tmp_path, monkeypatch)
210
-
211
- def empty_run_git_text_command(
212
- all_command_arguments: list[str],
213
- ) -> tuple[int, str]:
214
- return GIT_MISSING_REFERENCE_EXIT_CODE, ""
215
-
216
- monkeypatch.setattr(pre_push, "run_git_text_command", empty_run_git_text_command)
217
-
218
- exit_code = pre_push.main()
219
-
220
- captured_streams = capsys.readouterr()
221
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
222
- assert UNRESOLVABLE_BASE_REPORT_MARKER in captured_streams.err
223
- assert GIT_UNAVAILABLE_REPORT_MARKER not in captured_streams.err
224
-
225
-
226
- @pytest.fixture(autouse=True)
227
- def resolve_remote_head_reference(monkeypatch: pytest.MonkeyPatch) -> None:
228
- """Make every reference resolve, so these tests ignore the clone's remote state.
229
-
230
- ``main`` resolves the gate base through git before it invokes the gate. A
231
- clone with no ``origin/HEAD`` would send these tests down the unresolvable
232
- path and change what ``main`` returns. Stubbing the seam keeps each test
233
- about the flow it names. Base resolution itself is covered by
234
- ``test_pre_push_base_reference.py``.
235
- """
236
-
237
- def fake_run_git_text_command(all_command_arguments: list[str]) -> tuple[int, str]:
238
- return GIT_RESOLVED_EXIT_CODE, RESOLVED_COMMIT_OBJECT_NAME
239
-
240
- monkeypatch.setattr(pre_push, "run_git_text_command", fake_run_git_text_command)
241
- monkeypatch.setattr(
242
- sys,
243
- "argv",
244
- [HOOK_INVOCATION_NAME, git_hooks_constants.DEFAULT_REMOTE_NAME, PUSHED_REMOTE_URL],
245
- )
246
-
247
-
248
-
249
- def test_resolve_base_reference_from_stdin_uses_remote_object_when_non_zero() -> None:
250
- stdin_text = (
251
- f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
252
- )
253
-
254
- base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
255
-
256
- assert base_reference == NON_ZERO_REMOTE_SHA_ONE
257
-
258
-
259
- def test_resolve_base_reference_from_stdin_falls_back_when_remote_is_all_zeros() -> None:
260
- stdin_text = f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
261
-
262
- base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
263
-
264
- assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
265
-
266
-
267
- def test_resolve_base_reference_from_stdin_falls_back_when_stdin_is_empty() -> None:
268
- base_reference = pre_push.resolve_base_reference_from_stdin("")
269
-
270
- assert base_reference == pre_push.DEFAULT_REMOTE_BASE_REFERENCE
271
-
272
-
273
- def test_resolve_base_reference_from_stdin_prefers_first_non_zero_remote_object() -> (
274
- None
275
- ):
276
- stdin_text = (
277
- f"refs/heads/new_branch {ALL_ZEROS_OBJECT_NAME} refs/heads/new_branch {ALL_ZEROS_OBJECT_NAME}\n"
278
- f"refs/heads/existing {NON_ZERO_LOCAL_SHA} refs/heads/existing {NON_ZERO_REMOTE_SHA_TWO}\n"
279
- )
280
-
281
- base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
282
-
283
- assert base_reference == NON_ZERO_REMOTE_SHA_TWO
284
-
285
-
286
- def test_main_exits_zero_when_gate_script_missing(
287
- tmp_path: Path,
288
- monkeypatch: pytest.MonkeyPatch,
289
- ) -> None:
290
- monkeypatch.setenv(
291
- "CODE_RULES_GATE_PATH",
292
- str(tmp_path / "does_not_exist.py"),
293
- )
294
- monkeypatch.setattr(sys, "stdin", io.StringIO(""))
295
-
296
- exit_code = pre_push.main()
297
-
298
- assert exit_code == 0
299
-
300
-
301
- def test_main_invokes_gate_with_resolved_base_reference(
302
- tmp_path: Path,
303
- monkeypatch: pytest.MonkeyPatch,
304
- ) -> None:
305
- recorded_arguments_path = tmp_path / "recorded_arguments.txt"
306
- recording_gate_script_path = tmp_path / "recording_gate.py"
307
- recording_gate_script_path.write_text(
308
- "import sys, pathlib\n"
309
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
310
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
311
- "sys.exit(0)\n",
312
- encoding="utf-8",
313
- )
314
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
315
- _isolate_from_default_branch_refs(tmp_path, monkeypatch)
316
- remote_sha = "9" * 40
317
- monkeypatch.setattr(
318
- sys,
319
- "stdin",
320
- io.StringIO(f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {remote_sha}\n"),
321
- )
322
-
323
- exit_code = pre_push.main()
324
-
325
- assert exit_code == 0
326
- assert recorded_arguments_path.exists(), (
327
- f"recording gate did not write to {recorded_arguments_path}"
328
- )
329
- recorded_arguments = recorded_arguments_path.read_text(
330
- encoding="utf-8"
331
- ).splitlines()
332
- assert recorded_arguments == ["--base", remote_sha]
333
-
334
-
335
- def test_main_passes_the_pushed_remote_name_to_base_resolution(
336
- tmp_path: Path,
337
- monkeypatch: pytest.MonkeyPatch,
338
- ) -> None:
339
- # No default-branch ref so the gate base is the stdin remote object, not a
340
- # merge-base skip; usable-base resolution then sees the pushed remote name.
341
- _isolate_repository_and_write_passing_code_rules_gate(tmp_path, monkeypatch)
342
- all_recorded_remote_names: list[str] = []
343
-
344
- def recording_resolve_usable_base_reference(
345
- base_reference: str,
346
- remote_name: str,
347
- ask_git: Callable[[list[str]], tuple[int, str]],
348
- ) -> str:
349
- all_recorded_remote_names.append(remote_name)
350
- return base_reference
351
-
352
- monkeypatch.setattr(
353
- pre_push,
354
- "resolve_usable_base_reference",
355
- recording_resolve_usable_base_reference,
356
- )
357
- monkeypatch.setattr(
358
- sys, "argv", [HOOK_INVOCATION_NAME, PUSHED_REMOTE_NAME, PUSHED_REMOTE_URL]
359
- )
360
-
361
- exit_code = pre_push.main()
362
-
363
- assert exit_code == 0
364
- assert all_recorded_remote_names == [PUSHED_REMOTE_NAME]
365
-
366
-
367
- def test_main_propagates_blocking_exit_code_from_gate(
368
- tmp_path: Path,
369
- monkeypatch: pytest.MonkeyPatch,
370
- ) -> None:
371
- blocking_gate_script_path = tmp_path / "blocking_gate.py"
372
- blocking_gate_script_path.write_text(
373
- "import sys\nsys.exit(1)\n",
374
- encoding="utf-8",
375
- )
376
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(blocking_gate_script_path))
377
- monkeypatch.setattr(sys, "stdin", io.StringIO(""))
378
-
379
- exit_code = pre_push.main()
380
-
381
- assert exit_code == 1
382
-
383
-
384
- def test_main_propagates_infrastructure_failure_exit_code_from_gate(
385
- tmp_path: Path,
386
- monkeypatch: pytest.MonkeyPatch,
387
- ) -> None:
388
- infrastructure_failure_gate_path = tmp_path / "infrastructure_failure_gate.py"
389
- infrastructure_failure_gate_path.write_text(
390
- "import sys\nsys.exit(2)\n",
391
- encoding="utf-8",
392
- )
393
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(infrastructure_failure_gate_path))
394
- monkeypatch.setattr(sys, "stdin", io.StringIO(""))
395
-
396
- exit_code = pre_push.main()
397
-
398
- assert exit_code == 2
399
-
400
-
401
- def test_main_exits_two_when_stdin_raises_ioerror(
402
- tmp_path: Path,
403
- monkeypatch: pytest.MonkeyPatch,
404
- ) -> None:
405
- gate_path = tmp_path / "gate.py"
406
- gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
407
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
408
-
409
- class RaisingStdin:
410
- def read(self) -> str:
411
- raise IOError("broken pipe")
412
-
413
- monkeypatch.setattr(sys, "stdin", RaisingStdin())
414
-
415
- exit_code = pre_push.main()
416
-
417
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
418
-
419
-
420
- def test_main_exits_two_when_invoke_gate_raises_oserror(
421
- tmp_path: Path,
422
- monkeypatch: pytest.MonkeyPatch,
423
- ) -> None:
424
- gate_path = tmp_path / "gate.py"
425
- gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
426
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
427
- monkeypatch.setattr(sys, "stdin", io.StringIO(""))
428
-
429
- def raising_run(*args: object, **kwargs: object) -> object:
430
- raise OSError("no such file")
431
-
432
- monkeypatch.setattr(__import__("subprocess"), "run", raising_run)
433
-
434
- exit_code = pre_push.main()
435
-
436
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
437
-
438
-
439
- def test_resolve_base_reference_from_stdin_emits_warning_for_malformed_line(
440
- capsys: pytest.CaptureFixture[str],
441
- ) -> None:
442
- malformed_stdin_text = "only_one_field\n"
443
-
444
- pre_push.resolve_base_reference_from_stdin(malformed_stdin_text)
445
-
446
- captured = capsys.readouterr()
447
- assert "malformed" in captured.err
448
-
449
-
450
- def test_resolve_base_reference_from_stdin_returns_none_when_local_sha_is_all_zeros() -> None:
451
- stdin_text = f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
452
-
453
- base_reference = pre_push.resolve_base_reference_from_stdin(stdin_text)
454
-
455
- assert base_reference is None
456
-
457
-
458
- def test_main_exits_zero_immediately_when_push_is_deletion(
459
- tmp_path: Path,
460
- monkeypatch: pytest.MonkeyPatch,
461
- ) -> None:
462
- gate_path = tmp_path / "gate.py"
463
- gate_path.write_text("import sys\nsys.exit(1)\n", encoding="utf-8")
464
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
465
- deletion_stdin = f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
466
- monkeypatch.setattr(sys, "stdin", io.StringIO(deletion_stdin))
467
-
468
- exit_code = pre_push.main()
469
-
470
- assert exit_code == 0
471
-
472
-
473
- def test_resolve_base_reference_from_stdin_returns_sentinel_for_malformed_lines() -> None:
474
- malformed_only_stdin = "one_field_only\nalso_malformed\n"
475
-
476
- base_reference = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
477
-
478
- assert base_reference == git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL
479
-
480
-
481
- def test_main_prints_stderr_when_gate_script_missing(
482
- tmp_path: Path,
483
- monkeypatch: pytest.MonkeyPatch,
484
- capsys: pytest.CaptureFixture[str],
485
- ) -> None:
486
- monkeypatch.setenv(
487
- "CODE_RULES_GATE_PATH",
488
- str(tmp_path / "does_not_exist.py"),
489
- )
490
- monkeypatch.setattr(sys, "stdin", io.StringIO(""))
491
-
492
- pre_push.main()
493
-
494
- captured = capsys.readouterr()
495
- assert captured.err != ""
496
-
497
-
498
- def test_resolve_base_reference_from_stdin_reports_sentinel_with_no_valid_lines() -> None:
499
- malformed_only_stdin = "one_field_only\nalso_malformed\n"
500
-
501
- result = pre_push.resolve_base_reference_from_stdin(malformed_only_stdin)
502
-
503
- assert result == git_hooks_constants.NO_PARSEABLE_STDIN_LINES_SENTINEL
504
-
505
-
506
- def test_main_exits_two_when_all_stdin_lines_are_malformed(
507
- tmp_path: Path,
508
- monkeypatch: pytest.MonkeyPatch,
509
- capsys: pytest.CaptureFixture[str],
510
- ) -> None:
511
- gate_path = tmp_path / "gate.py"
512
- gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
513
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_path))
514
- monkeypatch.setattr(sys, "stdin", io.StringIO("one_field_only\nalso_malformed\n"))
515
-
516
- exit_code = pre_push.main()
517
-
518
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
519
- captured = capsys.readouterr()
520
- assert "no parseable stdin lines" in captured.err
521
-
522
-
523
- def test_invoke_gate_returns_infrastructure_failure_when_strict_resolve_raises(
524
- tmp_path: Path,
525
- monkeypatch: pytest.MonkeyPatch,
526
- ) -> None:
527
- gate_path = tmp_path / "gate.py"
528
- gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
529
-
530
- original_resolve = Path.resolve
531
-
532
- def raising_resolve(self: Path, strict: bool = False) -> Path:
533
- if strict and self == gate_path.resolve():
534
- raise FileNotFoundError("not found")
535
- return original_resolve(self, strict=strict)
536
-
537
- monkeypatch.setattr(Path, "resolve", raising_resolve)
538
-
539
- exit_code = pre_push.invoke_gate(gate_path, "origin/main")
540
-
541
- assert exit_code == 2
542
-
543
-
544
- def test_invoke_gate_uses_resolved_path(
545
- tmp_path: Path,
546
- monkeypatch: pytest.MonkeyPatch,
547
- ) -> None:
548
- real_gate_dir = tmp_path / "real"
549
- real_gate_dir.mkdir()
550
- real_gate_path = real_gate_dir / "gate.py"
551
- recorded_path_file = tmp_path / "recorded_path.txt"
552
- real_gate_path.write_text(
553
- "import sys, pathlib\n"
554
- f'pathlib.Path(r"{recorded_path_file}").write_text(sys.argv[0], encoding="utf-8")\n'
555
- "sys.exit(0)\n",
556
- encoding="utf-8",
557
- )
558
- symlink_gate_path = tmp_path / "link_gate.py"
559
- symlink_gate_path.symlink_to(real_gate_path)
560
- resolved_path = symlink_gate_path.resolve()
561
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
562
- _isolate_from_default_branch_refs(tmp_path, monkeypatch)
563
- monkeypatch.setattr(sys, "stdin", io.StringIO(
564
- f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
565
- ))
566
-
567
- exit_code = pre_push.main()
568
-
569
- assert exit_code == 0
570
- executed_path = recorded_path_file.read_text(encoding="utf-8")
571
- assert executed_path == str(resolved_path)
572
-
573
-
574
- def test_find_protected_branch_push_violation_flags_feature_branch_to_main() -> None:
575
- stdin_text = (
576
- f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
577
- )
578
-
579
- violation = pre_push.find_protected_branch_push_violation(stdin_text)
580
-
581
- assert violation == ("feat/example", "main")
582
-
583
-
584
- def test_find_protected_branch_push_violation_flags_feature_branch_to_master() -> None:
585
- stdin_text = (
586
- f"refs/heads/topic {NON_ZERO_LOCAL_SHA} refs/heads/master {NON_ZERO_REMOTE_SHA_ONE}\n"
587
- )
588
-
589
- violation = pre_push.find_protected_branch_push_violation(stdin_text)
590
-
591
- assert violation == ("topic", "master")
592
-
593
-
594
- def test_find_protected_branch_push_violation_allows_main_onto_main() -> None:
595
- stdin_text = (
596
- f"refs/heads/main {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
597
- )
598
-
599
- violation = pre_push.find_protected_branch_push_violation(stdin_text)
600
-
601
- assert violation is None
602
-
603
-
604
- def test_find_protected_branch_push_violation_allows_feature_onto_own_ref() -> None:
605
- stdin_text = (
606
- f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/feat/example {NON_ZERO_REMOTE_SHA_ONE}\n"
607
- )
608
-
609
- violation = pre_push.find_protected_branch_push_violation(stdin_text)
610
-
611
- assert violation is None
612
-
613
-
614
- def test_find_protected_branch_push_violation_ignores_deletion_of_main() -> None:
615
- stdin_text = (
616
- f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
617
- )
618
-
619
- violation = pre_push.find_protected_branch_push_violation(stdin_text)
620
-
621
- assert violation is None
622
-
623
-
624
- def test_main_blocks_feature_branch_push_onto_main(
625
- tmp_path: Path,
626
- monkeypatch: pytest.MonkeyPatch,
627
- capsys: pytest.CaptureFixture[str],
628
- ) -> None:
629
- passing_gate_path = tmp_path / "gate.py"
630
- passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
631
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
632
- monkeypatch.setattr(
633
- sys,
634
- "stdin",
635
- io.StringIO(
636
- f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
637
- ),
638
- )
639
-
640
- exit_code = pre_push.main()
641
-
642
- assert exit_code == git_hooks_constants.PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
643
- captured = capsys.readouterr()
644
- assert "feat/example" in captured.err
645
- assert "main" in captured.err
646
-
647
-
648
- def test_main_blocks_protected_push_even_when_gate_script_missing(
649
- tmp_path: Path,
650
- monkeypatch: pytest.MonkeyPatch,
651
- capsys: pytest.CaptureFixture[str],
652
- ) -> None:
653
- monkeypatch.setenv(
654
- "CODE_RULES_GATE_PATH",
655
- str(tmp_path / "does_not_exist.py"),
656
- )
657
- monkeypatch.setattr(
658
- sys,
659
- "stdin",
660
- io.StringIO(
661
- f"refs/heads/feat/example {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
662
- ),
663
- )
664
-
665
- exit_code = pre_push.main()
666
-
667
- assert exit_code == git_hooks_constants.PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE
668
- captured = capsys.readouterr()
669
- assert "feat/example" in captured.err
670
-
671
-
672
- def test_main_allows_main_onto_main_push(
673
- tmp_path: Path,
674
- monkeypatch: pytest.MonkeyPatch,
675
- ) -> None:
676
- passing_gate_path = tmp_path / "gate.py"
677
- passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
678
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
679
- monkeypatch.setattr(
680
- sys,
681
- "stdin",
682
- io.StringIO(
683
- f"refs/heads/main {NON_ZERO_LOCAL_SHA} refs/heads/main {NON_ZERO_REMOTE_SHA_ONE}\n"
684
- ),
685
- )
686
-
687
- exit_code = pre_push.main()
688
-
689
- assert exit_code == 0
690
-
691
-
692
-
693
- def _write_passing_code_rules_gate(
694
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
695
- ) -> None:
696
- """Install a passing CODE_RULES gate and a one-line feature push on stdin."""
697
- passing_gate_path = tmp_path / "code_rules_gate.py"
698
- passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
699
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
700
- monkeypatch.setattr(
701
- sys,
702
- "stdin",
703
- io.StringIO(
704
- f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
705
- ),
706
- )
707
-
708
-
709
- def _isolate_repository_and_write_passing_code_rules_gate(
710
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
711
- ) -> None:
712
- """Move into a repository without origin refs, then install a passing gate."""
713
- _isolate_from_default_branch_refs(tmp_path, monkeypatch)
714
- _write_passing_code_rules_gate(tmp_path, monkeypatch)
715
-
716
-
717
- DEFAULT_BRANCH_NAME: str = "main"
718
- FEATURE_BRANCH_NAME: str = "feature/example"
719
- UNPROTECTED_DEFAULT_BRANCH_NAME: str = "develop"
720
- ORIGIN_DEFAULT_BRANCH_REFERENCE: str = (
721
- git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX + DEFAULT_BRANCH_NAME
722
- )
723
- ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE: str = (
724
- git_hooks_constants.ORIGIN_REMOTE_TRACKING_REFERENCE_PREFIX
725
- + UNPROTECTED_DEFAULT_BRANCH_NAME
726
- )
727
- ORIGIN_HEAD_REFERENCE: str = git_hooks_constants.ORIGIN_HEAD_SYMBOLIC_REFERENCE
728
- LOCAL_ALIAS_BRANCH_NAME: str = "release-candidate"
729
- TOPIC_REMOTE_BRANCH_NAME: str = "topic-x"
730
- BASE_FILE_NAME: str = "README.md"
731
- FLAGGED_FILE_NAME: str = "flagged_module.py"
732
- BRANCH_FILE_NAME: str = "branch_module.py"
733
- DEFAULT_BRANCH_FILE_NAME: str = "default_branch_module.py"
734
- ALL_FIXTURE_GIT_FLAGS: tuple[str, ...] = (
735
- "-c",
736
- "user.name=hooks-test",
737
- "-c",
738
- "user.email=hooks-test@example.invalid",
739
- "-c",
740
- "commit.gpgsign=false",
741
- "-c",
742
- "core.hooksPath=absent_fixture_hooks",
743
- )
744
- FIXTURE_GIT_TIMEOUT_SECONDS: int = 60
745
-
746
-
747
- def _run_fixture_git(repository: Path, *all_arguments: str) -> str:
748
- """Run one git command inside a fixture repository, isolated from user hooks."""
749
- completion = subprocess.run(
750
- ["git", *ALL_FIXTURE_GIT_FLAGS, *all_arguments],
751
- cwd=str(repository),
752
- capture_output=True,
753
- text=True,
754
- check=True,
755
- timeout=FIXTURE_GIT_TIMEOUT_SECONDS,
756
- )
757
- return completion.stdout.strip()
758
-
759
-
760
- def _commit_file(repository: Path, file_name: str) -> str:
761
- """Write and commit one file, returning the resulting commit object name."""
762
- (repository / file_name).write_text(f"body of {file_name}\n", encoding="utf-8")
763
- _run_fixture_git(repository, "add", file_name)
764
- _run_fixture_git(repository, "commit", "-m", f"add {file_name}")
765
- return _run_fixture_git(repository, "rev-parse", "HEAD")
766
-
767
-
768
- def _set_origin_default_branch_tip(
769
- repository: Path, default_branch_reference: str, default_branch_tip: str
770
- ) -> None:
771
- """Point an origin default-branch remote-tracking ref at a commit."""
772
- _run_fixture_git(
773
- repository, "update-ref", default_branch_reference, default_branch_tip
774
- )
775
-
776
-
777
- def _set_origin_head(repository: Path, default_branch_reference: str) -> None:
778
- """Point the origin HEAD symbolic ref at an origin default-branch ref."""
779
- _run_fixture_git(
780
- repository, "symbolic-ref", ORIGIN_HEAD_REFERENCE, default_branch_reference
781
- )
782
-
783
-
784
- def _push_stdin_line(
785
- local_branch_name: str,
786
- pushed_object_name: str,
787
- remote_branch_name: str,
788
- remote_object_name: str,
789
- ) -> str:
790
- """Build the pre-push stdin line git writes for one branch update."""
791
- return (
792
- f"refs/heads/{local_branch_name} {pushed_object_name} "
793
- f"refs/heads/{remote_branch_name} {remote_object_name}\n"
794
- )
795
-
796
-
797
- def _build_rebased_branch_repository(
798
- tmp_path: Path, default_branch_file_name: str, branch_file_name: str
799
- ) -> tuple[Path, str, str]:
800
- """Build a repository whose feature branch sits rebased onto an advanced main.
801
-
802
- Args:
803
- tmp_path: The pytest temporary directory the repository is built in.
804
- default_branch_file_name: File the default branch gains after the branch forks.
805
- branch_file_name: File the feature branch adds.
806
-
807
- Returns:
808
- The repository path, the rebased branch tip, and the pre-rebase tip the
809
- remote still holds. The origin HEAD symbolic ref stays unset.
810
- """
811
- repository = tmp_path / "rebased_branch_repository"
812
- repository.mkdir()
813
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
814
- _commit_file(repository, BASE_FILE_NAME)
815
- _run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
816
- pre_rebase_tip = _commit_file(repository, branch_file_name)
817
- _run_fixture_git(repository, "checkout", DEFAULT_BRANCH_NAME)
818
- _set_origin_default_branch_tip(
819
- repository,
820
- ORIGIN_DEFAULT_BRANCH_REFERENCE,
821
- _commit_file(repository, default_branch_file_name),
822
- )
823
- _run_fixture_git(repository, "checkout", FEATURE_BRANCH_NAME)
824
- _run_fixture_git(repository, "rebase", DEFAULT_BRANCH_NAME)
825
- return repository, _run_fixture_git(repository, "rev-parse", "HEAD"), pre_rebase_tip
826
-
827
-
828
- def _write_diff_scoped_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
829
- """Install a gate that flags the marker file when it sits in the base..HEAD diff."""
830
- gate_script_path = tmp_path / "diff_scoped_gate.py"
831
- gate_script_path.write_text(
832
- "import subprocess\n"
833
- "import sys\n"
834
- "base_reference = sys.argv[2]\n"
835
- "merge_base = subprocess.run(\n"
836
- " ['git', 'merge-base', 'HEAD', base_reference],\n"
837
- " capture_output=True, text=True, check=True,\n"
838
- ").stdout.strip()\n"
839
- "all_changed_names = subprocess.run(\n"
840
- " ['git', 'diff', '--name-only', merge_base, 'HEAD'],\n"
841
- " capture_output=True, text=True, check=True,\n"
842
- ").stdout.split()\n"
843
- f"sys.exit(1 if {FLAGGED_FILE_NAME!r} in all_changed_names else 0)\n",
844
- encoding="utf-8",
845
- )
846
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(gate_script_path))
847
-
848
-
849
- def _build_default_branch_repository(tmp_path: Path) -> tuple[Path, str, str]:
850
- """Build a repository with an origin default branch and a topic branch off it.
851
-
852
- Args:
853
- tmp_path: The pytest temporary directory the repository is built in.
854
-
855
- Returns:
856
- The repository path, the default branch tip, and the topic branch tip.
857
- """
858
- repository = tmp_path / "default_branch_repository"
859
- repository.mkdir()
860
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
861
- _commit_file(repository, BASE_FILE_NAME)
862
- default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
863
- _run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
864
- topic_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
865
- _set_origin_default_branch_tip(
866
- repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
867
- )
868
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
869
- return repository, default_branch_tip, topic_branch_tip
870
-
871
-
872
- def _isolate_from_default_branch_refs(
873
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
874
- ) -> None:
875
- """Move the test into a fresh repository that holds no origin default branch.
876
-
877
- Tests that feed synthetic object names need a working directory where no
878
- default-branch ref resolves, so the hook takes its stdin remote-object
879
- fallback rather than reading the ambient repository's origin refs.
880
- """
881
- isolated_repository = tmp_path / "repository_without_origin_refs"
882
- isolated_repository.mkdir()
883
- _run_fixture_git(isolated_repository, "init", "-b", DEFAULT_BRANCH_NAME)
884
- monkeypatch.chdir(isolated_repository)
885
-
886
-
887
- def _build_unrelated_history_repository(tmp_path: Path) -> tuple[Path, str]:
888
- """Build a repository whose feature branch shares no history with the default.
889
-
890
- The origin default-branch ref and the origin HEAD symbolic ref both resolve,
891
- so ``git merge-base`` runs and reports no merge base rather than the hook
892
- failing to find a default branch at all.
893
-
894
- Args:
895
- tmp_path: The pytest temporary directory the repository is built in.
896
-
897
- Returns:
898
- The repository path and the orphan feature branch tip.
899
- """
900
- repository = tmp_path / "unrelated_history_repository"
901
- repository.mkdir()
902
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
903
- _commit_file(repository, BASE_FILE_NAME)
904
- default_branch_tip = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
905
- _run_fixture_git(repository, "checkout", "--orphan", FEATURE_BRANCH_NAME)
906
- _run_fixture_git(repository, "reset")
907
- orphan_branch_tip = _commit_file(repository, BRANCH_FILE_NAME)
908
- _set_origin_default_branch_tip(
909
- repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, default_branch_tip
910
- )
911
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
912
- return repository, orphan_branch_tip
913
-
914
-
915
- def _same_branch_push_line(
916
- local_object_name: str, remote_object_name: str
917
- ) -> pre_push.PushLine:
918
- """Build a PushLine whose local and remote refs name the same branch."""
919
- return pre_push.PushLine(
920
- local_branch_name=FEATURE_BRANCH_NAME,
921
- local_object_name=local_object_name,
922
- remote_branch_name=FEATURE_BRANCH_NAME,
923
- remote_object_name=remote_object_name,
924
- )
925
-
926
-
927
- def test_parse_push_line_reads_both_branch_names_and_both_object_names() -> None:
928
- parsed_line = pre_push.parse_push_line(
929
- f"refs/heads/{FEATURE_BRANCH_NAME} {NON_ZERO_LOCAL_SHA} "
930
- f"refs/heads/{DEFAULT_BRANCH_NAME} {NON_ZERO_REMOTE_SHA_ONE}"
931
- )
932
-
933
- assert parsed_line == pre_push.PushLine(
934
- local_branch_name=FEATURE_BRANCH_NAME,
935
- local_object_name=NON_ZERO_LOCAL_SHA,
936
- remote_branch_name=DEFAULT_BRANCH_NAME,
937
- remote_object_name=NON_ZERO_REMOTE_SHA_ONE,
938
- )
939
-
940
-
941
- def test_parse_push_line_returns_none_for_a_line_missing_fields() -> None:
942
- assert pre_push.parse_push_line("refs/heads/feature one_more_field") is None
943
-
944
-
945
- def test_is_branch_update_separates_updates_from_deletions_and_new_branches() -> None:
946
- assert pre_push.is_branch_update(
947
- _same_branch_push_line(NON_ZERO_LOCAL_SHA, NON_ZERO_REMOTE_SHA_ONE)
948
- )
949
- assert not pre_push.is_branch_update(
950
- _same_branch_push_line(ALL_ZEROS_OBJECT_NAME, NON_ZERO_REMOTE_SHA_ONE)
951
- )
952
- assert not pre_push.is_branch_update(
953
- _same_branch_push_line(NON_ZERO_LOCAL_SHA, ALL_ZEROS_OBJECT_NAME)
954
- )
955
-
956
-
957
- def test_run_git_reference_query_returns_stdout_for_a_resolvable_reference(
958
- tmp_path: Path,
959
- monkeypatch: pytest.MonkeyPatch,
960
- ) -> None:
961
- repository, default_branch_tip, _topic_branch_tip = _build_default_branch_repository(
962
- tmp_path
963
- )
964
- monkeypatch.chdir(repository)
965
-
966
- resolved_object_name = pre_push.run_git_reference_query(
967
- ("git", "rev-parse", ORIGIN_DEFAULT_BRANCH_REFERENCE)
968
- )
969
-
970
- assert resolved_object_name == default_branch_tip
971
-
972
-
973
- def test_run_git_reference_query_returns_none_for_an_absent_reference(
974
- tmp_path: Path,
975
- monkeypatch: pytest.MonkeyPatch,
976
- ) -> None:
977
- repository, _default_branch_tip, _topic_branch_tip = (
978
- _build_default_branch_repository(tmp_path)
979
- )
980
- monkeypatch.chdir(repository)
981
-
982
- resolved_object_name = pre_push.run_git_reference_query(
983
- ("git", "rev-parse", "--verify", "--quiet", "refs/remotes/origin/absent")
984
- )
985
-
986
- assert resolved_object_name is None
987
-
988
-
989
- def test_resolve_default_branch_reference_reads_the_origin_head_symbolic_reference(
990
- tmp_path: Path,
991
- monkeypatch: pytest.MonkeyPatch,
992
- ) -> None:
993
- repository, _default_branch_tip, _topic_branch_tip = (
994
- _build_default_branch_repository(tmp_path)
995
- )
996
- monkeypatch.chdir(repository)
997
-
998
- assert pre_push.resolve_default_branch_reference() == ORIGIN_DEFAULT_BRANCH_REFERENCE
999
-
1000
-
1001
- def test_resolve_default_branch_reference_reads_the_requested_remote_head(
1002
- tmp_path: Path,
1003
- monkeypatch: pytest.MonkeyPatch,
1004
- ) -> None:
1005
- repository, default_branch_tip, _topic_branch_tip = _build_default_branch_repository(
1006
- tmp_path
1007
- )
1008
- upstream_reference = "refs/remotes/upstream/main"
1009
- upstream_head_reference = "refs/remotes/upstream/HEAD"
1010
- _run_fixture_git(repository, "update-ref", upstream_reference, default_branch_tip)
1011
- _run_fixture_git(
1012
- repository, "symbolic-ref", upstream_head_reference, upstream_reference
1013
- )
1014
- monkeypatch.chdir(repository)
1015
-
1016
- assert pre_push.resolve_default_branch_reference("upstream") == upstream_reference
1017
-
1018
-
1019
- def test_resolve_default_branch_merge_base_uses_requested_remote_trunk_fallback(
1020
- tmp_path: Path,
1021
- monkeypatch: pytest.MonkeyPatch,
1022
- ) -> None:
1023
- repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
1024
- tmp_path
1025
- )
1026
- upstream_trunk_reference = "refs/remotes/upstream/trunk"
1027
- _run_fixture_git(
1028
- repository, "update-ref", upstream_trunk_reference, default_branch_tip
1029
- )
1030
- monkeypatch.chdir(repository)
1031
-
1032
- merge_base_object_name = pre_push.resolve_default_branch_merge_base(
1033
- FEATURE_BRANCH_NAME,
1034
- topic_branch_tip,
1035
- "upstream",
1036
- )
1037
-
1038
- assert merge_base_object_name == default_branch_tip
1039
-
1040
-
1041
- def test_resolve_default_branch_reference_returns_none_without_remote_tracking_refs(
1042
- tmp_path: Path,
1043
- monkeypatch: pytest.MonkeyPatch,
1044
- ) -> None:
1045
- repository = tmp_path / "repository_without_remote_tracking_refs"
1046
- repository.mkdir()
1047
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
1048
- _commit_file(repository, BASE_FILE_NAME)
1049
- monkeypatch.chdir(repository)
1050
-
1051
- assert pre_push.resolve_default_branch_reference() is None
1052
-
1053
-
1054
- def test_find_branch_update_push_returns_the_update_that_follows_a_deletion_line() -> (
1055
- None
1056
- ):
1057
- stdin_text = (
1058
- f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/retired {NON_ZERO_REMOTE_SHA_ONE}\n"
1059
- + _push_stdin_line(
1060
- FEATURE_BRANCH_NAME,
1061
- NON_ZERO_LOCAL_SHA,
1062
- DEFAULT_BRANCH_NAME,
1063
- NON_ZERO_REMOTE_SHA_TWO,
1064
- )
1065
- )
1066
-
1067
- branch_update = pre_push.find_branch_update_push(stdin_text)
1068
-
1069
- assert branch_update == pre_push.PushLine(
1070
- local_branch_name=FEATURE_BRANCH_NAME,
1071
- local_object_name=NON_ZERO_LOCAL_SHA,
1072
- remote_branch_name=DEFAULT_BRANCH_NAME,
1073
- remote_object_name=NON_ZERO_REMOTE_SHA_TWO,
1074
- )
1075
-
1076
-
1077
- def test_find_branch_update_push_returns_none_when_every_line_is_a_deletion() -> None:
1078
- deletion_only_stdin = _push_stdin_line(
1079
- FEATURE_BRANCH_NAME,
1080
- ALL_ZEROS_OBJECT_NAME,
1081
- FEATURE_BRANCH_NAME,
1082
- NON_ZERO_REMOTE_SHA_ONE,
1083
- )
1084
-
1085
- assert pre_push.find_branch_update_push(deletion_only_stdin) is None
1086
-
1087
-
1088
- def test_resolve_default_branch_merge_base_returns_the_fork_point(
1089
- tmp_path: Path,
1090
- monkeypatch: pytest.MonkeyPatch,
1091
- ) -> None:
1092
- repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
1093
- tmp_path
1094
- )
1095
- monkeypatch.chdir(repository)
1096
-
1097
- merge_base_object_name = pre_push.resolve_default_branch_merge_base(
1098
- FEATURE_BRANCH_NAME, topic_branch_tip
1099
- )
1100
-
1101
- assert merge_base_object_name == default_branch_tip
1102
-
1103
-
1104
- def test_resolve_default_branch_merge_base_returns_none_for_a_default_branch_update(
1105
- tmp_path: Path,
1106
- monkeypatch: pytest.MonkeyPatch,
1107
- ) -> None:
1108
- repository, _default_branch_tip, topic_branch_tip = (
1109
- _build_default_branch_repository(tmp_path)
1110
- )
1111
- monkeypatch.chdir(repository)
1112
-
1113
- merge_base_object_name = pre_push.resolve_default_branch_merge_base(
1114
- DEFAULT_BRANCH_NAME, topic_branch_tip
1115
- )
1116
-
1117
- assert merge_base_object_name is None
1118
-
1119
-
1120
- def test_resolve_gate_base_reference_returns_the_default_branch_merge_base(
1121
- tmp_path: Path,
1122
- monkeypatch: pytest.MonkeyPatch,
1123
- ) -> None:
1124
- repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
1125
- tmp_path
1126
- )
1127
- monkeypatch.chdir(repository)
1128
- stdin_text = _push_stdin_line(
1129
- FEATURE_BRANCH_NAME,
1130
- topic_branch_tip,
1131
- FEATURE_BRANCH_NAME,
1132
- NON_ZERO_REMOTE_SHA_ONE,
1133
- )
1134
-
1135
- assert pre_push.resolve_gate_base_reference(stdin_text) == default_branch_tip
1136
-
1137
-
1138
- def test_resolve_gate_base_reference_returns_none_for_a_deletion_only_push() -> None:
1139
- deletion_only_stdin = _push_stdin_line(
1140
- FEATURE_BRANCH_NAME,
1141
- ALL_ZEROS_OBJECT_NAME,
1142
- FEATURE_BRANCH_NAME,
1143
- ALL_ZEROS_OBJECT_NAME,
1144
- )
1145
-
1146
- assert pre_push.resolve_gate_base_reference(deletion_only_stdin) is None
1147
-
1148
-
1149
- def test_resolve_gate_base_reference_flags_an_unresolvable_merge_base(
1150
- tmp_path: Path,
1151
- monkeypatch: pytest.MonkeyPatch,
1152
- ) -> None:
1153
- repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
1154
- monkeypatch.chdir(repository)
1155
- assert (
1156
- pre_push.run_git_reference_query(
1157
- ("git", "merge-base", orphan_branch_tip, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1158
- )
1159
- is None
1160
- ), "fixture branch shares history with the default branch"
1161
- stdin_text = _push_stdin_line(
1162
- FEATURE_BRANCH_NAME,
1163
- orphan_branch_tip,
1164
- FEATURE_BRANCH_NAME,
1165
- NON_ZERO_REMOTE_SHA_ONE,
1166
- )
1167
-
1168
- base_reference = pre_push.resolve_gate_base_reference(stdin_text)
1169
-
1170
- assert base_reference == git_hooks_constants.UNRESOLVABLE_MERGE_BASE_SENTINEL
1171
-
1172
-
1173
- def test_resolve_gate_base_reference_treats_a_dangling_origin_head_as_no_default(
1174
- tmp_path: Path,
1175
- monkeypatch: pytest.MonkeyPatch,
1176
- ) -> None:
1177
- """A symbolic origin/HEAD whose target is missing falls through to no default branch ref.
1178
-
1179
- The gate base then uses the stdin remote object, the same path as when no
1180
- default-branch ref resolves at all.
1181
- """
1182
- repository = tmp_path / "dangling_origin_head_repository"
1183
- repository.mkdir()
1184
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
1185
- pushed_object_name = _commit_file(repository, BASE_FILE_NAME)
1186
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1187
- monkeypatch.chdir(repository)
1188
- stdin_text = _push_stdin_line(
1189
- FEATURE_BRANCH_NAME,
1190
- pushed_object_name,
1191
- FEATURE_BRANCH_NAME,
1192
- NON_ZERO_REMOTE_SHA_ONE,
1193
- )
1194
-
1195
- base_reference = pre_push.resolve_gate_base_reference(stdin_text)
1196
-
1197
- assert base_reference == NON_ZERO_REMOTE_SHA_ONE
1198
-
1199
-
1200
- def test_main_allows_the_push_when_origin_head_names_an_absent_reference(
1201
- tmp_path: Path,
1202
- monkeypatch: pytest.MonkeyPatch,
1203
- ) -> None:
1204
- """Dangling origin/HEAD does not skip the gate; it uses the remote object base."""
1205
- repository = tmp_path / "dangling_origin_head_push_repository"
1206
- repository.mkdir()
1207
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
1208
- remote_object_name = _commit_file(repository, BASE_FILE_NAME)
1209
- _run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
1210
- pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
1211
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1212
- recorded_arguments_path = tmp_path / "recorded_arguments_dangling.txt"
1213
- recording_gate_script_path = tmp_path / "recording_gate_dangling.py"
1214
- recording_gate_script_path.write_text(
1215
- "import sys, pathlib\n"
1216
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
1217
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
1218
- "sys.exit(0)\n",
1219
- encoding="utf-8",
1220
- )
1221
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
1222
- monkeypatch.chdir(repository)
1223
- monkeypatch.setattr(
1224
- sys,
1225
- "stdin",
1226
- io.StringIO(
1227
- _push_stdin_line(
1228
- FEATURE_BRANCH_NAME,
1229
- pushed_object_name,
1230
- FEATURE_BRANCH_NAME,
1231
- remote_object_name,
1232
- )
1233
- ),
1234
- )
1235
-
1236
- exit_code = pre_push.main()
1237
-
1238
- assert exit_code == 0
1239
- recorded_arguments = recorded_arguments_path.read_text(
1240
- encoding="utf-8"
1241
- ).splitlines()
1242
- assert recorded_arguments == ["--base", remote_object_name]
7
+ import pre_push
8
+ import pytest
9
+ from test_native_hook_support import (
10
+ configure_remote_url_rewrite,
11
+ create_bare_repository,
12
+ create_native_repository,
13
+ install_native_hook,
14
+ read_git_head,
15
+ run_git,
16
+ run_native_git_action,
17
+ )
1243
18
 
1244
19
 
1245
- def test_main_keeps_the_gate_pending_when_no_merge_base_resolves(
1246
- tmp_path: Path,
20
+ def test_main_keeps_protected_destination_as_advisory(
1247
21
  monkeypatch: pytest.MonkeyPatch,
1248
22
  capsys: pytest.CaptureFixture[str],
1249
23
  ) -> None:
1250
- repository, orphan_branch_tip = _build_unrelated_history_repository(tmp_path)
1251
- recorded_arguments_path = tmp_path / "unreached_gate_arguments.txt"
1252
- recording_gate_script_path = tmp_path / "unreached_recording_gate.py"
1253
- recording_gate_script_path.write_text(
1254
- "import sys, pathlib\n"
1255
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
1256
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
1257
- "sys.exit(0)\n",
1258
- encoding="utf-8",
1259
- )
1260
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
1261
- monkeypatch.chdir(repository)
1262
- monkeypatch.setattr(
1263
- sys,
1264
- "stdin",
1265
- io.StringIO(
1266
- _push_stdin_line(
1267
- FEATURE_BRANCH_NAME,
1268
- orphan_branch_tip,
1269
- FEATURE_BRANCH_NAME,
1270
- NON_ZERO_REMOTE_SHA_ONE,
1271
- )
1272
- ),
1273
- )
1274
-
1275
- exit_code = pre_push.main()
1276
-
1277
- assert exit_code == git_hooks_constants.GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
1278
- assert not recorded_arguments_path.exists(), (
1279
- "the gate remains pending until the hook verifies a usable base"
1280
- )
1281
- assert git_hooks_constants.UNRESOLVABLE_MERGE_BASE_MESSAGE in capsys.readouterr().err
1282
-
1283
-
1284
- def test_main_uses_the_pushed_remote_when_origin_has_unrelated_history(
1285
- tmp_path: Path,
1286
- monkeypatch: pytest.MonkeyPatch,
1287
- ) -> None:
1288
- repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
1289
- tmp_path
1290
- )
1291
- _run_fixture_git(repository, "checkout", "--orphan", "unrelated-origin")
1292
- _run_fixture_git(repository, "rm", "-rf", ".")
1293
- unrelated_origin_tip = _commit_file(repository, "unrelated.txt")
1294
- _run_fixture_git(repository, "checkout", FEATURE_BRANCH_NAME)
1295
- _run_fixture_git(
1296
- repository,
1297
- "update-ref",
1298
- ORIGIN_DEFAULT_BRANCH_REFERENCE,
1299
- unrelated_origin_tip,
1300
- )
1301
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1302
- upstream_reference = "refs/remotes/upstream/main"
1303
- upstream_head_reference = "refs/remotes/upstream/HEAD"
1304
- _run_fixture_git(repository, "update-ref", upstream_reference, default_branch_tip)
1305
- _run_fixture_git(
1306
- repository, "symbolic-ref", upstream_head_reference, upstream_reference
1307
- )
1308
- recorded_arguments_path = tmp_path / "upstream_recorded_arguments.txt"
1309
- recording_gate_script_path = tmp_path / "upstream_recording_gate.py"
1310
- recording_gate_script_path.write_text(
1311
- "import pathlib, sys\n"
1312
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
1313
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
1314
- "sys.exit(0)\n",
1315
- encoding="utf-8",
1316
- )
1317
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
1318
- monkeypatch.chdir(repository)
1319
- monkeypatch.setattr(
1320
- sys, "argv", [HOOK_INVOCATION_NAME, PUSHED_REMOTE_NAME, PUSHED_REMOTE_URL]
1321
- )
1322
- monkeypatch.setattr(
1323
- sys,
1324
- "stdin",
1325
- io.StringIO(
1326
- _push_stdin_line(
1327
- FEATURE_BRANCH_NAME,
1328
- topic_branch_tip,
1329
- FEATURE_BRANCH_NAME,
1330
- NON_ZERO_REMOTE_SHA_ONE,
1331
- )
1332
- ),
1333
- )
1334
-
1335
- exit_code = pre_push.main()
1336
-
1337
- assert exit_code == 0
1338
- assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
1339
- "--base",
1340
- default_branch_tip,
1341
- ]
1342
-
1343
-
1344
- def test_main_allows_default_branch_file_the_rebase_replayed_onto(
1345
- tmp_path: Path,
1346
- monkeypatch: pytest.MonkeyPatch,
1347
- ) -> None:
1348
- repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
1349
- tmp_path,
1350
- default_branch_file_name=FLAGGED_FILE_NAME,
1351
- branch_file_name=BRANCH_FILE_NAME,
1352
- )
1353
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1354
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1355
- monkeypatch.chdir(repository)
1356
- monkeypatch.setattr(
1357
- sys,
1358
- "stdin",
1359
- io.StringIO(
1360
- _push_stdin_line(
1361
- FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
1362
- )
1363
- ),
1364
- )
1365
-
1366
- exit_code = pre_push.main()
1367
-
1368
- assert exit_code == 0
1369
-
1370
-
1371
- def test_main_blocks_violation_the_branch_itself_adds(
1372
- tmp_path: Path,
1373
- monkeypatch: pytest.MonkeyPatch,
1374
- ) -> None:
1375
- repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
1376
- tmp_path,
1377
- default_branch_file_name=DEFAULT_BRANCH_FILE_NAME,
1378
- branch_file_name=FLAGGED_FILE_NAME,
1379
- )
1380
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1381
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1382
- monkeypatch.chdir(repository)
1383
- monkeypatch.setattr(
1384
- sys,
1385
- "stdin",
1386
- io.StringIO(
1387
- _push_stdin_line(
1388
- FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
1389
- )
1390
- ),
1391
- )
1392
-
1393
- exit_code = pre_push.main()
1394
-
1395
- assert exit_code == 1
1396
-
1397
-
1398
- def test_main_uses_remote_object_when_no_default_branch_reference_resolves(
1399
- tmp_path: Path,
1400
- monkeypatch: pytest.MonkeyPatch,
1401
- ) -> None:
1402
- repository = tmp_path / "repository_without_remote"
1403
- repository.mkdir()
1404
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
1405
- remote_object_name = _commit_file(repository, BASE_FILE_NAME)
1406
- _run_fixture_git(repository, "checkout", "-b", FEATURE_BRANCH_NAME)
1407
- pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
1408
- recorded_arguments_path = tmp_path / "recorded_arguments.txt"
1409
- recording_gate_script_path = tmp_path / "recording_gate.py"
1410
- recording_gate_script_path.write_text(
1411
- "import sys, pathlib\n"
1412
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
1413
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
1414
- "sys.exit(0)\n",
1415
- encoding="utf-8",
1416
- )
1417
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
1418
- monkeypatch.chdir(repository)
1419
- monkeypatch.setattr(
1420
- sys,
1421
- "stdin",
1422
- io.StringIO(
1423
- _push_stdin_line(
1424
- FEATURE_BRANCH_NAME,
1425
- pushed_object_name,
1426
- FEATURE_BRANCH_NAME,
1427
- remote_object_name,
1428
- )
1429
- ),
1430
- )
1431
-
1432
- exit_code = pre_push.main()
1433
-
1434
- assert exit_code == 0
1435
- recorded_arguments = recorded_arguments_path.read_text(
1436
- encoding="utf-8"
1437
- ).splitlines()
1438
- assert recorded_arguments == ["--base", remote_object_name]
1439
-
1440
-
1441
- def test_main_uses_remote_object_when_the_default_branch_is_pushed(
1442
- tmp_path: Path,
1443
- monkeypatch: pytest.MonkeyPatch,
1444
- ) -> None:
1445
- repository = tmp_path / "stale_tracking_repository"
1446
- repository.mkdir()
1447
- _run_fixture_git(repository, "init", "-b", DEFAULT_BRANCH_NAME)
1448
- stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
1449
- remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
1450
- pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
1451
- _set_origin_default_branch_tip(
1452
- repository, ORIGIN_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
1453
- )
1454
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1455
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1456
- monkeypatch.chdir(repository)
1457
- monkeypatch.setattr(
1458
- sys,
1459
- "stdin",
1460
- io.StringIO(
1461
- _push_stdin_line(
1462
- DEFAULT_BRANCH_NAME,
1463
- pushed_object_name,
1464
- DEFAULT_BRANCH_NAME,
1465
- remote_object_name,
1466
- )
1467
- ),
1468
- )
1469
-
1470
- exit_code = pre_push.main()
1471
-
1472
- assert exit_code == 0
1473
-
1474
-
1475
- def test_main_uses_merge_base_when_a_default_named_local_branch_updates_a_topic_branch(
1476
- tmp_path: Path,
1477
- monkeypatch: pytest.MonkeyPatch,
1478
- ) -> None:
1479
- repository = tmp_path / "default_named_local_branch_repository"
1480
- repository.mkdir()
1481
- _run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
1482
- remote_object_name = _commit_file(repository, BASE_FILE_NAME)
1483
- default_branch_tip = _commit_file(repository, FLAGGED_FILE_NAME)
1484
- pushed_object_name = _commit_file(repository, BRANCH_FILE_NAME)
1485
- _set_origin_default_branch_tip(
1486
- repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, default_branch_tip
1487
- )
1488
- _set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
1489
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1490
- monkeypatch.chdir(repository)
1491
- monkeypatch.setattr(
1492
- sys,
1493
- "stdin",
1494
- io.StringIO(
1495
- _push_stdin_line(
1496
- UNPROTECTED_DEFAULT_BRANCH_NAME,
1497
- pushed_object_name,
1498
- TOPIC_REMOTE_BRANCH_NAME,
1499
- remote_object_name,
1500
- )
1501
- ),
1502
- )
1503
-
1504
- exit_code = pre_push.main()
1505
-
1506
- assert exit_code == 0
1507
-
1508
-
1509
- def test_main_uses_remote_object_when_a_renamed_local_branch_updates_the_default_branch(
1510
- tmp_path: Path,
1511
- monkeypatch: pytest.MonkeyPatch,
1512
- ) -> None:
1513
- repository = tmp_path / "renamed_local_branch_repository"
1514
- repository.mkdir()
1515
- _run_fixture_git(repository, "init", "-b", UNPROTECTED_DEFAULT_BRANCH_NAME)
1516
- stale_tracking_tip = _commit_file(repository, BASE_FILE_NAME)
1517
- remote_object_name = _commit_file(repository, FLAGGED_FILE_NAME)
1518
- pushed_object_name = _commit_file(repository, DEFAULT_BRANCH_FILE_NAME)
1519
- _run_fixture_git(repository, "checkout", "-b", LOCAL_ALIAS_BRANCH_NAME)
1520
- _set_origin_default_branch_tip(
1521
- repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE, stale_tracking_tip
1522
- )
1523
- _set_origin_head(repository, ORIGIN_UNPROTECTED_DEFAULT_BRANCH_REFERENCE)
1524
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1525
- monkeypatch.chdir(repository)
1526
- monkeypatch.setattr(
1527
- sys,
1528
- "stdin",
1529
- io.StringIO(
1530
- _push_stdin_line(
1531
- LOCAL_ALIAS_BRANCH_NAME,
1532
- pushed_object_name,
1533
- UNPROTECTED_DEFAULT_BRANCH_NAME,
1534
- remote_object_name,
1535
- )
1536
- ),
1537
- )
1538
-
1539
- exit_code = pre_push.main()
1540
-
1541
- assert exit_code == 0
1542
-
1543
-
1544
- def test_main_reads_default_branch_tracking_reference_when_origin_head_is_unset(
1545
- tmp_path: Path,
1546
- monkeypatch: pytest.MonkeyPatch,
1547
- ) -> None:
1548
- repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
1549
- tmp_path,
1550
- default_branch_file_name=FLAGGED_FILE_NAME,
1551
- branch_file_name=BRANCH_FILE_NAME,
1552
- )
1553
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1554
- monkeypatch.chdir(repository)
1555
24
  monkeypatch.setattr(
1556
25
  sys,
1557
26
  "stdin",
1558
27
  io.StringIO(
1559
- _push_stdin_line(
1560
- FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
1561
- )
28
+ "refs/heads/topic "
29
+ + "a" * 40
30
+ + " refs/heads/main "
31
+ + "b" * 40
32
+ + "\n"
1562
33
  ),
1563
34
  )
1564
35
 
1565
36
  exit_code = pre_push.main()
1566
37
 
38
+ captured_streams = capsys.readouterr()
1567
39
  assert exit_code == 0
40
+ assert "push would send local branch 'topic'" in captured_streams.err
1568
41
 
1569
42
 
1570
- def test_main_selects_stdin_bases_for_new_and_existing_branch_pushes(
1571
- tmp_path: Path,
1572
- monkeypatch: pytest.MonkeyPatch,
1573
- ) -> None:
1574
- repository, default_branch_tip, topic_branch_tip = _build_default_branch_repository(
1575
- tmp_path
1576
- )
1577
- recorded_arguments_path = tmp_path / "recorded_arguments.txt"
1578
- recording_gate_script_path = tmp_path / "recording_gate.py"
1579
- recording_gate_script_path.write_text(
1580
- "import pathlib, sys\n"
1581
- f'pathlib.Path(r"{recorded_arguments_path}").write_text('
1582
- "'\\n'.join(sys.argv[1:]), encoding='utf-8')\n"
1583
- "sys.exit(0)\n",
1584
- encoding="utf-8",
1585
- )
1586
- monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
1587
- monkeypatch.chdir(repository)
1588
- monkeypatch.setattr(
1589
- sys,
1590
- "stdin",
1591
- io.StringIO(
1592
- _push_stdin_line(
1593
- FEATURE_BRANCH_NAME,
1594
- topic_branch_tip,
1595
- FEATURE_BRANCH_NAME,
1596
- ALL_ZEROS_OBJECT_NAME,
1597
- )
1598
- ),
1599
- )
1600
-
1601
- assert pre_push.main() == 0
1602
- assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
1603
- "--base",
1604
- git_hooks_constants.DEFAULT_REMOTE_BASE_REFERENCE,
1605
- ]
1606
-
1607
- monkeypatch.setattr(
1608
- sys,
1609
- "stdin",
1610
- io.StringIO(
1611
- _push_stdin_line(
1612
- FEATURE_BRANCH_NAME,
1613
- topic_branch_tip,
1614
- FEATURE_BRANCH_NAME,
1615
- NON_ZERO_REMOTE_SHA_ONE,
1616
- )
1617
- ),
1618
- )
1619
-
1620
- assert pre_push.main() == 0
1621
- assert recorded_arguments_path.read_text(encoding="utf-8").splitlines() == [
1622
- "--base",
1623
- default_branch_tip,
1624
- ]
1625
-
1626
-
1627
- def test_main_skips_a_deletion_line_to_reach_the_branch_update(
1628
- tmp_path: Path,
1629
- monkeypatch: pytest.MonkeyPatch,
1630
- ) -> None:
1631
- repository, rebased_tip, pre_rebase_tip = _build_rebased_branch_repository(
43
+ def test_native_git_push_to_local_bare_remote_allows_missing_setup(tmp_path: Path) -> None:
44
+ repository_path = create_native_repository(
1632
45
  tmp_path,
1633
- default_branch_file_name=FLAGGED_FILE_NAME,
1634
- branch_file_name=BRANCH_FILE_NAME,
1635
- )
1636
- _set_origin_head(repository, ORIGIN_DEFAULT_BRANCH_REFERENCE)
1637
- _write_diff_scoped_gate(tmp_path, monkeypatch)
1638
- monkeypatch.chdir(repository)
1639
- deletion_line = (
1640
- f"(delete) {ALL_ZEROS_OBJECT_NAME} refs/heads/retired {NON_ZERO_REMOTE_SHA_ONE}\n"
1641
- )
1642
- monkeypatch.setattr(
1643
- sys,
1644
- "stdin",
1645
- io.StringIO(
1646
- deletion_line
1647
- + _push_stdin_line(
1648
- FEATURE_BRANCH_NAME, rebased_tip, FEATURE_BRANCH_NAME, pre_rebase_tip
1649
- )
1650
- ),
1651
- )
1652
-
1653
- exit_code = pre_push.main()
1654
-
1655
- assert exit_code == 0
46
+ "repo",
47
+ "https://github.com/JonEcho/python-automation.git",
48
+ initial_branch="main",
49
+ )
50
+ run_git(repository_path, "add", "README.md")
51
+ run_git(repository_path, "commit", "--quiet", "-m", "initial")
52
+
53
+ bare_repository_path = create_bare_repository(tmp_path)
54
+ configure_remote_url_rewrite(
55
+ repository_path,
56
+ bare_repository_path,
57
+ "https://github.com/JonEcho/python-automation.git",
58
+ )
59
+ hook_directory = tmp_path / "hooks"
60
+ install_native_hook(repository_path, hook_directory, "pre-push", "pre_push")
61
+
62
+ completed_push = run_native_git_action(repository_path, "push", "origin", "main")
63
+
64
+ pushed_head = read_git_head(bare_repository_path, "refs/heads/main")
65
+ current_head = read_git_head(repository_path, "HEAD")
66
+ combined_output = completed_push.stdout + completed_push.stderr
67
+ assert completed_push.returncode == 0
68
+ assert pushed_head == current_head
69
+ assert "LOCAL VERIFICATION ADVISORY" in combined_output
70
+ assert "Event: push" in combined_output
71
+ assert f"Current SHA: {current_head}" in combined_output
72
+ assert "State: pending" in combined_output