Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ Format: [Keep a Changelog](https://keepachangelog.com/). Versioning: [SemVer](ht
no persisted checkout credentials, and the release tag passed via env instead
of template expansion. Adds a `gitleaks` secret-scan job (checksum-verified
binary, redacted output) and documents an optional pre-commit hook.
- Makes the subprocess timeout test deterministic: it raced a 1-second child
against a 1-second timeout (~40 ms margin) and failed randomly on macOS. It
now checks that the child process is gone instead of racing the clock.

## [0.6.0] — 2026-09-20

Expand Down
29 changes: 24 additions & 5 deletions src/session_recall/codex_fix/tests/test_v5_runner_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from __future__ import annotations

import json
import os
import subprocess
import sys
import time
Expand Down Expand Up @@ -132,16 +133,34 @@ def test_real_subprocess_output_flood_is_killed_and_bounded(tmp_path):
)


def _process_gone(pid: int, deadline: float) -> bool:
end = time.monotonic() + deadline
while time.monotonic() < end:
try:
os.kill(pid, 0)
except ProcessLookupError:
return True
except PermissionError:
return True # PID reused by another user's process: ours is gone
time.sleep(0.05)
return False


def test_real_subprocess_timeout_kills_child_process_group(tmp_path):
# The child outlives the timeout by a wide margin, so the outcome depends
# only on whether the process group is killed, not on scheduling luck.
sentinel = tmp_path / "child-survived"
child = "import time; from pathlib import Path; time.sleep(1); Path(%r).write_text('bad')" % str(sentinel)
pid_file = tmp_path / "child.pid"
child = "import time; from pathlib import Path; time.sleep(30); Path(%r).write_text('bad')" % str(sentinel)
parent = (
"import subprocess,sys,time; "
f"subprocess.Popen([sys.executable,'-c',{child!r}]); time.sleep(30)"
"import subprocess,sys,time; from pathlib import Path; "
f"p = subprocess.Popen([sys.executable,'-c',{child!r}]); "
f"Path({str(pid_file)!r}).write_text(str(p.pid)); time.sleep(30)"
)
with pytest.raises(subprocess.TimeoutExpired):
_actual_run([sys.executable, "-c", parent], timeout=1, cwd=tmp_path)
time.sleep(1.2)
_actual_run([sys.executable, "-c", parent], timeout=2, cwd=tmp_path)
child_pid = int(pid_file.read_text())
assert _process_gone(child_pid, deadline=5.0), "child survived the group kill"
assert not sentinel.exists()


Expand Down
Loading