hyf

Context-aware query service for Radroots
git clone https://radroots.dev/git/hyf.git
Log | Files | Refs | README | LICENSE

commit b4fae0bcd78143075dacd4f4ae441b86406f3c1a
parent 0eedd94057364a3cd78b6c64b44e1cc4f6d0abf0
Author: triesap <tyson@radroots.org>
Date:   Wed, 23 Sep 2026 18:17:50 +0000

H005A: complete measurement timing, provenance and ownership guarantees

- Initialize the measured wall interval for zero and positive warmup.
- Replace masked shell hash pipelines with checked argv-based digests.
- Bind the tooling identity to its imported helper closure; missing inputs fail.
- Fix retained-descriptor reuse and prove isolated late exit and recovery.

Diffstat:
Mtests/measurement_process_helper.mojo | 283++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mtests/parent_lifecycle.mojo | 30++++++++++++++++++++----------
Mtests/test_measurement_contract.mojo | 309+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_provider_helpers.mojo | 32++++++++++++++++++++++++++++++++
4 files changed, 593 insertions(+), 61 deletions(-)

diff --git a/tests/measurement_process_helper.mojo b/tests/measurement_process_helper.mojo @@ -60,6 +60,9 @@ from std.ffi import ( ) from json import Value, loads +from std.pathlib import Path + +from safe_tempdir import SafeTempDir from parent_lifecycle import ( IO_DEADLINE_EXPIRED, @@ -489,30 +492,165 @@ def source_tree_id( return text^ +# ── Fail-closed checked digest pipelines (ADR-0020 MC02) ──────────────────── + + +def _path_basename(path: String) -> String: + """Last path component, used to keep manifest text path-independent.""" + var slash = -1 + var index = 0 + for byte in path.as_bytes(): + if Int(byte) == 47: + slash = index + index += 1 + if slash < 0: + return String(path) + return String(path[byte = slash + 1 :]) + + +def _checked_file_digests( + var files: List[String], mut guard: CleanupGuard +) raises -> List[String]: + """Per-file sha256 of explicit argv paths, fail-closed. + + Every path is passed as argv data, so apostrophes, spaces and other shell + characters are handled literally and no interpolation is performed. A + missing or unreadable input, an empty input list, a failed hasher stage or + an unavailable hasher raises instead of yielding a valid empty digest. This + replaces the former status-masking shell pipeline whose final stage could + succeed on empty input and report the empty-input digest as success. + """ + if len(files) == 0: + raise Error("measurement: refusing to digest an empty input list") + var shasum_args = List[String]() + shasum_args.append("-a") + shasum_args.append("256") + for index in range(len(files)): + shasum_args.append(files[index]) + var out = run_capture( + "shasum", shasum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard + ) + if out.exit_code != 0: + var sum_args = List[String]() + for index in range(len(files)): + sum_args.append(files[index]) + out = run_capture( + "sha256sum", sum_args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard + ) + if out.exit_code != 0: + raise Error( + "measurement: sha256 unavailable for " + + String(len(files)) + + " input(s) (" + + out.describe() + + ")" + ) + var digests = List[String]() + for line in out.stdout.split("\n"): + var entry = String(line).strip() + if entry.byte_length() == 0: + continue + if entry.byte_length() < 64: + raise Error("measurement: truncated sha256 output line") + var digest = String(entry[byte=0:64]) + _require_hex(digest, 64, "sha256 input") + digests.append(digest) + if len(digests) != len(files): + raise Error( + "measurement: sha256 digest count mismatch (" + + String(len(digests)) + + " of " + + String(len(files)) + + ")" + ) + return digests^ + + +def sha256_text( + text: String, label: String, mut guard: CleanupGuard +) raises -> String: + """sha256 of exact text via a private temp file and a checked argv call.""" + with SafeTempDir() as temp_dir: + var path = temp_dir + "/measurement-manifest.txt" + try: + Path(path).write_text(text) + except: + raise Error("measurement: " + label + " staging failed") + var files = List[String]() + files.append(path) + var digests = _checked_file_digests(files^, guard) + _require_hex(digests[0], 64, label) + return digests[0] + + +def sha256_file_set( + label: String, var files: List[String], mut guard: CleanupGuard +) raises -> String: + """Deterministic, path-independent digest of an explicit file set. + + Each declared file's exact content digest is checked first, then the + manifest text ``<basename> <digest>`` (in declared order) is hashed, so the + result depends only on the declared files' content and names, never on the + checkout location. + """ + var names = List[String]() + for index in range(len(files)): + names.append(_path_basename(files[index])) + var digests = _checked_file_digests(files^, guard) + var canonical = "" + for index in range(len(names)): + canonical += names[index] + " " + digests[index] + "\n" + return sha256_text(canonical, label, guard) + + +def measurement_tooling_files(source_root: String) -> List[String]: + """Bounded closure of the test-only tooling that produced the evidence. + + The set is enumerated explicitly — never a workspace scan — and covers the + measurement helper/runner, the contract test and the shared helpers those + import (lifecycle, stdio, temp, fixtures). A change to an imported helper + therefore changes the recorded tooling identity (ADR-0020 MC02). Product + ``src`` inputs are bound separately by the clean source content manifest. + """ + var files = List[String]() + files.append(source_root + "/tests/measurement_process_helper.mojo") + files.append(source_root + "/tests/measurement_runner.mojo") + files.append(source_root + "/tests/test_measurement_contract.mojo") + files.append(source_root + "/tests/parent_lifecycle.mojo") + files.append(source_root + "/tests/safe_tempdir.mojo") + files.append(source_root + "/tests/stdio_process_helper.mojo") + files.append(source_root + "/tests/max_local_process_helper.mojo") + files.append(source_root + "/tests/strict_fixture.mojo") + return files^ + + def source_manifest_sha256( source_root: String, mut guard: CleanupGuard ) raises -> String: """Deterministic content digest of the tracked ``src`` build inputs. - The digest covers the index entries (mode/blob/path) of every tracked file - under ``src`` — the exact inputs of ``mojo build -I src src/main.mojo``. - It never reads or hashes secrets or arbitrary workspace files. + The digest covers the exact ``git ls-files -s -- src`` index listing + (mode/blob/path) — the inputs of ``mojo build -I src src/main.mojo``. The + git stage and the digest stage are both checked, so a missing repository, a + failed git command or an empty listing is an error, never a valid empty + digest. It never reads or hashes secrets or arbitrary workspace files. """ var args = List[String]() - args.append("-c") - args.append( - "git ls-files -s -- src | LC_ALL=C sort | shasum -a 256 | cut -d' ' -f1" - ) - var out = run_capture( - "sh", args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard, source_root - ) - var text = String(out.stdout.strip()) + args.append("ls-files") + args.append("-s") + args.append("--") + args.append("src") + var out = git_capture(source_root, args^, guard) if out.exit_code != 0: raise Error( "measurement: source content manifest unavailable in " + source_root ) - _require_hex(text, 64, "source content manifest") - return text^ + var listing = String(out.stdout) + if listing.strip().byte_length() == 0: + raise Error( + "measurement: source content manifest is empty in " + source_root + ) + return sha256_text(listing, "source content manifest", guard) def source_dirty_status( @@ -541,31 +679,13 @@ def tooling_manifest_sha256( The tooling is test-only source outside the product build inputs, so it is not covered by the clean-tree build binding. Recording its exact content - digest ties the emitted evidence to the reviewed tooling revision without - requiring the working tree to be committed at capture time. + digest ties the emitted evidence to the reviewed tooling revision and its + imported helper closure without requiring the working tree to be committed + at capture time. A missing input or failed hasher stage is a bounded error, + never a valid empty digest. """ - var args = List[String]() - args.append("-c") - args.append( - "shasum -a 256 '" - + source_root - + "/tests/measurement_process_helper.mojo' '" - + source_root - + "/tests/measurement_runner.mojo' '" - + source_root - + "/tests/test_measurement_contract.mojo' | shasum -a 256 | cut -d'" - " ' -f1" - ) - var out = run_capture( - "sh", args^, MEASUREMENT_SAMPLE_DEADLINE_MS, guard, source_root - ) - var text = String(out.stdout.strip()) - if out.exit_code != 0: - raise Error( - "measurement: tooling manifest unavailable in " + source_root - ) - _require_hex(text, 64, "tooling manifest") - return text^ + var files = measurement_tooling_files(source_root) + return sha256_file_set("tooling manifest", files^, guard) def source_identity( @@ -1561,6 +1681,7 @@ struct MeasurementSession(Movable): var measured_wall_ms: Int var measured_sampling_ms: Int var measured_ms: Int + var run_wall_ms: Int var request_total_ms: Int var request_min_ms: Int var request_max_ms: Int @@ -1577,6 +1698,17 @@ struct MeasurementSession(Movable): var sampling_method: String var sampling_cadence: String + def accounting_error_ms(self) -> Int: + """Coherent accounting identity: wall = measured + instrumentation (ms). + + ``measured_ms`` and ``measured_sampling_ms`` are both milliseconds + inside the same measured wall interval, so a truthful session returns + exactly zero here (ADR-0020 MC01). + """ + return ( + self.measured_wall_ms - self.measured_ms - self.measured_sampling_ms + ) + def summary(self) -> String: return ( "frames=" @@ -1599,6 +1731,8 @@ struct MeasurementSession(Movable): + String(self.measured_wall_ms) + " measured_sampling_ms=" + String(self.measured_sampling_ms) + + " run_wall_ms=" + + String(self.run_wall_ms) + " request_total_ms=" + String(self.request_total_ms) + " request_min_ms=" @@ -1636,6 +1770,34 @@ def argv_profile_of(binary_path: String, var argv: List[String]) -> String: return profile^ +def sample_warmup_boundary( + mut process: MeasurementProcess, + rss_sampler: String, + fd_sampler: String, + mut guard: CleanupGuard, + mut rss_peak: Int, + mut fd_peak: Int, +) raises -> Int: + """Instrumentation between warmup and the measured wall interval. + + Both samples are taken *before* the measured interval opens, so they are + recorded as after-warmup/peak observations and are neither subtracted from + nor added to the measured instrumentation total. Subtracting this interval + afterwards (the period-11 defect) biased the measured time upward; ADR-0020 + MC01 requires it to be excluded exactly once by staying outside the + interval. Returns the recorded after-warmup RSS in kB. + """ + var remaining = process.work_remaining_ms() + var rss = sample_rss_kb(process.pid, rss_sampler, guard, remaining) + var remaining_fd = process.work_remaining_ms() + var fds = sample_fd_count(process.pid, fd_sampler, guard, remaining_fd) + if rss > rss_peak: + rss_peak = rss + if fds > fd_peak: + fd_peak = fds + return rss + + def measure_persistent_process( source_root: String, binary_path: String, @@ -1718,6 +1880,7 @@ def measure_persistent_process( var measured_wall_ms = 0 var measured_sampling_ms = 0 var measured_ms = 0 + var run_wall_ms = 0 var request_total_ms = 0 var request_min_ms = -1 var request_max_ms = 0 @@ -1750,6 +1913,16 @@ def measure_persistent_process( var warmup_start = now_ms() var measured_start = -1 var total = warmup_frames + measured_frames + if warmup_frames == 0: + # ADR-0020 MC01: a zero-warmup measurement must open the measured + # wall interval before its first measured request. Leaving it unset + # reported host uptime as a duration (the period-11 defect: an + # 804 ms run reported 347472213 ms). No warmup ran, so the + # after-warmup checkpoint is the pre-warmup observation and the + # measured instrumentation total starts empty. + warmup_ms = 0 + rss_after_warmup = rss_before + measured_start = now_ms() for index in range(total): var pair = build_status_frame(index) var frame = pair[0] @@ -1790,22 +1963,13 @@ def measure_persistent_process( raise Error("measurement: frame invalid " + first_failure) if index == warmup_frames - 1: warmup_ms = now_ms() - warmup_start - var s_start = now_ms() - var s_remaining = process.work_remaining_ms() - rss_after_warmup = sample_rss_kb( - process.pid, rss_sampler, guard, s_remaining - ) - var s_remaining_fd = process.work_remaining_ms() - fd_peak = max( - fd_peak, - sample_fd_count( - process.pid, fd_sampler, guard, s_remaining_fd - ), + # Boundary instrumentation is taken before the measured wall + # interval opens, so it is excluded exactly once by never + # entering that interval; it is never subtracted afterwards. + rss_after_warmup = sample_warmup_boundary( + process, rss_sampler, fd_sampler, guard, rss_peak, fd_peak ) - if rss_after_warmup > rss_peak: - rss_peak = rss_after_warmup measured_start = now_ms() - measured_sampling_ms -= now_ms() - s_start if index >= warmup_frames: var measured_index = index - warmup_frames request_total_ms += latency @@ -1843,8 +2007,24 @@ def measure_persistent_process( rss_peak = rss_after_measured if fd_after > fd_peak: fd_peak = fd_after + if measured_start < 0: + raise Error("measurement: measured phase was never initialized") measured_wall_ms = now_ms() - measured_start + if measured_wall_ms < 0 or measured_sampling_ms < 0: + raise Error("measurement: inconsistent measured timing window") measured_ms = measured_wall_ms - measured_sampling_ms + if measured_ms < 0: + raise Error( + "measurement: instrumentation exceeds the measured wall" + " interval" + ) + # The measured interval is bounded by the observed run, not by host + # uptime: an uninitialized start would report the latter (ADR-0020 MC01). + run_wall_ms = now_ms() - process.spawn_ms + if measured_wall_ms > run_wall_ms: + raise Error( + "measurement: measured interval exceeds the observed run" + ) var st = process.finish_expected(total) child_exit = st.describe() + " cleanup=proved" stderr_excerpt = process.stderr_text() @@ -1887,6 +2067,7 @@ def measure_persistent_process( measured_wall_ms=measured_wall_ms, measured_sampling_ms=measured_sampling_ms, measured_ms=measured_ms, + run_wall_ms=run_wall_ms, request_total_ms=request_total_ms, request_min_ms=request_min_ms, request_max_ms=request_max_ms, diff --git a/tests/parent_lifecycle.mojo b/tests/parent_lifecycle.mojo @@ -739,23 +739,33 @@ struct CleanupGuard(Movable): def close_retained_fd(mut self, fd: Int) -> Bool: """Close a retained report descriptor at most once. - Returns True when this guard owns an entry for ``fd`` (closed now or - already closed earlier), so the owning handle must not close that - number again and can never target a reused descriptor. Returns False - when the guard holds no retained entry, letting the normal proved path - keep its own descriptor ownership. + Returns True only when this guard still owns an *open* retained + descriptor with number ``fd`` and closes it now. An entry whose + descriptor was already closed has released that number, so it must not + claim or close a later descriptor that reused the same number (the + period-11 R73/MC03 defect: a recovered entry matched a new measurement's + descriptor and made it skip its own close, leaking one descriptor). + Returns False when the guard holds no open retained entry, letting the + normal proved path keep its own descriptor ownership. """ if fd < 0: return False - var owned = False + var claimed = -1 for index in range(len(self.failures)): if self.failures[index].report_fd != fd: continue - owned = True - if not self.failures[index].fd_closed: - close_fd(fd) + if self.failures[index].fd_closed: + # Already released: this number now belongs to a new owner. + continue + claimed = index + break + if claimed < 0: + return False + close_fd(fd) + for index in range(len(self.failures)): + if self.failures[index].report_fd == fd: self.failures[index].fd_closed = True - return owned + return True def recover_all(mut self) -> Int: """Retry cleanup for every retained exact-owned child. diff --git a/tests/test_measurement_contract.mojo b/tests/test_measurement_contract.mojo @@ -22,6 +22,9 @@ dependency or lock. from std.collections import List from std.testing import TestSuite, assert_equal, assert_true +import std.os +from std.pathlib import Path + from safe_tempdir import SafeTempDir from parent_lifecycle import ( @@ -45,9 +48,15 @@ from measurement_process_helper import ( measure_persistent_process, measurement_poll, measurement_poll_retry, + measurement_tooling_files, + require_clean_source, run_capture, sample_fd_count, sample_rss_kb, + sha256_file_set, + source_identity, + source_manifest_sha256, + tooling_manifest_sha256, validate_status_frame, ) from json import Value, loads @@ -93,6 +102,23 @@ def _sh(args_text: String) -> List[String]: return args^ +def _multi_response() -> String: + """A well-behaved responder for any frame count. + + Request ids/trace ids are deterministic (``meas-status-<index>``), so a + counter reproduces the exact expected correlation for each frame in order. + """ + return ( + "i=0\n" + "while IFS= read -r line; do\n" + 'printf \'{"version":1,"request_id":"meas-status-%s",' + '"trace_id":"meas-trace-%s","ok":true,' + '"output":{"daemon":"hyfd"}}\\n\' "$i" "$i"\n' + "i=$((i+1))\n" + "done" + ) + + def _run_sh_measurement( args_text: String, warmup: Int, @@ -858,5 +884,288 @@ def test_frame_validation_controls() raises: ) +# ── MC01 corrected timing windows ─────────────────────────────────────────── + + +def test_measurement_zero_warmup_timing_is_initialized() raises: + # ADR-0020 MC01: a zero-warmup measurement must open a bounded measured wall + # interval before its first request. The period-11 defect left the start + # uninitialized, so an 804 ms run reported 347472213 ms (host uptime). + var guard = CleanupGuard() + var session = _run_sh_measurement(_one_response(), 0, 1, guard) + assert_equal(session.warmup_frames, 0) + assert_equal(session.ok_frames, 1) + assert_equal(session.warmup_ms, 0) + assert_true(session.measured_wall_ms >= 0) + assert_true(session.measured_wall_ms <= session.run_wall_ms) + assert_true(session.measured_ms >= 0) + assert_true(session.measured_sampling_ms >= 0) + assert_true(session.measured_ms <= session.measured_wall_ms) + assert_equal(session.accounting_error_ms(), 0) + assert_true(session.rss_kb_after_warmup > 0) + guard.assert_clean() + + +def test_measurement_positive_warmup_timing_accounting() raises: + # ADR-0020 MC01: with a positive warmup the boundary instrumentation is + # taken before the measured interval opens and must not be subtracted from + # the instrumentation total afterwards (that overstated measured time). + var guard = CleanupGuard() + var session = _run_sh_measurement(_multi_response(), 3, 2, guard) + assert_equal(session.warmup_frames, 3) + assert_equal(session.ok_frames, 5) + assert_true(session.warmup_ms >= 0) + assert_true(session.measured_wall_ms >= 0) + assert_true(session.measured_wall_ms <= session.run_wall_ms) + assert_true(session.measured_sampling_ms >= 0) + assert_true(session.measured_ms >= 0) + assert_true(session.measured_ms <= session.measured_wall_ms) + assert_equal(session.accounting_error_ms(), 0) + guard.assert_clean() + + +def test_measurement_known_delayed_request_is_observed() raises: + # ADR-0020 MC01: a known delayed request is reflected in both the per-request + # timing and the measured wall interval, which stay internally consistent. + var guard = CleanupGuard() + var delayed = ( + "IFS= read -r line; sleep 0.15; printf '%s\\n' '" + + STATUS0 + + "'; while IFS= read -r line; do :; done" + ) + var session = _run_sh_measurement(delayed, 0, 1, guard) + assert_equal(session.ok_frames, 1) + assert_true(session.request_total_ms >= 100) + assert_true(session.request_max_ms >= 100) + assert_true(session.measured_wall_ms >= 100) + assert_true(session.measured_wall_ms <= session.run_wall_ms) + assert_true(session.measured_ms >= 0) + assert_equal(session.accounting_error_ms(), 0) + guard.assert_clean() + + +def test_measurement_slow_boundary_sampler_is_excluded_once() raises: + # ADR-0020 MC01: a slow after-warmup boundary sampler runs before the + # measured wall interval opens. It must be excluded exactly once: the + # period-11 code subtracted it from the instrumentation total and reported + # more measured time than the interval contained. + var guard = CleanupGuard() + with SafeTempDir() as temp_dir: + var counter = temp_dir + "/calls" + var sampler = temp_dir + "/slow_rss.sh" + var body = ( + '#!/bin/sh\nn=$(cat "' + + counter + + '" 2>/dev/null || echo 0)\n' + + "n=$((n+1))\n" + + 'printf \'%s\' "$n" > "' + + counter + + '"\n' + + 'if [ "$n" -le 2 ]; then sleep 1.2; fi\n' + + "echo 17000\n" + ) + Path(sampler).write_text(body) + var chmod_args = List[String]() + chmod_args.append("+x") + chmod_args.append(sampler) + var made = run_capture("chmod", chmod_args^, 10000, guard) + assert_equal(made.exit_code, 0) + var session = _run_sh_measurement( + _multi_response(), + 1, + 1, + guard, + sampler, + "lsof", + MEASUREMENT_DEADLINE_MS, + ) + assert_equal(session.ok_frames, 2) + assert_true(session.measured_sampling_ms >= 0) + assert_true(session.measured_ms >= 0) + assert_true(session.measured_ms <= session.measured_wall_ms) + assert_equal(session.accounting_error_ms(), 0) + # The two 1.2 s boundary samples stayed outside the measured interval. + assert_true(session.measured_wall_ms < 1000) + guard.assert_clean() + + +# ── MC02 fail-closed provenance ───────────────────────────────────────────── + + +def test_measurement_tooling_manifest_rejects_missing_inputs() raises: + # ADR-0020 MC02: a missing tooling input must fail, never return the valid + # empty-input digest that the period-11 masked pipeline produced. + var guard = CleanupGuard() + with SafeTempDir() as temp_dir: + var message = "" + try: + _ = tooling_manifest_sha256(temp_dir + "/absent-root", guard) + except e: + message = String(e) + assert_true(message.find("sha256") >= 0) + assert_true(message.find("e3b0c442") < 0) + guard.assert_clean() + + +def test_measurement_source_manifest_rejects_missing_repository() raises: + # ADR-0020 MC02: a failed git stage must be an error, not an empty digest. + var guard = CleanupGuard() + with SafeTempDir() as temp_dir: + var message = "" + try: + _ = source_manifest_sha256(temp_dir, guard) + except e: + message = String(e) + assert_true(message.find("source content manifest") >= 0) + assert_true(message.find("e3b0c442") < 0) + guard.assert_clean() + + +def test_measurement_digest_handles_path_characters() raises: + # ADR-0020 MC02: paths are argv data, so apostrophes and spaces in a path + # must be handled literally (the period-11 tooling pipeline interpolated + # paths into a single-quoted shell string and could mis-hash or fail). + var guard = CleanupGuard() + with SafeTempDir() as base: + var weird = base + "/hyf 'quoted' dir" + _ = std.os.makedirs(weird, exist_ok=True) + var one = weird + "/a 'one'.txt" + var two = weird + "/b two.txt" + Path(one).write_text("alpha") + Path(two).write_text("beta") + var files = List[String]() + files.append(one) + files.append(two) + var digest = sha256_file_set("weird path set", files^, guard) + assert_equal(digest.byte_length(), 64) + Path(two).write_text("gamma") + var files_two = List[String]() + files_two.append(one) + files_two.append(two) + var digest_two = sha256_file_set("weird path set", files_two^, guard) + assert_true(digest != digest_two) + guard.assert_clean() + + +def test_measurement_tooling_manifest_binds_imported_helpers() raises: + # ADR-0020 MC02: the tooling identity must bind the imported helper closure, + # not only the three top-level tooling files. Mutating an *imported* helper + # in an isolated copy changes the digest. + var guard = CleanupGuard() + with SafeTempDir() as root: + var tests_dir = root + "/tests" + _ = std.os.makedirs(tests_dir, exist_ok=True) + var sources = measurement_tooling_files(".") + var cp_args = List[String]() + for index in range(len(sources)): + cp_args.append(sources[index]) + cp_args.append(tests_dir) + var copied = run_capture("cp", cp_args^, 20000, guard) + assert_equal(copied.exit_code, 0) + var before = tooling_manifest_sha256(root, guard) + assert_equal(before.byte_length(), 64) + Path(tests_dir + "/parent_lifecycle.mojo").write_text("// mutated\n") + var after = tooling_manifest_sha256(root, guard) + assert_true(before != after) + guard.assert_clean() + + +def test_measurement_rejects_dirty_measured_tree() raises: + # ADR-0020 MC02/MR04: a dirty measured build input is rejected at capture. + # The control uses an isolated owned git repository, never the real + # checkout, and never changes real index or host flags. + var guard = CleanupGuard() + with SafeTempDir() as root: + _ = std.os.makedirs(root + "/src", exist_ok=True) + Path(root + "/src/main.mojo").write_text("fn main():\n pass\n") + Path(root + "/pixi.toml").write_text("[workspace]\n") + Path(root + "/pixi.lock").write_text("version: 4\n") + var init_args = List[String]() + init_args.append("init") + init_args.append("--quiet") + var initialized = run_capture("git", init_args^, 20000, guard, root) + assert_equal(initialized.exit_code, 0) + var add_args = List[String]() + add_args.append("add") + add_args.append("-A") + var added = run_capture("git", add_args^, 20000, guard, root) + assert_equal(added.exit_code, 0) + var commit_args = List[String]() + commit_args.append("-c") + commit_args.append("user.email=hyf-test@invalid") + commit_args.append("-c") + commit_args.append("user.name=hyf test") + commit_args.append("-c") + commit_args.append("commit.gpgsign=false") + commit_args.append("commit") + commit_args.append("--no-verify") + commit_args.append("--quiet") + commit_args.append("-m") + commit_args.append("init") + var committed = run_capture("git", commit_args^, 20000, guard, root) + assert_equal(committed.exit_code, 0) + Path(root + "/src/main.mojo").write_text("fn main():\n return\n") + var identity = source_identity(root, guard) + assert_true(identity.dirty_status != "") + var message = "" + try: + require_clean_source(identity, "test capture") + except e: + message = String(e) + assert_true(message.find("dirty") >= 0) + guard.assert_clean() + + +# ── MC03 isolated late exit and ownership reuse ───────────────────────────── + + +def test_measurement_rejects_isolated_late_child_exit() raises: + # ADR-0020 MC03: valid output AND closed stdout/stderr must be proved before + # the late-exit phase. The child answers, closes its own stdout/stderr and + # then stays alive past the budget, so the bounded failure is the child-exit + # wait rather than an output-drain timeout. + var guard = CleanupGuard() + var script = ( + "while IFS= read -r line; do printf '%s\\n' '" + + STATUS0 + + "'; done; exec 1>&- 2>&-; sleep 2" + ) + var message = _run_sh_failure(script, 0, 1, guard, "ps", "lsof", 400) + assert_true(message.find("did not exit within its budget") >= 0) + assert_true(message.find("output drain") < 0) + assert_true(message.find("frame invalid") < 0) + guard.assert_clean() + + +def test_measurement_recovery_then_valid_call_leaks_nothing() raises: + # ADR-0020 MC03/R73: after a retained-and-recovered cleanup, a following + # valid measurement with the same guard must not skip closing a descriptor + # number that was reused. The period-11 probe observed FD delta +1 here. + var guard = CleanupGuard() + var self_pid = owned_pid() + var child_before = child_process_count(self_pid, guard) + var fd_before = open_fd_count_checked() + var message = _run_sh_failure( + "while IFS= read -r line; do printf 'not-json\\n'; done", + 0, + 1, + guard, + "ps", + "lsof", + MEASUREMENT_DEADLINE_MS, + MeasurementFaults(cleanup_failures=1), + ) + assert_true(message.find("not_json") >= 0) + assert_true(guard.retained() >= 1) + assert_equal(guard.recover_all(), 0) + guard.assert_clean() + var recovered = _run_sh_measurement(_one_response(), 0, 1, guard) + assert_equal(recovered.ok_frames, 1) + assert_equal(recovered.failed_frames, 0) + guard.assert_clean() + assert_equal(open_fd_count_checked() - fd_before, 0) + assert_equal(child_process_count(self_pid, guard), child_before) + + def main() raises: TestSuite.discover_tests[__functions_in_module()]().run() diff --git a/tests/test_provider_helpers.mojo b/tests/test_provider_helpers.mojo @@ -2031,6 +2031,38 @@ def test_cleanup_recovery_through_guard_both_providers() raises: assert_true(open_fd_count_checked() <= jev_fd_before) +def test_released_retained_descriptor_is_not_reclaimed_both_providers() raises: + # MC03/RA02: once a retained report descriptor is released, a following + # owned child that reuses that descriptor number must close its own + # descriptor. The shared guard must not claim a reused number, which would + # leak one descriptor per recovered failure (period-11 R73). + var guard = CleanupGuard() + var fd_before = open_fd_count_checked() + var stub = spawn_max_local_stub(0, "count_requests", 1, guard, 2000) + stub.state.faults.cleanup_failures = 1 + stub.cleanup() + assert_equal(guard.retained(), 1) + assert_equal(guard.recover_all(), 0) + guard.assert_clean() + var reused = spawn_max_local_stub(0, "count_requests", 1, guard, 2000) + reused.cleanup() + guard.assert_clean() + assert_equal(open_fd_count_checked() - fd_before, 0) + + var jev_guard = CleanupGuard() + var jev_fd_before = open_fd_count_checked() + var jev_stub = spawn_jev_stub_auto("ok", 1, jev_guard, 2000) + jev_stub.stub.state.faults.cleanup_failures = 1 + jev_stub.stub.cleanup() + assert_equal(jev_guard.retained(), 1) + assert_equal(jev_guard.recover_all(), 0) + jev_guard.assert_clean() + var jev_reused = spawn_jev_stub_auto("ok", 1, jev_guard, 2000) + jev_reused.stub.cleanup() + jev_guard.assert_clean() + assert_equal(open_fd_count_checked() - jev_fd_before, 0) + + def test_reap_wait_error_retains_ownership_both_providers() raises: # RA02: a transient wait error consumed by reap() must stay retryable and # must not become a cached terminal result; the retry reports success.