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.
- package/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +42 -29
- package/skills/orchestrator-refresh/SKILL.md +17 -9
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- 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)
|