claude-dev-env 2.0.2 → 2.2.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 (110) hide show
  1. package/CLAUDE.md +1 -1
  2. package/hooks/blocking/CLAUDE.md +1 -0
  3. package/hooks/blocking/code_rules_shared.py +126 -47
  4. package/hooks/blocking/config/CLAUDE.md +2 -0
  5. package/hooks/blocking/config/verified_commit_context_constants.py +21 -0
  6. package/hooks/blocking/config/verified_commit_gate_output_constants.py +14 -0
  7. package/hooks/blocking/conftest.py +36 -30
  8. package/hooks/blocking/convergence_gate_blocker.py +76 -8
  9. package/hooks/blocking/plain_language_blocker.py +8 -0
  10. package/hooks/blocking/state_description_blocker.py +4 -1
  11. package/hooks/blocking/test_code_rules_shared.py +120 -20
  12. package/hooks/blocking/test_convergence_gate_blocker.py +146 -0
  13. package/hooks/blocking/test_plain_language_blocker.py +15 -0
  14. package/hooks/blocking/test_state_description_blocker.py +15 -0
  15. package/hooks/blocking/test_verified_commit_config_bootstrap.py +18 -0
  16. package/hooks/blocking/test_verified_commit_gate_additional_context.py +134 -0
  17. package/hooks/blocking/tests/test_verified_commit_gate.py +41 -0
  18. package/hooks/blocking/verified_commit_config_bootstrap.py +22 -10
  19. package/hooks/blocking/verified_commit_gate.py +113 -568
  20. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +28 -0
  21. package/hooks/blocking/verified_commit_gate_parts/__init__.py +1 -0
  22. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +174 -0
  23. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +53 -0
  24. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +80 -0
  25. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +170 -0
  26. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +205 -0
  27. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +10 -0
  28. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +94 -0
  29. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +17 -0
  30. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +38 -0
  31. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +71 -0
  32. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +61 -0
  33. package/hooks/hooks_constants/CLAUDE.md +4 -1
  34. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  35. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +36 -0
  36. package/hooks/hooks_constants/harness_scratchpad_constants.py +10 -9
  37. package/hooks/hooks_constants/mypy_integration_constants.py +16 -0
  38. package/hooks/validators/mypy_integration.py +145 -24
  39. package/hooks/validators/python_antipattern_checks.py +16 -0
  40. package/hooks/validators/run_all_validators.py +9 -3
  41. package/hooks/validators/test_mypy_integration.py +154 -0
  42. package/hooks/validators/test_python_antipattern_checks.py +112 -1
  43. package/hooks/validators/test_run_all_validators_pretooluse.py +16 -0
  44. package/package.json +1 -1
  45. package/rules/CLAUDE.md +1 -0
  46. package/rules/verified-commit-gate-skip.md +28 -0
  47. package/skills/_shared/pr-loop/CLAUDE.md +18 -13
  48. package/skills/_shared/pr-loop/portable-driver.md +150 -0
  49. package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
  50. package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
  51. package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
  52. package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
  53. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
  54. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
  55. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
  56. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
  57. package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
  58. package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
  59. package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
  60. package/skills/auditing-claude-config/CLAUDE.md +2 -1
  61. package/skills/auditing-claude-config/SKILL.md +114 -176
  62. package/skills/auditing-claude-config/reference/probe-hook.md +74 -0
  63. package/skills/autoconverge/CLAUDE.md +6 -3
  64. package/skills/autoconverge/SKILL.md +421 -346
  65. package/skills/autoconverge/reference/CLAUDE.md +1 -0
  66. package/skills/autoconverge/reference/convergence.md +5 -5
  67. package/skills/autoconverge/reference/copilot-findings.md +51 -0
  68. package/skills/autoconverge/reference/multi-pr.md +33 -2
  69. package/skills/autoconverge/reference/stop-conditions.md +9 -6
  70. package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
  71. package/skills/closeout/SKILL.md +7 -9
  72. package/skills/copilot-finding-triage/SKILL.md +21 -11
  73. package/skills/copilot-review/CLAUDE.md +3 -2
  74. package/skills/copilot-review/SKILL.md +119 -155
  75. package/skills/copilot-review/templates/subagent-prompt.md +49 -0
  76. package/skills/fresh-branch/CLAUDE.md +6 -9
  77. package/skills/fresh-branch/SKILL.md +84 -33
  78. package/skills/fresh-branch/scripts/create_fresh_branch.py +445 -0
  79. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/__init__.py +1 -0
  80. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +74 -0
  81. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +670 -0
  82. package/skills/pr-converge/CLAUDE.md +4 -3
  83. package/skills/pr-converge/SKILL.md +469 -422
  84. package/skills/pr-converge/reference/CLAUDE.md +1 -0
  85. package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
  86. package/skills/pr-converge/reference/per-tick.md +51 -24
  87. package/skills/pr-converge/reference/progress-checklist.md +168 -0
  88. package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
  89. package/skills/pr-converge/test_step5_host_branch.py +8 -6
  90. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
  91. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  92. package/skills/privacy-hygiene/SKILL.md +68 -115
  93. package/skills/privacy-hygiene/reference/sweep-procedure.md +62 -0
  94. package/skills/session-log/CLAUDE.md +2 -1
  95. package/skills/session-log/SKILL.md +4 -26
  96. package/skills/session-log/templates/frontmatter.md +40 -0
  97. package/skills/skill-builder/CLAUDE.md +8 -7
  98. package/skills/skill-builder/SKILL.md +26 -11
  99. package/skills/skill-builder/references/CLAUDE.md +3 -1
  100. package/skills/skill-builder/references/delegation-map.md +21 -12
  101. package/skills/skill-builder/references/description-field.md +9 -11
  102. package/skills/skill-builder/references/deterministic-elements.md +218 -0
  103. package/skills/skill-builder/references/self-audit-checklist.md +62 -45
  104. package/skills/skill-builder/references/skill-modularity.md +8 -9
  105. package/skills/skill-builder/templates/CLAUDE.md +2 -2
  106. package/skills/skill-builder/templates/gap-analysis.md +15 -0
  107. package/skills/skill-builder/workflows/CLAUDE.md +5 -5
  108. package/skills/skill-builder/workflows/improve-skill.md +18 -9
  109. package/skills/skill-builder/workflows/new-skill.md +23 -15
  110. package/skills/skill-builder/workflows/polish-skill.md +28 -21
