AprilNEA/OpenLogi · error
cassette audit has multiple sanitized receiver identities
Error message
cassette audit has multiple sanitized receiver identities
What it means
During cassette replay, the audit must supply a single sanitized (synthetic) receiver identity for a given replacement kind. `unique_replacement` filters the audit's replacements by kind and errors both when none is found and — this error — when more than one is found. It throws because a cassette audit that contains multiple sanitized identities for one kind is ambiguous: the replay cannot decide which synthetic value to substitute for that kind.
Solutions
- Open the cassette's audit and remove or merge duplicate sanitized receiver entries so exactly one replacement exists per kind
- Re-record the fixture in a session that uses only one receiver so the audit sanitizes a single identity
- If multiple identities are legitimately needed, change the code to select a specific replacement (by id) instead of requiring uniqueness
Example fix
// before: cassette audit has two entries of kind Receiver
{"replacements":[{"kind":"receiver","syntheticValue":"SYN-RCV-1"},{"kind":"receiver","syntheticValue":"SYN-RCV-2"}]}
// after: keep one per kind
{"replacements":[{"kind":"receiver","syntheticValue":"SYN-RCV-1"}]} Defensive patterns
Strategy: validation
Validate before calling
let kinds: Vec<_> = audit.replacements.iter().map(|r| r.kind).collect();
let dupes: Vec<_> = kinds.iter().filter(|k| kinds.iter().filter(|k2| k2 == *k).count() > 1).collect();
if !dupes.is_empty() { /* fix the cassette before replay */ } Type guard
fn unique_replacement_of<'a>(audit: &'a Audit, kind: Kind) -> Option<&'a Replacement> {
let mut it = audit.replacements.iter().filter(|r| r.kind == kind);
let first = it.next()?;
it.next().is_none().then_some(first)
} Prevention
- Record each fixture session against a single physical receiver
- Never hand-merge audit JSON from multiple recordings
- Validate cassette audits with a linter before committing them
When it happens
Trigger: Calling `unique_replacement` (via `derive_replay_route`) on a cassette whose audit recorded more than one replacement of the same `kind` — e.g. a recording session that saw two different physical receivers and sanitized them into two distinct synthetic identities.
Common situations: Merging or hand-editing cassette JSON and accidentally duplicating a receiver entry; recording a fixture while switching between receivers (Bolt vs Unifying vs a second dongle) so the audit accumulates multiple sanitized identities for one kind.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- --name must not be empty
- --channel must not be empty
- no sanitized channel candidate reproduced the captured…
- sanitized channel candidates reproduced the capture; target…
- sanitized Bolt receiver identity is not 16-byte ASCII
AI-assisted analysis of AprilNEA/OpenLogi@e846e6f4b4 (2026-09-13).
Data as JSON: /api/errors/3397bceabdb1157a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/openlogi-cli/src/cmd/fixture/record_case/replay.rs:160
product_id: *product_id,
}),
DeviceRoute::RawHid { .. } => {
bail!("raw HID routes are outside HID++ fixture case capture")
}
}
}
fn unique_replacement(audit: &HidCassetteAudit, kind: SanitizedIdentityKind) -> Result<&[u8]> {
let mut replacements = audit
.replacements
.iter()
.filter(|replacement| replacement.kind == kind);
let value = replacements
.next()
.map(|replacement| replacement.synthetic_value.as_slice())
.ok_or_else(|| anyhow::anyhow!("cassette audit has no sanitized receiver identity"))?;
if replacements.next().is_some() {
bail!("cassette audit has multiple sanitized receiver identities");
}
Ok(value)
}
#[cfg(test)]
mod tests {
use openlogi_device::write::FeatureEntry;
use openlogi_fixture::{
CassetteExchange, FIXTURE_SCHEMA_VERSION, HidCassette, ReportSupport, RequestMatch,
};
use openlogi_hid::FeatureType;
use openlogi_hid::recording::IdentityReplacement;
use super::*;
fn target(route: DeviceRoute, product_id: u16) -> TargetCandidate {
TargetCandidate {
route,View on GitHub (pinned to e846e6f4b4)