claude-dev-env 1.94.0 → 2.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,1054 @@
1
+ """Behavioral tests for the grok worker preflight soft gate."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
13
+ if str(_SCRIPTS_DIR) not in sys.path:
14
+ sys.path.insert(0, str(_SCRIPTS_DIR))
15
+
16
+ import grok_worker_preflight as preflight # noqa: E402
17
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
18
+ AGENTS_SUBDIRECTORY,
19
+ ALL_AGENT_FILENAMES_BY_ROLE,
20
+ ALL_AUTH_FAILURE_SIGNATURES,
21
+ ALL_USAGE_EXHAUSTION_SIGNATURES,
22
+ AUTH_LEADER_SOCKET_FILENAME,
23
+ CLI_PING_FLAG,
24
+ CLI_ROLE_FLAG,
25
+ CLI_RUN_STATE_DIR_FLAG,
26
+ DEFAULT_ROLE,
27
+ EXIT_FALLTHROUGH,
28
+ EXIT_USABLE,
29
+ GROK_BINARY_NAME,
30
+ LEADER_SOCKET_FLAG,
31
+ MANIFEST_FILENAME,
32
+ MODELS_SUBCOMMAND,
33
+ PING_CACHE_CHECKED_AT_KEY,
34
+ PING_CACHE_FILENAME,
35
+ PING_CACHE_IS_OK_KEY,
36
+ PING_LEADER_SOCKET_FILENAME,
37
+ PING_MAX_TURNS,
38
+ PING_PROMPT,
39
+ PING_TTL_SECONDS,
40
+ REASON_CLAUDE_DEV_ENV_CONFIG_MISSING,
41
+ REASON_GROK_AUTH_FAILED,
42
+ REASON_GROK_BINARY_MISSING,
43
+ REASON_GROK_USAGE_EXHAUSTED,
44
+ ROLE_BUGTEAM,
45
+ SINGLE_TURN_FLAG,
46
+ STDOUT_FALLTHROUGH_TEMPLATE,
47
+ STDOUT_OK_LINE,
48
+ UTF8_DECODE_ERRORS,
49
+ UTF8_ENCODING,
50
+ )
51
+
52
+
53
+ def _completed(
54
+ command: list[str],
55
+ returncode: int,
56
+ stdout: str = "",
57
+ stderr: str = "",
58
+ ) -> subprocess.CompletedProcess[str]:
59
+ return subprocess.CompletedProcess(
60
+ args=command, returncode=returncode, stdout=stdout, stderr=stderr
61
+ )
62
+
63
+
64
+ class _Recorder:
65
+ def __init__(self, all_completions: list[subprocess.CompletedProcess[str]]) -> None:
66
+ self.all_completions = list(all_completions)
67
+ self.invocations: list[list[str]] = []
68
+
69
+ def __call__(
70
+ self,
71
+ invocation: list[str],
72
+ **_keyword_arguments: object,
73
+ ) -> subprocess.CompletedProcess[str]:
74
+ self.invocations.append(list(invocation))
75
+ if not self.all_completions:
76
+ raise AssertionError(f"unexpected invocation: {invocation}")
77
+ return self.all_completions.pop(0)
78
+
79
+
80
+ def _write_install_layout(claude_home: Path, *, role: str = ROLE_BUGTEAM) -> None:
81
+ claude_home.mkdir(parents=True, exist_ok=True)
82
+ (claude_home / MANIFEST_FILENAME).write_text("{}", encoding=UTF8_ENCODING)
83
+ agents_directory = claude_home / AGENTS_SUBDIRECTORY
84
+ agents_directory.mkdir(parents=True, exist_ok=True)
85
+ for each_filename in ALL_AGENT_FILENAMES_BY_ROLE[role]:
86
+ (agents_directory / each_filename).write_text(
87
+ f"# {each_filename}\n", encoding=UTF8_ENCODING
88
+ )
89
+
90
+
91
+ def _install_ok_static_seams(
92
+ monkeypatch: pytest.MonkeyPatch,
93
+ claude_home: Path,
94
+ recorder: _Recorder,
95
+ ) -> None:
96
+ _write_install_layout(claude_home)
97
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
98
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
99
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
100
+
101
+
102
+ def test_binary_missing_falls_through_with_reason(
103
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
104
+ ) -> None:
105
+ claude_home = tmp_path / "claude"
106
+ run_state_directory = tmp_path / "run"
107
+ run_state_directory.mkdir()
108
+ _write_install_layout(claude_home)
109
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
110
+ monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
111
+ monkeypatch.setattr(
112
+ preflight,
113
+ "preflight_subprocess_runner",
114
+ _Recorder([_completed([GROK_BINARY_NAME], 0)]),
115
+ )
116
+
117
+ exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
118
+
119
+ captured = capsys.readouterr()
120
+ assert exit_code == EXIT_FALLTHROUGH
121
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
122
+ reason=REASON_GROK_BINARY_MISSING
123
+ )
124
+
125
+
126
+ def test_auth_failed_falls_through_with_reason(
127
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
128
+ ) -> None:
129
+ claude_home = tmp_path / "claude"
130
+ run_state_directory = tmp_path / "run"
131
+ run_state_directory.mkdir()
132
+ recorder = _Recorder(
133
+ [
134
+ _completed(
135
+ [GROK_BINARY_NAME, MODELS_SUBCOMMAND],
136
+ 1,
137
+ stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
138
+ )
139
+ ]
140
+ )
141
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
142
+
143
+ exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
144
+
145
+ captured = capsys.readouterr()
146
+ assert exit_code == EXIT_FALLTHROUGH
147
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
148
+ reason=REASON_GROK_AUTH_FAILED
149
+ )
150
+ assert recorder.invocations[0][0] == GROK_BINARY_NAME
151
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
152
+ assert LEADER_SOCKET_FLAG in recorder.invocations[0]
153
+ leader_socket_path = Path(
154
+ recorder.invocations[0][recorder.invocations[0].index(LEADER_SOCKET_FLAG) + 1]
155
+ )
156
+ assert leader_socket_path.name == AUTH_LEADER_SOCKET_FILENAME
157
+ assert leader_socket_path.parent == run_state_directory
158
+
159
+
160
+ def test_config_missing_manifest_falls_through(
161
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
162
+ ) -> None:
163
+ claude_home = tmp_path / "claude"
164
+ claude_home.mkdir()
165
+ run_state_directory = tmp_path / "run"
166
+ run_state_directory.mkdir()
167
+ recorder = _Recorder(
168
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
169
+ )
170
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
171
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
172
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
173
+
174
+ exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
175
+
176
+ captured = capsys.readouterr()
177
+ assert exit_code == EXIT_FALLTHROUGH
178
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
179
+ reason=REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
180
+ )
181
+
182
+
183
+ def test_config_missing_role_agent_falls_through(
184
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
185
+ ) -> None:
186
+ claude_home = tmp_path / "claude"
187
+ claude_home.mkdir()
188
+ (claude_home / MANIFEST_FILENAME).write_text("{}", encoding=UTF8_ENCODING)
189
+ agents_directory = claude_home / AGENTS_SUBDIRECTORY
190
+ agents_directory.mkdir()
191
+ (agents_directory / "code-quality-agent.md").write_text(
192
+ "# present\n", encoding=UTF8_ENCODING
193
+ )
194
+ run_state_directory = tmp_path / "run"
195
+ run_state_directory.mkdir()
196
+ recorder = _Recorder(
197
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
198
+ )
199
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
200
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
201
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
202
+
203
+ exit_code = preflight.main(
204
+ [
205
+ CLI_ROLE_FLAG,
206
+ ROLE_BUGTEAM,
207
+ CLI_RUN_STATE_DIR_FLAG,
208
+ str(run_state_directory),
209
+ ]
210
+ )
211
+
212
+ captured = capsys.readouterr()
213
+ assert exit_code == EXIT_FALLTHROUGH
214
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
215
+ reason=REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
216
+ )
217
+
218
+
219
+ def test_ok_path_without_ping(
220
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
221
+ ) -> None:
222
+ claude_home = tmp_path / "claude"
223
+ run_state_directory = tmp_path / "run"
224
+ run_state_directory.mkdir()
225
+ recorder = _Recorder(
226
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
227
+ )
228
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
229
+
230
+ exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
231
+
232
+ captured = capsys.readouterr()
233
+ assert exit_code == EXIT_USABLE
234
+ assert captured.out.strip() == STDOUT_OK_LINE
235
+ assert len(recorder.invocations) == 1
236
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
237
+
238
+
239
+ def test_ping_cache_miss_invokes_live_call_and_writes_cache(
240
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
241
+ ) -> None:
242
+ claude_home = tmp_path / "claude"
243
+ run_state_directory = tmp_path / "run"
244
+ run_state_directory.mkdir()
245
+ fixed_now = 1_700_000_000.0
246
+ recorder = _Recorder(
247
+ [
248
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
249
+ _completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
250
+ ]
251
+ )
252
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
253
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
254
+
255
+ exit_code = preflight.main(
256
+ [
257
+ CLI_PING_FLAG,
258
+ CLI_RUN_STATE_DIR_FLAG,
259
+ str(run_state_directory),
260
+ ]
261
+ )
262
+
263
+ captured = capsys.readouterr()
264
+ assert exit_code == EXIT_USABLE
265
+ assert captured.out.strip() == STDOUT_OK_LINE
266
+ assert len(recorder.invocations) == 2
267
+ ping_invocation = recorder.invocations[1]
268
+ assert SINGLE_TURN_FLAG in ping_invocation
269
+ assert PING_PROMPT in ping_invocation
270
+ assert PING_MAX_TURNS in ping_invocation
271
+ assert LEADER_SOCKET_FLAG in ping_invocation
272
+ ping_socket = Path(ping_invocation[ping_invocation.index(LEADER_SOCKET_FLAG) + 1])
273
+ assert ping_socket.name == PING_LEADER_SOCKET_FILENAME
274
+ cache_path = run_state_directory / PING_CACHE_FILENAME
275
+ all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
276
+ assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
277
+ assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
278
+
279
+
280
+ def test_ping_cache_hit_skips_live_call(
281
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
282
+ ) -> None:
283
+ claude_home = tmp_path / "claude"
284
+ run_state_directory = tmp_path / "run"
285
+ run_state_directory.mkdir()
286
+ fixed_now = 1_700_000_100.0
287
+ cache_path = run_state_directory / PING_CACHE_FILENAME
288
+ cache_path.write_text(
289
+ json.dumps(
290
+ {
291
+ PING_CACHE_CHECKED_AT_KEY: fixed_now - 10.0,
292
+ PING_CACHE_IS_OK_KEY: True,
293
+ }
294
+ ),
295
+ encoding=UTF8_ENCODING,
296
+ )
297
+ recorder = _Recorder(
298
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
299
+ )
300
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
301
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
302
+
303
+ exit_code = preflight.main(
304
+ [
305
+ CLI_PING_FLAG,
306
+ CLI_RUN_STATE_DIR_FLAG,
307
+ str(run_state_directory),
308
+ ]
309
+ )
310
+
311
+ captured = capsys.readouterr()
312
+ assert exit_code == EXIT_USABLE
313
+ assert captured.out.strip() == STDOUT_OK_LINE
314
+ assert len(recorder.invocations) == 1
315
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
316
+
317
+
318
+ def test_ping_cache_ttl_expired_reinvokes_live_call(
319
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
320
+ ) -> None:
321
+ claude_home = tmp_path / "claude"
322
+ run_state_directory = tmp_path / "run"
323
+ run_state_directory.mkdir()
324
+ fixed_now = 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
325
+ cache_path = run_state_directory / PING_CACHE_FILENAME
326
+ cache_path.write_text(
327
+ json.dumps(
328
+ {
329
+ PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
330
+ PING_CACHE_IS_OK_KEY: True,
331
+ }
332
+ ),
333
+ encoding=UTF8_ENCODING,
334
+ )
335
+ recorder = _Recorder(
336
+ [
337
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
338
+ _completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
339
+ ]
340
+ )
341
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
342
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
343
+
344
+ exit_code = preflight.main(
345
+ [
346
+ CLI_PING_FLAG,
347
+ CLI_RUN_STATE_DIR_FLAG,
348
+ str(run_state_directory),
349
+ ]
350
+ )
351
+
352
+ captured = capsys.readouterr()
353
+ assert exit_code == EXIT_USABLE
354
+ assert captured.out.strip() == STDOUT_OK_LINE
355
+ assert len(recorder.invocations) == 2
356
+
357
+
358
+ def test_auth_failure_invalidates_ping_cache(
359
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
360
+ ) -> None:
361
+ claude_home = tmp_path / "claude"
362
+ run_state_directory = tmp_path / "run"
363
+ run_state_directory.mkdir()
364
+ cache_path = run_state_directory / PING_CACHE_FILENAME
365
+ cache_path.write_text(
366
+ json.dumps(
367
+ {
368
+ PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
369
+ PING_CACHE_IS_OK_KEY: True,
370
+ }
371
+ ),
372
+ encoding=UTF8_ENCODING,
373
+ )
374
+ recorder = _Recorder(
375
+ [
376
+ _completed(
377
+ [GROK_BINARY_NAME, MODELS_SUBCOMMAND],
378
+ 1,
379
+ stderr="not logged in",
380
+ )
381
+ ]
382
+ )
383
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
384
+
385
+ exit_code = preflight.main(
386
+ [
387
+ CLI_PING_FLAG,
388
+ CLI_RUN_STATE_DIR_FLAG,
389
+ str(run_state_directory),
390
+ ]
391
+ )
392
+
393
+ captured = capsys.readouterr()
394
+ assert exit_code == EXIT_FALLTHROUGH
395
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
396
+ reason=REASON_GROK_AUTH_FAILED
397
+ )
398
+ assert not cache_path.exists()
399
+
400
+
401
+ def test_ping_usage_exhaustion_is_distinct_reason(
402
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
403
+ ) -> None:
404
+ claude_home = tmp_path / "claude"
405
+ run_state_directory = tmp_path / "run"
406
+ run_state_directory.mkdir()
407
+ recorder = _Recorder(
408
+ [
409
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
410
+ _completed(
411
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
412
+ 1,
413
+ stderr=ALL_USAGE_EXHAUSTION_SIGNATURES[0],
414
+ ),
415
+ ]
416
+ )
417
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
418
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
419
+
420
+ exit_code = preflight.main(
421
+ [
422
+ CLI_PING_FLAG,
423
+ CLI_RUN_STATE_DIR_FLAG,
424
+ str(run_state_directory),
425
+ ]
426
+ )
427
+
428
+ captured = capsys.readouterr()
429
+ assert exit_code == EXIT_FALLTHROUGH
430
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
431
+ reason=REASON_GROK_USAGE_EXHAUSTED
432
+ )
433
+ assert not (run_state_directory / PING_CACHE_FILENAME).exists()
434
+
435
+
436
+ def test_ping_auth_failure_is_auth_reason_not_usage(
437
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
438
+ ) -> None:
439
+ claude_home = tmp_path / "claude"
440
+ run_state_directory = tmp_path / "run"
441
+ run_state_directory.mkdir()
442
+ recorder = _Recorder(
443
+ [
444
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
445
+ _completed(
446
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
447
+ 1,
448
+ stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
449
+ ),
450
+ ]
451
+ )
452
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
453
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
454
+
455
+ exit_code = preflight.main(
456
+ [
457
+ CLI_PING_FLAG,
458
+ CLI_RUN_STATE_DIR_FLAG,
459
+ str(run_state_directory),
460
+ ]
461
+ )
462
+
463
+ captured = capsys.readouterr()
464
+ assert exit_code == EXIT_FALLTHROUGH
465
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
466
+ reason=REASON_GROK_AUTH_FAILED
467
+ )
468
+
469
+
470
+ def test_reason_strings_are_pinned_to_constants() -> None:
471
+ assert REASON_GROK_BINARY_MISSING == "grok_binary_missing"
472
+ assert REASON_GROK_AUTH_FAILED == "grok_auth_failed"
473
+ assert REASON_CLAUDE_DEV_ENV_CONFIG_MISSING == "claude_dev_env_config_missing"
474
+ assert REASON_GROK_USAGE_EXHAUSTED == "grok_usage_exhausted"
475
+ assert STDOUT_OK_LINE == "grok_preflight: ok"
476
+ assert "reason=" in STDOUT_FALLTHROUGH_TEMPLATE
477
+ assert DEFAULT_ROLE == ROLE_BUGTEAM
478
+
479
+
480
+ def test_claude_config_home_points_at_user_claude_home() -> None:
481
+ config_home = preflight.claude_config_home()
482
+ assert config_home.name == ".claude"
483
+
484
+
485
+ def test_run_preflight_binary_missing_returns_fallthrough(
486
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
487
+ ) -> None:
488
+ claude_home = tmp_path / "claude"
489
+ run_state_directory = tmp_path / "run"
490
+ run_state_directory.mkdir()
491
+ _write_install_layout(claude_home)
492
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
493
+ monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
494
+
495
+ outcome = preflight.run_preflight(
496
+ role=ROLE_BUGTEAM,
497
+ should_ping=False,
498
+ run_state_directory=run_state_directory,
499
+ )
500
+
501
+ assert outcome.is_usable is False
502
+ assert outcome.reason == REASON_GROK_BINARY_MISSING
503
+
504
+
505
+ def test_run_preflight_auth_failure_invalidates_cache_and_returns_reason(
506
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
507
+ ) -> None:
508
+ claude_home = tmp_path / "claude"
509
+ run_state_directory = tmp_path / "run"
510
+ run_state_directory.mkdir()
511
+ cache_path = run_state_directory / PING_CACHE_FILENAME
512
+ cache_path.write_text(
513
+ json.dumps(
514
+ {
515
+ PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
516
+ PING_CACHE_IS_OK_KEY: True,
517
+ }
518
+ ),
519
+ encoding=UTF8_ENCODING,
520
+ )
521
+ recorder = _Recorder(
522
+ [
523
+ _completed(
524
+ [GROK_BINARY_NAME, MODELS_SUBCOMMAND],
525
+ 1,
526
+ stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
527
+ )
528
+ ]
529
+ )
530
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
531
+
532
+ outcome = preflight.run_preflight(
533
+ role=ROLE_BUGTEAM,
534
+ should_ping=False,
535
+ run_state_directory=run_state_directory,
536
+ )
537
+
538
+ assert outcome.is_usable is False
539
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
540
+ assert not cache_path.exists()
541
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
542
+ leader_socket_path = Path(
543
+ recorder.invocations[0][recorder.invocations[0].index(LEADER_SOCKET_FLAG) + 1]
544
+ )
545
+ assert leader_socket_path.name == AUTH_LEADER_SOCKET_FILENAME
546
+
547
+
548
+ def test_run_preflight_config_missing_returns_fallthrough(
549
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
550
+ ) -> None:
551
+ claude_home = tmp_path / "claude"
552
+ claude_home.mkdir()
553
+ run_state_directory = tmp_path / "run"
554
+ run_state_directory.mkdir()
555
+ recorder = _Recorder(
556
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
557
+ )
558
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
559
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
560
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", recorder)
561
+
562
+ outcome = preflight.run_preflight(
563
+ role=ROLE_BUGTEAM,
564
+ should_ping=False,
565
+ run_state_directory=run_state_directory,
566
+ )
567
+
568
+ assert outcome.is_usable is False
569
+ assert outcome.reason == REASON_CLAUDE_DEV_ENV_CONFIG_MISSING
570
+
571
+
572
+ def test_run_preflight_ok_without_ping_returns_usable(
573
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
574
+ ) -> None:
575
+ claude_home = tmp_path / "claude"
576
+ run_state_directory = tmp_path / "run"
577
+ run_state_directory.mkdir()
578
+ recorder = _Recorder(
579
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
580
+ )
581
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
582
+
583
+ outcome = preflight.run_preflight(
584
+ role=ROLE_BUGTEAM,
585
+ should_ping=False,
586
+ run_state_directory=run_state_directory,
587
+ )
588
+
589
+ assert outcome.is_usable is True
590
+ assert outcome.reason is None
591
+ assert len(recorder.invocations) == 1
592
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
593
+
594
+
595
+ def test_run_preflight_ping_cache_miss_writes_cache_and_returns_usable(
596
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
597
+ ) -> None:
598
+ claude_home = tmp_path / "claude"
599
+ run_state_directory = tmp_path / "run"
600
+ run_state_directory.mkdir()
601
+ fixed_now = 1_700_000_000.0
602
+ recorder = _Recorder(
603
+ [
604
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
605
+ _completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
606
+ ]
607
+ )
608
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
609
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
610
+
611
+ outcome = preflight.run_preflight(
612
+ role=ROLE_BUGTEAM,
613
+ should_ping=True,
614
+ run_state_directory=run_state_directory,
615
+ )
616
+
617
+ assert outcome.is_usable is True
618
+ assert outcome.reason is None
619
+ assert len(recorder.invocations) == 2
620
+ ping_invocation = recorder.invocations[1]
621
+ assert SINGLE_TURN_FLAG in ping_invocation
622
+ assert PING_PROMPT in ping_invocation
623
+ ping_socket = Path(ping_invocation[ping_invocation.index(LEADER_SOCKET_FLAG) + 1])
624
+ assert ping_socket.name == PING_LEADER_SOCKET_FILENAME
625
+ cache_path = run_state_directory / PING_CACHE_FILENAME
626
+ all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
627
+ assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
628
+ assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
629
+
630
+
631
+ def test_run_preflight_ping_cache_hit_skips_live_call(
632
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
633
+ ) -> None:
634
+ claude_home = tmp_path / "claude"
635
+ run_state_directory = tmp_path / "run"
636
+ run_state_directory.mkdir()
637
+ fixed_now = 1_700_000_100.0
638
+ cache_path = run_state_directory / PING_CACHE_FILENAME
639
+ cache_path.write_text(
640
+ json.dumps(
641
+ {
642
+ PING_CACHE_CHECKED_AT_KEY: fixed_now - 10.0,
643
+ PING_CACHE_IS_OK_KEY: True,
644
+ }
645
+ ),
646
+ encoding=UTF8_ENCODING,
647
+ )
648
+ recorder = _Recorder(
649
+ [_completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in")]
650
+ )
651
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
652
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
653
+
654
+ outcome = preflight.run_preflight(
655
+ role=ROLE_BUGTEAM,
656
+ should_ping=True,
657
+ run_state_directory=run_state_directory,
658
+ )
659
+
660
+ assert outcome.is_usable is True
661
+ assert outcome.reason is None
662
+ assert len(recorder.invocations) == 1
663
+ assert MODELS_SUBCOMMAND in recorder.invocations[0]
664
+
665
+
666
+ def test_run_preflight_ping_usage_exhaustion_returns_distinct_reason(
667
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
668
+ ) -> None:
669
+ claude_home = tmp_path / "claude"
670
+ run_state_directory = tmp_path / "run"
671
+ run_state_directory.mkdir()
672
+ recorder = _Recorder(
673
+ [
674
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
675
+ _completed(
676
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
677
+ 1,
678
+ stderr=ALL_USAGE_EXHAUSTION_SIGNATURES[0],
679
+ ),
680
+ ]
681
+ )
682
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
683
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
684
+
685
+ outcome = preflight.run_preflight(
686
+ role=ROLE_BUGTEAM,
687
+ should_ping=True,
688
+ run_state_directory=run_state_directory,
689
+ )
690
+
691
+ assert outcome.is_usable is False
692
+ assert outcome.reason == REASON_GROK_USAGE_EXHAUSTED
693
+ assert not (run_state_directory / PING_CACHE_FILENAME).exists()
694
+
695
+
696
+ def test_run_preflight_ping_auth_failure_invalidates_cache(
697
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
698
+ ) -> None:
699
+ claude_home = tmp_path / "claude"
700
+ run_state_directory = tmp_path / "run"
701
+ run_state_directory.mkdir()
702
+ cache_path = run_state_directory / PING_CACHE_FILENAME
703
+ cache_path.write_text(
704
+ json.dumps(
705
+ {
706
+ PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
707
+ PING_CACHE_IS_OK_KEY: True,
708
+ }
709
+ ),
710
+ encoding=UTF8_ENCODING,
711
+ )
712
+ recorder = _Recorder(
713
+ [
714
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
715
+ _completed(
716
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
717
+ 1,
718
+ stderr=ALL_AUTH_FAILURE_SIGNATURES[0],
719
+ ),
720
+ ]
721
+ )
722
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
723
+ monkeypatch.setattr(
724
+ preflight, "preflight_time", lambda: 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
725
+ )
726
+
727
+ outcome = preflight.run_preflight(
728
+ role=ROLE_BUGTEAM,
729
+ should_ping=True,
730
+ run_state_directory=run_state_directory,
731
+ )
732
+
733
+ assert outcome.is_usable is False
734
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
735
+ assert not cache_path.exists()
736
+
737
+
738
+ def test_run_preflight_ping_missing_binary_invalidates_cache(
739
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
740
+ ) -> None:
741
+ claude_home = tmp_path / "claude"
742
+ run_state_directory = tmp_path / "run"
743
+ run_state_directory.mkdir()
744
+ cache_path = run_state_directory / PING_CACHE_FILENAME
745
+ cache_path.write_text(
746
+ json.dumps(
747
+ {
748
+ PING_CACHE_CHECKED_AT_KEY: 1_700_000_000.0,
749
+ PING_CACHE_IS_OK_KEY: True,
750
+ }
751
+ ),
752
+ encoding=UTF8_ENCODING,
753
+ )
754
+ all_completions = [
755
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
756
+ ]
757
+
758
+ def _raise_on_ping(
759
+ invocation: list[str], **_keyword_arguments: object
760
+ ) -> subprocess.CompletedProcess[str]:
761
+ if SINGLE_TURN_FLAG in invocation:
762
+ raise FileNotFoundError(GROK_BINARY_NAME)
763
+ if not all_completions:
764
+ raise AssertionError(f"unexpected invocation: {invocation}")
765
+ return all_completions.pop(0)
766
+
767
+ _write_install_layout(claude_home)
768
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
769
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
770
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", _raise_on_ping)
771
+ monkeypatch.setattr(
772
+ preflight, "preflight_time", lambda: 1_700_000_000.0 + PING_TTL_SECONDS + 1.0
773
+ )
774
+
775
+ outcome = preflight.run_preflight(
776
+ role=ROLE_BUGTEAM,
777
+ should_ping=True,
778
+ run_state_directory=run_state_directory,
779
+ )
780
+
781
+ assert outcome.is_usable is False
782
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
783
+ assert not cache_path.exists()
784
+
785
+
786
+ def test_main_uncreatable_run_state_dir_falls_through_without_crashing(
787
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
788
+ ) -> None:
789
+ """An uncreatable run-state dir yields a parseable fallthrough line, not a crash."""
790
+ blocker_file = tmp_path / "blocker"
791
+ blocker_file.write_text("x", encoding=UTF8_ENCODING)
792
+ run_state_directory = blocker_file / "run"
793
+ monkeypatch.setattr(preflight, "preflight_which", lambda _name: None)
794
+
795
+ exit_code = preflight.main([CLI_RUN_STATE_DIR_FLAG, str(run_state_directory)])
796
+
797
+ captured = capsys.readouterr()
798
+ assert exit_code == EXIT_FALLTHROUGH
799
+ assert captured.out.strip() == STDOUT_FALLTHROUGH_TEMPLATE.format(
800
+ reason=REASON_GROK_BINARY_MISSING
801
+ )
802
+
803
+
804
+ def test_auth_returncode_zero_with_none_stdout_falls_through(
805
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
806
+ ) -> None:
807
+ """returncode 0 with undecoded None streams must not report usable."""
808
+ claude_home = tmp_path / "claude"
809
+ run_state_directory = tmp_path / "run"
810
+ run_state_directory.mkdir()
811
+ none_stream_completion = subprocess.CompletedProcess(
812
+ args=[GROK_BINARY_NAME, MODELS_SUBCOMMAND],
813
+ returncode=0,
814
+ stdout=None,
815
+ stderr=None,
816
+ )
817
+ recorder = _Recorder([none_stream_completion])
818
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
819
+
820
+ outcome = preflight.run_preflight(
821
+ role=ROLE_BUGTEAM,
822
+ should_ping=False,
823
+ run_state_directory=run_state_directory,
824
+ )
825
+
826
+ assert outcome.is_usable is False
827
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
828
+
829
+
830
+ def test_run_grok_command_passes_utf8_decode_errors_replace(
831
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
832
+ ) -> None:
833
+ """Subprocess text mode must use errors=replace so invalid UTF-8 never yields None."""
834
+ captured_keyword_arguments: dict[str, object] = {}
835
+
836
+ def _capture_runner(
837
+ invocation: list[str], **keyword_arguments: object
838
+ ) -> subprocess.CompletedProcess[str]:
839
+ captured_keyword_arguments.update(keyword_arguments)
840
+ return _completed(invocation, 0, stdout="logged in")
841
+
842
+ claude_home = tmp_path / "claude"
843
+ run_state_directory = tmp_path / "run"
844
+ run_state_directory.mkdir()
845
+ _write_install_layout(claude_home)
846
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
847
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
848
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", _capture_runner)
849
+
850
+ outcome = preflight.run_preflight(
851
+ role=ROLE_BUGTEAM,
852
+ should_ping=False,
853
+ run_state_directory=run_state_directory,
854
+ )
855
+
856
+ assert outcome.is_usable is True
857
+ assert captured_keyword_arguments.get("encoding") == UTF8_ENCODING
858
+ assert captured_keyword_arguments.get("errors") == UTF8_DECODE_ERRORS
859
+ assert UTF8_DECODE_ERRORS == "replace"
860
+
861
+
862
+ def test_run_preflight_mkdirs_missing_run_state_directory_for_cache(
863
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
864
+ ) -> None:
865
+ """Library path creates the run state directory so ping cache write succeeds."""
866
+ claude_home = tmp_path / "claude"
867
+ run_state_directory = tmp_path / "missing-run"
868
+ assert not run_state_directory.exists()
869
+ fixed_now = 1_700_000_000.0
870
+ recorder = _Recorder(
871
+ [
872
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
873
+ _completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
874
+ ]
875
+ )
876
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
877
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
878
+
879
+ outcome = preflight.run_preflight(
880
+ role=ROLE_BUGTEAM,
881
+ should_ping=True,
882
+ run_state_directory=run_state_directory,
883
+ )
884
+
885
+ assert outcome.is_usable is True
886
+ assert outcome.reason is None
887
+ assert run_state_directory.is_dir()
888
+ cache_path = run_state_directory / PING_CACHE_FILENAME
889
+ assert cache_path.is_file()
890
+ all_cache_payload = json.loads(cache_path.read_text(encoding=UTF8_ENCODING))
891
+ assert all_cache_payload[PING_CACHE_IS_OK_KEY] is True
892
+ assert all_cache_payload[PING_CACHE_CHECKED_AT_KEY] == fixed_now
893
+
894
+
895
+ def test_ping_cache_write_oserror_still_reports_usable(
896
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
897
+ ) -> None:
898
+ """Soft-gate stays usable when the successful ping cache write fails."""
899
+ claude_home = tmp_path / "claude"
900
+ run_state_directory = tmp_path / "run"
901
+ run_state_directory.mkdir()
902
+ fixed_now = 1_700_000_000.0
903
+ recorder = _Recorder(
904
+ [
905
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
906
+ _completed([GROK_BINARY_NAME, SINGLE_TURN_FLAG], 0, stdout="ok"),
907
+ ]
908
+ )
909
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
910
+ monkeypatch.setattr(preflight, "preflight_time", lambda: fixed_now)
911
+
912
+ def _raise_oserror_on_write(
913
+ self: Path, *_arguments: object, **_keywords: object
914
+ ) -> None:
915
+ raise OSError("simulated cache write failure")
916
+
917
+ monkeypatch.setattr(Path, "write_text", _raise_oserror_on_write)
918
+
919
+ outcome = preflight.run_preflight(
920
+ role=ROLE_BUGTEAM,
921
+ should_ping=True,
922
+ run_state_directory=run_state_directory,
923
+ )
924
+
925
+ assert outcome.is_usable is True
926
+ assert outcome.reason is None
927
+ assert not (run_state_directory / PING_CACHE_FILENAME).exists()
928
+
929
+
930
+ def test_auth_timeout_falls_through_as_grok_auth_failed(
931
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
932
+ ) -> None:
933
+ """TimeoutExpired maps to the catch-all non-usage bucket grok_auth_failed."""
934
+ claude_home = tmp_path / "claude"
935
+ run_state_directory = tmp_path / "run"
936
+ run_state_directory.mkdir()
937
+
938
+ def _raise_timeout(
939
+ invocation: list[str], **_keyword_arguments: object
940
+ ) -> subprocess.CompletedProcess[str]:
941
+ raise subprocess.TimeoutExpired(cmd=invocation, timeout=1)
942
+
943
+ _write_install_layout(claude_home)
944
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
945
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
946
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", _raise_timeout)
947
+
948
+ outcome = preflight.run_preflight(
949
+ role=ROLE_BUGTEAM,
950
+ should_ping=False,
951
+ run_state_directory=run_state_directory,
952
+ )
953
+
954
+ assert outcome.is_usable is False
955
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
956
+
957
+
958
+ def test_ping_timeout_falls_through_as_grok_auth_failed(
959
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
960
+ ) -> None:
961
+ """Ping TimeoutExpired also uses the catch-all non-usage bucket."""
962
+ claude_home = tmp_path / "claude"
963
+ run_state_directory = tmp_path / "run"
964
+ run_state_directory.mkdir()
965
+ all_completions = [
966
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
967
+ ]
968
+
969
+ def _timeout_on_ping(
970
+ invocation: list[str], **_keyword_arguments: object
971
+ ) -> subprocess.CompletedProcess[str]:
972
+ if SINGLE_TURN_FLAG in invocation:
973
+ raise subprocess.TimeoutExpired(cmd=invocation, timeout=1)
974
+ if not all_completions:
975
+ raise AssertionError(f"unexpected invocation: {invocation}")
976
+ return all_completions.pop(0)
977
+
978
+ _write_install_layout(claude_home)
979
+ monkeypatch.setattr(preflight, "claude_config_home", lambda: claude_home)
980
+ monkeypatch.setattr(preflight, "preflight_which", lambda name: f"/fake/bin/{name}")
981
+ monkeypatch.setattr(preflight, "preflight_subprocess_runner", _timeout_on_ping)
982
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
983
+
984
+ outcome = preflight.run_preflight(
985
+ role=ROLE_BUGTEAM,
986
+ should_ping=True,
987
+ run_state_directory=run_state_directory,
988
+ )
989
+
990
+ assert outcome.is_usable is False
991
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
992
+
993
+
994
+ def test_ping_dual_match_prefers_auth_over_usage(
995
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
996
+ ) -> None:
997
+ claude_home = tmp_path / "claude"
998
+ run_state_directory = tmp_path / "run"
999
+ run_state_directory.mkdir()
1000
+ dual_match_stderr = "HTTP 401 rate limit exceeded: unauthorized"
1001
+ recorder = _Recorder(
1002
+ [
1003
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
1004
+ _completed(
1005
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
1006
+ 1,
1007
+ stderr=dual_match_stderr,
1008
+ ),
1009
+ ]
1010
+ )
1011
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
1012
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
1013
+
1014
+ outcome = preflight.run_preflight(
1015
+ role=ROLE_BUGTEAM,
1016
+ should_ping=True,
1017
+ run_state_directory=run_state_directory,
1018
+ )
1019
+
1020
+ assert outcome.is_usable is False
1021
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
1022
+
1023
+
1024
+ def test_ping_incidental_credit_and_port_429_are_not_usage_exhaustion(
1025
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1026
+ ) -> None:
1027
+ claude_home = tmp_path / "claude"
1028
+ run_state_directory = tmp_path / "run"
1029
+ run_state_directory.mkdir()
1030
+ incidental_stderr = (
1031
+ "accreditation failed while probing; port 429 refused; quota of tools open"
1032
+ )
1033
+ recorder = _Recorder(
1034
+ [
1035
+ _completed([GROK_BINARY_NAME, MODELS_SUBCOMMAND], 0, stdout="logged in"),
1036
+ _completed(
1037
+ [GROK_BINARY_NAME, SINGLE_TURN_FLAG],
1038
+ 1,
1039
+ stderr=incidental_stderr,
1040
+ ),
1041
+ ]
1042
+ )
1043
+ _install_ok_static_seams(monkeypatch, claude_home, recorder)
1044
+ monkeypatch.setattr(preflight, "preflight_time", lambda: 1_700_000_000.0)
1045
+
1046
+ outcome = preflight.run_preflight(
1047
+ role=ROLE_BUGTEAM,
1048
+ should_ping=True,
1049
+ run_state_directory=run_state_directory,
1050
+ )
1051
+
1052
+ assert outcome.is_usable is False
1053
+ assert outcome.reason == REASON_GROK_AUTH_FAILED
1054
+ assert outcome.reason != REASON_GROK_USAGE_EXHAUSTED