host/observer: address Codex phase-3 review (2 P1 + P3s)
Cross-review round: Codex adversarially reviewed my pure core, found two merge-blocking P1s and several P3s. Triaged each for reachability; fixes below, each mutation-verified (revert killed by its intended test). P1 finding 1 — graph_ready was sticky-once-Complete, so a Link added post-enumeration whose endpoints are still binding (an INVISIBLE edge, absent from the snapshot) left graph_ready=true and a candidate could be reported eligible over unseen tainted ancestry. graph_ready is now dynamic: Complete AND no outstanding obligations. Readiness::Complete stays sticky as the epoch marker. New regression test + flipped the old sticky-churn test. P1 finding 2 — snd_aloop presents with an allowlisted ALSA factory and device.api=alsa exactly like a real card but forwards audio through a kernel hop the Link graph cannot see; it was classified session_device=true, dropping its owner keys + backstop (leak). Added alsa.driver_name to DeviceClaim and a NON_TERMINAL_ALSA_DRIVERS denylist under the factory allowlist; adapter now populates it. Negative fixture added. P3 finding 5 — the BlueZ allowlist entries (api.bluez5.pcm.*) were invented; removed them (real names are api.bluez5.media.*). A BT sink now over-excludes (safe) pending a measured fixture. P3 finding 6 — strengthened the timeout test to assert TimedOut stays sticky through later DeviceAdded/sync/tick. Findings 3 (dropped-link unrepresented) and 4 (missed-removal generation ambiguity) documented as accepted low-reachability limitations (links carry object.serial — confirmed by the live gate; registry does not drop removals). Codex confirmed the pulse-PID matrix fails safe and the adapter add() FIFO is lockstep. 120 unit + live gate row 6 green, clippy + fmt clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+95
-18
@@ -37,6 +37,7 @@ fn hw_claim(device_id: u32, api: &str, factory: &str) -> DeviceClaim {
|
||||
device_id: Some(gid(device_id)),
|
||||
device_api: Some(api.to_string()),
|
||||
factory_name: Some(factory.to_string()),
|
||||
alsa_driver_name: Some("snd_hda_intel".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,25 +115,53 @@ fn classify_unresolved_device_withholds() {
|
||||
|
||||
#[test]
|
||||
fn classify_resolved_hardware_pcm_is_session_device() {
|
||||
for factory in [
|
||||
"api.alsa.pcm.sink",
|
||||
"api.alsa.pcm.source",
|
||||
"api.bluez5.pcm.sink",
|
||||
"api.bluez5.pcm.source",
|
||||
] {
|
||||
let api = if factory.contains("bluez5") {
|
||||
"bluez5"
|
||||
} else {
|
||||
"alsa"
|
||||
};
|
||||
// Only the measured ALSA factories are allowlisted (finding 5: the BlueZ
|
||||
// entries were invented and were removed).
|
||||
for factory in ["api.alsa.pcm.sink", "api.alsa.pcm.source"] {
|
||||
assert_eq!(
|
||||
classify(&hw_claim(7, api, factory), true),
|
||||
classify(&hw_claim(7, "alsa", factory), true),
|
||||
Classification::SessionDevice,
|
||||
"factory {factory} should be a session device"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_invented_bluez_factories_are_not_session_devices() {
|
||||
// Finding 5: `api.bluez5.pcm.*` is not a real factory name; whatever it is,
|
||||
// it is not on the measured allowlist, so it fails closed to false
|
||||
// (over-exclusion, safe) rather than being trusted.
|
||||
for factory in ["api.bluez5.pcm.sink", "api.bluez5.pcm.source"] {
|
||||
let claim = DeviceClaim {
|
||||
device_id: Some(gid(7)),
|
||||
device_api: Some("bluez5".to_string()),
|
||||
factory_name: Some(factory.to_string()),
|
||||
alsa_driver_name: None,
|
||||
};
|
||||
assert_eq!(classify(&claim, true), Classification::NotSessionDevice);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_snd_aloop_is_not_a_session_device() {
|
||||
// Finding 2: an ALSA loopback presents with an allowlisted factory and
|
||||
// device.api=alsa exactly like a real card, but forwards audio through a
|
||||
// kernel hop the Link graph cannot see. It must NOT earn session_device.
|
||||
for factory in ["api.alsa.pcm.sink", "api.alsa.pcm.source"] {
|
||||
let claim = DeviceClaim {
|
||||
device_id: Some(gid(7)),
|
||||
device_api: Some("alsa".to_string()),
|
||||
factory_name: Some(factory.to_string()),
|
||||
alsa_driver_name: Some("snd_aloop".to_string()),
|
||||
};
|
||||
assert_eq!(
|
||||
classify(&claim, true),
|
||||
Classification::NotSessionDevice,
|
||||
"snd_aloop {factory} must fail closed"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classify_resolved_but_not_hardware_pcm_fails_closed() {
|
||||
// A null sink, a loopback, and an unknown factory are all forwarders, not
|
||||
@@ -154,6 +183,7 @@ fn classify_missing_device_api_fails_closed() {
|
||||
device_id: Some(gid(7)),
|
||||
device_api: None,
|
||||
factory_name: Some("api.alsa.pcm.sink".to_string()),
|
||||
alsa_driver_name: Some("snd_hda_intel".to_string()),
|
||||
};
|
||||
assert_eq!(classify(&claim, true), Classification::NotSessionDevice);
|
||||
}
|
||||
@@ -488,6 +518,15 @@ fn model_readiness_times_out_fail_closed() {
|
||||
m.apply(RegEvent::Tick { now: 5000 });
|
||||
assert_eq!(m.readiness(), Readiness::TimedOut);
|
||||
assert!(!m.graph_ready(), "timeout fails closed");
|
||||
|
||||
// Finding 6: TimedOut must be sticky. Resolving the obligation, syncing
|
||||
// again, and ticking further must NOT flip it to Complete — a timed-out
|
||||
// observer stays fail-closed for its lifetime.
|
||||
m.apply(RegEvent::DeviceAdded { id: gid(42) });
|
||||
m.apply(RegEvent::ServerSynced);
|
||||
m.apply(RegEvent::Tick { now: 6000 });
|
||||
assert_eq!(m.readiness(), Readiness::TimedOut, "timeout is sticky");
|
||||
assert!(!m.graph_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -504,23 +543,61 @@ fn model_tick_before_deadline_does_not_time_out() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_complete_is_sticky_across_later_churn() {
|
||||
fn model_complete_epoch_is_sticky_but_graph_ready_is_dynamic() {
|
||||
let mut m = model();
|
||||
m.apply(RegEvent::ServerSynced);
|
||||
assert_eq!(m.readiness(), Readiness::Complete);
|
||||
// A node withheld AFTER completion must not un-complete the epoch — post
|
||||
// enumeration, withholding is per-object (the node is simply absent).
|
||||
assert!(m.graph_ready());
|
||||
// A node withheld AFTER completion does not revert the sticky EPOCH...
|
||||
m.apply(device_node(
|
||||
100,
|
||||
50,
|
||||
MediaRole::Sink,
|
||||
hw_claim(42, "alsa", "api.alsa.pcm.sink"),
|
||||
));
|
||||
assert_eq!(m.readiness(), Readiness::Complete);
|
||||
assert!(m.graph_ready());
|
||||
// ...and a late timeout Tick is inert once Complete.
|
||||
assert_eq!(m.readiness(), Readiness::Complete, "epoch stays sticky");
|
||||
// ...but graph_ready DOES drop while the obligation is outstanding
|
||||
// (Codex finding 1: unresolved ancestry ⇒ fail closed, even post-epoch).
|
||||
assert!(
|
||||
!m.graph_ready(),
|
||||
"an outstanding obligation makes decisions unsafe"
|
||||
);
|
||||
// A late timeout Tick is inert once Complete.
|
||||
m.apply(RegEvent::Tick { now: 100_000 });
|
||||
assert_eq!(m.readiness(), Readiness::Complete);
|
||||
// Resolving the obligation restores graph_ready.
|
||||
m.apply(RegEvent::DeviceAdded { id: gid(42) });
|
||||
assert!(m.graph_ready());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn model_pending_link_drops_graph_ready_after_completion() {
|
||||
// Codex finding 1, the leak that mattered: a real Link added post-epoch
|
||||
// whose endpoints are still binding is an INVISIBLE edge (absent from the
|
||||
// snapshot, not dangling). graph_ready must go false until it resolves,
|
||||
// or a candidate can be reported eligible while tainted ancestry it cannot
|
||||
// see already carries call audio.
|
||||
let mut m = model();
|
||||
m.apply(RegEvent::ServerSynced);
|
||||
assert!(m.graph_ready());
|
||||
|
||||
m.apply(RegEvent::LinkAdded {
|
||||
serial: ser(300),
|
||||
id: gid(90),
|
||||
endpoints: None,
|
||||
});
|
||||
assert!(!m.graph_ready(), "an unresolved link must gate decisions");
|
||||
// The snapshot genuinely omits it, which is exactly why graph_ready must
|
||||
// compensate.
|
||||
assert_eq!(m.project().snapshot.links().count(), 0);
|
||||
assert!(!m.project().graph_ready);
|
||||
|
||||
m.apply(RegEvent::LinkEndpointsResolved {
|
||||
serial: ser(300),
|
||||
endpoints: endpoints(50, 55),
|
||||
});
|
||||
assert!(m.graph_ready(), "resolved ⇒ decisions safe again");
|
||||
assert_eq!(m.project().snapshot.links().count(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user