返回 CodeWhale
rename.rs
根目录 / crates / tui / src / commands / groups / session / rename.rs
1 //! `/rename` command — portable handler over the session-control facet.
2 //!
3 //! The handler owns sanitization, blank and 100-character validation, and
4 //! exact message composition. The atomic `rename_session` delegate owns only
5 //! first-snapshot recovery, persistence, and publication so the policy moves
6 //! with the handler while host mutation ordering cannot drift.
7
8 use super::CommandResult;
9 use codewhale_command_contract::facets::CommandSessionControlContext;
10 use codewhale_command_contract::handler::{CommandContexts, CommandHandler};
11 use codewhale_command_contract::metadata::{
12 CommandInfo as ContractInfo, RegisterCommand as ContractRegisterCommand,
13 };
14
15 pub(in crate::commands) struct RenameCmd;
16
17 pub(in crate::commands) const CONTRACT_INFO: ContractInfo = ContractInfo {
18 name: "rename",
19 aliases: &["gaiming", "chongmingming"],
20 usage: "/rename <new title>",
21 description_key: "cmd_rename_description",
22 };
23
24 impl ContractRegisterCommand<CommandResult> for RenameCmd {
25 fn info() -> &'static ContractInfo {
26 &CONTRACT_INFO
27 }
28 fn handler() -> CommandHandler<CommandResult> {
29 CommandHandler::Contextual {
30 capabilities: codewhale_command_contract::handler::CommandCapabilities::SESSION_CONTROL,
31 handler: rename_contextual,
32 }
33 }
34 }
35
36 pub(in crate::commands) fn rename_contextual(
37 contexts: CommandContexts<'_>,
38 arg: Option<&str>,
39 ) -> CommandResult {
40 let mut parts = contexts.into_parts();
41 let Some(control) = parts.control.as_deref_mut() else {
42 return CommandResult::error("Command capability unavailable: session_control".to_string());
43 };
44 rename_portable(control, arg)
45 }
46
47 pub(in crate::commands) fn rename_portable(
48 control: &mut dyn CommandSessionControlContext,
49 arg: Option<&str>,
50 ) -> CommandResult {
51 let Some(raw) = arg else {
52 return CommandResult::error("Usage: /rename <new title>");
53 };
54 let sanitized = control.sanitize_session_title(raw);
55 let title = sanitized.trim();
56 if title.is_empty() {
57 return CommandResult::error("Usage: /rename <new title>");
58 }
59 if title.chars().count() > super::MAX_TITLE_LEN {
60 return CommandResult::error(format!(
61 "Title too long (max {} characters)",
62 super::MAX_TITLE_LEN
63 ));
64 }
65 match control.rename_session(title) {
66 Ok(receipt) => CommandResult::message(format!("Session renamed to \"{}\"", receipt.title)),
67 Err(error) => CommandResult::error(error),
68 }
69 }
70
71 #[cfg(test)]
72 mod tests {
73 use super::super::control_test_support::message;
74 use super::*;
75 use codewhale_command_contract::facets::SessionTitleReceipt;
76
77 fn control_fake() -> super::super::control_test_support::FakeControl {
78 super::super::control_test_support::FakeControl::default()
79 }
80
81 #[test]
82 fn rename_usage_boundaries_and_success_messages_are_exact() {
83 let mut no_arg = control_fake();
84 let result = rename_portable(&mut no_arg, None);
85 assert!(result.is_error);
86 assert_eq!(message(&result), "Usage: /rename <new title>");
87 assert!(
88 no_arg.calls.borrow().is_empty(),
89 "no delegate call for blank input"
90 );
91
92 let mut blank = control_fake();
93 let result = rename_portable(&mut blank, Some(" "));
94 assert!(result.is_error);
95 assert_eq!(message(&result), "Usage: /rename <new title>");
96 assert_eq!(
97 blank.calls.borrow().as_slice(),
98 ["sanitize_session_title( )"],
99 "sanitized blank input never reaches host mutation"
100 );
101
102 let mut oversized = control_fake();
103 oversized.sanitized_title = Some("x".repeat(super::super::MAX_TITLE_LEN + 1));
104 let result = rename_portable(&mut oversized, Some("raw"));
105 assert!(result.is_error);
106 assert_eq!(message(&result), "Title too long (max 100 characters)");
107 assert_eq!(
108 oversized.calls.borrow().as_slice(),
109 ["sanitize_session_title(raw)"],
110 "length policy runs before host mutation"
111 );
112
113 let mut ok = control_fake();
114 ok.rename = Some(Ok(SessionTitleReceipt {
115 title: "New Name".to_string(),
116 }));
117 let result = rename_portable(&mut ok, Some("New Name"));
118 assert!(!result.is_error);
119 assert_eq!(message(&result), "Session renamed to \"New Name\"");
120 assert_eq!(
121 ok.calls.borrow().as_slice(),
122 [
123 "sanitize_session_title(New Name)",
124 "rename_session(New Name)"
125 ]
126 );
127 assert!(result.action.is_none(), "/rename emits no action");
128 }
129
130 #[test]
131 fn rename_host_errors_pass_through_exactly() {
132 let mut fake = control_fake();
133 fake.rename = Some(Err("Could not save session: boom".to_string()));
134 let result = rename_portable(&mut fake, Some("Whatever"));
135 assert!(result.is_error);
136 assert_eq!(message(&result), "Could not save session: boom");
137 }
138
139 #[test]
140 fn rename_missing_control_authority_fails_safely() {
141 let contexts = codewhale_command_contract::handler::CommandContexts::empty();
142 let result = rename_contextual(contexts, None);
143 assert!(result.is_error);
144 assert_eq!(
145 message(&result),
146 "Command capability unavailable: session_control"
147 );
148 }
149 }
150
150 lines RUST