claude-dev-env 2.1.0 → 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.
- package/package.json +1 -1
- package/skills/_shared/pr-loop/CLAUDE.md +18 -13
- package/skills/_shared/pr-loop/portable-driver.md +150 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/build_converge_task_list.py +310 -0
- package/skills/_shared/pr-loop/scripts/portable_converge_driver.py +1637 -0
- package/skills/_shared/pr-loop/scripts/select_converge_pacer.py +215 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +3 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +56 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +47 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +181 -0
- package/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +140 -0
- package/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +1119 -0
- package/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +207 -0
- package/skills/autoconverge/CLAUDE.md +5 -3
- package/skills/autoconverge/SKILL.md +185 -74
- package/skills/autoconverge/reference/convergence.md +5 -5
- package/skills/autoconverge/reference/multi-pr.md +33 -2
- package/skills/autoconverge/reference/stop-conditions.md +9 -6
- package/skills/autoconverge/test_portable_pacer_gate.py +54 -0
- package/skills/copilot-finding-triage/SKILL.md +16 -4
- package/skills/pr-converge/CLAUDE.md +4 -3
- package/skills/pr-converge/SKILL.md +236 -44
- package/skills/pr-converge/reference/multi-pr-orchestration.md +5 -1
- package/skills/pr-converge/reference/per-tick.md +51 -24
- package/skills/pr-converge/test_portable_pacer_gate.py +67 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +5 -3
|
@@ -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
|
+
|