diff --git a/src/routing.rs b/src/routing.rs index 2a32c72..61342f6 100644 --- a/src/routing.rs +++ b/src/routing.rs @@ -97,9 +97,16 @@ impl RoutingTable { } let d = self.default.clone(); push(&mut out, &d); - let matched = rule - .map(|r| !r.any.is_empty() || r.per.values().any(|v| !v.is_empty())) - .unwrap_or(false); + // "task-rule" must mean "the rule chose the head of this chain" — a + // rule that exists but lost to the tier chain (math has a high pick, + // the request came in low) must not claim the credit. + let matched = match (rule, out.first()) { + (Some(r), Some(head)) => { + r.per.get(tier).map(|v| v.contains(head)).unwrap_or(false) + || r.any.first() == Some(head) + } + _ => false, + }; (out, matched) } @@ -184,7 +191,9 @@ mod tests { assert_eq!(c[0], "cheap-coder"); // high: tier chain leads, task rule follows as fallback let (c, m) = t.candidates_for("code", "high"); - assert!(m); + // escalation put the global high chain first → the rule did not pick + // the head, so no task-rule credit + assert!(!m); // default rides along as the very last fallback assert_eq!(c, vec!["big", "cheap-coder", "cheap"]); } @@ -202,8 +211,10 @@ mod tests { let (c, m) = t.candidates_for("code", "high"); assert!(m); assert_eq!(c, vec!["best-coder", "big", "generic-coder", "cheap"]); - // at low: any-tier task models lead as before - let (c, _) = t.candidates_for("code", "low"); + // at low: any-tier task models lead as before (and the badge is + // honest — the head came from the rule) + let (c, m) = t.candidates_for("code", "low"); + assert!(m); assert_eq!(c, vec!["generic-coder", "cheap"]); // a tier with an explicit pick but no any-models let t2 = with_task_tier_models( @@ -215,6 +226,10 @@ mod tests { let (c, m) = t2.candidates_for("legal", "high"); assert!(m); assert_eq!(c, vec!["strong", "big", "cheap"]); + // the same rule at LOW did not shape the head → no task-rule credit + let (c, m) = t2.candidates_for("legal", "low"); + assert!(!m); + assert_eq!(c, vec!["cheap"]); } #[test]