host/taint: match peerspeak's second ownership carrier
The consumer half of phase 1 (plan §5.1, impl plan §3). The engine's tag root becomes a union: `peerspeak.owned` truthy OR `node.name` starting with `peerspeak_owned_`. Round 8 added the second carrier because a node property is invisible to the registry `global` event and recoverable only by binding the node — which is exactly how the phase-5 gate failed — while `node.name` is announced directly. The union lives in `local_root_reason`, not in the adapter. Folding both into the one `peerspeak_owned` bool at the observation boundary would make each carrier untestable alone, which is the phase-3r lesson: a gate asserting a value two sources can satisfy gates neither. The existing `peerspeak_tagged_nodes_…` fixture now carries both carriers, so it would keep passing if either were deleted; two new tests pin them individually, and a third pins that the prefix matches only at the start of a name. Both literals are now named constants — they are a cross-repo wire contract with peerspeak, not local naming — and asserted against tests/fixtures/ownership-tag-contract.txt, committed byte-identical in both repos. That test also runs the fixture's own worked example name through the engine, so the shared file cannot document a value this side does not actually exclude. Five mutations verified: drop either carrier, loosen `starts_with` to `contains`, or rename either constant, and exactly the intended test fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+16
-4
@@ -22,9 +22,17 @@ use super::metrics::{BUCKET_LABELS, Metrics, QUEUE_THRESHOLD_US, Sample};
|
||||
use super::*;
|
||||
use crate::host::aec::AecConfig;
|
||||
use crate::host::observer::{EventKind, Readiness};
|
||||
use crate::host::taint::PEERSPEAK_OWNED_NODE_PREFIX;
|
||||
use crate::host::taint::fixture::{self, Graph, NodeRef};
|
||||
use crate::host::taint::snapshot::{GraphSnapshot, MediaRole};
|
||||
|
||||
/// The `node.name` a [`Graph::peerspeak_node`] fixture produces. Built from
|
||||
/// the same constant the engine matches on, so these audit rows report the
|
||||
/// name shape a live peerspeak node actually has (v3.5 §5.1, carrier 2).
|
||||
fn owned_name(role: &str, pid: u32) -> String {
|
||||
format!("{PEERSPEAK_OWNED_NODE_PREFIX}{role}_{pid}")
|
||||
}
|
||||
|
||||
const AEC_MODULE: u64 = 7;
|
||||
const TIMEOUT: Millis = 5_000;
|
||||
|
||||
@@ -284,13 +292,14 @@ fn a_shut_gate_preserves_the_engines_own_reasons() {
|
||||
.body;
|
||||
let (_, excluded) = partition(&body);
|
||||
|
||||
let playback = owned_name("peerspeak-playback", 200);
|
||||
assert!(!body.fan_out_permitted);
|
||||
assert_eq!(
|
||||
excluded,
|
||||
vec![
|
||||
("music", "aec-validating"),
|
||||
// Tagged, so it keeps the reason that actually applies to it.
|
||||
("peerspeak-playback", "peerspeak-owned"),
|
||||
(playback.as_str(), "peerspeak-owned"),
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -457,11 +466,12 @@ fn row_1_owner_bridge_forwarder_with_an_untainted_control() {
|
||||
.body;
|
||||
let (eligible, excluded) = partition(&body);
|
||||
|
||||
let call_name = owned_name("peerspeak-call", 200);
|
||||
assert_eq!(eligible, vec!["clean-loopback-playback"]);
|
||||
assert_eq!(
|
||||
excluded,
|
||||
vec![
|
||||
("peerspeak-call", "peerspeak-owned"),
|
||||
(call_name.as_str(), "peerspeak-owned"),
|
||||
("tainted-loopback-playback", "tainted-owner-bridge"),
|
||||
]
|
||||
);
|
||||
@@ -494,11 +504,12 @@ fn row_3_one_tainted_module_does_not_taint_the_other() {
|
||||
.body;
|
||||
let (eligible, excluded) = partition(&body);
|
||||
|
||||
let call_name = owned_name("peerspeak-call", 200);
|
||||
assert_eq!(eligible, vec!["module-b-playback"]);
|
||||
assert_eq!(
|
||||
excluded,
|
||||
vec![
|
||||
("peerspeak-call", "peerspeak-owned"),
|
||||
(call_name.as_str(), "peerspeak-owned"),
|
||||
("module-a-playback", "tainted-owner-bridge"),
|
||||
]
|
||||
);
|
||||
@@ -701,7 +712,8 @@ fn the_taint_view_covers_non_candidate_roles() {
|
||||
tainted.contains(&("null-sink", "tainted-upstream")),
|
||||
"the sink is not a candidate but its taint is what explains the row: {tainted:?}"
|
||||
);
|
||||
assert!(tainted.contains(&("peerspeak-call", "peerspeak-owned")));
|
||||
let call_name = owned_name("peerspeak-call", 200);
|
||||
assert!(tainted.contains(&(call_name.as_str(), "peerspeak-owned")));
|
||||
}
|
||||
|
||||
/// A record must serialise to a single line. Newlines inside a JSON Lines
|
||||
|
||||
Reference in New Issue
Block a user