fix: SACP notifies clients of generated session names (#8983)
Signed-off-by: Matt Toohey <contact@matttoohey.com>
This commit is contained in:
@@ -12,6 +12,12 @@ use fs_err as fs;
|
||||
use goose::acp::server::AcpProviderFactory;
|
||||
use goose::config::base::CONFIG_YAML_NAME;
|
||||
use goose::config::GooseMode;
|
||||
use goose::conversation::message::Message;
|
||||
use goose::model::ModelConfig;
|
||||
use goose::providers::base::{
|
||||
stream_from_single_message, MessageStream, Provider, ProviderUsage, Usage,
|
||||
};
|
||||
use goose::providers::errors::ProviderError;
|
||||
use goose_test_support::{McpFixture, FAKE_CODE, TEST_IMAGE_B64, TEST_MODEL};
|
||||
use sacp::schema::{
|
||||
ListSessionsResponse, McpServer, McpServerHttp, ModelId, SessionInfo, SessionModeId,
|
||||
@@ -19,6 +25,7 @@ use sacp::schema::{
|
||||
};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
const SHELL_TEST_CONTENT: &str = "test-shell-content-98765";
|
||||
|
||||
@@ -51,6 +58,46 @@ async fn new_basic_session<C: Connection>(config: TestConnectionConfig) -> Basic
|
||||
BasicSession { conn, session }
|
||||
}
|
||||
|
||||
struct NamingProvider {
|
||||
model_config: ModelConfig,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Provider for NamingProvider {
|
||||
fn get_name(&self) -> &str {
|
||||
"naming-test"
|
||||
}
|
||||
|
||||
async fn stream(
|
||||
&self,
|
||||
_model_config: &ModelConfig,
|
||||
_session_id: &str,
|
||||
system: &str,
|
||||
_messages: &[Message],
|
||||
_tools: &[rmcp::model::Tool],
|
||||
) -> Result<MessageStream, ProviderError> {
|
||||
let text = if system.contains("four words or less") || system.contains("4 words or less") {
|
||||
"Generated Test Title"
|
||||
} else {
|
||||
"2"
|
||||
};
|
||||
Ok(stream_from_single_message(
|
||||
Message::assistant().with_text(text),
|
||||
ProviderUsage::new(self.model_config.model_name.clone(), Usage::default()),
|
||||
))
|
||||
}
|
||||
|
||||
fn get_model_config(&self) -> ModelConfig {
|
||||
self.model_config.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn naming_provider_factory() -> AcpProviderFactory {
|
||||
Arc::new(|_provider_name, model_config, _extensions| {
|
||||
Box::pin(async move { Ok(Arc::new(NamingProvider { model_config }) as Arc<dyn Provider>) })
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn run_list_sessions<C: Connection>() {
|
||||
let BasicSession { conn, session } =
|
||||
new_basic_session::<C>(TestConnectionConfig::default()).await;
|
||||
@@ -80,6 +127,56 @@ pub async fn run_list_sessions<C: Connection>() {
|
||||
);
|
||||
}
|
||||
|
||||
pub async fn run_session_name_update_notification<C: Connection>() {
|
||||
let expected_session_id = C::expected_session_id();
|
||||
let openai = OpenAiFixture::new(vec![], expected_session_id.clone()).await;
|
||||
let config = TestConnectionConfig {
|
||||
provider_factory: Some(naming_provider_factory()),
|
||||
disable_session_naming: false,
|
||||
..Default::default()
|
||||
};
|
||||
let mut conn = C::new(config, openai).await;
|
||||
let SessionData { mut session, .. } = conn.new_session().await.unwrap();
|
||||
expected_session_id.set(&session.session_id().0);
|
||||
|
||||
let output = session
|
||||
.prompt(
|
||||
"what should we call this conversation?",
|
||||
PermissionDecision::Cancel,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(output.text, "2");
|
||||
|
||||
let mut notifications = session.notifications();
|
||||
let deadline = tokio::time::Instant::now() + Duration::from_secs(1);
|
||||
while !notifications
|
||||
.iter()
|
||||
.any(|n| matches!(n, Notification::SessionInfoUpdate { .. }))
|
||||
&& tokio::time::Instant::now() < deadline
|
||||
{
|
||||
tokio::time::sleep(Duration::from_millis(10)).await;
|
||||
notifications.extend(session.notifications());
|
||||
}
|
||||
|
||||
let update = notifications
|
||||
.iter()
|
||||
.find_map(|notification| match notification {
|
||||
Notification::SessionInfoUpdate {
|
||||
title,
|
||||
updated_at,
|
||||
message_count,
|
||||
user_set_name,
|
||||
} => Some((title, updated_at, message_count, user_set_name)),
|
||||
_ => None,
|
||||
})
|
||||
.expect("expected generated session name notification");
|
||||
assert_eq!(update.0.as_deref(), Some("Generated Test Title"));
|
||||
assert!(update.1.is_some());
|
||||
assert!(update.2.unwrap_or_default() >= 1);
|
||||
assert_eq!(*update.3, Some(false));
|
||||
}
|
||||
|
||||
pub async fn run_close_session<C: Connection>() {
|
||||
let BasicSession { conn, session } =
|
||||
new_basic_session::<C>(TestConnectionConfig::default()).await;
|
||||
|
||||
@@ -157,6 +157,7 @@ pub async fn spawn_acp_server_in_process(
|
||||
goose_mode: GooseMode,
|
||||
provider_factory: Option<AcpProviderFactory>,
|
||||
current_model: &str,
|
||||
disable_session_naming: bool,
|
||||
) -> (DuplexTransport, JoinHandle<()>, Arc<PermissionManager>) {
|
||||
fs::create_dir_all(data_root).unwrap();
|
||||
// TODO: Paths::in_state_dir is global, ignoring per-test data_root
|
||||
@@ -190,7 +191,7 @@ pub async fn spawn_acp_server_in_process(
|
||||
data_root.to_path_buf(),
|
||||
data_root.to_path_buf(),
|
||||
goose_mode,
|
||||
true,
|
||||
disable_session_naming,
|
||||
GoosePlatform::GooseCli,
|
||||
)
|
||||
.await
|
||||
@@ -221,6 +222,12 @@ pub enum Notification {
|
||||
AvailableCommands,
|
||||
CurrentMode,
|
||||
ConfigOption,
|
||||
SessionInfoUpdate {
|
||||
title: Option<String>,
|
||||
updated_at: Option<String>,
|
||||
message_count: Option<u64>,
|
||||
user_set_name: Option<bool>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn to_notifications(updates: &[SessionUpdate]) -> Vec<Notification> {
|
||||
@@ -266,6 +273,19 @@ pub fn to_notifications(updates: &[SessionUpdate]) -> Vec<Notification> {
|
||||
SessionUpdate::AvailableCommandsUpdate(_) => out.push(Notification::AvailableCommands),
|
||||
SessionUpdate::CurrentModeUpdate(_) => out.push(Notification::CurrentMode),
|
||||
SessionUpdate::ConfigOptionUpdate(_) => out.push(Notification::ConfigOption),
|
||||
SessionUpdate::SessionInfoUpdate(update) => {
|
||||
let meta = update.meta.as_ref();
|
||||
out.push(Notification::SessionInfoUpdate {
|
||||
title: update.title.value().cloned(),
|
||||
updated_at: update.updated_at.value().cloned(),
|
||||
message_count: meta
|
||||
.and_then(|m| m.get("messageCount"))
|
||||
.and_then(|v| v.as_u64()),
|
||||
user_set_name: meta
|
||||
.and_then(|m| m.get("userSetName"))
|
||||
.and_then(|v| v.as_bool()),
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -482,6 +502,7 @@ pub struct TestConnectionConfig {
|
||||
pub strip_config_options: bool,
|
||||
// The model the server-side provider starts with. Defaults to TEST_MODEL.
|
||||
pub current_model: String,
|
||||
pub disable_session_naming: bool,
|
||||
}
|
||||
|
||||
impl Default for TestConnectionConfig {
|
||||
@@ -498,6 +519,7 @@ impl Default for TestConnectionConfig {
|
||||
terminal: None,
|
||||
strip_config_options: false,
|
||||
current_model: TEST_MODEL.to_string(),
|
||||
disable_session_naming: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +162,7 @@ impl Connection for AcpProviderConnection {
|
||||
goose_mode,
|
||||
config.provider_factory,
|
||||
¤t_model,
|
||||
config.disable_session_naming,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ impl Connection for AcpServerConnection {
|
||||
config.goose_mode,
|
||||
config.provider_factory,
|
||||
&config.current_model,
|
||||
config.disable_session_naming,
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ use common_tests::{
|
||||
run_model_list, run_model_set, run_model_set_error_session_not_found,
|
||||
run_new_session_returns_initial_config, run_permission_persistence, run_prompt_basic,
|
||||
run_prompt_codemode, run_prompt_error, run_prompt_image, run_prompt_image_attachment,
|
||||
run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill, run_shell_terminal_false,
|
||||
run_shell_terminal_true,
|
||||
run_prompt_mcp, run_prompt_model_mismatch, run_prompt_skill,
|
||||
run_session_name_update_notification, run_shell_terminal_false, run_shell_terminal_true,
|
||||
};
|
||||
|
||||
tests_config_option_set_error!(AcpServerConnection);
|
||||
@@ -33,6 +33,11 @@ fn test_list_sessions() {
|
||||
run_test(async { run_list_sessions::<AcpServerConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_session_name_update_notification() {
|
||||
run_test(async { run_session_name_update_notification::<AcpServerConnection>().await });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_close_session() {
|
||||
run_test(async { run_close_session::<AcpServerConnection>().await });
|
||||
|
||||
Reference in New Issue
Block a user