claude-dev-env 4.2.0 → 7.0.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 (350) 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 +7 -0
  12. package/.agents/skills/pull-request/SKILL.md +216 -0
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  15. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  16. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
  17. package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
  18. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  19. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
  20. package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
  21. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  22. package/.agents/skills/skill-builder/SKILL.md +3 -196
  23. package/.agents/skills/test-runner/SKILL.md +69 -0
  24. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  25. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  26. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  27. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  28. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  29. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  30. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  31. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  32. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  33. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  34. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  35. package/bin/ever-shipped-skills.mjs +2 -0
  36. package/bin/ever-shipped-skills.test.mjs +8 -0
  37. package/bin/install.agents-home.test.mjs +15 -13
  38. package/bin/install.mjs +37 -1
  39. package/bin/install.policy-lint.test.mjs +78 -0
  40. package/bin/install.prune.test.mjs +149 -20
  41. package/bin/install.pstack-plugin.test.mjs +114 -0
  42. package/bin/install.test.mjs +61 -7
  43. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  44. package/hooks/advisory/pr_done_reminder.py +342 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  46. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  47. package/hooks/blocking/code_rules_paired_test.py +22 -15
  48. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  49. package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
  50. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  51. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  52. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  53. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  54. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  55. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  56. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  57. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  58. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  59. package/hooks/gh_post_body_texts.py +195 -0
  60. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  61. package/hooks/git-hooks/pre_commit.py +52 -11
  62. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  63. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  64. package/hooks/git-hooks/test_pre_commit.py +246 -0
  65. package/hooks/hooks.json +1 -16
  66. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
  67. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  68. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  69. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  70. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  71. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +17 -7
  72. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  73. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  74. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  75. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  76. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  77. package/hooks/observability/test_failure_recorder.py +1 -1
  78. package/hooks/pyproject.toml +1 -1
  79. package/hooks/test_gh_post_body_texts.py +79 -0
  80. package/package.json +2 -2
  81. package/rules/durable-post-artifacts.md +32 -20
  82. package/rules/gh-cli-conventions.md +23 -13
  83. package/rules/git-workflow.md +21 -8
  84. package/scripts/check.ps1 +0 -1
  85. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  86. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  87. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  88. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  89. package/scripts/durable_post_lint.py +206 -0
  90. package/scripts/durable_post_lint_config/config/constants.py +96 -0
  91. package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
  92. package/scripts/policy_lint/adapter_configuration.py +53 -12
  93. package/scripts/policy_lint/adapter_detectors.py +13 -6
  94. package/scripts/policy_lint/adapters.py +7 -4
  95. package/scripts/policy_lint/config/constants.py +5 -0
  96. package/scripts/policy_lint/engine.py +48 -4
  97. package/scripts/policy_lint/model.py +1 -0
  98. package/scripts/policy_lint/registry.py +1 -1
  99. package/scripts/policy_lint/selection.py +2 -1
  100. package/scripts/policy_lint/selection_git.py +2 -0
  101. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  102. package/scripts/repository_checks/config/constants.py +0 -5
  103. package/scripts/shared_tree_paths.py +32 -3
  104. package/scripts/test_durable_post_lint.py +332 -0
  105. package/scripts/test_shared_tree_paths.py +33 -0
  106. package/scripts/tests/test_adapter_configuration.py +54 -0
  107. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  108. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  109. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  110. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  111. package/scripts/tests/test_policy_lint_selection.py +25 -2
  112. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  113. package/scripts/tests/test_rules.py +3 -7
  114. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  115. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  116. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  117. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  118. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  119. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  120. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  121. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  122. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  123. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  124. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  125. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  126. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  127. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  128. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  129. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  130. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  131. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  132. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  133. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  134. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  135. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  136. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  137. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  138. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  139. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  140. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  141. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  142. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  143. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  144. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  145. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  146. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  147. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  148. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  149. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  150. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  151. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  152. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  153. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  154. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  155. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  156. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  157. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  158. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  159. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  160. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  161. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  162. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  163. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  164. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  165. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  166. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  167. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  168. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  169. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  170. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  171. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  172. package/.agents/skills/autoconverge/SKILL.md +0 -431
  173. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  174. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  175. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  176. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  177. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  178. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  179. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  180. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  181. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  182. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  183. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  184. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  185. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  186. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  187. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  188. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  189. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  190. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  191. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  192. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  193. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  194. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  195. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  196. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  197. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  198. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  199. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  200. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  201. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  202. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  203. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  204. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  205. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  206. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  207. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  208. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  209. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  210. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  211. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  212. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  213. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  214. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  215. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  216. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  217. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  218. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  219. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  220. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  221. package/.agents/skills/hitl/SKILL.md +0 -45
  222. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  223. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  224. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  225. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  226. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  227. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  228. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  229. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  230. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  231. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  232. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  233. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  234. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  235. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  236. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  237. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  238. package/.agents/skills/prototype/SKILL.md +0 -85
  239. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  240. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  241. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  242. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  243. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  244. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  245. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  246. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  247. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  248. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  249. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  250. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  251. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  252. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  253. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  254. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  255. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  256. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  257. package/.agents/skills/rebase/AGENTS.md +0 -1
  258. package/.agents/skills/rebase/SKILL.md +0 -178
  259. package/.agents/skills/review-router/SKILL.md +0 -51
  260. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  261. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  262. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  263. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  264. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  265. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  266. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  267. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  268. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  269. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  270. package/.agents/skills/review-tier/SKILL.md +0 -56
  271. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  272. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  273. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  274. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  275. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  276. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  277. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  278. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  279. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  280. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  281. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  282. package/.agents/skills/session-log/AGENTS.md +0 -1
  283. package/.agents/skills/session-log/SKILL.md +0 -170
  284. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  285. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  286. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  287. package/.agents/skills/session-tidy/SKILL.md +0 -191
  288. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  289. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  290. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  291. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  292. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  293. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  294. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  295. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  296. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  297. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  298. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  299. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  300. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  301. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  302. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  303. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  304. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  305. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  306. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  307. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  308. package/.agents/skills/update/AGENTS.md +0 -1
  309. package/.agents/skills/update/SKILL.md +0 -190
  310. package/_shared/pr-loop/AGENTS.md +0 -1
  311. package/_shared/pr-loop/CLAUDE.md +0 -1
  312. package/_shared/pr-loop/audit-contract.md +0 -183
  313. package/_shared/pr-loop/audit-reply-template.md +0 -151
  314. package/_shared/pr-loop/code-rules-gate.md +0 -65
  315. package/_shared/pr-loop/fix-protocol.md +0 -60
  316. package/_shared/pr-loop/gh-payloads.md +0 -75
  317. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  318. package/_shared/pr-loop/precatch-rubric.md +0 -66
  319. package/_shared/pr-loop/preflight-proposal.md +0 -56
  320. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  321. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  322. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  323. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  324. package/_shared/pr-loop/state-schema.md +0 -45
  325. package/_shared/pr-loop/worker-spawn.md +0 -189
  326. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  327. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  328. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  329. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  330. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  331. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  332. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  333. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  334. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  335. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  336. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  337. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  338. package/hooks/blocking/test_preflight_check.py +0 -343
  339. package/hooks/blocking/test_test_preflight_check.py +0 -80
  340. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  341. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  342. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  343. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  344. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  345. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  346. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  347. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  348. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  349. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  350. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -1,1115 +0,0 @@
