返回 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 extension: None,
89 }
90 }
91
92 fn slice(commands: &[UserCommandMetadata]) -> Vec<&UserCommandMetadata> {
93 commands.iter().collect()
94 }
95
96 #[test]
97 fn canonical_name_claims_builtin_canonical_token() {
98 let user = metadata("help", &[], false);
99 let owned = [user];
100 let users = slice(&owned);
101 assert!(user_command_shadows_builtin_canonical(
102 help_builtin(),
103 &users
104 ));
105 }
106
107 #[test]
108 fn accepted_alias_claims_builtin_canonical_token() {
109 let user = metadata("assistant", &["help"], false);
110 let owned = [user];
111 let users = slice(&owned);
112 assert!(user_command_shadows_builtin_canonical(
113 help_builtin(),
114 &users
115 ));
116 }
117
118 #[test]
119 fn hidden_user_command_retains_canonical_shadow_ownership() {
120 let user = metadata("help", &[], true);
121 let owned = [user];
122 let users = slice(&owned);
123 assert!(user_command_shadows_builtin_canonical(
124 help_builtin(),
125 &users
126 ));
127 }
128
129 #[test]
130 fn unrelated_user_commands_do_not_shadow() {
131 let user = metadata("assistant", &["a"], false);
132 let owned = [user];
133 let users = slice(&owned);
134 assert!(!user_command_shadows_builtin_canonical(
135 help_builtin(),
136 &users
137 ));
138 assert!(!user_command_shadows_builtin_alias("?", &users));
139 }
140
141 #[test]
142 fn canonical_name_claims_builtin_alias_token() {
143 let user = metadata("?", &[], false);
144 let owned = [user];
145 let users = slice(&owned);
146 assert!(user_command_shadows_builtin_alias("?", &users));
147 }
148
149 #[test]
150 fn accepted_alias_claims_builtin_alias_token() {
151 let user = metadata("assistant", &["image"], false);
152 let owned = [user];
153 let users = slice(&owned);
154 assert!(user_command_shadows_builtin_alias("image", &users));
155 }
156
157 #[test]
158 fn unshadowed_aliases_preserve_declaration_order() {
159 let user = metadata("assistant", &["image"], false);
160 let owned = [user];
161 let users = slice(&owned);
162 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
163 assert_eq!(aliases, vec!["media", "fujian"]);
164 }
165
166 #[test]
167 fn claimed_canonical_token_does_not_change_alias_projection() {
168 // A user command claiming the built-in canonical token makes the whole
169 // built-in invisible; the alias projection stays stable so consumers
170 // can rely on it for the description fallback path.
171 let user = metadata("attach", &[], false);
172 let owned = [user];
173 let users = slice(&owned);
174 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
175 assert_eq!(aliases, vec!["image", "media", "fujian"]);
176 }
177
178 #[test]
179 fn hidden_commands_shadow_aliases_too() {
180 let user = metadata("secret", &["image"], true);
181 let owned = [user];
182 let users = slice(&owned);
183 assert!(user_command_shadows_builtin_alias("image", &users));
184 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
185 assert_eq!(aliases, vec!["media", "fujian"]);
186 }
187
188 #[test]
189 fn all_aliases_shadowed_yields_empty_projection() {
190 let user = metadata("assistant", &["image", "media", "fujian"], false);
191 let owned = [user];
192 let users = slice(&owned);
193 let aliases = unshadowed_builtin_aliases(attach_builtin(), &users);
194 assert!(aliases.is_empty());
195 }
196
197 #[test]
198 fn rejected_aliases_do_not_shadow() {
199 // Rejected aliases are absent from accepted metadata and therefore
200 // cannot claim any token.
201 let user = metadata("assistant", &[], false);
202 let owned = [user];
203 let users = slice(&owned);
204 assert!(!user_command_shadows_builtin_alias("collision", &users));
205 }
206
207 #[test]
208 fn empty_metadata_shadows_nothing() {
209 let users: Vec<&UserCommandMetadata> = Vec::new();
210 assert!(!user_command_shadows_builtin_canonical(
211 help_builtin(),
212 &users
213 ));
214 assert!(!user_command_shadows_builtin_alias("?", &users));
215 let aliases = unshadowed_builtin_aliases(help_builtin(), &users);
216 assert_eq!(aliases, vec!["?", "bangzhu", "帮助"]);
217 }
218
219 #[test]
220 fn discovery_predicates_agree_with_registry_lookup() {
221 // Contract guard: the shared predicates must agree with the registry's
222 // own alias-aware `get` lookup used by the palette, so the Phase 3/4
223 // rewiring cannot introduce a behavioral divergence.
224 let user = metadata("assistant", &["help"], false);
225 let owned = [user];
226 let users = slice(&owned);
227 let registry = crate::commands::user_registry::UserCommandRegistry::from_loaded(vec![(
228 "assistant".to_string(),
229 "---\ndescription: d\naliases: help\n---\nbody".to_string(),
230 )]);
231 assert_eq!(
232 registry.get("help").is_some(),
233 user_command_shadows_builtin_canonical(help_builtin(), &users),
234 "registry lookup and shared predicate must agree"
235 );
236 }
237 }
238
238 lines RUST