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:
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.