test(code-runtime-python): pin the reap-poll group-emptied arm with a dispose-timing case

The review's premise that the group-emptied arm could not be pinned was
incorrect; the same-group reap case already exercises it. This adds the missing
seam-observable case: dispose() while a setsid orphan holds the pipes and the
run is unresolved — settle kills the child, the group empties (the orphan is in
its own session), and the poll finalizes promptly instead of waiting out the
60 s grace. The v8 ignore on that arm is removed.
This commit is contained in:
Chinesezjc
2026-08-31 15:02:38 +08:00
committed by Tianyi Cui
parent 9b7b5489be
commit bc08f405cc
2 changed files with 25 additions and 2 deletions
@@ -1825,8 +1825,6 @@ export class PythonCodeRuntime extends CodeRuntime {
// final hard bound where nothing more can be done.
let hardDeadline = 0
const pollGroup = (): void => {
/* v8 ignore next -- the group-emptied arm needs the close-driven settle to win the
* race against the grace SIGKILL; no seam-observable test pins that interleaving. */
if (groupEmpty()) {
// The group is gone; the grace SIGKILL is moot. Cancel it (it may not
// have fired yet) and finalize. graceTimer is always defined here:
@@ -2952,6 +2952,31 @@ describe('PythonCodeRuntime — budgets, termination, disposal', () => {
expect(still).toBe(true)
}, 20_000)
it('dispose resolves promptly when the kill empties the group and an orphan holds the pipes', async () => {
// The group-emptied arm of the reap poll: dispose() drives settle, kill()
// kills the still-running child, and a setsid orphan holds the pipes open
// so close never fires — the poll must run, see the group empty (the
// orphan escaped into its own session), cancel the pending grace SIGKILL,
// and finalize immediately. A prompt resolve proves the arm ran
// (fail-before: dropping clearTimeout/finalize from that arm leaves dispose
// waiting for the never-firing grace escalation and blows the bound).
const { runtime, fiber } = await setup({ maxWallMs: 60_000, graceMs: 60_000 })
const start = Date.now()
const runPromise = runtime.run({
program: [
'import subprocess, sys, time',
'subprocess.Popen([sys.executable, "-c", "import time; time.sleep(30)"],',
' start_new_session=True)',
'time.sleep(30)',
].join('\n'),
bindings: [],
})
await fiber.dispose()
const result = await runPromise
expect(result.error?.kind).toBe('abort')
expect(Date.now() - start).toBeLessThan(5_000)
}, 20_000)
it('dispose awaits reaping of a same-group survivor from a completed run', async () => {
// The quiescence contract also holds for a run that ALREADY resolved: the run
// stays tracked in `live` until its process group is reaped, so a `dispose()`