From f0b7f96c4548f4e4600ec6f6ed180b3d2feafad3 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:35:18 +0200 Subject: [PATCH 1/2] fix(tests): keep the stuck-launcher survivor clear of the job's CPU limit The executor test for a launcher that outlives its kill failed twice in CI on 2026-10-23 with `stuck == 0` and `wall_s` just over the 1.0 s timeout. The fake launcher execs the harness, so the process the test keeps alive is the harness itself and carries the run's RLIMIT_CPU: one second for a one-second job (`run_cpu_seconds`). The harness start (matplotlib, a fresh font cache per test) costs about 0.33 CPU seconds locally and about 2.4 times that on the CI runner, so once it spent the full second the kernel's SIGKILL ended the "stuck" launcher inside the grace window. A real `sandbox do` runs on the host, outside that limit, so the executor's `stuck` count is right and only the test raced. The test now gives the run the host's full CPU cap and asserts that limit on the survivor with `resource.prlimit`, so the test fails every time without the fix rather than once in a while: 31 of 31 failures with the fix removed, idle and under 48 busy loops; 50 of 50 passes with it. Also takes ruff's PEP 758 `except` form in two lines from #12244 that main's Lint job flags since the 3.14 move (the same hunks as #12259). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza --- automation/scripts/adaptability_check.py | 2 +- changelog.d/executor-stuck-test.md | 8 ++++++++ core/adaptability.py | 2 +- tests/unit/agents/renderer/test_executor.py | 10 +++++++++- 4 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 changelog.d/executor-stuck-test.md diff --git a/automation/scripts/adaptability_check.py b/automation/scripts/adaptability_check.py index d843782f8d..1714342405 100644 --- a/automation/scripts/adaptability_check.py +++ b/automation/scripts/adaptability_check.py @@ -1207,7 +1207,7 @@ def _comments(source: str) -> list[tuple[int, int, str]]: for token in tokenize.generate_tokens(io.StringIO(source).readline): if token.type == tokenize.COMMENT: out.append((token.start[0], token.start[1], token.string)) - except (tokenize.TokenError, IndentationError, SyntaxError): + except tokenize.TokenError, IndentationError, SyntaxError: for number, line in enumerate(source.splitlines(), start=1): stripped = line.lstrip() if stripped.startswith("#"): diff --git a/changelog.d/executor-stuck-test.md b/changelog.d/executor-stuck-test.md new file mode 100644 index 0000000000..22cc10d0d2 --- /dev/null +++ b/changelog.d/executor-stuck-test.md @@ -0,0 +1,8 @@ +### Fixed + +- **The stuck-launcher executor test no longer fails on a slow CI runner.** The fake + launcher execs the harness, so the process the test keeps alive carried the job's + one-second `RLIMIT_CPU`; when the harness start spent that second, the kernel killed + the "stuck" launcher inside the grace window and `stuck` read 0. The test now gives + the run the host's full CPU cap, which a real `sandbox do` outside the sandbox always + has, and asserts that limit on the survivor, so it fails every time without the fix. diff --git a/core/adaptability.py b/core/adaptability.py index f4895fcaff..ec34cd3ab0 100644 --- a/core/adaptability.py +++ b/core/adaptability.py @@ -876,7 +876,7 @@ def _comments(source: str) -> list[tuple[int, int, str]]: for token in tokenize.generate_tokens(io.StringIO(source).readline): if token.type == tokenize.COMMENT: out.append((token.start[0], token.start[1], token.string)) - except (tokenize.TokenError, IndentationError, SyntaxError): + except tokenize.TokenError, IndentationError, SyntaxError: for number, line in enumerate(source.splitlines(), start=1): stripped = line.lstrip() if stripped.startswith("#"): diff --git a/tests/unit/agents/renderer/test_executor.py b/tests/unit/agents/renderer/test_executor.py index 4c54742259..24b88792ed 100644 --- a/tests/unit/agents/renderer/test_executor.py +++ b/tests/unit/agents/renderer/test_executor.py @@ -7,6 +7,7 @@ import asyncio import base64 import os +import resource import signal import stat import time @@ -148,13 +149,20 @@ async def test_a_launcher_that_outlives_its_kill_blocks_renders_until_it_exits( executor = SandboxExecutor(replace(config, kill_grace_s=0.2)) monkeypatch.setattr(executor_module, "READER_GRACE_S", 0.2) monkeypatch.setattr(executor_module.os, "killpg", lambda pid, sig: None) # the kill has no effect + # The fake launcher execs the harness, so the survivor carries the run's RLIMIT_CPU, which a real + # `sandbox do` on the host never does. With the one second of a one-second job, the kernel's SIGKILL + # ended the "stuck" launcher inside the grace window whenever the harness start (matplotlib, a fresh + # font cache) spent that second, as it did on a loaded CI runner (2026-10-23). The full host cap + # leaves the survivor nothing but the test's own kill to die of. + monkeypatch.setattr(executor_module, "run_cpu_seconds", lambda limit_s, timeout_s: limit_s) run = await executor.run_theme(job(SLEEPER, timeout_s=1.0), "light") assert run.timed_out and executor.stuck == 1 + survivor = executor._stuck[0] + assert resource.prlimit(survivor.pid, resource.RLIMIT_CPU) == (config.rlimit_cpu_s,) * 2 with pytest.raises(Unavailable, match="stuck"): await executor.run_theme(job(PLOT), "light") - survivor = executor._stuck[0] os.kill(survivor.pid, signal.SIGKILL) await survivor.wait() assert executor.stuck == 0 From eb94d80ac34f3983caf4b2bcccd1d3559a196716 Mon Sep 17 00:00:00 2001 From: Markus Neusinger <2921697+MarkusNeusinger@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:35:48 +0200 Subject: [PATCH 2/2] docs(changelog): reference #12260 in the executor-stuck-test fragment Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0175hA62juZoajMcqKxWFGza --- changelog.d/executor-stuck-test.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.d/executor-stuck-test.md b/changelog.d/executor-stuck-test.md index 22cc10d0d2..fecd6023a1 100644 --- a/changelog.d/executor-stuck-test.md +++ b/changelog.d/executor-stuck-test.md @@ -5,4 +5,5 @@ one-second `RLIMIT_CPU`; when the harness start spent that second, the kernel killed the "stuck" launcher inside the grace window and `stuck` read 0. The test now gives the run the host's full CPU cap, which a real `sandbox do` outside the sandbox always - has, and asserts that limit on the survivor, so it fails every time without the fix. + has, and asserts that limit on the survivor, so it fails every time without the fix + (#12260).