claude-dev-env 8.6.1 → 8.7.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 (102) hide show
  1. package/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  2. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
  3. package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
  4. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
  5. package/_shared/pr-loop/scripts/terminology_sweep.py +10 -9
  6. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +13 -0
  7. package/bin/install.test.mjs +25 -0
  8. package/docs/high-trust-agent-delivery.md +132 -0
  9. package/hooks/advisory/refactor_guard.py +2 -2
  10. package/hooks/advisory/test_refactor_guard.py +21 -0
  11. package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
  12. package/hooks/blocking/code_rules_shared.py +4 -7
  13. package/hooks/blocking/conftest.py +22 -0
  14. package/hooks/blocking/state_description_blocker.py +10 -8
  15. package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
  16. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +4 -29
  17. package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
  18. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
  19. package/hooks/blocking/test_code_rules_shared.py +18 -0
  20. package/hooks/blocking/test_conftest.py +35 -0
  21. package/hooks/blocking/test_state_description_blocker.py +16 -0
  22. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
  23. package/hooks/git-hooks/pull_request_handoff.py +2 -2
  24. package/hooks/git-hooks/test_config.py +10 -3
  25. package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
  26. package/hooks/git-hooks/test_verification_notice_state.py +50 -0
  27. package/hooks/git-hooks/verification_notice_state.py +3 -1
  28. package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
  29. package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
  30. package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
  31. package/hooks/hooks_constants/state_description_blocker_constants.py +0 -3
  32. package/hooks/hooks_constants/test_state_description_blocker_constants.py +9 -7
  33. package/hooks/validators/exempt_paths.py +3 -18
  34. package/hooks/validators/test_exempt_paths.py +20 -0
  35. package/hooks/validators/test_python_antipattern_checks.py +14 -0
  36. package/package.json +1 -1
  37. package/rules/agent-merges-its-own-green-pull-request.md +54 -0
  38. package/rules/archiving-agent-config.md +10 -0
  39. package/rules/code-standards.md +1 -0
  40. package/rules/correction-lens.md +134 -0
  41. package/rules/flag-non-breaking-findings.md +11 -0
  42. package/rules/no-contrast-framing.md +68 -0
  43. package/rules/review-closure-is-a-check.md +44 -0
  44. package/scripts/Invoke-PesterSuites.ps1 +42 -0
  45. package/scripts/agent_merge_check.py +381 -0
  46. package/scripts/check.ps1 +12 -1
  47. package/scripts/ci/install-playtest.mjs +291 -0
  48. package/scripts/ci/install-playtest.test.mjs +84 -0
  49. package/scripts/ci/installer-lifecycle.mjs +11 -30
  50. package/scripts/ci/installer-lifecycle.test.mjs +14 -0
  51. package/scripts/ci/pstack-command-shim.test.mjs +33 -0
  52. package/scripts/ci/scratch-home-install.mjs +73 -0
  53. package/scripts/ci/scratch-home-install.test.mjs +49 -0
  54. package/scripts/codex_compat_materializer.py +33 -16
  55. package/scripts/contrast_framing.py +101 -0
  56. package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
  57. package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
  58. package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
  59. package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
  60. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
  61. package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
  62. package/scripts/durable_post_lint.py +30 -4
  63. package/scripts/followup_cli.py +6 -10
  64. package/scripts/merge_gate_checks.py +236 -0
  65. package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
  66. package/scripts/policy_lint/adapters.py +30 -0
  67. package/scripts/policy_lint/config/constants.py +2 -0
  68. package/scripts/policy_lint/registry.py +5 -0
  69. package/scripts/repository_checks/config/constants.py +12 -0
  70. package/scripts/repository_checks/config/policy_document.py +28 -2
  71. package/scripts/repository_checks/followups.py +54 -0
  72. package/scripts/repository_checks/match_exemptions.py +67 -0
  73. package/scripts/repository_checks/models.py +46 -2
  74. package/scripts/repository_checks/reporting.py +23 -7
  75. package/scripts/repository_checks/tracked_secrets.py +2 -2
  76. package/scripts/repository_policy.py +4 -2
  77. package/scripts/review_closure.py +134 -0
  78. package/scripts/review_closure_github.py +345 -0
  79. package/scripts/review_closure_model.py +360 -0
  80. package/scripts/test_agent_merge_check.py +317 -0
  81. package/scripts/test_durable_post_lint.py +46 -0
  82. package/scripts/test_followup_cli.py +40 -0
  83. package/scripts/test_merge_gate_checks.py +92 -0
  84. package/scripts/tests/test_code_review_constants.py +78 -0
  85. package/scripts/tests/test_codex_compat_materializer.py +48 -4
  86. package/scripts/tests/test_contrast_framing.py +117 -0
  87. package/scripts/tests/test_followups.py +88 -0
  88. package/scripts/tests/test_match_exemptions.py +291 -0
  89. package/scripts/tests/test_models.py +67 -2
  90. package/scripts/tests/test_package_inventory.py +6 -4
  91. package/scripts/tests/test_pester_suites_run.py +53 -0
  92. package/scripts/tests/test_policy_document.py +39 -1
  93. package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
  94. package/scripts/tests/test_reporting.py +18 -0
  95. package/scripts/tests/test_repository_policy.py +43 -1
  96. package/scripts/tests/test_review_closure.py +126 -0
  97. package/scripts/tests/test_review_closure_github.py +240 -0
  98. package/scripts/tests/test_review_closure_model.py +299 -0
  99. package/scripts/tests/test_tracked_secrets.py +42 -0
  100. package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
  101. package/scripts/repository_checks/email_exemptions.py +0 -39
  102. package/scripts/tests/test_email_exemptions.py +0 -158
