use ahash::AHashMap;
use std::sync::Arc;
use terraphim_config::{Config, ConfigId, ConfigState, Haystack, Role, ServiceType};
use terraphim_rolegraph::{RoleGraph, RoleGraphSync};
use terraphim_service::auto_route::{AutoRouteContext, AutoRouteReason, auto_select_role};
use terraphim_types::{
NormalizedTerm, NormalizedTermValue, RelevanceFunction, RoleName, Thesaurus,
};
use tokio::sync::Mutex;
fn build_thesaurus(name: &str, terms: &[(&str, u64, &str)]) -> Thesaurus {
let mut t = Thesaurus::new(name.to_string());
for (synonym, id, concept) in terms {
t.insert(
NormalizedTermValue::from(*synonym),
NormalizedTerm::new(*id, NormalizedTermValue::from(*concept)),
);
}
t
}
async fn build_rolegraph(role_name: &RoleName, thesaurus: Thesaurus) -> RoleGraphSync {
let rg = RoleGraph::new(role_name.clone(), thesaurus).await.unwrap();
RoleGraphSync::from(rg)
}
fn make_role(name: &str, has_jmap: bool) -> Role {
let mut role = Role::new(RoleName::new(name));
role.relevance_function = RelevanceFunction::TerraphimGraph;
if has_jmap {
role.haystacks.push(Haystack::new(
"jmap://test".to_string(),
ServiceType::Jmap,
true,
));
}
role
}
struct Fixture {
config: Config,
state: ConfigState,
}
fn assemble(roles: Vec<(Role, RoleGraphSync)>, default: &str, selected: &str) -> Fixture {
let mut role_map = AHashMap::new();
let mut rg_map = AHashMap::new();
for (role, rg) in roles {
role_map.insert(role.name.clone(), role.clone());
rg_map.insert(role.name.clone(), rg);
}
let config = Config {
id: ConfigId::Embedded,
global_shortcut: Some("Ctrl+X".to_string()),
roles: role_map,
default_role: RoleName::new(default),
selected_role: RoleName::new(selected),
};
let state = ConfigState {
config: Arc::new(Mutex::new(config.clone())),
roles: rg_map,
};
Fixture { config, state }
}
#[tokio::test]
async fn t1_single_role_wins_clearly() {
let sysop_name = RoleName::new("System Operator");
let default_name = RoleName::new("Default");
let sysop_thes = build_thesaurus(
"sysop",
&[
("rfp", 1, "acquisition need"),
("acquisition", 1, "acquisition need"),
],
);
let default_thes = build_thesaurus("default", &[("anything", 2, "anything")]);
let sysop_rg = build_rolegraph(&sysop_name, sysop_thes).await;
let default_rg = build_rolegraph(&default_name, default_thes).await;
let fixture = assemble(
vec![
(make_role("System Operator", false), sysop_rg),
(make_role("Default", false), default_rg),
],
"Default",
"Default",
);
let ctx = AutoRouteContext {
selected_role: Some(default_name.clone()),
jmap_token_present: true,
};
let result = auto_select_role("RFP", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "System Operator");
assert_eq!(result.score, 1);
assert_eq!(result.reason, AutoRouteReason::ScoredWinner);
}
#[tokio::test]
async fn t2_tie_selected_role_wins() {
let a_name = RoleName::new("Personal Assistant");
let b_name = RoleName::new("Terraphim Engineer");
let thes_a = build_thesaurus("a", &[("widget", 10, "widget")]);
let thes_b = build_thesaurus("b", &[("widget", 20, "widget")]);
let rg_a = build_rolegraph(&a_name, thes_a).await;
let rg_b = build_rolegraph(&b_name, thes_b).await;
let fixture = assemble(
vec![
(make_role("Personal Assistant", false), rg_a),
(make_role("Terraphim Engineer", false), rg_b),
],
"Default",
"Terraphim Engineer",
);
let ctx = AutoRouteContext {
selected_role: Some(b_name.clone()),
jmap_token_present: true,
};
let result = auto_select_role("widget", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "Terraphim Engineer");
assert_eq!(result.reason, AutoRouteReason::TieBrokenBySelectedRole);
assert_eq!(result.candidates[0].1, result.candidates[1].1);
assert_eq!(result.score, 1);
}
#[tokio::test]
async fn t3_tie_alphabetical() {
let a_name = RoleName::new("Personal Assistant");
let b_name = RoleName::new("Terraphim Engineer");
let thes_a = build_thesaurus("a", &[("widget", 10, "widget")]);
let thes_b = build_thesaurus("b", &[("widget", 20, "widget")]);
let rg_a = build_rolegraph(&a_name, thes_a).await;
let rg_b = build_rolegraph(&b_name, thes_b).await;
let fixture = assemble(
vec![
(make_role("Personal Assistant", false), rg_a),
(make_role("Terraphim Engineer", false), rg_b),
],
"Default",
"Personal Assistant",
);
let outsider = RoleName::new("Other Role Not In Config");
let ctx = AutoRouteContext {
selected_role: Some(outsider),
jmap_token_present: true,
};
let result = auto_select_role("widget", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "Personal Assistant");
assert_eq!(result.reason, AutoRouteReason::TieBrokenAlphabetically);
}
#[tokio::test]
async fn t4_zero_match_selected_role() {
let rust_name = RoleName::new("Rust Engineer");
let default_name = RoleName::new("Default");
let rust_thes = build_thesaurus("rust", &[("rust", 1, "rust")]);
let default_thes = build_thesaurus("default", &[("anything", 2, "anything")]);
let rg_rust = build_rolegraph(&rust_name, rust_thes).await;
let rg_default = build_rolegraph(&default_name, default_thes).await;
let fixture = assemble(
vec![
(make_role("Rust Engineer", false), rg_rust),
(make_role("Default", false), rg_default),
],
"Default",
"Rust Engineer",
);
let ctx = AutoRouteContext {
selected_role: Some(rust_name.clone()),
jmap_token_present: true,
};
let result = auto_select_role("xyzzy", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "Rust Engineer");
assert_eq!(result.reason, AutoRouteReason::ZeroMatchSelectedRole);
assert_eq!(result.score, 0);
}
#[tokio::test]
async fn t5_zero_match_default() {
let default_name = RoleName::new("Default");
let other_name = RoleName::new("Other");
let default_thes = build_thesaurus("default", &[("anything", 1, "anything")]);
let other_thes = build_thesaurus("other", &[("anything_else", 2, "anything_else")]);
let rg_default = build_rolegraph(&default_name, default_thes).await;
let rg_other = build_rolegraph(&other_name, other_thes).await;
let fixture = assemble(
vec![
(make_role("Default", false), rg_default),
(make_role("Other", false), rg_other),
],
"Default",
"Default",
);
let ctx = AutoRouteContext {
selected_role: None,
jmap_token_present: true,
};
let result = auto_select_role("xyzzy", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "Default");
assert_eq!(result.reason, AutoRouteReason::ZeroMatchDefault);
assert_eq!(result.score, 0);
}
#[tokio::test]
async fn t6_pa_loses_to_stronger_rival() {
let pa_name = RoleName::new("Personal Assistant");
let sysop_name = RoleName::new("System Operator");
let pa_thes = build_thesaurus("pa", &[("invoice", 1, "invoice")]);
let sysop_thes = build_thesaurus(
"sysop",
&[("invoice", 2, "invoice"), ("procurement", 3, "procurement")],
);
let rg_pa = build_rolegraph(&pa_name, pa_thes).await;
let rg_sysop = build_rolegraph(&sysop_name, sysop_thes).await;
let fixture = assemble(
vec![
(make_role("Personal Assistant", true), rg_pa),
(make_role("System Operator", false), rg_sysop),
],
"Default",
"Personal Assistant",
);
let ctx = AutoRouteContext {
selected_role: Some(pa_name.clone()),
jmap_token_present: false,
};
let result =
auto_select_role("invoice procurement", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "System Operator");
assert_eq!(result.score, 2);
assert_eq!(result.reason, AutoRouteReason::ScoredWinner);
}
#[tokio::test]
async fn t7_pa_wins_when_only_pa_matches() {
let pa_name = RoleName::new("Personal Assistant");
let other_name = RoleName::new("Default");
let pa_thes = build_thesaurus(
"pa",
&[("invoice", 1, "invoice"), ("receipt", 2, "receipt")],
);
let other_thes = build_thesaurus("default", &[("rust", 3, "rust")]);
let rg_pa = build_rolegraph(&pa_name, pa_thes).await;
let rg_other = build_rolegraph(&other_name, other_thes).await;
let fixture = assemble(
vec![
(make_role("Personal Assistant", true), rg_pa),
(make_role("Default", false), rg_other),
],
"Default",
"Personal Assistant",
);
let ctx = AutoRouteContext {
selected_role: Some(pa_name.clone()),
jmap_token_present: false,
};
let result =
auto_select_role("invoice and receipt", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(result.role.as_str(), "Personal Assistant");
assert_eq!(result.score, 1);
assert_eq!(result.reason, AutoRouteReason::ScoredWinner);
}
#[tokio::test]
async fn t11_cold_start_no_documents_indexed() {
let sysop_name = RoleName::new("System Operator");
let default_name = RoleName::new("Default");
let engineer_name = RoleName::new("Terraphim Engineer");
let sysop_thes = build_thesaurus("sysop", &[("rfp", 1, "rfp")]);
let default_thes = build_thesaurus("default", &[("readme", 2, "readme")]);
let engineer_thes = build_thesaurus("engineer", &[("crate", 3, "crate")]);
let sysop_rg = build_rolegraph(&sysop_name, sysop_thes).await;
let default_rg = build_rolegraph(&default_name, default_thes).await;
let engineer_rg = build_rolegraph(&engineer_name, engineer_thes).await;
let fixture = assemble(
vec![
(make_role("System Operator", false), sysop_rg),
(make_role("Default", false), default_rg),
(make_role("Terraphim Engineer", false), engineer_rg),
],
"Default",
"Default",
);
let ctx = AutoRouteContext {
selected_role: Some(default_name.clone()),
jmap_token_present: true,
};
let result = auto_select_role("RFP", &fixture.config, &fixture.state, &ctx).await;
assert_eq!(
result.role.as_str(),
"System Operator",
"cold-start: System Operator must win on 'RFP' without document indexing"
);
assert!(
result.score >= 1,
"cold-start: score must be >= 1 (was {}). Prior rank-sum scorer would have returned 0.",
result.score
);
assert_eq!(result.reason, AutoRouteReason::ScoredWinner);
}