claude-dev-env 7.0.0 → 7.1.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 (210) hide show
  1. package/.agents/skills/pull-request/scripts/pull_request.py +2 -0
  2. package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
  3. package/.agents/skills/session-continuity/AGENTS.md +1 -0
  4. package/.agents/skills/session-continuity/CLAUDE.md +1 -0
  5. package/.agents/skills/session-continuity/README.md +189 -0
  6. package/.agents/skills/session-continuity/SKILL.md +114 -0
  7. package/.agents/skills/session-continuity/continuity.mjs +375 -0
  8. package/.agents/skills/session-continuity/continuity.test.mjs +303 -0
  9. package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
  10. package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
  11. package/bin/cde.mjs +36 -8
  12. package/bin/cde.test.mjs +48 -0
  13. package/bin/ever-shipped-skills.mjs +2 -20
  14. package/bin/ever-shipped-skills.test.mjs +8 -0
  15. package/bin/expand_home_directory_tokens.mjs +4 -5
  16. package/bin/expand_home_directory_tokens.test.mjs +128 -0
  17. package/bin/git_hooks_installer.mjs +22 -3
  18. package/bin/git_hooks_installer.test.mjs +159 -5
  19. package/bin/install-session-continuity.mjs +78 -0
  20. package/bin/install-session-continuity.test.mjs +42 -0
  21. package/bin/install.codex-rules.test.mjs +5 -2
  22. package/bin/install.cursor-rules.test.mjs +5 -2
  23. package/bin/install.mjs +478 -63
  24. package/bin/install.policy-lint.test.mjs +56 -4
  25. package/bin/install.profiles.test.mjs +10 -0
  26. package/bin/install.prune.test.mjs +227 -17
  27. package/bin/install.test.mjs +500 -120
  28. package/bin/install.transaction.test.mjs +3 -3
  29. package/docs/CODE_RULES.md +1 -1
  30. package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
  31. package/hooks/blocking/precommit_code_rules_gate.py +5 -8
  32. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
  33. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
  34. package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
  35. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
  36. package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
  37. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
  38. package/hooks/git-hooks/post_commit.py +33 -0
  39. package/hooks/git-hooks/pre_commit.py +18 -90
  40. package/hooks/git-hooks/pre_push.py +45 -541
  41. package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
  42. package/hooks/git-hooks/test_native_hook_support.py +129 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +39 -6
  44. package/hooks/git-hooks/test_post_commit.py +43 -0
  45. package/hooks/git-hooks/test_pre_commit.py +36 -392
  46. package/hooks/git-hooks/test_pre_push.py +48 -1631
  47. package/hooks/git-hooks/test_verification_notice.py +60 -0
  48. package/hooks/git-hooks/test_verification_start.py +173 -0
  49. package/hooks/git-hooks/verification_notice.py +192 -0
  50. package/hooks/git-hooks/verification_notice_context.py +253 -0
  51. package/hooks/git-hooks/verification_notice_state.py +237 -0
  52. package/hooks/git-hooks/verification_start.py +100 -0
  53. package/hooks/hooks.json +0 -119
  54. package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
  55. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
  56. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
  57. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
  58. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
  59. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
  60. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
  61. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
  62. package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
  63. package/hooks/validators/run_all_validators.py +5 -1
  64. package/hooks/validators/security_checks.py +57 -2
  65. package/hooks/validators/test_hook_timing_harness.py +2 -5
  66. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
  67. package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
  68. package/hooks/validators/test_security_checks.py +57 -0
  69. package/package.json +1 -1
  70. package/rules/confirm-implementation-forks.md +2 -0
  71. package/rules/git-workflow.md +12 -0
  72. package/rules/pstack-host-mapping.md +48 -0
  73. package/rules/vault-context.md +3 -5
  74. package/rules/verify-runtime-state.md +7 -0
  75. package/scripts/audit-hooks-cli.mjs +5 -0
  76. package/scripts/audit-hooks.mjs +72 -16
  77. package/scripts/automatic_advisory/__init__.py +1 -0
  78. package/scripts/automatic_advisory/checkout.py +51 -0
  79. package/scripts/automatic_advisory/cli.py +334 -0
  80. package/scripts/automatic_advisory/config/__init__.py +1 -0
  81. package/scripts/automatic_advisory/config/constants.py +93 -0
  82. package/scripts/automatic_advisory/config/timing.py +4 -0
  83. package/scripts/automatic_advisory/configuration.py +197 -0
  84. package/scripts/automatic_advisory/execution.py +350 -0
  85. package/scripts/automatic_advisory/git.py +69 -0
  86. package/scripts/automatic_advisory/model.py +107 -0
  87. package/scripts/automatic_advisory/process_host.py +32 -0
  88. package/scripts/automatic_advisory/publisher.py +84 -0
  89. package/scripts/automatic_advisory/runner.py +399 -0
  90. package/scripts/automatic_advisory/state.py +116 -0
  91. package/scripts/automatic_advisory/window_flags.py +41 -0
  92. package/scripts/automatic_advisory/windows_job.py +122 -0
  93. package/scripts/codex_compat_materializer.py +99 -13
  94. package/scripts/durable_post_lint.py +30 -2
  95. package/scripts/durable_post_lint_config/config/constants.py +10 -0
  96. package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
  97. package/scripts/local_report_cli.py +184 -0
  98. package/scripts/local_report_core.py +247 -0
  99. package/scripts/local_report_publisher.py +12 -0
  100. package/scripts/local_report_validation.py +357 -0
  101. package/scripts/local_verification/__init__.py +23 -0
  102. package/scripts/local_verification/check_runner.py +364 -0
  103. package/scripts/local_verification/check_support.py +99 -0
  104. package/scripts/local_verification/cli.py +158 -0
  105. package/scripts/local_verification/command_runner.py +84 -0
  106. package/scripts/local_verification/config/__init__.py +1 -0
  107. package/scripts/local_verification/config/constants.py +89 -0
  108. package/scripts/local_verification/config/timing.py +1 -0
  109. package/scripts/local_verification/git_state.py +146 -0
  110. package/scripts/local_verification/manifest.py +224 -0
  111. package/scripts/local_verification/model.py +174 -0
  112. package/scripts/local_verification/runner.py +234 -0
  113. package/scripts/policy_lint/adapter_pairing.py +58 -3
  114. package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
  115. package/scripts/pr_verification/__init__.py +1 -0
  116. package/scripts/pr_verification/branch_refs.py +114 -0
  117. package/scripts/pr_verification/config/__init__.py +1 -0
  118. package/scripts/pr_verification/config/constants.py +84 -0
  119. package/scripts/pr_verification/config/timing.py +5 -0
  120. package/scripts/pr_verification/github.py +137 -0
  121. package/scripts/pr_verification/github_api.py +376 -0
  122. package/scripts/pr_verification/github_labels.py +88 -0
  123. package/scripts/pr_verification/github_parsing.py +182 -0
  124. package/scripts/pr_verification/github_transport.py +91 -0
  125. package/scripts/pr_verification/lock.py +122 -0
  126. package/scripts/pr_verification/model.py +43 -0
  127. package/scripts/pr_verification/requirements.txt +1 -0
  128. package/scripts/test_durable_post_lint.py +97 -0
  129. package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
  130. package/scripts/tests/test-audit-hooks.mjs +94 -2
  131. package/scripts/tests/test_automatic_advisory.py +376 -0
  132. package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
  133. package/scripts/tests/test_automatic_advisory_polling.py +216 -0
  134. package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
  135. package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
  136. package/scripts/tests/test_automatic_advisory_settings.py +48 -0
  137. package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
  138. package/scripts/tests/test_closed_pr_label.py +157 -0
  139. package/scripts/tests/test_codex_compat_materializer.py +189 -6
  140. package/scripts/tests/test_github_labels.py +101 -0
  141. package/scripts/tests/test_github_transport.py +125 -0
  142. package/scripts/tests/test_local_report_publisher.py +396 -0
  143. package/scripts/tests/test_local_report_publisher_cli.py +85 -0
  144. package/scripts/tests/test_local_verification.py +348 -0
  145. package/scripts/tests/test_local_verification_cli.py +98 -0
  146. package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
  147. package/scripts/tests/test_pr_verification_github.py +392 -0
  148. package/scripts/tests/test_pr_verification_lock.py +168 -0
  149. package/scripts/tests/test_verification_notice.py +347 -0
  150. package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
  151. package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
  152. package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
  153. package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
  154. package/hooks/blocking/destructive_command_blocker.py +0 -2073
  155. package/hooks/blocking/fable_spawn_gate.py +0 -187
  156. package/hooks/blocking/luna_fast_mode_gate.py +0 -160
  157. package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
  158. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
  159. package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
  160. package/hooks/blocking/pii_prevention_blocker.py +0 -365
  161. package/hooks/blocking/piped_pytest_blocker.py +0 -1223
  162. package/hooks/blocking/question_to_user_enforcer.py +0 -130
  163. package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
  164. package/hooks/blocking/sensitive_file_protector.py +0 -167
  165. package/hooks/blocking/session_edit_stage_gate.py +0 -684
  166. package/hooks/blocking/session_handoff_blocker.py +0 -182
  167. package/hooks/blocking/shell_substitution_blocker.py +0 -350
  168. package/hooks/blocking/test_ask_user_question_shape.py +0 -69
  169. package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
  170. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
  171. package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
  172. package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
  173. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
  174. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
  175. package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
  176. package/hooks/blocking/test_fable_spawn_gate.py +0 -381
  177. package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
  178. package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
  179. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
  180. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
  181. package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
  182. package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
  183. package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
  184. package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
  185. package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
  186. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
  187. package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
  188. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
  189. package/hooks/blocking/test_sensitive_file_protector.py +0 -365
  190. package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
  191. package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
  192. package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
  193. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
  194. package/hooks/blocking/test_session_handoff_blocker.py +0 -318
  195. package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
  196. package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
  197. package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
  198. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
  199. package/hooks/blocking/unscoped_search_blocker.py +0 -391
  200. package/hooks/blocking/write_existing_file_blocker.py +0 -124
  201. package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
  202. package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
  203. package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
  204. package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
  205. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
  206. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
  207. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
  208. package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
  209. package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
  210. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sys
