feat(v2): complete multi-LLM providers, TUI redesign, and advanced agent features
Multi-LLM Provider Support: - Add llm-core crate with LlmProvider trait abstraction - Implement Anthropic Claude API client with streaming - Implement OpenAI API client with streaming - Add token counting with SimpleTokenCounter and ClaudeTokenCounter - Add retry logic with exponential backoff and jitter Borderless TUI Redesign: - Rewrite theme system with terminal capability detection (Full/Unicode256/Basic) - Add provider tabs component with keybind switching [1]/[2]/[3] - Implement vim-modal input (Normal/Insert/Visual/Command modes) - Redesign chat panel with timestamps and streaming indicators - Add multi-provider status bar with cost tracking - Add Nerd Font icons with graceful ASCII fallbacks - Add syntax highlighting (syntect) and markdown rendering (pulldown-cmark) Advanced Agent Features: - Add system prompt builder with configurable components - Enhance subagent orchestration with parallel execution - Add git integration module for safe command detection - Add streaming tool results via channels - Expand tool set: AskUserQuestion, TodoWrite, LS, MultiEdit, BashOutput, KillShell - Add WebSearch with provider abstraction Plugin System Enhancement: - Add full agent definition parsing from YAML frontmatter - Add skill system with progressive disclosure - Wire plugin hooks into HookManager 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use config_agent::{load_settings, Settings};
|
||||
use permissions::{Mode, PermissionDecision, Tool};
|
||||
use llm_core::ProviderType;
|
||||
use std::{env, fs};
|
||||
|
||||
#[test]
|
||||
@@ -45,4 +46,189 @@ fn settings_parse_mode_from_config() {
|
||||
// Code mode should allow everything
|
||||
assert_eq!(mgr.check(Tool::Write, None), PermissionDecision::Allow);
|
||||
assert_eq!(mgr.check(Tool::Bash, None), PermissionDecision::Allow);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_provider_is_ollama() {
|
||||
let s = Settings::default();
|
||||
assert_eq!(s.provider, "ollama");
|
||||
assert_eq!(s.get_provider(), Some(ProviderType::Ollama));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn provider_from_config_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"provider="anthropic""#).unwrap();
|
||||
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.provider, "anthropic");
|
||||
assert_eq!(s.get_provider(), Some(ProviderType::Anthropic));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn provider_from_env_var() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
env::set_var("OWLEN_PROVIDER", "openai");
|
||||
env::remove_var("PROVIDER");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
}
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.provider, "openai");
|
||||
assert_eq!(s.get_provider(), Some(ProviderType::OpenAI));
|
||||
unsafe { env::remove_var("OWLEN_PROVIDER"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn provider_from_provider_env_var() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
env::set_var("PROVIDER", "anthropic");
|
||||
env::remove_var("OWLEN_PROVIDER");
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
}
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.provider, "anthropic");
|
||||
assert_eq!(s.get_provider(), Some(ProviderType::Anthropic));
|
||||
unsafe { env::remove_var("PROVIDER"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anthropic_api_key_from_owlen_env() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"provider="anthropic""#).unwrap();
|
||||
|
||||
unsafe { env::set_var("OWLEN_ANTHROPIC_API_KEY", "sk-ant-test123"); }
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.anthropic_api_key, Some("sk-ant-test123".to_string()));
|
||||
assert_eq!(s.get_provider_api_key(), Some("sk-ant-test123".to_string()));
|
||||
unsafe { env::remove_var("OWLEN_ANTHROPIC_API_KEY"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openai_api_key_from_owlen_env() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"provider="openai""#).unwrap();
|
||||
|
||||
unsafe { env::set_var("OWLEN_OPENAI_API_KEY", "sk-test-456"); }
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.openai_api_key, Some("sk-test-456".to_string()));
|
||||
assert_eq!(s.get_provider_api_key(), Some("sk-test-456".to_string()));
|
||||
unsafe { env::remove_var("OWLEN_OPENAI_API_KEY"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn api_keys_from_config_file() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"
|
||||
provider = "anthropic"
|
||||
anthropic_api_key = "sk-ant-from-file"
|
||||
openai_api_key = "sk-openai-from-file"
|
||||
"#).unwrap();
|
||||
|
||||
// Clear any env vars that might interfere
|
||||
unsafe {
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::remove_var("OPENAI_API_KEY");
|
||||
env::remove_var("OWLEN_ANTHROPIC_API_KEY");
|
||||
env::remove_var("OWLEN_OPENAI_API_KEY");
|
||||
}
|
||||
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.anthropic_api_key, Some("sk-ant-from-file".to_string()));
|
||||
assert_eq!(s.openai_api_key, Some("sk-openai-from-file".to_string()));
|
||||
assert_eq!(s.get_provider_api_key(), Some("sk-ant-from-file".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn anthropic_api_key_from_standard_env() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"provider="anthropic""#).unwrap();
|
||||
|
||||
unsafe {
|
||||
env::set_var("ANTHROPIC_API_KEY", "sk-ant-std");
|
||||
env::remove_var("OWLEN_ANTHROPIC_API_KEY");
|
||||
env::remove_var("PROVIDER");
|
||||
env::remove_var("OWLEN_PROVIDER");
|
||||
}
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.anthropic_api_key, Some("sk-ant-std".to_string()));
|
||||
assert_eq!(s.get_provider_api_key(), Some("sk-ant-std".to_string()));
|
||||
unsafe { env::remove_var("ANTHROPIC_API_KEY"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn openai_api_key_from_standard_env() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let project_file = tmp.path().join(".owlen.toml");
|
||||
fs::write(&project_file, r#"provider="openai""#).unwrap();
|
||||
|
||||
unsafe {
|
||||
env::set_var("OPENAI_API_KEY", "sk-openai-std");
|
||||
env::remove_var("OWLEN_OPENAI_API_KEY");
|
||||
env::remove_var("PROVIDER");
|
||||
env::remove_var("OWLEN_PROVIDER");
|
||||
}
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
assert_eq!(s.openai_api_key, Some("sk-openai-std".to_string()));
|
||||
assert_eq!(s.get_provider_api_key(), Some("sk-openai-std".to_string()));
|
||||
unsafe { env::remove_var("OPENAI_API_KEY"); }
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore] // Ignore due to env var interaction in parallel tests
|
||||
fn owlen_prefix_overrides_standard_env() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
|
||||
unsafe {
|
||||
env::set_var("ANTHROPIC_API_KEY", "sk-ant-std");
|
||||
env::set_var("OWLEN_ANTHROPIC_API_KEY", "sk-ant-owlen");
|
||||
}
|
||||
let s = load_settings(Some(tmp.path().to_str().unwrap())).unwrap();
|
||||
// OWLEN_ prefix should take precedence
|
||||
assert_eq!(s.anthropic_api_key, Some("sk-ant-owlen".to_string()));
|
||||
unsafe {
|
||||
env::remove_var("ANTHROPIC_API_KEY");
|
||||
env::remove_var("OWLEN_ANTHROPIC_API_KEY");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_model_uses_provider_default() {
|
||||
// Test Anthropic provider default
|
||||
let mut s = Settings::default();
|
||||
s.provider = "anthropic".to_string();
|
||||
assert_eq!(s.get_effective_model(), "claude-sonnet-4-20250514");
|
||||
|
||||
// Test OpenAI provider default
|
||||
s.provider = "openai".to_string();
|
||||
assert_eq!(s.get_effective_model(), "gpt-4o");
|
||||
|
||||
// Test Ollama provider default
|
||||
s.provider = "ollama".to_string();
|
||||
assert_eq!(s.get_effective_model(), "qwen3:8b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_model_respects_explicit_model() {
|
||||
let mut s = Settings::default();
|
||||
s.provider = "anthropic".to_string();
|
||||
s.model = "claude-opus-4-20250514".to_string();
|
||||
|
||||
// Should use explicit model, not provider default
|
||||
assert_eq!(s.get_effective_model(), "claude-opus-4-20250514");
|
||||
}
|
||||
Reference in New Issue
Block a user