claude-dev-env 2.2.0 → 2.3.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 +1 -1
- 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/pyproject.toml +22 -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/code-verifier.md +1 -1
- package/hooks/blocking/code_rules_shared.py +4 -2
- package/hooks/blocking/config/verified_commit_constants.py +11 -0
- package/hooks/blocking/pii_payload_scan.py +78 -20
- package/hooks/blocking/pii_prevention_blocker.py +3 -1
- package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +221 -0
- package/hooks/blocking/test_verification_verdict_store.py +93 -0
- package/hooks/blocking/verification_verdict_store.py +91 -0
- package/hooks/validators/ruff_integration.py +2 -1
- package/hooks/validators/run_all_validators.py +460 -24
- package/hooks/validators/test_ruff_integration.py +21 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +223 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/cleanup-command-forms.md +23 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/check.ps1 +32 -6
- 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/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +13 -1
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +1 -1
- package/scripts/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -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/team-advisor/SKILL.md +3 -2
- package/skills/usage-pause/SKILL.md +5 -4
- package/skills/usage-pause/scripts/resolve_usage_window.py +68 -13
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +121 -9
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +7 -3
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Named constants for the claude chain weekly-usage report tool.
|
|
2
|
+
|
|
3
|
+
Per the project's configuration conventions, every scalar and structural
|
|
4
|
+
constant the usage reporter needs lives here rather than inline in the module.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
FULL_WEEKLY_PERCENT: float = 100.0
|
|
10
|
+
"""Percent scale ceiling for weekly utilization (remaining = this minus used)."""
|
|
11
|
+
|
|
12
|
+
USAGE_PAUSE_SKILL_DIRECTORY_NAME: str = "skills"
|
|
13
|
+
"""Package-root directory name that holds skill trees (sibling of ``scripts/``)."""
|
|
14
|
+
|
|
15
|
+
USAGE_PAUSE_SKILL_NAME: str = "usage-pause"
|
|
16
|
+
"""Skill directory name under ``skills/`` that owns the OAuth usage probe."""
|
|
17
|
+
|
|
18
|
+
USAGE_PAUSE_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
|
19
|
+
"""Directory under the usage-pause skill that holds the OAuth probe module."""
|
|
20
|
+
|
|
21
|
+
RESOLVE_USAGE_WINDOW_MODULE_NAME: str = "resolve_usage_window"
|
|
22
|
+
"""Module name of the usage-pause OAuth probe loader."""
|
|
23
|
+
|
|
24
|
+
RESOLVE_USAGE_WINDOW_FILENAME: str = "resolve_usage_window.py"
|
|
25
|
+
"""Filename of the usage-pause OAuth probe module on disk."""
|
|
26
|
+
|
|
27
|
+
JSON_ACCOUNTS_KEY: str = "accounts"
|
|
28
|
+
"""Top-level key in the CLI JSON report holding the per-account list."""
|
|
29
|
+
|
|
30
|
+
JSON_COMMAND_KEY: str = "command"
|
|
31
|
+
"""Per-account JSON key naming the chain binary."""
|
|
32
|
+
|
|
33
|
+
JSON_WEEKLY_REMAINING_PERCENT_KEY: str = "weekly_remaining_percent"
|
|
34
|
+
"""Per-account JSON key for remaining weekly usage percent (or null)."""
|
|
35
|
+
|
|
36
|
+
JSON_ERROR_KEY: str = "error"
|
|
37
|
+
"""Per-account JSON key carrying the unmeasurable-reason string."""
|
|
38
|
+
|
|
39
|
+
CLI_CONFIG_PATH_FLAG: str = "--config-path"
|
|
40
|
+
"""CLI flag that overrides the chain configuration file path."""
|
|
41
|
+
|
|
42
|
+
NO_ACCESS_TOKEN_ERROR_TEMPLATE: str = (
|
|
43
|
+
"no usable bearer token from the OAuth credential file at {credentials_path}"
|
|
44
|
+
)
|
|
45
|
+
"""Error when the entry credential file yields no usable OAuth bearer."""
|
|
46
|
+
|
|
47
|
+
WEEKLY_UTILIZATION_MISSING_ERROR: str = (
|
|
48
|
+
"usage response carried no weekly utilization"
|
|
49
|
+
)
|
|
50
|
+
"""Error when the OAuth usage payload has no readable seven_day utilization."""
|
|
51
|
+
|
|
52
|
+
USAGE_PROBE_FAILED_ERROR_TEMPLATE: str = "usage probe failed: {error}"
|
|
53
|
+
"""Error when the OAuth usage HTTP probe raises."""
|
|
54
|
+
|
|
55
|
+
RESOLVE_USAGE_WINDOW_MISSING_ERROR_TEMPLATE: str = (
|
|
56
|
+
"usage-pause probe module not found at {module_path}"
|
|
57
|
+
)
|
|
58
|
+
"""Error when the resolve_usage_window script cannot be located on disk."""
|
|
@@ -11,7 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
CODE_REVIEW_SLASH_COMMAND: str = "/code-review"
|
|
12
12
|
"""Built-in Claude Code slash command that runs the repository review."""
|
|
13
13
|
|
|
14
|
-
CODE_REVIEW_EFFORT: str = "
|
|
14
|
+
CODE_REVIEW_EFFORT: str = "xhigh"
|
|
15
15
|
"""Effort level passed to the built-in review slash command."""
|
|
16
16
|
|
|
17
17
|
CODE_REVIEW_FIX_FLAG: str = "--fix"
|
|
@@ -13,5 +13,5 @@ DEFAULT_POLL_INTERVAL: int = 30
|
|
|
13
13
|
WORKER_STAGGER_SECONDS: int = 15
|
|
14
14
|
"""Seconds between staggered headless grok worker process starts in a batch."""
|
|
15
15
|
|
|
16
|
-
DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS: int =
|
|
16
|
+
DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS: int = 2400
|
|
17
17
|
"""Default timeout applied to one headless `/code-review` chain invocation, in seconds."""
|
|
@@ -14,6 +14,7 @@ if str(_SCRIPTS_DIR) not in sys.path:
|
|
|
14
14
|
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
15
15
|
|
|
16
16
|
import claude_chain_runner as runner # noqa: E402
|
|
17
|
+
import claude_chain_usage as chain_usage # noqa: E402
|
|
17
18
|
from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
18
19
|
ALL_USAGE_LIMIT_SIGNATURES,
|
|
19
20
|
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
@@ -51,6 +52,10 @@ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
|
|
|
51
52
|
|
|
52
53
|
_A_SIGNATURE = ALL_USAGE_LIMIT_SIGNATURES[0]
|
|
53
54
|
_PROMPT_ARGUMENTS = ["-p", "hello"]
|
|
55
|
+
_EQUAL_WEEKLY_REMAINING_PERCENT = 50.0
|
|
56
|
+
_HIGH_WEEKLY_REMAINING_PERCENT = 90.0
|
|
57
|
+
_MID_WEEKLY_REMAINING_PERCENT = 50.0
|
|
58
|
+
_LOW_WEEKLY_REMAINING_PERCENT = 10.0
|
|
54
59
|
|
|
55
60
|
|
|
56
61
|
def _completed(command, returncode, stdout="", stderr=""):
|
|
@@ -74,6 +79,43 @@ def _write_chain_config(tmp_path, chain_entries):
|
|
|
74
79
|
return config_file
|
|
75
80
|
|
|
76
81
|
|
|
82
|
+
def _config_order_usage_reporter(
|
|
83
|
+
*, config_path: Path
|
|
84
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
85
|
+
all_entries = runner.load_chain(config_path)
|
|
86
|
+
return [
|
|
87
|
+
chain_usage.AccountUsageReport(
|
|
88
|
+
command=each_entry.command,
|
|
89
|
+
weekly_remaining_percent=_EQUAL_WEEKLY_REMAINING_PERCENT,
|
|
90
|
+
)
|
|
91
|
+
for each_entry in all_entries
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _usage_reporter_from_remaining(
|
|
96
|
+
remaining_percent_by_command: dict[str, float | None],
|
|
97
|
+
):
|
|
98
|
+
call_count = {"count": 0}
|
|
99
|
+
|
|
100
|
+
def active_reporter(
|
|
101
|
+
*, config_path: Path
|
|
102
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
103
|
+
call_count["count"] += 1
|
|
104
|
+
all_entries = runner.load_chain(config_path)
|
|
105
|
+
return [
|
|
106
|
+
chain_usage.AccountUsageReport(
|
|
107
|
+
command=each_entry.command,
|
|
108
|
+
weekly_remaining_percent=remaining_percent_by_command[
|
|
109
|
+
each_entry.command
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
for each_entry in all_entries
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
active_reporter.call_count = call_count
|
|
116
|
+
return active_reporter
|
|
117
|
+
|
|
118
|
+
|
|
77
119
|
class _Recorder:
|
|
78
120
|
def __init__(self, behavior_by_command):
|
|
79
121
|
self.behavior_by_command = behavior_by_command
|
|
@@ -100,14 +142,364 @@ def _install_tty_stdin(monkeypatch):
|
|
|
100
142
|
monkeypatch.setattr(runner.sys, "stdin", _TtyStdin())
|
|
101
143
|
|
|
102
144
|
|
|
103
|
-
def _install(
|
|
145
|
+
def _install(
|
|
146
|
+
monkeypatch,
|
|
147
|
+
config_file,
|
|
148
|
+
behavior_by_command,
|
|
149
|
+
*,
|
|
150
|
+
weekly_usage_reporter=None,
|
|
151
|
+
):
|
|
104
152
|
recorder = _Recorder(behavior_by_command)
|
|
105
153
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
106
154
|
monkeypatch.setattr(runner, "chain_subprocess_runner", recorder)
|
|
155
|
+
active_reporter = (
|
|
156
|
+
weekly_usage_reporter
|
|
157
|
+
if weekly_usage_reporter is not None
|
|
158
|
+
else _config_order_usage_reporter
|
|
159
|
+
)
|
|
160
|
+
monkeypatch.setattr(runner, "chain_weekly_usage_reporter", active_reporter)
|
|
107
161
|
_install_tty_stdin(monkeypatch)
|
|
108
162
|
return recorder
|
|
109
163
|
|
|
110
164
|
|
|
165
|
+
def test_highest_weekly_remaining_is_tried_first(
|
|
166
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
167
|
+
) -> None:
|
|
168
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
169
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
170
|
+
{
|
|
171
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
172
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
recorder = _install(
|
|
176
|
+
monkeypatch,
|
|
177
|
+
config_file,
|
|
178
|
+
{
|
|
179
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
180
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
181
|
+
},
|
|
182
|
+
weekly_usage_reporter=usage_reporter,
|
|
183
|
+
)
|
|
184
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
185
|
+
assert chain_result.served_command == "claude-ev"
|
|
186
|
+
assert chain_result.returncode == 0
|
|
187
|
+
assert chain_result.stdout == "from-ev"
|
|
188
|
+
assert recorder.invocations[0][0] == "claude-ev"
|
|
189
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
190
|
+
"claude-ev"
|
|
191
|
+
]
|
|
192
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_SERVED
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_usage_limit_failsover_remaining_ranked_accounts(
|
|
196
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
197
|
+
) -> None:
|
|
198
|
+
config_file = _write_chain_config(
|
|
199
|
+
tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
|
|
200
|
+
)
|
|
201
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
202
|
+
{
|
|
203
|
+
"claude": _MID_WEEKLY_REMAINING_PERCENT,
|
|
204
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
205
|
+
"claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
206
|
+
}
|
|
207
|
+
)
|
|
208
|
+
_install(
|
|
209
|
+
monkeypatch,
|
|
210
|
+
config_file,
|
|
211
|
+
{
|
|
212
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
213
|
+
"claude": _completed("claude", 0, stdout="ok"),
|
|
214
|
+
"claude-editor": _completed("claude-editor", 0, stdout="should not run"),
|
|
215
|
+
},
|
|
216
|
+
weekly_usage_reporter=usage_reporter,
|
|
217
|
+
)
|
|
218
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
219
|
+
assert chain_result.served_command == "claude"
|
|
220
|
+
assert chain_result.returncode == 0
|
|
221
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
222
|
+
"claude-ev",
|
|
223
|
+
"claude",
|
|
224
|
+
]
|
|
225
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_USAGE_LIMITED
|
|
226
|
+
assert chain_result.attempts[1].status == ATTEMPT_STATUS_SERVED
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def test_non_usage_error_on_highest_ranked_stops_without_rest(
|
|
230
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
231
|
+
) -> None:
|
|
232
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
233
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
234
|
+
{
|
|
235
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
236
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
_install(
|
|
240
|
+
monkeypatch,
|
|
241
|
+
config_file,
|
|
242
|
+
{
|
|
243
|
+
"claude-ev": _completed("claude-ev", 2, stderr="unknown flag"),
|
|
244
|
+
"claude": _completed("claude", 0, stdout="should not run"),
|
|
245
|
+
},
|
|
246
|
+
weekly_usage_reporter=usage_reporter,
|
|
247
|
+
)
|
|
248
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
249
|
+
assert chain_result.served_command == "claude-ev"
|
|
250
|
+
assert chain_result.returncode == 2
|
|
251
|
+
assert len(chain_result.attempts) == 1
|
|
252
|
+
assert chain_result.attempts[0].status == ATTEMPT_STATUS_NONZERO_EXIT
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_weekly_usage_probe_runs_once_per_run_claude(
|
|
256
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
257
|
+
) -> None:
|
|
258
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
259
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
260
|
+
{
|
|
261
|
+
"claude": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
262
|
+
"claude-ev": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
263
|
+
}
|
|
264
|
+
)
|
|
265
|
+
_install(
|
|
266
|
+
monkeypatch,
|
|
267
|
+
config_file,
|
|
268
|
+
{
|
|
269
|
+
"claude": _completed("claude", 1, stderr=_A_SIGNATURE),
|
|
270
|
+
"claude-ev": _completed("claude-ev", 0, stdout="ok"),
|
|
271
|
+
},
|
|
272
|
+
weekly_usage_reporter=usage_reporter,
|
|
273
|
+
)
|
|
274
|
+
runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
275
|
+
assert usage_reporter.call_count["count"] == 1
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_usage_reporter_import_failure_falls_back_to_config_order(
|
|
279
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
280
|
+
) -> None:
|
|
281
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
282
|
+
|
|
283
|
+
def broken_usage_reporter(
|
|
284
|
+
*, config_path: Path
|
|
285
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
286
|
+
raise ImportError("claude_chain_usage unavailable")
|
|
287
|
+
|
|
288
|
+
recorder = _install(
|
|
289
|
+
monkeypatch,
|
|
290
|
+
config_file,
|
|
291
|
+
{
|
|
292
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
293
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
294
|
+
},
|
|
295
|
+
weekly_usage_reporter=broken_usage_reporter,
|
|
296
|
+
)
|
|
297
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
298
|
+
assert chain_result.served_command == "claude"
|
|
299
|
+
assert chain_result.returncode == 0
|
|
300
|
+
assert chain_result.stdout == "from-claude"
|
|
301
|
+
assert recorder.invocations[0][0] == "claude"
|
|
302
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
303
|
+
"claude"
|
|
304
|
+
]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def test_missing_high_remaining_binary_falls_through_to_lower_remaining(
|
|
308
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
309
|
+
) -> None:
|
|
310
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
311
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
312
|
+
{
|
|
313
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
314
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
315
|
+
}
|
|
316
|
+
)
|
|
317
|
+
_install(
|
|
318
|
+
monkeypatch,
|
|
319
|
+
config_file,
|
|
320
|
+
{
|
|
321
|
+
"claude-ev": FileNotFoundError(),
|
|
322
|
+
"claude": _completed("claude", 0, stdout="from-claude"),
|
|
323
|
+
},
|
|
324
|
+
weekly_usage_reporter=usage_reporter,
|
|
325
|
+
)
|
|
326
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
327
|
+
assert chain_result.served_command == "claude"
|
|
328
|
+
assert chain_result.returncode == 0
|
|
329
|
+
assert chain_result.stdout == "from-claude"
|
|
330
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
331
|
+
"claude-ev",
|
|
332
|
+
"claude",
|
|
333
|
+
]
|
|
334
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
335
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
336
|
+
ATTEMPT_STATUS_SERVED,
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def test_all_missing_binaries_exhausts_chain(
|
|
341
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
342
|
+
) -> None:
|
|
343
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
344
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
345
|
+
{
|
|
346
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
347
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
348
|
+
}
|
|
349
|
+
)
|
|
350
|
+
_install(
|
|
351
|
+
monkeypatch,
|
|
352
|
+
config_file,
|
|
353
|
+
{
|
|
354
|
+
"claude-ev": FileNotFoundError(),
|
|
355
|
+
"claude": FileNotFoundError(),
|
|
356
|
+
},
|
|
357
|
+
weekly_usage_reporter=usage_reporter,
|
|
358
|
+
)
|
|
359
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
360
|
+
assert chain_result.served_command is None
|
|
361
|
+
assert chain_result.returncode == NO_COMPLETED_PROCESS_RETURN_CODE
|
|
362
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
363
|
+
"claude-ev",
|
|
364
|
+
"claude",
|
|
365
|
+
]
|
|
366
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
367
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
368
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
369
|
+
]
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_missing_later_ranked_binary_is_skipped(
|
|
373
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
374
|
+
) -> None:
|
|
375
|
+
config_file = _write_chain_config(
|
|
376
|
+
tmp_path, [_entry("claude"), _entry("claude-ev"), _entry("claude-editor")]
|
|
377
|
+
)
|
|
378
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
379
|
+
{
|
|
380
|
+
"claude": _MID_WEEKLY_REMAINING_PERCENT,
|
|
381
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
382
|
+
"claude-editor": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
383
|
+
}
|
|
384
|
+
)
|
|
385
|
+
_install(
|
|
386
|
+
monkeypatch,
|
|
387
|
+
config_file,
|
|
388
|
+
{
|
|
389
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
390
|
+
"claude": FileNotFoundError(),
|
|
391
|
+
"claude-editor": _completed("claude-editor", 0, stdout="done"),
|
|
392
|
+
},
|
|
393
|
+
weekly_usage_reporter=usage_reporter,
|
|
394
|
+
)
|
|
395
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
396
|
+
assert chain_result.served_command == "claude-editor"
|
|
397
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
398
|
+
"claude-ev",
|
|
399
|
+
"claude",
|
|
400
|
+
"claude-editor",
|
|
401
|
+
]
|
|
402
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
403
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
404
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
405
|
+
ATTEMPT_STATUS_SERVED,
|
|
406
|
+
]
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def test_ranked_walk_preserves_extra_args_for_mapped_entry(
|
|
410
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
411
|
+
) -> None:
|
|
412
|
+
config_file = _write_chain_config(
|
|
413
|
+
tmp_path,
|
|
414
|
+
[
|
|
415
|
+
_entry("claude", extra_args=["--account", "primary"]),
|
|
416
|
+
_entry("claude-ev", extra_args=["--account", "ev"]),
|
|
417
|
+
],
|
|
418
|
+
)
|
|
419
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
420
|
+
{
|
|
421
|
+
"claude": _LOW_WEEKLY_REMAINING_PERCENT,
|
|
422
|
+
"claude-ev": _HIGH_WEEKLY_REMAINING_PERCENT,
|
|
423
|
+
}
|
|
424
|
+
)
|
|
425
|
+
recorder = _install(
|
|
426
|
+
monkeypatch,
|
|
427
|
+
config_file,
|
|
428
|
+
{"claude-ev": _completed("claude-ev", 0)},
|
|
429
|
+
weekly_usage_reporter=usage_reporter,
|
|
430
|
+
)
|
|
431
|
+
runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
432
|
+
assert recorder.invocations[0] == [
|
|
433
|
+
"claude-ev",
|
|
434
|
+
"-p",
|
|
435
|
+
"hello",
|
|
436
|
+
"--account",
|
|
437
|
+
"ev",
|
|
438
|
+
]
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def test_duplicate_command_entries_are_both_walked(
|
|
442
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
443
|
+
) -> None:
|
|
444
|
+
config_file = _write_chain_config(
|
|
445
|
+
tmp_path,
|
|
446
|
+
[
|
|
447
|
+
_entry("claude", extra_args=["--account", "primary"]),
|
|
448
|
+
_entry("claude", extra_args=["--account", "secondary"]),
|
|
449
|
+
],
|
|
450
|
+
)
|
|
451
|
+
usage_reporter = _usage_reporter_from_remaining(
|
|
452
|
+
{"claude": _HIGH_WEEKLY_REMAINING_PERCENT}
|
|
453
|
+
)
|
|
454
|
+
recorder = _install(
|
|
455
|
+
monkeypatch,
|
|
456
|
+
config_file,
|
|
457
|
+
{"claude": _completed("claude", 1, stderr=_A_SIGNATURE)},
|
|
458
|
+
weekly_usage_reporter=usage_reporter,
|
|
459
|
+
)
|
|
460
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
461
|
+
assert [each_invocation[-1] for each_invocation in recorder.invocations] == [
|
|
462
|
+
"primary",
|
|
463
|
+
"secondary",
|
|
464
|
+
]
|
|
465
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
466
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
467
|
+
ATTEMPT_STATUS_USAGE_LIMITED,
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def test_entry_absent_from_usage_report_is_still_walked(
|
|
472
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
473
|
+
) -> None:
|
|
474
|
+
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
475
|
+
|
|
476
|
+
def partial_usage_reporter(
|
|
477
|
+
*, config_path: Path
|
|
478
|
+
) -> list[chain_usage.AccountUsageReport]:
|
|
479
|
+
return [
|
|
480
|
+
chain_usage.AccountUsageReport(
|
|
481
|
+
command="claude-ev",
|
|
482
|
+
weekly_remaining_percent=_HIGH_WEEKLY_REMAINING_PERCENT,
|
|
483
|
+
)
|
|
484
|
+
]
|
|
485
|
+
|
|
486
|
+
_install(
|
|
487
|
+
monkeypatch,
|
|
488
|
+
config_file,
|
|
489
|
+
{
|
|
490
|
+
"claude-ev": _completed("claude-ev", 1, stderr=_A_SIGNATURE),
|
|
491
|
+
"claude": _completed("claude", 0, stdout="ok"),
|
|
492
|
+
},
|
|
493
|
+
weekly_usage_reporter=partial_usage_reporter,
|
|
494
|
+
)
|
|
495
|
+
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
496
|
+
assert chain_result.served_command == "claude"
|
|
497
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
498
|
+
"claude-ev",
|
|
499
|
+
"claude",
|
|
500
|
+
]
|
|
501
|
+
|
|
502
|
+
|
|
111
503
|
def test_usage_limited_primary_falls_over_to_second(
|
|
112
504
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
113
505
|
) -> None:
|
|
@@ -170,7 +562,7 @@ def test_timeout_on_primary_does_not_fall_over(
|
|
|
170
562
|
assert chain_result.attempts[0].status == ATTEMPT_STATUS_TIMEOUT
|
|
171
563
|
|
|
172
564
|
|
|
173
|
-
def
|
|
565
|
+
def test_missing_primary_binary_falls_through_to_next(
|
|
174
566
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
175
567
|
) -> None:
|
|
176
568
|
config_file = _write_chain_config(tmp_path, [_entry("claude"), _entry("claude-ev")])
|
|
@@ -179,13 +571,21 @@ def test_missing_primary_binary_does_not_fall_over(
|
|
|
179
571
|
config_file,
|
|
180
572
|
{
|
|
181
573
|
"claude": FileNotFoundError(),
|
|
182
|
-
"claude-ev": _completed("claude-ev", 0),
|
|
574
|
+
"claude-ev": _completed("claude-ev", 0, stdout="from-ev"),
|
|
183
575
|
},
|
|
184
576
|
)
|
|
185
577
|
chain_result = runner.run_claude(_PROMPT_ARGUMENTS, timeout_seconds=5)
|
|
186
|
-
assert chain_result.served_command
|
|
187
|
-
assert
|
|
188
|
-
assert chain_result.
|
|
578
|
+
assert chain_result.served_command == "claude-ev"
|
|
579
|
+
assert chain_result.returncode == 0
|
|
580
|
+
assert chain_result.stdout == "from-ev"
|
|
581
|
+
assert [each_attempt.command for each_attempt in chain_result.attempts] == [
|
|
582
|
+
"claude",
|
|
583
|
+
"claude-ev",
|
|
584
|
+
]
|
|
585
|
+
assert [each_attempt.status for each_attempt in chain_result.attempts] == [
|
|
586
|
+
ATTEMPT_STATUS_EXECUTABLE_NOT_FOUND,
|
|
587
|
+
ATTEMPT_STATUS_SERVED,
|
|
588
|
+
]
|
|
189
589
|
|
|
190
590
|
|
|
191
591
|
def test_missing_fallback_binary_is_skipped_and_walk_continues(
|
|
@@ -530,6 +930,9 @@ def test_real_subprocess_capture_replaces_undecodable_bytes(
|
|
|
530
930
|
) -> None:
|
|
531
931
|
config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
|
|
532
932
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
933
|
+
monkeypatch.setattr(
|
|
934
|
+
runner, "chain_weekly_usage_reporter", _config_order_usage_reporter
|
|
935
|
+
)
|
|
533
936
|
child_code = 'import sys; sys.stdout.buffer.write(b"ok \\x90 end")'
|
|
534
937
|
chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
|
|
535
938
|
assert chain_result.served_command == sys.executable
|
|
@@ -542,6 +945,9 @@ def test_real_subprocess_capture_preserves_utf8_text(
|
|
|
542
945
|
) -> None:
|
|
543
946
|
config_file = _write_chain_config(tmp_path, [_entry(sys.executable)])
|
|
544
947
|
monkeypatch.setattr(runner, "chain_config_path", lambda: config_file)
|
|
948
|
+
monkeypatch.setattr(
|
|
949
|
+
runner, "chain_weekly_usage_reporter", _config_order_usage_reporter
|
|
950
|
+
)
|
|
545
951
|
child_code = 'import sys; sys.stdout.buffer.write("report ✅ done".encode("utf-8"))'
|
|
546
952
|
chain_result = runner.run_claude(["-c", child_code], timeout_seconds=60)
|
|
547
953
|
assert chain_result.served_command == sys.executable
|