返回 CodeWhale
discovery.rs
根目录 / crates / tui / src / commands / discovery.rs
1 //! Shared command-discovery shadowing contract.
2 //!
3 //! Both the command palette (`tui::command_palette`) and slash completion
4 //! (`tui::widgets`) must interpret user-command ownership over built-in
5 //! command tokens identically. This module owns the three decisions they
6 //! share:
7 //!
8 //! - whether a user command (by canonical name or accepted alias) shadows a
9 //! built-in canonical token,
10 //! - whether a user command shadows a specific built-in alias token, and
11 //! - which built-in aliases remain unshadowed and may be presented.
12 //!
13 //! The functions are pure: they consume immutable built-in metadata and
14 //! accepted user-command metadata and never persist or cache registry state.
15
16 use super::traits::CommandInfo;
17 use super::user_registry::UserCommandMetadata;
18
19 /// Returns true when any user command claims the built-in canonical token,
20 /// either through its canonical name or through an accepted alias.
21 ///
22 /// Hidden user commands retain token ownership even though they are excluded
23 /// from discovery output rows.
24 pub fn user_command_shadows_builtin_canonical(
25 builtin: &CommandInfo,
26 user_commands: &[&UserCommandMetadata],
27 ) -> bool {
28 user_commands.iter().any(|user| {
29 user.name == builtin.name || user.aliases.iter().any(|alias| alias == builtin.name)
30 })
31 }
32
33 /// Returns true when any user command claims the given built-in alias token,
34 /// either through its canonical name or through an accepted alias.
35 pub fn user_command_shadows_builtin_alias(
36 builtin_alias: &str,
37 user_commands: &[&UserCommandMetadata],
38 ) -> bool {
39 user_commands.iter().any(|user| {
40 user.name == builtin_alias || user.aliases.iter().any(|alias| alias == builtin_alias)
41 })
42 }
43
44 /// Returns the built-in aliases that are not claimed by any user command,
45 /// preserving the built-in declaration order.
46 ///
47 /// The built-in canonical token itself is not part of this projection; callers
48 /// decide canonical visibility through [`user_command_shadows_builtin_canonical`].
49 pub fn unshadowed_builtin_aliases<'a>(
50 builtin: &'a CommandInfo,
51 user_commands: &[&UserCommandMetadata],
52 ) -> Vec<&'a str> {
53 builtin
54 .aliases
55 .iter()
56 .copied()
57 .filter(|alias| !user_command_shadows_builtin_alias(alias, user_commands))
58 .collect()
59 }
60
61 #[cfg(test)]
62 mod tests {
63 use super::*;
64
65 use crate::commands::get_command_info;
66
67 fn help_builtin() -> &'static CommandInfo {
68 get_command_info("help").expect("built-in help must be registered")
69 }
70
71 fn attach_builtin() -> &'static CommandInfo {
72 get_command_info("attach").expect("built-in attach must be registered")
73 }
74
75 fn metadata(name: &str, aliases: &[&str], hidden: bool) -> UserCommandMetadata {
76 UserCommandMetadata {
77 name: name.to_string(),
78 body: String::new(),
79 description: Some(format!("description of {name}")),
80 usage: None,
81 arguments: None,
82 argument_hint: None,
83 allowed_tools: None,
84 pausable: false,
85 aliases: aliases.iter().map(|s| s.to_string()).collect(),
86 hidden,
87 plugin_authority: None,
88 }
89 }
90
91 fn slice(commands: &[UserCommandMetadata]) -> Vec<&UserCommandMetadata> {
92 commands.iter().collect()
93 }
94
95 #[test]
96 fn canonical_name_claims_builtin_canonical_token() {
97 let user = metadata("help", &[], false);
98 let owned = [user];
99 let users = slice(&owned);
100 assert!(user_command_shadows_builtin_canonical(
101 help_builtin(),
102 &users
103 ));
104 }
105
106 #[test]
107 fn accepted_alias_claims_builtin_canonical_token() {
108 let user = metadata("assistant", &["help"], false);
109 let owned = [user];
110 let users = slice(&owned);
111 assert!(user_command_shadows_builtin_canonical(
112 help_builtin(),
113 &users
114 ));
115 }
116
117 #[test]
118 fn hidden_user_command_retains_canonical_shadow_ownership() {
119 let user = metadata("help", &[], true);
120 let owned = [user];
121 let users = slice(&owned);
122 assert!(user_command_shadows_builtin_canonical(
123 help_builtin(),
124 &users
125 ));
126 }
127
128 #[test]
129 fn unrelated_user_commands_do_not_shadow() {
130 let user = metadata("assistant", &["a"], false);
131 let owned = [user];
132 let users = slice(&owned);
133 assert!(!user_command_shadows_builtin_canonical(
134 help_builtin(),
135 &users
136 ));
137 assert!(!user_command_shadows_builtin_alias("?", &users));
138 }
139
140 #[test]
141 fn canonical_name_claims_builtin_alias_token() {
142 let user = metadata("?", &[], false);
143 let owned = [user];
144 let users = slice(&owned);
145 assert!(user_command_shadows_builtin_alias("?", &users));
146 }
147
148 #[test]
149 fn accepted_alias_claims_builtin_alias_token() {
150 let user = metadata("assistant", &["image"], false);
151 let owned = [user];
152 let users = slice(&owned);
153 assert!(user_command_shadows_builtin_alias("image", &users));
154 }
155
156 #[test]
157 fn unshadowed_aliases_preserve_declaration_order() {
158 let user = metadata("assistant", &["image"], false);
159 let owned = [user];
160 let users = slice(&owned);
161 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
162 assert_eq!(aliases, vec!["media", "fujian"]);
163 }
164
165 #[test]
166 fn claimed_canonical_token_does_not_change_alias_projection() {
167 // A user command claiming the built-in canonical token makes the whole
168 // built-in invisible; the alias projection stays stable so consumers
169 // can rely on it for the description fallback path.
170 let user = metadata("attach", &[], false);
171 let owned = [user];
172 let users = slice(&owned);
173 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
174 assert_eq!(aliases, vec!["image", "media", "fujian"]);
175 }
176
177 #[test]
178 fn hidden_commands_shadow_aliases_too() {
179 let user = metadata("secret", &["image"], true);
180 let owned = [user];
181 let users = slice(&owned);
182 assert!(user_command_shadows_builtin_alias("image", &users));
183 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
184 assert_eq!(aliases, vec!["media", "fujian"]);
185 }
186
187 #[test]
188 fn all_aliases_shadowed_yields_empty_projection() {
189 let user = metadata("assistant", &["image", "media", "fujian"], false);
190 let owned = [user];
191 let users = slice(&owned);
192 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
193 assert!(aliases.is_empty());
194 }
195
196 #[test]
197 fn rejected_aliases_do_not_shadow() {
198 // Rejected aliases are absent from accepted metadata and therefore
199 // cannot claim any token.
200 let user = metadata("assistant", &[], false);
201 let owned = [user];
202 let users = slice(&owned);
203 assert!(!user_command_shadows_builtin_alias("collision", &users));
204 }
205
206 #[test]
207 fn empty_metadata_shadows_nothing() {
208 let users: Vec<&UserCommandMetadata> = Vec::new();
209 assert!(!user_command_shadows_builtin_canonical(
210 help_builtin(),
211 &users
212 ));
213 assert!(!user_command_shadows_builtin_alias("?", &users));
214 let aliases = unshadowed_builtin_aliases(help_builtin(), &users);
215 assert_eq!(aliases, vec!["?", "bangzhu", "帮助"]);
216 }
217
218 #[test]
219 fn discovery_predicates_agree_with_registry_lookup() {
220 // Contract guard: the shared predicates must agree with the registry's
221 // own alias-aware `get` lookup used by the palette, so the Phase 3/4
222 // rewiring cannot introduce a behavioral divergence.
223 let user = metadata("assistant", &["help"], false);
224 let owned = [user];
225 let users = slice(&owned);
226 let registry = crate::commands::user_registry::UserCommandRegistry::from_loaded(vec![(
227 "assistant".to_string(),
228 "---\ndescription: d\naliases: help\n---\nbody".to_string(),
229 )]);
230 assert_eq!(
231 registry.get("help").is_some(),
232 user_command_shadows_builtin_canonical(help_builtin(), &users),
233 "registry lookup and shared predicate must agree"
234 );
235 }
236 }
237
237 lines RUST