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.
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/scripts/codex_astra_advisor.py +13 -13
- package/_shared/advisor/scripts/codex_astra_preflight.py +62 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +5 -2
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +64 -33
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +125 -3
- package/_shared/pr-loop/scripts/check_convergence.py +23 -17
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +4 -3
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +64 -25
- package/docs/codex-accounts.md +27 -31
- package/hooks/session/skill_loaded_reminder.py +54 -25
- package/hooks/session/test_skill_loaded_reminder.py +94 -12
- package/package.json +1 -1
- package/scripts/codex_account_choice.py +8 -303
- package/scripts/test_codex_account_choice.py +11 -273
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
"""Tests for
|
|
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,
|
|
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
|
|
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), "
|
|
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
|
|
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(
|