claude-dev-env 8.54.7 → 8.54.9

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.
@@ -5,7 +5,7 @@
5
5
  title "docs: ..." -> pass, nothing to prove
6
6
  title "fix: ..." changing only docs or .github -> pass, nothing to prove
7
7
  title "fix: ..." with no changed Python test -> fail
8
- title "fix: ..." with no changed Python or Node test -> fail
8
+ title "fix: ..." with no changed Python, Node, or plugin test -> fail
9
9
  changed test passes on head, fails on base -> pass
10
10
  changed test passes on both -> fail
11
11
  any changed test fails on head -> fail
@@ -14,8 +14,10 @@ Python tests reuse the regression gate's detached worktree: the changed test
14
14
  files are copied onto the base checkout, which runs them with import isolation.
15
15
  Node tests (``*.test.mjs``, ``*.test.js``, ``*.test.cjs``) run through
16
16
  ``node --test`` on the head, then on a detached base checkout that holds a copy
17
- of the changed test files. The fix is proven when at least one language's
18
- changed tests fail on the base.
17
+ of the changed test files. Plugin tests (``*.test.ts``, ``*.test.tsx`` under a
18
+ directory that holds ``.claude-plugin/``) run through ``claude plugin test`` in
19
+ each plugin root on the head, then on the same kind of detached base checkout.
20
+ The fix is proven when at least one language's changed tests fail on the base.
19
21
  """
20
22
 
21
23
  import argparse
@@ -23,7 +25,9 @@ import shutil
23
25
  import subprocess
24
26
  import sys
25
27
  import tempfile
28
+ from collections.abc import Callable
26
29
  from enum import Enum
30
+ from functools import partial
27
31
  from pathlib import Path
28
32
 
29
33
  from code_rules_gate_parts import staged_test_regression, staged_test_running
@@ -32,6 +36,8 @@ from code_rules_gate_parts.wrapper_plumb_check import is_test_path
32
36
  from pr_loop_shared_constants.fix_pr_test_proof_constants import (
33
37
  ALL_NODE_TEST_COMMAND,
34
38
  ALL_NODE_TEST_SUFFIXES,
39
+ ALL_PLUGIN_TEST_COMMAND,
40
+ ALL_PLUGIN_TEST_SUFFIXES,
35
41
  ALL_PRODUCTION_CODE_SUFFIXES,
36
42
  CI_CONFIG_DIRECTORY_NAME,
37
43
  FAILED_EXIT_CODE,
@@ -44,6 +50,7 @@ from pr_loop_shared_constants.fix_pr_test_proof_constants import (
44
50
  NODE_BASE_WORKTREE_TEMP_DIRECTORY_PREFIX,
45
51
  NOT_A_FIX_MESSAGE,
46
52
  PASSED_EXIT_CODE,
53
+ PLUGIN_MANIFEST_DIRECTORY_NAME,
47
54
  PROVEN_MESSAGE,
48
55
  WORKTREE_FAILED_MESSAGE,
49
56
  )
@@ -102,6 +109,25 @@ def _changed_node_tests(all_changed_paths: list[Path]) -> list[Path]:
102
109
  ]
103
110
 
104
111
 
112
+ def _plugin_root(test_path: Path, repository_root: Path) -> Path | None:
113
+ for each_directory in test_path.parents:
114
+ if (each_directory / PLUGIN_MANIFEST_DIRECTORY_NAME).is_dir():
115
+ return each_directory
116
+ if each_directory == repository_root:
117
+ return None
118
+ return None
119
+
120
+
121
+ def _changed_plugin_tests(all_changed_paths: list[Path], repository_root: Path) -> list[Path]:
122
+ return [
123
+ each_path
124
+ for each_path in all_changed_paths
125
+ if each_path.name.endswith(ALL_PLUGIN_TEST_SUFFIXES)
126
+ and each_path.is_file()
127
+ and _plugin_root(each_path, repository_root) is not None
128
+ ]
129
+
130
+
105
131
  def _python_proof_verdict(
106
132
  all_changed_tests: list[Path], repository_root: Path, base_revision: str
107
133
  ) -> ProofVerdict:
@@ -130,7 +156,7 @@ def _python_proof_verdict(
130
156
  return ProofVerdict.PROVEN
131
157
 
132
158
 
133
- def _node_tests_pass(working_directory: Path, all_relative_tests: list[Path]) -> bool:
159
+ def _node_tests_pass(all_relative_tests: list[Path], working_directory: Path) -> bool:
134
160
  node_run = subprocess.run(
135
161
  [*ALL_NODE_TEST_COMMAND, *(each_path.as_posix() for each_path in all_relative_tests)],
136
162
  cwd=working_directory,
@@ -141,6 +167,20 @@ def _node_tests_pass(working_directory: Path, all_relative_tests: list[Path]) ->
141
167
  return node_run.returncode == 0
142
168
 
143
169
 
170
+ def _plugin_tests_pass(all_relative_plugin_roots: list[Path], working_directory: Path) -> bool:
171
+ return all(
172
+ subprocess.run(
173
+ ALL_PLUGIN_TEST_COMMAND,
174
+ cwd=working_directory / each_relative_root,
175
+ capture_output=True,
176
+ text=True,
177
+ check=False,
178
+ ).returncode
179
+ == 0
180
+ for each_relative_root in all_relative_plugin_roots
181
+ )
182
+
183
+
144
184
  def _copy_tests_onto(
145
185
  base_worktree: Path, repository_root: Path, all_relative_tests: list[Path]
146
186
  ) -> None:
@@ -150,11 +190,14 @@ def _copy_tests_onto(
150
190
  shutil.copy2(repository_root / each_relative_test, base_test_path)
151
191
 
152
192
 
153
- def _node_proof_verdict(
154
- all_changed_tests: list[Path], repository_root: Path, base_revision: str
193
+ def _head_and_base_proof_verdict(
194
+ all_changed_tests: list[Path],
195
+ repository_root: Path,
196
+ base_revision: str,
197
+ tests_pass: Callable[[Path], bool],
155
198
  ) -> ProofVerdict:
156
199
  all_relative_tests = [each_path.relative_to(repository_root) for each_path in all_changed_tests]
157
- if not _node_tests_pass(repository_root, all_relative_tests):
200
+ if not tests_pass(repository_root):
158
201
  sys.stderr.write(HEAD_FAILURE_MESSAGE.format(group_root=repository_root) + "\n")
159
202
  return ProofVerdict.HEAD_FAILED
160
203
  with tempfile.TemporaryDirectory(
@@ -168,7 +211,7 @@ def _node_proof_verdict(
168
211
  return ProofVerdict.BASE_UNAVAILABLE
169
212
  try:
170
213
  _copy_tests_onto(base_worktree, repository_root, all_relative_tests)
171
- passes_on_base = _node_tests_pass(base_worktree, all_relative_tests)
214
+ passes_on_base = tests_pass(base_worktree)
172
215
  finally:
173
216
  staged_test_regression._remove_baseline_worktree(repository_root, base_worktree)
174
217
  if passes_on_base:
@@ -177,6 +220,36 @@ def _node_proof_verdict(
177
220
  return ProofVerdict.PROVEN
178
221
 
179
222
 
223
+ def _node_proof_verdict(
224
+ all_changed_tests: list[Path], repository_root: Path, base_revision: str
225
+ ) -> ProofVerdict:
226
+ all_relative_tests = [each_path.relative_to(repository_root) for each_path in all_changed_tests]
227
+ return _head_and_base_proof_verdict(
228
+ all_changed_tests,
229
+ repository_root,
230
+ base_revision,
231
+ partial(_node_tests_pass, all_relative_tests),
232
+ )
233
+
234
+
235
+ def _plugin_proof_verdict(
236
+ all_changed_tests: list[Path], repository_root: Path, base_revision: str
237
+ ) -> ProofVerdict:
238
+ all_relative_plugin_roots = sorted(
239
+ {
240
+ each_root.relative_to(repository_root)
241
+ for each_path in all_changed_tests
242
+ if (each_root := _plugin_root(each_path, repository_root)) is not None
243
+ }
244
+ )
245
+ return _head_and_base_proof_verdict(
246
+ all_changed_tests,
247
+ repository_root,
248
+ base_revision,
249
+ partial(_plugin_tests_pass, all_relative_plugin_roots),
250
+ )
251
+
252
+
180
253
  def _proof_exit_code(
181
254
  all_verdicts: list[ProofVerdict], base_revision: str
182
255
  ) -> int:
@@ -213,7 +286,8 @@ def main(all_arguments: list[str]) -> int:
213
286
  return PASSED_EXIT_CODE
214
287
  all_changed_python_tests = _changed_python_tests(all_changed_paths)
215
288
  all_changed_node_tests = _changed_node_tests(all_changed_paths)
216
- if not all_changed_python_tests and not all_changed_node_tests:
289
+ all_changed_plugin_tests = _changed_plugin_tests(all_changed_paths, repository_root)
290
+ if not (all_changed_python_tests or all_changed_node_tests or all_changed_plugin_tests):
217
291
  sys.stderr.write(NO_CHANGED_TEST_MESSAGE + "\n")
218
292
  return FAILED_EXIT_CODE
219
293
  base_revision = parsed_arguments.base_revision
@@ -226,6 +300,10 @@ def main(all_arguments: list[str]) -> int:
226
300
  all_verdicts.append(
227
301
  _node_proof_verdict(all_changed_node_tests, repository_root, base_revision)
228
302
  )
303
+ if all_changed_plugin_tests:
304
+ all_verdicts.append(
305
+ _plugin_proof_verdict(all_changed_plugin_tests, repository_root, base_revision)
306
+ )
229
307
  return _proof_exit_code(all_verdicts, base_revision)
230
308
 
231
309
 
@@ -17,6 +17,9 @@ ALL_PRODUCTION_CODE_SUFFIXES: tuple[str, ...] = (
17
17
  )
18
18
  ALL_NODE_TEST_SUFFIXES: tuple[str, ...] = (".test.mjs", ".test.js", ".test.cjs")
19
19
  ALL_NODE_TEST_COMMAND: tuple[str, ...] = ("node", "--test")
20
+ ALL_PLUGIN_TEST_SUFFIXES: tuple[str, ...] = (".test.ts", ".test.tsx")
21
+ ALL_PLUGIN_TEST_COMMAND: tuple[str, ...] = ("claude", "plugin", "test", ".")
22
+ PLUGIN_MANIFEST_DIRECTORY_NAME: str = ".claude-plugin"
20
23
  NODE_BASE_WORKTREE_TEMP_DIRECTORY_PREFIX: str = "fix_pr_test_proof_node_base_"
21
24
  NODE_BASE_WORKTREE_DIRECTORY_NAME: str = "tree"
22
25
  CI_CONFIG_DIRECTORY_NAME: str = ".github"
@@ -28,7 +31,7 @@ NO_PRODUCTION_CHANGE_MESSAGE: str = (
28
31
  "fix_pr_test_proof: the fix changes no production code; nothing to prove."
29
32
  )
30
33
  NO_CHANGED_TEST_MESSAGE: str = (
31
- "fix_pr_test_proof: this fix changes production code and no Python or Node test. "
34
+ "fix_pr_test_proof: this fix changes production code and no Python, Node, or plugin test. "
32
35
  "Add a test that fails on the base and passes on the head."
33
36
  )
34
37
  WORKTREE_FAILED_MESSAGE: str = (
@@ -1,7 +1,7 @@
1
1
  """Behavioral tests for the fix pull request test-proof check.