@@ -0,0 +1,1119 @@
1
+ """Tests for portable_converge_driver — deterministic phase machine only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
11
+ if str(_SCRIPTS_DIR) not in sys.path:
12
+ sys.path.insert(0, str(_SCRIPTS_DIR))
13
+
14
+ import portable_converge_driver as driver # noqa: E402
15
+ from skills_pr_loop_constants.portable_driver_constants import ( # noqa: E402
16
+ BLOCKER_COPILOT_WAIT_CAP,
17
+ BLOCKER_INLINE_LAG_CAP,
18
+ BLOCKER_NOT_PORTABLE,
19
+ BUGBOT_INLINE_LAG_WAIT_SECONDS,
20
+ COPILOT_WAIT_HARD_CAP,
21
+ DEFAULT_WAIT_SECONDS,
22
+ EXIT_CONTRACT_ERROR,
23
+ EXIT_SUCCESS,
24
+ EXIT_USAGE_ERROR,
25
+ INLINE_LAG_STREAK_CAP,
26
+ NEXT_APPLY_FIXES,
27
+ NEXT_CHECK_READY,
28
+ NEXT_MARK_READY,
29
+ NEXT_POLL_WAIT,
30
+ NEXT_RUN_BUGTEAM,
31
+ NEXT_RUN_BUGBOT_GATE,
32
+ NEXT_RUN_CODE_REVIEW,
33
+ NEXT_RUN_CODEX,
34
+ NEXT_STOP_BLOCKED,
35
+ PHASE_BLOCKED,
36
+ PHASE_BUGTEAM,
37
+ PHASE_BUGBOT,
38
+ PHASE_CODE_REVIEW,
39
+ PHASE_COPILOT_WAIT,
40
+ PHASE_READY,
41
+ RESULT_KEY_BLOCKER,
42
+ RESULT_KEY_NEXT,
43
+ RESULT_KEY_PACER,
44
+ RESULT_KEY_PHASE,
45
+ RESULT_KEY_STATUS,
46
+ RESULT_KEY_WAIT_SECONDS,
47
+ SERVED_COMMAND_IN_SESSION,
48
+ STATE_KEY_CODEX_CLEAN_AT,
49
+ STATE_KEY_CODEX_DOWN,
50
+ STATE_KEY_CODEX_REQUIRED,
51
+ STATE_KEY_PENDING_NEXT,
52
+ STATE_KEY_PENDING_WAIT_SECONDS,
53
+ STATUS_ERROR,
54
+ STATUS_OK,
55
+ )
56
+
57
+
58
+ _REAL_PROBE_WEEKLY_USAGE_REQUIRES_CODEX = driver.probe_weekly_usage_requires_codex
59
+
60
+
61
+ @pytest.fixture(autouse=True)
62
+ def stub_codex_usage_probe_off(monkeypatch: pytest.MonkeyPatch) -> None:
63
+ """Keep unit tests offline: live weekly probe never requires Codex."""
64
+ monkeypatch.setattr(
65
+ driver,
66
+ "probe_weekly_usage_requires_codex",
67
+ lambda: False,
68
+ )
69
+
70
+
71
+ @pytest.fixture
72
+ def state_file(tmp_path: Path) -> Path:
73
+ path = tmp_path / "pr-converge-state.json"
74
+ state = driver.build_initial_state(
75
+ current_head="abc123",
76
+ entry_skill="autoconverge",
77
+ is_copilot_down=True,
78
+ is_bugbot_down=True,
79
+ is_codex_down=True,
80
+ is_codex_required=False,
81
+ owner="owner",
82
+ repo="repo",
83
+ pr_number=220,
84
+ session_model="third-party",
85
+ cwd_path=str(tmp_path / "worktree"),
86
+ )
87
+ driver.save_state(path, state)
88
+ return path
89
+
90
+
91
+ def test_after_code_review_clean_advances_to_bugteam(state_file: Path) -> None:
92
+ payload, exit_code = driver.run_after_code_review(
93
+ state_file=state_file,
94
+ returncode=0,
95
+ is_dirty_tree=False,
96
+ served_command="claude.exe",
97
+ current_head="abc123",
98
+ )
99
+ assert exit_code == EXIT_SUCCESS
100
+ assert payload[RESULT_KEY_STATUS] == STATUS_OK
101
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_BUGTEAM
102
+ assert payload[RESULT_KEY_PHASE] == PHASE_BUGTEAM
103
+ reloaded = driver.load_state(state_file)
104
+ assert reloaded["code_review_clean_at"] == "abc123"
105
+
106
+
107
+ def test_after_code_review_dirty_tree_requests_fixes(state_file: Path) -> None:
108
+ payload, exit_code = driver.run_after_code_review(
109
+ state_file=state_file,
110
+ returncode=0,
111
+ is_dirty_tree=True,
112
+ served_command="claude.exe",
113
+ current_head="abc123",
114
+ )
115
+ assert exit_code == EXIT_SUCCESS
116
+ assert payload[RESULT_KEY_NEXT] == NEXT_APPLY_FIXES
117
+ assert payload[RESULT_KEY_PHASE] == PHASE_CODE_REVIEW
118
+
119
+
120
+ def test_after_code_review_failed_serve_stays_code_review(
121
+ state_file: Path,
122
+ ) -> None:
123
+ payload, exit_code = driver.run_after_code_review(
124
+ state_file=state_file,
125
+ returncode=1,
126
+ is_dirty_tree=False,
127
+ served_command="",
128
+ current_head="abc123",
129
+ )
130
+ assert exit_code == EXIT_SUCCESS
131
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
132
+ assert payload[RESULT_KEY_BLOCKER] == "code_review_failed"
133
+
134
+
135
+ def test_after_bugteam_converged_with_down_gates_checks_ready(
136
+ state_file: Path,
137
+ ) -> None:
138
+ payload, exit_code = driver.run_after_bugteam(
139
+ state_file=state_file,
140
+ is_pushed=False,
141
+ is_converged=True,
142
+ current_head="abc123",
143
+ )
144
+ assert exit_code == EXIT_SUCCESS
145
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
146
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
147
+ all_commands = payload["commands"]
148
+ assert isinstance(all_commands, list)
149
+ joined = " ".join(str(each_token) for each_token in all_commands)
150
+ assert "check_convergence.py" in joined
151
+ assert "--codex-down" in joined
152
+
153
+
154
+ def test_after_bugteam_pushed_resets_to_code_review(state_file: Path) -> None:
155
+ state = driver.load_state(state_file)
156
+ state["code_review_clean_at"] = "old"
157
+ state["bugteam_clean_at"] = "old"
158
+ state["bugbot_down"] = True
159
+ state["copilot_down"] = True
160
+ state[STATE_KEY_CODEX_DOWN] = True
161
+ state[STATE_KEY_CODEX_CLEAN_AT] = "old"
162
+ state["inline_lag_streak"] = 2
163
+ driver.save_state(state_file, state)
164
+ payload, exit_code = driver.run_after_bugteam(
165
+ state_file=state_file,
166
+ is_pushed=True,
167
+ is_converged=False,
168
+ current_head="def456",
169
+ )
170
+ assert exit_code == EXIT_SUCCESS
171
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
172
+ reloaded = driver.load_state(state_file)
173
+ assert reloaded["code_review_clean_at"] is None
174
+ assert reloaded["current_head"] == "def456"
175
+ assert reloaded["bugbot_down"] is False
176
+ assert reloaded["copilot_down"] is True
177
+ assert reloaded[STATE_KEY_CODEX_DOWN] is False
178
+ assert reloaded[STATE_KEY_CODEX_CLEAN_AT] is None
179
+ assert reloaded["inline_lag_streak"] == 0
180
+
181
+
182
+ def test_after_bugbot_clean_with_down_gates_checks_ready(
183
+ state_file: Path,
184
+ ) -> None:
185
+ payload, exit_code = driver.run_after_bugbot(
186
+ state_file=state_file,
187
+ classification="clean",
188
+ current_head="abc123",
189
+ is_inline_lag=False,
190
+ )
191
+ assert exit_code == EXIT_SUCCESS
192
+ assert payload[RESULT_KEY_STATUS] == STATUS_OK
193
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
194
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
195
+ reloaded = driver.load_state(state_file)
196
+ assert reloaded["bugbot_clean_at"] == "abc123"
197
+ all_commands = payload["commands"]
198
+ assert isinstance(all_commands, list)
199
+ assert "check_convergence.py" in " ".join(
200
+ str(each_token) for each_token in all_commands
201
+ )
202
+
203
+
204
+ def test_after_bugbot_dirty_requests_fixes(state_file: Path) -> None:
205
+ payload, exit_code = driver.run_after_bugbot(
206
+ state_file=state_file,
207
+ classification="dirty",
208
+ current_head="abc123",
209
+ is_inline_lag=False,
210
+ )
211
+ assert exit_code == EXIT_SUCCESS
212
+ assert payload[RESULT_KEY_NEXT] == NEXT_APPLY_FIXES
213
+ assert payload[RESULT_KEY_PHASE] == PHASE_CODE_REVIEW
214
+ reloaded = driver.load_state(state_file)
215
+ assert reloaded["inline_lag_streak"] == 0
216
+
217
+
218
+ def test_copilot_wait_cap_blocks(state_file: Path) -> None:
219
+ state = driver.load_state(state_file)
220
+ state["copilot_down"] = False
221
+ state["copilot_wait_count"] = COPILOT_WAIT_HARD_CAP - 1
222
+ driver.save_state(state_file, state)
223
+ payload, exit_code = driver.run_after_copilot_wait(
224
+ state_file=state_file,
225
+ is_review_surfaced=False,
226
+ classification="absent",
227
+ current_head="abc123",
228
+ )
229
+ assert exit_code == EXIT_SUCCESS
230
+ assert payload[RESULT_KEY_NEXT] == NEXT_STOP_BLOCKED
231
+ assert payload[RESULT_KEY_BLOCKER] == BLOCKER_COPILOT_WAIT_CAP
232
+ assert payload[RESULT_KEY_PHASE] == PHASE_BLOCKED
233
+
234
+
235
+ def test_copilot_wait_poll_emits_delay(state_file: Path) -> None:
236
+ state = driver.load_state(state_file)
237
+ state["copilot_down"] = False
238
+ state["copilot_wait_count"] = 0
239
+ driver.save_state(state_file, state)
240
+ payload, exit_code = driver.run_after_copilot_wait(
241
+ state_file=state_file,
242
+ is_review_surfaced=False,
243
+ classification="absent",
244
+ current_head="abc123",
245
+ )
246
+ assert exit_code == EXIT_SUCCESS
247
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
248
+ assert payload[RESULT_KEY_WAIT_SECONDS] == DEFAULT_WAIT_SECONDS
249
+
250
+
251
+ def test_after_copilot_surfaced_clean_checks_ready(state_file: Path) -> None:
252
+ state = driver.load_state(state_file)
253
+ state["copilot_down"] = False
254
+ driver.save_state(state_file, state)
255
+ payload, exit_code = driver.run_after_copilot_wait(
256
+ state_file=state_file,
257
+ is_review_surfaced=True,
258
+ classification="clean",
259
+ current_head="abc123",
260
+ )
261
+ assert exit_code == EXIT_SUCCESS
262
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
263
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
264
+ reloaded = driver.load_state(state_file)
265
+ assert reloaded["copilot_clean_at"] == "abc123"
266
+
267
+
268
+ def test_after_copilot_surfaced_absent_keeps_waiting(state_file: Path) -> None:
269
+ state = driver.load_state(state_file)
270
+ state["copilot_down"] = False
271
+ state["copilot_wait_count"] = 0
272
+ driver.save_state(state_file, state)
273
+ payload, exit_code = driver.run_after_copilot_wait(
274
+ state_file=state_file,
275
+ is_review_surfaced=True,
276
+ classification="absent",
277
+ current_head="abc123",
278
+ )
279
+ assert exit_code == EXIT_SUCCESS
280
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
281
+ assert payload[RESULT_KEY_PHASE] == PHASE_COPILOT_WAIT
282
+ reloaded = driver.load_state(state_file)
283
+ assert reloaded["copilot_clean_at"] is None
284
+ assert reloaded["copilot_wait_count"] == 1
285
+
286
+
287
+ def test_after_ready_check_zero_marks_ready(state_file: Path) -> None:
288
+ payload, exit_code = driver.run_after_ready_check(
289
+ state_file=state_file,
290
+ check_exit=0,
291
+ current_head="abc123",
292
+ )
293
+ assert exit_code == EXIT_SUCCESS
294
+ assert payload[RESULT_KEY_NEXT] == NEXT_MARK_READY
295
+ all_commands = payload["commands"]
296
+ assert isinstance(all_commands, list)
297
+ assert all_commands[0] == "gh"
298
+ reloaded = driver.load_state(state_file)
299
+ assert reloaded[STATE_KEY_PENDING_NEXT] == NEXT_MARK_READY
300
+
301
+
302
+ def test_open_run_rejects_non_portable_pacer(tmp_path: Path) -> None:
303
+ payload, exit_code = driver.run_open_run(
304
+ entry_skill="autoconverge",
305
+ has_workflow=True,
306
+ has_schedule_wakeup=False,
307
+ owner="owner",
308
+ repo="repo",
309
+ pr_number=1,
310
+ cwd_path=tmp_path,
311
+ state_dir=tmp_path / "state",
312
+ session_model="third-party",
313
+ is_copilot_down=True,
314
+ is_bugbot_down=True,
315
+ )
316
+ assert exit_code == EXIT_CONTRACT_ERROR
317
+ assert payload[RESULT_KEY_STATUS] == STATUS_ERROR
318
+ assert payload[RESULT_KEY_BLOCKER] == BLOCKER_NOT_PORTABLE
319
+ assert payload[RESULT_KEY_PACER] == "workflow"
320
+
321
+
322
+ def test_open_run_portable_seeds_state_and_code_review_commands(
323
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
324
+ ) -> None:
325
+ monkeypatch.setattr(
326
+ driver, "_run_preflight", lambda **_kwargs: (None, EXIT_SUCCESS)
327
+ )
328
+ monkeypatch.setattr(
329
+ driver, "_read_git_head", lambda _cwd: ("deadbeef", None, EXIT_SUCCESS)
330
+ )
331
+ state_dir = tmp_path / "state"
332
+ payload, exit_code = driver.run_open_run(
333
+ entry_skill="autoconverge",
334
+ has_workflow=False,
335
+ has_schedule_wakeup=False,
336
+ owner="owner",
337
+ repo="repo",
338
+ pr_number=220,
339
+ cwd_path=tmp_path / "worktree",
340
+ state_dir=state_dir,
341
+ session_model="third-party",
342
+ is_copilot_down=False,
343
+ is_bugbot_down=False,
344
+ )
345
+ assert exit_code == EXIT_SUCCESS
346
+ assert payload[RESULT_KEY_STATUS] == STATUS_OK
347
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
348
+ assert payload[RESULT_KEY_PHASE] == PHASE_CODE_REVIEW
349
+ all_commands = payload["commands"]
350
+ assert isinstance(all_commands, list)
351
+ assert all_commands[0] == "python"
352
+ assert "tasks" in payload
353
+ reloaded = driver.load_state(state_dir / "pr-converge-state.json")
354
+ assert reloaded[STATE_KEY_PENDING_NEXT] == NEXT_RUN_CODE_REVIEW
355
+ assert reloaded["current_head"] == "deadbeef"
356
+
357
+
358
+ def test_main_after_code_review_cli(state_file: Path) -> None:
359
+ exit_code = driver.main(
360
+ [
361
+ "after-code-review",
362
+ "--state-file",
363
+ str(state_file),
364
+ "--returncode",
365
+ "0",
366
+ "--dirty-tree",
367
+ "0",
368
+ "--served-command",
369
+ "claude",
370
+ "--current-head",
371
+ "abc123",
372
+ ]
373
+ )
374
+ assert exit_code == EXIT_SUCCESS
375
+
376
+
377
+ def test_after_code_review_empty_served_command_is_successful(
378
+ state_file: Path,
379
+ ) -> None:
380
+ payload, exit_code = driver.run_after_code_review(
381
+ state_file=state_file,
382
+ returncode=0,
383
+ is_dirty_tree=False,
384
+ served_command="",
385
+ current_head="abc123",
386
+ )
387
+ assert exit_code == EXIT_SUCCESS
388
+ assert payload[RESULT_KEY_STATUS] == STATUS_OK
389
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_BUGTEAM
390
+ assert payload[RESULT_KEY_PHASE] == PHASE_BUGTEAM
391
+ reloaded = driver.load_state(state_file)
392
+ assert reloaded["code_review_clean_at"] == "abc123"
393
+
394
+
395
+ def test_after_code_review_in_session_token_is_successful(
396
+ state_file: Path,
397
+ ) -> None:
398
+ payload, exit_code = driver.run_after_code_review(
399
+ state_file=state_file,
400
+ returncode=0,
401
+ is_dirty_tree=False,
402
+ served_command=SERVED_COMMAND_IN_SESSION,
403
+ current_head="abc123",
404
+ )
405
+ assert exit_code == EXIT_SUCCESS
406
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_BUGTEAM
407
+
408
+
409
+ def test_after_bugbot_inline_lag_polls_under_cap(state_file: Path) -> None:
410
+ state = driver.load_state(state_file)
411
+ state["inline_lag_streak"] = 0
412
+ driver.save_state(state_file, state)
413
+ payload, exit_code = driver.run_after_bugbot(
414
+ state_file=state_file,
415
+ classification="dirty",
416
+ current_head="abc123",
417
+ is_inline_lag=True,
418
+ )
419
+ assert exit_code == EXIT_SUCCESS
420
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
421
+ assert payload[RESULT_KEY_PHASE] == PHASE_BUGBOT
422
+ assert payload[RESULT_KEY_WAIT_SECONDS] == BUGBOT_INLINE_LAG_WAIT_SECONDS
423
+ reloaded = driver.load_state(state_file)
424
+ assert reloaded["inline_lag_streak"] == 1
425
+ assert reloaded[STATE_KEY_PENDING_WAIT_SECONDS] == BUGBOT_INLINE_LAG_WAIT_SECONDS
426
+
427
+
428
+ def test_after_bugbot_inline_lag_cap_blocks(state_file: Path) -> None:
429
+ state = driver.load_state(state_file)
430
+ state["inline_lag_streak"] = INLINE_LAG_STREAK_CAP - 1
431
+ driver.save_state(state_file, state)
432
+ payload, exit_code = driver.run_after_bugbot(
433
+ state_file=state_file,
434
+ classification="dirty",
435
+ current_head="abc123",
436
+ is_inline_lag=True,
437
+ )
438
+ assert exit_code == EXIT_SUCCESS
439
+ assert payload[RESULT_KEY_NEXT] == NEXT_STOP_BLOCKED
440
+ assert payload[RESULT_KEY_BLOCKER] == BLOCKER_INLINE_LAG_CAP
441
+ assert payload[RESULT_KEY_PHASE] == PHASE_BLOCKED
442
+ reloaded = driver.load_state(state_file)
443
+ assert reloaded["inline_lag_streak"] == INLINE_LAG_STREAK_CAP
444
+
445
+
446
+ def test_after_copilot_clean_stamps_and_checks_ready(state_file: Path) -> None:
447
+ state = driver.load_state(state_file)
448
+ state["copilot_down"] = False
449
+ driver.save_state(state_file, state)
450
+ payload, exit_code = driver.run_after_copilot_wait(
451
+ state_file=state_file,
452
+ is_review_surfaced=True,
453
+ classification="clean",
454
+ current_head="abc123",
455
+ )
456
+ assert exit_code == EXIT_SUCCESS
457
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
458
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
459
+ reloaded = driver.load_state(state_file)
460
+ assert reloaded["copilot_clean_at"] == "abc123"
461
+
462
+
463
+ def test_after_copilot_absent_when_surfaced_polls_without_stamp(
464
+ state_file: Path,
465
+ ) -> None:
466
+ state = driver.load_state(state_file)
467
+ state["copilot_down"] = False
468
+ state["copilot_wait_count"] = 0
469
+ driver.save_state(state_file, state)
470
+ payload, exit_code = driver.run_after_copilot_wait(
471
+ state_file=state_file,
472
+ is_review_surfaced=True,
473
+ classification="absent",
474
+ current_head="abc123",
475
+ )
476
+ assert exit_code == EXIT_SUCCESS
477
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
478
+ reloaded = driver.load_state(state_file)
479
+ assert reloaded["copilot_clean_at"] is None
480
+ assert reloaded["copilot_wait_count"] == 1
481
+
482
+
483
+ def test_after_copilot_unknown_classification_rejects(
484
+ state_file: Path,
485
+ ) -> None:
486
+ state = driver.load_state(state_file)
487
+ state["copilot_down"] = False
488
+ driver.save_state(state_file, state)
489
+ payload, exit_code = driver.run_after_copilot_wait(
490
+ state_file=state_file,
491
+ is_review_surfaced=True,
492
+ classification="mystery",
493
+ current_head="abc123",
494
+ )
495
+ assert exit_code == EXIT_USAGE_ERROR
496
+ assert payload[RESULT_KEY_STATUS] == STATUS_ERROR
497
+ reloaded = driver.load_state(state_file)
498
+ assert reloaded["copilot_clean_at"] is None
499
+
500
+
501
+ def test_after_copilot_down_advances_without_clean_stamp(
502
+ state_file: Path,
503
+ ) -> None:
504
+ state = driver.load_state(state_file)
505
+ state["copilot_down"] = False
506
+ driver.save_state(state_file, state)
507
+ payload, exit_code = driver.run_after_copilot_wait(
508
+ state_file=state_file,
509
+ is_review_surfaced=True,
510
+ classification="down",
511
+ current_head="abc123",
512
+ )
513
+ assert exit_code == EXIT_SUCCESS
514
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
515
+ reloaded = driver.load_state(state_file)
516
+ assert reloaded["copilot_down"] is True
517
+ assert reloaded["copilot_clean_at"] is None
518
+
519
+
520
+ def test_after_bugbot_clean_when_codex_required_runs_codex(
521
+ state_file: Path,
522
+ ) -> None:
523
+ state = driver.load_state(state_file)
524
+ state[STATE_KEY_CODEX_DOWN] = False
525
+ state[STATE_KEY_CODEX_REQUIRED] = True
526
+ state["copilot_down"] = True
527
+ driver.save_state(state_file, state)
528
+ payload, exit_code = driver.run_after_bugbot(
529
+ state_file=state_file,
530
+ classification="clean",
531
+ current_head="abc123",
532
+ is_inline_lag=False,
533
+ )
534
+ assert exit_code == EXIT_SUCCESS
535
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODEX
536
+ assert payload[RESULT_KEY_PHASE] == PHASE_BUGBOT
537
+ reloaded = driver.load_state(state_file)
538
+ assert reloaded["bugbot_clean_at"] == "abc123"
539
+ assert reloaded[STATE_KEY_CODEX_CLEAN_AT] is None
540
+
541
+
542
+ def test_after_codex_clean_advances_to_check_ready(state_file: Path) -> None:
543
+ state = driver.load_state(state_file)
544
+ state[STATE_KEY_CODEX_DOWN] = False
545
+ state[STATE_KEY_CODEX_REQUIRED] = True
546
+ state["copilot_down"] = True
547
+ state["bugbot_down"] = True
548
+ driver.save_state(state_file, state)
549
+ payload, exit_code = driver.run_after_codex(
550
+ state_file=state_file,
551
+ classification="clean",
552
+ current_head="abc123",
553
+ )
554
+ assert exit_code == EXIT_SUCCESS
555
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
556
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
557
+ reloaded = driver.load_state(state_file)
558
+ assert reloaded[STATE_KEY_CODEX_CLEAN_AT] == "abc123"
559
+ all_commands = payload["commands"]
560
+ assert isinstance(all_commands, list)
561
+ joined = " ".join(str(each_token) for each_token in all_commands)
562
+ assert "--codex-clean-at" in joined
563
+ assert "abc123" in joined
564
+
565
+
566
+ def test_after_codex_down_waives_and_checks_ready(state_file: Path) -> None:
567
+ state = driver.load_state(state_file)
568
+ state[STATE_KEY_CODEX_DOWN] = False
569
+ state[STATE_KEY_CODEX_REQUIRED] = True
570
+ state["copilot_down"] = True
571
+ driver.save_state(state_file, state)
572
+ payload, exit_code = driver.run_after_codex(
573
+ state_file=state_file,
574
+ classification="down",
575
+ current_head="abc123",
576
+ )
577
+ assert exit_code == EXIT_SUCCESS
578
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
579
+ reloaded = driver.load_state(state_file)
580
+ assert reloaded[STATE_KEY_CODEX_DOWN] is True
581
+ assert reloaded[STATE_KEY_CODEX_CLEAN_AT] is None
582
+ all_commands = payload["commands"]
583
+ assert isinstance(all_commands, list)
584
+ assert "--codex-down" in " ".join(
585
+ str(each_token) for each_token in all_commands
586
+ )
587
+
588
+
589
+ def test_after_codex_dirty_requests_fixes(state_file: Path) -> None:
590
+ state = driver.load_state(state_file)
591
+ state[STATE_KEY_CODEX_REQUIRED] = True
592
+ driver.save_state(state_file, state)
593
+ payload, exit_code = driver.run_after_codex(
594
+ state_file=state_file,
595
+ classification="dirty",
596
+ current_head="abc123",
597
+ )
598
+ assert exit_code == EXIT_SUCCESS
599
+ assert payload[RESULT_KEY_NEXT] == NEXT_APPLY_FIXES
600
+ assert payload[RESULT_KEY_PHASE] == PHASE_CODE_REVIEW
601
+
602
+
603
+ def test_after_bugteam_when_codex_required_runs_codex_before_ready(
604
+ state_file: Path,
605
+ ) -> None:
606
+ state = driver.load_state(state_file)
607
+ state["bugbot_down"] = True
608
+ state["copilot_down"] = True
609
+ state[STATE_KEY_CODEX_DOWN] = False
610
+ state[STATE_KEY_CODEX_REQUIRED] = True
611
+ driver.save_state(state_file, state)
612
+ payload, exit_code = driver.run_after_bugteam(
613
+ state_file=state_file,
614
+ is_pushed=False,
615
+ is_converged=True,
616
+ current_head="abc123",
617
+ )
618
+ assert exit_code == EXIT_SUCCESS
619
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODEX
620
+ assert payload[RESULT_KEY_PHASE] == PHASE_BUGBOT
621
+
622
+
623
+ def test_after_bugteam_usage_probe_requires_codex_when_seed_false(
624
+ state_file: Path,
625
+ monkeypatch: pytest.MonkeyPatch,
626
+ ) -> None:
627
+ monkeypatch.setattr(
628
+ driver,
629
+ "probe_weekly_usage_requires_codex",
630
+ lambda: True,
631
+ )
632
+ state = driver.load_state(state_file)
633
+ state["bugbot_down"] = True
634
+ state["copilot_down"] = True
635
+ state[STATE_KEY_CODEX_DOWN] = False
636
+ state[STATE_KEY_CODEX_REQUIRED] = False
637
+ driver.save_state(state_file, state)
638
+ payload, exit_code = driver.run_after_bugteam(
639
+ state_file=state_file,
640
+ is_pushed=False,
641
+ is_converged=True,
642
+ current_head="abc123",
643
+ )
644
+ assert exit_code == EXIT_SUCCESS
645
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODEX
646
+ reloaded = driver.load_state(state_file)
647
+ assert reloaded[STATE_KEY_CODEX_REQUIRED] is True
648
+
649
+
650
+ def test_after_ready_check_fail_routes_to_codex_when_usage_requires(
651
+ state_file: Path,
652
+ monkeypatch: pytest.MonkeyPatch,
653
+ ) -> None:
654
+ monkeypatch.setattr(
655
+ driver,
656
+ "probe_weekly_usage_requires_codex",
657
+ lambda: True,
658
+ )
659
+ state = driver.load_state(state_file)
660
+ state["bugbot_down"] = True
661
+ state["copilot_down"] = True
662
+ state[STATE_KEY_CODEX_DOWN] = False
663
+ state[STATE_KEY_CODEX_REQUIRED] = False
664
+ state[STATE_KEY_CODEX_CLEAN_AT] = None
665
+ state["phase"] = PHASE_READY
666
+ driver.save_state(state_file, state)
667
+ payload, exit_code = driver.run_after_ready_check(
668
+ state_file=state_file,
669
+ check_exit=1,
670
+ current_head="abc123",
671
+ )
672
+ assert exit_code == EXIT_SUCCESS
673
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODEX
674
+ reloaded = driver.load_state(state_file)
675
+ assert reloaded[STATE_KEY_CODEX_REQUIRED] is True
676
+
677
+
678
+ def test_resolve_codex_required_flag_force_on_skips_probe(
679
+ monkeypatch: pytest.MonkeyPatch,
680
+ ) -> None:
681
+ monkeypatch.setattr(
682
+ driver,
683
+ "probe_weekly_usage_requires_codex",
684
+ lambda: False,
685
+ )
686
+ assert (
687
+ driver.resolve_codex_required_flag(
688
+ is_codex_down=False,
689
+ is_codex_required=True,
690
+ )
691
+ is True
692
+ )
693
+
694
+
695
+ def test_resolve_codex_required_flag_uses_probe_when_seed_false(
696
+ monkeypatch: pytest.MonkeyPatch,
697
+ ) -> None:
698
+ monkeypatch.setattr(
699
+ driver,
700
+ "probe_weekly_usage_requires_codex",
701
+ lambda: True,
702
+ )
703
+ assert (
704
+ driver.resolve_codex_required_flag(
705
+ is_codex_down=False,
706
+ is_codex_required=False,
707
+ )
708
+ is True
709
+ )
710
+
711
+
712
+ def test_resolve_codex_required_flag_down_never_requires(
713
+ monkeypatch: pytest.MonkeyPatch,
714
+ ) -> None:
715
+ monkeypatch.setattr(
716
+ driver,
717
+ "probe_weekly_usage_requires_codex",
718
+ lambda: True,
719
+ )
720
+ assert (
721
+ driver.resolve_codex_required_flag(
722
+ is_codex_down=True,
723
+ is_codex_required=True,
724
+ )
725
+ is False
726
+ )
727
+
728
+
729
+ def test_probe_weekly_usage_requires_codex_true_above_threshold(
730
+ monkeypatch: pytest.MonkeyPatch,
731
+ ) -> None:
732
+ monkeypatch.setattr(
733
+ driver,
734
+ "probe_weekly_usage_requires_codex",
735
+ _REAL_PROBE_WEEKLY_USAGE_REQUIRES_CODEX,
736
+ )
737
+ monkeypatch.setattr(
738
+ driver,
739
+ "probe_weekly_usage_via_subprocess",
740
+ lambda: {driver.USAGE_REPORT_KEY_PERCENT_LEFT: 50.0},
741
+ )
742
+ monkeypatch.setattr(
743
+ driver,
744
+ "is_codex_review_required",
745
+ lambda percent_left: percent_left is not None and percent_left > 10.0,
746
+ )
747
+ assert driver.probe_weekly_usage_requires_codex() is True
748
+
749
+
750
+ def test_probe_weekly_usage_requires_codex_false_when_unknown(
751
+ monkeypatch: pytest.MonkeyPatch,
752
+ ) -> None:
753
+ monkeypatch.setattr(
754
+ driver,
755
+ "probe_weekly_usage_requires_codex",
756
+ _REAL_PROBE_WEEKLY_USAGE_REQUIRES_CODEX,
757
+ )
758
+ monkeypatch.setattr(
759
+ driver,
760
+ "probe_weekly_usage_via_subprocess",
761
+ lambda: {driver.USAGE_REPORT_KEY_PERCENT_LEFT: None},
762
+ )
763
+ monkeypatch.setattr(
764
+ driver,
765
+ "is_codex_review_required",
766
+ lambda percent_left: False,
767
+ )
768
+ assert driver.probe_weekly_usage_requires_codex() is False
769
+
770
+
771
+ def test_probe_weekly_usage_requires_codex_false_on_probe_error(
772
+ monkeypatch: pytest.MonkeyPatch,
773
+ ) -> None:
774
+ monkeypatch.setattr(
775
+ driver,
776
+ "probe_weekly_usage_requires_codex",
777
+ _REAL_PROBE_WEEKLY_USAGE_REQUIRES_CODEX,
778
+ )
779
+
780
+ def _raise_probe_error() -> dict[str, object]:
781
+ raise OSError("codex CLI missing")
782
+
783
+ monkeypatch.setattr(
784
+ driver,
785
+ "probe_weekly_usage_via_subprocess",
786
+ _raise_probe_error,
787
+ )
788
+ assert driver.probe_weekly_usage_requires_codex() is False
789
+
790
+
791
+ def test_show_state_ready_with_pending_mark_ready(state_file: Path) -> None:
792
+ driver.run_after_ready_check(
793
+ state_file=state_file,
794
+ check_exit=0,
795
+ current_head="abc123",
796
+ )
797
+ exit_code = driver.main(
798
+ ["show-state", "--state-file", str(state_file)]
799
+ )
800
+ assert exit_code == EXIT_SUCCESS
801
+ reloaded = driver.load_state(state_file)
802
+ assert reloaded["phase"] == PHASE_READY
803
+ assert reloaded[STATE_KEY_PENDING_NEXT] == NEXT_MARK_READY
804
+ payload, show_exit = driver._dispatch_show_state( # noqa: SLF001
805
+ type(
806
+ "Namespace",
807
+ (),
808
+ {"state_file": str(state_file)},
809
+ )()
810
+ )
811
+ assert show_exit == EXIT_SUCCESS
812
+ assert payload[RESULT_KEY_NEXT] == NEXT_MARK_READY
813
+ all_commands = payload["commands"]
814
+ assert isinstance(all_commands, list)
815
+ assert all_commands[0] == "gh"
816
+ assert "ready" in all_commands
817
+
818
+
819
+ def test_show_state_ready_without_pending_defaults_to_mark_ready(
820
+ state_file: Path,
821
+ ) -> None:
822
+ state = driver.load_state(state_file)
823
+ state["phase"] = PHASE_READY
824
+ state.pop(STATE_KEY_PENDING_NEXT, None)
825
+ driver.save_state(state_file, state)
826
+ payload, exit_code = driver._dispatch_show_state( # noqa: SLF001
827
+ type(
828
+ "Namespace",
829
+ (),
830
+ {"state_file": str(state_file)},
831
+ )()
832
+ )
833
+ assert exit_code == EXIT_SUCCESS
834
+ assert payload[RESULT_KEY_NEXT] == NEXT_MARK_READY
835
+
836
+
837
+ def test_show_state_echoes_pending_check_ready(state_file: Path) -> None:
838
+ driver.run_after_bugteam(
839
+ state_file=state_file,
840
+ is_pushed=False,
841
+ is_converged=True,
842
+ current_head="abc123",
843
+ )
844
+ payload, exit_code = driver._dispatch_show_state( # noqa: SLF001
845
+ type(
846
+ "Namespace",
847
+ (),
848
+ {"state_file": str(state_file)},
849
+ )()
850
+ )
851
+ assert exit_code == EXIT_SUCCESS
852
+ assert payload[RESULT_KEY_NEXT] == NEXT_CHECK_READY
853
+ assert payload[RESULT_KEY_PHASE] == PHASE_READY
854
+ joined = _command_joined(payload)
855
+ assert "check_convergence.py" in joined
856
+
857
+
858
+ def _command_joined(payload: dict[str, object]) -> str:
859
+ all_commands = payload["commands"]
860
+ assert isinstance(all_commands, list)
861
+ return " ".join(str(each_token) for each_token in all_commands)
862
+
863
+
864
+ def test_after_code_review_failed_serve_emits_code_review_commands(
865
+ state_file: Path,
866
+ ) -> None:
867
+ payload, exit_code = driver.run_after_code_review(
868
+ state_file=state_file,
869
+ returncode=1,
870
+ is_dirty_tree=False,
871
+ served_command="",
872
+ current_head="abc123",
873
+ )
874
+ assert exit_code == EXIT_SUCCESS
875
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
876
+ joined = _command_joined(payload)
877
+ assert "invoke_code_review.py" in joined
878
+ assert "--session-model" in joined
879
+ assert "third-party" in joined
880
+ all_commands = payload["commands"]
881
+ assert isinstance(all_commands, list)
882
+ assert len(all_commands) > 0
883
+
884
+
885
+ def test_after_bugteam_pushed_emits_code_review_commands(
886
+ state_file: Path,
887
+ ) -> None:
888
+ payload, exit_code = driver.run_after_bugteam(
889
+ state_file=state_file,
890
+ is_pushed=True,
891
+ is_converged=False,
892
+ current_head="def456",
893
+ )
894
+ assert exit_code == EXIT_SUCCESS
895
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
896
+ joined = _command_joined(payload)
897
+ assert "invoke_code_review.py" in joined
898
+ assert "--session-model" in joined
899
+
900
+
901
+ def test_show_state_rehydrates_code_review_commands(state_file: Path) -> None:
902
+ state = driver.load_state(state_file)
903
+ state[STATE_KEY_PENDING_NEXT] = NEXT_RUN_CODE_REVIEW
904
+ state["phase"] = PHASE_CODE_REVIEW
905
+ driver.save_state(state_file, state)
906
+ payload, exit_code = driver._dispatch_show_state( # noqa: SLF001
907
+ type(
908
+ "Namespace",
909
+ (),
910
+ {"state_file": str(state_file)},
911
+ )()
912
+ )
913
+ assert exit_code == EXIT_SUCCESS
914
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_CODE_REVIEW
915
+ joined = _command_joined(payload)
916
+ assert "invoke_code_review.py" in joined
917
+ assert "--session-model" in joined
918
+ assert "third-party" in joined
919
+
920
+
921
+ def test_after_code_review_head_change_resets_push_markers(
922
+ state_file: Path,
923
+ ) -> None:
924
+ state = driver.load_state(state_file)
925
+ state["current_head"] = "abc123"
926
+ state["code_review_clean_at"] = "abc123"
927
+ state["bugbot_clean_at"] = "abc123"
928
+ state["bugteam_clean_at"] = "abc123"
929
+ state["copilot_clean_at"] = "abc123"
930
+ state[STATE_KEY_CODEX_CLEAN_AT] = "abc123"
931
+ state["bugbot_down"] = True
932
+ state[STATE_KEY_CODEX_DOWN] = True
933
+ state["inline_lag_streak"] = 2
934
+ driver.save_state(state_file, state)
935
+ payload, exit_code = driver.run_after_code_review(
936
+ state_file=state_file,
937
+ returncode=0,
938
+ is_dirty_tree=False,
939
+ served_command="claude.exe",
940
+ current_head="def456",
941
+ )
942
+ assert exit_code == EXIT_SUCCESS
943
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_BUGTEAM
944
+ reloaded = driver.load_state(state_file)
945
+ assert reloaded["code_review_clean_at"] == "def456"
946
+ assert reloaded["bugbot_clean_at"] is None
947
+ assert reloaded["bugteam_clean_at"] is None
948
+ assert reloaded["copilot_clean_at"] is None
949
+ assert reloaded[STATE_KEY_CODEX_CLEAN_AT] is None
950
+ assert reloaded["bugbot_down"] is False
951
+ assert reloaded[STATE_KEY_CODEX_DOWN] is False
952
+ assert reloaded["inline_lag_streak"] == 0
953
+ assert reloaded["current_head"] == "def456"
954
+
955
+
956
+ def test_copilot_surfaced_absent_preserves_wait_count_to_cap(
957
+ state_file: Path,
958
+ ) -> None:
959
+ state = driver.load_state(state_file)
960
+ state["copilot_down"] = False
961
+ state["copilot_wait_count"] = COPILOT_WAIT_HARD_CAP - 1
962
+ driver.save_state(state_file, state)
963
+ payload, exit_code = driver.run_after_copilot_wait(
964
+ state_file=state_file,
965
+ is_review_surfaced=True,
966
+ classification="absent",
967
+ current_head="abc123",
968
+ )
969
+ assert exit_code == EXIT_SUCCESS
970
+ assert payload[RESULT_KEY_NEXT] == NEXT_STOP_BLOCKED
971
+ assert payload[RESULT_KEY_BLOCKER] == BLOCKER_COPILOT_WAIT_CAP
972
+ reloaded = driver.load_state(state_file)
973
+ assert reloaded["copilot_wait_count"] == COPILOT_WAIT_HARD_CAP
974
+
975
+
976
+ def test_copilot_surfaced_absent_increments_from_two(state_file: Path) -> None:
977
+ state = driver.load_state(state_file)
978
+ state["copilot_down"] = False
979
+ state["copilot_wait_count"] = 2
980
+ driver.save_state(state_file, state)
981
+ payload, exit_code = driver.run_after_copilot_wait(
982
+ state_file=state_file,
983
+ is_review_surfaced=True,
984
+ classification="absent",
985
+ current_head="abc123",
986
+ )
987
+ assert exit_code == EXIT_SUCCESS
988
+ reloaded = driver.load_state(state_file)
989
+ assert reloaded["copilot_wait_count"] == 3
990
+ if COPILOT_WAIT_HARD_CAP <= 3:
991
+ assert payload[RESULT_KEY_NEXT] == NEXT_STOP_BLOCKED
992
+ else:
993
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
994
+
995
+ def test_show_state_rehydrates_poll_wait_seconds(state_file: Path) -> None:
996
+ state = driver.load_state(state_file)
997
+ state[STATE_KEY_PENDING_NEXT] = NEXT_POLL_WAIT
998
+ state["phase"] = PHASE_COPILOT_WAIT
999
+ driver.save_state(state_file, state)
1000
+ payload, exit_code = driver._dispatch_show_state( # noqa: SLF001
1001
+ type(
1002
+ "Namespace",
1003
+ (),
1004
+ {"state_file": str(state_file)},
1005
+ )()
1006
+ )
1007
+ assert exit_code == EXIT_SUCCESS
1008
+ assert payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
1009
+ assert payload[RESULT_KEY_WAIT_SECONDS] == DEFAULT_WAIT_SECONDS
1010
+
1011
+
1012
+ def test_after_bugbot_inline_lag_show_state_preserves_wait_seconds(
1013
+ state_file: Path,
1014
+ ) -> None:
1015
+ state = driver.load_state(state_file)
1016
+ state["inline_lag_streak"] = 0
1017
+ driver.save_state(state_file, state)
1018
+ after_payload, after_exit = driver.run_after_bugbot(
1019
+ state_file=state_file,
1020
+ classification="dirty",
1021
+ current_head="abc123",
1022
+ is_inline_lag=True,
1023
+ )
1024
+ assert after_exit == EXIT_SUCCESS
1025
+ assert after_payload[RESULT_KEY_WAIT_SECONDS] == BUGBOT_INLINE_LAG_WAIT_SECONDS
1026
+ show_payload, show_exit = driver._dispatch_show_state( # noqa: SLF001
1027
+ type(
1028
+ "Namespace",
1029
+ (),
1030
+ {"state_file": str(state_file)},
1031
+ )()
1032
+ )
1033
+ assert show_exit == EXIT_SUCCESS
1034
+ assert show_payload[RESULT_KEY_NEXT] == NEXT_POLL_WAIT
1035
+ assert show_payload[RESULT_KEY_WAIT_SECONDS] == BUGBOT_INLINE_LAG_WAIT_SECONDS
1036
+
1037
+
1038
+ def test_after_bugteam_head_change_resets_down_flags(state_file: Path) -> None:
1039
+ state = driver.load_state(state_file)
1040
+ state["current_head"] = "abc123"
1041
+ state["code_review_clean_at"] = "abc123"
1042
+ state["bugbot_down"] = True
1043
+ state[STATE_KEY_CODEX_DOWN] = True
1044
+ driver.save_state(state_file, state)
1045
+ payload, exit_code = driver.run_after_bugteam(
1046
+ state_file=state_file,
1047
+ is_pushed=False,
1048
+ is_converged=True,
1049
+ current_head="def456",
1050
+ )
1051
+ assert exit_code == EXIT_SUCCESS
1052
+ reloaded = driver.load_state(state_file)
1053
+ assert reloaded["bugbot_down"] is False
1054
+ assert reloaded[STATE_KEY_CODEX_DOWN] is False
1055
+ assert reloaded["code_review_clean_at"] is None
1056
+ assert reloaded["current_head"] == "def456"
1057
+ assert payload[RESULT_KEY_NEXT] == NEXT_RUN_BUGBOT_GATE
1058
+
1059
+
1060
+ def test_after_ready_check_head_change_resets_markers(state_file: Path) -> None:
1061
+ state = driver.load_state(state_file)
1062
+ state["current_head"] = "abc123"
1063
+ state["code_review_clean_at"] = "abc123"
1064
+ state["bugbot_down"] = True
1065
+ state[STATE_KEY_CODEX_DOWN] = True
1066
+ driver.save_state(state_file, state)
1067
+ payload, exit_code = driver.run_after_ready_check(
1068
+ state_file=state_file,
1069
+ check_exit=0,
1070
+ current_head="def456",
1071
+ )
1072
+ assert exit_code == EXIT_SUCCESS
1073
+ reloaded = driver.load_state(state_file)
1074
+ assert reloaded["bugbot_down"] is False
1075
+ assert reloaded[STATE_KEY_CODEX_DOWN] is False
1076
+ assert reloaded["code_review_clean_at"] is None
1077
+ assert reloaded["current_head"] == "def456"
1078
+ assert payload[RESULT_KEY_NEXT] == NEXT_MARK_READY
1079
+
1080
+
1081
+ def test_after_codex_dirty_resets_down_flags(state_file: Path) -> None:
1082
+ state = driver.load_state(state_file)
1083
+ state[STATE_KEY_CODEX_REQUIRED] = True
1084
+ state["bugbot_down"] = True
1085
+ state[STATE_KEY_CODEX_DOWN] = True
1086
+ state["code_review_clean_at"] = "abc123"
1087
+ driver.save_state(state_file, state)
1088
+ payload, exit_code = driver.run_after_codex(
1089
+ state_file=state_file,
1090
+ classification="dirty",
1091
+ current_head="abc123",
1092
+ )
1093
+ assert exit_code == EXIT_SUCCESS
1094
+ assert payload[RESULT_KEY_NEXT] == NEXT_APPLY_FIXES
1095
+ reloaded = driver.load_state(state_file)
1096
+ assert reloaded["bugbot_down"] is False
1097
+ assert reloaded[STATE_KEY_CODEX_DOWN] is False
1098
+ assert reloaded["code_review_clean_at"] is None
1099
+
1100
+
1101
+ def test_after_bugbot_down_rebuilds_task_list(state_file: Path) -> None:
1102
+ state = driver.load_state(state_file)
1103
+ state["bugbot_down"] = False
1104
+ state["tasks"] = []
1105
+ state["runnable_review_ids"] = ["code_review"]
1106
+ driver.save_state(state_file, state)
1107
+ payload, exit_code = driver.run_after_bugbot(
1108
+ state_file=state_file,
1109
+ classification="down",
1110
+ current_head="abc123",
1111
+ is_inline_lag=False,
1112
+ )
1113
+ assert exit_code == EXIT_SUCCESS
1114
+ reloaded = driver.load_state(state_file)
1115
+ assert reloaded["bugbot_down"] is True
1116
+ all_runnable = reloaded.get("runnable_review_ids")
1117
+ assert isinstance(all_runnable, list)
1118
+ assert "bugbot" not in all_runnable
1119
+