commit a3977ea399652073e5306abc006c034982b46017
parent 75ed3bff92e5c267e2db55a52f0c927a5b3d31e4
Author: triesap <tyson@radroots.org>
Date: Mon, 24 Aug 2026 22:15:23 +0000
test: close the RHI process qualification wave
Bind the Step 214 failure corpus to actual executable coverage, bounded parallel inspection, and repeated state reopen verification. Harden child-process capture, timeout, interruption, and cleanup behavior without adding a production failpoint surface.
Diffstat:
5 files changed, 467 insertions(+), 42 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
@@ -455,6 +455,13 @@
a production failpoint surface, or a test environment selector. Step 215
alone owns actual-process and bounded-soak qualification; Steps 216 and 217
own native release evidence and promotion respectively.
+- Step 215 closes wave `130-c` with the actual RHI executable. Keep the process
+ corpus loopback-only, deadline- and output-bounded, and free of production
+ failpoints or environment-selected test behavior. Every daemon must be
+ interrupted, joined, and reaped; every admin socket must be absent after
+ shutdown; and the durable state must verify after every one of the exact
+ bounded reopen-soak iterations. Step 216 owns native release artifacts, and
+ Step 217 alone owns promotion and parent-pin alignment.
- Keep plaintext keys, decrypted identity, wrapping credentials, tokens, raw
sensitive evidence, private identifiers, paths, upstream errors, and
equivalent protected material out of config, logs, status, metrics, audit,
diff --git a/README b/README
@@ -787,6 +787,18 @@ artifacts remain Step 216 ownership, and promotion and parent-pin alignment
remain Step 217 ownership. Nix, OCI, signing, publication, and deployment are
deferred and unclaimed.
+The Step 215
+[`process_qualification.v1.json`](contracts/services_hardening/process_qualification.v1.json)
+contract closes wave `130-c` by binding that component corpus to the actual
+RHI executable. It exercises offline bootstrap and required-dependency outage,
+the complete supervised task graph against loopback relay sessions, eight
+concurrent read-only configuration-validation processes, and 32 clean database
+reopen-and-verify cycles. The live daemon is interrupted, joined, reaped, and
+checked for socket cleanup. Every child process has a fixed deadline and
+bounded captured output. The test surface adds no production failpoint,
+environment selector, detached worker, ambient network dependency, or later
+release authority.
+
## Standalone native release contract
The capsule owns a private `cargo xtask native-release` generator and the
diff --git a/contracts/services_hardening/process_qualification.v1.json b/contracts/services_hardening/process_qualification.v1.json
@@ -0,0 +1,55 @@
+{
+ "schema": "radroots.rhi.process-qualification.v1",
+ "contract_version": 1,
+ "step": 215,
+ "service": "rhi",
+ "binary": "rhi",
+ "source_lock": {
+ "schema": "radroots.service.source-lock.v2",
+ "lib_revision": "21b11e7a5120ea949f7ad0838c746873fc73aac2"
+ },
+ "component_qualification": {
+ "schema": "radroots.rhi.failure-qualification.v1",
+ "step": 214,
+ "sha256": "e9c782185a4a2b7512193a3cd237008026193ba3f8bb49fab1c70039a2f35bca"
+ },
+ "bounds": {
+ "process_deadline_ms": 30000,
+ "poll_interval_ms": 2,
+ "connect_deadline_ms": 5000,
+ "relay_handshake_timeout_ms": 5000,
+ "relay_io_timeout_ms": 100,
+ "parallel_inspection_processes": 8,
+ "soak_iterations": 32,
+ "maximum_stdout_bytes": 1048576,
+ "maximum_stderr_bytes": 8192
+ },
+ "actual_process_corpus": [
+ "actual_binary_executes_offline_bootstrap_and_reaches_real_runtime_dependency_boundary",
+ "actual_binary_runs_the_task_graph_serves_admin_and_shuts_down_on_interrupt",
+ "actual_binary_is_bounded_under_parallel_inspection_and_reopen_soak"
+ ],
+ "invariants": {
+ "actual_executable_required": true,
+ "loopback_relay_only": true,
+ "production_failpoint_surface": false,
+ "test_environment_selector": false,
+ "detached_test_worker": false,
+ "parallel_inspection_is_read_only": true,
+ "every_daemon_is_interrupted_joined_and_reaped": true,
+ "admin_socket_absent_after_shutdown": true,
+ "state_verifies_after_every_reopen": true,
+ "captured_output_bounded": true,
+ "diagnostics_path_secret_free": true
+ },
+ "deferred": [
+ "native_release_artifacts_step_216",
+ "rcld_promotion_step_217",
+ "parent_pin_alignment_step_217",
+ "nix",
+ "oci",
+ "signing",
+ "publication",
+ "deployment"
+ ]
+}
diff --git a/tests/package_boundary.rs b/tests/package_boundary.rs
@@ -43,6 +43,10 @@ const PUBLICATION_WAVE_QUALIFICATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/publication_wave_qualification.v1.json");
const FAILURE_QUALIFICATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/failure_qualification.v1.json");
+const PROCESS_QUALIFICATION_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/process_qualification.v1.json");
+const PROCESS_QUALIFICATION_TESTS: &str = include_str!("services_hardening_process.rs");
+const RUNTIME_GRAPH: &str = include_str!("../src/runtime_graph.rs");
const PUBLICATION_CONTRACT: &str =
include_str!("../contracts/services_hardening/publication_outbox.v1.json");
const PUBLICATION_SUBMISSION: &str = include_str!("../src/publication_submission.rs");
@@ -1268,6 +1272,43 @@ fn failure_qualification_is_source_locked_bounded_and_nonexpansive() {
}
#[test]
+fn process_qualification_is_actual_bounded_and_wave_closed() {
+ let contract: serde_json::Value = serde_json::from_str(PROCESS_QUALIFICATION_CONTRACT)
+ .expect("process qualification contract");
+ assert_eq!(contract["schema"], "radroots.rhi.process-qualification.v1");
+ assert_eq!(contract["step"], 215);
+ assert_eq!(contract["service"], "rhi");
+ assert_eq!(contract["binary"], "rhi");
+ assert_eq!(contract["bounds"]["parallel_inspection_processes"], 8);
+ assert_eq!(contract["bounds"]["soak_iterations"], 32);
+ assert_eq!(
+ contract["component_qualification"]["sha256"],
+ "e9c782185a4a2b7512193a3cd237008026193ba3f8bb49fab1c70039a2f35bca"
+ );
+ assert_eq!(contract["invariants"]["actual_executable_required"], true);
+ assert_eq!(
+ contract["invariants"]["production_failpoint_surface"],
+ false
+ );
+ assert_eq!(contract["invariants"]["test_environment_selector"], false);
+ for name in contract["actual_process_corpus"]
+ .as_array()
+ .expect("actual process corpus")
+ {
+ let name = name.as_str().expect("process test name");
+ assert!(
+ PROCESS_QUALIFICATION_TESTS.contains(&format!("fn {name}(")),
+ "missing actual-process test {name}"
+ );
+ }
+ for forbidden in ["RHI_TEST_", "RADROOTS_TEST_", "FAILPOINT"] {
+ assert!(!MAIN.contains(forbidden));
+ assert!(!RUNTIME_GRAPH.contains(forbidden));
+ }
+ assert!(!PROCESS_QUALIFICATION_TESTS.contains("std::env::var"));
+}
+
+#[test]
fn reconciliation_attempts_are_exact_bounded_and_effect_free() {
let contract: serde_json::Value = serde_json::from_str(RECONCILIATION_ATTEMPT_CONTRACT)
.expect("reconciliation-attempt contract");
diff --git a/tests/services_hardening_process.rs b/tests/services_hardening_process.rs
@@ -2,12 +2,13 @@
#![cfg(any(target_os = "linux", target_os = "macos"))]
use std::{
+ collections::BTreeSet,
fs,
io::{Read as _, Write as _},
net::{TcpListener, TcpStream},
os::unix::fs::PermissionsExt as _,
path::PathBuf,
- process::{Child, Command, Output, Stdio},
+ process::{Child, Command, ExitStatus, Output, Stdio},
sync::{
Arc,
atomic::{AtomicBool, Ordering},
@@ -25,8 +26,24 @@ use sha2::{Digest as _, Sha256};
use tungstenite::{Error as WebSocketError, Message, accept};
const CONFIG_EXAMPLE: &str = include_str!("../contracts/services_hardening/config.v1.example.toml");
+const PROCESS_QUALIFICATION_CONTRACT: &str =
+ include_str!("../contracts/services_hardening/process_qualification.v1.json");
+const FAILURE_QUALIFICATION_CONTRACT: &[u8] =
+ include_bytes!("../contracts/services_hardening/failure_qualification.v1.json");
+const SOURCE_LOCK: &str = include_str!("../radroots.service.source-lock.v2.toml");
const PROCESS_DEADLINE: Duration = Duration::from_secs(30);
+const POLL_INTERVAL: Duration = Duration::from_millis(2);
+const CONNECT_DEADLINE_MILLISECONDS: u64 = 5_000;
+const RELAY_HANDSHAKE_TIMEOUT: Duration = Duration::from_millis(5_000);
const RELAY_IO_TIMEOUT: Duration = Duration::from_millis(100);
+const PARALLEL_INSPECTIONS: usize = 8;
+const SOAK_ITERATIONS: usize = 32;
+const MAXIMUM_STDOUT_BYTES: usize = 1_048_576;
+const MAXIMUM_STDERR_BYTES: usize = 8_192;
+#[cfg(target_os = "linux")]
+const PROCESS_TEMPORARY_ROOT: &str = "/tmp";
+#[cfg(target_os = "macos")]
+const PROCESS_TEMPORARY_ROOT: &str = "/private/tmp";
struct RelayHarness {
address: std::net::SocketAddr,
@@ -85,15 +102,23 @@ impl Drop for RelayHarness {
fn relay_session(stream: TcpStream, stop: &AtomicBool) {
stream
- .set_read_timeout(Some(RELAY_IO_TIMEOUT))
- .expect("relay read timeout");
+ .set_read_timeout(Some(RELAY_HANDSHAKE_TIMEOUT))
+ .expect("relay handshake read timeout");
stream
- .set_write_timeout(Some(RELAY_IO_TIMEOUT))
- .expect("relay write timeout");
+ .set_write_timeout(Some(RELAY_HANDSHAKE_TIMEOUT))
+ .expect("relay handshake write timeout");
let mut websocket = match accept(stream) {
Ok(websocket) => websocket,
Err(_) => return,
};
+ websocket
+ .get_mut()
+ .set_read_timeout(Some(RELAY_IO_TIMEOUT))
+ .expect("relay read timeout");
+ websocket
+ .get_mut()
+ .set_write_timeout(Some(RELAY_IO_TIMEOUT))
+ .expect("relay write timeout");
while !stop.load(Ordering::SeqCst) {
let message = match websocket.read() {
Ok(message) => message,
@@ -161,7 +186,7 @@ impl ProcessFixture {
fn new() -> Self {
let root = tempfile::Builder::new()
.prefix("rhi-")
- .tempdir_in("/private/tmp")
+ .tempdir_in(PROCESS_TEMPORARY_ROOT)
.expect("short repo-local root");
fs::set_permissions(root.path(), fs::Permissions::from_mode(0o700))
.expect("secure repo-local root");
@@ -210,41 +235,129 @@ impl ProcessFixture {
}
fn run(&self, command: &[&str]) -> Output {
- wait_bounded(self.command(command).spawn().expect("RHI process"))
+ BoundedProcess::spawn(self.command(command)).wait()
}
fn run_with_stdin(&self, command: &[&str], bytes: &[u8]) -> Output {
let mut process = self.command(command);
process.stdin(Stdio::piped());
- let mut child = process.spawn().expect("RHI process");
+ let mut child = BoundedProcess::spawn(process);
child
+ .child
.stdin
.take()
.expect("process stdin")
.write_all(bytes)
.expect("bounded stdin");
- wait_bounded(child)
+ child.wait()
}
}
-fn wait_bounded(mut child: Child) -> Output {
- let deadline = Instant::now() + PROCESS_DEADLINE;
- loop {
- if child.try_wait().expect("poll RHI process").is_some() {
- let output = child.wait_with_output().expect("collect RHI process");
- assert!(output.stdout.len() <= 1_048_576);
- assert!(output.stderr.len() <= 8_192);
- return output;
+struct BoundedProcess {
+ child: Child,
+ stdout_reader: Option<thread::JoinHandle<Vec<u8>>>,
+ stderr_reader: Option<thread::JoinHandle<Vec<u8>>>,
+ completed: bool,
+}
+
+impl BoundedProcess {
+ fn spawn(mut command: Command) -> Self {
+ let child = command.spawn().expect("RHI process");
+ Self::from_child(child)
+ }
+
+ fn from_child(mut child: Child) -> Self {
+ let stdout = child.stdout.take().expect("captured process stdout");
+ let stderr = child.stderr.take().expect("captured process stderr");
+ Self {
+ child,
+ stdout_reader: Some(read_bounded(stdout, MAXIMUM_STDOUT_BYTES)),
+ stderr_reader: Some(read_bounded(stderr, MAXIMUM_STDERR_BYTES)),
+ completed: false,
}
- if Instant::now() >= deadline {
- let _ = child.kill();
- let output = child.wait_with_output().expect("reap RHI process");
- panic!("RHI process exceeded deadline: {:?}", output.stderr);
+ }
+
+ fn id(&self) -> u32 {
+ self.child.id()
+ }
+
+ fn try_wait(&mut self) -> Option<ExitStatus> {
+ self.child.try_wait().expect("poll RHI process")
+ }
+
+ fn collect(&mut self, status: ExitStatus) -> Output {
+ self.completed = true;
+ let stdout = self
+ .stdout_reader
+ .take()
+ .expect("stdout reader available")
+ .join()
+ .expect("join stdout reader");
+ let stderr = self
+ .stderr_reader
+ .take()
+ .expect("stderr reader available")
+ .join()
+ .expect("join stderr reader");
+ assert!(stdout.len() <= MAXIMUM_STDOUT_BYTES);
+ assert!(stderr.len() <= MAXIMUM_STDERR_BYTES);
+ Output {
+ status,
+ stdout,
+ stderr,
+ }
+ }
+
+ fn wait(mut self) -> Output {
+ let deadline = Instant::now() + PROCESS_DEADLINE;
+ loop {
+ if let Some(status) = self.try_wait() {
+ return self.collect(status);
+ }
+ if Instant::now() >= deadline {
+ let _ = self.child.kill();
+ let status = self.child.wait().expect("reap RHI process");
+ let output = self.collect(status);
+ panic!("RHI process exceeded deadline: {:?}", output.stderr);
+ }
+ thread::sleep(POLL_INTERVAL);
}
- thread::sleep(Duration::from_millis(2));
}
}
+impl Drop for BoundedProcess {
+ fn drop(&mut self) {
+ if !self.completed {
+ let _ = self.child.kill();
+ let _ = self.child.wait();
+ }
+ if let Some(reader) = self.stdout_reader.take() {
+ let _ = reader.join();
+ }
+ if let Some(reader) = self.stderr_reader.take() {
+ let _ = reader.join();
+ }
+ }
+}
+
+fn read_bounded(
+ reader: impl std::io::Read + Send + 'static,
+ maximum_bytes: usize,
+) -> thread::JoinHandle<Vec<u8>> {
+ thread::spawn(move || {
+ let limit = u64::try_from(maximum_bytes)
+ .expect("output bound fits u64")
+ .checked_add(1)
+ .expect("output bound plus sentinel fits u64");
+ let mut output = Vec::with_capacity(maximum_bytes.min(8_192));
+ reader
+ .take(limit)
+ .read_to_end(&mut output)
+ .expect("read bounded process output");
+ output
+ })
+}
+
fn identity_secret() -> [u8; 32] {
let mut candidate: [u8; 32] =
Sha256::digest(b"radroots.rhi.step-213.process-identity.v1").into();
@@ -283,7 +396,10 @@ fn configuration(
.replace(&"2".repeat(64), expected_public_key)
.replace("wss://relay.example.com/", primary_relay)
.replace("wss://relay-secondary.example.com/", secondary_relay)
- .replace("connect_deadline_ms = 10000", "connect_deadline_ms = 100")
+ .replace(
+ "connect_deadline_ms = 10000",
+ &format!("connect_deadline_ms = {CONNECT_DEADLINE_MILLISECONDS}"),
+ )
.replace("request_deadline_ms = 15000", "request_deadline_ms = 1000")
}
@@ -346,19 +462,16 @@ fn bootstrap(fixture: &ProcessFixture, configuration: &str, secret: [u8; 32]) ->
expected_public_key
}
-fn wait_for_live_status(fixture: &ProcessFixture, daemon: &mut Child) -> Output {
+fn wait_for_live_status(fixture: &ProcessFixture, daemon: &mut BoundedProcess) -> Output {
let deadline = Instant::now() + PROCESS_DEADLINE;
let mut last_diagnostic = Vec::new();
loop {
- if let Some(status) = daemon.try_wait().expect("poll RHI daemon") {
- let mut stderr = Vec::new();
- daemon
- .stderr
- .take()
- .expect("RHI daemon stderr")
- .read_to_end(&mut stderr)
- .expect("read RHI daemon stderr");
- panic!("RHI daemon exited before admin became ready: {status}; stderr={stderr:?}");
+ if let Some(status) = daemon.try_wait() {
+ let output = daemon.collect(status);
+ panic!(
+ "RHI daemon exited before admin became ready: {status}; stderr={:?}",
+ output.stderr
+ );
}
if fixture.runtime.artifacts().admin_socket().exists() {
let status = fixture.run(&["status"]);
@@ -373,10 +486,175 @@ fn wait_for_live_status(fixture: &ProcessFixture, daemon: &mut Child) -> Output
fixture.runtime.artifacts().admin_socket().exists()
);
}
- thread::sleep(Duration::from_millis(2));
+ thread::sleep(POLL_INTERVAL);
}
}
+fn interrupt_and_wait(daemon: BoundedProcess) -> Output {
+ let signal = Command::new("/bin/kill")
+ .arg("-INT")
+ .arg(daemon.id().to_string())
+ .status()
+ .expect("send interrupt");
+ assert!(signal.success());
+ daemon.wait()
+}
+
+#[test]
+fn process_qualification_contract_freezes_the_exact_wave_closure() {
+ let contract: serde_json::Value =
+ serde_json::from_str(PROCESS_QUALIFICATION_CONTRACT).expect("process contract");
+ assert_eq!(
+ contract
+ .as_object()
+ .expect("process contract object")
+ .keys()
+ .map(String::as_str)
+ .collect::<BTreeSet<_>>(),
+ BTreeSet::from([
+ "actual_process_corpus",
+ "binary",
+ "bounds",
+ "component_qualification",
+ "contract_version",
+ "deferred",
+ "invariants",
+ "schema",
+ "service",
+ "source_lock",
+ "step",
+ ])
+ );
+ assert_eq!(contract["schema"], "radroots.rhi.process-qualification.v1");
+ assert_eq!(contract["contract_version"], 1);
+ assert_eq!(contract["step"], 215);
+ assert_eq!(contract["service"], "rhi");
+ assert_eq!(contract["binary"], "rhi");
+ assert_eq!(
+ contract["source_lock"],
+ serde_json::json!({
+ "schema": "radroots.service.source-lock.v2",
+ "lib_revision": "21b11e7a5120ea949f7ad0838c746873fc73aac2"
+ })
+ );
+ assert_eq!(
+ contract["component_qualification"],
+ serde_json::json!({
+ "schema": "radroots.rhi.failure-qualification.v1",
+ "step": 214,
+ "sha256": "e9c782185a4a2b7512193a3cd237008026193ba3f8bb49fab1c70039a2f35bca"
+ })
+ );
+ assert_eq!(
+ contract["bounds"],
+ serde_json::json!({
+ "process_deadline_ms": 30_000,
+ "poll_interval_ms": 2,
+ "connect_deadline_ms": 5_000,
+ "relay_handshake_timeout_ms": 5_000,
+ "relay_io_timeout_ms": 100,
+ "parallel_inspection_processes": 8,
+ "soak_iterations": 32,
+ "maximum_stdout_bytes": 1_048_576,
+ "maximum_stderr_bytes": 8_192
+ })
+ );
+ assert_eq!(
+ PROCESS_DEADLINE,
+ Duration::from_millis(
+ contract["bounds"]["process_deadline_ms"]
+ .as_u64()
+ .expect("process deadline"),
+ )
+ );
+ assert_eq!(
+ POLL_INTERVAL,
+ Duration::from_millis(
+ contract["bounds"]["poll_interval_ms"]
+ .as_u64()
+ .expect("poll interval"),
+ )
+ );
+ assert_eq!(
+ RELAY_HANDSHAKE_TIMEOUT,
+ Duration::from_millis(
+ contract["bounds"]["relay_handshake_timeout_ms"]
+ .as_u64()
+ .expect("relay handshake timeout"),
+ )
+ );
+ assert_eq!(
+ RELAY_IO_TIMEOUT,
+ Duration::from_millis(
+ contract["bounds"]["relay_io_timeout_ms"]
+ .as_u64()
+ .expect("relay timeout"),
+ )
+ );
+ assert_eq!(
+ CONNECT_DEADLINE_MILLISECONDS,
+ contract["bounds"]["connect_deadline_ms"]
+ );
+ assert_eq!(
+ u64::try_from(PARALLEL_INSPECTIONS).expect("inspection bound"),
+ contract["bounds"]["parallel_inspection_processes"]
+ );
+ assert_eq!(
+ u64::try_from(SOAK_ITERATIONS).expect("soak bound"),
+ contract["bounds"]["soak_iterations"]
+ );
+ assert_eq!(
+ u64::try_from(MAXIMUM_STDOUT_BYTES).expect("stdout bound"),
+ contract["bounds"]["maximum_stdout_bytes"]
+ );
+ assert_eq!(
+ u64::try_from(MAXIMUM_STDERR_BYTES).expect("stderr bound"),
+ contract["bounds"]["maximum_stderr_bytes"]
+ );
+ assert_eq!(
+ contract["actual_process_corpus"],
+ serde_json::json!([
+ "actual_binary_executes_offline_bootstrap_and_reaches_real_runtime_dependency_boundary",
+ "actual_binary_runs_the_task_graph_serves_admin_and_shuts_down_on_interrupt",
+ "actual_binary_is_bounded_under_parallel_inspection_and_reopen_soak"
+ ])
+ );
+ assert_eq!(
+ contract["invariants"],
+ serde_json::json!({
+ "actual_executable_required": true,
+ "loopback_relay_only": true,
+ "production_failpoint_surface": false,
+ "test_environment_selector": false,
+ "detached_test_worker": false,
+ "parallel_inspection_is_read_only": true,
+ "every_daemon_is_interrupted_joined_and_reaped": true,
+ "admin_socket_absent_after_shutdown": true,
+ "state_verifies_after_every_reopen": true,
+ "captured_output_bounded": true,
+ "diagnostics_path_secret_free": true
+ })
+ );
+ assert_eq!(
+ contract["deferred"],
+ serde_json::json!([
+ "native_release_artifacts_step_216",
+ "rcld_promotion_step_217",
+ "parent_pin_alignment_step_217",
+ "nix",
+ "oci",
+ "signing",
+ "publication",
+ "deployment"
+ ])
+ );
+ assert_eq!(
+ lower_hex(&Sha256::digest(FAILURE_QUALIFICATION_CONTRACT)),
+ contract["component_qualification"]["sha256"]
+ );
+ assert!(SOURCE_LOCK.contains("revision = \"21b11e7a5120ea949f7ad0838c746873fc73aac2\""));
+}
+
#[test]
fn actual_binary_executes_offline_bootstrap_and_reaches_real_runtime_dependency_boundary() {
let fixture = ProcessFixture::new();
@@ -418,7 +696,7 @@ fn actual_binary_runs_the_task_graph_serves_admin_and_shuts_down_on_interrupt()
);
bootstrap(&fixture, &configuration, secret);
- let mut daemon = fixture.command(&["run"]).spawn().expect("RHI daemon");
+ let mut daemon = BoundedProcess::spawn(fixture.command(&["run"]));
let status = wait_for_live_status(&fixture, &mut daemon);
assert_success(&status);
let status_value: serde_json::Value =
@@ -428,14 +706,46 @@ fn actual_binary_runs_the_task_graph_serves_admin_and_shuts_down_on_interrupt()
assert_eq!(status_value["ready"], true);
assert_eq!(status_value["persistence"]["schema_version"], 11);
- let signal = Command::new("/bin/kill")
- .arg("-INT")
- .arg(daemon.id().to_string())
- .status()
- .expect("send interrupt");
- assert!(signal.success());
- let shutdown = wait_bounded(daemon);
+ let shutdown = interrupt_and_wait(daemon);
assert_success(&shutdown);
assert!(shutdown.stdout.is_empty());
assert!(!fixture.runtime.artifacts().admin_socket().exists());
}
+
+#[test]
+fn actual_binary_is_bounded_under_parallel_inspection_and_reopen_soak() {
+ let fixture = ProcessFixture::new();
+ let secret = identity_secret();
+ let expected_public_key = Keys::new(SecretKey::from_slice(&secret).expect("secret"))
+ .public_key()
+ .to_hex();
+ let configuration = configuration(
+ &fixture,
+ &expected_public_key,
+ "ws://127.0.0.1:9/",
+ "ws://127.0.0.1:10/",
+ );
+ bootstrap(&fixture, &configuration, secret);
+
+ let inspections = (0..PARALLEL_INSPECTIONS)
+ .map(|_| BoundedProcess::spawn(fixture.command(&["config", "validate"])))
+ .collect::<Vec<_>>();
+ for inspection in inspections {
+ assert_success(&inspection.wait());
+ }
+
+ for _ in 0..SOAK_ITERATIONS {
+ assert_success(&fixture.run(&["state", "verify"]));
+ }
+ assert!(!fixture.runtime.artifacts().admin_socket().exists());
+}
+
+fn lower_hex(bytes: &[u8]) -> String {
+ const DIGITS: &[u8; 16] = b"0123456789abcdef";
+ let mut output = String::with_capacity(bytes.len() * 2);
+ for byte in bytes {
+ output.push(char::from(DIGITS[usize::from(byte >> 4)]));
+ output.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
+ }
+ output
+}