2
2
 
3
- Every case drives a git repository under ``tmp_path`` and a pytest or
4
- ``node --test`` subprocess, the same shape the CI job runs.
3
+ Every case drives a git repository under ``tmp_path`` and a pytest,
4
+ ``node --test``, or ``claude plugin test`` subprocess, the same shape the CI job runs.
5
5
  """
6
6
 
7
7
  from pathlib import Path
@@ -39,6 +39,25 @@ NODE_ALWAYS_PASSING_TEST_TEXT = (
39
39
  "import { test } from 'node:test';\n\n"
40
40
  "test('nothing in particular', () => {});\n"
41
41
  )
42
+ PLUGIN_MANIFEST_TEXT = '{"name": "calc"}\n'
43
+ PLUGIN_HOOKS_TEXT = '{ "modules": ["./index.ts"] }\n'
44
+ BUGGY_PLUGIN_PRODUCTION_TEXT = (
45
+ "export function add(left: number, right: number): number {\n"
46
+ " return left - right\n}\n"
47
+ )
48
+ FIXED_PLUGIN_PRODUCTION_TEXT = (
49
+ "export function add(left: number, right: number): number {\n"
50
+ " return left + right\n}\n"
51
+ )
52
+ PLUGIN_PROOF_TEST_TEXT = (
53
+ "import { test, expect } from 'claude-code/testing'\n"
54
+ "import { add } from '../hooks/index'\n\n"
55
+ "test('add sums both operands', () => {\n expect(add(1, 2)).toBe(3)\n})\n"
56
+ )
57
+ PLUGIN_ALWAYS_PASSING_TEST_TEXT = (
58
+ "import { test, expect } from 'claude-code/testing'\n\n"
59
+ "test('nothing in particular', () => {\n expect(1).toBe(1)\n})\n"
60
+ )
42
61
 
43
62
 
44
63
  def _commit_files(repository_root: Path, all_file_texts: dict[str, str]) -> None:
@@ -66,6 +85,19 @@ def _repository_at_buggy_node_base(tmp_path: Path) -> tuple[Path, str]:
66
85
  return repository_root, _head_revision(repository_root)
67
86
 
68
87
 
88
+ def _repository_at_buggy_plugin_base(tmp_path: Path) -> tuple[Path, str]:
89
+ repository_root = repository_with_root_pytest_config(tmp_path)
90
+ _commit_files(
91
+ repository_root,
92
+ {
93
+ "plugins/calc/.claude-plugin/plugin.json": PLUGIN_MANIFEST_TEXT,
94
+ "plugins/calc/hooks/hooks.json": PLUGIN_HOOKS_TEXT,
95
+ "plugins/calc/hooks/index.ts": BUGGY_PLUGIN_PRODUCTION_TEXT,
96
+ },
97
+ )
98
+ return repository_root, _head_revision(repository_root)
99
+
100
+
69
101
  def _run_check(repository_root: Path, title: str, base_revision: str) -> int:
70
102
  return fix_pr_test_proof.main(
71
103
  [
@@ -100,7 +132,7 @@ def test_a_fix_with_no_changed_test_fails(
100
132
  _commit_files(repository_root, {"pkg/calc.py": FIXED_PRODUCTION_TEXT})
101
133
 
102
134
  assert _run_check(repository_root, "fix: sum operands", base_revision) == 1
103
- assert "no Python or Node test" in capsys.readouterr().err
135
+ assert "no Python, Node, or plugin test" in capsys.readouterr().err
104
136
 
105
137
 
106
138
  def test_a_fix_whose_test_fails_on_base_and_passes_on_head_passes(
@@ -186,6 +218,64 @@ def test_a_node_fix_whose_changed_test_fails_on_head_fails(
186
218
  assert "fails on the head" in capsys.readouterr().err
187
219
 
188
220
 
221
+ def test_a_plugin_fix_with_no_changed_test_fails(
222
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
223
+ ) -> None:
224
+ repository_root, base_revision = _repository_at_buggy_plugin_base(tmp_path)
225
+ _commit_files(repository_root, {"plugins/calc/hooks/index.ts": FIXED_PLUGIN_PRODUCTION_TEXT})
226
+
227
+ assert _run_check(repository_root, "fix(calc): sum operands", base_revision) == 1
228
+ assert "no Python, Node, or plugin test" in capsys.readouterr().err
229
+
230
+
231
+ def test_a_plugin_fix_whose_test_fails_on_base_and_passes_on_head_passes(
232
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
233
+ ) -> None:
234
+ repository_root, base_revision = _repository_at_buggy_plugin_base(tmp_path)
235
+ _commit_files(
236
+ repository_root,
237
+ {
238
+ "plugins/calc/hooks/index.ts": FIXED_PLUGIN_PRODUCTION_TEXT,
239
+ "plugins/calc/tests/calc.test.ts": PLUGIN_PROOF_TEST_TEXT,
240
+ },
241
+ )
242
+
243
+ assert _run_check(repository_root, "fix(calc): sum operands", base_revision) == 0
244
+ assert "fail on the base and pass on the head" in capsys.readouterr().out
245
+
246
+
247
+ def test_a_plugin_fix_whose_changed_test_also_passes_on_base_fails(
248
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
249
+ ) -> None:
250
+ repository_root, base_revision = _repository_at_buggy_plugin_base(tmp_path)
251
+ _commit_files(
252
+ repository_root,
253
+ {
254
+ "plugins/calc/hooks/index.ts": FIXED_PLUGIN_PRODUCTION_TEXT,
255
+ "plugins/calc/tests/calc.test.ts": PLUGIN_ALWAYS_PASSING_TEST_TEXT,
256
+ },
257
+ )
258
+
259
+ assert _run_check(repository_root, "fix(calc): sum operands", base_revision) == 1
260
+ assert "passes on the base" in capsys.readouterr().err
261
+
262
+
263
+ def test_a_plugin_fix_whose_changed_test_fails_on_head_fails(
264
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
265
+ ) -> None:
266
+ repository_root, base_revision = _repository_at_buggy_plugin_base(tmp_path)
267
+ _commit_files(
268
+ repository_root,
269
+ {
270
+ "plugins/calc/tests/calc.test.ts": PLUGIN_PROOF_TEST_TEXT,
271
+ "plugins/calc/hooks/extra.ts": "export const VALUE = 1\n",
272
+ },
273
+ )
274
+
275
+ assert _run_check(repository_root, "fix(calc): sum operands", base_revision) == 1
276
+ assert "fails on the head" in capsys.readouterr().err
277
+
278
+
189
279
  BUGGY_GREETING_TEXT = 'def greet() -> str:\n return "helo"\n'
190
280
  FIXED_GREETING_TEXT = 'def greet() -> str:\n return "hello"\n'
191
281
  GREETING_TEST_TEXT = (
@@ -222,7 +312,7 @@ def test_a_fix_without_its_own_test_fails_when_the_base_branch_moved_with_a_test
222
312
  )
223
313
 
224
314
  assert _run_check(repository_root, "fix: sum operands", MERGE_REF_FIRST_PARENT) == 1
225
- assert "no Python or Node test" in capsys.readouterr().err
315
+ assert "no Python, Node, or plugin test" in capsys.readouterr().err
226
316
 
227
317
 
228
318
  def test_a_fix_on_a_moved_base_branch_judges_only_its_own_test(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "8.54.7",
3
+ "version": "8.54.9",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -48,6 +48,7 @@ from dev_env_scripts_constants.account_broker_constants import (
48
48
  ALL_PARENT_CLAUDE_SESSION_VARIABLES,
49
49
  COMMAND_MISSING_EXIT_CODE,
50
50
  REPORT_INDENT_SPACES,
51
+ TIMEOUT_EXIT_CODE,
51
52
  WAIT_EXIT_CODE,
52
53
  utc_time_text as _time_text,
53
54
  )
@@ -287,13 +288,16 @@ def _attempt_once(context: _RunContext, decision: Decision) -> JobOutcome | None
287
288
  account = decision.account
288
289
  try:
289
290
  completion = _invoke(context, account)
290
- except (OSError, subprocess.TimeoutExpired) as error:
291
- status = "timeout" if isinstance(error, subprocess.TimeoutExpired) else "start_failed"
292
- _record_spent_attempt(context, account, status, COMMAND_MISSING_EXIT_CODE)
291
+ except subprocess.TimeoutExpired as error:
292
+ _record_spent_attempt(context, account, "timeout", TIMEOUT_EXIT_CODE)
293
+ context.report.final_decision = decision
294
+ return JobOutcome(TIMEOUT_EXIT_CODE, "", str(error), account.name, tuple(context.all_attempts), "timeout", None, None)
295
+ except OSError as error:
296
+ _record_spent_attempt(context, account, "start_failed", COMMAND_MISSING_EXIT_CODE)
293
297
  context.start_failure_text = str(error)
294
- if isinstance(error, subprocess.TimeoutExpired) or not context.all_readings:
298
+ if not context.all_readings:
295
299
  context.report.final_decision = decision
296
- final_status = "advisor_blocked" if context.product is Product.CLAUDE else status
300
+ final_status = "advisor_blocked" if context.product is Product.CLAUDE else "start_failed"
297
301
  return JobOutcome(COMMAND_MISSING_EXIT_CODE, "", str(error), account.name, tuple(context.all_attempts), final_status, None, None)
298
302
  return None
299
303
  combined = f"{completion.stdout}{completion.stderr}".casefold()
@@ -27,8 +27,6 @@ from dev_env_scripts_constants.claude_account_worker_constants import (
27
27
  PROMPT_FILE_FLAG,
28
28
  REPORT_FILE_FLAG,
29
29
  SINGLE_PROMPT_FLAG,
30
- TIMEOUT_ATTEMPT_STATUS,
31
- TIMEOUT_EXIT_CODE,
32
30
  TIMEOUT_MINUTES_FLAG,
33
31
  UTF8_ENCODING,
34
32
  )
@@ -104,11 +102,10 @@ def run_worker(
104
102
  if outcome.status in {"wait", "exhausted"}:
105
103
  report = wait_report("wait", _wait_reason(outcome), outcome.wait_reset_at)
106
104
  return finalize_report(report_file, report)
107
- is_timed_out = bool(outcome.attempts) and outcome.attempts[-1][1] == TIMEOUT_ATTEMPT_STATUS
108
105
  report = make_report(
109
106
  outcome.account_name or "none",
110
- TIMEOUT_ATTEMPT_STATUS if is_timed_out else outcome.status,
111
- exit_code=TIMEOUT_EXIT_CODE if is_timed_out else outcome.returncode,
107
+ outcome.status,
108
+ exit_code=outcome.returncode,
112
109
  duration_seconds=duration_seconds,
113
110
  stdout_text=outcome.stdout,
114
111
  )
@@ -50,6 +50,7 @@ ALL_CODEX_FLOORS = {
50
50
 
51
51
  WAIT_EXIT_CODE = 3
52
52
  COMMAND_MISSING_EXIT_CODE = 127
53
+ TIMEOUT_EXIT_CODE = 124
53
54
  REPORT_INDENT_SPACES = 2
54
55
  BROKER_STATE_DIRECTORY_NAME = "account-broker"
55
56
  BROKER_STATE_FILE_NAME = "state.json"
@@ -13,10 +13,6 @@ MINIMUM_TIMEOUT_MINUTES: int = 1
13
13
 
14
14
  LAUNCH_FAILURE_EXIT_CODE: int = 1
15
15
 
16
- TIMEOUT_EXIT_CODE: int = 124
17
-
18
- TIMEOUT_ATTEMPT_STATUS: str = "timeout"
19
-
20
16
  CONFIGURATION_FAILURE_EXIT_CODE: int = 2
21
17
 
22
18
  WAIT_DURATION_SECONDS: float = 0.0
@@ -104,8 +100,6 @@ __all__ = [
104
100
  "SINGLE_PROMPT_FLAG",
105
101
  "STDOUT_TAIL_CHARACTER_LIMIT",
106
102
  "SUMMARY_LINE_TEMPLATE",
107
- "TIMEOUT_ATTEMPT_STATUS",
108
- "TIMEOUT_EXIT_CODE",
109
103
  "TIMEOUT_MINUTES_FLAG",
110
104
  "UTF8_ENCODING",
111
105
  "WAIT_DURATION_SECONDS",
@@ -687,6 +687,7 @@ def test_should_stop_after_timeout_without_running_job_again(
687
687
  assert invoked_homes == ["first"]
688
688
  assert outcome.attempts == (("first", "timeout"),)
689
689
  assert outcome.status == "timeout"
690
+ assert outcome.returncode == 124
690
691
  assert outcome.account_name == "first"
691
692
  assert report.final_decision.account.name == "first"
692
693
  assert account_broker.main(("choose", "--product", "codex")) == 0
@@ -696,12 +697,12 @@ def test_should_stop_after_timeout_without_running_job_again(
696
697
  @pytest.mark.parametrize(
697
698
  ("product", "roster_names", "start_error", "expected_code"),
698
699
  (
699
- (Product.CLAUDE, ("first",), subprocess.TimeoutExpired("job", 1), 4),
700
- (Product.CODEX, ("first",), subprocess.TimeoutExpired("job", 1), 127),
700
+ (Product.CLAUDE, ("first",), subprocess.TimeoutExpired("job", 1), 124),
701
+ (Product.CODEX, ("first",), subprocess.TimeoutExpired("job", 1), 124),
701
702
  (Product.CODEX, (), OSError("missing command"), 127),
702
703
  ),
703
704
  )
704
- def test_should_exit_four_only_for_a_blocked_claude_job(
705
+ def test_should_exit_with_job_failure_code(
705
706
  monkeypatch: pytest.MonkeyPatch,
706
707
  tmp_path: Path,
707
708
  product: Product,
@@ -165,7 +165,7 @@ def test_should_preserve_served_failure_code(
165
165
  def test_should_exit_124_with_timeout_reason_when_the_broker_attempt_timed_out(
166
166
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
167
167
  ) -> None:
168
- timed_out = JobOutcome(127, "", "timed out", "extra_2", (("extra_2", "timeout"),), "advisor_blocked", None, None)
168
+ timed_out = JobOutcome(124, "", "timed out", "extra_2", (("extra_2", "timeout"),), "timeout", None, None)
169
169
 
170
170
  exit_code, report, _ = _run_worker(monkeypatch, tmp_path, timed_out)
171
171