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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user