claude-dev-env 8.38.2 → 8.39.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.
@@ -1,18 +1,17 @@
1
- """Tests for the picker that names which Codex account a runner job uses."""
1
+ """Tests for Codex account rosters, homes, and launchers."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
6
  import os
7
7
  from collections.abc import Callable, Iterator
8
- from datetime import datetime, timedelta, timezone
8
+ from datetime import datetime, timezone
9
9
  from pathlib import Path
10
10
 
11
11
  import pytest
12
12
 
13
13
  import codex_account_choice as choice
14
14
  from claude_account_profile import links_to
15
- from codex_account_meters import CodexAccountMeters, CodexMeterUnreadError, UsageWindow
16
15
 
17
16
  NOW = datetime(2026, 9, 23, 17, 0, tzinfo=timezone.utc)
18
17
  ALL_SHARED_ENTRY_NAMES = (
@@ -71,202 +70,6 @@ def install(main_home: Path, tmp_path: Path, *all_names: str) -> dict[str, dict[
71
70
  )
72
71
 
73
72
 
74
- def reading(
75
- name: str,
76
- *,
77
- weekly_used: float,
78
- short_used: float = 0.0,
79
- weekly_resets_in: timedelta = timedelta(days=3),
80
- ) -> choice.AccountReading:
81
- return choice.AccountReading(
82
- name=name,
83
- codex_home=Path("/profiles") / name,
84
- meters=CodexAccountMeters(
85
- all_windows=(
86
- UsageWindow(300, short_used, NOW + timedelta(hours=2)),
87
- UsageWindow(10080, weekly_used, NOW + weekly_resets_in),
88
- )
89
- ),
90
- )
91
-
92
-
93
- def unread(name: str) -> choice.AccountReading:
94
- return choice.AccountReading(name, Path("/profiles") / name, None, "not signed in")
95
-
96
-
97
- class TestChooseCodexAccount:
98
- def should_take_the_first_account_in_order_with_room(self) -> None:
99
- decision = choice.choose_codex_account(
100
- [reading("codex-1", weekly_used=50.0), reading("codex-2", weekly_used=0.0)]
101
- )
102
- assert (decision.tier, decision.reading.name) == ("normal", "codex-1")
103
- assert decision.stop_below_percent is None
104
-
105
- def should_pass_over_an_account_at_the_ten_percent_bar(self) -> None:
106
- decision = choice.choose_codex_account(
107
- [reading("codex-1", weekly_used=90.0), reading("codex-2", weekly_used=60.0)]
108
- )
109
- assert (decision.tier, decision.reading.name) == ("normal", "codex-2")
110
-
111
- def should_measure_room_by_the_tightest_window(self) -> None:
112
- decision = choice.choose_codex_account(
113
- [
114
- reading("codex-1", weekly_used=10.0, short_used=95.0),
115
- reading("codex-2", weekly_used=70.0),
116
- ]
117
- )
118
- assert decision.reading.name == "codex-2"
119
-
120
- def should_skip_an_unread_account(self) -> None:
121
- decision = choice.choose_codex_account(
122
- [unread("codex-1"), reading("codex-2", weekly_used=20.0)]
123
- )
124
- assert (decision.tier, decision.reading.name) == ("normal", "codex-2")
125
-
126
- def should_fall_back_to_luna_on_the_roomiest_account_under_the_bar(self) -> None:
127
- decision = choice.choose_codex_account(
128
- [
129
- reading("codex-1", weekly_used=97.0),
130
- reading("codex-2", weekly_used=92.0),
131
- reading("codex-3", weekly_used=100.0),
132
- ]
133
- )
134
- assert (decision.tier, decision.reading.name) == ("luna", "codex-2")
135
- assert decision.stop_below_percent == 1.0
136
-
137
- def should_keep_luna_off_an_account_under_twenty_percent_of_its_five_hour_window(
138
- self,
139
- ) -> None:
140
- decision = choice.choose_codex_account(
141
- [
142
- reading("codex-1", weekly_used=92.0, short_used=85.0),
143
- reading("codex-2", weekly_used=97.0, short_used=80.0),
144
- ]
145
- )
146
- assert (decision.tier, decision.reading.name) == ("luna", "codex-2")
147
-
148
- def should_run_luna_on_an_account_with_no_five_hour_window(self) -> None:
149
- weekly_only = choice.AccountReading(
150
- "codex-3",
151
- Path("/profiles/codex-3"),
152
- CodexAccountMeters((UsageWindow(10080, 95.0, NOW + timedelta(days=1)),)),
153
- )
154
- decision = choice.choose_codex_account(
155
- [reading("codex-1", weekly_used=92.0, short_used=90.0), weekly_only]
156
- )
157
- assert (decision.tier, decision.reading.name) == ("luna", "codex-3")
158
-
159
- def should_wait_on_the_account_that_resets_first_when_none_passes_one_percent(
160
- self,
161
- ) -> None:
162
- decision = choice.choose_codex_account(
163
- [
164
- reading(
165
- "codex-1", weekly_used=99.5, weekly_resets_in=timedelta(days=2)
166
- ),
167
- reading(
168
- "codex-2", weekly_used=100.0, weekly_resets_in=timedelta(hours=6)
169
- ),
170
- ]
171
- )
172
- assert decision.tier == "wait"
173
- assert decision.reading.name == "codex-2"
174
- assert (NOW + timedelta(hours=6)).isoformat() in decision.reason
175
-
176
- def should_wait_when_no_meter_reads(self) -> None:
177
- decision = choice.choose_codex_account([unread("codex-1"), unread("codex-2")])
178
- assert (decision.tier, decision.reading) == ("wait", None)
179
-
180
-
181
- class TestDecisionPayload:
182
- def should_name_no_account_or_home_on_wait(self) -> None:
183
- all_readings = [reading("codex-1", weekly_used=100.0)]
184
- payload = choice.decision_payload(
185
- choice.choose_codex_account(all_readings), all_readings
186
- )
187
- assert (payload["tier"], payload["account"], payload["codex_home"]) == (
188
- "wait",
189
- None,
190
- None,
191
- )
192
-
193
- def should_report_every_account_and_the_chosen_home(self) -> None:
194
- all_readings = [unread("codex-1"), reading("codex-2", weekly_used=25.0)]
195
- payload = choice.decision_payload(
196
- choice.choose_codex_account(all_readings), all_readings
197
- )
198
- assert payload["codex_home"] == str(Path("/profiles") / "codex-2")
199
- assert payload["percent_left"] == 75.0
200
- assert [each["name"] for each in payload["accounts"]] == ["codex-1", "codex-2"]
201
- assert payload["accounts"][0]["unread"] == "not signed in"
202
- json.dumps(payload)
203
-
204
-
205
- class TestReadAccount:
206
- def should_call_an_account_without_a_sign_in_unread(self, tmp_path: Path) -> None:
207
- def never_called(codex_home: Path) -> CodexAccountMeters:
208
- raise AssertionError("no sign-in, no read")
209
-
210
- account_reading = choice.read_account("codex-1", tmp_path, never_called)
211
- assert (account_reading.meters, account_reading.unread_reason) == (None, "not signed in")
212
-
213
- def should_read_under_the_accounts_own_home(self, tmp_path: Path) -> None:
214
- (tmp_path / "codex-2").mkdir()
215
- (tmp_path / "codex-2" / "auth.json").write_text("{}")
216
- all_homes: list[Path] = []
217
-
218
- def fake_reader(codex_home: Path) -> CodexAccountMeters:
219
- all_homes.append(codex_home)
220
- return CodexAccountMeters((UsageWindow(10080, 40.0, None),))
221
-
222
- account_reading = choice.read_account("codex-2", tmp_path, fake_reader)
223
- assert all_homes == [tmp_path / "codex-2"]
224
- assert account_reading.meters.percent_left == 60.0
225
-
226
- def should_keep_the_unread_reason(self, tmp_path: Path) -> None:
227
- (tmp_path / "codex-1").mkdir()
228
- (tmp_path / "codex-1" / "auth.json").write_text("{}")
229
-
230
- def failing_reader(codex_home: Path) -> CodexAccountMeters:
231
- raise CodexMeterUnreadError("codex app-server sent no rate-limit reply")
232
-
233
- account_reading = choice.read_account("codex-1", tmp_path, failing_reader)
234
- assert account_reading.unread_reason == "codex app-server sent no rate-limit reply"
235
-
236
-
237
- class TestCheck:
238
- @pytest.mark.parametrize(
239
- ("weekly_used", "exit_code"), [(98.0, 0), (99.0, 3), (100.0, 3)]
240
- )
241
- def should_answer_room_only_above_the_floor(
242
- self,
243
- tmp_path: Path,
244
- monkeypatch: pytest.MonkeyPatch,
245
- weekly_used: float,
246
- exit_code: int,
247
- ) -> None:
248
- (tmp_path / "codex-3").mkdir()
249
- (tmp_path / "codex-3" / "auth.json").write_text("{}")
250
- monkeypatch.setattr(
251
- choice,
252
- "_meter_reader",
253
- lambda codex_path: (
254
- lambda codex_home: CodexAccountMeters(
255
- (UsageWindow(10080, weekly_used, None),)
256
- )
257
- ),
258
- )
259
- assert (
260
- choice.main(["--profiles-root", str(tmp_path), "check", "codex-3"])
261
- == exit_code
262
- )
263
-
264
- def should_answer_no_room_for_an_unread_account(
265
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
266
- ) -> None:
267
- monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
268
- assert choice.main(["--profiles-root", str(tmp_path), "check", "codex-4"]) == 3
269
-
270
73
 
271
74
  class TestSync:
272
75
  @pytest.mark.skipif(
@@ -326,13 +129,14 @@ class TestSmallHelpers:
326
129
  monkeypatch.delenv("CODEX_PROFILES_ROOT", raising=False)
327
130
  assert choice.default_profiles_root() == Path.home() / ".codex-profiles"
328
131
 
329
- def should_list_each_window_in_a_read_accounts_payload(self) -> None:
330
- payload = choice.reading_payload(reading("codex-1", weekly_used=40.0, short_used=5.0))
331
- assert payload["percent_left"] == 60.0
332
- assert [each["minutes"] for each in payload["windows"]] == [300, 10080]
333
-
334
132
 
335
133
  class TestAccountRoster:
134
+ @pytest.mark.parametrize("command", ["choose", "check"])
135
+ def test_should_reject_removed_choice_commands(self, command: str) -> None:
136
+ with pytest.raises(SystemExit) as exit_info:
137
+ choice.main([command])
138
+ assert exit_info.value.code == 2
139
+
336
140
  def should_take_the_environment_roster_over_the_saved_file(
337
141
  self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
338
142
  ) -> None:
@@ -364,12 +168,12 @@ class TestAccountRoster:
364
168
  with pytest.raises(ValueError, match="account-launchers.json"):
365
169
  choice.saved_codex_account_names(tmp_path)
366
170
 
367
- def should_refuse_a_saved_file_that_is_not_json(
171
+ def test_should_refuse_a_saved_file_that_is_not_json(
368
172
  self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
369
173
  ) -> None:
370
174
  (tmp_path / "account-launchers.json").write_text('["alpha",', encoding="utf-8")
371
175
  with pytest.raises(SystemExit) as exit_info:
372
- choice.main(["--profiles-root", str(tmp_path), "check", "alpha"])
176
+ choice.main(["--profiles-root", str(tmp_path), "sync"])
373
177
  assert exit_info.value.code == 2
374
178
  assert "account-launchers.json" in capsys.readouterr().err
375
179
 
@@ -398,48 +202,7 @@ class TestAccountRoster:
398
202
  )
399
203
 
400
204
 
401
- class TestRosterDrivesTheJobCommands:
402
- def should_try_the_roster_in_order_and_skip_the_fixed_accounts(
403
- self,
404
- tmp_path: Path,
405
- monkeypatch: pytest.MonkeyPatch,
406
- capsys: pytest.CaptureFixture[str],
407
- ) -> None:
408
- all_weekly_used = {"beta": 95.0, "alpha": 50.0, "codex-1": 0.0}
409
- for each_name in all_weekly_used:
410
- (tmp_path / each_name).mkdir()
411
- (tmp_path / each_name / "auth.json").write_text("{}")
412
- monkeypatch.setenv("CODEX_ACCOUNT_PROFILES", "beta,alpha")
413
- monkeypatch.setattr(
414
- choice,
415
- "_meter_reader",
416
- lambda codex_path: (
417
- lambda codex_home: CodexAccountMeters(
418
- (UsageWindow(10080, all_weekly_used[codex_home.name], None),)
419
- )
420
- ),
421
- )
422
- assert choice.main(["--profiles-root", str(tmp_path), "choose"]) == 0
423
- payload = json.loads(capsys.readouterr().out)
424
- assert (payload["tier"], payload["account"]) == ("normal", "alpha")
425
- assert [each["name"] for each in payload["accounts"]] == ["beta", "alpha"]
426
-
427
- def should_choose_among_the_four_fixed_accounts_without_a_roster(
428
- self,
429
- tmp_path: Path,
430
- monkeypatch: pytest.MonkeyPatch,
431
- capsys: pytest.CaptureFixture[str],
432
- ) -> None:
433
- monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
434
- assert choice.main(["--profiles-root", str(tmp_path), "choose"]) == 0
435
- payload = json.loads(capsys.readouterr().out)
436
- assert [each["name"] for each in payload["accounts"]] == [
437
- "codex-1",
438
- "codex-2",
439
- "codex-3",
440
- "codex-4",
441
- ]
442
-
205
+ class TestRosterDrivesTheSyncCommand:
443
206
  def should_sync_only_the_roster_accounts(
444
207
  self,
445
208
  tmp_path: Path,
@@ -455,31 +218,6 @@ class TestRosterDrivesTheJobCommands:
455
218
  assert links_to(profiles_root / "alpha" / "agents", main_home / "agents")
456
219
  assert not (profiles_root / "codex-1").exists()
457
220
 
458
- def should_check_a_saved_roster_account(
459
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
460
- ) -> None:
461
- save_roster(tmp_path, ["alpha"])
462
- (tmp_path / "alpha").mkdir()
463
- (tmp_path / "alpha" / "auth.json").write_text("{}")
464
- monkeypatch.setattr(
465
- choice,
466
- "_meter_reader",
467
- lambda codex_path: (
468
- lambda codex_home: CodexAccountMeters((UsageWindow(10080, 40.0, None),))
469
- ),
470
- )
471
- assert choice.main(["--profiles-root", str(tmp_path), "check", "alpha"]) == 0
472
-
473
- @pytest.mark.parametrize("account", ["gamma", "codex-1"])
474
- def should_refuse_to_check_an_account_outside_the_roster(
475
- self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch, account: str
476
- ) -> None:
477
- save_roster(tmp_path, ["alpha"])
478
- monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
479
- with pytest.raises(SystemExit) as exit_info:
480
- choice.main(["--profiles-root", str(tmp_path), "check", account])
481
- assert exit_info.value.code == 2
482
-
483
221
 
484
222
  class TestInstallLaunchers:
485
223
  def should_link_the_shared_setup_and_keep_each_sign_in_per_account(