返回 CodeWhale
epic_discovery_acceptance.rs
根目录 / crates / tui / src / commands / epic_discovery_acceptance.rs
1 //! Focused Gherkin acceptance evidence for FEAT-012 palette and slash
2 //! completion discovery filtering. Bound through separate scenario-level
3 //! cucumber worlds that prove the six FEAT-012 acceptance criteria plus the
4 //! alias-aware canonical unification decision (AT-010) through the live
5 //! palette builder, the live completion function, and the live dispatch
6 //! entry point.
7
8 use cucumber::{World as _, given, then, when, writer::Stats as _};
9 use tempfile::TempDir;
10
11 use crate::commands::{self, CommandResult};
12 use crate::config::ApiProvider;
13 use crate::config::Config;
14 use crate::tui::app::{App, TuiOptions};
15 use crate::tui::command_palette::{self, CommandPaletteEntry};
16 use crate::tui::widgets::{self, SlashMenuEntry};
17 use codewhale_localization::Locale;
18
19 // --- FEAT-012 discovery filtering constants ---
20
21 const DISCOVERY_FEATURE_NAME: &str = "FEAT-012 Discovery Filtering (Palette And Slash Completion)";
22 const DISCOVERY_FEATURE_PATH: &str = concat!(
23 env!("CARGO_MANIFEST_DIR"),
24 "/tests/features/feat-012-discovery-filtering.feature"
25 );
26
27 const AC1_SCENARIO: &str = "AC1 Visible user command appears once in the palette with metadata";
28 const AC2_SCENARIO: &str = "AC2 Hidden user command is runnable but excluded from the palette";
29 const AC3_SCENARIO: &str = "AC3 Visible user command appears in matching slash completion";
30 const AC4_SCENARIO: &str = "AC4 Hidden user command is excluded from slash completion";
31 const AC5_SCENARIO: &str = "AC5 User canonical shadow suppresses a built-in in both surfaces";
32 const AC6_SCENARIO: &str =
33 "AC6 User command shadows a built-in alias without hiding canonical access";
34 const AT010_SCENARIO: &str =
35 "AT-010 Alias-aware unification - accepted user alias claims a built-in canonical token";
36
37 // --- Shared helpers ---
38
39 fn create_discovery_app(tmpdir: &TempDir) -> App {
40 let options = TuiOptions {
41 skills_dir: tmpdir.path().join("skills"),
42 memory_path: tmpdir.path().join("memory.md"),
43 notes_path: tmpdir.path().join("notes.txt"),
44 mcp_config_path: tmpdir.path().join("mcp.json"),
45 ..crate::test_support::test_tui_options(tmpdir.path())
46 };
47 App::new(options, &Config::default())
48 }
49
50 fn write_user_command(tmpdir: &TempDir, name: &str, content: &str) {
51 let commands_dir = tmpdir.path().join(".codewhale").join("commands");
52 std::fs::create_dir_all(commands_dir).expect("create commands dir");
53 let path = tmpdir
54 .path()
55 .join(".codewhale")
56 .join("commands")
57 .join(format!("{name}.md"));
58 std::fs::write(path, content).expect("write user command");
59 }
60
61 fn palette_entries(tmpdir: &TempDir) -> Vec<CommandPaletteEntry> {
62 command_palette::build_entries(
63 Locale::En,
64 tmpdir.path().join("skills").as_path(),
65 false,
66 tmpdir.path(),
67 tmpdir.path().join("mcp.json").as_path(),
68 None,
69 )
70 }
71
72 fn completion_hints(tmpdir: &TempDir, input: &str) -> Vec<SlashMenuEntry> {
73 widgets::slash_completion_hints(
74 input,
75 128,
76 &[],
77 Locale::En,
78 Some(tmpdir.path()),
79 ApiProvider::Deepseek,
80 )
81 }
82
83 fn sent_message(result: &CommandResult) -> String {
84 match &result.action {
85 Some(crate::tui::app::AppAction::SendMessage(message)) => message.clone(),
86 other => panic!("expected SendMessage action, got {other:?}"),
87 }
88 }
89
90 // --- AC1: Visible user command appears once in the palette with metadata ---
91
92 #[derive(cucumber::World)]
93 #[world(init = Self::new)]
94 struct DiscoveryWorld01 {
95 tmpdir: Option<TempDir>,
96 entries: Option<Vec<CommandPaletteEntry>>,
97 }
98
99 impl std::fmt::Debug for DiscoveryWorld01 {
100 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
101 f.debug_struct("DiscoveryWorld01")
102 .field("has_tmpdir", &self.tmpdir.is_some())
103 .field("has_entries", &self.entries.is_some())
104 .finish()
105 }
106 }
107
108 impl DiscoveryWorld01 {
109 fn new() -> Self {
110 Self {
111 tmpdir: None,
112 entries: None,
113 }
114 }
115 }
116
117 #[given("a workspace with a visible user command that has a description and usage")]
118 fn ac1_given_visible_with_metadata(world: &mut DiscoveryWorld01) {
119 let tmpdir = TempDir::new().expect("AC1 TempDir");
120 write_user_command(
121 &tmpdir,
122 "review",
123 "---\ndescription: Review with context\nusage: /review <path>\n---\nreview $ARGUMENTS",
124 );
125 commands::user_registry::reload(Some(tmpdir.path()));
126 world.tmpdir = Some(tmpdir);
127 }
128
129 #[when("the command palette is queried")]
130 fn ac1_when_palette_queried(world: &mut DiscoveryWorld01) {
131 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
132 world.entries = Some(palette_entries(tmpdir));
133 }
134
135 #[then("the user command appears exactly once with its description and usage")]
136 fn ac1_then_user_entry_once(world: &mut DiscoveryWorld01) {
137 let entries = world.entries.as_ref().expect("entries should exist");
138 let rows: Vec<_> = entries
139 .iter()
140 .filter(|entry| entry.label == "/review")
141 .collect();
142 assert_eq!(rows.len(), 1, "exactly one /review row must exist");
143 assert!(
144 rows[0].description.contains("Review with context"),
145 "row must carry the description: {}",
146 rows[0].description
147 );
148 assert!(
149 rows[0].description.contains("/review <path>"),
150 "row must carry the usage: {}",
151 rows[0].description
152 );
153 }
154
155 #[then("no duplicate entry appears for the same effective token")]
156 fn ac1_then_no_duplicate(world: &mut DiscoveryWorld01) {
157 let entries = world.entries.as_ref().expect("entries should exist");
158 let labels: Vec<_> = entries.iter().map(|entry| entry.label.clone()).collect();
159 let duplicates: Vec<_> = labels
160 .iter()
161 .filter(|label| labels.iter().filter(|other| other == label).count() > 1)
162 .collect();
163 assert!(
164 duplicates.is_empty(),
165 "no duplicate labels may appear: {duplicates:?}"
166 );
167 }
168
169 #[tokio::test(flavor = "current_thread")]
170 async fn feat012_ac1_visible_user_command_appears_once_in_palette() {
171 let writer = DiscoveryWorld01::cucumber()
172 .fail_on_skipped()
173 .with_default_cli()
174 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
175 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC1_SCENARIO
176 })
177 .await;
178 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC1_SCENARIO}");
179 assert_eq!(
180 writer.skipped_steps(),
181 0,
182 "scenario skipped: {AC1_SCENARIO}"
183 );
184 assert_eq!(
185 writer.passed_steps(),
186 4,
187 "scenario did not run: {AC1_SCENARIO}"
188 );
189 }
190
191 // --- AC2: Hidden user command is runnable but excluded from the palette ---
192
193 #[derive(cucumber::World)]
194 #[world(init = Self::new)]
195 struct DiscoveryWorld02 {
196 tmpdir: Option<TempDir>,
197 app: Option<Box<App>>,
198 result: Option<CommandResult>,
199 entries: Option<Vec<CommandPaletteEntry>>,
200 }
201
202 impl std::fmt::Debug for DiscoveryWorld02 {
203 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
204 f.debug_struct("DiscoveryWorld02")
205 .field("has_tmpdir", &self.tmpdir.is_some())
206 .field("has_app", &self.app.is_some())
207 .field("has_result", &self.result.is_some())
208 .field("has_entries", &self.entries.is_some())
209 .finish()
210 }
211 }
212
213 impl DiscoveryWorld02 {
214 fn new() -> Self {
215 Self {
216 tmpdir: None,
217 app: None,
218 result: None,
219 entries: None,
220 }
221 }
222 }
223
224 #[given("a workspace with a hidden user command")]
225 fn ac2_given_hidden(world: &mut DiscoveryWorld02) {
226 let tmpdir = TempDir::new().expect("AC2 TempDir");
227 write_user_command(
228 &tmpdir,
229 "secret",
230 "---\ndescription: Internal workflow\nhidden: true\n---\nsecret $ARGUMENTS",
231 );
232 let mut app = create_discovery_app(&tmpdir);
233 app.workspace = tmpdir.path().to_path_buf();
234 commands::user_registry::reload(Some(tmpdir.path()));
235 world.tmpdir = Some(tmpdir);
236 world.app = Some(Box::new(app));
237 }
238
239 #[when("the user runs the hidden command directly")]
240 fn ac2_when_run_hidden(world: &mut DiscoveryWorld02) {
241 let app = world.app.as_deref_mut().expect("app should exist");
242 let result = commands::execute("/secret now", app);
243 world.result = Some(result);
244 }
245
246 #[then("the user command executes")]
247 fn ac2_then_user_executes(world: &mut DiscoveryWorld02) {
248 let result = world.result.as_ref().expect("result should exist");
249 assert!(!result.is_error, "hidden command should run: {result:?}");
250 assert_eq!(sent_message(result), "secret now");
251 }
252
253 #[when("the command palette is queried")]
254 fn ac2_when_palette_queried(world: &mut DiscoveryWorld02) {
255 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
256 world.entries = Some(palette_entries(tmpdir));
257 }
258
259 #[then("the hidden command is absent from the palette")]
260 fn ac2_then_hidden_absent(world: &mut DiscoveryWorld02) {
261 let entries = world.entries.as_ref().expect("entries should exist");
262 assert!(
263 !entries.iter().any(|entry| entry.label == "/secret"),
264 "hidden command must not be listed"
265 );
266 }
267
268 #[tokio::test(flavor = "current_thread")]
269 async fn feat012_ac2_hidden_user_command_runnable_but_excluded_from_palette() {
270 let writer = DiscoveryWorld02::cucumber()
271 .fail_on_skipped()
272 .with_default_cli()
273 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
274 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC2_SCENARIO
275 })
276 .await;
277 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC2_SCENARIO}");
278 assert_eq!(
279 writer.skipped_steps(),
280 0,
281 "scenario skipped: {AC2_SCENARIO}"
282 );
283 assert_eq!(
284 writer.passed_steps(),
285 5,
286 "scenario did not run: {AC2_SCENARIO}"
287 );
288 }
289
290 // --- AC3: Visible user command appears in matching slash completion ---
291
292 #[derive(cucumber::World)]
293 #[world(init = Self::new)]
294 struct DiscoveryWorld03 {
295 tmpdir: Option<TempDir>,
296 hints: Option<Vec<SlashMenuEntry>>,
297 }
298
299 impl std::fmt::Debug for DiscoveryWorld03 {
300 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
301 f.debug_struct("DiscoveryWorld03")
302 .field("has_tmpdir", &self.tmpdir.is_some())
303 .field("has_hints", &self.hints.is_some())
304 .finish()
305 }
306 }
307
308 impl DiscoveryWorld03 {
309 fn new() -> Self {
310 Self {
311 tmpdir: None,
312 hints: None,
313 }
314 }
315 }
316
317 #[given("a workspace with a visible user command that has a description")]
318 fn ac3_given_visible(world: &mut DiscoveryWorld03) {
319 let tmpdir = TempDir::new().expect("AC3 TempDir");
320 write_user_command(
321 &tmpdir,
322 "deploy",
323 "---\ndescription: Deploy target\n---\ndeploy $ARGUMENTS",
324 );
325 commands::user_registry::reload(Some(tmpdir.path()));
326 world.tmpdir = Some(tmpdir);
327 }
328
329 #[when("slash completion is queried with a matching prefix")]
330 fn ac3_when_completion_matching(world: &mut DiscoveryWorld03) {
331 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
332 world.hints = Some(completion_hints(tmpdir, "/dep"));
333 }
334
335 #[then("the user command appears exactly once with its description")]
336 fn ac3_then_user_completion_once(world: &mut DiscoveryWorld03) {
337 let hints = world.hints.as_ref().expect("hints should exist");
338 let rows: Vec<_> = hints.iter().filter(|hint| hint.name == "/deploy").collect();
339 assert_eq!(rows.len(), 1, "exactly one /deploy hint must exist");
340 assert_eq!(rows[0].description, "Deploy target");
341 }
342
343 #[tokio::test(flavor = "current_thread")]
344 async fn feat012_ac3_visible_user_command_appears_in_matching_completion() {
345 let writer = DiscoveryWorld03::cucumber()
346 .fail_on_skipped()
347 .with_default_cli()
348 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
349 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC3_SCENARIO
350 })
351 .await;
352 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC3_SCENARIO}");
353 assert_eq!(
354 writer.skipped_steps(),
355 0,
356 "scenario skipped: {AC3_SCENARIO}"
357 );
358 assert_eq!(
359 writer.passed_steps(),
360 3,
361 "scenario did not run: {AC3_SCENARIO}"
362 );
363 }
364
365 // --- AC4: Hidden user command is excluded from slash completion ---
366
367 #[derive(cucumber::World)]
368 #[world(init = Self::new)]
369 struct DiscoveryWorld04 {
370 tmpdir: Option<TempDir>,
371 hints: Option<Vec<SlashMenuEntry>>,
372 }
373
374 impl std::fmt::Debug for DiscoveryWorld04 {
375 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
376 f.debug_struct("DiscoveryWorld04")
377 .field("has_tmpdir", &self.tmpdir.is_some())
378 .field("has_hints", &self.hints.is_some())
379 .finish()
380 }
381 }
382
383 impl DiscoveryWorld04 {
384 fn new() -> Self {
385 Self {
386 tmpdir: None,
387 hints: None,
388 }
389 }
390 }
391
392 #[given("a workspace with a hidden user command")]
393 fn ac4_given_hidden(world: &mut DiscoveryWorld04) {
394 let tmpdir = TempDir::new().expect("AC4 TempDir");
395 write_user_command(
396 &tmpdir,
397 "secret",
398 "---\ndescription: Internal workflow\nhidden: true\n---\nsecret $ARGUMENTS",
399 );
400 commands::user_registry::reload(Some(tmpdir.path()));
401 world.tmpdir = Some(tmpdir);
402 }
403
404 #[when("slash completion is queried for its prefix")]
405 fn ac4_when_completion_prefix(world: &mut DiscoveryWorld04) {
406 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
407 world.hints = Some(completion_hints(tmpdir, "/sec"));
408 }
409
410 #[then("the hidden command is absent from slash completion")]
411 fn ac4_then_hidden_absent(world: &mut DiscoveryWorld04) {
412 let hints = world.hints.as_ref().expect("hints should exist");
413 assert!(
414 !hints.iter().any(|hint| hint.name == "/secret"),
415 "hidden command must not be suggested"
416 );
417 }
418
419 #[tokio::test(flavor = "current_thread")]
420 async fn feat012_ac4_hidden_user_command_excluded_from_completion() {
421 let writer = DiscoveryWorld04::cucumber()
422 .fail_on_skipped()
423 .with_default_cli()
424 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
425 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC4_SCENARIO
426 })
427 .await;
428 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC4_SCENARIO}");
429 assert_eq!(
430 writer.skipped_steps(),
431 0,
432 "scenario skipped: {AC4_SCENARIO}"
433 );
434 assert_eq!(
435 writer.passed_steps(),
436 3,
437 "scenario did not run: {AC4_SCENARIO}"
438 );
439 }
440
441 // --- AC5: User canonical shadow suppresses a built-in in both surfaces ---
442
443 #[derive(cucumber::World)]
444 #[world(init = Self::new)]
445 struct DiscoveryWorld05 {
446 tmpdir: Option<TempDir>,
447 entries: Option<Vec<CommandPaletteEntry>>,
448 hints: Option<Vec<SlashMenuEntry>>,
449 }
450
451 impl std::fmt::Debug for DiscoveryWorld05 {
452 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
453 f.debug_struct("DiscoveryWorld05")
454 .field("has_tmpdir", &self.tmpdir.is_some())
455 .field("has_entries", &self.entries.is_some())
456 .field("has_hints", &self.hints.is_some())
457 .finish()
458 }
459 }
460
461 impl DiscoveryWorld05 {
462 fn new() -> Self {
463 Self {
464 tmpdir: None,
465 entries: None,
466 hints: None,
467 }
468 }
469 }
470
471 #[given("a workspace with a visible user command owning a built-in canonical token")]
472 fn ac5_given_canonical_shadow(world: &mut DiscoveryWorld05) {
473 let tmpdir = TempDir::new().expect("AC5 TempDir");
474 write_user_command(
475 &tmpdir,
476 "my-help",
477 "---\nname: help\ndescription: My private help\n---\ncustom help $ARGUMENTS",
478 );
479 commands::user_registry::reload(Some(tmpdir.path()));
480 world.tmpdir = Some(tmpdir);
481 }
482
483 #[when("the command palette and slash completion are queried")]
484 fn ac5_when_both_queried(world: &mut DiscoveryWorld05) {
485 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
486 world.entries = Some(palette_entries(tmpdir));
487 world.hints = Some(completion_hints(tmpdir, "/help"));
488 }
489
490 #[then("only the user-owned discovery entry appears for that token")]
491 fn ac5_then_only_user_entry(world: &mut DiscoveryWorld05) {
492 let entries = world.entries.as_ref().expect("entries should exist");
493 let palette_rows: Vec<_> = entries
494 .iter()
495 .filter(|entry| entry.label == "/help")
496 .collect();
497 assert_eq!(palette_rows.len(), 1, "exactly one palette /help row");
498 assert!(
499 palette_rows[0].description.contains("My private help"),
500 "palette row must be user-owned: {}",
501 palette_rows[0].description
502 );
503
504 let hints = world.hints.as_ref().expect("hints should exist");
505 let completion_rows: Vec<_> = hints.iter().filter(|hint| hint.name == "/help").collect();
506 assert_eq!(completion_rows.len(), 1, "exactly one completion /help row");
507 }
508
509 #[then("its metadata identifies the user command")]
510 fn ac5_then_user_metadata(world: &mut DiscoveryWorld05) {
511 let hints = world.hints.as_ref().expect("hints should exist");
512 let completion_rows: Vec<_> = hints.iter().filter(|hint| hint.name == "/help").collect();
513 assert_eq!(completion_rows.len(), 1);
514 assert!(
515 completion_rows[0].description.contains("My private help"),
516 "completion row must carry user metadata: {}",
517 completion_rows[0].description
518 );
519 }
520
521 #[tokio::test(flavor = "current_thread")]
522 async fn feat012_ac5_canonical_shadow_suppresses_builtin_in_both_surfaces() {
523 let writer = DiscoveryWorld05::cucumber()
524 .fail_on_skipped()
525 .with_default_cli()
526 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
527 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC5_SCENARIO
528 })
529 .await;
530 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC5_SCENARIO}");
531 assert_eq!(
532 writer.skipped_steps(),
533 0,
534 "scenario skipped: {AC5_SCENARIO}"
535 );
536 assert_eq!(
537 writer.passed_steps(),
538 4,
539 "scenario did not run: {AC5_SCENARIO}"
540 );
541 }
542
543 // --- AC6: User command shadows a built-in alias without hiding canonical access ---
544
545 #[derive(cucumber::World)]
546 #[world(init = Self::new)]
547 struct DiscoveryWorld06 {
548 tmpdir: Option<TempDir>,
549 alias_hints: Option<Vec<SlashMenuEntry>>,
550 canonical_hints: Option<Vec<SlashMenuEntry>>,
551 }
552
553 impl std::fmt::Debug for DiscoveryWorld06 {
554 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
555 f.debug_struct("DiscoveryWorld06")
556 .field("has_tmpdir", &self.tmpdir.is_some())
557 .field("has_alias_hints", &self.alias_hints.is_some())
558 .field("has_canonical_hints", &self.canonical_hints.is_some())
559 .finish()
560 }
561 }
562
563 impl DiscoveryWorld06 {
564 fn new() -> Self {
565 Self {
566 tmpdir: None,
567 alias_hints: None,
568 canonical_hints: None,
569 }
570 }
571 }
572
573 #[given("a workspace with a visible user command owning a built-in alias token")]
574 fn ac6_given_alias_shadow(world: &mut DiscoveryWorld06) {
575 let tmpdir = TempDir::new().expect("AC6 TempDir");
576 write_user_command(
577 &tmpdir,
578 "image-review",
579 "---\ndescription: Review an image\nalias: image\n---\nreview image",
580 );
581 commands::user_registry::reload(Some(tmpdir.path()));
582 world.tmpdir = Some(tmpdir);
583 }
584
585 #[when("slash completion is queried for the shadowed alias")]
586 fn ac6_when_alias_completion(world: &mut DiscoveryWorld06) {
587 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
588 world.alias_hints = Some(completion_hints(tmpdir, "/image"));
589 }
590
591 #[then("the user command appears and the aliased built-in does not")]
592 fn ac6_then_alias_owned(world: &mut DiscoveryWorld06) {
593 let hints = world
594 .alias_hints
595 .as_ref()
596 .expect("alias hints should exist");
597 assert!(
598 hints.iter().any(|hint| hint.name == "/image-review"),
599 "user command must complete through the /image alias"
600 );
601 assert!(
602 !hints.iter().any(|hint| hint.name == "/attach"),
603 "built-in /attach must not complete through shadowed /image alias"
604 );
605 }
606
607 #[when("slash completion is queried for the built-in canonical prefix")]
608 fn ac6_when_canonical_completion(world: &mut DiscoveryWorld06) {
609 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
610 world.canonical_hints = Some(completion_hints(tmpdir, "/att"));
611 }
612
613 #[then("the built-in canonical command remains available")]
614 fn ac6_then_canonical_available(world: &mut DiscoveryWorld06) {
615 let hints = world
616 .canonical_hints
617 .as_ref()
618 .expect("canonical hints should exist");
619 assert!(
620 hints.iter().any(|hint| hint.name == "/attach"),
621 "canonical /attach must remain available"
622 );
623 }
624
625 #[tokio::test(flavor = "current_thread")]
626 async fn feat012_ac6_alias_shadow_preserves_canonical_access() {
627 let writer = DiscoveryWorld06::cucumber()
628 .fail_on_skipped()
629 .with_default_cli()
630 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
631 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AC6_SCENARIO
632 })
633 .await;
634 assert_eq!(writer.failed_steps(), 0, "scenario failed: {AC6_SCENARIO}");
635 assert_eq!(
636 writer.skipped_steps(),
637 0,
638 "scenario skipped: {AC6_SCENARIO}"
639 );
640 assert_eq!(
641 writer.passed_steps(),
642 5,
643 "scenario did not run: {AC6_SCENARIO}"
644 );
645 }
646
647 // --- AT-010: Accepted user alias claims a built-in canonical token ---
648
649 #[derive(cucumber::World)]
650 #[world(init = Self::new)]
651 struct DiscoveryWorld10 {
652 tmpdir: Option<TempDir>,
653 entries: Option<Vec<CommandPaletteEntry>>,
654 hints: Option<Vec<SlashMenuEntry>>,
655 }
656
657 impl std::fmt::Debug for DiscoveryWorld10 {
658 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
659 f.debug_struct("DiscoveryWorld10")
660 .field("has_tmpdir", &self.tmpdir.is_some())
661 .field("has_entries", &self.entries.is_some())
662 .field("has_hints", &self.hints.is_some())
663 .finish()
664 }
665 }
666
667 impl DiscoveryWorld10 {
668 fn new() -> Self {
669 Self {
670 tmpdir: None,
671 entries: None,
672 hints: None,
673 }
674 }
675 }
676
677 #[given(
678 "a workspace with a visible user command whose accepted alias equals a built-in canonical token"
679 )]
680 fn at010_given_alias_claims_canonical(world: &mut DiscoveryWorld10) {
681 let tmpdir = TempDir::new().expect("AT-010 TempDir");
682 write_user_command(
683 &tmpdir,
684 "assistant",
685 "---\ndescription: My assistant\nalias: help\n---\nassistant",
686 );
687 commands::user_registry::reload(Some(tmpdir.path()));
688 world.tmpdir = Some(tmpdir);
689 }
690
691 #[when("the command palette and slash completion are queried for that token")]
692 fn at010_when_both_queried(world: &mut DiscoveryWorld10) {
693 let tmpdir = world.tmpdir.as_ref().expect("tmpdir should exist");
694 world.entries = Some(palette_entries(tmpdir));
695 world.hints = Some(completion_hints(tmpdir, "/help"));
696 }
697
698 #[then("both surfaces suppress the built-in canonical entry")]
699 fn at010_then_both_suppress(world: &mut DiscoveryWorld10) {
700 let entries = world.entries.as_ref().expect("entries should exist");
701 assert!(
702 !entries.iter().any(|entry| entry.label == "/help"),
703 "palette must suppress built-in /help"
704 );
705 let hints = world.hints.as_ref().expect("hints should exist");
706 assert!(
707 !hints.iter().any(|hint| hint.name == "/help"),
708 "completion must suppress built-in /help"
709 );
710 }
711
712 #[then("the user command is the only discovery entry for that token")]
713 fn at010_then_only_user_entry(world: &mut DiscoveryWorld10) {
714 let entries = world.entries.as_ref().expect("entries should exist");
715 assert!(
716 entries.iter().any(|entry| entry.label == "/assistant"),
717 "user command must appear in the palette"
718 );
719 let hints = world.hints.as_ref().expect("hints should exist");
720 assert!(
721 hints.iter().any(|hint| hint.name == "/assistant"),
722 "user command must appear in completion"
723 );
724 }
725
726 #[tokio::test(flavor = "current_thread")]
727 async fn feat012_at010_alias_claims_builtin_canonical_token_consistently() {
728 let writer = DiscoveryWorld10::cucumber()
729 .fail_on_skipped()
730 .with_default_cli()
731 .filter_run(DISCOVERY_FEATURE_PATH, move |feature, _, scenario| {
732 feature.name == DISCOVERY_FEATURE_NAME && scenario.name == AT010_SCENARIO
733 })
734 .await;
735 assert_eq!(
736 writer.failed_steps(),
737 0,
738 "scenario failed: {AT010_SCENARIO}"
739 );
740 assert_eq!(
741 writer.skipped_steps(),
742 0,
743 "scenario skipped: {AT010_SCENARIO}"
744 );
745 assert_eq!(
746 writer.passed_steps(),
747 4,
748 "scenario did not run: {AT010_SCENARIO}"
749 );
750 }
751
751 lines RUST