use std::path::Path;
use crate::config_edit::branch::config_file;
use crate::config_edit::brazen::{BzRunner, NOT_REQUIRED, ProviderRow, RealBzRunner};
use crate::model_pick::{BRANCH, PROVIDERS};
use super::super::config::wall_env;
use super::Deps;
pub(super) fn gate(deps: &Deps, workspace: &Path, lineage: Option<&str>) -> Result<(), String> {
let rows = RealBzRunner::resolve(&wall_env(deps, workspace)).providers();
if rows.is_empty() || ready(&rows, &named_rows(workspace, lineage)) {
return Ok(());
}
Err(refusal(&rows))
}
fn named_rows(workspace: &Path, lineage: Option<&str>) -> Vec<String> {
let refspec = format!("config/{}", lineage.unwrap_or(BRANCH));
let text = config_file(workspace, &refspec, PROVIDERS)
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned())
.unwrap_or_default();
crate::model_pick::grammar::roles(&text)
.into_iter()
.map(|role| role.provider)
.collect()
}
pub(crate) fn ready(rows: &[ProviderRow], named: &[String]) -> bool {
rows.iter().any(|row| {
row.credentialed() || (row.credential == NOT_REQUIRED && named.contains(&row.name))
})
}
fn refusal(rows: &[ProviderRow]) -> String {
let names: Vec<&str> = rows.iter().map(|row| row.name.as_str()).collect();
format!(
"sign in first: no provider in this workspace's wall holds a credential, so a \
conversation begun here would reach no model — /login <provider> (rows: {})",
names.join(", ")
)
}
#[cfg(test)]
mod tests {
use super::*;
fn row(name: &str, credential: &str) -> ProviderRow {
ProviderRow {
name: name.to_owned(),
protocol: String::new(),
auth: String::new(),
credential: credential.to_owned(),
effort: false,
priority: false,
tools: Some(true),
device: String::new(),
}
}
#[test]
fn a_credential_readies_a_keyless_row_only_when_a_role_names_it() {
let keyless = [row("ollama", NOT_REQUIRED), row("acme", "missing")];
assert!(!ready(&keyless, &[]));
assert!(ready(&keyless, &["ollama".to_owned()]));
assert!(
!ready(&keyless, &["acme".to_owned()]),
"missing is the one refusal"
);
assert!(ready(&[row("acme", "stored")], &[]));
assert!(ready(
&[row("acme", "a-spelling-this-build-never-heard-of")],
&[]
));
}
#[test]
fn the_refusal_names_the_act_and_the_rows() {
let said = refusal(&[row("ollama", NOT_REQUIRED), row("acme", "missing")]);
assert!(said.starts_with("sign in first"), "{said}");
assert!(said.contains("/login"), "{said}");
assert!(said.ends_with("(rows: ollama, acme)"), "{said}");
}
}