@@ -0,0 +1,317 @@
1
+ """Tests for the agent merge-readiness check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
13
+
14
+ import agent_merge_check
15
+ from dev_env_scripts_constants.agent_merge_check_constants import (
16
+ BEHIND_HOLD_REASON,
17
+ BLOCKED_HOLD_REASON,
18
+ DIRTY_HOLD_REASON,
19
+ DRAFT_HOLD_REASON,
20
+ HOLD_VERDICT_LABEL,
21
+ MERGE_VERDICT_LABEL,
22
+ SETTLE_ATTEMPT_COUNT,
23
+ UNSTABLE_HOLD_REASON,
24
+ )
25
+
26
+ CLEAN_PULL_REQUEST = {
27
+ "number": 1442,
28
+ "draft": False,
29
+ "mergeable_state": "clean",
30
+ "head": {"sha": "ab845eb6945650055ebe852312a9057b9f067d6a"},
31
+ }
32
+
33
+
34
+ def _pull_request(**all_overrides: object) -> dict[str, object]:
35
+ return {**CLEAN_PULL_REQUEST, **all_overrides}
36
+
37
+
38
+ def test_clean_pull_request_with_no_open_thread_may_merge() -> None:
39
+ assert agent_merge_check.hold_reason(CLEAN_PULL_REQUEST, 0) is None
40
+
41
+
42
+ @pytest.mark.parametrize(
43
+ ("all_overrides", "expected_reason"),
44
+ [
45
+ ({"draft": True}, DRAFT_HOLD_REASON),
46
+ ({"mergeable_state": "behind"}, BEHIND_HOLD_REASON),
47
+ ({"mergeable_state": "dirty"}, DIRTY_HOLD_REASON),
48
+ ({"mergeable_state": "blocked"}, BLOCKED_HOLD_REASON),
49
+ ({"mergeable_state": "unstable"}, UNSTABLE_HOLD_REASON),
50
+ ],
51
+ )
52
+ def test_each_unready_state_holds_with_its_own_reason(
53
+ all_overrides: dict[str, object],
54
+ expected_reason: str,
55
+ ) -> None:
56
+ assert (
57
+ agent_merge_check.hold_reason(_pull_request(**all_overrides), 0)
58
+ == expected_reason
59
+ )
60
+
61
+
62
+ def test_a_draft_holds_even_when_its_merge_state_is_clean() -> None:
63
+ assert (
64
+ agent_merge_check.hold_reason(_pull_request(draft=True), 0) == DRAFT_HOLD_REASON
65
+ )
66
+
67
+
68
+ def test_an_unreported_merge_state_holds_and_names_what_github_said() -> None:
69
+ reason = agent_merge_check.hold_reason(_pull_request(mergeable_state="unknown"), 0)
70
+ assert reason is not None
71
+ assert "unknown" in reason
72
+
73
+
74
+ def test_an_open_review_thread_holds_a_green_pull_request() -> None:
75
+ reason = agent_merge_check.hold_reason(CLEAN_PULL_REQUEST, 2)
76
+ assert reason is not None
77
+ assert "2" in reason
78
+
79
+
80
+ def test_an_open_thread_count_reaches_the_verdict_line() -> None:
81
+ reason = agent_merge_check.hold_reason(CLEAN_PULL_REQUEST, 1)
82
+ line = agent_merge_check.verdict_line(
83
+ "jl-cmd/claude-dev-env", CLEAN_PULL_REQUEST, reason
84
+ )
85
+ assert line.startswith(HOLD_VERDICT_LABEL)
86
+ assert "jl-cmd/claude-dev-env#1442" in line
87
+
88
+
89
+ def test_the_ready_verdict_line_names_the_repository_and_head() -> None:
90
+ line = agent_merge_check.verdict_line(
91
+ "jl-cmd/claude-dev-env", CLEAN_PULL_REQUEST, None
92
+ )
93
+ assert line.startswith(MERGE_VERDICT_LABEL)
94
+ assert "jl-cmd/claude-dev-env#1442" in line
95
+ assert "ab845eb" in line
96
+
97
+
98
+ def test_a_resolved_thread_and_an_outdated_thread_hold_nothing_back() -> None:
99
+ all_thread_records = [
100
+ {"isResolved": True, "isOutdated": False},
101
+ {"isResolved": False, "isOutdated": True},
102
+ ]
103
+ assert agent_merge_check.count_unresolved_threads(all_thread_records) == 0
104
+
105
+
106
+ def test_an_unresolved_current_thread_counts() -> None:
107
+ all_thread_records = [
108
+ {"isResolved": False, "isOutdated": False},
109
+ {"isResolved": True, "isOutdated": False},
110
+ {"isResolved": False, "isOutdated": False},
111
+ ]
112
+ assert agent_merge_check.count_unresolved_threads(all_thread_records) == 2
113
+
114
+
115
+ def test_a_thread_record_of_another_shape_counts_as_nothing() -> None:
116
+ assert agent_merge_check.count_unresolved_threads(["", None]) == 0
117
+
118
+
119
+ def test_read_pull_request_returns_the_fields_the_api_answered_with(
120
+ monkeypatch: pytest.MonkeyPatch,
121
+ ) -> None:
122
+ monkeypatch.setattr(
123
+ agent_merge_check,
124
+ "_request_json",
125
+ lambda url, token, all_payload_fields: CLEAN_PULL_REQUEST,
126
+ )
127
+ assert (
128
+ agent_merge_check.read_pull_request("jl-cmd/claude-dev-env", 1442, "token")
129
+ == CLEAN_PULL_REQUEST
130
+ )
131
+
132
+
133
+ def test_read_pull_request_rejects_an_answer_of_another_shape(
134
+ monkeypatch: pytest.MonkeyPatch,
135
+ ) -> None:
136
+ monkeypatch.setattr(
137
+ agent_merge_check,
138
+ "_request_json",
139
+ lambda url, token, all_payload_fields: ["not a pull request"],
140
+ )
141
+ with pytest.raises(agent_merge_check.MergeCheckError):
142
+ agent_merge_check.read_pull_request("jl-cmd/claude-dev-env", 1442, "token")
143
+
144
+
145
+ def test_read_unresolved_thread_count_reads_the_rest_route(
146
+ monkeypatch: pytest.MonkeyPatch,
147
+ ) -> None:
148
+ monkeypatch.setattr(
149
+ agent_merge_check,
150
+ "_request_json",
151
+ lambda url, token, all_payload_fields: [
152
+ {"isResolved": False, "isOutdated": False},
153
+ {"isResolved": True, "isOutdated": False},
154
+ ],
155
+ )
156
+ assert (
157
+ agent_merge_check.read_unresolved_thread_count(
158
+ "jl-cmd/claude-dev-env", 1442, "token"
159
+ )
160
+ == 1
161
+ )
162
+
163
+
164
+ def test_read_unresolved_thread_count_falls_back_to_the_query(
165
+ monkeypatch: pytest.MonkeyPatch,
166
+ ) -> None:
167
+ def _answer(
168
+ url: str,
169
+ token: str,
170
+ all_payload_fields: object,
171
+ ) -> object:
172
+ if all_payload_fields is None:
173
+ raise agent_merge_check.MergeCheckError("HTTP Error 403: Forbidden")
174
+ return {
175
+ "data": {
176
+ "repository": {
177
+ "pullRequest": {
178
+ "reviewThreads": {
179
+ "nodes": [{"isResolved": False, "isOutdated": False}]
180
+ }
181
+ }
182
+ }
183
+ }
184
+ }
185
+
186
+ monkeypatch.setattr(agent_merge_check, "_request_json", _answer)
187
+ assert (
188
+ agent_merge_check.read_unresolved_thread_count(
189
+ "jl-cmd/claude-dev-env", 1442, "token"
190
+ )
191
+ == 1
192
+ )
193
+
194
+
195
+ def test_read_unresolved_thread_count_rejects_a_query_answer_of_another_shape(
196
+ monkeypatch: pytest.MonkeyPatch,
197
+ ) -> None:
198
+ def _answer(
199
+ url: str,
200
+ token: str,
201
+ all_payload_fields: object,
202
+ ) -> object:
203
+ if all_payload_fields is None:
204
+ raise agent_merge_check.MergeCheckError("HTTP Error 403: Forbidden")
205
+ return {"errors": [{"message": "Bad credentials"}]}
206
+
207
+ monkeypatch.setattr(agent_merge_check, "_request_json", _answer)
208
+ with pytest.raises(agent_merge_check.MergeCheckError):
209
+ agent_merge_check.read_unresolved_thread_count(
210
+ "jl-cmd/claude-dev-env", 1442, "token"
211
+ )
212
+
213
+
214
+ def test_read_settled_pull_request_reads_again_while_the_state_is_unknown(
215
+ monkeypatch: pytest.MonkeyPatch,
216
+ ) -> None:
217
+ all_answers = [
218
+ _pull_request(mergeable_state="unknown"),
219
+ _pull_request(mergeable_state="unknown"),
220
+ CLEAN_PULL_REQUEST,
221
+ ]
222
+ all_waits: list[float] = []
223
+ monkeypatch.setattr(
224
+ agent_merge_check,
225
+ "read_pull_request",
226
+ lambda slug, number, token: all_answers.pop(0),
227
+ )
228
+ settled = agent_merge_check.read_settled_pull_request(
229
+ "jl-cmd/claude-dev-env", 1442, "token", all_waits.append
230
+ )
231
+ assert settled == CLEAN_PULL_REQUEST
232
+ assert len(all_waits) == 2
233
+
234
+
235
+ def test_read_settled_pull_request_stops_after_its_last_attempt(
236
+ monkeypatch: pytest.MonkeyPatch,
237
+ ) -> None:
238
+ all_waits: list[float] = []
239
+ read_count = 0
240
+
241
+ def _unknown(slug: str, number: int, token: str) -> dict[str, object]:
242
+ nonlocal read_count
243
+ read_count += 1
244
+ return _pull_request(mergeable_state="unknown")
245
+
246
+ monkeypatch.setattr(agent_merge_check, "read_pull_request", _unknown)
247
+ settled = agent_merge_check.read_settled_pull_request(
248
+ "jl-cmd/claude-dev-env", 1442, "token", all_waits.append
249
+ )
250
+ assert settled["mergeable_state"] == "unknown"
251
+ assert read_count == SETTLE_ATTEMPT_COUNT
252
+ assert len(all_waits) == SETTLE_ATTEMPT_COUNT - 1
253
+
254
+
255
+ def test_read_settled_pull_request_waits_for_no_settled_state(
256
+ monkeypatch: pytest.MonkeyPatch,
257
+ ) -> None:
258
+ all_waits: list[float] = []
259
+ monkeypatch.setattr(
260
+ agent_merge_check,
261
+ "read_pull_request",
262
+ lambda slug, number, token: CLEAN_PULL_REQUEST,
263
+ )
264
+ agent_merge_check.read_settled_pull_request(
265
+ "jl-cmd/claude-dev-env", 1442, "token", all_waits.append
266
+ )
267
+ assert all_waits == []
268
+
269
+
270
+ def test_a_missing_token_reports_the_error_exit_code(
271
+ monkeypatch: pytest.MonkeyPatch,
272
+ ) -> None:
273
+ monkeypatch.delenv("GH_TOKEN", raising=False)
274
+ monkeypatch.delenv("GITHUB_TOKEN", raising=False)
275
+ assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 2
276
+
277
+
278
+ def test_a_ready_pull_request_exits_zero(monkeypatch: pytest.MonkeyPatch) -> None:
279
+ monkeypatch.setenv("GH_TOKEN", "token")
280
+ monkeypatch.setattr(
281
+ agent_merge_check,
282
+ "read_pull_request",
283
+ lambda slug, number, token: CLEAN_PULL_REQUEST,
284
+ )
285
+ monkeypatch.setattr(agent_merge_check.time, "sleep", lambda seconds: None)
286
+ monkeypatch.setattr(
287
+ agent_merge_check,
288
+ "read_unresolved_thread_count",
289
+ lambda slug, number, token: 0,
290
+ )
291
+ assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 0
292
+
293
+
294
+ def test_a_held_pull_request_exits_one(monkeypatch: pytest.MonkeyPatch) -> None:
295
+ monkeypatch.setenv("GH_TOKEN", "token")
296
+ monkeypatch.setattr(
297
+ agent_merge_check,
298
+ "read_pull_request",
299
+ lambda slug, number, token: _pull_request(mergeable_state="behind"),
300
+ )
301
+ monkeypatch.setattr(
302
+ agent_merge_check,
303
+ "read_unresolved_thread_count",
304
+ lambda slug, number, token: 0,
305
+ )
306
+ assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 1
307
+
308
+
309
+ def test_an_unreadable_pull_request_exits_two(
310
+ monkeypatch: pytest.MonkeyPatch,
311
+ ) -> None:
312
+ def _fail(slug: str, number: int, token: str) -> dict[str, object]:
313
+ raise agent_merge_check.MergeCheckError("no route to host")
314
+
315
+ monkeypatch.setenv("GH_TOKEN", "token")
316
+ monkeypatch.setattr(agent_merge_check, "read_pull_request", _fail)
317
+ assert agent_merge_check.main(["jl-cmd/claude-dev-env", "1442"]) == 2
@@ -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 test_render_codex_failure_blast_radius_requires_the_excerpt_heading() -> None:
177
- with pytest.raises(MaterializerError, match="requires a Codex excerpt"):
178
- materializer.render_codex_failure_blast_radius("# No excerpt\n")
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.render_codex_failure_blast_radius(canonical_rule)
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: