use super::*;
fn paired_slots(probe: &NodeProbe) -> Vec<u8> {
let Some(inventory) = probe.inventory.as_ref() else {
panic!("expected an inventory");
};
inventory.paired.iter().map(|d| d.slot).collect()
}
#[test]
fn bolt_probe_is_complete_when_count_matches_readable_slots() {
let probe = assemble_bolt_probe(
bolt_receiver_info(),
Some(2),
vec![bolt_slot(1), bolt_slot(2)],
);
assert_eq!(
probe.verdict,
ProbeVerdict::Healthy { complete: true },
"a count matching the readable slots is authoritative and complete"
);
assert_eq!(paired_slots(&probe), vec![1, 2], "slots surface in order");
assert_eq!(
probe.outcomes.len(),
2,
"one cache outcome per readable slot"
);
}
#[test]
fn bolt_probe_is_incomplete_when_a_counted_slot_is_unreadable() {
let probe = assemble_bolt_probe(bolt_receiver_info(), Some(2), vec![bolt_slot(1)]);
assert_eq!(
paired_slots(&probe),
vec![1],
"only the readable slot surfaces"
);
assert_eq!(
probe.verdict,
ProbeVerdict::Failed,
"an incomplete Bolt walk is not authoritative"
);
}
#[test]
fn bolt_probe_is_incomplete_when_the_count_register_is_unanswered() {
let probe = assemble_bolt_probe(bolt_receiver_info(), None, vec![bolt_slot(1), bolt_slot(2)]);
assert_eq!(paired_slots(&probe), vec![1, 2]);
assert_eq!(
probe.verdict,
ProbeVerdict::Failed,
"no count register means we couldn't fully check"
);
}
const BOLT_RECEIVER_PID: u16 = 0xc548;
const SILENT_SLOT_UNIT_ID: [u8; 4] = [0xde, 0xad, 0xbe, 0xef];
fn bolt_receiver_with_a_silent_slot(request: &[u8]) -> Option<Vec<u8>> {
let [_, device, sub_id, address, sub_register, ..] = *request else {
return None;
};
if device != 0xff {
return None;
}
let short = |data: [u8; 3]| Some(vec![0x10, 0xff, sub_id, address, data[0], data[1], data[2]]);
let long = |data: &[u8]| {
let mut report = vec![0x11, 0xff, 0x83, address];
report.extend_from_slice(data);
report.resize(20, 0);
Some(report)
};
match (sub_id, address) {
(0x81, 0x00 | 0x02) => short([0x00, 0x01, 0x00]),
(0x80, _) => short([0x00, 0x00, 0x00]),
(0x83, 0xfb) => long(b"0000000012345678"),
(0x83, 0xb5) => match sub_register {
0x51 => long(&[0x51, 0x02, 0x9d, 0xc0, 0xde, 0xad, 0xbe, 0xef]),
0x61 => long(&[
0x61, 0x01, 0x08, b'M', b'X', b' ', b'P', b'r', b'o', b'b', b'e',
]),
_ => Some(vec![0x10, 0xff, 0x8f, 0x83, 0xb5, 0x08, 0x00]),
},
_ => None,
}
}
fn bolt_receiver_node(tag: &str) -> NodeInfo {
let mut info = scripted_node_info(&format!("{tag}-{}", std::process::id()));
info.product_id = BOLT_RECEIVER_PID;
info
}
fn quick_timeouts() -> ProbeTimeouts {
ProbeTimeouts {
register_lock: Duration::from_secs(2),
receiver: Duration::from_millis(900),
direct: Duration::from_millis(900),
arrival_drain: Duration::from_millis(100),
bolt_slot_probe: Duration::from_millis(400),
unifying_slot_probe: Duration::from_millis(400),
unifying_cached_slot_probe: Duration::from_millis(100),
}
}
fn stale_silent_slot_cache() -> (CacheKey, HashMap<CacheKey, Cached>) {
let key = CacheKey::Bolt {
unit_id: SILENT_SLOT_UNIT_ID,
};
let mut entry = cache_entry();
entry.probe = probed(Some(model(SILENT_SLOT_UNIT_ID, Some("SN-1"))), false);
entry.probed_at = Instant::now()
.checked_sub(REFRESH_INTERVAL + Duration::from_nanos(1))
.expect("the process has been up longer than the refresh interval's worth of ticks");
let cache = HashMap::from([(key.clone(), entry)]);
(key, cache)
}
#[tokio::test]
async fn a_receiver_probe_that_waited_for_its_register_phase_keeps_its_whole_io_budget() {
let info = bolt_receiver_node("register-phase-wait");
let timeouts = quick_timeouts();
let lock_held_for = Duration::from_millis(600);
let held = host_lock::try_lock(&host_lock::node_lock_name(&info.id))
.unwrap()
.expect("the test takes the phase first");
let release = tokio::spawn(async move {
tokio::time::sleep(lock_held_for).await;
drop(held);
});
let (raw, _handle) = ScriptedRawHidChannel::with_responder(bolt_receiver_with_a_silent_slot);
let channel = scripted_channel(raw.presenting_as(BOLT_RECEIVER_PID)).await;
let (key, cache) = stale_silent_slot_cache();
let pass = PassContext {
cache: &cache,
now: Instant::now(),
subscriptions: None,
timeouts: &timeouts,
};
let started = Instant::now();
let probe = probe_one(info, channel, pass).await;
release.await.unwrap();
let io_floor = timeouts.arrival_drain + timeouts.bolt_slot_probe;
assert!(
lock_held_for + io_floor > timeouts.receiver,
"the test must compose a wait and an I/O floor that together outrun the budget"
);
assert!(
started.elapsed() >= lock_held_for + io_floor,
"the probe waited for the phase and the slot ran to its own budget: {:?}",
started.elapsed()
);
assert_eq!(
probe.verdict,
ProbeVerdict::Healthy { complete: true },
"the wait must not have eaten into the I/O budget"
);
let inventory = probe.inventory.expect("the receiver answered");
assert_eq!(
inventory.receiver.unique_id.as_deref(),
Some("0000000012345678")
);
assert_eq!(inventory.paired.len(), 1);
let device = &inventory.paired[0];
assert_eq!(device.slot, 1);
assert_eq!(device.codename.as_deref(), Some("MX Probe"));
assert!(device.online);
assert_eq!(
device.model_info.as_ref().map(|m| m.unit_id),
Some(SILENT_SLOT_UNIT_ID),
"the timed-out slot fell back to its cached probe"
);
assert!(
matches!(probe.outcomes.as_slice(), [CacheOutcome::Seen(seen)] if *seen == key),
"a timed-out slot keeps its cache entry alive without refreshing it"
);
}
#[tokio::test]
async fn a_receiver_probe_defers_when_the_register_phase_is_held_past_the_wait() {
let info = bolt_receiver_node("register-phase-held");
let timeouts = ProbeTimeouts {
register_lock: Duration::from_millis(100),
..quick_timeouts()
};
let _held = host_lock::try_lock(&host_lock::node_lock_name(&info.id))
.unwrap()
.expect("the test takes the phase first");
let (raw, handle) = ScriptedRawHidChannel::with_responder(bolt_receiver_with_a_silent_slot);
let channel = scripted_channel(raw.presenting_as(BOLT_RECEIVER_PID)).await;
let cache = HashMap::new();
let pass = PassContext {
cache: &cache,
now: Instant::now(),
subscriptions: None,
timeouts: &timeouts,
};
let probe = probe_one(info, channel, pass).await;
assert_eq!(probe.verdict, ProbeVerdict::Deferred);
assert!(probe.inventory.is_none());
assert!(probe.outcomes.is_empty());
assert!(
handle.written_reports().is_empty(),
"a deferred probe must not touch the receiver"
);
}
#[tokio::test]
async fn a_receiver_probe_whose_io_outruns_the_budget_is_failed() {
let info = bolt_receiver_node("io-outruns-budget");
let timeouts = ProbeTimeouts {
receiver: Duration::from_millis(150),
..quick_timeouts()
};
let (raw, _handle) = ScriptedRawHidChannel::with_responder(bolt_receiver_with_a_silent_slot);
let channel = scripted_channel(raw.presenting_as(BOLT_RECEIVER_PID)).await;
let (_, cache) = stale_silent_slot_cache();
let pass = PassContext {
cache: &cache,
now: Instant::now(),
subscriptions: None,
timeouts: &timeouts,
};
let probe = probe_one(info, channel, pass).await;
assert_eq!(probe.verdict, ProbeVerdict::Failed);
assert!(probe.inventory.is_none());
}