- Introduce reference MCP presets with installation/audit helpers and remove legacy connector lists. - Add CLI `owlen tools` commands to install presets or audit configuration, with optional pruning. - Extend the TUI :tools command to support listing presets, installing them, and auditing current configuration. - Document the preset workflow and provide regression tests for preset application.
165 lines
4.8 KiB
Rust
165 lines
4.8 KiB
Rust
use std::{any::Any, sync::Arc};
|
|
|
|
use async_trait::async_trait;
|
|
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
|
use futures_util::stream;
|
|
use owlen_core::{
|
|
Config, Mode, Provider,
|
|
config::McpMode,
|
|
session::SessionController,
|
|
storage::StorageManager,
|
|
types::{ChatResponse, Message, Role, ToolCall},
|
|
ui::{NoOpUiController, UiController},
|
|
};
|
|
use owlen_tui::ChatApp;
|
|
use owlen_tui::app::UiRuntime;
|
|
use owlen_tui::events::Event;
|
|
use tempfile::tempdir;
|
|
use tokio::sync::mpsc;
|
|
|
|
struct StubProvider;
|
|
|
|
#[async_trait]
|
|
impl Provider for StubProvider {
|
|
fn name(&self) -> &str {
|
|
"stub-provider"
|
|
}
|
|
|
|
async fn list_models(&self) -> owlen_core::Result<Vec<owlen_core::types::ModelInfo>> {
|
|
Ok(vec![owlen_core::types::ModelInfo {
|
|
id: "stub-model".into(),
|
|
name: "Stub Model".into(),
|
|
description: Some("Stub model for testing".into()),
|
|
provider: self.name().into(),
|
|
context_window: Some(4096),
|
|
capabilities: vec!["chat".into()],
|
|
supports_tools: true,
|
|
}])
|
|
}
|
|
|
|
async fn send_prompt(
|
|
&self,
|
|
_request: owlen_core::types::ChatRequest,
|
|
) -> owlen_core::Result<ChatResponse> {
|
|
Ok(ChatResponse {
|
|
message: Message::assistant("stub response".to_string()),
|
|
usage: None,
|
|
is_streaming: false,
|
|
is_final: true,
|
|
})
|
|
}
|
|
|
|
async fn stream_prompt(
|
|
&self,
|
|
_request: owlen_core::types::ChatRequest,
|
|
) -> owlen_core::Result<owlen_core::ChatStream> {
|
|
Ok(Box::pin(stream::empty()))
|
|
}
|
|
|
|
async fn health_check(&self) -> owlen_core::Result<()> {
|
|
Ok(())
|
|
}
|
|
|
|
fn as_any(&self) -> &(dyn Any + Send + Sync) {
|
|
self
|
|
}
|
|
}
|
|
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn denied_consent_appends_apology_message() {
|
|
let temp_dir = tempdir().expect("temp dir");
|
|
let storage = Arc::new(
|
|
StorageManager::with_database_path(temp_dir.path().join("owlen-tui-tests.db"))
|
|
.await
|
|
.expect("storage"),
|
|
);
|
|
|
|
let mut config = Config::default();
|
|
config.privacy.encrypt_local_data = false;
|
|
config.general.default_model = Some("stub-model".into());
|
|
config.mcp.mode = McpMode::LocalOnly;
|
|
config
|
|
.refresh_mcp_servers(None)
|
|
.expect("refresh MCP servers");
|
|
|
|
let provider: Arc<dyn Provider> = Arc::new(StubProvider);
|
|
let ui: Arc<dyn UiController> = Arc::new(NoOpUiController);
|
|
let (event_tx, controller_event_rx) = mpsc::unbounded_channel();
|
|
|
|
// Pre-populate a pending consent request before handing the controller to the TUI.
|
|
let mut session = SessionController::new(
|
|
Arc::clone(&provider),
|
|
config,
|
|
Arc::clone(&storage),
|
|
Arc::clone(&ui),
|
|
true,
|
|
Some(event_tx.clone()),
|
|
)
|
|
.await
|
|
.expect("session controller");
|
|
|
|
session
|
|
.set_operating_mode(Mode::Code)
|
|
.await
|
|
.expect("code mode");
|
|
|
|
let tool_call = ToolCall {
|
|
id: "call-1".to_string(),
|
|
name: "resources_delete".to_string(),
|
|
arguments: serde_json::json!({"path": "/tmp/example.txt"}),
|
|
};
|
|
|
|
let message_id = session
|
|
.conversation_mut()
|
|
.push_assistant_message("Preparing to modify files.");
|
|
session
|
|
.conversation_mut()
|
|
.set_tool_calls_on_message(message_id, vec![tool_call])
|
|
.expect("tool calls");
|
|
|
|
let advertised_calls = session
|
|
.check_streaming_tool_calls(message_id)
|
|
.expect("queued consent");
|
|
assert_eq!(advertised_calls.len(), 1);
|
|
|
|
let (mut app, mut session_rx) = ChatApp::new(session, controller_event_rx)
|
|
.await
|
|
.expect("chat app");
|
|
// Session events are not used in this test.
|
|
session_rx.close();
|
|
|
|
// Process the controller event emitted by check_streaming_tool_calls.
|
|
UiRuntime::poll_controller_events(&mut app).expect("poll controller events");
|
|
assert!(app.has_pending_consent());
|
|
|
|
let consent_state = app
|
|
.consent_dialog()
|
|
.expect("consent dialog should be visible")
|
|
.clone();
|
|
assert_eq!(consent_state.tool_name, "resources_delete");
|
|
|
|
// Simulate the user pressing "4" to deny consent.
|
|
let deny_key = KeyEvent::new(KeyCode::Char('4'), KeyModifiers::NONE);
|
|
UiRuntime::handle_ui_event(&mut app, Event::Key(deny_key))
|
|
.await
|
|
.expect("handle deny key");
|
|
|
|
assert!(!app.has_pending_consent());
|
|
assert!(
|
|
app.status_message()
|
|
.to_lowercase()
|
|
.contains("consent denied")
|
|
);
|
|
|
|
let conversation = app.conversation();
|
|
let last_message = conversation.messages.last().expect("last message");
|
|
assert_eq!(last_message.role, Role::Assistant);
|
|
assert!(
|
|
last_message
|
|
.content
|
|
.to_lowercase()
|
|
.contains("consent was denied"),
|
|
"assistant should acknowledge the denied consent"
|
|
);
|
|
}
|