diff --git a/conductor-core/src/identity.rs b/conductor-core/src/identity.rs index 974ef0d3..91586a1d 100644 --- a/conductor-core/src/identity.rs +++ b/conductor-core/src/identity.rs @@ -206,9 +206,9 @@ impl DeviceMatcher { /// against the provided identity. Returns false if identity is absent. /// For all other matchers, delegates to `matches(port_name)`. /// - /// Note: `PortResolver::resolve()` calls `matches_with_usb()` (not this method). - /// SysEx Identity metadata is not yet passed to the resolver. `SysExIdentity` - /// matchers will not affect binding resolution until the resolver is updated. + /// Note: `PortResolver::resolve()` dispatches by matcher type: `SysExIdentity` + /// matchers use this method with `port.sysex_identity` from `PortInfo`; all other + /// matchers use `matches_with_usb()`. Active in resolution when identity data exists. pub fn matches_with_sysex( &self, port_name: &str, diff --git a/conductor-core/src/resolver.rs b/conductor-core/src/resolver.rs index f2854be5..d536bce6 100644 --- a/conductor-core/src/resolver.rs +++ b/conductor-core/src/resolver.rs @@ -6,9 +6,13 @@ //! Pure logic: `(Vec, Vec) → Vec` use crate::config::DeviceIdentityConfig; +use crate::device_intelligence::sysex_identity::SysExIdentity; use crate::identity::DeviceId; /// Information about an available MIDI port. +/// +/// Prefer constructors (`PortInfo::new()`, `new_with_usb()`) over struct literals +/// to avoid breakage when new metadata fields are added. #[derive(Debug, Clone)] pub struct PortInfo { pub name: String, @@ -17,16 +21,19 @@ pub struct PortInfo { pub vendor_id: Option, /// USB Product ID (populated when available from platform APIs) pub product_id: Option, + /// SysEx Identity Reply data (populated by probing, when available) + pub sysex_identity: Option, } impl PortInfo { - /// Construct a PortInfo when USB IDs are unknown or unavailable. + /// Construct a PortInfo when no metadata is available. pub fn new(name: impl Into, index: usize) -> Self { Self { name: name.into(), index, vendor_id: None, product_id: None, + sysex_identity: None, } } @@ -42,6 +49,7 @@ impl PortInfo { index, vendor_id: Some(vendor_id), product_id: Some(product_id), + sysex_identity: None, } } } @@ -92,7 +100,18 @@ impl PortResolver { } // Check all matchers on this identity; take highest specificity match for matcher in &identity.matchers { - if matcher.matches_with_usb(&port.name, port.vendor_id, port.product_id) { + // Use the appropriate matching method based on matcher type + // to avoid redundant name checks (matches_with_usb handles + // name-based and USB matchers; matches_with_sysex only for SysEx) + let matched = if matches!( + matcher, + crate::identity::DeviceMatcher::SysExIdentity { .. } + ) { + matcher.matches_with_sysex(&port.name, port.sysex_identity.as_ref()) + } else { + matcher.matches_with_usb(&port.name, port.vendor_id, port.product_id) + }; + if matched { let specificity = matcher.specificity(); if best_match .as_ref() diff --git a/conductor-core/tests/resolver_test.rs b/conductor-core/tests/resolver_test.rs index c767a564..1dc3d969 100644 --- a/conductor-core/tests/resolver_test.rs +++ b/conductor-core/tests/resolver_test.rs @@ -349,3 +349,61 @@ fn test_name_matcher_still_works_with_usb_fields() { matches!(&results[0], BindingResult::Bound { device_id, .. } if device_id.as_str() == "pads") ); } + +// #752: SysExIdentity matcher works in PortResolver when identity data provided +#[test] +fn test_sysex_identity_matches_with_metadata() { + use conductor_core::device_intelligence::sysex_identity::SysExIdentity; + + let mut port = PortInfo::new("Generic MIDI Port", 0); + port.sysex_identity = Some(SysExIdentity { + manufacturer_id: vec![0x42], // KORG + family: 0x0034, + model: 0x0001, + version: [1, 0, 0, 0], + }); + let ports = vec![port]; + let identities = vec![DeviceIdentityConfig { + alias: "korg".to_string(), + matchers: vec![DeviceMatcher::SysExIdentity { + manufacturer_id: vec![0x42], + family: Some(0x0034), + model: None, + }], + description: None, + enabled: true, + input: None, + output: None, + protocol: None, + channels: vec![], + }]; + + let results = PortResolver::resolve(&ports, &identities); + assert_eq!(results.len(), 1); + assert!( + matches!(&results[0], BindingResult::Bound { device_id, .. } if device_id.as_str() == "korg") + ); +} + +#[test] +fn test_sysex_identity_no_match_without_metadata() { + let ports = vec![PortInfo::new("Generic MIDI Port", 0)]; + let identities = vec![DeviceIdentityConfig { + alias: "korg".to_string(), + matchers: vec![DeviceMatcher::SysExIdentity { + manufacturer_id: vec![0x42], + family: None, + model: None, + }], + description: None, + enabled: true, + input: None, + output: None, + protocol: None, + channels: vec![], + }]; + + let results = PortResolver::resolve(&ports, &identities); + assert_eq!(results.len(), 1); + assert!(matches!(&results[0], BindingResult::Unbound { .. })); +}