claude-dev-env 8.12.0 → 8.13.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/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Tests for the picker that names which Codex account a runner job uses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timedelta, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import codex_account_choice as choice
|
|
13
|
+
from codex_account_meters import CodexAccountMeters, CodexMeterUnreadError, UsageWindow
|
|
14
|
+
|
|
15
|
+
NOW = datetime(2026, 9, 23, 17, 0, tzinfo=timezone.utc)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def reading(
|
|
19
|
+
name: str,
|
|
20
|
+
*,
|
|
21
|
+
weekly_used: float,
|
|
22
|
+
short_used: float = 0.0,
|
|
23
|
+
weekly_resets_in: timedelta = timedelta(days=3),
|
|
24
|
+
) -> choice.AccountReading:
|
|
25
|
+
return choice.AccountReading(
|
|
26
|
+
name=name,
|
|
27
|
+
codex_home=Path("/profiles") / name,
|
|
28
|
+
meters=CodexAccountMeters(
|
|
29
|
+
all_windows=(
|
|
30
|
+
UsageWindow(300, short_used, NOW + timedelta(hours=2)),
|
|
31
|
+
UsageWindow(10080, weekly_used, NOW + weekly_resets_in),
|
|
32
|
+
)
|
|
33
|
+
),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def unread(name: str) -> choice.AccountReading:
|
|
38
|
+
return choice.AccountReading(name, Path("/profiles") / name, None, "not signed in")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestChooseCodexAccount:
|
|
42
|
+
def should_take_the_first_account_in_order_with_room(self) -> None:
|
|
43
|
+
decision = choice.choose_codex_account(
|
|
44
|
+
[reading("codex-1", weekly_used=50.0), reading("codex-2", weekly_used=0.0)]
|
|
45
|
+
)
|
|
46
|
+
assert (decision.tier, decision.reading.name) == ("normal", "codex-1")
|
|
47
|
+
assert decision.stop_below_percent is None
|
|
48
|
+
|
|
49
|
+
def should_pass_over_an_account_at_the_ten_percent_bar(self) -> None:
|
|
50
|
+
decision = choice.choose_codex_account(
|
|
51
|
+
[reading("codex-1", weekly_used=90.0), reading("codex-2", weekly_used=60.0)]
|
|
52
|
+
)
|
|
53
|
+
assert (decision.tier, decision.reading.name) == ("normal", "codex-2")
|
|
54
|
+
|
|
55
|
+
def should_measure_room_by_the_tightest_window(self) -> None:
|
|
56
|
+
decision = choice.choose_codex_account(
|
|
57
|
+
[
|
|
58
|
+
reading("codex-1", weekly_used=10.0, short_used=95.0),
|
|
59
|
+
reading("codex-2", weekly_used=70.0),
|
|
60
|
+
]
|
|
61
|
+
)
|
|
62
|
+
assert decision.reading.name == "codex-2"
|
|
63
|
+
|
|
64
|
+
def should_skip_an_unread_account(self) -> None:
|
|
65
|
+
decision = choice.choose_codex_account(
|
|
66
|
+
[unread("codex-1"), reading("codex-2", weekly_used=20.0)]
|
|
67
|
+
)
|
|
68
|
+
assert (decision.tier, decision.reading.name) == ("normal", "codex-2")
|
|
69
|
+
|
|
70
|
+
def should_fall_back_to_luna_on_the_roomiest_account_under_the_bar(self) -> None:
|
|
71
|
+
decision = choice.choose_codex_account(
|
|
72
|
+
[
|
|
73
|
+
reading("codex-1", weekly_used=97.0),
|
|
74
|
+
reading("codex-2", weekly_used=92.0),
|
|
75
|
+
reading("codex-3", weekly_used=100.0),
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
assert (decision.tier, decision.reading.name) == ("luna", "codex-2")
|
|
79
|
+
assert decision.stop_below_percent == 1.0
|
|
80
|
+
|
|
81
|
+
def should_keep_luna_off_an_account_under_twenty_percent_of_its_five_hour_window(
|
|
82
|
+
self,
|
|
83
|
+
) -> None:
|
|
84
|
+
decision = choice.choose_codex_account(
|
|
85
|
+
[
|
|
86
|
+
reading("codex-1", weekly_used=92.0, short_used=85.0),
|
|
87
|
+
reading("codex-2", weekly_used=97.0, short_used=80.0),
|
|
88
|
+
]
|
|
89
|
+
)
|
|
90
|
+
assert (decision.tier, decision.reading.name) == ("luna", "codex-2")
|
|
91
|
+
|
|
92
|
+
def should_run_luna_on_an_account_with_no_five_hour_window(self) -> None:
|
|
93
|
+
weekly_only = choice.AccountReading(
|
|
94
|
+
"codex-3",
|
|
95
|
+
Path("/profiles/codex-3"),
|
|
96
|
+
CodexAccountMeters((UsageWindow(10080, 95.0, NOW + timedelta(days=1)),)),
|
|
97
|
+
)
|
|
98
|
+
decision = choice.choose_codex_account(
|
|
99
|
+
[reading("codex-1", weekly_used=92.0, short_used=90.0), weekly_only]
|
|
100
|
+
)
|
|
101
|
+
assert (decision.tier, decision.reading.name) == ("luna", "codex-3")
|
|
102
|
+
|
|
103
|
+
def should_wait_on_the_account_that_resets_first_when_none_passes_one_percent(
|
|
104
|
+
self,
|
|
105
|
+
) -> None:
|
|
106
|
+
decision = choice.choose_codex_account(
|
|
107
|
+
[
|
|
108
|
+
reading(
|
|
109
|
+
"codex-1", weekly_used=99.5, weekly_resets_in=timedelta(days=2)
|
|
110
|
+
),
|
|
111
|
+
reading(
|
|
112
|
+
"codex-2", weekly_used=100.0, weekly_resets_in=timedelta(hours=6)
|
|
113
|
+
),
|
|
114
|
+
]
|
|
115
|
+
)
|
|
116
|
+
assert decision.tier == "wait"
|
|
117
|
+
assert decision.reading.name == "codex-2"
|
|
118
|
+
assert (NOW + timedelta(hours=6)).isoformat() in decision.reason
|
|
119
|
+
|
|
120
|
+
def should_wait_when_no_meter_reads(self) -> None:
|
|
121
|
+
decision = choice.choose_codex_account([unread("codex-1"), unread("codex-2")])
|
|
122
|
+
assert (decision.tier, decision.reading) == ("wait", None)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestDecisionPayload:
|
|
126
|
+
def should_name_no_account_or_home_on_wait(self) -> None:
|
|
127
|
+
all_readings = [reading("codex-1", weekly_used=100.0)]
|
|
128
|
+
payload = choice.decision_payload(
|
|
129
|
+
choice.choose_codex_account(all_readings), all_readings
|
|
130
|
+
)
|
|
131
|
+
assert (payload["tier"], payload["account"], payload["codex_home"]) == (
|
|
132
|
+
"wait",
|
|
133
|
+
None,
|
|
134
|
+
None,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def should_report_every_account_and_the_chosen_home(self) -> None:
|
|
138
|
+
all_readings = [unread("codex-1"), reading("codex-2", weekly_used=25.0)]
|
|
139
|
+
payload = choice.decision_payload(
|
|
140
|
+
choice.choose_codex_account(all_readings), all_readings
|
|
141
|
+
)
|
|
142
|
+
assert payload["codex_home"] == str(Path("/profiles") / "codex-2")
|
|
143
|
+
assert payload["percent_left"] == 75.0
|
|
144
|
+
assert [each["name"] for each in payload["accounts"]] == ["codex-1", "codex-2"]
|
|
145
|
+
assert payload["accounts"][0]["unread"] == "not signed in"
|
|
146
|
+
json.dumps(payload)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class TestReadAccount:
|
|
150
|
+
def should_call_an_account_without_a_sign_in_unread(self, tmp_path: Path) -> None:
|
|
151
|
+
def never_called(codex_home: Path) -> CodexAccountMeters:
|
|
152
|
+
raise AssertionError("no sign-in, no read")
|
|
153
|
+
|
|
154
|
+
account_reading = choice.read_account("codex-1", tmp_path, never_called)
|
|
155
|
+
assert (account_reading.meters, account_reading.unread_reason) == (None, "not signed in")
|
|
156
|
+
|
|
157
|
+
def should_read_under_the_accounts_own_home(self, tmp_path: Path) -> None:
|
|
158
|
+
(tmp_path / "codex-2").mkdir()
|
|
159
|
+
(tmp_path / "codex-2" / "auth.json").write_text("{}")
|
|
160
|
+
all_homes: list[Path] = []
|
|
161
|
+
|
|
162
|
+
def fake_reader(codex_home: Path) -> CodexAccountMeters:
|
|
163
|
+
all_homes.append(codex_home)
|
|
164
|
+
return CodexAccountMeters((UsageWindow(10080, 40.0, None),))
|
|
165
|
+
|
|
166
|
+
account_reading = choice.read_account("codex-2", tmp_path, fake_reader)
|
|
167
|
+
assert all_homes == [tmp_path / "codex-2"]
|
|
168
|
+
assert account_reading.meters.percent_left == 60.0
|
|
169
|
+
|
|
170
|
+
def should_keep_the_unread_reason(self, tmp_path: Path) -> None:
|
|
171
|
+
(tmp_path / "codex-1").mkdir()
|
|
172
|
+
(tmp_path / "codex-1" / "auth.json").write_text("{}")
|
|
173
|
+
|
|
174
|
+
def failing_reader(codex_home: Path) -> CodexAccountMeters:
|
|
175
|
+
raise CodexMeterUnreadError("codex app-server sent no rate-limit reply")
|
|
176
|
+
|
|
177
|
+
account_reading = choice.read_account("codex-1", tmp_path, failing_reader)
|
|
178
|
+
assert account_reading.unread_reason == "codex app-server sent no rate-limit reply"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class TestCheck:
|
|
182
|
+
@pytest.mark.parametrize(
|
|
183
|
+
("weekly_used", "exit_code"), [(98.0, 0), (99.0, 3), (100.0, 3)]
|
|
184
|
+
)
|
|
185
|
+
def should_answer_room_only_above_the_floor(
|
|
186
|
+
self,
|
|
187
|
+
tmp_path: Path,
|
|
188
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
189
|
+
weekly_used: float,
|
|
190
|
+
exit_code: int,
|
|
191
|
+
) -> None:
|
|
192
|
+
(tmp_path / "codex-3").mkdir()
|
|
193
|
+
(tmp_path / "codex-3" / "auth.json").write_text("{}")
|
|
194
|
+
monkeypatch.setattr(
|
|
195
|
+
choice,
|
|
196
|
+
"_meter_reader",
|
|
197
|
+
lambda codex_path: (
|
|
198
|
+
lambda codex_home: CodexAccountMeters(
|
|
199
|
+
(UsageWindow(10080, weekly_used, None),)
|
|
200
|
+
)
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
assert (
|
|
204
|
+
choice.main(["--profiles-root", str(tmp_path), "check", "codex-3"])
|
|
205
|
+
== exit_code
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
def should_answer_no_room_for_an_unread_account(
|
|
209
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
210
|
+
) -> None:
|
|
211
|
+
monkeypatch.setattr(choice, "_meter_reader", lambda codex_path: None)
|
|
212
|
+
assert choice.main(["--profiles-root", str(tmp_path), "check", "codex-4"]) == 3
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class TestSync:
|
|
216
|
+
@pytest.mark.skipif(
|
|
217
|
+
os.name == "nt", reason="junction creation needs a Windows shell"
|
|
218
|
+
)
|
|
219
|
+
def should_link_the_shared_setup_and_keep_each_sign_in(
|
|
220
|
+
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
221
|
+
) -> None:
|
|
222
|
+
main_home = tmp_path / "main"
|
|
223
|
+
(main_home / "rules").mkdir(parents=True)
|
|
224
|
+
(main_home / "config.toml").write_text("model = 'x'")
|
|
225
|
+
(main_home / "auth.json").write_text("{}")
|
|
226
|
+
(main_home / "sessions").mkdir()
|
|
227
|
+
profiles_root = tmp_path / "profiles"
|
|
228
|
+
|
|
229
|
+
assert (
|
|
230
|
+
choice.main(
|
|
231
|
+
[
|
|
232
|
+
"--profiles-root",
|
|
233
|
+
str(profiles_root),
|
|
234
|
+
"sync",
|
|
235
|
+
"--main-home",
|
|
236
|
+
str(main_home),
|
|
237
|
+
]
|
|
238
|
+
)
|
|
239
|
+
== 0
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
report = json.loads(capsys.readouterr().out)
|
|
243
|
+
assert list(report) == ["codex-1", "codex-2", "codex-3", "codex-4"]
|
|
244
|
+
each_home = profiles_root / "codex-3"
|
|
245
|
+
assert os.readlink(each_home / "rules") == str(main_home / "rules")
|
|
246
|
+
assert os.readlink(each_home / "config.toml") == str(main_home / "config.toml")
|
|
247
|
+
assert not (each_home / "auth.json").exists()
|
|
248
|
+
assert not (each_home / "sessions").exists()
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
class TestSmallHelpers:
|
|
252
|
+
@pytest.mark.parametrize(
|
|
253
|
+
("entry_name", "is_local"),
|
|
254
|
+
[("auth.json", True), ("sessions", True), ("state_5.sqlite", True), ("config.toml", False), ("plugins", False)],
|
|
255
|
+
)
|
|
256
|
+
def should_keep_every_entry_outside_the_shared_set_per_account(
|
|
257
|
+
self, entry_name: str, is_local: bool
|
|
258
|
+
) -> None:
|
|
259
|
+
assert choice.is_account_local_codex_entry(entry_name) is is_local
|
|
260
|
+
|
|
261
|
+
def should_take_the_profiles_root_from_the_environment(
|
|
262
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
263
|
+
) -> None:
|
|
264
|
+
monkeypatch.setenv("CODEX_PROFILES_ROOT", str(tmp_path))
|
|
265
|
+
assert choice.default_profiles_root() == tmp_path
|
|
266
|
+
|
|
267
|
+
def should_default_the_profiles_root_under_the_user_home(
|
|
268
|
+
self, monkeypatch: pytest.MonkeyPatch
|
|
269
|
+
) -> None:
|
|
270
|
+
monkeypatch.delenv("CODEX_PROFILES_ROOT", raising=False)
|
|
271
|
+
assert choice.default_profiles_root() == Path.home() / ".codex-profiles"
|
|
272
|
+
|
|
273
|
+
def should_list_each_window_in_a_read_accounts_payload(self) -> None:
|
|
274
|
+
payload = choice.reading_payload(reading("codex-1", weekly_used=40.0, short_used=5.0))
|
|
275
|
+
assert payload["percent_left"] == 60.0
|
|
276
|
+
assert [each["minutes"] for each in payload["windows"]] == [300, 10080]
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Tests for the reader of one Codex account's rate-limit windows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Mapping, Sequence
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from codex_account_meters import (
|
|
14
|
+
CodexMeterUnreadError,
|
|
15
|
+
exchange_with_app_server,
|
|
16
|
+
parse_rate_limits_reply,
|
|
17
|
+
read_codex_meters,
|
|
18
|
+
request_messages,
|
|
19
|
+
resolve_codex_path,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
RESET_SECONDS = 1790000000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def reply_line(rate_limits: object) -> str:
|
|
26
|
+
return json.dumps(
|
|
27
|
+
{"jsonrpc": "2.0", "id": 2, "result": {"rateLimits": rate_limits}}
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TestParseRateLimitsReply:
|
|
32
|
+
def should_measure_room_by_the_tightest_window(self) -> None:
|
|
33
|
+
meters = parse_rate_limits_reply(
|
|
34
|
+
reply_line(
|
|
35
|
+
{
|
|
36
|
+
"primary": {
|
|
37
|
+
"usedPercent": 30,
|
|
38
|
+
"windowDurationMins": 300,
|
|
39
|
+
"resetsAt": RESET_SECONDS,
|
|
40
|
+
},
|
|
41
|
+
"secondary": {
|
|
42
|
+
"usedPercent": 85,
|
|
43
|
+
"windowDurationMins": 10080,
|
|
44
|
+
"resetsAt": RESET_SECONDS,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
assert meters.percent_left == 15.0
|
|
50
|
+
assert [each.duration_minutes for each in meters.all_windows] == [300, 10080]
|
|
51
|
+
assert meters.all_windows[0].resets_at == datetime.fromtimestamp(
|
|
52
|
+
RESET_SECONDS, tz=timezone.utc
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
def should_read_a_reply_with_one_window(self) -> None:
|
|
56
|
+
meters = parse_rate_limits_reply(
|
|
57
|
+
reply_line(
|
|
58
|
+
{
|
|
59
|
+
"primary": None,
|
|
60
|
+
"secondary": {"usedPercent": 40, "windowDurationMins": 10080},
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
assert meters.percent_left == 60.0
|
|
65
|
+
|
|
66
|
+
def should_clamp_used_percent_past_full(self) -> None:
|
|
67
|
+
meters = parse_rate_limits_reply(reply_line({"primary": {"usedPercent": 130}}))
|
|
68
|
+
assert meters.percent_left == 0.0
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize(
|
|
71
|
+
"line",
|
|
72
|
+
[
|
|
73
|
+
"not json",
|
|
74
|
+
json.dumps({"id": 2, "error": {"message": "not signed in"}}),
|
|
75
|
+
reply_line({"primary": None, "secondary": None}),
|
|
76
|
+
reply_line({"primary": {"usedPercent": "high"}}),
|
|
77
|
+
json.dumps({"id": 2, "result": {}}),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def should_raise_unread_for_a_reply_without_a_usable_window(
|
|
81
|
+
self, line: str
|
|
82
|
+
) -> None:
|
|
83
|
+
with pytest.raises(CodexMeterUnreadError):
|
|
84
|
+
parse_rate_limits_reply(line)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestShortWindowPercentLeft:
|
|
88
|
+
def should_read_the_room_left_in_the_five_hour_window(self) -> None:
|
|
89
|
+
meters = parse_rate_limits_reply(
|
|
90
|
+
reply_line(
|
|
91
|
+
{
|
|
92
|
+
"primary": {"usedPercent": 70, "windowDurationMins": 300},
|
|
93
|
+
"secondary": {"usedPercent": 20, "windowDurationMins": 10080},
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
assert meters.short_window_percent_left == 30.0
|
|
98
|
+
|
|
99
|
+
def should_name_no_short_window_for_a_weekly_only_account(self) -> None:
|
|
100
|
+
meters = parse_rate_limits_reply(
|
|
101
|
+
reply_line({"secondary": {"usedPercent": 20, "windowDurationMins": 10080}})
|
|
102
|
+
)
|
|
103
|
+
assert meters.short_window_percent_left is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class TestBlockingReset:
|
|
107
|
+
def should_name_when_the_last_blocking_window_resets(self) -> None:
|
|
108
|
+
meters = parse_rate_limits_reply(
|
|
109
|
+
reply_line(
|
|
110
|
+
{
|
|
111
|
+
"primary": {"usedPercent": 95, "resetsAt": RESET_SECONDS},
|
|
112
|
+
"secondary": {"usedPercent": 99, "resetsAt": RESET_SECONDS + 3600},
|
|
113
|
+
}
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
assert meters.resets_before_room(10.0) == datetime.fromtimestamp(
|
|
117
|
+
RESET_SECONDS + 3600, tz=timezone.utc
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestReadCodexMeters:
|
|
122
|
+
def should_send_the_handshake_then_the_read_under_the_home(self) -> None:
|
|
123
|
+
all_calls: list[tuple[Path, Path, list[str]]] = []
|
|
124
|
+
|
|
125
|
+
def fake_exchange(
|
|
126
|
+
codex_path: Path,
|
|
127
|
+
codex_home: Path,
|
|
128
|
+
all_messages: Sequence[Mapping[str, object]],
|
|
129
|
+
) -> list[str]:
|
|
130
|
+
all_calls.append(
|
|
131
|
+
(
|
|
132
|
+
codex_path,
|
|
133
|
+
codex_home,
|
|
134
|
+
[str(each.get("method")) for each in all_messages],
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
return [
|
|
138
|
+
json.dumps({"id": 1, "result": {}}),
|
|
139
|
+
reply_line({"primary": {"usedPercent": 20}}),
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
meters = read_codex_meters(Path("codex"), Path("profiles") / "codex-1", fake_exchange)
|
|
143
|
+
assert meters.percent_left == 80.0
|
|
144
|
+
assert all_calls == [
|
|
145
|
+
(
|
|
146
|
+
Path("codex"),
|
|
147
|
+
Path("profiles") / "codex-1",
|
|
148
|
+
["initialize", "initialized", "account/rateLimits/read"],
|
|
149
|
+
)
|
|
150
|
+
]
|
|
151
|
+
|
|
152
|
+
def should_raise_unread_when_the_server_never_answers(self) -> None:
|
|
153
|
+
with pytest.raises(CodexMeterUnreadError):
|
|
154
|
+
read_codex_meters(
|
|
155
|
+
Path("codex"), Path("/h"), lambda *_: [json.dumps({"id": 1})]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def should_raise_unread_when_the_server_cannot_start(self) -> None:
|
|
159
|
+
def broken_exchange(*_: object) -> list[str]:
|
|
160
|
+
raise FileNotFoundError("codex")
|
|
161
|
+
|
|
162
|
+
with pytest.raises(CodexMeterUnreadError):
|
|
163
|
+
read_codex_meters(Path("codex"), Path("/h"), broken_exchange)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
FAKE_APP_SERVER = """#!/usr/bin/env python3
|
|
167
|
+
import json, os, sys
|
|
168
|
+
for line in sys.stdin:
|
|
169
|
+
message = json.loads(line)
|
|
170
|
+
if message.get("id") == 2:
|
|
171
|
+
used = 12 if os.environ.get("CODEX_HOME", "").endswith("codex-2") else 99
|
|
172
|
+
print(json.dumps({"id": 2, "result": {"rateLimits": {"primary": {"usedPercent": used}}}}), flush=True)
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class TestExchangeWithAppServer:
|
|
177
|
+
@pytest.mark.skipif(os.name == "nt", reason="the fake server runs through a shebang")
|
|
178
|
+
def should_read_the_reply_while_input_stays_open(self, tmp_path: Path) -> None:
|
|
179
|
+
fake_codex = tmp_path / "codex"
|
|
180
|
+
fake_codex.write_text(FAKE_APP_SERVER)
|
|
181
|
+
fake_codex.chmod(0o755)
|
|
182
|
+
|
|
183
|
+
meters = read_codex_meters(fake_codex, tmp_path / "codex-2", exchange_with_app_server)
|
|
184
|
+
|
|
185
|
+
assert meters.percent_left == 88.0
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class TestRequestMessages:
|
|
189
|
+
def should_ask_for_rate_limits_after_the_handshake(self) -> None:
|
|
190
|
+
all_messages = request_messages()
|
|
191
|
+
assert [each.get("id") for each in all_messages] == [1, None, 2]
|
|
192
|
+
assert all_messages[2]["method"] == "account/rateLimits/read"
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestResolveCodexPath:
|
|
196
|
+
def should_use_the_named_path(self, tmp_path: Path) -> None:
|
|
197
|
+
assert resolve_codex_path(tmp_path / "codex.exe") == tmp_path / "codex.exe"
|
|
198
|
+
|
|
199
|
+
def should_raise_when_no_codex_is_found(
|
|
200
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
201
|
+
) -> None:
|
|
202
|
+
monkeypatch.setenv("PATH", "")
|
|
203
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
204
|
+
with pytest.raises(FileNotFoundError):
|
|
205
|
+
resolve_codex_path(None)
|
|
@@ -172,7 +172,7 @@ def _build_registration(tmp_path: Path, checkout_path: Path) -> AdvisoryRegistra
|
|
|
172
172
|
encoding="utf-8",
|
|
173
173
|
)
|
|
174
174
|
return AdvisoryRegistration(
|
|
175
|
-
RepositorySettings("
|
|
175
|
+
RepositorySettings("example-owner/example-repo", "unused"),
|
|
176
176
|
2985,
|
|
177
177
|
checkout_path,
|
|
178
178
|
PurePosixPath("manifest.json"),
|
|
@@ -185,7 +185,7 @@ def _build_registration(tmp_path: Path, checkout_path: Path) -> AdvisoryRegistra
|
|
|
185
185
|
|
|
186
186
|
def _build_candidate(head_sha: str, merge_sha: str = "unused") -> PullRequestCandidate:
|
|
187
187
|
return PullRequestCandidate(
|
|
188
|
-
"
|
|
188
|
+
"example-owner/example-repo",
|
|
189
189
|
2985,
|
|
190
190
|
"main",
|
|
191
191
|
head_sha,
|
|
@@ -291,7 +291,7 @@ def _publish_recorded(
|
|
|
291
291
|
) -> FakePublication:
|
|
292
292
|
all_events.append("publish")
|
|
293
293
|
assert github_client is github
|
|
294
|
-
assert repository.slug == "
|
|
294
|
+
assert repository.slug == "example-owner/example-repo"
|
|
295
295
|
assert pull_request_number == 2985
|
|
296
296
|
assert checkout.is_dir()
|
|
297
297
|
assert manifest.is_file()
|
|
@@ -271,7 +271,7 @@ def test_start_polling_returns_without_waiting_for_detached_process(
|
|
|
271
271
|
def test_configuration_rejects_two_pairs_for_one_repository(tmp_path: Path) -> None:
|
|
272
272
|
settings_path = tmp_path / "settings.json"
|
|
273
273
|
registration = {
|
|
274
|
-
"repository": "
|
|
274
|
+
"repository": "example-owner/example-repo",
|
|
275
275
|
"pull_request": 2985,
|
|
276
276
|
"app_id": 4841271,
|
|
277
277
|
"installation_id": 159293880,
|
|
@@ -47,7 +47,7 @@ class ClosedPullRequestGitHub(AdvisoryGitHub):
|
|
|
47
47
|
|
|
48
48
|
def _build_registration(tmp_path: Path) -> AdvisoryRegistration:
|
|
49
49
|
return AdvisoryRegistration(
|
|
50
|
-
RepositorySettings("
|
|
50
|
+
RepositorySettings("example-owner/example-repo", "unused"),
|
|
51
51
|
3082,
|
|
52
52
|
tmp_path / "checkout",
|
|
53
53
|
PurePosixPath("manifest.json"),
|
|
@@ -162,6 +162,34 @@ def test_should_flag_a_commit_message_and_a_commit_identity(tmp_path: Path) -> N
|
|
|
162
162
|
assert all("acme" not in each_finding.lower() for each_finding in all_findings)
|
|
163
163
|
|
|
164
164
|
|
|
165
|
+
def test_should_skip_a_github_noreply_address_in_a_commit_identity(
|
|
166
|
+
tmp_path: Path,
|
|
167
|
+
) -> None:
|
|
168
|
+
repository_root, base, head = _repository_with_commits(
|
|
169
|
+
tmp_path,
|
|
170
|
+
[
|
|
171
|
+
("fix: clean change", "Pat", "81234567+acmewidget@users.noreply.github.com"),
|
|
172
|
+
("chore: clean", *_CLEAN_IDENTITY),
|
|
173
|
+
],
|
|
174
|
+
)
|
|
175
|
+
assert private_term_scan.scan_commits(repository_root, f"{base}..{head}") == []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def test_should_flag_a_private_name_beside_a_github_noreply_address(
|
|
179
|
+
tmp_path: Path,
|
|
180
|
+
) -> None:
|
|
181
|
+
repository_root, base, head = _repository_with_commits(
|
|
182
|
+
tmp_path,
|
|
183
|
+
[
|
|
184
|
+
("fix: clean change", "Acme Widget", "81234567+pat@users.noreply.github.com"),
|
|
185
|
+
("chore: clean", *_CLEAN_IDENTITY),
|
|
186
|
+
],
|
|
187
|
+
)
|
|
188
|
+
all_findings = private_term_scan.scan_commits(repository_root, f"{base}..{head}")
|
|
189
|
+
assert len(all_findings) == 1
|
|
190
|
+
assert " identity:" in all_findings[0]
|
|
191
|
+
|
|
192
|
+
|
|
165
193
|
def test_should_print_findings_and_fail_from_the_command_line(
|
|
166
194
|
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
167
195
|
) -> None:
|
|
@@ -60,12 +60,12 @@ def stub_reads(
|
|
|
60
60
|
|
|
61
61
|
def should_build_a_parser_that_collects_extra_driver_logins() -> None:
|
|
62
62
|
parsed = command.build_parser().parse_args(
|
|
63
|
-
[SLUG, "7", "--driver-login", "
|
|
63
|
+
[SLUG, "7", "--driver-login", "octocat", "--driver-login", "claude[bot]"]
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
assert parsed.slug == SLUG
|
|
67
67
|
assert parsed.number == 7
|
|
68
|
-
assert parsed.driver_login == ["
|
|
68
|
+
assert parsed.driver_login == ["octocat", "claude[bot]"]
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
def should_report_an_unanswered_finding(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -236,9 +236,9 @@ def should_count_the_pull_request_author_as_the_driving_agent() -> None:
|
|
|
236
236
|
|
|
237
237
|
|
|
238
238
|
def should_add_each_login_named_on_the_command_line() -> None:
|
|
239
|
-
all_logins = model.driver_logins({"user": {"login": "claude[bot]"}}, ["
|
|
239
|
+
all_logins = model.driver_logins({"user": {"login": "claude[bot]"}}, ["octocat"])
|
|
240
240
|
|
|
241
|
-
assert all_logins == frozenset({"claude[bot]", "
|
|
241
|
+
assert all_logins == frozenset({"claude[bot]", "octocat"})
|
|
242
242
|
|
|
243
243
|
|
|
244
244
|
def should_read_the_head_commit_from_the_pull_request() -> None:
|