claude-dev-env 8.6.1 → 8.8.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/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -2
- package/_shared/advisor/reference/identity.md +1 -1
- package/_shared/advisor/reference/spawn-walk-log.md +1 -1
- package/_shared/advisor/reference/warm-up.md +3 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +16 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +23 -5
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +85 -6
- package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/docs/references/advisor-tool.md +3 -2
- package/docs/second-claude-account.md +50 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +10 -8
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_pii_scanner.py +8 -0
- package/hooks/blocking/test_state_description_blocker.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/long-horizon-autonomy.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/claude_account_choice.py +246 -0
- package/scripts/claude_account_profile.py +371 -0
- package/scripts/claude_chain_usage.py +49 -5
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/claude_account_constants.py +166 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_claude_account_constants.py +21 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapters.py +30 -0
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_claude_account_choice.py +217 -0
- package/scripts/test_claude_account_profile.py +220 -0
- package/scripts/test_claude_chain_usage.py +40 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Tests for the second-account profile sync and its launcher."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import claude_account_profile as profile
|
|
13
|
+
|
|
14
|
+
NOW = datetime(2026, 9, 22, 21, 0, tzinfo=timezone.utc)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_main_home(tmp_path: Path) -> Path:
|
|
18
|
+
main_home = tmp_path / "main"
|
|
19
|
+
(main_home / "skills" / "tdd").mkdir(parents=True)
|
|
20
|
+
(main_home / "rules").mkdir()
|
|
21
|
+
(main_home / "projects").mkdir()
|
|
22
|
+
(main_home / "CLAUDE.md").write_text("main instructions", encoding="utf-8")
|
|
23
|
+
(main_home / "settings.json").write_text("{}", encoding="utf-8")
|
|
24
|
+
(main_home / ".credentials.json").write_text("main sign-in", encoding="utf-8")
|
|
25
|
+
(main_home / ".claude.json").write_text("main state", encoding="utf-8")
|
|
26
|
+
(main_home / ".claude.json.backup").write_text("main backup", encoding="utf-8")
|
|
27
|
+
return main_home
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def sync(main_home: Path, profile_home: Path) -> profile.ProfileSyncReport:
|
|
31
|
+
return profile.sync_profile(main_home=main_home, profile_home=profile_home, now=NOW)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class TestDefaultProfileHome:
|
|
35
|
+
def should_use_the_profiles_root_the_environment_names(
|
|
36
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
37
|
+
) -> None:
|
|
38
|
+
monkeypatch.setenv("LLM_SETTINGS_PROFILES_ROOT", str(tmp_path))
|
|
39
|
+
assert profile.default_profile_home() == tmp_path / "ev"
|
|
40
|
+
|
|
41
|
+
def should_fall_back_to_the_profiles_root_in_the_user_home(
|
|
42
|
+
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
43
|
+
) -> None:
|
|
44
|
+
monkeypatch.delenv("LLM_SETTINGS_PROFILES_ROOT", raising=False)
|
|
45
|
+
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
46
|
+
assert profile.default_profile_home() == tmp_path / ".claude-profiles" / "ev"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestIsAccountLocal:
|
|
50
|
+
def should_keep_sign_in_state_and_history_local(self) -> None:
|
|
51
|
+
for each_name in (
|
|
52
|
+
".credentials.json",
|
|
53
|
+
".credentials.json.bak-session-dashboard",
|
|
54
|
+
".claude.json",
|
|
55
|
+
".claude.json.backup",
|
|
56
|
+
"projects",
|
|
57
|
+
):
|
|
58
|
+
assert profile.is_account_local(each_name)
|
|
59
|
+
|
|
60
|
+
def should_share_skills_rules_and_settings(self) -> None:
|
|
61
|
+
for each_name in ("skills", "rules", "CLAUDE.md", "settings.json", "plugins"):
|
|
62
|
+
assert not profile.is_account_local(each_name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestSyncProfile:
|
|
66
|
+
def should_link_every_shared_entry_to_the_main_home(self, tmp_path: Path) -> None:
|
|
67
|
+
main_home = build_main_home(tmp_path)
|
|
68
|
+
profile_home = tmp_path / "ev"
|
|
69
|
+
sync(main_home, profile_home)
|
|
70
|
+
assert (profile_home / "skills" / "tdd").is_dir()
|
|
71
|
+
assert (profile_home / "CLAUDE.md").read_text(
|
|
72
|
+
encoding="utf-8"
|
|
73
|
+
) == "main instructions"
|
|
74
|
+
assert profile.links_to(profile_home / "rules", main_home / "rules")
|
|
75
|
+
assert profile.links_to(
|
|
76
|
+
profile_home / "settings.json", main_home / "settings.json"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def should_keep_the_accounts_own_sign_in_state_and_history_apart(
|
|
80
|
+
self, tmp_path: Path
|
|
81
|
+
) -> None:
|
|
82
|
+
main_home = build_main_home(tmp_path)
|
|
83
|
+
profile_home = tmp_path / "ev"
|
|
84
|
+
profile_home.mkdir()
|
|
85
|
+
(profile_home / ".credentials.json").write_text(
|
|
86
|
+
"second sign-in", encoding="utf-8"
|
|
87
|
+
)
|
|
88
|
+
sync(main_home, profile_home)
|
|
89
|
+
assert (profile_home / ".credentials.json").read_text(
|
|
90
|
+
encoding="utf-8"
|
|
91
|
+
) == "second sign-in"
|
|
92
|
+
assert not (profile_home / ".claude.json").exists()
|
|
93
|
+
assert not (profile_home / ".claude.json.backup").exists()
|
|
94
|
+
assert not (profile_home / "projects").exists()
|
|
95
|
+
|
|
96
|
+
def should_move_a_stale_copy_aside_and_keep_its_content(
|
|
97
|
+
self, tmp_path: Path
|
|
98
|
+
) -> None:
|
|
99
|
+
main_home = build_main_home(tmp_path)
|
|
100
|
+
profile_home = tmp_path / "ev"
|
|
101
|
+
profile_home.mkdir()
|
|
102
|
+
(profile_home / "CLAUDE.md").write_text("stale copy", encoding="utf-8")
|
|
103
|
+
report = sync(main_home, profile_home)
|
|
104
|
+
moved_copy = (
|
|
105
|
+
profile_home / ".replaced" / NOW.strftime("%Y%m%dT%H%M%SZ") / "CLAUDE.md"
|
|
106
|
+
)
|
|
107
|
+
assert moved_copy.read_text(encoding="utf-8") == "stale copy"
|
|
108
|
+
assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
|
|
109
|
+
assert report.all_moved_aside == ("CLAUDE.md",)
|
|
110
|
+
|
|
111
|
+
def should_remove_a_same_content_copy_without_moving_it_aside(
|
|
112
|
+
self, tmp_path: Path
|
|
113
|
+
) -> None:
|
|
114
|
+
main_home = build_main_home(tmp_path)
|
|
115
|
+
profile_home = tmp_path / "ev"
|
|
116
|
+
profile_home.mkdir()
|
|
117
|
+
(profile_home / "CLAUDE.md").write_text("main instructions", encoding="utf-8")
|
|
118
|
+
report = sync(main_home, profile_home)
|
|
119
|
+
assert report.all_moved_aside == ()
|
|
120
|
+
assert not (profile_home / ".replaced").exists()
|
|
121
|
+
assert profile.links_to(profile_home / "CLAUDE.md", main_home / "CLAUDE.md")
|
|
122
|
+
|
|
123
|
+
def should_move_aside_a_link_that_points_somewhere_else(
|
|
124
|
+
self, tmp_path: Path
|
|
125
|
+
) -> None:
|
|
126
|
+
main_home = build_main_home(tmp_path)
|
|
127
|
+
elsewhere = tmp_path / "old-shared" / "rules"
|
|
128
|
+
elsewhere.mkdir(parents=True)
|
|
129
|
+
(elsewhere / "keep.md").write_text("old rule", encoding="utf-8")
|
|
130
|
+
profile_home = tmp_path / "ev"
|
|
131
|
+
profile_home.mkdir()
|
|
132
|
+
(profile_home / "rules").symlink_to(elsewhere, target_is_directory=True)
|
|
133
|
+
report = sync(main_home, profile_home)
|
|
134
|
+
assert (elsewhere / "keep.md").read_text(encoding="utf-8") == "old rule"
|
|
135
|
+
assert profile.links_to(profile_home / "rules", main_home / "rules")
|
|
136
|
+
assert report.all_moved_aside == ("rules",)
|
|
137
|
+
|
|
138
|
+
def should_change_nothing_on_a_second_run(self, tmp_path: Path) -> None:
|
|
139
|
+
main_home = build_main_home(tmp_path)
|
|
140
|
+
profile_home = tmp_path / "ev"
|
|
141
|
+
sync(main_home, profile_home)
|
|
142
|
+
second_report = sync(main_home, profile_home)
|
|
143
|
+
assert second_report.all_linked == ()
|
|
144
|
+
assert second_report.all_moved_aside == ()
|
|
145
|
+
assert second_report.all_unlinked == ()
|
|
146
|
+
|
|
147
|
+
def should_unlink_an_entry_the_main_home_no_longer_has(
|
|
148
|
+
self, tmp_path: Path
|
|
149
|
+
) -> None:
|
|
150
|
+
main_home = build_main_home(tmp_path)
|
|
151
|
+
profile_home = tmp_path / "ev"
|
|
152
|
+
sync(main_home, profile_home)
|
|
153
|
+
(main_home / "rules").rmdir()
|
|
154
|
+
report = sync(main_home, profile_home)
|
|
155
|
+
assert not os.path.lexists(profile_home / "rules")
|
|
156
|
+
assert report.all_unlinked == ("rules",)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def should_remove_a_main_link_left_under_an_account_local_name(
|
|
160
|
+
self, tmp_path: Path
|
|
161
|
+
) -> None:
|
|
162
|
+
main_home = build_main_home(tmp_path)
|
|
163
|
+
(main_home / ".credentials.json.bak").write_text("main backup", encoding="utf-8")
|
|
164
|
+
profile_home = tmp_path / "ev"
|
|
165
|
+
profile_home.mkdir()
|
|
166
|
+
os.link(main_home / ".credentials.json.bak", profile_home / ".credentials.json.bak")
|
|
167
|
+
report = sync(main_home, profile_home)
|
|
168
|
+
assert not os.path.lexists(profile_home / ".credentials.json.bak")
|
|
169
|
+
assert (main_home / ".credentials.json.bak").read_text(
|
|
170
|
+
encoding="utf-8"
|
|
171
|
+
) == "main backup"
|
|
172
|
+
assert report.all_unlinked == (".credentials.json.bak",)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class TestWriteLauncher:
|
|
176
|
+
def should_point_claude_at_the_profile_and_pass_every_argument(
|
|
177
|
+
self, tmp_path: Path
|
|
178
|
+
) -> None:
|
|
179
|
+
profile_home = tmp_path / "ev"
|
|
180
|
+
launcher_path = profile.write_launcher(
|
|
181
|
+
launcher_directory=tmp_path / "bin", profile_home=profile_home, now=NOW
|
|
182
|
+
)
|
|
183
|
+
launcher_text = launcher_path.read_text(encoding="utf-8")
|
|
184
|
+
assert f'set "CLAUDE_CONFIG_DIR={profile_home}"' in launcher_text
|
|
185
|
+
assert "claude %*" in launcher_text
|
|
186
|
+
|
|
187
|
+
def should_move_an_older_launcher_aside(self, tmp_path: Path) -> None:
|
|
188
|
+
launcher_directory = tmp_path / "bin"
|
|
189
|
+
launcher_directory.mkdir()
|
|
190
|
+
(launcher_directory / "claude-ev.cmd").write_text(
|
|
191
|
+
"old launcher", encoding="utf-8"
|
|
192
|
+
)
|
|
193
|
+
profile.write_launcher(
|
|
194
|
+
launcher_directory=launcher_directory, profile_home=tmp_path / "ev", now=NOW
|
|
195
|
+
)
|
|
196
|
+
moved_launcher = launcher_directory / (
|
|
197
|
+
"claude-ev.cmd.replaced-" + NOW.strftime("%Y%m%dT%H%M%SZ")
|
|
198
|
+
)
|
|
199
|
+
assert moved_launcher.read_text(encoding="utf-8") == "old launcher"
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestMain:
|
|
203
|
+
def should_print_what_the_sync_changed(
|
|
204
|
+
self, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
205
|
+
) -> None:
|
|
206
|
+
main_home = build_main_home(tmp_path)
|
|
207
|
+
exit_code = profile.main(
|
|
208
|
+
[
|
|
209
|
+
"--main-home",
|
|
210
|
+
str(main_home),
|
|
211
|
+
"--profile-home",
|
|
212
|
+
str(tmp_path / "ev"),
|
|
213
|
+
"--launcher-directory",
|
|
214
|
+
str(tmp_path / "bin"),
|
|
215
|
+
]
|
|
216
|
+
)
|
|
217
|
+
printed = json.loads(capsys.readouterr().out)
|
|
218
|
+
assert exit_code == 0
|
|
219
|
+
assert "CLAUDE.md" in printed["linked"]
|
|
220
|
+
assert printed["launcher"] == str(tmp_path / "bin" / "claude-ev.cmd")
|
|
@@ -5,6 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import json
|
|
6
6
|
import subprocess
|
|
7
7
|
import sys
|
|
8
|
+
from datetime import datetime
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import pytest
|
|
@@ -372,7 +373,10 @@ def test_probe_weekly_utilization_reuses_resolver_windows(
|
|
|
372
373
|
monkeypatch: pytest.MonkeyPatch,
|
|
373
374
|
) -> None:
|
|
374
375
|
class _FakeWindows:
|
|
376
|
+
session_utilization = None
|
|
377
|
+
session_resets_at = None
|
|
375
378
|
weekly_utilization = 37.5
|
|
379
|
+
weekly_resets_at = None
|
|
376
380
|
|
|
377
381
|
class _FakeResolver:
|
|
378
382
|
def read_oauth_access_token(self, credentials_path: Path, now: object) -> str:
|
|
@@ -401,6 +405,42 @@ def test_probe_weekly_utilization_reuses_resolver_windows(
|
|
|
401
405
|
assert weekly_utilization == pytest.approx(37.5)
|
|
402
406
|
|
|
403
407
|
|
|
408
|
+
def should_read_both_meters_and_their_resets_for_one_account(
|
|
409
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
410
|
+
) -> None:
|
|
411
|
+
session_reset = datetime(2026, 9, 22, 23, 0).astimezone()
|
|
412
|
+
weekly_reset = datetime(2026, 9, 26, 9, 0).astimezone()
|
|
413
|
+
|
|
414
|
+
class _FakeWindows:
|
|
415
|
+
session_utilization = 12.0
|
|
416
|
+
session_resets_at = session_reset
|
|
417
|
+
weekly_utilization = 64.0
|
|
418
|
+
weekly_resets_at = weekly_reset
|
|
419
|
+
|
|
420
|
+
class _FakeResolver:
|
|
421
|
+
def read_oauth_access_token(self, credentials_path: Path, now: object) -> str:
|
|
422
|
+
return "token-value"
|
|
423
|
+
|
|
424
|
+
def _fetch_usage_payload(self, access_token: str) -> dict[str, object]:
|
|
425
|
+
return {}
|
|
426
|
+
|
|
427
|
+
def extract_usage_windows(
|
|
428
|
+
self, usage_payload: dict[str, object]
|
|
429
|
+
) -> _FakeWindows:
|
|
430
|
+
return _FakeWindows()
|
|
431
|
+
|
|
432
|
+
monkeypatch.setattr(
|
|
433
|
+
usage, "_load_resolve_usage_window_module", lambda: _FakeResolver()
|
|
434
|
+
)
|
|
435
|
+
account_meters = usage.probe_account_meters(Path(PLACEHOLDER_CREDENTIALS_PRIMARY))
|
|
436
|
+
assert account_meters == usage.AccountUsageMeters(
|
|
437
|
+
session_utilization=12.0,
|
|
438
|
+
session_resets_at=session_reset,
|
|
439
|
+
weekly_utilization=64.0,
|
|
440
|
+
weekly_resets_at=weekly_reset,
|
|
441
|
+
)
|
|
442
|
+
|
|
443
|
+
|
|
404
444
|
def test_probe_weekly_utilization_raises_when_token_missing(
|
|
405
445
|
monkeypatch: pytest.MonkeyPatch,
|
|
406
446
|
) -> None:
|
|
@@ -460,3 +460,49 @@ def should_report_a_rewritten_release_body_through_the_command_line(
|
|
|
460
460
|
)
|
|
461
461
|
assert completed_process.returncode == 1
|
|
462
462
|
assert "keep the generated text whole" in completed_process.stderr
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def test_contrast_framing_in_a_body_reports() -> None:
|
|
466
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
467
|
+
action="pr-comment",
|
|
468
|
+
title=None,
|
|
469
|
+
body_text="The queue ejected the entry, not the runner.\n",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
assert [each_finding.code for each_finding in all_findings] == ["contrast-framing"]
|
|
473
|
+
assert "trailing-comma-not" in all_findings[0].message
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def test_contrast_framing_in_a_title_reports() -> None:
|
|
477
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
478
|
+
action="pr-create",
|
|
479
|
+
title="fix(lint): read the base rather than the worktree",
|
|
480
|
+
body_text=VALID_PR_BODY,
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
assert [each_finding.code for each_finding in all_findings] == ["contrast-framing"]
|
|
484
|
+
assert "substitution-rather-than" in all_findings[0].message
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def test_a_body_stating_what_is_stays_clean() -> None:
|
|
488
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
489
|
+
action="pr-comment",
|
|
490
|
+
title=None,
|
|
491
|
+
body_text="The queue ejected the entry. The runner stayed up.\n",
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
assert all_findings == ()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def test_a_release_body_carrying_a_contrast_stays_accepted() -> None:
|
|
498
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
499
|
+
action="pr-edit",
|
|
500
|
+
title=None,
|
|
501
|
+
body_text=RELEASE_BODY.replace(
|
|
502
|
+
"This PR was generated with",
|
|
503
|
+
"Bumped rather than pinned. This PR was generated with",
|
|
504
|
+
),
|
|
505
|
+
head_branch=RELEASE_BRANCH,
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
assert all_findings == ()
|
|
@@ -9,6 +9,8 @@ from pathlib import Path
|
|
|
9
9
|
|
|
10
10
|
from dev_env_scripts_constants.followup_constants import (
|
|
11
11
|
BACKLOG_EXCEEDED_EXIT_CODE,
|
|
12
|
+
BACKLOG_EXCEEDED_TEMPLATE,
|
|
13
|
+
BACKLOG_WITHIN_TEMPLATE,
|
|
12
14
|
FOLLOWUP_BACKLOG_THRESHOLD,
|
|
13
15
|
)
|
|
14
16
|
from followup_cli import main
|
|
@@ -251,6 +253,44 @@ def test_count_escalates_once_the_backlog_passes_the_threshold(
|
|
|
251
253
|
assert str(FOLLOWUP_BACKLOG_THRESHOLD) in output_text
|
|
252
254
|
|
|
253
255
|
|
|
256
|
+
def test_count_writes_the_within_threshold_template_verbatim(
|
|
257
|
+
tmp_path: Path,
|
|
258
|
+
) -> None:
|
|
259
|
+
exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
|
|
260
|
+
|
|
261
|
+
assert exit_code == 0
|
|
262
|
+
assert output_text == (
|
|
263
|
+
BACKLOG_WITHIN_TEMPLATE.format(
|
|
264
|
+
finding_count=0, threshold=FOLLOWUP_BACKLOG_THRESHOLD
|
|
265
|
+
)
|
|
266
|
+
+ "\n"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def test_count_writes_the_exceeded_template_verbatim(tmp_path: Path) -> None:
|
|
271
|
+
for each_index in range(FOLLOWUP_BACKLOG_THRESHOLD + 1):
|
|
272
|
+
record_followup_finding(
|
|
273
|
+
tmp_path,
|
|
274
|
+
FollowupFinding(
|
|
275
|
+
"code-rules",
|
|
276
|
+
f"run{each_index}.py",
|
|
277
|
+
"one smell",
|
|
278
|
+
f"code-rules/x{each_index}",
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
exit_code, output_text = run_command(["count", "--repository-root", str(tmp_path)])
|
|
283
|
+
|
|
284
|
+
assert exit_code == BACKLOG_EXCEEDED_EXIT_CODE
|
|
285
|
+
assert output_text == (
|
|
286
|
+
BACKLOG_EXCEEDED_TEMPLATE.format(
|
|
287
|
+
finding_count=FOLLOWUP_BACKLOG_THRESHOLD + 1,
|
|
288
|
+
threshold=FOLLOWUP_BACKLOG_THRESHOLD,
|
|
289
|
+
)
|
|
290
|
+
+ "\n"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
|
|
254
294
|
def test_list_names_the_check_behind_each_finding(tmp_path: Path) -> None:
|
|
255
295
|
record_followup_finding(
|
|
256
296
|
tmp_path,
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Specifications for holding a merge policy document to its branch ruleset."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from merge_gate_checks import (
|
|
9
|
+
main,
|
|
10
|
+
missing_contexts,
|
|
11
|
+
parse_declared_contexts,
|
|
12
|
+
ruleset_contexts,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
|
16
|
+
POLICY_DOCUMENT_PATH = REPOSITORY_ROOT / "docs" / "merge-policy.md"
|
|
17
|
+
RULESET_SNAPSHOT_PATH = REPOSITORY_ROOT / "docs" / "merge-policy-ruleset.json"
|
|
18
|
+
INSTRUCTION_PAIRS_CONTEXT = "instruction-pairs / instruction-pairs"
|
|
19
|
+
UTF8_ENCODING = "utf-8"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _ruleset_with_contexts(*all_contexts: str) -> list[dict[str, object]]:
|
|
23
|
+
return [
|
|
24
|
+
{"type": "deletion"},
|
|
25
|
+
{
|
|
26
|
+
"type": "required_status_checks",
|
|
27
|
+
"parameters": {
|
|
28
|
+
"required_status_checks": [
|
|
29
|
+
{"context": each_context, "integration_id": 15368}
|
|
30
|
+
for each_context in all_contexts
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def should_read_the_contexts_the_fenced_block_declares() -> None:
|
|
38
|
+
document_text = (
|
|
39
|
+
"# Merge policy\n\n"
|
|
40
|
+
"```required-status-checks\n"
|
|
41
|
+
f"{INSTRUCTION_PAIRS_CONTEXT}\n"
|
|
42
|
+
"```\n\n"
|
|
43
|
+
"```\n"
|
|
44
|
+
"python merge_gate_checks.py --refresh\n"
|
|
45
|
+
"```\n"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
assert parse_declared_contexts(document_text) == [INSTRUCTION_PAIRS_CONTEXT]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def should_ignore_a_document_with_no_declaration_block() -> None:
|
|
52
|
+
assert parse_declared_contexts("# Merge policy\n\nNo block here.\n") == []
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def should_read_every_context_the_ruleset_requires() -> None:
|
|
56
|
+
payload = _ruleset_with_contexts(INSTRUCTION_PAIRS_CONTEXT, "Ruff")
|
|
57
|
+
|
|
58
|
+
assert ruleset_contexts(payload) == {INSTRUCTION_PAIRS_CONTEXT, "Ruff"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def should_report_a_check_the_ruleset_stopped_requiring() -> None:
|
|
62
|
+
document_text = f"```required-status-checks\n{INSTRUCTION_PAIRS_CONTEXT}\n```\n"
|
|
63
|
+
|
|
64
|
+
assert missing_contexts(document_text, _ruleset_with_contexts("Ruff")) == [
|
|
65
|
+
INSTRUCTION_PAIRS_CONTEXT
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def should_find_every_check_the_checked_in_policy_names() -> None:
|
|
70
|
+
document_text = POLICY_DOCUMENT_PATH.read_text(encoding=UTF8_ENCODING)
|
|
71
|
+
ruleset_payload = json.loads(
|
|
72
|
+
RULESET_SNAPSHOT_PATH.read_text(encoding=UTF8_ENCODING)
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
assert parse_declared_contexts(document_text)
|
|
76
|
+
assert missing_contexts(document_text, ruleset_payload) == []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def should_exit_non_zero_when_a_named_check_left_the_ruleset(tmp_path: Path) -> None:
|
|
80
|
+
document_path = tmp_path / "merge-policy.md"
|
|
81
|
+
document_path.write_text(
|
|
82
|
+
f"```required-status-checks\n{INSTRUCTION_PAIRS_CONTEXT}\n```\n",
|
|
83
|
+
encoding=UTF8_ENCODING,
|
|
84
|
+
)
|
|
85
|
+
ruleset_path = tmp_path / "merge-policy-ruleset.json"
|
|
86
|
+
ruleset_path.write_text(
|
|
87
|
+
json.dumps(_ruleset_with_contexts("Ruff")), encoding=UTF8_ENCODING
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
exit_code = main(["--document", str(document_path), "--ruleset", str(ruleset_path)])
|
|
91
|
+
|
|
92
|
+
assert exit_code == 1
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""Specifications for the permission modes the review binary is asked for.
|
|
2
|
+
|
|
3
|
+
The review binary accepts one permission mode for an ordinary caller and a
|
|
4
|
+
different one for root. Naming the wrong mode means the binary refuses the
|
|
5
|
+
call, no review runs, and no stamp is ever minted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import importlib
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
from dev_env_scripts_constants import code_review_constants
|
|
15
|
+
|
|
16
|
+
BYPASS_MODE_TOKEN: str = "bypassPermissions"
|
|
17
|
+
ACCEPT_EDITS_MODE_TOKEN: str = "acceptEdits"
|
|
18
|
+
ROOT_USER_ID: int = 0
|
|
19
|
+
UNPRIVILEGED_USER_ID: int = 1000
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _permission_mode_seen_by(
|
|
23
|
+
monkeypatch: pytest.MonkeyPatch, effective_user_id: int
|
|
24
|
+
) -> str:
|
|
25
|
+
"""Read the review permission mode a caller with this user id resolves."""
|
|
26
|
+
monkeypatch.setattr(os, "geteuid", lambda: effective_user_id)
|
|
27
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
28
|
+
return str(reloaded_constants.REVIEW_PERMISSION_MODE)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_root_detection_agrees_with_this_process(
|
|
32
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
33
|
+
) -> None:
|
|
34
|
+
monkeypatch.setattr(os, "geteuid", lambda: ROOT_USER_ID)
|
|
35
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
36
|
+
|
|
37
|
+
assert reloaded_constants.IS_ROOT_CALLER is True
|
|
38
|
+
assert reloaded_constants.ROOT_EFFECTIVE_USER_ID == ROOT_USER_ID
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_bypass_mode_names_the_token_the_binary_reads() -> None:
|
|
42
|
+
assert code_review_constants.PERMISSION_MODE_BYPASS == BYPASS_MODE_TOKEN
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_accept_edits_mode_names_the_token_the_binary_reads() -> None:
|
|
46
|
+
assert code_review_constants.PERMISSION_MODE_ACCEPT_EDITS == ACCEPT_EDITS_MODE_TOKEN
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_a_root_caller_resolves_the_mode_the_binary_accepts(
|
|
50
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
51
|
+
) -> None:
|
|
52
|
+
assert _permission_mode_seen_by(monkeypatch, ROOT_USER_ID) == (
|
|
53
|
+
ACCEPT_EDITS_MODE_TOKEN
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_an_ordinary_caller_resolves_the_bypass_mode(
|
|
58
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
59
|
+
) -> None:
|
|
60
|
+
assert _permission_mode_seen_by(monkeypatch, UNPRIVILEGED_USER_ID) == (
|
|
61
|
+
BYPASS_MODE_TOKEN
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_a_platform_without_a_user_id_still_loads_the_constants(
|
|
66
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
67
|
+
) -> None:
|
|
68
|
+
"""Windows exposes no ``os.geteuid``, and the module still imports.
|
|
69
|
+
|
|
70
|
+
Reading a missing ``os.geteuid`` raises, and every caller of the review
|
|
71
|
+
invoker dies at import. A platform that reports no user id is never the
|
|
72
|
+
root the review binary refuses, so it reads as an ordinary caller.
|
|
73
|
+
"""
|
|
74
|
+
monkeypatch.delattr(os, "geteuid", raising=False)
|
|
75
|
+
reloaded_constants = importlib.reload(code_review_constants)
|
|
76
|
+
|
|
77
|
+
assert reloaded_constants.IS_ROOT_CALLER is False
|
|
78
|
+
assert reloaded_constants.REVIEW_PERMISSION_MODE == BYPASS_MODE_TOKEN
|
|
@@ -173,9 +173,9 @@ def test_public_legacy_call_forms_validate_collection_entries(tmp_path: Path) ->
|
|
|
173
173
|
publish_plan(config, all_planned_files=[object()])
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
def
|
|
177
|
-
with pytest.raises(MaterializerError, match="requires a Codex excerpt"):
|
|
178
|
-
materializer.
|
|
176
|
+
def test_render_codex_instruction_excerpt_requires_the_excerpt_heading() -> None:
|
|
177
|
+
with pytest.raises(MaterializerError, match="rules/correction-lens.md requires a Codex excerpt"):
|
|
178
|
+
materializer.render_codex_instruction_excerpt("# No excerpt\n", "rules/correction-lens.md")
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
def test_validation_rejects_reparse_point_from_portable_attribute_seam(
|
|
@@ -823,13 +823,57 @@ def test_failure_blast_radius_projection_uses_the_canonical_excerpt() -> None:
|
|
|
823
823
|
canonical_rule_path = Path(__file__).parents[2] / "rules" / "failure-blast-radius.md"
|
|
824
824
|
canonical_rule = canonical_rule_path.read_text(encoding="utf-8")
|
|
825
825
|
|
|
826
|
-
projected_instruction = materializer.
|
|
826
|
+
projected_instruction = materializer.render_codex_instruction_excerpt(
|
|
827
|
+
canonical_rule, "rules/failure-blast-radius.md"
|
|
828
|
+
)
|
|
827
829
|
|
|
828
830
|
assert projected_instruction.startswith("Failure handling for this run")
|
|
829
831
|
assert "Three attempts, then park." in projected_instruction
|
|
830
832
|
assert "Report as: N of M complete" in projected_instruction
|
|
831
833
|
|
|
832
834
|
|
|
835
|
+
def test_codex_instruction_projection_carries_every_listed_rule(tmp_path: Path) -> None:
|
|
836
|
+
rules_root = Path(__file__).parents[2] / "rules"
|
|
837
|
+
source = tmp_path / "source"
|
|
838
|
+
target = tmp_path / "target"
|
|
839
|
+
(source / "rules").mkdir(parents=True)
|
|
840
|
+
for each_relative_path in materializer.codex_instruction_rule_relative_paths:
|
|
841
|
+
canonical_path = rules_root / Path(each_relative_path).name
|
|
842
|
+
(source / each_relative_path).write_text(
|
|
843
|
+
canonical_path.read_text(encoding="utf-8"), encoding="utf-8"
|
|
844
|
+
)
|
|
845
|
+
config = MaterializerConfig(source, target, should_apply=True)
|
|
846
|
+
|
|
847
|
+
planned, report = build_plan(config, all_agents=[])
|
|
848
|
+
publish_plan(config, planned, report)
|
|
849
|
+
|
|
850
|
+
projected_instruction = (target / "AGENTS.md").read_text(encoding="utf-8")
|
|
851
|
+
assert "Failure handling for this run" in projected_instruction
|
|
852
|
+
assert "Correction handling for this run" in projected_instruction
|
|
853
|
+
manifest_record = _manifest_files(_required_manifest_path(config))["AGENTS.md"]
|
|
854
|
+
assert manifest_record["source"] == "rules/failure-blast-radius.md, rules/correction-lens.md"
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
def test_codex_instruction_projection_skips_an_absent_listed_rule(
|
|
858
|
+
tmp_path: Path,
|
|
859
|
+
) -> None:
|
|
860
|
+
rules_root = Path(__file__).parents[2] / "rules"
|
|
861
|
+
source = tmp_path / "source"
|
|
862
|
+
(source / "rules").mkdir(parents=True)
|
|
863
|
+
(source / "rules" / "correction-lens.md").write_text(
|
|
864
|
+
(rules_root / "correction-lens.md").read_text(encoding="utf-8"),
|
|
865
|
+
encoding="utf-8",
|
|
866
|
+
)
|
|
867
|
+
config = MaterializerConfig(source, tmp_path / "target", should_apply=False)
|
|
868
|
+
|
|
869
|
+
projection = materializer._build_codex_instruction_projection(config)
|
|
870
|
+
|
|
871
|
+
assert projection is not None
|
|
872
|
+
assert projection.source_identity == "rules/correction-lens.md"
|
|
873
|
+
assert "Correction handling for this run" in projection.content
|
|
874
|
+
assert "Failure handling for this run" not in projection.content
|
|
875
|
+
|
|
876
|
+
|
|
833
877
|
def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
|
|
834
878
|
tmp_path: Path,
|
|
835
879
|
) -> None:
|