use std::path::Path;
use opi_ai::provider::{EventStream, ModelInfo, Provider, ProviderError, Request};
use opi_ai::stream::AssistantStreamEvent;
use opi_coding_agent::picker;
use opi_tui::select_list::SelectListState;
struct TestProvider {
id: String,
models: Vec<ModelInfo>,
}
impl TestProvider {
fn new(id: &str, models: Vec<ModelInfo>) -> Self {
Self {
id: id.into(),
models,
}
}
}
impl Provider for TestProvider {
fn id(&self) -> &str {
&self.id
}
fn models(&self) -> &[ModelInfo] {
&self.models
}
fn stream(&self, _request: Request) -> EventStream {
let stream: Vec<Result<AssistantStreamEvent, ProviderError>> = vec![];
Box::pin(futures_util::stream::iter(stream))
}
}
fn sample_registry() -> opi_ai::registry::ProviderRegistry {
let models = vec![
ModelInfo {
id: "claude-sonnet-4-5-20250514".into(),
display_name: "Claude Sonnet 4.5".into(),
context_window: 200000,
max_output_tokens: 8192,
supports_images: true,
supports_streaming: true,
supports_thinking: true,
},
ModelInfo {
id: "claude-opus-4-20250514".into(),
display_name: "Claude Opus 4".into(),
context_window: 200000,
max_output_tokens: 8192,
supports_images: true,
supports_streaming: true,
supports_thinking: true,
},
];
let provider = TestProvider::new("anthropic", models);
let mut registry = opi_ai::registry::ProviderRegistry::new();
registry.register(Box::new(provider));
registry
}
#[test]
fn model_picker_items_from_registry() {
let registry = sample_registry();
let items = picker::model_picker_items(®istry);
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, "anthropic:claude-sonnet-4-5-20250514");
assert_eq!(items[0].display, "Claude Sonnet 4.5");
assert_eq!(items[0].metadata, "anthropic");
}
#[test]
fn model_picker_filter_and_select() {
let registry = sample_registry();
let items = picker::model_picker_items(®istry);
let mut state = SelectListState::new(items);
state.set_filter("opus");
assert_eq!(state.visible_count(), 1);
let selected = state.confirm().unwrap();
assert_eq!(selected.id, "anthropic:claude-opus-4-20250514");
}
#[test]
fn model_picker_multiple_providers() {
let p1 = TestProvider::new(
"anthropic",
vec![ModelInfo {
id: "claude-sonnet-4-5-20250514".into(),
display_name: "Claude Sonnet 4.5".into(),
context_window: 200000,
max_output_tokens: 8192,
supports_images: true,
supports_streaming: true,
supports_thinking: true,
}],
);
let p2 = TestProvider::new(
"openai",
vec![ModelInfo {
id: "gpt-4o".into(),
display_name: "GPT-4o".into(),
context_window: 128000,
max_output_tokens: 4096,
supports_images: true,
supports_streaming: true,
supports_thinking: false,
}],
);
let mut registry = opi_ai::registry::ProviderRegistry::new();
registry.register(Box::new(p1));
registry.register(Box::new(p2));
let items = picker::model_picker_items(®istry);
assert_eq!(items.len(), 2);
let mut state = SelectListState::new(items);
state.set_filter("gpt");
assert_eq!(state.visible_count(), 1);
assert_eq!(state.confirm().unwrap().id, "openai:gpt-4o");
}
#[test]
fn session_picker_items_from_directory() {
let dir = tempfile::tempdir().unwrap();
create_test_session(
dir.path(),
"sess-001",
"2026-05-26T10:00:00Z",
"/home/user/project",
);
create_test_session(
dir.path(),
"sess-002",
"2026-05-26T11:00:00Z",
"/home/user/other",
);
let items = picker::session_picker_items(dir.path()).unwrap();
assert_eq!(items.len(), 2);
assert_eq!(items[0].id, "sess-002");
assert!(items[1].display.contains("project"));
assert_eq!(items[1].id, "sess-001");
}
#[test]
fn session_picker_empty_directory() {
let dir = tempfile::tempdir().unwrap();
let items = picker::session_picker_items(dir.path()).unwrap();
assert!(items.is_empty());
}
#[test]
fn session_picker_filter_and_select() {
let dir = tempfile::tempdir().unwrap();
create_test_session(
dir.path(),
"sess-001",
"2026-05-26T10:00:00Z",
"/home/user/project",
);
create_test_session(
dir.path(),
"sess-002",
"2026-05-26T11:00:00Z",
"/home/user/other",
);
let items = picker::session_picker_items(dir.path()).unwrap();
let mut state = SelectListState::new(items);
state.set_filter("other");
assert_eq!(state.visible_count(), 1);
let selected = state.confirm().unwrap();
assert_eq!(selected.id, "sess-002");
}
#[test]
fn session_picker_corrupt_file_is_skipped() {
let dir = tempfile::tempdir().unwrap();
create_test_session(
dir.path(),
"sess-001",
"2026-05-26T10:00:00Z",
"/home/user/project",
);
std::fs::write(dir.path().join("corrupt.jsonl"), "not valid json\n").unwrap();
let items = picker::session_picker_items(dir.path()).unwrap();
assert_eq!(items.len(), 1);
assert_eq!(items[0].id, "sess-001");
}
#[test]
fn session_picker_long_cwd_is_truncated() {
let dir = tempfile::tempdir().unwrap();
let long_cwd =
"/home/user/very/deeply/nested/directory/structure/that/exceeds/forty/characters";
create_test_session(dir.path(), "sess-001", "2026-05-26T10:00:00Z", long_cwd);
let items = picker::session_picker_items(dir.path()).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].display.starts_with("..."));
assert!(items[0].display.len() <= 40);
}
#[test]
fn session_picker_multibyte_cwd_does_not_panic() {
let dir = tempfile::tempdir().unwrap();
let cjk_cwd = "/home/\u{4f60}\u{597d}\u{4e16}\u{754c}\u{6587}\u{4ef6}\u{76ee}\u{5f55}\u{8def}\u{5f84}\u{6d4b}\u{8bd5}\u{6570}\u{636e}\u{9879}\u{76ee}\u{5de5}\u{7a0b}\u{4ee3}\u{7801}/end";
create_test_session(dir.path(), "sess-cjk", "2026-05-26T10:00:00Z", cjk_cwd);
let items = picker::session_picker_items(dir.path()).unwrap();
assert_eq!(items.len(), 1);
assert!(items[0].display.starts_with("...") || items[0].display.len() <= 40);
}
fn create_test_session(dir: &Path, id: &str, timestamp: &str, cwd: &str) {
use std::io::Write;
let header = serde_json::json!({
"type": "session",
"version": 1,
"id": id,
"timestamp": timestamp,
"cwd": cwd,
});
let path = dir.join(format!("{id}.jsonl"));
let mut f = std::fs::File::create(path).unwrap();
writeln!(f, "{header}").unwrap();
}