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