feat(host): build desktop audio exclusion foundation
This commit is contained in:
+83
-1
@@ -16,7 +16,7 @@ use std::collections::BTreeSet;
|
||||
|
||||
use super::fixture::{Graph, NodeRef, PULSE_PID, app};
|
||||
use super::owner::{OwnerCtx, OwnerKey, strongest_shared_key};
|
||||
use super::snapshot::{MediaRole, NodeProps, PortDirection, Serial};
|
||||
use super::snapshot::{GlobalId, MediaRole, NodeProps, PortDirection, Serial};
|
||||
use super::{Decisions, Eligibility, ExclusionCtx, ObjectRef, Reason, StickyState, evaluate};
|
||||
|
||||
fn ctx() -> ExclusionCtx {
|
||||
@@ -176,6 +176,88 @@ fn peerspeak_tagged_nodes_are_excluded_and_plain_apps_are_not() {
|
||||
assert_tainted(&decisions, sink, "tainted-upstream");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────
|
||||
// Hidden hardware playback-to-capture paths — same Device only
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn same_hardware_device_closes_an_unpublished_playback_to_capture_hop() {
|
||||
let mut graph = Graph::new();
|
||||
let card = GlobalId(700);
|
||||
let sink = graph.device_node_on("card-playback", MediaRole::Sink, card);
|
||||
let source = graph.device_node_on("card-capture", MediaRole::Source, card);
|
||||
let call = graph.peerspeak_node("peerspeak-call", 7);
|
||||
let music = graph.app_node("music", MediaRole::StreamOutput, 8);
|
||||
let recorder_in = graph.app_node("recorder-in", MediaRole::StreamInput, 9);
|
||||
let recorder_out = graph.app_node("recorder-out", MediaRole::StreamOutput, 9);
|
||||
|
||||
graph.link(call, sink);
|
||||
graph.link(music, sink);
|
||||
// There is deliberately no sink → source Link: the hardware bridge is
|
||||
// the route being modeled.
|
||||
graph.link(source, recorder_in);
|
||||
|
||||
let decisions = run(&graph, &ctx());
|
||||
assert_partition(
|
||||
&decisions,
|
||||
&[("music", music)],
|
||||
&[
|
||||
("call", call, "peerspeak-owned"),
|
||||
("recorder-out", recorder_out, "tainted-owner-bridge"),
|
||||
],
|
||||
);
|
||||
assert_tainted(&decisions, sink, "tainted-upstream");
|
||||
assert_tainted(&decisions, source, "tainted-upstream");
|
||||
assert_tainted(&decisions, recorder_in, "tainted-upstream");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn different_hardware_devices_do_not_invent_a_capture_path() {
|
||||
let mut graph = Graph::new();
|
||||
let sink = graph.device_node_on("speaker", MediaRole::Sink, GlobalId(700));
|
||||
let source = graph.device_node_on("usb-mic", MediaRole::Source, GlobalId(701));
|
||||
let call = graph.peerspeak_node("peerspeak-call", 7);
|
||||
let recorder_in = graph.app_node("recorder-in", MediaRole::StreamInput, 9);
|
||||
let recorder_out = graph.app_node("recorder-out", MediaRole::StreamOutput, 9);
|
||||
|
||||
graph.link(call, sink);
|
||||
graph.link(source, recorder_in);
|
||||
|
||||
let decisions = run(&graph, &ctx());
|
||||
assert_partition(
|
||||
&decisions,
|
||||
&[("recorder-out", recorder_out)],
|
||||
&[("call", call, "peerspeak-owned")],
|
||||
);
|
||||
assert_untainted(&decisions, source);
|
||||
assert_untainted(&decisions, recorder_in);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_device_microphone_use_is_intentionally_over_excluded() {
|
||||
// The hardware's private mixer/firmware path is not observable in the
|
||||
// PipeWire graph. If an app captures the same device receiving the call,
|
||||
// v1 cannot prove that its capture is clean, so its playback is excluded.
|
||||
let mut graph = Graph::new();
|
||||
let card = GlobalId(700);
|
||||
let sink = graph.device_node_on("headset-output", MediaRole::Sink, card);
|
||||
let mic = graph.device_node_on("headset-mic", MediaRole::Source, card);
|
||||
let call = graph.peerspeak_node("peerspeak-call", 7);
|
||||
let firefox_in = graph.app_node("firefox-mic", MediaRole::StreamInput, 11_114);
|
||||
let firefox_out = graph.app_node("firefox-audio", MediaRole::StreamOutput, 11_114);
|
||||
graph.link(call, sink);
|
||||
graph.link(mic, firefox_in);
|
||||
|
||||
assert_partition(
|
||||
&run(&graph, &ctx()),
|
||||
&[],
|
||||
&[
|
||||
("call", call, "peerspeak-owned"),
|
||||
("firefox-out", firefox_out, "tainted-owner-bridge"),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Each ownership carrier must work **alone** (v3.5 §5.1).
|
||||
///
|
||||
/// ⚠️ The phase-3r lesson, applied deliberately: a gate that asserts a value
|
||||
|
||||
Reference in New Issue
Block a user