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.
Files changed (74) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  3. package/_shared/advisor/reference/astra-rung.md +7 -7
  4. package/_shared/advisor/reference/third-party-bind.md +2 -2
  5. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  6. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  7. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  8. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  9. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  10. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  11. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  13. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  14. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  15. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  16. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  17. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  18. package/bin/git_hooks_installer.test.mjs +1 -1
  19. package/bin/install-constants.mjs +1 -6
  20. package/bin/install.cursor-rules.test.mjs +24 -51
  21. package/bin/install.mjs +16 -70
  22. package/docs/codex-accounts.md +79 -0
  23. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  24. package/hooks/blocking/test_pii_scanner.py +1 -1
  25. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  26. package/hooks/git-hooks/conftest.py +16 -0
  27. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  28. package/hooks/git-hooks/test_post_commit.py +1 -1
  29. package/hooks/git-hooks/test_pre_commit.py +1 -1
  30. package/hooks/git-hooks/test_pre_push.py +2 -2
  31. package/hooks/git-hooks/test_verification_start.py +1 -1
  32. package/hooks/git-hooks/verification_notice.py +2 -2
  33. package/hooks/git-hooks/verification_notice_context.py +21 -2
  34. package/hooks/git-hooks/verification_start.py +2 -3
  35. package/package.json +1 -1
  36. package/rules/durable-post-artifacts.md +4 -3
  37. package/scripts/claude_account_profile.py +21 -9
  38. package/scripts/codex_account_choice.py +396 -0
  39. package/scripts/codex_account_meters.py +372 -0
  40. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  41. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  42. package/scripts/private_term_scan.py +6 -2
  43. package/scripts/repository_checks/config/constants.py +1 -0
  44. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  45. package/scripts/test_claude_account_profile.py +12 -0
  46. package/scripts/test_codex_account_choice.py +276 -0
  47. package/scripts/test_codex_account_meters.py +205 -0
  48. package/scripts/tests/test_automatic_advisory.py +3 -3
  49. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  50. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  51. package/scripts/tests/test_closed_pr_label.py +1 -1
  52. package/scripts/tests/test_private_term_scan.py +28 -0
  53. package/scripts/tests/test_review_closure.py +2 -2
  54. package/scripts/tests/test_review_closure_model.py +2 -2
  55. package/scripts/tests/test_rules.py +2 -83
  56. package/scripts/tests/test_tracked_private_terms.py +15 -0
  57. package/scripts/tests/test_verification_notice.py +29 -13
  58. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  59. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  60. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  61. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  62. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  63. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  64. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  65. package/scripts/sync_to_cursor/__init__.py +0 -13
  66. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  67. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  68. package/scripts/sync_to_cursor/engine.py +0 -219
  69. package/scripts/sync_to_cursor/hashing.py +0 -7
  70. package/scripts/sync_to_cursor/paths.py +0 -18
  71. package/scripts/sync_to_cursor/rules.py +0 -494
  72. package/scripts/sync_to_cursor.py +0 -22
  73. package/scripts/tests/test_engine.py +0 -101
  74. 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("JonEcho/python-automation", "unused"),
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
- "JonEcho/python-automation",
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 == "JonEcho/python-automation"
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()
@@ -28,7 +28,7 @@ class _CycleFailsOnceRunner:
28
28
  raise self.cycle_error
29
29
  return (
30
30
  AdvisoryState(
31
- "JonEcho/python-automation",
31
+ "example-owner/example-repo",
32
32
  3087,
33
33
  "passed",
34
34
  "unchanged remote head and base",
@@ -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": "JonEcho/python-automation",
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("JonEcho/python-automation", "unused"),
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", "JonEcho", "--driver-login", "claude[bot]"]
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 == ["JonEcho", "claude[bot]"]
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]"}}, ["JonEcho"])
239
+ all_logins = model.driver_logins({"user": {"login": "claude[bot]"}}, ["octocat"])
240
240
 
241
- assert all_logins == frozenset({"claude[bot]", "JonEcho"})
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: