commit 66cd9c0663c2ee705433a36f5c0d46b5b1ea3598
parent 5f82d3d097b7afa5f40a0d945f7ab0da36c72ce8
Author: triesap <tyson@radroots.org>
Date: Wed, 23 Sep 2026 03:04:01 +0000
test(hyf): retain unproved reap ownership and add PC03/PC04 controls (C002D review)
Independent read-only review of 5f82d3d returned FAIL with mandatory findings.
Repair within ADR-0017 D37 PC01-PC05; test-only.
PC02: reap() no longer marks the child reaped when the wait/termination is
unproved. A wait_error retains identity/ownership for a retry and records the
uncertainty in the caller-owned ledger; a watchdog termination records cleanup
failure instead of claiming collection. New control drives reap() on a real
owned child with a controlled wait failure, then retries and proves cleanup on
both provider paths.
PC01: the descriptor-read control now executes through BOTH provider reap
paths (real owned child, unavailable report descriptor -> read_error, never a
silent EOF), not only the shared reader.
PC03: add a late-exit control entrypoint (valid JSON, closed stdio, exit past
the declared budget) and prove bounded rejection within the declared budget
with cleanup truth exposed; the cleanup grace is not extra successful work.
Re-verified on final source: provider-helpers 76/76, repo-local-process 9/9,
test-stdio 56/32/24/0 with identical D16 names and reasons, format/architecture/
build green.
Diffstat:
5 files changed, 146 insertions(+), 15 deletions(-)
diff --git a/tests/jev_provider_helper.mojo b/tests/jev_provider_helper.mojo
@@ -363,19 +363,32 @@ struct SpawnedJevStub(Movable):
var term = terminate_owned(self.pid, TERMINATION_GRACE_MS)
self.state.status = term.copy()
self.state.store(False, "watchdog", "-", "timeout", 0, 0)
- if not term.cleanup_proved():
+ if term.cleanup_proved():
+ self.state.reaped = True
+ self.state.close_reader()
+ else:
self.state.cleanup_error = "unreaped:" + term.describe()
self.state.reason = "timeout_unreaped"
- self.state.reaped = True
- self.state.close_reader()
+ self.state.ledger.record(
+ "owned-child cleanup unproved " + term.describe()
+ )
return
- if status.state == "gone" or status.state == "wait_error":
- self.state.store(False, "watchdog", "-", status.state, 0, 0)
- if status.state == "wait_error":
- self.state.cleanup_error = "wait_error:" + status.error
+ if status.state == "gone":
+ # No waitable owned child remains: cleanup is proved without a
+ # signal and the report cannot be trusted, but ownership is done.
+ self.state.store(False, "watchdog", "-", "gone", 0, 0)
self.state.reaped = True
self.state.close_reader()
return
+ if status.state == "wait_error":
+ # Identity/ownership is unproved: retain it for a retry and record
+ # the uncertainty instead of claiming the child was collected.
+ self.state.store(False, "watchdog", "-", "wait_error", 0, 0)
+ self.state.cleanup_error = "wait_error:" + status.error
+ self.state.ledger.record(
+ "owned-child wait unproved " + status.describe()
+ )
+ return
var report_text = ""
var report_error = ""
try:
diff --git a/tests/max_local_process_helper.mojo b/tests/max_local_process_helper.mojo
@@ -468,19 +468,32 @@ struct SpawnedMaxLocalStub(Movable):
var term = terminate_owned(self.pid, TERMINATION_GRACE_MS)
self.state.status = term.copy()
self.state.store(False, "watchdog", "-", "timeout", 0, 0)
- if not term.cleanup_proved():
+ if term.cleanup_proved():
+ self.state.reaped = True
+ self.state.close_reader()
+ else:
self.state.cleanup_error = "unreaped:" + term.describe()
self.state.reason = "timeout_unreaped"
- self.state.reaped = True
- self.state.close_reader()
+ self.state.ledger.record(
+ "owned-child cleanup unproved " + term.describe()
+ )
return
- if status.state == "gone" or status.state == "wait_error":
- self.state.store(False, "watchdog", "-", status.state, 0, 0)
- if status.state == "wait_error":
- self.state.cleanup_error = "wait_error:" + status.error
+ if status.state == "gone":
+ # No waitable owned child remains: cleanup is proved without a
+ # signal and the report cannot be trusted, but ownership is done.
+ self.state.store(False, "watchdog", "-", "gone", 0, 0)
self.state.reaped = True
self.state.close_reader()
return
+ if status.state == "wait_error":
+ # Identity/ownership is unproved: retain it for a retry and record
+ # the uncertainty instead of claiming the child was collected.
+ self.state.store(False, "watchdog", "-", "wait_error", 0, 0)
+ self.state.cleanup_error = "wait_error:" + status.error
+ self.state.ledger.record(
+ "owned-child wait unproved " + status.describe()
+ )
+ return
var report_text = ""
var report_error = ""
try:
diff --git a/tests/stdio_late_exit_entrypoint.mojo b/tests/stdio_late_exit_entrypoint.mojo
@@ -0,0 +1,16 @@
+"""Test-only stdio entrypoint that emits valid JSON, closes stdio and lingers.
+
+Exercises PC03: a child that finishes its output and then delays exit past the
+parent's declared budget must be rejected within that budget, with the bounded
+cleanup grace used only for cleanup.
+"""
+
+from std.ffi import c_int, external_call
+from std.sys._libc import close
+
+
+def main():
+ print('{"ok":true}')
+ _ = close(c_int(1))
+ _ = close(c_int(2))
+ _ = external_call["usleep", c_int](c_int(1500000))
diff --git a/tests/test_provider_helpers.mojo b/tests/test_provider_helpers.mojo
@@ -1672,6 +1672,72 @@ def test_cleanup_failure_preserves_retryable_ownership() raises:
assert_true(pid_not_waitable(jev_actual))
+def test_reap_wait_error_retains_ownership_both_providers() raises:
+ # PC02: an unproved/uncertain wait consumed by reap() must not mark the
+ # child collected or discard retryable ownership; a later retry with the
+ # restored exact identity still collects it.
+ var recorded = List[String]()
+ var ledger = CleanupLedger(UnsafePointer(to=recorded))
+ var stub = spawn_max_local_stub(0, "count_requests", 1, 2000, ledger)
+ var actual = stub.pid
+ stub.pid = 0
+ stub.reap()
+ assert_true(not stub.ok())
+ assert_true(not stub.status().cleanup_proved())
+ assert_equal(len(recorded), 1)
+ stub.pid = actual
+ stub.cleanup()
+ assert_true(stub.status().cleanup_proved())
+ assert_true(pid_not_waitable(actual))
+
+ var jev_stub = spawn_jev_stub_auto("ok", 1, 2000, ledger)
+ var jev_actual = jev_stub.stub.pid
+ jev_stub.stub.pid = 0
+ jev_stub.stub.reap()
+ assert_true(not jev_stub.stub.ok())
+ assert_true(not jev_stub.stub.status().cleanup_proved())
+ assert_equal(len(recorded), 2)
+ jev_stub.stub.pid = jev_actual
+ jev_stub.stub.cleanup()
+ assert_true(jev_stub.stub.status().cleanup_proved())
+ assert_true(pid_not_waitable(jev_actual))
+
+
+def test_provider_reap_descriptor_read_error_both_paths() raises:
+ # PC01: the descriptor-read control executes through BOTH provider reap
+ # paths, not only the shared reader: a real owned child with an unavailable
+ # report descriptor fails with read_error, never a silent EOF/empty report.
+ var pipe = make_pipe()
+ var closed_fd = pipe.read_fd
+ close_fd(pipe.read_fd)
+ close_fd(pipe.write_fd)
+ var pid = fork_pid()
+ if pid == 0:
+ child_exit(0)
+ var state = piped_child_state(pid, closed_fd, 2000, 1, CleanupLedger())
+ var stub = SpawnedMaxLocalStub(pid, 0, state^)
+ stub.reap()
+ assert_true(not stub.ok())
+ assert_equal(stub.reason(), "read_error")
+ assert_true(pid_not_waitable(pid))
+
+ var jev_pipe = make_pipe()
+ var jev_closed_fd = jev_pipe.read_fd
+ close_fd(jev_pipe.read_fd)
+ close_fd(jev_pipe.write_fd)
+ var jev_pid = fork_pid()
+ if jev_pid == 0:
+ child_exit(0)
+ var jev_state = piped_child_state(
+ jev_pid, jev_closed_fd, 2000, 1, CleanupLedger()
+ )
+ var jev_stub = SpawnedJevStub(jev_pid, jev_state^)
+ jev_stub.reap()
+ assert_true(not jev_stub.ok())
+ assert_equal(jev_stub.reason(), "read_error")
+ assert_true(pid_not_waitable(jev_pid))
+
+
def test_cleanup_failure_survives_scope_exit() raises:
# PC02: cleanup failure must remain observable after the owning handle is
# destroyed at scope exit, not merely stored in an inaccessible object.
diff --git a/tests/test_repo_local_process_contract.mojo b/tests/test_repo_local_process_contract.mojo
@@ -4,7 +4,7 @@ from safe_tempdir import SafeTempDir
from json import Value
from fixture_assertions import load_scenario_request_json
-from parent_lifecycle import POLLIN, close_fd, make_pipe, write_raw
+from parent_lifecycle import POLLIN, close_fd, make_pipe, now_ms, write_raw
from stdio_process_helper import (
HYF_PATHS_PROFILE_ENV,
HYF_PATHS_REPO_LOCAL_ROOT_ENV,
@@ -126,6 +126,29 @@ def test_run_stdio_entrypoint_rejects_unread_request() raises:
assert_true(message.find("cleanup_error=") >= 0)
+def test_run_stdio_entrypoint_rejects_late_success() raises:
+ # PC03: a child that emits valid JSON, closes stdio and delays exit past the
+ # declared budget must be rejected within the budget; the bounded cleanup
+ # grace is not extra successful work.
+ var start = now_ms()
+ var message = ""
+ try:
+ _ = run_stdio_entrypoint_with_deadline(
+ "tests/stdio_late_exit_entrypoint.mojo", "{}", "", "", 1200
+ )
+ except e:
+ message = String(e)
+ var elapsed = now_ms() - start
+ assert_true(message.find("stdio-entrypoint") >= 0)
+ assert_true(
+ message.find("timeout") >= 0
+ or message.find("read_deadline_expired") >= 0
+ )
+ assert_true(message.find("cleanup_error=") >= 0)
+ assert_true(elapsed >= 1000)
+ assert_true(elapsed <= 3700)
+
+
def test_run_stdio_entrypoint_classifies_loader_failure() raises:
var message = ""
try: