{"record":{"id":"3397bceabdb1157a","repo":"AprilNEA/OpenLogi","slug":"cassette-audit-has-multiple-sanitized-receiver-identities","errorCode":null,"errorMessage":"cassette audit has multiple sanitized receiver identities","messagePattern":"cassette audit has multiple sanitized receiver identities","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/openlogi-cli/src/cmd/fixture/record_case/replay.rs","lineNumber":160,"sourceCode":"            product_id: *product_id,\n        }),\n        DeviceRoute::RawHid { .. } => {\n            bail!(\"raw HID routes are outside HID++ fixture case capture\")\n        }\n    }\n}\n\nfn unique_replacement(audit: &HidCassetteAudit, kind: SanitizedIdentityKind) -> Result<&[u8]> {\n    let mut replacements = audit\n        .replacements\n        .iter()\n        .filter(|replacement| replacement.kind == kind);\n    let value = replacements\n        .next()\n        .map(|replacement| replacement.synthetic_value.as_slice())\n        .ok_or_else(|| anyhow::anyhow!(\"cassette audit has no sanitized receiver identity\"))?;\n    if replacements.next().is_some() {\n        bail!(\"cassette audit has multiple sanitized receiver identities\");\n    }\n    Ok(value)\n}\n\n#[cfg(test)]\nmod tests {\n    use openlogi_device::write::FeatureEntry;\n    use openlogi_fixture::{\n        CassetteExchange, FIXTURE_SCHEMA_VERSION, HidCassette, ReportSupport, RequestMatch,\n    };\n    use openlogi_hid::FeatureType;\n    use openlogi_hid::recording::IdentityReplacement;\n\n    use super::*;\n\n    fn target(route: DeviceRoute, product_id: u16) -> TargetCandidate {\n        TargetCandidate {\n            route,","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/AprilNEA/OpenLogi/blob/e846e6f4b4405e33bd6a9aaf949a482ce34cb6d8/crates/openlogi-cli/src/cmd/fixture/record_case/replay.rs#L142-L178","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: cassette audit has two entries of kind Receiver\n{\"replacements\":[{\"kind\":\"receiver\",\"syntheticValue\":\"SYN-RCV-1\"},{\"kind\":\"receiver\",\"syntheticValue\":\"SYN-RCV-2\"}]}\n// after: keep one per kind\n{\"replacements\":[{\"kind\":\"receiver\",\"syntheticValue\":\"SYN-RCV-1\"}]}","handlingStrategy":"validation","validationCode":"let kinds: Vec<_> = audit.replacements.iter().map(|r| r.kind).collect();\nlet dupes: Vec<_> = kinds.iter().filter(|k| kinds.iter().filter(|k2| k2 == *k).count() > 1).collect();\nif !dupes.is_empty() { /* fix the cassette before replay */ }","typeGuard":"fn unique_replacement_of<'a>(audit: &'a Audit, kind: Kind) -> Option<&'a Replacement> {\n    let mut it = audit.replacements.iter().filter(|r| r.kind == kind);\n    let first = it.next()?;\n    it.next().is_none().then_some(first)\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["fixture","cassette","validation"],"backgroundTag":"internal-invariant-violation","analyzedSha":"e846e6f4b4405e33bd6a9aaf949a482ce34cb6d8","analyzedAt":"2026-09-13T03:07:16.451Z","contentChangedAt":"2026-09-13T03:07:16.451Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}