claude-dev-env 1.95.0 → 2.0.1

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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  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/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,678 @@
1
+ """Tests for the Codex weekly-usage probe CLI.
2
+
3
+ Fixtures note the binary version they were captured against:
4
+ codex-cli 0.144.3 (account/rateLimits/read via ``codex app-server``).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import importlib.util
10
+ import json
11
+ import subprocess
12
+ import sys
13
+ import threading
14
+ import time
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+ import pytest
19
+
20
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
21
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
22
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
23
+
24
+ from codex_review_scripts_constants.codex_usage_probe_constants import (
25
+ EXIT_CODE_CRASH,
26
+ EXIT_CODE_SUCCESS,
27
+ PERCENT_FULL,
28
+ READER_THREAD_JOIN_TIMEOUT_SECONDS,
29
+ SOURCE_APP_SERVER_RATE_LIMITS,
30
+ SOURCE_NO_USAGE_SURFACE,
31
+ SOURCE_TEXT_STATUS,
32
+ USAGE_REPORT_KEY_PERCENT_LEFT,
33
+ USAGE_REPORT_KEY_SOURCE,
34
+ USAGE_REPORT_KEY_WINDOW_RESET,
35
+ WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
36
+ WEEKLY_WINDOW_DURATION_MINUTES,
37
+ WINDOWS_OS_NAME,
38
+ WINDOWS_TASKKILL_COMMAND,
39
+ WINDOWS_TASKKILL_TREE_FLAG,
40
+ )
41
+
42
+ PROBE_PATH = SCRIPTS_DIRECTORY / "codex_usage_probe.py"
43
+
44
+ REAL_APP_SERVER_RATE_LIMITS_REPLY_CODEX_0_144_3 = json.dumps(
45
+ {
46
+ "id": 2,
47
+ "result": {
48
+ "rateLimits": {
49
+ "limitId": "codex",
50
+ "limitName": None,
51
+ "primary": {
52
+ "usedPercent": 95,
53
+ "windowDurationMins": WEEKLY_WINDOW_DURATION_MINUTES,
54
+ "resetsAt": 1784504948,
55
+ },
56
+ "secondary": None,
57
+ "credits": {
58
+ "hasCredits": False,
59
+ "unlimited": False,
60
+ "balance": None,
61
+ },
62
+ "individualLimit": None,
63
+ "planType": "team",
64
+ "rateLimitReachedType": None,
65
+ }
66
+ },
67
+ }
68
+ )
69
+
70
+ SESSION_WINDOW_DURATION_MINUTES = 300
71
+
72
+ DUAL_WINDOW_APP_SERVER_RATE_LIMITS_REPLY = json.dumps(
73
+ {
74
+ "id": 2,
75
+ "result": {
76
+ "rateLimits": {
77
+ "limitId": "codex",
78
+ "primary": {
79
+ "usedPercent": 40,
80
+ "windowDurationMins": SESSION_WINDOW_DURATION_MINUTES,
81
+ "resetsAt": 1783958400,
82
+ },
83
+ "secondary": {
84
+ "usedPercent": 20,
85
+ "windowDurationMins": WEEKLY_WINDOW_DURATION_MINUTES,
86
+ "resetsAt": 1784504948,
87
+ },
88
+ }
89
+ },
90
+ }
91
+ )
92
+
93
+ PRIMARY_ONLY_SESSION_WINDOW_REPLY = json.dumps(
94
+ {
95
+ "id": 2,
96
+ "result": {
97
+ "rateLimits": {
98
+ "limitId": "codex",
99
+ "primary": {
100
+ "usedPercent": 10,
101
+ "windowDurationMins": SESSION_WINDOW_DURATION_MINUTES,
102
+ "resetsAt": 1783958400,
103
+ },
104
+ "secondary": None,
105
+ }
106
+ },
107
+ }
108
+ )
109
+
110
+ EXPECTED_TEXT_STATUS_WINDOW_RESET = "2026-07-19T23:49:08+00:00"
111
+ EXPECTED_FIVE_HOUR_WINDOW_RESET = "2026-07-01T00:00:00+00:00"
112
+
113
+ NO_USAGE_SURFACE_LOGIN_STATUS_OUTPUT = "Logged in using ChatGPT\n"
114
+
115
+ TEXT_STATUS_WITH_FIVE_HOUR_AND_WEEKLY_RESETS = (
116
+ "Status\n"
117
+ f"5-hour limit: 60% left (resets {EXPECTED_FIVE_HOUR_WINDOW_RESET})\n"
118
+ f"Weekly limit: 42% left (resets {EXPECTED_TEXT_STATUS_WINDOW_RESET})\n"
119
+ )
120
+
121
+ PROCESS_TREE_WAIT_SECONDS = 5
122
+ READER_JOIN_TEARDOWN_WAIT_SECONDS = 0.2
123
+
124
+ NO_RATE_LIMITS_APP_SERVER_REPLY = json.dumps(
125
+ {
126
+ "id": 2,
127
+ "result": {"rateLimits": {}},
128
+ }
129
+ )
130
+
131
+ JSON_RPC_ERROR_APP_SERVER_REPLY = json.dumps(
132
+ {
133
+ "id": 2,
134
+ "error": {
135
+ "code": -32601,
136
+ "message": "Method not found",
137
+ },
138
+ }
139
+ )
140
+
141
+ TEXT_STATUS_WITH_WEEKLY_LEFT = (
142
+ "Status\n"
143
+ "5-hour limit: 60% left\n"
144
+ "Weekly limit: 42% left (resets 2026-07-19T23:49:08+00:00)\n"
145
+ )
146
+
147
+ TEXT_STATUS_WITH_RESETS_AT_PHRASING = (
148
+ "Status\n"
149
+ "Weekly limit: 42% left, resets at 2026-07-19T23:49:08+00:00\n"
150
+ )
151
+
152
+ TIMEOUT_SECONDS = 30
153
+ TIMEOUT_COMMAND = "codex"
154
+
155
+
156
+
157
+ def load_probe_module() -> ModuleType:
158
+ spec = importlib.util.spec_from_file_location("codex_usage_probe", PROBE_PATH)
159
+ assert spec is not None
160
+ assert spec.loader is not None
161
+ probe_module = importlib.util.module_from_spec(spec)
162
+ sys.modules[spec.name] = probe_module
163
+ spec.loader.exec_module(probe_module)
164
+ return probe_module
165
+
166
+
167
+ def _app_server_lines_from_rate_limits_payload(payload_line: str) -> list[str]:
168
+ return [
169
+ json.dumps(
170
+ {
171
+ "id": 1,
172
+ "result": {
173
+ "userAgent": "codex-usage-probe/0.144.3",
174
+ "platformOs": "windows",
175
+ },
176
+ }
177
+ ),
178
+ payload_line,
179
+ ]
180
+
181
+
182
+ def _assert_main_yields_null_skip_report(
183
+ probe: ModuleType,
184
+ capsys: pytest.CaptureFixture[str],
185
+ ) -> None:
186
+ exit_code = probe.main()
187
+ assert exit_code == EXIT_CODE_SUCCESS
188
+ captured = capsys.readouterr()
189
+ payload = json.loads(captured.out)
190
+ assert payload[USAGE_REPORT_KEY_PERCENT_LEFT] is None
191
+ assert payload[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
192
+ assert USAGE_REPORT_KEY_WINDOW_RESET in payload
193
+
194
+
195
+ def _assert_main_yields_cli_failure_not_usage_skip(
196
+ probe: ModuleType,
197
+ capsys: pytest.CaptureFixture[str],
198
+ ) -> None:
199
+ exit_code = probe.main()
200
+ assert exit_code == EXIT_CODE_CRASH
201
+ captured = capsys.readouterr()
202
+ payload = json.loads(captured.out)
203
+ assert payload[USAGE_REPORT_KEY_PERCENT_LEFT] is None
204
+ assert payload[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
205
+ assert USAGE_REPORT_KEY_WINDOW_RESET in payload
206
+
207
+
208
+ class TestParseRateLimitsMessage:
209
+ def should_compute_percent_left_from_weekly_primary_window(
210
+ self,
211
+ ) -> None:
212
+ probe = load_probe_module()
213
+ usage_report = probe.parse_rate_limits_message(
214
+ REAL_APP_SERVER_RATE_LIMITS_REPLY_CODEX_0_144_3
215
+ )
216
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 5
217
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] is not None
218
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_APP_SERVER_RATE_LIMITS
219
+
220
+ def should_prefer_secondary_weekly_window_over_primary_session(
221
+ self,
222
+ ) -> None:
223
+ probe = load_probe_module()
224
+ usage_report = probe.parse_rate_limits_message(
225
+ DUAL_WINDOW_APP_SERVER_RATE_LIMITS_REPLY
226
+ )
227
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 80
228
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] is not None
229
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_APP_SERVER_RATE_LIMITS
230
+
231
+ def should_return_null_when_only_non_weekly_session_window(self) -> None:
232
+ probe = load_probe_module()
233
+ usage_report = probe.parse_rate_limits_message(
234
+ PRIMARY_ONLY_SESSION_WINDOW_REPLY
235
+ )
236
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
237
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] is None
238
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
239
+
240
+ def should_return_null_percent_when_rate_limits_empty(self) -> None:
241
+ probe = load_probe_module()
242
+ usage_report = probe.parse_rate_limits_message(
243
+ NO_RATE_LIMITS_APP_SERVER_REPLY
244
+ )
245
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
246
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] is None
247
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
248
+
249
+ def should_return_null_percent_when_json_rpc_error_payload(self) -> None:
250
+ probe = load_probe_module()
251
+ usage_report = probe.parse_rate_limits_message(JSON_RPC_ERROR_APP_SERVER_REPLY)
252
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
253
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] is None
254
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
255
+
256
+
257
+ class TestParseTextStatus:
258
+ def should_parse_weekly_percent_left_from_status_text(self) -> None:
259
+ probe = load_probe_module()
260
+ usage_report = probe.parse_text_usage_status(TEXT_STATUS_WITH_WEEKLY_LEFT)
261
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 42.0
262
+ assert (
263
+ usage_report[USAGE_REPORT_KEY_WINDOW_RESET]
264
+ == EXPECTED_TEXT_STATUS_WINDOW_RESET
265
+ )
266
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_TEXT_STATUS
267
+
268
+ def should_parse_window_reset_from_resets_at_phrasing(self) -> None:
269
+ probe = load_probe_module()
270
+ usage_report = probe.parse_text_usage_status(
271
+ TEXT_STATUS_WITH_RESETS_AT_PHRASING
272
+ )
273
+ assert (
274
+ usage_report[USAGE_REPORT_KEY_WINDOW_RESET]
275
+ == EXPECTED_TEXT_STATUS_WINDOW_RESET
276
+ )
277
+
278
+ def should_prefer_weekly_reset_when_five_hour_reset_appears_first(self) -> None:
279
+ probe = load_probe_module()
280
+ usage_report = probe.parse_text_usage_status(
281
+ TEXT_STATUS_WITH_FIVE_HOUR_AND_WEEKLY_RESETS
282
+ )
283
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 42.0
284
+ assert (
285
+ usage_report[USAGE_REPORT_KEY_WINDOW_RESET]
286
+ == EXPECTED_TEXT_STATUS_WINDOW_RESET
287
+ )
288
+ assert usage_report[USAGE_REPORT_KEY_WINDOW_RESET] != (
289
+ EXPECTED_FIVE_HOUR_WINDOW_RESET
290
+ )
291
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_TEXT_STATUS
292
+
293
+ def should_yield_null_for_login_status_without_usage(self) -> None:
294
+ probe = load_probe_module()
295
+ usage_report = probe.parse_text_usage_status(
296
+ NO_USAGE_SURFACE_LOGIN_STATUS_OUTPUT
297
+ )
298
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
299
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
300
+
301
+
302
+ class TestProcessTreeTeardown:
303
+ def should_run_taskkill_tree_when_os_name_is_windows(
304
+ self,
305
+ monkeypatch: pytest.MonkeyPatch,
306
+ ) -> None:
307
+ probe = load_probe_module()
308
+ all_taskkill_commands: list[list[str]] = []
309
+ fake_process = subprocess.Popen(
310
+ [sys.executable, "-c", "import time; time.sleep(30)"],
311
+ stdout=subprocess.DEVNULL,
312
+ stderr=subprocess.DEVNULL,
313
+ )
314
+ try:
315
+ monkeypatch.setattr(probe.os, "name", WINDOWS_OS_NAME)
316
+
317
+ def capture_run(
318
+ command: list[str],
319
+ **_kwargs: object,
320
+ ) -> subprocess.CompletedProcess[str]:
321
+ all_taskkill_commands.append(list(command))
322
+ fake_process.kill()
323
+ fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
324
+ return subprocess.CompletedProcess(command, 0)
325
+
326
+ monkeypatch.setattr(probe.subprocess, "run", capture_run)
327
+ probe._terminate_process_tree(fake_process)
328
+ finally:
329
+ if fake_process.poll() is None:
330
+ fake_process.kill()
331
+ fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
332
+ assert len(all_taskkill_commands) == 1
333
+ assert all_taskkill_commands[0][0] == WINDOWS_TASKKILL_COMMAND
334
+ assert WINDOWS_TASKKILL_TREE_FLAG in all_taskkill_commands[0]
335
+ assert str(fake_process.pid) in all_taskkill_commands[0]
336
+
337
+ def should_kill_process_directly_when_os_name_is_not_windows(
338
+ self,
339
+ monkeypatch: pytest.MonkeyPatch,
340
+ ) -> None:
341
+ probe = load_probe_module()
342
+ fake_process = subprocess.Popen(
343
+ [sys.executable, "-c", "import time; time.sleep(30)"],
344
+ stdout=subprocess.DEVNULL,
345
+ stderr=subprocess.DEVNULL,
346
+ )
347
+ try:
348
+ monkeypatch.setattr(probe.os, "name", "posix")
349
+ probe._terminate_process_tree(fake_process)
350
+ assert fake_process.poll() is not None
351
+ finally:
352
+ if fake_process.poll() is None:
353
+ fake_process.kill()
354
+ fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
355
+
356
+
357
+ class TestReaderThreadLifecycle:
358
+ def should_join_stdout_reader_after_process_tree_teardown(self) -> None:
359
+ probe = load_probe_module()
360
+ is_exchange_over = threading.Event()
361
+ all_server_lines: list[str] = []
362
+
363
+ def collect_until_event(
364
+ _server_stdout: object,
365
+ collected_lines: list[str],
366
+ exchange_over: threading.Event,
367
+ ) -> None:
368
+ collected_lines.append("reader-started")
369
+ time.sleep(READER_JOIN_TEARDOWN_WAIT_SECONDS)
370
+ collected_lines.append("reader-finished")
371
+ exchange_over.set()
372
+
373
+ reader_thread = threading.Thread(
374
+ target=collect_until_event,
375
+ args=(None, all_server_lines, is_exchange_over),
376
+ daemon=True,
377
+ )
378
+ reader_thread.start()
379
+ finished_process = subprocess.Popen(
380
+ [sys.executable, "-c", "pass"],
381
+ stdout=subprocess.DEVNULL,
382
+ stderr=subprocess.DEVNULL,
383
+ )
384
+ finished_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
385
+ probe._teardown_app_server_exchange(
386
+ server_process=finished_process,
387
+ reader_thread=reader_thread,
388
+ )
389
+ assert reader_thread.is_alive() is False
390
+ assert all_server_lines == ["reader-started", "reader-finished"]
391
+ assert READER_THREAD_JOIN_TIMEOUT_SECONDS >= 1
392
+
393
+
394
+ class TestDefaultExchangeWiring:
395
+ def should_wire_subprocess_exchange_from_main(
396
+ self,
397
+ monkeypatch: pytest.MonkeyPatch,
398
+ capsys: pytest.CaptureFixture[str],
399
+ ) -> None:
400
+ probe = load_probe_module()
401
+ all_server_lines = _app_server_lines_from_rate_limits_payload(
402
+ REAL_APP_SERVER_RATE_LIMITS_REPLY_CODEX_0_144_3
403
+ )
404
+ all_call_counts = {"exchange": 0}
405
+
406
+ def fake_exchange(all_request_messages: list[dict[str, object]]) -> list[str]:
407
+ all_call_counts["exchange"] += 1
408
+ assert len(all_request_messages) >= 2
409
+ return all_server_lines
410
+
411
+ monkeypatch.setattr(
412
+ probe,
413
+ "_exchange_app_server_messages_via_subprocess",
414
+ fake_exchange,
415
+ )
416
+ exit_code = probe.main()
417
+ captured = capsys.readouterr()
418
+ usage_report = json.loads(captured.out)
419
+ assert exit_code == EXIT_CODE_SUCCESS
420
+ assert all_call_counts["exchange"] == 1
421
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 5
422
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_APP_SERVER_RATE_LIMITS
423
+
424
+
425
+ class TestProbeWeeklyUsage:
426
+ def should_probe_through_swappable_app_server_exchange(self) -> None:
427
+ probe = load_probe_module()
428
+ all_server_lines = _app_server_lines_from_rate_limits_payload(
429
+ REAL_APP_SERVER_RATE_LIMITS_REPLY_CODEX_0_144_3
430
+ )
431
+
432
+ def fake_exchange(all_request_messages: list[dict[str, object]]) -> list[str]:
433
+ assert len(all_request_messages) >= 2
434
+ return all_server_lines
435
+
436
+ usage_report = probe.probe_weekly_usage(
437
+ exchange_app_server_messages=fake_exchange
438
+ )
439
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 5
440
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_APP_SERVER_RATE_LIMITS
441
+
442
+ def should_yield_null_when_exchange_returns_no_usage_payload(self) -> None:
443
+ probe = load_probe_module()
444
+
445
+ def empty_exchange(
446
+ all_request_messages: list[dict[str, object]],
447
+ ) -> list[str]:
448
+ del all_request_messages
449
+ return [
450
+ json.dumps({"id": 1, "result": {}}),
451
+ NO_RATE_LIMITS_APP_SERVER_REPLY,
452
+ ]
453
+
454
+ usage_report = probe.probe_weekly_usage(
455
+ exchange_app_server_messages=empty_exchange
456
+ )
457
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
458
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
459
+
460
+ def should_yield_null_when_exchange_returns_json_rpc_error(self) -> None:
461
+ probe = load_probe_module()
462
+
463
+ def error_exchange(
464
+ all_request_messages: list[dict[str, object]],
465
+ ) -> list[str]:
466
+ del all_request_messages
467
+ return _app_server_lines_from_rate_limits_payload(
468
+ JSON_RPC_ERROR_APP_SERVER_REPLY
469
+ )
470
+
471
+ usage_report = probe.probe_weekly_usage(
472
+ exchange_app_server_messages=error_exchange
473
+ )
474
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] is None
475
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_NO_USAGE_SURFACE
476
+
477
+
478
+ class TestProbeWeeklyUsageViaSubprocess:
479
+ def should_report_usage_from_the_real_subprocess_exchange(
480
+ self,
481
+ monkeypatch: pytest.MonkeyPatch,
482
+ ) -> None:
483
+ probe = load_probe_module()
484
+ all_server_lines = _app_server_lines_from_rate_limits_payload(
485
+ REAL_APP_SERVER_RATE_LIMITS_REPLY_CODEX_0_144_3
486
+ )
487
+
488
+ def fake_subprocess_exchange(
489
+ all_request_messages: list[dict[str, object]],
490
+ ) -> list[str]:
491
+ assert len(all_request_messages) >= 2
492
+ return all_server_lines
493
+
494
+ monkeypatch.setattr(
495
+ probe,
496
+ "_exchange_app_server_messages_via_subprocess",
497
+ fake_subprocess_exchange,
498
+ )
499
+
500
+ usage_report = probe.probe_weekly_usage_via_subprocess()
501
+
502
+ assert usage_report[USAGE_REPORT_KEY_PERCENT_LEFT] == 5
503
+ assert usage_report[USAGE_REPORT_KEY_SOURCE] == SOURCE_APP_SERVER_RATE_LIMITS
504
+
505
+
506
+ class TestGateThresholdHelper:
507
+ def should_export_threshold_constant_of_ten(self) -> None:
508
+ assert WEEKLY_USAGE_GATE_THRESHOLD_PERCENT == 10
509
+
510
+ def should_skip_review_when_percent_is_null(self) -> None:
511
+ probe = load_probe_module()
512
+ assert probe.is_codex_review_required(None) is False
513
+
514
+ def should_skip_review_when_percent_at_or_below_threshold(self) -> None:
515
+ probe = load_probe_module()
516
+ assert (
517
+ probe.is_codex_review_required(float(WEEKLY_USAGE_GATE_THRESHOLD_PERCENT))
518
+ is False
519
+ )
520
+ assert probe.is_codex_review_required(5.0) is False
521
+
522
+ def should_require_review_when_percent_above_threshold(self) -> None:
523
+ probe = load_probe_module()
524
+ assert probe.is_codex_review_required(10.1) is True
525
+ assert probe.is_codex_review_required(float(PERCENT_FULL)) is True
526
+
527
+
528
+ class TestMainExitCodes:
529
+ def should_exit_zero_with_json_even_when_usage_is_null(
530
+ self,
531
+ capsys: pytest.CaptureFixture[str],
532
+ monkeypatch: pytest.MonkeyPatch,
533
+ ) -> None:
534
+ probe = load_probe_module()
535
+
536
+ def empty_exchange(
537
+ all_request_messages: list[dict[str, object]],
538
+ ) -> list[str]:
539
+ del all_request_messages
540
+ return [NO_RATE_LIMITS_APP_SERVER_REPLY]
541
+
542
+ monkeypatch.setattr(
543
+ probe,
544
+ "_exchange_app_server_messages_via_subprocess",
545
+ empty_exchange,
546
+ )
547
+ _assert_main_yields_null_skip_report(probe, capsys)
548
+
549
+ def should_exit_nonzero_when_binary_missing(
550
+ self,
551
+ capsys: pytest.CaptureFixture[str],
552
+ monkeypatch: pytest.MonkeyPatch,
553
+ ) -> None:
554
+ probe = load_probe_module()
555
+
556
+ def missing_binary_exchange(
557
+ all_request_messages: list[dict[str, object]],
558
+ ) -> list[str]:
559
+ del all_request_messages
560
+ raise FileNotFoundError("codex binary not found on PATH")
561
+
562
+ monkeypatch.setattr(
563
+ probe,
564
+ "_exchange_app_server_messages_via_subprocess",
565
+ missing_binary_exchange,
566
+ )
567
+ _assert_main_yields_cli_failure_not_usage_skip(probe, capsys)
568
+
569
+ def should_exit_nonzero_when_subprocess_times_out(
570
+ self,
571
+ capsys: pytest.CaptureFixture[str],
572
+ monkeypatch: pytest.MonkeyPatch,
573
+ ) -> None:
574
+ probe = load_probe_module()
575
+
576
+ def timeout_exchange(
577
+ all_request_messages: list[dict[str, object]],
578
+ ) -> list[str]:
579
+ del all_request_messages
580
+ raise subprocess.TimeoutExpired(
581
+ cmd=TIMEOUT_COMMAND,
582
+ timeout=TIMEOUT_SECONDS,
583
+ )
584
+
585
+ monkeypatch.setattr(
586
+ probe,
587
+ "_exchange_app_server_messages_via_subprocess",
588
+ timeout_exchange,
589
+ )
590
+ _assert_main_yields_cli_failure_not_usage_skip(probe, capsys)
591
+
592
+ def should_exit_nonzero_when_os_error(
593
+ self,
594
+ capsys: pytest.CaptureFixture[str],
595
+ monkeypatch: pytest.MonkeyPatch,
596
+ ) -> None:
597
+ probe = load_probe_module()
598
+
599
+ def os_error_exchange(
600
+ all_request_messages: list[dict[str, object]],
601
+ ) -> list[str]:
602
+ del all_request_messages
603
+ raise OSError("permission denied launching codex")
604
+
605
+ monkeypatch.setattr(
606
+ probe,
607
+ "_exchange_app_server_messages_via_subprocess",
608
+ os_error_exchange,
609
+ )
610
+ _assert_main_yields_cli_failure_not_usage_skip(probe, capsys)
611
+
612
+ def should_exit_nonzero_when_subprocess_error(
613
+ self,
614
+ capsys: pytest.CaptureFixture[str],
615
+ monkeypatch: pytest.MonkeyPatch,
616
+ ) -> None:
617
+ probe = load_probe_module()
618
+
619
+ def subprocess_error_exchange(
620
+ all_request_messages: list[dict[str, object]],
621
+ ) -> list[str]:
622
+ del all_request_messages
623
+ raise subprocess.SubprocessError("codex app-server failed")
624
+
625
+ monkeypatch.setattr(
626
+ probe,
627
+ "_exchange_app_server_messages_via_subprocess",
628
+ subprocess_error_exchange,
629
+ )
630
+ _assert_main_yields_cli_failure_not_usage_skip(probe, capsys)
631
+
632
+ def should_exit_zero_with_null_report_when_json_rpc_error_payload(
633
+ self,
634
+ capsys: pytest.CaptureFixture[str],
635
+ monkeypatch: pytest.MonkeyPatch,
636
+ ) -> None:
637
+ probe = load_probe_module()
638
+
639
+ def error_payload_exchange(
640
+ all_request_messages: list[dict[str, object]],
641
+ ) -> list[str]:
642
+ del all_request_messages
643
+ return _app_server_lines_from_rate_limits_payload(
644
+ JSON_RPC_ERROR_APP_SERVER_REPLY
645
+ )
646
+
647
+ monkeypatch.setattr(
648
+ probe,
649
+ "_exchange_app_server_messages_via_subprocess",
650
+ error_payload_exchange,
651
+ )
652
+ _assert_main_yields_null_skip_report(probe, capsys)
653
+
654
+ def should_exit_nonzero_when_unicode_decode_error(
655
+ self,
656
+ capsys: pytest.CaptureFixture[str],
657
+ monkeypatch: pytest.MonkeyPatch,
658
+ ) -> None:
659
+ probe = load_probe_module()
660
+
661
+ def unicode_decode_error_exchange(
662
+ all_request_messages: list[dict[str, object]],
663
+ ) -> list[str]:
664
+ del all_request_messages
665
+ raise UnicodeDecodeError(
666
+ "utf-8",
667
+ b"\xff",
668
+ 0,
669
+ 1,
670
+ "invalid start byte",
671
+ )
672
+
673
+ monkeypatch.setattr(
674
+ probe,
675
+ "_exchange_app_server_messages_via_subprocess",
676
+ unicode_decode_error_exchange,
677
+ )
678
+ _assert_main_yields_cli_failure_not_usage_skip(probe, capsys)