use crate::command::context::CommandContext; use crate::command::handler::{CommandHandler, CommandMetadata}; use crate::command::handlers::list_topics::TopicSummary; use crate::command::response::{CommandError, CommandResponse, MessageKind}; use crate::command::Command; use crate::storage::SessionStore; use async_trait::async_trait; use std::sync::Arc; /// 重命名话题命令处理器 pub struct RenameTopicCommandHandler { store: Arc, } impl RenameTopicCommandHandler { pub fn new(store: Arc) -> Self { Self { store } } } #[async_trait] impl CommandHandler for RenameTopicCommandHandler { fn can_handle(&self, cmd: &Command) -> bool { matches!(cmd, Command::RenameTopic { .. }) } fn metadata(&self) -> Option { Some(CommandMetadata { name: "rename", description: "重命名指定话题", usage: "/rename ", }) } async fn handle( &self, cmd: Command, ctx: CommandContext, ) -> Result { match cmd { Command::RenameTopic { topic_id, title } => { handle_rename_topic(self, topic_id, title, ctx).await } _ => unreachable!(), } } } async fn handle_rename_topic( handler: &RenameTopicCommandHandler, topic_id: String, title: String, ctx: CommandContext, ) -> Result { let session_id = ctx .session_id .as_deref() .ok_or_else(|| CommandError::new("NO_SESSION", "No active session"))?; // 校验新标题非空 let trimmed_title = title.trim(); if trimmed_title.is_empty() { return Err(CommandError::new( "INVALID_TITLE", "Topic title must not be empty", )); } // 验证话题存在 let topic = handler .store .get_topic(&topic_id) .map_err(|e| CommandError::new("GET_TOPIC_ERROR", e.to_string()))? .ok_or_else(|| { CommandError::new("TOPIC_NOT_FOUND", format!("Topic not found: {}", topic_id)) })?; let old_title = topic.title.clone(); // 标题未变化时直接返回当前列表,避免无意义写入 if old_title == trimmed_title { let topics = handler .store .list_topics(session_id) .map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?; let topic_summaries = serialize_summaries(&topics); return Ok(CommandResponse::success(ctx.request_id) .with_message(MessageKind::Notification, &format!("✓ 话题标题未变化: {}", trimmed_title)) .with_metadata("topics", &topic_summaries) .with_metadata("topic_id", &topic_id) .with_metadata("title", trimmed_title) .with_metadata("session_id", session_id)); } // 执行重命名(存储层方法已存在) handler .store .update_topic_title(&topic_id, trimmed_title) .map_err(|e| CommandError::new("RENAME_TOPIC_ERROR", e.to_string()))?; // 查询更新后的话题列表,返回给前端刷新侧边栏 let topics = handler .store .list_topics(session_id) .map_err(|e| CommandError::new("LIST_TOPICS_ERROR", e.to_string()))?; let topic_summaries = serialize_summaries(&topics); let message = format!("✓ 已重命名话题: {} → {}", old_title, trimmed_title); Ok(CommandResponse::success(ctx.request_id) .with_message(MessageKind::Notification, &message) .with_metadata("topics", &topic_summaries) .with_metadata("topic_id", &topic_id) .with_metadata("title", trimmed_title) .with_metadata("session_id", session_id)) } fn serialize_summaries(topics: &[crate::storage::TopicRecord]) -> String { let summaries: Vec = topics .iter() .map(|t| TopicSummary { topic_id: t.id.clone(), session_id: t.session_id.clone(), title: t.title.clone(), description: t.description.clone().filter(|d| !d.is_empty()), message_count: t.message_count, created_at: t.created_at, last_active_at: t.last_active_at, }) .collect(); serde_json::to_string(&summaries).unwrap_or_else(|_| "[]".to_string()) } #[cfg(test)] mod tests { use super::*; use crate::storage::SessionStore; fn create_test_handler() -> RenameTopicCommandHandler { let store = Arc::new(SessionStore::in_memory().unwrap()); RenameTopicCommandHandler::new(store) } #[tokio::test] async fn test_rename_topic_success() { let handler = create_test_handler(); let store = handler.store.clone(); let session = store.create_session("test_channel", Some("test")).unwrap(); let topic = store .create_topic(&session.id, "old title", None) .unwrap(); let ctx = CommandContext::new("test", "test_channel") .with_session_id(&session.id) .with_chat_id(&session.id); let cmd = Command::RenameTopic { topic_id: topic.id.clone(), title: "new title".to_string(), }; let result = handler.handle(cmd, ctx).await; assert!(result.is_ok()); let resp = result.unwrap(); assert!(resp.success); assert_eq!(resp.metadata.get("title").map(String::as_str), Some("new title")); assert_eq!(resp.metadata.get("topic_id").map(String::as_str), Some(topic.id.as_str())); assert!(resp.metadata.contains_key("topics")); // 验证存储层已更新 let updated = store.get_topic(&topic.id).unwrap().unwrap(); assert_eq!(updated.title, "new title"); } #[tokio::test] async fn test_rename_topic_empty_title() { let handler = create_test_handler(); let store = handler.store.clone(); let session = store.create_session("test_channel", Some("test")).unwrap(); let topic = store .create_topic(&session.id, "old title", None) .unwrap(); let ctx = CommandContext::new("test", "test_channel") .with_session_id(&session.id) .with_chat_id(&session.id); let cmd = Command::RenameTopic { topic_id: topic.id.clone(), title: " ".to_string(), }; let result = handler.handle(cmd, ctx).await; assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.code, "INVALID_TITLE"); // 标题未被修改 let unchanged = store.get_topic(&topic.id).unwrap().unwrap(); assert_eq!(unchanged.title, "old title"); } #[tokio::test] async fn test_rename_nonexistent_topic() { let handler = create_test_handler(); let store = handler.store.clone(); let session = store.create_session("test_channel", Some("test")).unwrap(); let ctx = CommandContext::new("test", "test_channel") .with_session_id(&session.id) .with_chat_id(&session.id); let cmd = Command::RenameTopic { topic_id: "nonexistent".to_string(), title: "new title".to_string(), }; let result = handler.handle(cmd, ctx).await; assert!(result.is_err()); let err = result.unwrap_err(); assert_eq!(err.code, "TOPIC_NOT_FOUND"); } #[tokio::test] async fn test_rename_topic_same_title_skips_write() { let handler = create_test_handler(); let store = handler.store.clone(); let session = store.create_session("test_channel", Some("test")).unwrap(); let topic = store .create_topic(&session.id, "same title", None) .unwrap(); let original_updated_at = store.get_topic(&topic.id).unwrap().unwrap().updated_at; // 等待一秒确保 updated_at 会变化(如果真的写入) tokio::time::sleep(std::time::Duration::from_secs(1)).await; let ctx = CommandContext::new("test", "test_channel") .with_session_id(&session.id) .with_chat_id(&session.id); let cmd = Command::RenameTopic { topic_id: topic.id.clone(), title: "same title".to_string(), }; let result = handler.handle(cmd, ctx).await; assert!(result.is_ok()); // updated_at 未变化说明未触发写入 let after = store.get_topic(&topic.id).unwrap().unwrap(); assert_eq!(after.updated_at, original_updated_at); } #[test] fn test_can_handle() { let handler = create_test_handler(); assert!(handler.can_handle(&Command::RenameTopic { topic_id: "test".to_string(), title: "test".to_string(), })); assert!(!handler.can_handle(&Command::Help)); assert!(!handler.can_handle(&Command::DeleteTopic { topic_id: "test".to_string(), })); } }