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.
Files changed (36) hide show
  1. package/_shared/advisor/advisor-protocol.md +2 -2
  2. package/_shared/advisor/reference/cli-chain.md +9 -12
  3. package/_shared/advisor/reference/lifecycle.md +1 -1
  4. package/_shared/advisor/reference/third-party-bind.md +3 -3
  5. package/bin/install.mjs +39 -3
  6. package/bin/install.shared-settings.test.mjs +46 -0
  7. package/bin/install.test.mjs +1 -1
  8. package/docs/second-claude-account.md +11 -13
  9. package/package.json +1 -1
  10. package/scripts/_code_review_test_support.py +29 -39
  11. package/scripts/claude_account_worker.py +46 -297
  12. package/scripts/claude_account_worker_process.py +18 -154
  13. package/scripts/claude_account_worker_report.py +8 -4
  14. package/scripts/claude_chain_usage.py +7 -271
  15. package/scripts/codec_forwarding_test_support.py +11 -65
  16. package/scripts/dev_env_scripts_constants/claude_account_worker_constants.py +6 -24
  17. package/scripts/invoke_code_review.py +34 -56
  18. package/scripts/policy_lint/config/approved_test_pairs.py +1 -0
  19. package/scripts/resolve_worker_spawn.py +40 -72
  20. package/scripts/test_account_broker_guard.py +0 -4
  21. package/scripts/test_claude_account_worker.py +105 -350
  22. package/scripts/test_claude_chain_usage.py +50 -510
  23. package/scripts/test_dispatcher_profile_import.py +5 -9
  24. package/scripts/test_invoke_code_review.py +7 -6
  25. package/scripts/test_invoke_code_review_chain.py +26 -0
  26. package/scripts/test_invoke_code_review_cli.py +8 -8
  27. package/scripts/test_invoke_code_review_codec.py +3 -11
  28. package/scripts/test_invoke_code_review_contract.py +5 -1
  29. package/scripts/test_resolve_worker_spawn.py +118 -265
  30. package/scripts/test_resolve_worker_spawn_codec.py +9 -21
  31. package/scripts/tests/test_code_review_constants.py +2 -2
  32. package/scripts/tests/test_policy_lint_rules_pairing.py +13 -0
  33. package/scripts/claude_account_choice.py +0 -422
  34. package/scripts/claude_chain_runner.py +0 -1289
  35. package/scripts/test_claude_account_choice.py +0 -355
  36. package/scripts/test_claude_chain_runner.py +0 -1839
