claude-dev-env 2.2.1 → 2.4.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.
- package/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +24 -9
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -5
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/team-advisor/SKILL.md +5 -4
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
"""Behavioral tests for the claude chain weekly-usage report tool."""
|
|
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 claude_chain_runner as chain_runner # noqa: E402
|
|
17
|
+
import claude_chain_usage as usage # noqa: E402
|
|
18
|
+
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
19
|
+
CHAIN_CONFIG_ERROR_EXIT_CODE,
|
|
20
|
+
CONFIG_CHAIN_KEY,
|
|
21
|
+
CONFIG_COMMAND_KEY,
|
|
22
|
+
CONFIG_CREDENTIALS_PATH_KEY,
|
|
23
|
+
CONFIG_EXTRA_ARGS_KEY,
|
|
24
|
+
CONFIG_FILENAME,
|
|
25
|
+
UTF8_ENCODING,
|
|
26
|
+
)
|
|
27
|
+
from dev_env_scripts_constants.claude_chain_usage_constants import ( # noqa: E402
|
|
28
|
+
CLI_CONFIG_PATH_FLAG,
|
|
29
|
+
FULL_WEEKLY_PERCENT,
|
|
30
|
+
JSON_ACCOUNTS_KEY,
|
|
31
|
+
JSON_COMMAND_KEY,
|
|
32
|
+
JSON_ERROR_KEY,
|
|
33
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY,
|
|
34
|
+
RESOLVE_USAGE_WINDOW_MODULE_NAME,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
PLACEHOLDER_CREDENTIALS_PRIMARY = "/path/to/account-primary/.credentials.json"
|
|
38
|
+
PLACEHOLDER_CREDENTIALS_SECONDARY = "/path/to/account-secondary/.credentials.json"
|
|
39
|
+
PLACEHOLDER_CREDENTIALS_TERTIARY = "/path/to/account-tertiary/.credentials.json"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _entry(
|
|
43
|
+
command: str,
|
|
44
|
+
*,
|
|
45
|
+
extra_args: list[str] | None = None,
|
|
46
|
+
credentials_path: str | None = None,
|
|
47
|
+
) -> dict[str, object]:
|
|
48
|
+
chain_entry: dict[str, object] = {
|
|
49
|
+
CONFIG_COMMAND_KEY: command,
|
|
50
|
+
CONFIG_EXTRA_ARGS_KEY: extra_args if extra_args is not None else [],
|
|
51
|
+
}
|
|
52
|
+
if credentials_path is not None:
|
|
53
|
+
chain_entry[CONFIG_CREDENTIALS_PATH_KEY] = credentials_path
|
|
54
|
+
return chain_entry
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _write_chain_config(
|
|
58
|
+
tmp_path: Path, all_chain_entries: list[dict[str, object]]
|
|
59
|
+
) -> Path:
|
|
60
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
61
|
+
config_file.write_text(
|
|
62
|
+
json.dumps({CONFIG_CHAIN_KEY: all_chain_entries}), encoding=UTF8_ENCODING
|
|
63
|
+
)
|
|
64
|
+
return config_file
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _probe_from_utilization_by_path(
|
|
68
|
+
utilization_by_path: dict[str, float | Exception],
|
|
69
|
+
) -> usage.WeeklyUtilizationProbe:
|
|
70
|
+
def active_probe(credentials_path: Path) -> float:
|
|
71
|
+
path_key = credentials_path.as_posix()
|
|
72
|
+
probe_outcome = utilization_by_path[path_key]
|
|
73
|
+
if isinstance(probe_outcome, Exception):
|
|
74
|
+
raise probe_outcome
|
|
75
|
+
return probe_outcome
|
|
76
|
+
|
|
77
|
+
return active_probe
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_remaining_percent_is_full_scale_minus_utilization(
|
|
81
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
82
|
+
) -> None:
|
|
83
|
+
config_file = _write_chain_config(
|
|
84
|
+
tmp_path,
|
|
85
|
+
[
|
|
86
|
+
_entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
|
|
87
|
+
_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
monkeypatch.setattr(
|
|
91
|
+
usage,
|
|
92
|
+
"weekly_utilization_probe",
|
|
93
|
+
_probe_from_utilization_by_path(
|
|
94
|
+
{
|
|
95
|
+
PLACEHOLDER_CREDENTIALS_PRIMARY: 42.0,
|
|
96
|
+
PLACEHOLDER_CREDENTIALS_SECONDARY: 9.0,
|
|
97
|
+
}
|
|
98
|
+
),
|
|
99
|
+
)
|
|
100
|
+
all_reports = usage.report_chain_weekly_usage(config_path=config_file)
|
|
101
|
+
assert [each_report.command for each_report in all_reports] == [
|
|
102
|
+
"claude",
|
|
103
|
+
"claude-ev",
|
|
104
|
+
]
|
|
105
|
+
assert all_reports[0].weekly_remaining_percent == pytest.approx(
|
|
106
|
+
FULL_WEEKLY_PERCENT - 42.0
|
|
107
|
+
)
|
|
108
|
+
assert all_reports[1].weekly_remaining_percent == pytest.approx(
|
|
109
|
+
FULL_WEEKLY_PERCENT - 9.0
|
|
110
|
+
)
|
|
111
|
+
assert all_reports[0].error is None
|
|
112
|
+
assert all_reports[1].error is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_rank_orders_by_remaining_desc_preserves_ties_and_puts_nulls_last() -> None:
|
|
116
|
+
all_reports = [
|
|
117
|
+
usage.AccountUsageReport(command="first", weekly_remaining_percent=40.0),
|
|
118
|
+
usage.AccountUsageReport(command="second", weekly_remaining_percent=70.0),
|
|
119
|
+
usage.AccountUsageReport(
|
|
120
|
+
command="third", weekly_remaining_percent=None, error="probe failed"
|
|
121
|
+
),
|
|
122
|
+
usage.AccountUsageReport(command="fourth", weekly_remaining_percent=70.0),
|
|
123
|
+
usage.AccountUsageReport(
|
|
124
|
+
command="fifth", weekly_remaining_percent=None, error="no token"
|
|
125
|
+
),
|
|
126
|
+
usage.AccountUsageReport(command="sixth", weekly_remaining_percent=10.0),
|
|
127
|
+
]
|
|
128
|
+
ranked_reports = usage.rank_accounts_by_weekly_remaining(all_reports)
|
|
129
|
+
assert [each_report.command for each_report in ranked_reports] == [
|
|
130
|
+
"second",
|
|
131
|
+
"fourth",
|
|
132
|
+
"first",
|
|
133
|
+
"sixth",
|
|
134
|
+
"third",
|
|
135
|
+
"fifth",
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_probe_failure_yields_null_remaining_and_error_string(
|
|
140
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
141
|
+
) -> None:
|
|
142
|
+
config_file = _write_chain_config(
|
|
143
|
+
tmp_path,
|
|
144
|
+
[
|
|
145
|
+
_entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
|
|
146
|
+
_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
|
|
147
|
+
],
|
|
148
|
+
)
|
|
149
|
+
monkeypatch.setattr(
|
|
150
|
+
usage,
|
|
151
|
+
"weekly_utilization_probe",
|
|
152
|
+
_probe_from_utilization_by_path(
|
|
153
|
+
{
|
|
154
|
+
PLACEHOLDER_CREDENTIALS_PRIMARY: usage.WeeklyUtilizationProbeError(
|
|
155
|
+
"token expired"
|
|
156
|
+
),
|
|
157
|
+
PLACEHOLDER_CREDENTIALS_SECONDARY: 25.0,
|
|
158
|
+
}
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
all_reports = usage.report_chain_weekly_usage(config_path=config_file)
|
|
162
|
+
assert all_reports[0].weekly_remaining_percent is None
|
|
163
|
+
assert all_reports[0].error == "token expired"
|
|
164
|
+
assert all_reports[1].weekly_remaining_percent == pytest.approx(
|
|
165
|
+
FULL_WEEKLY_PERCENT - 25.0
|
|
166
|
+
)
|
|
167
|
+
assert all_reports[1].error is None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def test_missing_config_raises_chain_configuration_error(tmp_path: Path) -> None:
|
|
171
|
+
missing_config = tmp_path / "absent-chain.json"
|
|
172
|
+
with pytest.raises(chain_runner.ChainConfigurationError):
|
|
173
|
+
usage.report_chain_weekly_usage(config_path=missing_config)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_empty_config_raises_chain_configuration_error(tmp_path: Path) -> None:
|
|
177
|
+
config_file = _write_chain_config(tmp_path, [])
|
|
178
|
+
with pytest.raises(chain_runner.ChainConfigurationError):
|
|
179
|
+
usage.report_chain_weekly_usage(config_path=config_file)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def test_entry_without_credentials_path_uses_default(
|
|
183
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
184
|
+
) -> None:
|
|
185
|
+
default_path = Path("/path/to/default/.credentials.json")
|
|
186
|
+
monkeypatch.setattr(usage, "_default_credentials_path", lambda: default_path)
|
|
187
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude")])
|
|
188
|
+
probed_paths: list[Path] = []
|
|
189
|
+
|
|
190
|
+
def active_probe(credentials_path: Path) -> float:
|
|
191
|
+
probed_paths.append(credentials_path)
|
|
192
|
+
return 10.0
|
|
193
|
+
|
|
194
|
+
monkeypatch.setattr(usage, "weekly_utilization_probe", active_probe)
|
|
195
|
+
all_reports = usage.report_chain_weekly_usage(config_path=config_file)
|
|
196
|
+
assert probed_paths == [default_path]
|
|
197
|
+
assert all_reports[0].weekly_remaining_percent == pytest.approx(
|
|
198
|
+
FULL_WEEKLY_PERCENT - 10.0
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_default_credentials_resolution_failure_yields_null_remaining(
|
|
203
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
204
|
+
) -> None:
|
|
205
|
+
def raise_probe_error() -> Path:
|
|
206
|
+
raise usage.WeeklyUtilizationProbeError("probe module not found")
|
|
207
|
+
|
|
208
|
+
monkeypatch.setattr(usage, "_default_credentials_path", raise_probe_error)
|
|
209
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude")])
|
|
210
|
+
all_reports = usage.report_chain_weekly_usage(config_path=config_file)
|
|
211
|
+
assert all_reports[0].weekly_remaining_percent is None
|
|
212
|
+
assert all_reports[0].error == "probe module not found"
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def test_entry_credentials_path_is_passed_to_probe(
|
|
216
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
217
|
+
) -> None:
|
|
218
|
+
config_file = _write_chain_config(
|
|
219
|
+
tmp_path,
|
|
220
|
+
[_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_TERTIARY)],
|
|
221
|
+
)
|
|
222
|
+
probed_paths: list[Path] = []
|
|
223
|
+
|
|
224
|
+
def active_probe(credentials_path: Path) -> float:
|
|
225
|
+
probed_paths.append(credentials_path)
|
|
226
|
+
return 0.0
|
|
227
|
+
|
|
228
|
+
monkeypatch.setattr(usage, "weekly_utilization_probe", active_probe)
|
|
229
|
+
usage.report_chain_weekly_usage(config_path=config_file)
|
|
230
|
+
assert probed_paths == [Path(PLACEHOLDER_CREDENTIALS_TERTIARY)]
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_entry_credentials_path_expands_user_home(
|
|
234
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
235
|
+
) -> None:
|
|
236
|
+
tilde_credentials_path = "~/.claude-accounts/secondary/.credentials.json"
|
|
237
|
+
config_file = _write_chain_config(
|
|
238
|
+
tmp_path,
|
|
239
|
+
[_entry("claude-ev", credentials_path=tilde_credentials_path)],
|
|
240
|
+
)
|
|
241
|
+
probed_paths: list[Path] = []
|
|
242
|
+
|
|
243
|
+
def active_probe(credentials_path: Path) -> float:
|
|
244
|
+
probed_paths.append(credentials_path)
|
|
245
|
+
return 0.0
|
|
246
|
+
|
|
247
|
+
monkeypatch.setattr(usage, "weekly_utilization_probe", active_probe)
|
|
248
|
+
usage.report_chain_weekly_usage(config_path=config_file)
|
|
249
|
+
assert probed_paths == [Path(tilde_credentials_path).expanduser()]
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_load_chain_carries_optional_credentials_path(tmp_path: Path) -> None:
|
|
253
|
+
config_file = _write_chain_config(
|
|
254
|
+
tmp_path,
|
|
255
|
+
[
|
|
256
|
+
_entry("claude"),
|
|
257
|
+
_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
|
|
258
|
+
],
|
|
259
|
+
)
|
|
260
|
+
all_entries = chain_runner.load_chain(config_file)
|
|
261
|
+
assert all_entries[0].credentials_path is None
|
|
262
|
+
assert all_entries[1].credentials_path == PLACEHOLDER_CREDENTIALS_SECONDARY
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_invalid_credentials_path_type_raises(tmp_path: Path) -> None:
|
|
266
|
+
config_file = tmp_path / CONFIG_FILENAME
|
|
267
|
+
config_file.write_text(
|
|
268
|
+
json.dumps(
|
|
269
|
+
{
|
|
270
|
+
CONFIG_CHAIN_KEY: [
|
|
271
|
+
{
|
|
272
|
+
CONFIG_COMMAND_KEY: "claude",
|
|
273
|
+
CONFIG_EXTRA_ARGS_KEY: [],
|
|
274
|
+
CONFIG_CREDENTIALS_PATH_KEY: 12,
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
),
|
|
279
|
+
encoding=UTF8_ENCODING,
|
|
280
|
+
)
|
|
281
|
+
with pytest.raises(chain_runner.ChainConfigurationError):
|
|
282
|
+
chain_runner.load_chain(config_file)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def test_reports_to_json_payload_matches_cli_contract() -> None:
|
|
286
|
+
all_reports = [
|
|
287
|
+
usage.AccountUsageReport(command="claude", weekly_remaining_percent=58.0),
|
|
288
|
+
usage.AccountUsageReport(
|
|
289
|
+
command="claude-ev",
|
|
290
|
+
weekly_remaining_percent=None,
|
|
291
|
+
error="probe failed",
|
|
292
|
+
),
|
|
293
|
+
]
|
|
294
|
+
payload = usage.reports_to_json_payload(all_reports)
|
|
295
|
+
assert payload == {
|
|
296
|
+
JSON_ACCOUNTS_KEY: [
|
|
297
|
+
{
|
|
298
|
+
JSON_COMMAND_KEY: "claude",
|
|
299
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY: 58.0,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
JSON_COMMAND_KEY: "claude-ev",
|
|
303
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY: None,
|
|
304
|
+
JSON_ERROR_KEY: "probe failed",
|
|
305
|
+
},
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def test_cli_writes_json_accounts_report(
|
|
311
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
312
|
+
) -> None:
|
|
313
|
+
config_file = _write_chain_config(
|
|
314
|
+
tmp_path,
|
|
315
|
+
[
|
|
316
|
+
_entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
|
|
317
|
+
_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
|
|
318
|
+
],
|
|
319
|
+
)
|
|
320
|
+
monkeypatch.setattr(
|
|
321
|
+
usage,
|
|
322
|
+
"weekly_utilization_probe",
|
|
323
|
+
_probe_from_utilization_by_path(
|
|
324
|
+
{
|
|
325
|
+
PLACEHOLDER_CREDENTIALS_PRIMARY: 42.0,
|
|
326
|
+
PLACEHOLDER_CREDENTIALS_SECONDARY: usage.WeeklyUtilizationProbeError(
|
|
327
|
+
"no token"
|
|
328
|
+
),
|
|
329
|
+
}
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
exit_code = usage.main([CLI_CONFIG_PATH_FLAG, str(config_file)])
|
|
333
|
+
captured = capsys.readouterr()
|
|
334
|
+
assert exit_code == 0
|
|
335
|
+
assert captured.err == ""
|
|
336
|
+
payload = json.loads(captured.out)
|
|
337
|
+
assert payload == {
|
|
338
|
+
JSON_ACCOUNTS_KEY: [
|
|
339
|
+
{
|
|
340
|
+
JSON_COMMAND_KEY: "claude",
|
|
341
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY: FULL_WEEKLY_PERCENT - 42.0,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
JSON_COMMAND_KEY: "claude-ev",
|
|
345
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY: None,
|
|
346
|
+
JSON_ERROR_KEY: "no token",
|
|
347
|
+
},
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_cli_missing_config_prints_stderr_and_returns_config_error(
|
|
353
|
+
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
354
|
+
) -> None:
|
|
355
|
+
missing_config = tmp_path / "absent-chain.json"
|
|
356
|
+
exit_code = usage.main([CLI_CONFIG_PATH_FLAG, str(missing_config)])
|
|
357
|
+
captured = capsys.readouterr()
|
|
358
|
+
assert exit_code == CHAIN_CONFIG_ERROR_EXIT_CODE
|
|
359
|
+
assert captured.out == ""
|
|
360
|
+
assert "not found" in captured.err.lower() or "claude chain" in captured.err.lower()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def test_weekly_remaining_from_utilization_uses_full_scale() -> None:
|
|
364
|
+
assert usage.weekly_remaining_from_utilization(9.0) == pytest.approx(91.0)
|
|
365
|
+
assert usage.weekly_remaining_from_utilization(0.0) == pytest.approx(
|
|
366
|
+
FULL_WEEKLY_PERCENT
|
|
367
|
+
)
|
|
368
|
+
assert usage.weekly_remaining_from_utilization(100.0) == pytest.approx(0.0)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_probe_weekly_utilization_reuses_resolver_windows(
|
|
372
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
373
|
+
) -> None:
|
|
374
|
+
class _FakeWindows:
|
|
375
|
+
weekly_utilization = 37.5
|
|
376
|
+
|
|
377
|
+
class _FakeResolver:
|
|
378
|
+
def read_oauth_access_token(self, credentials_path: Path, now: object) -> str:
|
|
379
|
+
assert credentials_path == Path(PLACEHOLDER_CREDENTIALS_PRIMARY)
|
|
380
|
+
return "token-value"
|
|
381
|
+
|
|
382
|
+
def resolve_access_token(self, credentials_path: Path, now: object) -> str:
|
|
383
|
+
raise AssertionError("probe must not fall back to session ingress")
|
|
384
|
+
|
|
385
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
386
|
+
assert access_token == "token-value"
|
|
387
|
+
return {"seven_day": {"utilization": 37.5}}
|
|
388
|
+
|
|
389
|
+
def extract_usage_windows(
|
|
390
|
+
self, usage_payload: dict[str, object]
|
|
391
|
+
) -> _FakeWindows:
|
|
392
|
+
assert usage_payload == {"seven_day": {"utilization": 37.5}}
|
|
393
|
+
return _FakeWindows()
|
|
394
|
+
|
|
395
|
+
monkeypatch.setattr(
|
|
396
|
+
usage, "_load_resolve_usage_window_module", lambda: _FakeResolver()
|
|
397
|
+
)
|
|
398
|
+
weekly_utilization = usage._probe_weekly_utilization(
|
|
399
|
+
Path(PLACEHOLDER_CREDENTIALS_PRIMARY)
|
|
400
|
+
)
|
|
401
|
+
assert weekly_utilization == pytest.approx(37.5)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def test_probe_weekly_utilization_raises_when_token_missing(
|
|
405
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
406
|
+
) -> None:
|
|
407
|
+
class _FakeResolver:
|
|
408
|
+
def read_oauth_access_token(
|
|
409
|
+
self, credentials_path: Path, now: object
|
|
410
|
+
) -> None:
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
def resolve_access_token(self, credentials_path: Path, now: object) -> str:
|
|
414
|
+
return "ingress-token-must-not-be-used"
|
|
415
|
+
|
|
416
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
417
|
+
raise AssertionError("must not probe with an ingress fallback token")
|
|
418
|
+
|
|
419
|
+
monkeypatch.setattr(
|
|
420
|
+
usage, "_load_resolve_usage_window_module", lambda: _FakeResolver()
|
|
421
|
+
)
|
|
422
|
+
with pytest.raises(usage.WeeklyUtilizationProbeError):
|
|
423
|
+
usage._probe_weekly_utilization(Path(PLACEHOLDER_CREDENTIALS_PRIMARY))
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def test_probe_ignores_ingress_when_credential_token_missing(
|
|
427
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
428
|
+
) -> None:
|
|
429
|
+
config_file = _write_chain_config(
|
|
430
|
+
tmp_path,
|
|
431
|
+
[_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY)],
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
class _FakeResolver:
|
|
435
|
+
def read_oauth_access_token(
|
|
436
|
+
self, credentials_path: Path, now: object
|
|
437
|
+
) -> None:
|
|
438
|
+
return None
|
|
439
|
+
|
|
440
|
+
def resolve_access_token(self, credentials_path: Path, now: object) -> str:
|
|
441
|
+
return "ingress-token-must-not-be-used"
|
|
442
|
+
|
|
443
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
444
|
+
raise AssertionError("must not probe with an ingress fallback token")
|
|
445
|
+
|
|
446
|
+
monkeypatch.setattr(
|
|
447
|
+
usage, "_load_resolve_usage_window_module", lambda: _FakeResolver()
|
|
448
|
+
)
|
|
449
|
+
monkeypatch.setattr(usage, "weekly_utilization_probe", usage._probe_weekly_utilization)
|
|
450
|
+
all_reports = usage.report_chain_weekly_usage(config_path=config_file)
|
|
451
|
+
assert all_reports[0].weekly_remaining_percent is None
|
|
452
|
+
assert all_reports[0].error is not None
|
|
453
|
+
assert "bearer token" in all_reports[0].error.lower()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def test_load_failure_yields_per_account_error_and_cli_exits_zero(
|
|
457
|
+
tmp_path: Path,
|
|
458
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
459
|
+
capsys: pytest.CaptureFixture[str],
|
|
460
|
+
) -> None:
|
|
461
|
+
config_file = _write_chain_config(
|
|
462
|
+
tmp_path,
|
|
463
|
+
[
|
|
464
|
+
_entry("claude", credentials_path=PLACEHOLDER_CREDENTIALS_PRIMARY),
|
|
465
|
+
_entry("claude-ev", credentials_path=PLACEHOLDER_CREDENTIALS_SECONDARY),
|
|
466
|
+
],
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
def active_probe(credentials_path: Path) -> float:
|
|
470
|
+
path_key = credentials_path.as_posix()
|
|
471
|
+
if path_key == PLACEHOLDER_CREDENTIALS_PRIMARY:
|
|
472
|
+
raise ImportError("usage probe module failed to import")
|
|
473
|
+
return 25.0
|
|
474
|
+
|
|
475
|
+
monkeypatch.setattr(usage, "weekly_utilization_probe", active_probe)
|
|
476
|
+
exit_code = usage.main([CLI_CONFIG_PATH_FLAG, str(config_file)])
|
|
477
|
+
captured = capsys.readouterr()
|
|
478
|
+
assert exit_code == 0
|
|
479
|
+
payload = json.loads(captured.out)
|
|
480
|
+
assert payload[JSON_ACCOUNTS_KEY][0][JSON_WEEKLY_REMAINING_PERCENT_KEY] is None
|
|
481
|
+
assert "failed to import" in payload[JSON_ACCOUNTS_KEY][0][JSON_ERROR_KEY]
|
|
482
|
+
assert payload[JSON_ACCOUNTS_KEY][1][JSON_WEEKLY_REMAINING_PERCENT_KEY] == pytest.approx(
|
|
483
|
+
FULL_WEEKLY_PERCENT - 25.0
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_failed_exec_module_does_not_poison_sys_modules(
|
|
488
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
489
|
+
) -> None:
|
|
490
|
+
if RESOLVE_USAGE_WINDOW_MODULE_NAME in sys.modules:
|
|
491
|
+
del sys.modules[RESOLVE_USAGE_WINDOW_MODULE_NAME]
|
|
492
|
+
(tmp_path / "resolve_usage_window.py").write_text(
|
|
493
|
+
"raise ImportError('intentional load failure')\n", encoding="utf-8"
|
|
494
|
+
)
|
|
495
|
+
monkeypatch.setattr(usage, "_usage_pause_scripts_directory", lambda: tmp_path)
|
|
496
|
+
with pytest.raises(ImportError, match="intentional load failure"):
|
|
497
|
+
usage._load_resolve_usage_window_module()
|
|
498
|
+
assert RESOLVE_USAGE_WINDOW_MODULE_NAME not in sys.modules
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def test_load_resolve_usage_window_module_loads_real_probe_api() -> None:
|
|
502
|
+
if RESOLVE_USAGE_WINDOW_MODULE_NAME in sys.modules:
|
|
503
|
+
del sys.modules[RESOLVE_USAGE_WINDOW_MODULE_NAME]
|
|
504
|
+
loaded_module = usage._load_resolve_usage_window_module()
|
|
505
|
+
assert callable(loaded_module.read_oauth_access_token)
|
|
506
|
+
assert callable(loaded_module.resolve_access_token)
|
|
507
|
+
assert callable(loaded_module.extract_usage_windows)
|
|
508
|
+
assert callable(loaded_module._fetch_usage_payload)
|
|
509
|
+
assert sys.modules[RESOLVE_USAGE_WINDOW_MODULE_NAME] is loaded_module
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def test_load_resolve_usage_window_module_reuses_cached_module() -> None:
|
|
513
|
+
first_load = usage._load_resolve_usage_window_module()
|
|
514
|
+
second_load = usage._load_resolve_usage_window_module()
|
|
515
|
+
assert first_load is second_load
|
|
516
|
+
|
|
517
|
+
|
|
518
|
+
def test_usage_module_imports_without_preloading_runner() -> None:
|
|
519
|
+
scripts_directory = str(_SCRIPTS_DIR)
|
|
520
|
+
import_probe = (
|
|
521
|
+
"import sys; "
|
|
522
|
+
f"sys.path.insert(0, {scripts_directory!r}); "
|
|
523
|
+
"import claude_chain_usage; "
|
|
524
|
+
"assert callable(claude_chain_usage.report_chain_weekly_usage); "
|
|
525
|
+
"assert callable(claude_chain_usage.rank_accounts_by_weekly_remaining)"
|
|
526
|
+
)
|
|
527
|
+
completed = subprocess.run(
|
|
528
|
+
[sys.executable, "-S", "-E", "-c", import_probe],
|
|
529
|
+
capture_output=True,
|
|
530
|
+
text=True,
|
|
531
|
+
timeout=60,
|
|
532
|
+
check=False,
|
|
533
|
+
)
|
|
534
|
+
assert completed.returncode == 0, completed.stderr
|