1
- """Live smoke tests for post_audit_thread.py.
2
-
3
- Runs against the real GitHub repo ``JonEcho/tests``. The class opens a
4
- single throwaway draft PR in ``setUpClass`` and reuses it across every
5
- test in the class; ``tearDownClass`` closes the PR with
6
- ``--delete-branch``. The CLEAN and DIRTY tests post real reviews against
7
- the shared PR. The retry tests stub the GitHub endpoint with a localhost
8
- HTTP server so the four-attempt retry loop runs deterministically without
9
- contacting api.github.com, but still reference the shared PR's number and
10
- HEAD SHA so the request URL is exercised end-to-end. Authentication uses
11
- ``gh auth token`` — empty token fails loudly per spec.
12
-
13
- Test files are exempt from the no-comment, magic-value, banned-identifier,
14
- and constants-location enforcer rules.
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import http.server
20
- import json
21
- import os
22
- import shutil
23
- import stat
24
- import subprocess
25
- import sys
26
- import tempfile
27
- import textwrap
28
- import threading
29
- import time
30
- import unittest
31
- import urllib.parse
32
- import uuid
33
- from pathlib import Path
34
- from typing import Any
35
-
36
- THIS_FILE_DIRECTORY = Path(__file__).resolve().parent
37
- SCRIPT_DIRECTORY = THIS_FILE_DIRECTORY.parent
38
-
39
- if str(SCRIPT_DIRECTORY) not in sys.path:
40
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
41
-
42
- from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
43
- ALL_GH_AUTH_TOKEN_COMMAND_PARTS,
44
- ALL_RETRY_BACKOFF_SECONDS,
45
- BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
46
- GH_TOKEN_ENV_VAR_NAME,
47
- GITHUB_TOKEN_ENV_VAR_NAME,
48
- CLI_FLAG_COMMIT,
49
- CLI_FLAG_FINDINGS_JSON,
50
- CLI_FLAG_OWNER,
51
- CLI_FLAG_PR_NUMBER,
52
- CLI_FLAG_REPO,
53
- CLI_FLAG_SKILL,
54
- CLI_FLAG_STATE,
55
- EXIT_CODE_RETRY_EXHAUSTED,
56
- INLINE_COMMENT_SIDE_RIGHT,
57
- JSON_FIELD_DESCRIPTION,
58
- JSON_FIELD_FIX_SUMMARY,
59
- JSON_FIELD_LINE,
60
- JSON_FIELD_PATH,
61
- JSON_FIELD_SEVERITY,
62
- JSON_FIELD_SIDE,
63
- MAX_RETRY_ATTEMPTS,
64
- SEVERITY_TAG_P0,
65
- SEVERITY_TAG_P1,
66
- SEVERITY_TAG_P2,
67
- SINGLE_REVIEW_API_PATH_TEMPLATE,
68
- SINGLE_REVIEW_COMMENTS_API_PATH_TEMPLATE,
69
- SKILL_BUGTEAM,
70
- STATE_CLEAN,
71
- STATE_DIRTY,
72
- )
73
- from post_audit_thread import ( # noqa: E402
74
- UserInputError,
75
- build_reviews_endpoint_url,
76
- fetch_gh_token_for_account,
77
- list_authenticated_gh_account_logins,
78
- query_active_gh_user_login,
79
- query_pull_request_author_login,
80
- resolve_reviewer_token,
81
- )
82
-
83
- LIVE_TEST_OWNER = "JonEcho"
84
- LIVE_TEST_REPO = "tests"
85
- LIVE_TEST_BRANCH_PREFIX = "pr-loop-test"
86
- LIVE_TEST_PR_TITLE = "TEST: post_audit_thread smoke test (auto-closed)"
87
- LIVE_TEST_PR_BODY = (
88
- "Throwaway PR for post_audit_thread.py live smoke tests. "
89
- "Auto-created by `test_post_audit_thread.py`; closed in `tearDownClass`."
90
- )
91
- LIVE_TEST_BASE_BRANCH = "main"
92
- LIVE_TEST_FIXTURE_FILENAME = "post-audit-thread-fixture.md"
93
- LIVE_TEST_FIXTURE_CONTENT = (
94
- "# Throwaway test fixture\n\n"
95
- "Created by `test_post_audit_thread.py` to satisfy GitHub's "
96
- "non-empty PR-diff requirement. Deleted when the PR closes.\n"
97
- )
98
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_ONE = 1
99
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_TWO = 2
100
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_THREE = 3
101
-
102
- SCRIPT_PATH = SCRIPT_DIRECTORY / "post_audit_thread.py"
103
- REPO_FULL_NAME = f"{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}"
104
-
105
- LIVE_TEST_AUDIT_ACCOUNT_NAME = "jl-cmd"
106
-
107
- GH_EVENT_APPROVED = "APPROVED"
108
- GH_EVENT_CHANGES_REQUESTED = "CHANGES_REQUESTED"
109
-
110
- UUID_SUFFIX_LENGTH = 8
111
-
112
- REVIEW_URL_ID_DELIMITER = "#pullrequestreview-"
113
-
114
-
115
- def _strip_read_only_and_retry(
116
- removal_function: Any, target_path: str, *_exc_info: Any
117
- ) -> None:
118
- try:
119
- os.chmod(target_path, stat.S_IWRITE)
120
- removal_function(target_path)
121
- except OSError:
122
- pass
123
-
124
-
125
- def force_remove_directory(target_path: Path) -> None:
126
- if not target_path.exists():
127
- return
128
- handler_kwargs: dict[str, Any]
129
- if sys.version_info >= (3, 12):
130
- handler_kwargs = {"onexc": _strip_read_only_and_retry}
131
- else:
132
- handler_kwargs = {"onerror": _strip_read_only_and_retry}
133
- try:
134
- shutil.rmtree(str(target_path), **handler_kwargs)
135
- except OSError as removal_error:
136
- sys.stderr.write(
137
- f"force_remove_directory: could not remove {target_path}: {removal_error}\n"
138
- )
139
-
140
-
141
- def resolve_gh_auth_token() -> str:
142
- completion = subprocess.run(
143
- list(ALL_GH_AUTH_TOKEN_COMMAND_PARTS),
144
- capture_output=True,
145
- text=True,
146
- encoding="utf-8",
147
- check=False,
148
- )
149
- if completion.returncode != 0:
150
- raise AssertionError(
151
- f"`gh auth token` failed: rc={completion.returncode} "
152
- f"stderr={completion.stderr.strip()} — live tests require gh to "
153
- f"be authenticated against github.com"
154
- )
155
- token_text = completion.stdout.strip()
156
- if not token_text:
157
- raise AssertionError(
158
- "`gh auth token` returned empty output — not authenticated"
159
- )
160
- return token_text
161
-
162
-
163
- def resolve_audit_account_token(account_name: str) -> str:
164
- completion = subprocess.run(
165
- list(ALL_GH_AUTH_TOKEN_COMMAND_PARTS) + ["--user", account_name],
166
- capture_output=True,
167
- text=True,
168
- encoding="utf-8",
169
- check=False,
170
- )
171
- if completion.returncode != 0:
172
- raise AssertionError(
173
- f"`gh auth token --user {account_name}` failed — the audit-side "
174
- f"account must be authenticated separately from the PR author so "
175
- f"GitHub allows APPROVE / REQUEST_CHANGES on the throwaway PR. "
176
- f"rc={completion.returncode} stderr={completion.stderr.strip()}"
177
- )
178
- token_text = completion.stdout.strip()
179
- if not token_text:
180
- raise AssertionError(
181
- f"`gh auth token --user {account_name}` returned empty output"
182
- )
183
- return token_text
184
-
185
-
186
- def gh_api_object_json(api_path: str) -> dict[str, Any]:
187
- completion = subprocess.run(
188
- ["gh", "api", api_path],
189
- capture_output=True,
190
- text=True,
191
- encoding="utf-8",
192
- check=True,
193
- )
194
- parsed_object: Any = json.loads(completion.stdout)
195
- if not isinstance(parsed_object, dict):
196
- raise AssertionError(
197
- f"unexpected gh api object shape: {type(parsed_object).__name__}"
198
- )
199
- return parsed_object
200
-
201
-
202
- def review_id_from_html_url(html_url: str) -> int:
203
- suffix_parts = html_url.rsplit(REVIEW_URL_ID_DELIMITER, 1)
204
- if len(suffix_parts) != 2:
205
- raise AssertionError(
206
- f"html_url {html_url!r} missing {REVIEW_URL_ID_DELIMITER!r} suffix"
207
- )
208
- return int(suffix_parts[1])
209
-
210
-
211
- def gh_api_paginated_json(api_path: str) -> list[dict[str, Any]]:
212
- completion = subprocess.run(
213
- ["gh", "api", api_path, "--paginate", "--slurp"],
214
- capture_output=True,
215
- text=True,
216
- encoding="utf-8",
217
- check=True,
218
- )
219
- parsed: Any = json.loads(completion.stdout)
220
- if not isinstance(parsed, list):
221
- raise AssertionError(
222
- f"unexpected gh api response shape: {type(parsed).__name__}"
223
- )
224
- flattened: list[dict[str, Any]] = []
225
- for each_page in parsed:
226
- if isinstance(each_page, list):
227
- for each_item in each_page:
228
- if isinstance(each_item, dict):
229
- flattened.append(each_item)
230
- elif isinstance(each_page, dict):
231
- flattened.append(each_page)
232
- return flattened
233
-
234
-
235
- def write_pr_body_temporary_file(body_text: str) -> Path:
236
- handle, body_path_str = tempfile.mkstemp(suffix=".md", prefix="post-audit-pr-body-")
237
- os.close(handle)
238
- body_path = Path(body_path_str)
239
- body_path.write_text(body_text, encoding="utf-8")
240
- return body_path
241
-
242
-
243
- def create_throwaway_pr(
244
- clone_directory: Path,
245
- branch_name: str,
246
- ) -> tuple[int, str]:
247
- subprocess.run(
248
- [
249
- "gh",
250
- "repo",
251
- "clone",
252
- REPO_FULL_NAME,
253
- str(clone_directory),
254
- "--",
255
- "--branch",
256
- LIVE_TEST_BASE_BRANCH,
257
- "--single-branch",
258
- "--depth",
259
- "1",
260
- ],
261
- check=True,
262
- capture_output=True,
263
- text=True,
264
- )
265
- subprocess.run(
266
- [
267
- "git",
268
- "-C",
269
- str(clone_directory),
270
- "config",
271
- "--local",
272
- "core.hooksPath",
273
- str(clone_directory / ".git" / "hooks"),
274
- ],
275
- check=True,
276
- capture_output=True,
277
- text=True,
278
- )
279
- subprocess.run(
280
- ["git", "-C", str(clone_directory), "checkout", "-b", branch_name],
281
- check=True,
282
- capture_output=True,
283
- text=True,
284
- )
285
- fixture_path = clone_directory / LIVE_TEST_FIXTURE_FILENAME
286
- fixture_path.write_text(LIVE_TEST_FIXTURE_CONTENT, encoding="utf-8")
287
- subprocess.run(
288
- ["git", "-C", str(clone_directory), "add", LIVE_TEST_FIXTURE_FILENAME],
289
- check=True,
290
- capture_output=True,
291
- text=True,
292
- )
293
- subprocess.run(
294
- [
295
- "git",
296
- "-C",
297
- str(clone_directory),
298
- "commit",
299
- "-m",
300
- "test: post_audit_thread.py live smoke fixture",
301
- ],
302
- check=True,
303
- capture_output=True,
304
- text=True,
305
- )
306
- head_sha_completion = subprocess.run(
307
- ["git", "-C", str(clone_directory), "rev-parse", "HEAD"],
308
- capture_output=True,
309
- text=True,
310
- encoding="utf-8",
311
- check=True,
312
- )
313
- head_sha = head_sha_completion.stdout.strip()
314
- subprocess.run(
315
- ["git", "-C", str(clone_directory), "push", "-u", "origin", branch_name],
316
- check=True,
317
- capture_output=True,
318
- text=True,
319
- )
320
- body_path = write_pr_body_temporary_file(LIVE_TEST_PR_BODY)
321
- try:
322
- create_completion = subprocess.run(
323
- [
324
- "gh",
325
- "pr",
326
- "create",
327
- "--draft",
328
- "--head",
329
- branch_name,
330
- "--base",
331
- LIVE_TEST_BASE_BRANCH,
332
- "--title",
333
- LIVE_TEST_PR_TITLE,
334
- "--body-file",
335
- str(body_path),
336
- "--repo",
337
- REPO_FULL_NAME,
338
- ],
339
- capture_output=True,
340
- text=True,
341
- encoding="utf-8",
342
- check=True,
343
- cwd=str(clone_directory),
344
- )
345
- finally:
346
- try:
347
- body_path.unlink()
348
- except OSError:
349
- pass
350
- pr_url = create_completion.stdout.strip().splitlines()[-1]
351
- parsed_pr_url = urllib.parse.urlparse(pr_url)
352
- pr_number = int(parsed_pr_url.path.rsplit("/", 1)[-1])
353
- return pr_number, head_sha
354
-
355
-
356
- def close_throwaway_pr(pr_number: int) -> None:
357
- subprocess.run(
358
- [
359
- "gh",
360
- "pr",
361
- "close",
362
- str(pr_number),
363
- "--delete-branch",
364
- "--repo",
365
- REPO_FULL_NAME,
366
- ],
367
- capture_output=True,
368
- text=True,
369
- check=False,
370
- )
371
-
372
-
373
- def remove_local_clone(clone_directory: Path) -> None:
374
- force_remove_directory(clone_directory)
375
-
376
-
377
- def best_effort_delete_remote_branch(branch_name: str) -> None:
378
- try:
379
- subprocess.run(
380
- [
381
- "gh",
382
- "api",
383
- "--method",
384
- "DELETE",
385
- f"repos/{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}/git/refs/heads/{branch_name}",
386
- ],
387
- capture_output=True,
388
- text=True,
389
- check=False,
390
- )
391
- except OSError as deletion_error:
392
- sys.stderr.write(
393
- f"best_effort_delete_remote_branch: could not delete "
394
- f"{branch_name}: {deletion_error}\n"
395
- )
396
-
397
-
398
- def write_findings_json(findings_payload: list[dict[str, Any]]) -> Path:
399
- handle, findings_path_str = tempfile.mkstemp(
400
- suffix=".json", prefix="post-audit-findings-"
401
- )
402
- os.close(handle)
403
- findings_path = Path(findings_path_str)
404
- findings_path.write_text(json.dumps(findings_payload), encoding="utf-8")
405
- return findings_path
406
-
407
-
408
- def invoke_post_audit_thread_script(
409
- pr_number: int,
410
- head_sha: str,
411
- state_argument: str,
412
- findings_json_path: Path,
413
- audit_token: str,
414
- ) -> subprocess.CompletedProcess[str]:
415
- child_environment = dict(os.environ)
416
- child_environment[GH_TOKEN_ENV_VAR_NAME] = audit_token
417
- return subprocess.run(
418
- [
419
- sys.executable,
420
- str(SCRIPT_PATH),
421
- CLI_FLAG_SKILL,
422
- SKILL_BUGTEAM,
423
- CLI_FLAG_OWNER,
424
- LIVE_TEST_OWNER,
425
- CLI_FLAG_REPO,
426
- LIVE_TEST_REPO,
427
- CLI_FLAG_PR_NUMBER,
428
- str(pr_number),
429
- CLI_FLAG_COMMIT,
430
- head_sha,
431
- CLI_FLAG_STATE,
432
- state_argument,
433
- CLI_FLAG_FINDINGS_JSON,
434
- str(findings_json_path),
435
- ],
436
- capture_output=True,
437
- text=True,
438
- encoding="utf-8",
439
- check=False,
440
- env=child_environment,
441
- )
442
-
443
-
444
- STUB_SERVER_HOST = "127.0.0.1"
445
- STUB_SERVER_PORT_DYNAMIC = 0
446
- STUB_RESPONSE_HEADER_CONTENT_TYPE = "Content-Type"
447
- STUB_RESPONSE_HEADER_CONTENT_LENGTH = "Content-Length"
448
- STUB_RESPONSE_CONTENT_TYPE_VALUE = "application/json"
449
- STUB_HTTP_STATUS_BAD_GATEWAY = 502
450
- STUB_HTTP_STATUS_OK = 200
451
- STUB_502_RESPONSE_BODY_BYTES = json.dumps(
452
- {"message": "stub server: simulated transient 502 for retry test"}
453
- ).encode("utf-8")
454
- STUB_200_RESPONSE_BODY_BYTES = json.dumps(
455
- {
456
- "html_url": (
457
- "https://github.com/stub-host/stub-repo/pull/0#pullrequestreview-1"
458
- )
459
- }
460
- ).encode("utf-8")
461
- STUB_SERVER_SHUTDOWN_JOIN_TIMEOUT_SECONDS = 5.0
462
-
463
- FAILURE_COUNT_FOR_RETRY_SUCCESS = 1
464
- TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS = 2
465
- FAILURE_COUNT_FOR_RETRY_EXHAUSTION = MAX_RETRY_ATTEMPTS + 1
466
- TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION = MAX_RETRY_ATTEMPTS + 1
467
-
468
- BACKOFF_TIMING_EPSILON_SECONDS = 0.1
469
- TOTAL_BACKOFF_SECONDS = sum(ALL_RETRY_BACKOFF_SECONDS)
470
-
471
- EXPECTED_RETRY_SUCCESS_ELAPSED_LOWER_BOUND_SECONDS = (
472
- ALL_RETRY_BACKOFF_SECONDS[0] - BACKOFF_TIMING_EPSILON_SECONDS
473
- )
474
- EXPECTED_RETRY_EXHAUSTION_ELAPSED_LOWER_BOUND_SECONDS = (
475
- TOTAL_BACKOFF_SECONDS - BACKOFF_TIMING_EPSILON_SECONDS
476
- )
477
-
478
- EXIT_CODE_SUCCESS = 0
479
-
480
- LAUNCHER_SOURCE_CODE = textwrap.dedent(
481
- """
482
- import sys
483
- sys.path.insert(0, sys.argv[1])
484
- import post_audit_thread
485
- post_audit_thread.GITHUB_API_BASE_URL = sys.argv[2]
486
- sys.exit(post_audit_thread.main(sys.argv[3:]))
487
- """
488
- ).strip()
489
-
490
-
491
- class _StubReviewsServer(http.server.HTTPServer):
492
- """HTTP server that records POST count and serves canned 502/200 responses."""
493
-
494
- request_count: int = 0
495
- failure_count: int = 0
496
- recorded_request_path: str = ""
497
-
498
-
499
- class _StubReviewsHandler(http.server.BaseHTTPRequestHandler):
500
- """Returns 502 for the first ``failure_count`` POSTs, then 200 thereafter.
501
-
502
- State lives on the owning :class:`_StubReviewsServer` instance so the
503
- test can inspect the final request count and the path of the last
504
- received POST after the script exits.
505
- """
506
-
507
- def do_POST(self) -> None:
508
- owning_server = self.server
509
- owning_server.request_count += 1
510
- owning_server.recorded_request_path = self.path
511
- if owning_server.request_count <= owning_server.failure_count:
512
- response_status = STUB_HTTP_STATUS_BAD_GATEWAY
513
- response_body_bytes = STUB_502_RESPONSE_BODY_BYTES
514
- else:
515
- response_status = STUB_HTTP_STATUS_OK
516
- response_body_bytes = STUB_200_RESPONSE_BODY_BYTES
517
- self.send_response(response_status)
518
- self.send_header(
519
- STUB_RESPONSE_HEADER_CONTENT_TYPE, STUB_RESPONSE_CONTENT_TYPE_VALUE
520
- )
521
- self.send_header(
522
- STUB_RESPONSE_HEADER_CONTENT_LENGTH, str(len(response_body_bytes))
523
- )
524
- self.end_headers()
525
- self.wfile.write(response_body_bytes)
526
-
527
- def log_message(self, format: str, *args: Any) -> None:
528
- return
529
-
530
-
531
- def spawn_stub_reviews_server(
532
- failure_count: int,
533
- ) -> tuple[_StubReviewsServer, threading.Thread]:
534
- """Start a localhost stub server returning leading 502s then 200s.
535
-
536
- Args:
537
- failure_count: Number of leading POSTs the stub responds to with
538
- 502. Subsequent POSTs receive a synthetic 200 carrying a fake
539
- ``html_url``. Set above the script's total retry budget to
540
- force the retry-exhaustion path end-to-end.
541
-
542
- Returns:
543
- Tuple of the stub server (bound to a random port on
544
- ``127.0.0.1``) and its serving thread.
545
- """
546
- stub_server = _StubReviewsServer(
547
- (STUB_SERVER_HOST, STUB_SERVER_PORT_DYNAMIC), _StubReviewsHandler
548
- )
549
- stub_server.request_count = 0
550
- stub_server.failure_count = failure_count
551
- stub_server.recorded_request_path = ""
552
- stub_thread = threading.Thread(target=stub_server.serve_forever, daemon=True)
553
- stub_thread.start()
554
- return stub_server, stub_thread
555
-
556
-
557
- def shutdown_stub_reviews_server(
558
- stub_server: _StubReviewsServer,
559
- stub_thread: threading.Thread,
560
- ) -> None:
561
- """Stop the stub server and join its serving thread.
562
-
563
- Args:
564
- stub_server: Server returned by :func:`spawn_stub_reviews_server`.
565
- stub_thread: Serving thread returned by
566
- :func:`spawn_stub_reviews_server`.
567
- """
568
- stub_server.shutdown()
569
- stub_server.server_close()
570
- stub_thread.join(timeout=STUB_SERVER_SHUTDOWN_JOIN_TIMEOUT_SECONDS)
571
-
572
-
573
- def stub_reviews_server_base_url(stub_server: _StubReviewsServer) -> str:
574
- """Return ``http://host:port`` for a bound stub server.
575
-
576
- Args:
577
- stub_server: Server returned by :func:`spawn_stub_reviews_server`.
578
-
579
- Returns:
580
- Base URL suitable for assigning to
581
- ``post_audit_thread.GITHUB_API_BASE_URL`` inside the launcher
582
- subprocess so the script targets the stub instead of api.github.com.
583
- """
584
- host_address, bound_port = stub_server.server_address[:2]
585
- return f"http://{host_address}:{bound_port}"
586
-
587
-
588
- def invoke_post_audit_thread_with_url_override(
589
- pr_number: int,
590
- head_sha: str,
591
- state_argument: str,
592
- findings_json_path: Path,
593
- audit_token: str,
594
- overridden_base_url: str,
595
- ) -> subprocess.CompletedProcess[str]:
596
- """Subprocess-invoke the script with ``GITHUB_API_BASE_URL`` redirected.
597
-
598
- The subprocess runs a short launcher (``LAUNCHER_SOURCE_CODE``) that
599
- imports ``post_audit_thread`` as a module, rebinds its
600
- ``GITHUB_API_BASE_URL`` attribute to ``overridden_base_url``, then
601
- delegates to ``main()``. Lets the retry tests point the script at the
602
- local stub server without modifying production source.
603
-
604
- Args:
605
- pr_number: Throwaway PR number created by ``setUpClass``.
606
- head_sha: HEAD SHA the script attaches the review to.
607
- state_argument: ``CLEAN`` or ``DIRTY``.
608
- findings_json_path: Path to the (empty-list) findings JSON.
609
- audit_token: Token assigned to ``GH_TOKEN`` in the child env.
610
- overridden_base_url: Base URL handed to the launcher (the local
611
- stub server URL).
612
-
613
- Returns:
614
- Completed subprocess result with ``returncode``, ``stdout``, and
615
- ``stderr`` for the test to inspect.
616
- """
617
- child_environment = dict(os.environ)
618
- child_environment[GH_TOKEN_ENV_VAR_NAME] = audit_token
619
- return subprocess.run(
620
- [
621
- sys.executable,
622
- "-c",
623
- LAUNCHER_SOURCE_CODE,
624
- str(SCRIPT_DIRECTORY),
625
- overridden_base_url,
626
- CLI_FLAG_SKILL,
627
- SKILL_BUGTEAM,
628
- CLI_FLAG_OWNER,
629
- LIVE_TEST_OWNER,
630
- CLI_FLAG_REPO,
631
- LIVE_TEST_REPO,
632
- CLI_FLAG_PR_NUMBER,
633
- str(pr_number),
634
- CLI_FLAG_COMMIT,
635
- head_sha,
636
- CLI_FLAG_STATE,
637
- state_argument,
638
- CLI_FLAG_FINDINGS_JSON,
639
- str(findings_json_path),
640
- ],
641
- capture_output=True,
642
- text=True,
643
- encoding="utf-8",
644
- check=False,
645
- env=child_environment,
646
- )
647
-
648
-
649
- class LivePostAuditThreadTests(unittest.TestCase):
650
- """Live smoke tests for post_audit_thread.py against JonEcho/tests.
651
-
652
- Every test in this class reuses a single throwaway draft PR created
653
- in :meth:`setUpClass` and closed in :meth:`tearDownClass`. The CLEAN
654
- and DIRTY tests post real reviews against that PR; the retry tests
655
- redirect the script's HTTP layer to a localhost stub server so the
656
- retry loop runs deterministically without touching api.github.com,
657
- while still consuming the shared PR's number and HEAD SHA.
658
- """
659
-
660
- audit_account_token: str
661
- local_clone_directory: Path
662
- branch_name: str
663
- pr_number: int
664
- head_sha: str
665
-
666
- @classmethod
667
- def setUpClass(cls) -> None:
668
- resolve_gh_auth_token()
669
- cls.audit_account_token = resolve_audit_account_token(
670
- LIVE_TEST_AUDIT_ACCOUNT_NAME
671
- )
672
- unique_suffix = uuid.uuid4().hex[:UUID_SUFFIX_LENGTH]
673
- cls.branch_name = f"{LIVE_TEST_BRANCH_PREFIX}/{unique_suffix}"
674
- cls.local_clone_directory = Path(
675
- tempfile.mkdtemp(prefix=f"post-audit-thread-test-{unique_suffix}-")
676
- )
677
- cls.pr_number = 0
678
- cls.head_sha = ""
679
- try:
680
- cls.pr_number, cls.head_sha = create_throwaway_pr(
681
- cls.local_clone_directory,
682
- cls.branch_name,
683
- )
684
- except Exception:
685
- try:
686
- remove_local_clone(cls.local_clone_directory)
687
- finally:
688
- best_effort_delete_remote_branch(cls.branch_name)
689
- raise
690
-
691
- @classmethod
692
- def tearDownClass(cls) -> None:
693
- try:
694
- if cls.pr_number > 0:
695
- close_throwaway_pr(cls.pr_number)
696
- finally:
697
- try:
698
- remove_local_clone(cls.local_clone_directory)
699
- finally:
700
- best_effort_delete_remote_branch(cls.branch_name)
701
-
702
- def _assert_review_state_for_url(
703
- self, html_url: str, expected_state: str
704
- ) -> dict[str, Any]:
705
- review_id = review_id_from_html_url(html_url)
706
- single_review_api_path = SINGLE_REVIEW_API_PATH_TEMPLATE.format(
707
- owner=LIVE_TEST_OWNER,
708
- repo=LIVE_TEST_REPO,
709
- pr_number=self.pr_number,
710
- review_id=review_id,
711
- )
712
- single_review = gh_api_object_json(single_review_api_path)
713
- self.assertEqual(
714
- single_review.get("state"),
715
- expected_state,
716
- f"unexpected review state for {html_url!r}: {single_review!r}",
717
- )
718
- return single_review
719
-
720
- def _fetch_comments_for_review(self, html_url: str) -> list[dict[str, Any]]:
721
- review_id = review_id_from_html_url(html_url)
722
- review_comments_api_path = SINGLE_REVIEW_COMMENTS_API_PATH_TEMPLATE.format(
723
- owner=LIVE_TEST_OWNER,
724
- repo=LIVE_TEST_REPO,
725
- pr_number=self.pr_number,
726
- review_id=review_id,
727
- )
728
- return gh_api_paginated_json(review_comments_api_path)
729
-
730
- def _run_script_capturing_html_url(
731
- self,
732
- state_argument: str,
733
- findings_payload: list[dict[str, Any]],
734
- ) -> str:
735
- findings_path = write_findings_json(findings_payload)
736
- try:
737
- completion = invoke_post_audit_thread_script(
738
- pr_number=self.pr_number,
739
- head_sha=self.head_sha,
740
- state_argument=state_argument,
741
- findings_json_path=findings_path,
742
- audit_token=self.audit_account_token,
743
- )
744
- finally:
745
- try:
746
- findings_path.unlink()
747
- except OSError:
748
- pass
749
- self.assertEqual(
750
- completion.returncode,
751
- 0,
752
- f"script exited {completion.returncode}; stdout={completion.stdout!r} "
753
- f"stderr={completion.stderr!r}",
754
- )
755
- emitted_html_url = completion.stdout.strip().splitlines()[-1]
756
- self.assertTrue(
757
- emitted_html_url.startswith("https://github.com/"),
758
- f"expected an html_url on stdout, got {emitted_html_url!r}",
759
- )
760
- return emitted_html_url
761
-
762
- def test_clean_state_posts_approved_review_with_empty_comments(self) -> None:
763
- emitted_html_url = self._run_script_capturing_html_url(
764
- state_argument=STATE_CLEAN, findings_payload=[]
765
- )
766
- self._assert_review_state_for_url(emitted_html_url, GH_EVENT_APPROVED)
767
- review_comments = self._fetch_comments_for_review(emitted_html_url)
768
- self.assertEqual(
769
- len(review_comments),
770
- 0,
771
- f"CLEAN state should produce zero inline comments on this review; "
772
- f"saw {len(review_comments)}: {review_comments!r}",
773
- )
774
-
775
- def test_dirty_state_with_three_findings_posts_changes_requested_with_three_inline_threads(
776
- self,
777
- ) -> None:
778
- findings_payload: list[dict[str, Any]] = [
779
- {
780
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
781
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_ONE,
782
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
783
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P0,
784
- JSON_FIELD_DESCRIPTION: "Smoke finding one (heading line).",
785
- JSON_FIELD_FIX_SUMMARY: "Trim the leading marker (smoke fix one).",
786
- },
787
- {
788
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
789
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_TWO,
790
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
791
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P1,
792
- JSON_FIELD_DESCRIPTION: "Smoke finding two (blank-line anchor).",
793
- JSON_FIELD_FIX_SUMMARY: "Collapse the blank separator (smoke fix two).",
794
- },
795
- {
796
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
797
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_THREE,
798
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
799
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P2,
800
- JSON_FIELD_DESCRIPTION: "Smoke finding three (body-line anchor).",
801
- JSON_FIELD_FIX_SUMMARY: "Tighten the description (smoke fix three).",
802
- },
803
- ]
804
- emitted_html_url = self._run_script_capturing_html_url(
805
- state_argument=STATE_DIRTY, findings_payload=findings_payload
806
- )
807
- self._assert_review_state_for_url(
808
- emitted_html_url, GH_EVENT_CHANGES_REQUESTED
809
- )
810
- review_comments = self._fetch_comments_for_review(emitted_html_url)
811
- self.assertEqual(
812
- len(review_comments),
813
- len(findings_payload),
814
- f"DIRTY state should produce one inline comment per finding on "
815
- f"this review; expected {len(findings_payload)} got "
816
- f"{len(review_comments)}: {review_comments!r}",
817
- )
818
-
819
- def _expected_reviews_request_path(self) -> str:
820
- full_endpoint_url = build_reviews_endpoint_url(
821
- LIVE_TEST_OWNER, LIVE_TEST_REPO, self.pr_number
822
- )
823
- parsed_endpoint = urllib.parse.urlparse(full_endpoint_url)
824
- return parsed_endpoint.path
825
-
826
- def _run_retry_simulation_and_measure_elapsed(
827
- self,
828
- failure_count: int,
829
- ) -> tuple[subprocess.CompletedProcess[str], _StubReviewsServer, float]:
830
- findings_path = write_findings_json([])
831
- try:
832
- stub_server, stub_thread = spawn_stub_reviews_server(
833
- failure_count=failure_count
834
- )
835
- try:
836
- overridden_base_url = stub_reviews_server_base_url(stub_server)
837
- start_time = time.perf_counter()
838
- completion = invoke_post_audit_thread_with_url_override(
839
- pr_number=self.pr_number,
840
- head_sha=self.head_sha,
841
- state_argument=STATE_CLEAN,
842
- findings_json_path=findings_path,
843
- audit_token=self.audit_account_token,
844
- overridden_base_url=overridden_base_url,
845
- )
846
- elapsed_seconds = time.perf_counter() - start_time
847
- finally:
848
- shutdown_stub_reviews_server(stub_server, stub_thread)
849
- finally:
850
- try:
851
- findings_path.unlink()
852
- except OSError:
853
- pass
854
- return completion, stub_server, elapsed_seconds
855
-
856
- def test_retry_succeeds_after_one_transient_502_response(self) -> None:
857
- completion, stub_server, elapsed_seconds = (
858
- self._run_retry_simulation_and_measure_elapsed(
859
- failure_count=FAILURE_COUNT_FOR_RETRY_SUCCESS
860
- )
861
- )
862
- self.assertEqual(
863
- completion.returncode,
864
- EXIT_CODE_SUCCESS,
865
- f"retry-success: expected exit {EXIT_CODE_SUCCESS}; got "
866
- f"{completion.returncode}; stdout={completion.stdout!r} "
867
- f"stderr={completion.stderr!r}",
868
- )
869
- self.assertEqual(
870
- stub_server.request_count,
871
- TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS,
872
- f"retry-success: stub should have received exactly "
873
- f"{TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS} POSTs (one 502 + "
874
- f"one 200); got {stub_server.request_count}",
875
- )
876
- expected_request_path = self._expected_reviews_request_path()
877
- self.assertEqual(
878
- stub_server.recorded_request_path,
879
- expected_request_path,
880
- f"retry-success: stub received POST at "
881
- f"{stub_server.recorded_request_path!r}; expected "
882
- f"{expected_request_path!r} per build_reviews_endpoint_url",
883
- )
884
- self.assertGreaterEqual(
885
- elapsed_seconds,
886
- EXPECTED_RETRY_SUCCESS_ELAPSED_LOWER_BOUND_SECONDS,
887
- f"retry-success should observe at least the first 1s backoff; "
888
- f"elapsed={elapsed_seconds:.2f}s",
889
- )
890
-
891
- def test_retry_exhausts_and_exits_two_after_four_consecutive_502_responses(
892
- self,
893
- ) -> None:
894
- completion, stub_server, elapsed_seconds = (
895
- self._run_retry_simulation_and_measure_elapsed(
896
- failure_count=FAILURE_COUNT_FOR_RETRY_EXHAUSTION
897
- )
898
- )
899
- self.assertEqual(
900
- completion.returncode,
901
- EXIT_CODE_RETRY_EXHAUSTED,
902
- f"retry-exhaustion: expected exit "
903
- f"{EXIT_CODE_RETRY_EXHAUSTED}; got "
904
- f"{completion.returncode}; stdout={completion.stdout!r} "
905
- f"stderr={completion.stderr!r}",
906
- )
907
- self.assertEqual(
908
- stub_server.request_count,
909
- TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION,
910
- f"retry-exhaustion: stub should have received exactly "
911
- f"{TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION} POSTs (one "
912
- f"initial plus three retries); got "
913
- f"{stub_server.request_count}",
914
- )
915
- expected_request_path = self._expected_reviews_request_path()
916
- self.assertEqual(
917
- stub_server.recorded_request_path,
918
- expected_request_path,
919
- f"retry-exhaustion: stub received POST at "
920
- f"{stub_server.recorded_request_path!r}; expected "
921
- f"{expected_request_path!r} per build_reviews_endpoint_url",
922
- )
923
- self.assertGreaterEqual(
924
- elapsed_seconds,
925
- EXPECTED_RETRY_EXHAUSTION_ELAPSED_LOWER_BOUND_SECONDS,
926
- f"retry-exhaustion should observe ~21s of backoff "
927
- f"(1s + 4s + 16s); elapsed={elapsed_seconds:.2f}s",
928
- )
929
-
930
- def _isolate_auth_env_vars(self) -> dict[str, str | None]:
931
- all_managed_env_var_names = (
932
- GH_TOKEN_ENV_VAR_NAME,
933
- GITHUB_TOKEN_ENV_VAR_NAME,
934
- BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
935
- )
936
- previous_env_state: dict[str, str | None] = {
937
- each_name: os.environ.get(each_name)
938
- for each_name in all_managed_env_var_names
939
- }
940
- for each_name in all_managed_env_var_names:
941
- os.environ.pop(each_name, None)
942
- return previous_env_state
943
-
944
- def _restore_auth_env_vars(
945
- self, previous_env_state: dict[str, str | None]
946
- ) -> None:
947
- for each_name, prior_value in previous_env_state.items():
948
- if prior_value is None:
949
- os.environ.pop(each_name, None)
950
- else:
951
- os.environ[each_name] = prior_value
952
-
953
- def test_query_active_gh_user_login_matches_gh_api_user_login_field(self) -> None:
954
- active_login = query_active_gh_user_login()
955
- self.assertTrue(
956
- active_login,
957
- "query_active_gh_user_login() returned empty",
958
- )
959
- gh_api_user_response = gh_api_object_json("user")
960
- self.assertEqual(active_login, gh_api_user_response.get("login"))
961
-
962
- def test_query_pull_request_author_login_matches_throwaway_pr_author(self) -> None:
963
- author_login = query_pull_request_author_login(
964
- owner=LIVE_TEST_OWNER,
965
- repo=LIVE_TEST_REPO,
966
- pr_number=self.pr_number,
967
- )
968
- pr_detail_path = f"repos/{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}/pulls/{self.pr_number}"
969
- pr_detail_object = gh_api_object_json(pr_detail_path)
970
- user_field_object = pr_detail_object.get("user")
971
- self.assertIsInstance(user_field_object, dict)
972
- if isinstance(user_field_object, dict):
973
- self.assertEqual(author_login, user_field_object.get("login"))
974
-
975
- def test_list_authenticated_gh_account_logins_includes_active_and_audit_accounts(
976
- self,
977
- ) -> None:
978
- all_logins = list_authenticated_gh_account_logins()
979
- active_login = query_active_gh_user_login()
980
- self.assertIn(active_login, all_logins)
981
- self.assertIn(LIVE_TEST_AUDIT_ACCOUNT_NAME, all_logins)
982
-
983
- def test_fetch_gh_token_for_account_returns_audit_account_cached_token(self) -> None:
984
- fetched_token = fetch_gh_token_for_account(LIVE_TEST_AUDIT_ACCOUNT_NAME)
985
- self.assertEqual(fetched_token, self.audit_account_token)
986
-
987
- def test_resolve_reviewer_token_returns_env_var_when_gh_token_is_set(self) -> None:
988
- sentinel_env_token = "sentinel-gh-token-from-env-var-precedence-test"
989
- previous_env_state = self._isolate_auth_env_vars()
990
- try:
991
- os.environ[GH_TOKEN_ENV_VAR_NAME] = sentinel_env_token
992
- returned_token = resolve_reviewer_token(
993
- owner=LIVE_TEST_OWNER,
994
- repo=LIVE_TEST_REPO,
995
- pr_number=self.pr_number,
996
- )
997
- self.assertEqual(returned_token, sentinel_env_token)
998
- finally:
999
- self._restore_auth_env_vars(previous_env_state)
1000
-
1001
- def test_resolve_reviewer_token_toggles_to_alternate_token_on_self_pr(self) -> None:
1002
- previous_env_state = self._isolate_auth_env_vars()
1003
- try:
1004
- returned_token = resolve_reviewer_token(
1005
- owner=LIVE_TEST_OWNER,
1006
- repo=LIVE_TEST_REPO,
1007
- pr_number=self.pr_number,
1008
- )
1009
- active_login = query_active_gh_user_login()
1010
- pr_author_login = query_pull_request_author_login(
1011
- owner=LIVE_TEST_OWNER,
1012
- repo=LIVE_TEST_REPO,
1013
- pr_number=self.pr_number,
1014
- )
1015
- self.assertEqual(
1016
- active_login.lower(),
1017
- pr_author_login.lower(),
1018
- "throwaway PR author must equal active gh account so the "
1019
- "self-PR toggle branch is exercised",
1020
- )
1021
- all_alternates = [
1022
- each_login
1023
- for each_login in list_authenticated_gh_account_logins()
1024
- if each_login.lower() != pr_author_login.lower()
1025
- ]
1026
- self.assertTrue(
1027
- all_alternates,
1028
- "test setup requires at least one alternate authenticated account",
1029
- )
1030
- expected_first_alternate_token = fetch_gh_token_for_account(
1031
- all_alternates[0]
1032
- )
1033
- self.assertEqual(returned_token, expected_first_alternate_token)
1034
- active_account_token = resolve_gh_auth_token()
1035
- self.assertNotEqual(
1036
- returned_token,
1037
- active_account_token,
1038
- "self-PR toggle must not return the active (author) token",
1039
- )
1040
- finally:
1041
- self._restore_auth_env_vars(previous_env_state)
1042
-
1043
- def test_resolve_reviewer_token_honors_bugteam_reviewer_account_pin(self) -> None:
1044
- previous_env_state = self._isolate_auth_env_vars()
1045
- try:
1046
- pr_author_login = query_pull_request_author_login(
1047
- owner=LIVE_TEST_OWNER,
1048
- repo=LIVE_TEST_REPO,
1049
- pr_number=self.pr_number,
1050
- )
1051
- all_alternates_excluding_pr_author = [
1052
- each_login
1053
- for each_login in list_authenticated_gh_account_logins()
1054
- if each_login.lower() != pr_author_login.lower()
1055
- ]
1056
- self.assertTrue(
1057
- all_alternates_excluding_pr_author,
1058
- "test setup requires at least one authenticated account that "
1059
- "is not the PR author so the pin has a valid target",
1060
- )
1061
- chosen_pin_login = all_alternates_excluding_pr_author[0]
1062
- os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = chosen_pin_login
1063
- returned_token = resolve_reviewer_token(
1064
- owner=LIVE_TEST_OWNER,
1065
- repo=LIVE_TEST_REPO,
1066
- pr_number=self.pr_number,
1067
- )
1068
- expected_pinned_token = fetch_gh_token_for_account(chosen_pin_login)
1069
- self.assertEqual(returned_token, expected_pinned_token)
1070
- finally:
1071
- self._restore_auth_env_vars(previous_env_state)
1072
-
1073
- def test_resolve_reviewer_token_error_excludes_pr_author_from_candidate_set(
1074
- self,
1075
- ) -> None:
1076
- unauthenticated_account_name = "intentionally-not-authenticated-account-zzz"
1077
- previous_env_state = self._isolate_auth_env_vars()
1078
- try:
1079
- os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = (
1080
- unauthenticated_account_name
1081
- )
1082
- with self.assertRaises(UserInputError) as raised_context:
1083
- resolve_reviewer_token(
1084
- owner=LIVE_TEST_OWNER,
1085
- repo=LIVE_TEST_REPO,
1086
- pr_number=self.pr_number,
1087
- )
1088
- error_message_text = str(raised_context.exception)
1089
- self.assertIn(unauthenticated_account_name, error_message_text)
1090
- pr_author_login = query_pull_request_author_login(
1091
- owner=LIVE_TEST_OWNER,
1092
- repo=LIVE_TEST_REPO,
1093
- pr_number=self.pr_number,
1094
- )
1095
- all_alternates_at_call_time = [
1096
- each_login
1097
- for each_login in list_authenticated_gh_account_logins()
1098
- if each_login.lower() != pr_author_login.lower()
1099
- ]
1100
- self.assertIn(
1101
- repr(all_alternates_at_call_time),
1102
- error_message_text,
1103
- "error must show the alternate-reviewer set actually searched",
1104
- )
1105
- self.assertNotIn(
1106
- f"authenticated set [{repr(pr_author_login)}",
1107
- error_message_text,
1108
- "error must not show a set whose head is the excluded PR author",
1109
- )
1110
- finally:
1111
- self._restore_auth_env_vars(previous_env_state)
1112
-
1113
-
1114
- if __name__ == "__main__":
1115
- unittest.main()