use std::collections::BTreeSet;
use std::path::Path;
use crate::harness::registry::HarnessId;
use super::Settings;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RoutingConfigDiagnostic {
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ResolvedRoutingSettings {
pub harness_order: Option<ParsedHarnessOrder>,
pub provider_order: Option<ParsedProviderOrder>,
pub default_harness: Option<ParsedHarnessValue>,
pub linked_harnesses: BTreeSet<HarnessId>,
pub diagnostics: Vec<RoutingConfigDiagnostic>,
}
impl ResolvedRoutingSettings {
pub fn from_config(root: &Path) -> Self {
match crate::config::load(root) {
Ok(config) => resolve(&config.settings),
Err(_) => resolve(&Settings::default()),
}
}
pub fn harness_order_names(&self) -> Option<Vec<String>> {
self.harness_order.as_ref().map(|order| {
order
.candidates
.iter()
.map(|c| c.harness.to_string())
.collect()
})
}
pub fn default_harness_name(&self) -> Option<String> {
self.default_harness
.as_ref()
.map(|value| value.harness.to_string())
}
pub fn provider_order_names(&self) -> Option<Vec<String>> {
self.provider_order
.as_ref()
.map(|order| order.providers.clone())
}
pub fn linked_harness_names(&self) -> Vec<String> {
self.linked_harnesses
.iter()
.map(|harness| harness.to_string())
.collect()
}
pub fn diagnostic_messages(&self) -> Vec<String> {
self.diagnostics
.iter()
.map(|diagnostic| diagnostic.message.clone())
.collect()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedHarnessValue {
pub harness: HarnessId,
pub original: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedHarnessOrder {
pub candidates: Vec<OrderedHarnessCandidate>,
pub failure: Option<HarnessOrderFailure>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedProviderOrder {
pub providers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HarnessOrderFailure {
Empty,
AllInvalid,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrderedHarnessCandidate {
pub harness: HarnessId,
pub original: String,
pub position: usize,
}
fn harness_order_input(settings: &Settings) -> Option<Vec<String>> {
match &settings.harness_order {
Some(order) => Some(order.clone()),
None => Some(crate::harness::registry::default_harness_order_names()),
}
}
pub fn resolve(settings: &Settings) -> ResolvedRoutingSettings {
let mut diagnostics = Vec::new();
let harness_order = harness_order_input(settings).map(|order| {
if order.is_empty() {
diagnostics.push(RoutingConfigDiagnostic {
message:
"settings.harness_order is empty; falling through to provider candidate order"
.to_string(),
});
return ParsedHarnessOrder {
candidates: Vec::new(),
failure: Some(HarnessOrderFailure::Empty),
};
}
let mut candidates = Vec::new();
for (position, candidate) in order.iter().enumerate() {
let Some(harness) = crate::harness::registry::parse(candidate) else {
diagnostics.push(RoutingConfigDiagnostic {
message: format!(
"settings.harness_order contains unrecognized harness `{candidate}`; skipping (valid: {})",
crate::harness::registry::names().join(", ")
),
});
continue;
};
candidates.push(OrderedHarnessCandidate {
harness,
original: candidate.clone(),
position,
});
}
let failure = if candidates.is_empty() {
diagnostics.push(RoutingConfigDiagnostic {
message:
"settings.harness_order has no valid candidates; falling through to provider candidate order"
.to_string(),
});
Some(HarnessOrderFailure::AllInvalid)
} else {
None
};
ParsedHarnessOrder {
candidates,
failure,
}
});
let default_harness = settings.default_harness.as_ref().and_then(|value| {
match crate::harness::registry::parse(value) {
Some(harness) => Some(ParsedHarnessValue {
harness,
original: value.clone(),
}),
None => {
diagnostics.push(RoutingConfigDiagnostic {
message: format!(
"settings.default_harness `{value}` is invalid; expected one of: {}",
crate::harness::registry::names().join(", ")
),
});
None
}
}
});
let provider_order = settings.provider_order.as_ref().map(|order| ParsedProviderOrder {
providers: order
.iter()
.filter_map(|provider| {
let normalized = provider.trim().to_ascii_lowercase();
if normalized.is_empty() {
return None;
}
if !is_known_provider_or_variant(&normalized) {
diagnostics.push(RoutingConfigDiagnostic {
message: format!(
"settings.provider_order contains unknown provider `{provider}`; keeping it for forward-compat routing preferences"
),
});
}
Some(normalized)
})
.collect(),
});
let linked_harnesses = settings.effective_links().linked_harnesses_set();
ResolvedRoutingSettings {
harness_order,
provider_order,
default_harness,
linked_harnesses,
diagnostics,
}
}
fn is_known_provider_or_variant(provider: &str) -> bool {
matches!(
provider,
"anthropic"
| "openai"
| "google"
| "meta"
| "mistral"
| "deepseek"
| "cohere"
| "openrouter"
| "openai-codex"
| "anthropic-claude"
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn settings_with_links(targets: Option<Vec<&str>>) -> Settings {
Settings {
managed_root: None,
targets: targets.map(|values| values.into_iter().map(str::to_string).collect()),
..Settings::default()
}
}
#[test]
fn parse_invalid_default_harness_to_diagnostic() {
let mut settings = settings_with_links(None);
settings.default_harness = Some("gemini".to_string());
let resolved = resolve(&settings);
assert!(resolved.default_harness.is_none());
assert!(
resolved
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("settings.default_harness"))
);
}
#[test]
fn default_harness_order_when_unset() {
let settings = settings_with_links(None);
assert!(settings.harness_order.is_none());
let resolved = resolve(&settings);
let order = resolved.harness_order.expect("default harness order");
assert_eq!(order.candidates.len(), 5);
assert_eq!(order.candidates[0].harness, HarnessId::Claude);
assert_eq!(order.candidates[1].harness, HarnessId::Pi);
assert_eq!(order.candidates[2].harness, HarnessId::Codex);
}
#[test]
fn parse_harness_order_preserves_original_positions() {
let mut settings = settings_with_links(None);
settings.harness_order = Some(vec!["pi".to_string(), "codex".to_string()]);
let resolved = resolve(&settings);
let order = resolved.harness_order.expect("harness order should be set");
assert_eq!(order.candidates[0].harness, HarnessId::Pi);
assert_eq!(order.candidates[0].position, 0);
assert_eq!(order.candidates[1].harness, HarnessId::Codex);
assert_eq!(order.candidates[1].position, 1);
}
#[test]
fn linked_harnesses_come_from_known_links_only() {
let settings = settings_with_links(Some(vec![".opencode", ".agents", "foo/bar"]));
let resolved = resolve(&settings);
assert!(resolved.linked_harnesses.contains(&HarnessId::OpenCode));
assert!(!resolved.linked_harnesses.contains(&HarnessId::Codex));
}
#[test]
fn from_config_falls_back_to_default_settings_when_missing() {
let temp = TempDir::new().expect("temp dir");
let resolved = ResolvedRoutingSettings::from_config(temp.path());
assert_eq!(resolved, resolve(&Settings::default()));
}
}