diff --git a/crates/aibridge/src/routing.rs b/crates/aibridge/src/routing.rs index 7c1bb77..dbbce61 100644 --- a/crates/aibridge/src/routing.rs +++ b/crates/aibridge/src/routing.rs @@ -70,15 +70,19 @@ pub struct RouteDecision { } fn glob_match(pattern: &str, name: &str) -> bool { - if pattern.contains('*') { - let parts: Vec<&str> = pattern.split('*').collect(); - if parts.len() == 2 { - return name.starts_with(parts[0]) && name.ends_with(parts[1]); - } else if parts.len() == 1 { - return name.starts_with(parts[0]) || name.ends_with(parts[1]); - } - } - pattern == name + if !pattern.contains('*') { return pattern == name; } + let parts: Vec<&str> = pattern.split('*').collect(); + // Multi-* support: first must be prefix, last must be suffix, each + // interior piece must appear in order. Fixes the iter-9 finding + // where gpt-*-large* silently fell through to an exact-match path. + // Also removes the dead `parts.len() == 1` branch that accessed + // parts[1] and would panic if ever reached (unreachable today + // since split('*') on a string containing '*' always yields ≥2). + if !name.starts_with(parts[0]) || !name.ends_with(parts.last().unwrap()) { return false; } + let mut cursor = parts[0].len(); + parts[1..parts.len() - 1].iter().all(|mid| { + name[cursor..].find(mid).map(|pos| { cursor += pos + mid.len(); true }).unwrap_or(false) + }) } impl Default for RoutingRule { @@ -90,4 +94,26 @@ impl Default for RoutingRule { temperature: None, } } +} + +#[cfg(test)] +mod glob_match_tests { + use super::glob_match; + + #[test] fn exact_match() { assert!(glob_match("gpt-oss:120b", "gpt-oss:120b")); } + #[test] fn exact_mismatch() { assert!(!glob_match("a", "b")); } + #[test] fn leading_wildcard() { assert!(glob_match("*:120b", "gpt-oss:120b")); } + #[test] fn trailing_wildcard() { assert!(glob_match("gpt-oss:*", "gpt-oss:120b")); } + #[test] fn bare_wildcard() { assert!(glob_match("*", "anything")); } + #[test] fn multi_wildcard_in_order() { assert!(glob_match("gpt-*-oss-*", "gpt-4-oss-120b")); } + #[test] fn multi_wildcard_wrong_order() { assert!(!glob_match("b*a*", "abba")); } + #[test] fn multi_wildcard_panic_safety() { + // Regression: earlier impl had an unreachable `parts.len() == 1` + // branch that indexed parts[1] — would panic if ever hit. Now + // the split('*') invariant guarantees ≥2 parts when * present, + // and we handle all N-part cases explicitly. + assert!(glob_match("a*b*c", "abc")); + assert!(glob_match("a*b*c", "axxxbxxxc")); + assert!(!glob_match("a*b*c", "xxxbxxx")); + } } \ No newline at end of file