5
+ from dataclasses import replace
6
+ from pathlib import Path
7
+ from types import SimpleNamespace
8
+
9
+ import pytest
10
+
11
+ TEST_DIRECTORY = Path(__file__).resolve().parent
12
+ if str(TEST_DIRECTORY) not in sys.path:
13
+ sys.path.insert(0, str(TEST_DIRECTORY))
14
+
15
+ import test_automatic_advisory as support
16
+ from automatic_advisory import cli as advisory_cli
17
+ from automatic_advisory import execution
18
+ from automatic_advisory.configuration import (
19
+ AdvisoryConfigurationError,
20
+ load_advisory_settings,
21
+ )
22
+ from automatic_advisory.model import ChildOutcome
23
+ from automatic_advisory.runner import (
24
+ AdvisoryGitHub,
25
+ AutomaticAdvisoryRunner,
26
+ Publication,
27
+ run_verification_child,
28
+ )
29
+
30
+
31
+ def test_runner_retries_after_dirty_checkout_becomes_clean(tmp_path: Path) -> None:
32
+ checkout_path, head_sha = support._build_checkout(tmp_path)
33
+ registration = support._build_registration(tmp_path, checkout_path)
34
+ github = support.FakeGitHub(support._build_candidate(head_sha))
35
+ child_calls: list[str] = []
36
+ dirty_path = checkout_path / "dirty.txt"
37
+ dirty_path.write_text("pending\n", encoding="utf-8")
38
+ runner = support._build_counting_runner(
39
+ registration, github, child_calls, support._publish_passed
40
+ )
41
+
42
+ waiting_state = runner.run_once()[0]
43
+ dirty_path.unlink()
44
+ passed_state = runner.run_once()[0]
45
+
46
+ assert waiting_state.status == "waiting"
47
+ assert passed_state.status == "passed"
48
+ assert child_calls == ["child"]
49
+
50
+
51
+ def test_runner_retries_after_publication_failure(tmp_path: Path) -> None:
52
+ checkout_path, head_sha = support._build_checkout(tmp_path)
53
+ registration = support._build_registration(tmp_path, checkout_path)
54
+ github = support.FakeGitHub(support._build_candidate(head_sha))
55
+ child_calls: list[str] = []
56
+ publication_calls: list[str] = []
57
+
58
+ def publish_once_then_pass(*all_arguments: object) -> Publication:
59
+ publication_calls.append("publish")
60
+ if len(publication_calls) == 1:
61
+ raise RuntimeError("temporary publication failure")
62
+ return support.FakePublication("passed")
63
+
64
+ runner = support._build_counting_runner(
65
+ registration, github, child_calls, publish_once_then_pass
66
+ )
67
+
68
+ failed_state = runner.run_once()[0]
69
+ passed_state = runner.run_once()[0]
70
+
71
+ assert failed_state.status == "error"
72
+ assert passed_state.status == "passed"
73
+ assert child_calls == ["child", "child"]
74
+ assert publication_calls == ["publish", "publish"]
75
+
76
+
77
+ def test_runner_retries_after_base_fetch_failure(tmp_path: Path) -> None:
78
+ checkout_path, head_sha = support._build_checkout(tmp_path)
79
+ registration = support._build_registration(tmp_path, checkout_path)
80
+ unavailable_registration = replace(registration, remote_name="missing")
81
+ github = support.FakeGitHub(support._build_candidate(head_sha))
82
+ failed_runner = support._build_counting_runner(
83
+ unavailable_registration, github, [], support._publish_passed
84
+ )
85
+
86
+ offline_state = failed_runner.run_once()[0]
87
+ recovered_runner = support._build_counting_runner(
88
+ registration, github, [], support._publish_passed
89
+ )
90
+ passed_state = recovered_runner.run_once()[0]
91
+
92
+ assert offline_state.status == "offline"
93
+ assert passed_state.status == "passed"
94
+
95
+
96
+ def test_runner_retries_label_invalidation_after_first_failure(
97
+ tmp_path: Path,
98
+ ) -> None:
99
+ checkout_path, head_sha = support._build_checkout(tmp_path)
100
+ registration = support._build_registration(tmp_path, checkout_path)
101
+ github = support.FakeGitHub(support._build_candidate(head_sha))
102
+ github.remove_label_failures = 1
103
+ dirty_path = checkout_path / "dirty.txt"
104
+ dirty_path.write_text("pending\n", encoding="utf-8")
105
+ runner = support._build_counting_runner(
106
+ registration, github, [], support._publish_passed
107
+ )
108
+
109
+ error_state = runner.run_once()[0]
110
+ waiting_state = runner.run_once()[0]
111
+
112
+ assert error_state.status == "error"
113
+ assert waiting_state.status == "waiting"
114
+ assert github.all_events == ["remove-label", "remove-label"]
115
+
116
+
117
+ def _build_refresh_failure_runner(
118
+ registration: support.AdvisoryRegistration,
119
+ github: support.FakeGitHub,
120
+ factory_calls: list[str],
121
+ ) -> AutomaticAdvisoryRunner:
122
+ def refresh_github(repository: object) -> AdvisoryGitHub:
123
+ factory_calls.append("refresh")
124
+ if len(factory_calls) > 1:
125
+ raise RuntimeError("temporary GitHub outage")
126
+ return github
127
+
128
+ def run_child(
129
+ registration: support.AdvisoryRegistration,
130
+ base_ref: str,
131
+ timeout_seconds: float,
132
+ ) -> ChildOutcome:
133
+ support._write_child_artifacts(registration, 0)
134
+ return ChildOutcome(0, "", "", False)
135
+
136
+ runner = AutomaticAdvisoryRunner(
137
+ support._build_settings(registration),
138
+ github,
139
+ github_factory=refresh_github,
140
+ child_runner=run_child,
141
+ publisher=support._publish_passed,
142
+ clock=lambda: "2026-09-05T00:00:00+00:00",
143
+ )
144
+ return runner
145
+
146
+
147
+ def test_runner_keeps_daemon_alive_when_refresh_fails(tmp_path: Path) -> None:
148
+ checkout_path, head_sha = support._build_checkout(tmp_path)
149
+ registration = support._build_registration(tmp_path, checkout_path)
150
+ github = support.FakeGitHub(support._build_candidate(head_sha))
151
+ factory_calls: list[str] = []
152
+ runner = _build_refresh_failure_runner(registration, github, factory_calls)
153
+
154
+ state = runner.run_once()[0]
155
+
156
+ assert state.status == "offline"
157
+ assert factory_calls == ["refresh", "refresh"]
158
+
159
+
160
+ def test_child_start_failure_clears_stale_report(tmp_path: Path) -> None:
161
+ checkout_path, head_sha = support._build_checkout(tmp_path)
162
+ registration = support._build_registration(tmp_path, checkout_path)
163
+ registration.report_path.parent.mkdir(parents=True)
164
+ registration.report_path.write_text("stale\n", encoding="utf-8")
165
+ registration.selected_manifest_path.write_text("stale\n", encoding="utf-8")
166
+ (checkout_path / ".github" / "ci" / "local_verify.py").unlink()
167
+
168
+ child_outcome = run_verification_child(
169
+ registration,
170
+ head_sha,
171
+ 30.0,
172
+ )
173
+
174
+ assert child_outcome.exit_code != 0
175
+ assert not registration.report_path.exists()
176
+ assert not registration.selected_manifest_path.exists()
177
+
178
+
179
+ def test_runner_rejects_child_report_exit_code_mismatch(tmp_path: Path) -> None:
180
+ checkout_path, head_sha = support._build_checkout(tmp_path)
181
+ registration = support._build_registration(tmp_path, checkout_path)
182
+ github = support.FakeGitHub(support._build_candidate(head_sha))
183
+ publisher_calls: list[str] = []
184
+
185
+ def mismatched_child(
186
+ child_registration: support.AdvisoryRegistration,
187
+ base_ref: str,
188
+ timeout_seconds: float,
189
+ ) -> ChildOutcome:
190
+ support._write_child_artifacts(child_registration, 0)
191
+ return ChildOutcome(1, "", "check failed", False)
192
+
193
+ def publish(*all_arguments: object) -> Publication:
194
+ publisher_calls.append("publish")
195
+ return support.FakePublication("passed")
196
+
197
+ runner = AutomaticAdvisoryRunner(
198
+ support._build_settings(registration),
199
+ github,
200
+ child_runner=mismatched_child,
201
+ publisher=publish,
202
+ clock=lambda: "2026-09-05T00:00:00+00:00",
203
+ )
204
+
205
+ state = runner.run_once()[0]
206
+ assert state.status == "error"
207
+ assert publisher_calls == []
208
+
209
+
210
+ def test_runner_removes_pass_label_before_changed_dirty_checkout_waits(
211
+ tmp_path: Path,
212
+ ) -> None:
213
+ checkout_path, head_sha = support._build_checkout(tmp_path)
214
+ registration = support._build_registration(tmp_path, checkout_path)
215
+ github = support.FakeGitHub(support._build_candidate(head_sha))
216
+ child_calls: list[str] = []
217
+ runner = support._build_counting_runner(
218
+ registration, github, child_calls, support._publish_passed
219
+ )
220
+
221
+ first_state = runner.run_once()[0]
222
+ (checkout_path / "dirty.txt").write_text("pending\n", encoding="utf-8")
223
+ github.candidate = support._build_candidate("changed-head")
224
+ changed_state = runner.run_once()[0]
225
+
226
+ assert first_state.status == "passed"
227
+ assert changed_state.status == "waiting"
228
+ assert github.all_events == ["remove-label", "remove-label"]
229
+ assert child_calls == ["child"]
230
+
231
+
232
+ def test_runner_accepts_open_conflicting_pull_request_candidate(
233
+ tmp_path: Path,
234
+ ) -> None:
235
+ checkout_path, head_sha = support._build_checkout(tmp_path)
236
+ registration = support._build_registration(tmp_path, checkout_path)
237
+ github = support.FakeGitHub(support._build_candidate(head_sha, merge_sha=""))
238
+ runner = support._build_counting_runner(
239
+ registration, github, [], support._publish_passed
240
+ )
241
+
242
+ state = runner.run_once()[0]
243
+
244
+ assert state.status == "passed"
245
+ assert github.all_merge_requirements == [False]
246
+
247
+
248
+ def test_start_polling_returns_without_waiting_for_detached_process(
249
+ monkeypatch: pytest.MonkeyPatch,
250
+ tmp_path: Path,
251
+ ) -> None:
252
+ all_launches: list[tuple[tuple[object, ...], dict[str, object]]] = []
253
+
254
+ def record_launch(
255
+ all_arguments: tuple[object, ...], **launch_options: object
256
+ ) -> None:
257
+ all_launches.append((all_arguments, launch_options))
258
+
259
+ monkeypatch.setattr(advisory_cli.subprocess, "Popen", record_launch)
260
+
261
+ exit_code = advisory_cli.start_polling(tmp_path / "settings.json")
262
+
263
+ assert exit_code == 0
264
+ assert len(all_launches) == 1
265
+ launch_arguments, launch_options = all_launches[0]
266
+ assert launch_arguments[-1] == "--poll"
267
+ assert launch_options["stdin"] is advisory_cli.subprocess.DEVNULL
268
+ assert launch_options["stdout"] is advisory_cli.subprocess.DEVNULL
269
+
270
+
271
+ def test_configuration_rejects_two_pairs_for_one_repository(tmp_path: Path) -> None:
272
+ settings_path = tmp_path / "settings.json"
273
+ registration = {
274
+ "repository": "JonEcho/python-automation",
275
+ "pull_request": 2985,
276
+ "app_id": 4841271,
277
+ "installation_id": 159293880,
278
+ "private_key_path": str(tmp_path / "private-key.pem"),
279
+ "checkout": str(tmp_path),
280
+ "manifest": "manifest.json",
281
+ "report": str(tmp_path / "report.json"),
282
+ "state": str(tmp_path / "state.json"),
283
+ "base_ref": "main",
284
+ "remote": "origin",
285
+ }
286
+ settings_path.write_text(
287
+ json.dumps({"version": 1, "registrations": [registration, registration]}),
288
+ encoding="utf-8",
289
+ )
290
+
291
+ with pytest.raises(AdvisoryConfigurationError, match="one automatic advisory"):
292
+ load_advisory_settings(settings_path)
293
+
294
+
295
+ class _BrokenStartStream:
296
+ def __init__(self) -> None:
297
+ self.close_calls = 0
298
+
299
+ def write(self, text: str) -> int:
300
+ raise BrokenPipeError(text)
301
+
302
+ def flush(self) -> None:
303
+ return None
304
+
305
+ def close(self) -> None:
306
+ self.close_calls += 1
307
+ raise BrokenPipeError("close")
308
+
309
+
310
+ def test_release_windows_process_host_returns_false_when_stdin_breaks_on_close() -> (
311
+ None
312
+ ):
313
+ broken_stream = _BrokenStartStream()
314
+ child_process = SimpleNamespace(stdin=broken_stream)
315
+
316
+ was_released = execution._release_windows_process_host(child_process)
317
+
318
+ assert was_released is False
319
+ assert child_process.stdin is None
320
+ assert broken_stream.close_calls == 1