@@ -1,355 +0,0 @@
1
- """Tests for the picker that names which Claude account a runner job uses."""
2
-
3
- from __future__ import annotations
4
-
5
- import json
6
- from datetime import datetime, timedelta, timezone
7
- from pathlib import Path
8
-
9
- import pytest
10
-
11
- import claude_account_choice as choice
12
- from claude_chain_usage import AccountUsageMeters, WeeklyUtilizationProbeError
13
-
14
- NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
15
-
16
-
17
- def meters(
18
- *,
19
- session_used: float | None,
20
- weekly_used: float | None,
21
- weekly_resets_in: timedelta | None,
22
- session_resets_in: timedelta = timedelta(hours=2),
23
- ) -> AccountUsageMeters:
24
- return AccountUsageMeters(
25
- session_utilization=session_used,
26
- session_resets_at=NOW + session_resets_in,
27
- weekly_utilization=weekly_used,
28
- weekly_resets_at=None if weekly_resets_in is None else NOW + weekly_resets_in,
29
- )
30
-
31
-
32
- class TestChooseAccount:
33
- def test_should_pick_main_when_week_resets_soon(self) -> None:
34
- main_meters = meters(
35
- session_used=10.0, weekly_used=80.0, weekly_resets_in=timedelta(hours=5)
36
- )
37
- second_meters = meters(
38
- session_used=10.0, weekly_used=82.0, weekly_resets_in=timedelta(days=4)
39
- )
40
- decision = choice.choose_account(
41
- main_meters=main_meters, second_meters=second_meters, now=NOW
42
- )
43
- assert decision.account == choice.CHOICE_MAIN
44
-
45
- def test_should_pick_second_when_main_week_resets_later_than_a_day(self) -> None:
46
- main_meters = meters(
47
- session_used=0.0, weekly_used=5.0, weekly_resets_in=timedelta(hours=30)
48
- )
49
- second_meters = meters(
50
- session_used=10.0, weekly_used=40.0, weekly_resets_in=timedelta(days=4)
51
- )
52
- decision = choice.choose_account(
53
- main_meters=main_meters, second_meters=second_meters, now=NOW
54
- )
55
- assert decision.account == choice.CHOICE_SECOND
56
-
57
- def test_should_keep_main_once_it_nears_its_weekly_ceiling(self) -> None:
58
- main_meters = meters(
59
- session_used=0.0, weekly_used=90.0, weekly_resets_in=timedelta(hours=3)
60
- )
61
- second_meters = meters(
62
- session_used=0.0, weekly_used=10.0, weekly_resets_in=timedelta(days=4)
63
- )
64
- decision = choice.choose_account(
65
- main_meters=main_meters, second_meters=second_meters, now=NOW
66
- )
67
- assert decision.account == choice.CHOICE_SECOND
68
-
69
- def test_should_keep_main_when_its_five_hour_window_is_half_spent(self) -> None:
70
- main_meters = meters(
71
- session_used=50.0, weekly_used=20.0, weekly_resets_in=timedelta(hours=3)
72
- )
73
- second_meters = meters(
74
- session_used=0.0, weekly_used=10.0, weekly_resets_in=timedelta(days=4)
75
- )
76
- decision = choice.choose_account(
77
- main_meters=main_meters, second_meters=second_meters, now=NOW
78
- )
79
- assert decision.account == choice.CHOICE_SECOND
80
-
81
- def test_should_never_pick_main_when_its_meter_is_unread(self) -> None:
82
- second_meters = meters(
83
- session_used=95.0, weekly_used=99.0, weekly_resets_in=timedelta(days=4)
84
- )
85
- decision = choice.choose_account(
86
- main_meters=None, second_meters=second_meters, now=NOW
87
- )
88
- assert decision.account == choice.CHOICE_WAIT
89
-
90
- def test_should_never_pick_main_when_its_reset_time_is_missing(self) -> None:
91
- main_meters = meters(session_used=0.0, weekly_used=0.0, weekly_resets_in=None)
92
- second_meters = meters(
93
- session_used=95.0, weekly_used=99.0, weekly_resets_in=timedelta(days=4)
94
- )
95
- decision = choice.choose_account(
96
- main_meters=main_meters, second_meters=second_meters, now=NOW
97
- )
98
- assert decision.account == choice.CHOICE_WAIT
99
-
100
- def test_should_pick_second_when_its_meter_is_unread(self) -> None:
101
- decision = choice.choose_account(main_meters=None, second_meters=None, now=NOW)
102
- assert decision.account == choice.CHOICE_SECOND
103
- assert decision.reason == choice.REASON_SECOND_UNREAD
104
-
105
- def test_should_wait_and_name_the_session_reset_when_the_five_hour_window_blocks(
106
- self,
107
- ) -> None:
108
- second_meters = meters(
109
- session_used=90.0,
110
- weekly_used=50.0,
111
- weekly_resets_in=timedelta(days=4),
112
- session_resets_in=timedelta(hours=1),
113
- )
114
- decision = choice.choose_account(
115
- main_meters=None, second_meters=second_meters, now=NOW
116
- )
117
- assert decision.account == choice.CHOICE_WAIT
118
- assert (NOW + timedelta(hours=1)).isoformat() in decision.reason
119
-
120
- def test_should_wait_and_name_the_weekly_reset_when_the_week_blocks(self) -> None:
121
- second_meters = meters(
122
- session_used=10.0, weekly_used=95.0, weekly_resets_in=timedelta(days=2)
123
- )
124
- decision = choice.choose_account(
125
- main_meters=None, second_meters=second_meters, now=NOW
126
- )
127
- assert decision.account == choice.CHOICE_WAIT
128
- assert (NOW + timedelta(days=2)).isoformat() in decision.reason
129
-
130
-
131
- class TestReadAccountMeters:
132
- def test_should_return_none_when_the_probe_cannot_read_the_account(
133
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
134
- ) -> None:
135
- def failing_probe(credentials_path: Path) -> AccountUsageMeters:
136
- raise WeeklyUtilizationProbeError(f"no token in {credentials_path}")
137
-
138
- monkeypatch.setattr(choice, "probe_account_meters", failing_probe)
139
- assert choice.read_account_meters(tmp_path / ".credentials.json") is None
140
-
141
- def test_should_return_the_probed_meters(
142
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
143
- ) -> None:
144
- probed = meters(
145
- session_used=1.0, weekly_used=2.0, weekly_resets_in=timedelta(days=1)
146
- )
147
- monkeypatch.setattr(choice, "probe_account_meters", lambda credentials_path: probed)
148
- assert choice.read_account_meters(tmp_path / ".credentials.json") == probed
149
-
150
-
151
- class TestDecisionPayload:
152
- def test_should_name_no_config_directory_for_a_wait(self) -> None:
153
- decision = choice.AccountDecision(account=choice.CHOICE_WAIT, reason="full")
154
- assert choice.decision_payload(decision, config_directory=None) == {
155
- "account": "wait",
156
- "config_dir": None,
157
- "reason": "full",
158
- }
159
-
160
-
161
- class TestMetersPayload:
162
- def test_should_name_no_meters_for_an_unread_account(self) -> None:
163
- assert choice.meters_payload(None) is None
164
-
165
- def test_should_carry_each_used_percent_and_reset_time(self) -> None:
166
- read_meters = meters(
167
- session_used=12.0,
168
- weekly_used=34.0,
169
- weekly_resets_in=timedelta(days=3),
170
- session_resets_in=timedelta(hours=1),
171
- )
172
- assert choice.meters_payload(read_meters) == {
173
- "session_used_percent": 12.0,
174
- "session_resets_at": "2026-09-22T22:00:00+00:00",
175
- "weekly_used_percent": 34.0,
176
- "weekly_resets_at": "2026-09-25T21:00:00+00:00",
177
- }
178
-
179
- def test_should_name_no_reset_time_the_meter_left_out(self) -> None:
180
- read_meters = meters(session_used=1.0, weekly_used=None, weekly_resets_in=None)
181
- payload = choice.meters_payload(read_meters)
182
- assert payload is not None
183
- assert payload["weekly_used_percent"] is None
184
- assert payload["weekly_resets_at"] is None
185
-
186
-
187
- class TestMain:
188
- def test_should_print_the_choice_and_the_config_directory_of_the_chosen_account(
189
- self,
190
- tmp_path: Path,
191
- monkeypatch: pytest.MonkeyPatch,
192
- capsys: pytest.CaptureFixture[str],
193
- ) -> None:
194
- main_home = tmp_path / "main"
195
- second_home = tmp_path / "second"
196
- meters_by_credentials_path = {
197
- main_home / ".credentials.json": None,
198
- second_home / ".credentials.json": meters(
199
- session_used=0.0, weekly_used=10.0, weekly_resets_in=timedelta(days=4)
200
- ),
201
- }
202
- monkeypatch.setattr(
203
- choice,
204
- "read_account_meters",
205
- lambda credentials_path: meters_by_credentials_path[credentials_path],
206
- )
207
- exit_code = choice.main(
208
- [
209
- "--main-config-dir",
210
- str(main_home),
211
- "--second-config-dir",
212
- str(second_home),
213
- ]
214
- )
215
- printed = json.loads(capsys.readouterr().out)
216
- assert exit_code == 0
217
- assert printed["account"] == choice.CHOICE_SECOND
218
- assert printed["config_dir"] == str(second_home)
219
- assert printed["meters"]["main"] is None
220
- assert printed["meters"]["second"]["weekly_used_percent"] == 10.0
221
-
222
- def test_should_print_no_config_directory_when_the_job_waits(
223
- self,
224
- tmp_path: Path,
225
- monkeypatch: pytest.MonkeyPatch,
226
- capsys: pytest.CaptureFixture[str],
227
- ) -> None:
228
- blocked_meters = meters(
229
- session_used=99.0, weekly_used=99.0, weekly_resets_in=timedelta(days=4)
230
- )
231
- monkeypatch.setattr(
232
- choice, "read_account_meters", lambda credentials_path: blocked_meters
233
- )
234
- exit_code = choice.main(
235
- [
236
- "--main-config-dir",
237
- str(tmp_path / "main"),
238
- "--second-config-dir",
239
- str(tmp_path / "second"),
240
- ]
241
- )
242
- printed = json.loads(capsys.readouterr().out)
243
- assert exit_code == 0
244
- assert printed["account"] == choice.CHOICE_WAIT
245
- assert printed["config_dir"] is None
246
-
247
-
248
- def test_should_pick_second_extra_in_order_and_print_every_meter(
249
- tmp_path: Path,
250
- monkeypatch: pytest.MonkeyPatch,
251
- capsys: pytest.CaptureFixture[str],
252
- ) -> None:
253
- main_home = tmp_path / "main"
254
- first_extra = tmp_path / "first"
255
- second_extra = tmp_path / "next"
256
- third_extra = tmp_path / "last"
257
- readings = {
258
- main_home: None,
259
- first_extra: meters(
260
- session_used=90.0, weekly_used=95.0, weekly_resets_in=timedelta(days=4)
261
- ),
262
- second_extra: meters(
263
- session_used=10.0, weekly_used=20.0, weekly_resets_in=timedelta(days=4)
264
- ),
265
- third_extra: meters(
266
- session_used=30.0, weekly_used=40.0, weekly_resets_in=timedelta(days=4)
267
- ),
268
- }
269
- monkeypatch.setattr(
270
- choice, "read_account_meters", lambda path: readings[path.parent]
271
- )
272
-
273
- assert choice.main(
274
- [
275
- "--main-config-dir",
276
- str(main_home),
277
- "--second-config-dir",
278
- str(first_extra),
279
- "--extra-config-dir",
280
- str(second_extra),
281
- "--extra-config-dir",
282
- str(third_extra),
283
- ]
284
- ) == 0
285
- printed = json.loads(capsys.readouterr().out)
286
-
287
- assert printed["account"] == "extra_2"
288
- assert printed["config_dir"] == str(second_extra)
289
- assert list(printed["meters"]) == ["main", "second", "extra_2", "extra_3"]
290
- assert printed["meters"]["main"] is None
291
- assert printed["meters"]["second"]["weekly_used_percent"] == 95.0
292
- assert printed["meters"]["extra_2"]["weekly_used_percent"] == 20.0
293
- assert printed["meters"]["extra_3"]["weekly_used_percent"] == 40.0
294
-
295
-
296
- def test_should_wait_for_first_usable_reset_when_all_extras_are_full(
297
- tmp_path: Path,
298
- monkeypatch: pytest.MonkeyPatch,
299
- capsys: pytest.CaptureFixture[str],
300
- ) -> None:
301
- first_extra = tmp_path / "first"
302
- second_extra = tmp_path / "next"
303
- third_extra = tmp_path / "last"
304
- readings = {
305
- first_extra: meters(
306
- session_used=90.0,
307
- weekly_used=95.0,
308
- weekly_resets_in=timedelta(hours=1),
309
- session_resets_in=timedelta(hours=3),
310
- ),
311
- second_extra: meters(
312
- session_used=10.0, weekly_used=95.0, weekly_resets_in=timedelta(hours=2)
313
- ),
314
- third_extra: meters(
315
- session_used=90.0, weekly_used=10.0, weekly_resets_in=timedelta(days=4),
316
- session_resets_in=timedelta(hours=4),
317
- ),
318
- }
319
- monkeypatch.setattr(
320
- choice, "read_account_meters", lambda path: readings.get(path.parent)
321
- )
322
-
323
- assert choice.main(
324
- [
325
- "--main-config-dir",
326
- str(tmp_path / "main"),
327
- "--second-config-dir",
328
- str(first_extra),
329
- "--extra-config-dir",
330
- str(second_extra),
331
- "--extra-config-dir",
332
- str(third_extra),
333
- ]
334
- ) == 0
335
- printed = json.loads(capsys.readouterr().out)
336
-
337
- assert printed["account"] == choice.CHOICE_WAIT
338
- assert printed["config_dir"] is None
339
- assert (NOW + timedelta(hours=2)).isoformat() in printed["reason"]
340
- assert (NOW + timedelta(hours=1)).isoformat() not in printed["reason"]
341
- assert list(printed["meters"]) == ["main", "second", "extra_2", "extra_3"]
342
-
343
-
344
- def test_should_reject_duplicate_extra_config_directories(tmp_path: Path) -> None:
345
- with pytest.raises(SystemExit) as error:
346
- choice.main(
347
- [
348
- "--second-config-dir",
349
- str(tmp_path / "first"),
350
- "--extra-config-dir",
351
- str(tmp_path / "first"),
352
- ]
353
- )
354
-
355
- assert error.value.code == 2