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:
2026-07-25 19:19:39 -04:00
co-authored by Claude Opus 5
parent 0af0124e17
commit 1b01847c66
7 changed files with 216 additions and 6 deletions
+98
View File
@@ -176,6 +176,104 @@ fn peerspeak_tagged_nodes_are_excluded_and_plain_apps_are_not() {
assert_tainted(&decisions, sink, "tainted-upstream");
}
/// Each ownership carrier must work **alone** (v3.5 §5.1).
///
/// ⚠️ The phase-3r lesson, applied deliberately: a gate that asserts a value
/// two sources can satisfy gates neither. `peerspeak_tagged_nodes_…` above
/// uses nodes carrying both carriers, so it would keep passing if either
/// were deleted. These are the rows that actually pin them.
#[test]
fn either_ownership_carrier_alone_taints_the_node() {
let mut graph = Graph::new();
let sink = graph.device_node("hw-sink", MediaRole::Sink);
// Carrier 1: the property, on a node whose name says nothing.
let prop_only = graph.peerspeak_node_prop_only("some-playback-stream", 7);
// Carrier 2: the name prefix, property absent — the F1 case.
let name_only = graph.peerspeak_node_name_only("mpv", 31_284);
let firefox = graph.app_node("firefox", MediaRole::StreamOutput, 11_114);
for node in [prop_only, name_only, firefox] {
graph.link(node, sink);
}
assert_partition(
&run(&graph, &ctx()),
&[("firefox", firefox)],
&[
("prop_only", prop_only, "peerspeak-owned"),
("name_only", name_only, "peerspeak-owned"),
],
);
}
/// The prefix is a **prefix**, not a substring: an unrelated app must not be
/// excluded because the literal appears somewhere in its name. Over-exclusion
/// is the safe direction, but it is still wrong, and the phase-5 gate now
/// asserts exact partitions in both halves.
#[test]
fn the_owned_prefix_matches_only_at_the_start_of_node_name() {
let mut graph = Graph::new();
let sink = graph.device_node("hw-sink", MediaRole::Sink);
let impostor = graph.app_node(
&format!("recorder-of-{}stuff", super::PEERSPEAK_OWNED_NODE_PREFIX),
MediaRole::StreamOutput,
11_114,
);
graph.link(impostor, sink);
assert_partition(&run(&graph, &ctx()), &[("impostor", impostor)], &[]);
}
/// The consumer half of the cross-repo contract test (impl plan §3
/// requirement 2). peerspeak runs the mirror of this against a byte-identical
/// copy of the same file, and asserts the environment a real child `Command`
/// would carry produces exactly these literals.
///
/// This proves the two repos agree on the *literals*. That pixelpass actually
/// *listens* is proven by the two carrier tests above, and against the live
/// graph by the phase 5 dry-run.
#[test]
fn ownership_carriers_match_the_cross_repo_fixture() {
const FIXTURE: &str = include_str!("../../../tests/fixtures/ownership-tag-contract.txt");
let pinned: Vec<(&str, &str)> = FIXTURE
.lines()
.map(str::trim)
.filter(|line| !line.is_empty() && !line.starts_with('#'))
.map(|line| line.split_once('=').expect("fixture line is key=value"))
.collect();
let get = |key: &str| -> &str {
pinned
.iter()
.find(|(k, _)| *k == key)
.unwrap_or_else(|| panic!("fixture has no key {key:?}"))
.1
};
assert_eq!(super::PEERSPEAK_OWNED_PROP, get("prop_key"));
assert_eq!(super::PEERSPEAK_OWNED_NODE_PREFIX, get("node_name_prefix"));
// The value the producer pins must be one this consumer reads as truthy.
// `truthy` lives at the observer boundary; assert the property of it that
// matters here rather than reaching across modules for the function.
let value = get("prop_value");
assert!(
value != "false" && value != "0",
"pinned prop value {value:?} would read as untruthy"
);
// And the fixture's own worked example must be one this engine excludes,
// through carrier 2, exactly as written in the shared file.
let mut graph = Graph::new();
let sink = graph.device_node("hw-sink", MediaRole::Sink);
let example = graph.app_node(get("node_name_example"), MediaRole::StreamOutput, 31_284);
graph.link(example, sink);
assert_partition(
&run(&graph, &ctx()),
&[],
&[("example", example, "peerspeak-owned")],
);
}
#[test]
fn aec_identity_is_exact_equality_and_other_modules_stay_eligible() {
let mut graph = Graph::new();