claude-dev-env 8.43.4 → 8.43.6
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/advisor-protocol.md +2 -2
- package/_shared/advisor/reference/cli-chain.md +9 -12
- package/_shared/advisor/reference/lifecycle.md +1 -1
- package/_shared/advisor/reference/third-party-bind.md +3 -3
- package/bin/install.mjs +39 -3
- package/bin/install.shared-settings.test.mjs +46 -0
- package/bin/install.test.mjs +1 -1
- package/docs/second-claude-account.md +11 -13
- package/package.json +1 -1
- package/scripts/_code_review_test_support.py +29 -39
- package/scripts/claude_account_worker.py +46 -297
- package/scripts/claude_account_worker_process.py +18 -154
- package/scripts/claude_account_worker_report.py +8 -4
- package/scripts/claude_chain_usage.py +7 -271
- package/scripts/codec_forwarding_test_support.py +11 -65
- package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
- package/scripts/invoke_code_review.py +34 -56
- package/scripts/policy_lint/config/approved_test_pairs.py +1 -0
- package/scripts/resolve_worker_spawn.py +40 -72
- package/scripts/test_account_broker_guard.py +0 -4
- package/scripts/test_claude_account_worker.py +105 -350
- package/scripts/test_claude_chain_usage.py +50 -510
- package/scripts/test_dispatcher_profile_import.py +5 -9
- package/scripts/test_invoke_code_review.py +7 -6
- package/scripts/test_invoke_code_review_chain.py +26 -0
- package/scripts/test_invoke_code_review_cli.py +8 -8
- package/scripts/test_invoke_code_review_codec.py +3 -11
- package/scripts/test_invoke_code_review_contract.py +5 -1
- package/scripts/test_resolve_worker_spawn.py +118 -265
- package/scripts/test_resolve_worker_spawn_codec.py +9 -21
- package/scripts/tests/test_code_review_constants.py +2 -2
- package/scripts/tests/test_policy_lint_rules_pairing.py +13 -0
- package/scripts/claude_account_choice.py +0 -422
- package/scripts/claude_chain_runner.py +0 -1289
- package/scripts/test_claude_account_choice.py +0 -355
- package/scripts/test_claude_chain_runner.py +0 -1839
|
@@ -1,446 +1,201 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
import
|
|
5
|
-
from collections.abc import Callable
|
|
6
|
-
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from datetime import datetime, timezone
|
|
7
5
|
from pathlib import Path
|
|
8
|
-
from typing import Any
|
|
9
6
|
|
|
10
7
|
import pytest
|
|
11
8
|
|
|
12
|
-
import
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
WorkerDependencies,
|
|
18
|
-
build_argument_parser,
|
|
19
|
-
select_account,
|
|
20
|
-
run_worker,
|
|
21
|
-
)
|
|
22
|
-
from dev_env_scripts_constants.claude_account_worker_constants import (
|
|
23
|
-
CHOICE_MAIN,
|
|
24
|
-
CHOICE_SECOND,
|
|
25
|
-
CHOICE_WAIT,
|
|
26
|
-
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
27
|
-
STDOUT_TAIL_CHARACTER_LIMIT,
|
|
28
|
-
TIMEOUT_EXIT_CODE,
|
|
9
|
+
import claude_account_worker as worker
|
|
10
|
+
from dev_env_scripts_constants.account_broker_constants import (
|
|
11
|
+
BrokerConfigurationError,
|
|
12
|
+
JobOutcome,
|
|
13
|
+
Product,
|
|
29
14
|
WAIT_EXIT_CODE,
|
|
30
15
|
)
|
|
31
|
-
|
|
32
|
-
NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class FakeProcess:
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
*,
|
|
39
|
-
stdout: str = '{"result":"ready","is_error":false}',
|
|
40
|
-
returncode: int = 0,
|
|
41
|
-
timeout_first: bool = False,
|
|
42
|
-
timeout_output: str = "",
|
|
43
|
-
drain_timeout_count: int = 0,
|
|
44
|
-
) -> None:
|
|
45
|
-
self.stdout = stdout
|
|
46
|
-
self.returncode = returncode
|
|
47
|
-
self.timeout_first = timeout_first
|
|
48
|
-
self.timeout_output = timeout_output
|
|
49
|
-
self.drain_timeout_count = drain_timeout_count
|
|
50
|
-
self.pid = 42
|
|
51
|
-
self.communicate_calls: list[tuple[str | None, float | None]] = []
|
|
52
|
-
self.terminated = False
|
|
53
|
-
|
|
54
|
-
def communicate(
|
|
55
|
-
self,
|
|
56
|
-
input: str | None = None,
|
|
57
|
-
timeout: float | None = None,
|
|
58
|
-
) -> tuple[str, str]:
|
|
59
|
-
self.communicate_calls.append((input, timeout))
|
|
60
|
-
if self.timeout_first and len(self.communicate_calls) == 1:
|
|
61
|
-
raise subprocess.TimeoutExpired(
|
|
62
|
-
cmd="claude",
|
|
63
|
-
timeout=timeout,
|
|
64
|
-
output=self.timeout_output,
|
|
65
|
-
)
|
|
66
|
-
if self.drain_timeout_count:
|
|
67
|
-
self.drain_timeout_count -= 1
|
|
68
|
-
raise subprocess.TimeoutExpired(
|
|
69
|
-
cmd="claude",
|
|
70
|
-
timeout=timeout,
|
|
71
|
-
output=self.timeout_output,
|
|
72
|
-
)
|
|
73
|
-
return self.stdout, ""
|
|
74
|
-
|
|
75
|
-
def poll(self) -> int | None:
|
|
76
|
-
return self.returncode if self.terminated else None
|
|
77
|
-
|
|
78
|
-
def kill(self) -> None:
|
|
79
|
-
self.terminated = True
|
|
80
|
-
self.returncode = -9
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def _selection(
|
|
84
|
-
account: str,
|
|
85
|
-
*,
|
|
86
|
-
config_dir: Path | None = None,
|
|
87
|
-
reason: str = "selected by test",
|
|
88
|
-
) -> AccountSelection:
|
|
89
|
-
return AccountSelection(account=account, config_dir=config_dir, reason=reason)
|
|
16
|
+
from dev_env_scripts_constants.claude_account_worker_constants import STDOUT_TAIL_CHARACTER_LIMIT
|
|
90
17
|
|
|
91
18
|
|
|
92
|
-
def
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
19
|
+
def _outcome(
|
|
20
|
+
*,
|
|
21
|
+
stdout: str = '{"result":"ready","is_error":false}',
|
|
22
|
+
returncode: int = 0,
|
|
23
|
+
account_name: str | None = "extra_2",
|
|
24
|
+
status: str = "served",
|
|
25
|
+
wait_reset_at: datetime | None = None,
|
|
26
|
+
) -> JobOutcome:
|
|
27
|
+
return JobOutcome(
|
|
28
|
+
returncode,
|
|
29
|
+
stdout,
|
|
30
|
+
"",
|
|
31
|
+
account_name,
|
|
32
|
+
((account_name, status),) if account_name else (),
|
|
33
|
+
status,
|
|
34
|
+
None,
|
|
35
|
+
wait_reset_at,
|
|
36
|
+
)
|
|
102
37
|
|
|
103
38
|
|
|
104
39
|
def _run_worker(
|
|
40
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
105
41
|
tmp_path: Path,
|
|
42
|
+
outcome: JobOutcome | Exception,
|
|
106
43
|
*,
|
|
107
|
-
selection: AccountSelection,
|
|
108
|
-
process: FakeProcess,
|
|
109
|
-
parent_environment: dict[str, str] | None = None,
|
|
110
|
-
process_terminator: Callable[[FakeProcess], None] | None = None,
|
|
111
44
|
model: str | None = None,
|
|
112
45
|
permission_mode: str = "auto",
|
|
113
|
-
) -> tuple[int, dict[str,
|
|
46
|
+
) -> tuple[int, dict[str, object], dict[str, object]]:
|
|
114
47
|
prompt_file = tmp_path / "brief.md"
|
|
115
48
|
prompt_file.write_text("standalone brief", encoding="utf-8")
|
|
116
49
|
report_file = tmp_path / "report.json"
|
|
117
|
-
captured: dict[str,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
),
|
|
128
|
-
monotonic_clock=lambda: 1.0,
|
|
129
|
-
)
|
|
130
|
-
exit_code = run_worker(
|
|
50
|
+
captured: dict[str, object] = {}
|
|
51
|
+
|
|
52
|
+
def run_job(product: Product, argv: list[str], **options: object) -> JobOutcome:
|
|
53
|
+
captured.update(product=product, argv=argv, **options)
|
|
54
|
+
if isinstance(outcome, Exception):
|
|
55
|
+
raise outcome
|
|
56
|
+
return outcome
|
|
57
|
+
|
|
58
|
+
monkeypatch.setattr(worker, "worker_job_runner", run_job)
|
|
59
|
+
exit_code = worker.run_worker(
|
|
131
60
|
prompt_file=prompt_file,
|
|
132
61
|
cwd=tmp_path,
|
|
133
62
|
report_file=report_file,
|
|
134
63
|
model=model,
|
|
135
64
|
permission_mode=permission_mode,
|
|
136
65
|
timeout_minutes=60,
|
|
137
|
-
decision_selector=lambda: selection,
|
|
138
|
-
dependencies=dependencies,
|
|
139
66
|
)
|
|
140
67
|
return exit_code, json.loads(report_file.read_text(encoding="utf-8")), captured
|
|
141
68
|
|
|
142
69
|
|
|
143
|
-
def
|
|
144
|
-
tmp_path: Path
|
|
70
|
+
def test_should_send_prompt_and_flags_to_broker(
|
|
71
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
145
72
|
) -> None:
|
|
146
|
-
profile_dir = tmp_path / "second-profile"
|
|
147
|
-
process = FakeProcess()
|
|
148
73
|
exit_code, report, captured = _run_worker(
|
|
149
|
-
tmp_path,
|
|
150
|
-
selection=_selection(CHOICE_SECOND, config_dir=profile_dir),
|
|
151
|
-
process=process,
|
|
152
|
-
parent_environment={
|
|
153
|
-
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
154
|
-
"KEEP_ME": "present",
|
|
155
|
-
},
|
|
156
|
-
model="sonnet",
|
|
157
|
-
permission_mode="plan",
|
|
74
|
+
monkeypatch, tmp_path, _outcome(), model="sonnet", permission_mode="plan"
|
|
158
75
|
)
|
|
159
76
|
|
|
160
77
|
assert exit_code == 0
|
|
161
|
-
assert report["account"] ==
|
|
162
|
-
assert
|
|
163
|
-
assert captured["
|
|
164
|
-
assert captured["
|
|
165
|
-
"claude
|
|
166
|
-
"-p",
|
|
167
|
-
"--output-format",
|
|
168
|
-
"json",
|
|
169
|
-
"--permission-mode",
|
|
170
|
-
"plan",
|
|
171
|
-
"--model",
|
|
172
|
-
"sonnet",
|
|
78
|
+
assert report["account"] == "extra_2"
|
|
79
|
+
assert report["result"] == "ready"
|
|
80
|
+
assert captured["product"] is Product.CLAUDE
|
|
81
|
+
assert captured["argv"] == [
|
|
82
|
+
"claude", "-p", "--output-format", "json", "--permission-mode", "plan", "--model", "sonnet"
|
|
173
83
|
]
|
|
174
|
-
assert captured["
|
|
175
|
-
assert
|
|
84
|
+
assert captured["stdin_text"] == "standalone brief"
|
|
85
|
+
assert captured["cwd"] == tmp_path
|
|
86
|
+
assert captured["timeout_seconds"] == 3600
|
|
176
87
|
|
|
177
88
|
|
|
178
|
-
def
|
|
179
|
-
|
|
89
|
+
def test_should_report_wait_with_reset_and_exit_three(
|
|
90
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
91
|
+
) -> None:
|
|
92
|
+
reset_at = datetime(2026, 10, 4, 12, tzinfo=timezone.utc)
|
|
180
93
|
exit_code, report, captured = _run_worker(
|
|
94
|
+
monkeypatch,
|
|
181
95
|
tmp_path,
|
|
182
|
-
|
|
183
|
-
process=process,
|
|
184
|
-
parent_environment={
|
|
185
|
-
CLAUDE_CONFIG_DIR_ENV_VAR: "inherited-profile",
|
|
186
|
-
"KEEP_ME": "present",
|
|
187
|
-
},
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
assert exit_code == 0
|
|
191
|
-
assert report["account"] == CHOICE_MAIN
|
|
192
|
-
assert CLAUDE_CONFIG_DIR_ENV_VAR not in captured["env"]
|
|
193
|
-
assert captured["env"]["KEEP_ME"] == "present"
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def test_should_write_wait_reason_and_start_no_process(tmp_path: Path) -> None:
|
|
197
|
-
report_file = tmp_path / "wait-report.json"
|
|
198
|
-
process_started = False
|
|
199
|
-
|
|
200
|
-
def forbidden_runner(*_: Any, **__: Any) -> FakeProcess:
|
|
201
|
-
nonlocal process_started
|
|
202
|
-
process_started = True
|
|
203
|
-
raise AssertionError("wait must not start a process")
|
|
204
|
-
|
|
205
|
-
exit_code = run_worker(
|
|
206
|
-
prompt_file=tmp_path / "missing-brief.md",
|
|
207
|
-
cwd=tmp_path,
|
|
208
|
-
report_file=report_file,
|
|
209
|
-
model=None,
|
|
210
|
-
permission_mode="auto",
|
|
211
|
-
timeout_minutes=60,
|
|
212
|
-
decision_selector=lambda: _selection(
|
|
213
|
-
CHOICE_WAIT,
|
|
214
|
-
reason="second account resets at 2026-09-24T12:00:00Z",
|
|
215
|
-
),
|
|
216
|
-
dependencies=WorkerDependencies(
|
|
217
|
-
process_factory=forbidden_runner,
|
|
218
|
-
all_parent_environment_variables={},
|
|
219
|
-
binary_resolver=lambda _: (_ for _ in ()).throw(
|
|
220
|
-
AssertionError("wait must not resolve a binary")
|
|
221
|
-
),
|
|
222
|
-
process_terminator=lambda _: None,
|
|
223
|
-
monotonic_clock=lambda: 1.0,
|
|
224
|
-
),
|
|
96
|
+
_outcome(stdout="", returncode=WAIT_EXIT_CODE, account_name=None, status="wait", wait_reset_at=reset_at),
|
|
225
97
|
)
|
|
226
|
-
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
227
98
|
|
|
228
99
|
assert exit_code == WAIT_EXIT_CODE
|
|
229
|
-
assert
|
|
230
|
-
assert report["
|
|
231
|
-
assert report["reason"] == "
|
|
232
|
-
assert report["is_error"] is
|
|
100
|
+
assert report["account"] == "wait"
|
|
101
|
+
assert report["wait_reset_at"] == reset_at.isoformat()
|
|
102
|
+
assert report["reason"] == f"no account has room; next reset at {reset_at.isoformat()}"
|
|
103
|
+
assert report["is_error"] is False
|
|
104
|
+
assert captured["product"] is Product.CLAUDE
|
|
233
105
|
|
|
234
106
|
|
|
235
|
-
def
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
returncode=-9,
|
|
239
|
-
timeout_first=True,
|
|
240
|
-
timeout_output='{"result":"partial"}',
|
|
241
|
-
)
|
|
242
|
-
termination_calls: list[FakeProcess] = []
|
|
107
|
+
def test_should_preserve_exhausted_wait_exit_code(
|
|
108
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
109
|
+
) -> None:
|
|
243
110
|
exit_code, report, _ = _run_worker(
|
|
111
|
+
monkeypatch,
|
|
244
112
|
tmp_path,
|
|
245
|
-
|
|
246
|
-
process=process,
|
|
247
|
-
process_terminator=lambda child: termination_calls.append(child),
|
|
113
|
+
_outcome(stdout="", returncode=WAIT_EXIT_CODE, account_name=None, status="exhausted"),
|
|
248
114
|
)
|
|
249
115
|
|
|
250
|
-
assert exit_code ==
|
|
251
|
-
assert
|
|
252
|
-
assert len(process.communicate_calls) == 2
|
|
253
|
-
assert report["exit_code"] == TIMEOUT_EXIT_CODE
|
|
254
|
-
assert report["is_error"] is True
|
|
116
|
+
assert exit_code == WAIT_EXIT_CODE
|
|
117
|
+
assert report["reason"] == "no account has room"
|
|
255
118
|
|
|
256
119
|
|
|
257
|
-
def
|
|
258
|
-
|
|
120
|
+
def test_should_copy_json_error_flag_into_report(
|
|
121
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
122
|
+
) -> None:
|
|
259
123
|
exit_code, report, _ = _run_worker(
|
|
260
|
-
tmp_path,
|
|
261
|
-
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
262
|
-
process=process,
|
|
124
|
+
monkeypatch, tmp_path, _outcome(stdout='{"result":"failed","is_error":true}')
|
|
263
125
|
)
|
|
264
126
|
|
|
265
127
|
assert exit_code == 0
|
|
266
|
-
assert report["result"] == "
|
|
267
|
-
assert report["is_error"] is
|
|
128
|
+
assert report["result"] == "failed"
|
|
129
|
+
assert report["is_error"] is True
|
|
268
130
|
|
|
269
131
|
|
|
270
|
-
def
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
276
|
-
process=process,
|
|
277
|
-
)
|
|
132
|
+
def test_should_keep_stdout_tail_when_output_is_not_json(
|
|
133
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
134
|
+
) -> None:
|
|
135
|
+
stdout_text = "x" * (STDOUT_TAIL_CHARACTER_LIMIT + 4)
|
|
136
|
+
exit_code, report, _ = _run_worker(monkeypatch, tmp_path, _outcome(stdout=stdout_text))
|
|
278
137
|
|
|
279
138
|
assert exit_code == 0
|
|
280
139
|
assert report["result"] == stdout_text[-STDOUT_TAIL_CHARACTER_LIMIT:]
|
|
281
140
|
assert report["is_error"] is True
|
|
282
141
|
|
|
283
142
|
|
|
284
|
-
def
|
|
285
|
-
tmp_path: Path
|
|
143
|
+
def test_should_preserve_served_failure_code(
|
|
144
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
286
145
|
) -> None:
|
|
287
|
-
process = FakeProcess(returncode=3)
|
|
288
146
|
exit_code, report, _ = _run_worker(
|
|
289
|
-
tmp_path,
|
|
290
|
-
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
291
|
-
process=process,
|
|
147
|
+
monkeypatch, tmp_path, _outcome(returncode=3, status="advisor_blocked")
|
|
292
148
|
)
|
|
293
149
|
|
|
294
150
|
assert exit_code == 3
|
|
295
|
-
assert report["account"] ==
|
|
296
|
-
assert report["exit_code"] == 3
|
|
297
|
-
assert report["is_error"] is True
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
def test_should_keep_claude_json_error_flag(tmp_path: Path) -> None:
|
|
301
|
-
process = FakeProcess(stdout='{"result":"failed","is_error":true}')
|
|
302
|
-
exit_code, report, _ = _run_worker(
|
|
303
|
-
tmp_path,
|
|
304
|
-
selection=_selection(CHOICE_SECOND, config_dir=tmp_path / "profile"),
|
|
305
|
-
process=process,
|
|
306
|
-
)
|
|
307
|
-
|
|
308
|
-
assert exit_code == 0
|
|
309
|
-
assert report["result"] == "failed"
|
|
151
|
+
assert report["account"] == "extra_2"
|
|
310
152
|
assert report["is_error"] is True
|
|
311
153
|
|
|
312
154
|
|
|
313
|
-
def
|
|
155
|
+
def test_should_write_report_for_unreadable_prompt(
|
|
314
156
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
315
157
|
) -> None:
|
|
316
|
-
main_home = tmp_path / "main-home"
|
|
317
|
-
second_home = tmp_path / "second-home"
|
|
318
|
-
monkeypatch.setattr(Path, "home", lambda: main_home)
|
|
319
|
-
monkeypatch.setattr(claude_account_worker, "default_profile_home", lambda: second_home)
|
|
320
|
-
|
|
321
|
-
selection = select_account()
|
|
322
|
-
|
|
323
|
-
assert selection.account == CHOICE_SECOND
|
|
324
|
-
assert selection.config_dir == second_home
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
def test_should_read_profile_list_and_pick_later_extra(
|
|
328
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
329
|
-
) -> None:
|
|
330
|
-
main_home = tmp_path / ".claude"
|
|
331
|
-
main_home.mkdir()
|
|
332
|
-
(main_home / "extra-profiles.json").write_text(
|
|
333
|
-
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
334
|
-
)
|
|
335
|
-
profiles_root = tmp_path / "profiles"
|
|
336
|
-
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
337
|
-
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(profiles_root))
|
|
338
|
-
readings = {
|
|
339
|
-
profiles_root / "profile_a": AccountUsageMeters(
|
|
340
|
-
session_utilization=90.0,
|
|
341
|
-
session_resets_at=NOW + timedelta(hours=2),
|
|
342
|
-
weekly_utilization=95.0,
|
|
343
|
-
weekly_resets_at=NOW + timedelta(days=2),
|
|
344
|
-
),
|
|
345
|
-
profiles_root / "profile_b": AccountUsageMeters(
|
|
346
|
-
session_utilization=10.0,
|
|
347
|
-
session_resets_at=NOW + timedelta(hours=2),
|
|
348
|
-
weekly_utilization=20.0,
|
|
349
|
-
weekly_resets_at=NOW + timedelta(days=2),
|
|
350
|
-
),
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
354
|
-
return readings.get(path.parent)
|
|
355
|
-
|
|
356
|
-
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
357
|
-
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
358
|
-
|
|
359
|
-
selection = select_account()
|
|
360
|
-
|
|
361
|
-
assert selection.account == "extra_2"
|
|
362
|
-
assert selection.config_dir == profiles_root / "profile_b"
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
def test_should_exit_three_when_every_listed_profile_is_full(
|
|
366
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
367
|
-
) -> None:
|
|
368
|
-
main_home = tmp_path / ".claude"
|
|
369
|
-
main_home.mkdir()
|
|
370
|
-
(main_home / "extra-profiles.json").write_text(
|
|
371
|
-
json.dumps(["profile_a", "profile_b"]), encoding="utf-8"
|
|
372
|
-
)
|
|
373
|
-
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
374
|
-
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path / "profiles"))
|
|
375
|
-
blocked = AccountUsageMeters(
|
|
376
|
-
session_utilization=90.0,
|
|
377
|
-
session_resets_at=NOW + timedelta(hours=2),
|
|
378
|
-
weekly_utilization=95.0,
|
|
379
|
-
weekly_resets_at=NOW + timedelta(days=2),
|
|
380
|
-
)
|
|
381
|
-
|
|
382
|
-
def read_meters(path: Path) -> AccountUsageMeters | None:
|
|
383
|
-
return None if path.parent == main_home else blocked
|
|
384
|
-
|
|
385
|
-
monkeypatch.setattr(claude_account_choice, "read_account_meters", read_meters)
|
|
386
|
-
monkeypatch.setattr(claude_account_worker, "read_account_meters", read_meters)
|
|
387
158
|
report_file = tmp_path / "report.json"
|
|
159
|
+
monkeypatch.setattr(worker, "worker_job_runner", lambda *args, **kwargs: pytest.fail("broker ran"))
|
|
388
160
|
|
|
389
|
-
exit_code = run_worker(
|
|
390
|
-
prompt_file=tmp_path / "
|
|
161
|
+
exit_code = worker.run_worker(
|
|
162
|
+
prompt_file=tmp_path / "missing.md",
|
|
391
163
|
cwd=tmp_path,
|
|
392
164
|
report_file=report_file,
|
|
393
165
|
model=None,
|
|
394
166
|
permission_mode="auto",
|
|
395
167
|
timeout_minutes=60,
|
|
396
|
-
decision_selector=select_account,
|
|
397
|
-
dependencies=WorkerDependencies(
|
|
398
|
-
process_factory=lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
399
|
-
AssertionError("wait must not start a process")
|
|
400
|
-
),
|
|
401
|
-
all_parent_environment_variables={},
|
|
402
|
-
binary_resolver=lambda _: None,
|
|
403
|
-
process_terminator=lambda _: None,
|
|
404
|
-
monotonic_clock=lambda: 1.0,
|
|
405
|
-
),
|
|
406
168
|
)
|
|
407
|
-
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
408
169
|
|
|
409
|
-
|
|
410
|
-
assert report["
|
|
411
|
-
assert
|
|
170
|
+
report = json.loads(report_file.read_text(encoding="utf-8"))
|
|
171
|
+
assert exit_code == report["exit_code"]
|
|
172
|
+
assert report["reason"] == "prompt file is unreadable"
|
|
412
173
|
|
|
413
174
|
|
|
414
|
-
def
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
process=FakeProcess(),
|
|
175
|
+
def test_should_report_broker_configuration_error(
|
|
176
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
177
|
+
) -> None:
|
|
178
|
+
exit_code, report, _ = _run_worker(
|
|
179
|
+
monkeypatch, tmp_path, BrokerConfigurationError("account list is invalid")
|
|
420
180
|
)
|
|
421
181
|
|
|
422
|
-
assert exit_code ==
|
|
423
|
-
assert report["
|
|
424
|
-
assert captured["env"][CLAUDE_CONFIG_DIR_ENV_VAR] == str(profile_dir)
|
|
182
|
+
assert exit_code == 2
|
|
183
|
+
assert report["reason"] == "account list is invalid"
|
|
425
184
|
|
|
426
185
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
)
|
|
431
|
-
def test_should_reject_invalid_profile_lists(tmp_path: Path, contents: str) -> None:
|
|
432
|
-
main_home = tmp_path / ".claude"
|
|
433
|
-
main_home.mkdir()
|
|
434
|
-
(main_home / "extra-profiles.json").write_text(contents, encoding="utf-8")
|
|
186
|
+
def test_should_ignore_extra_profile_list(
|
|
187
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
188
|
+
) -> None:
|
|
189
|
+
(tmp_path / "extra-profiles.json").write_text("{", encoding="utf-8")
|
|
435
190
|
|
|
436
|
-
|
|
437
|
-
claude_account_worker.extra_config_directories(main_home)
|
|
191
|
+
exit_code, report, _ = _run_worker(monkeypatch, tmp_path, _outcome())
|
|
438
192
|
|
|
193
|
+
assert exit_code == 0
|
|
194
|
+
assert report["account"] == "extra_2"
|
|
439
195
|
|
|
440
|
-
def test_should_require_prompt_file_and_report_file_flags() -> None:
|
|
441
|
-
parser = build_argument_parser()
|
|
442
196
|
|
|
443
|
-
|
|
197
|
+
def test_should_require_prompt_and_report_flags() -> None:
|
|
198
|
+
arguments = worker.build_argument_parser().parse_args(
|
|
444
199
|
["--prompt-file", "brief.md", "--report-file", "report.json"]
|
|
445
200
|
)
|
|
446
201
|
|