| 1 | use super::*; |
| 2 | |
| 3 | fn entry<'a>(commands: &'a [CommandCatalogEntry], name: &str) -> &'a CommandCatalogEntry { |
| 4 | commands |
| 5 | .iter() |
| 6 | .find(|command| command.name == name) |
| 7 | .unwrap_or_else(|| panic!("catalog must contain {name}")) |
| 8 | } |
| 9 | |
| 10 | #[test] |
| 11 | fn command_catalog_serves_builtins_with_host_binding() { |
| 12 | let users = crate::commands::user_registry::UserCommandRegistry::new(); |
| 13 | let commands = command_catalog(&users); |
| 14 | |
| 15 | let model = entry(&commands, "model"); |
| 16 | assert_eq!(model.kind, "builtin"); |
| 17 | assert_eq!(model.binding, "host"); |
| 18 | assert_eq!(model.discovery, Some("primary")); |
| 19 | assert!(!model.hidden); |
| 20 | assert_eq!(model.shadowed_by, None); |
| 21 | assert!(model.summary.is_some()); |
| 22 | assert!(model.usage.is_some()); |
| 23 | assert!(model.takes_arguments); |
| 24 | |
| 25 | // Unlisted builtins run but are not advertised — hidden, not absent. |
| 26 | assert!(entry(&commands, "lane").hidden); |
| 27 | |
| 28 | // A usage line's literal verbs surface as subcommands. |
| 29 | let goal = entry(&commands, "goal"); |
| 30 | assert!( |
| 31 | goal.subcommands.iter().any(|verb| verb == "blocked"), |
| 32 | "goal usage should declare its verbs: {:?}", |
| 33 | goal.subcommands |
| 34 | ); |
| 35 | } |
| 36 | |
| 37 | #[test] |
| 38 | fn command_catalog_marks_user_shadowing_of_builtin_names_and_aliases() { |
| 39 | let users = crate::commands::user_registry::UserCommandRegistry::from_loaded(vec![ |
| 40 | ("model".to_string(), "Pick the fast route.".to_string()), |
| 41 | ("agents".to_string(), "Alias-shaped command.".to_string()), |
| 42 | ]); |
| 43 | let commands = command_catalog(&users); |
| 44 | |
| 45 | let model = entry(&commands, "model"); |
| 46 | assert_eq!(model.shadowed_by.as_deref(), Some("model")); |
| 47 | |
| 48 | // The shadowing user command is served as a prompt-bound row. |
| 49 | let user_model = commands |
| 50 | .iter() |
| 51 | .find(|command| command.name == "model" && command.kind == "user") |
| 52 | .expect("user command row"); |
| 53 | assert_eq!(user_model.binding, "prompt"); |
| 54 | |
| 55 | // A user command colliding with a builtin's alias shadows that spelling: |
| 56 | // `agents` is a `subagents` alias, so the builtin reports it while keeping |
| 57 | // its canonical name. |
| 58 | let subagents = entry(&commands, "subagents"); |
| 59 | assert_eq!(subagents.kind, "builtin"); |
| 60 | assert_eq!(subagents.shadowed_by, None); |
| 61 | assert!( |
| 62 | subagents.shadowed_aliases.iter().any(|a| a == "agents"), |
| 63 | "shadowed_aliases must report the taken spelling: {:?}", |
| 64 | subagents.shadowed_aliases |
| 65 | ); |
| 66 | } |
| 67 | |
| 68 | #[tokio::test] |
| 69 | async fn get_v1_commands_serves_the_catalog_over_http() -> Result<()> { |
| 70 | let _env = lock_test_env(); |
| 71 | let temp = tempfile::tempdir()?; |
| 72 | let root = temp.path().join("commands-route"); |
| 73 | let sessions_dir = root.join("sessions"); |
| 74 | let workspace = root.join("workspace"); |
| 75 | let commands_dir = workspace.join(".codewhale").join("commands"); |
| 76 | fs::create_dir_all(&commands_dir)?; |
| 77 | fs::write(commands_dir.join("model.md"), "Pick the fast route.\n")?; |
| 78 | |
| 79 | let Some((addr, _runtime_threads, handle)) = |
| 80 | spawn_test_server_with_root_token_mobile_workspace( |
| 81 | root, |
| 82 | sessions_dir, |
| 83 | None, |
| 84 | false, |
| 85 | workspace, |
| 86 | ) |
| 87 | .await? |
| 88 | else { |
| 89 | return Ok(()); |
| 90 | }; |
| 91 | let client = crate::tls::reqwest_client(); |
| 92 | |
| 93 | let body: serde_json::Value = client |
| 94 | .get(format!("http://{addr}/v1/commands")) |
| 95 | .send() |
| 96 | .await? |
| 97 | .error_for_status()? |
| 98 | .json() |
| 99 | .await?; |
| 100 | let commands = body["commands"].as_array().expect("commands array"); |
| 101 | assert!( |
| 102 | commands |
| 103 | .iter() |
| 104 | .any(|command| command["kind"] == "builtin" && command["binding"] == "host"), |
| 105 | "the route must serve builtins: {commands:?}" |
| 106 | ); |
| 107 | |
| 108 | // The workspace user command named `model` shadows the builtin: the |
| 109 | // builtin reports the shadow and the winning definition is served. |
| 110 | let builtin_model = commands |
| 111 | .iter() |
| 112 | .find(|command| command["name"] == "model" && command["kind"] == "builtin") |
| 113 | .expect("builtin model row"); |
| 114 | assert_eq!(builtin_model["shadowed_by"], "model"); |
| 115 | let user_model = commands |
| 116 | .iter() |
| 117 | .find(|command| command["name"] == "model" && command["kind"] == "user") |
| 118 | .expect("user model row"); |
| 119 | assert_eq!(user_model["binding"], "prompt"); |
| 120 | |
| 121 | handle.abort(); |
| 122 | Ok(()) |
| 123 | } |
| 124 |