1pub mod grok;
9mod pack;
10
11pub use grok::GrokPack;
12pub use pack::{Pack, PackContext};
13
14static GROK: GrokPack = GrokPack;
16
17const PACKS: &[&(dyn Pack + 'static)] = &[&GROK];
19
20#[must_use]
22pub fn available() -> Vec<&'static str> {
23 PACKS.iter().map(|pack| pack.name()).collect()
24}
25
26pub fn resolve(name: &str) -> Result<&'static dyn Pack, UnknownHarness> {
32 PACKS
33 .iter()
34 .copied()
35 .find(|pack| pack.name() == name)
36 .ok_or_else(|| UnknownHarness {
37 requested: name.to_owned(),
38 available: available(),
39 })
40}
41
42#[derive(Debug, thiserror::Error)]
44#[error("unknown harness `{requested}`; available: {}", .available.join(", "))]
45pub struct UnknownHarness {
46 pub requested: String,
48 pub available: Vec<&'static str>,
50}
51
52#[cfg(test)]
53mod tests {
54 #![allow(clippy::unnecessary_literal_bound)]
56
57 use super::*;
58 use async_trait::async_trait;
59 use locode_host::{Host, HostConfig};
60 use locode_protocol::{ContentBlock, Message, Role};
61 use locode_tools::{Registry, Tool, ToolCtx, ToolError, ToolKind, ToolOutput};
62 use serde::Serialize;
63 use serde_json::{Value, json};
64 use std::path::PathBuf;
65 use std::sync::Arc;
66 use tokio_util::sync::CancellationToken;
67
68 fn test_host() -> (tempfile::TempDir, Arc<Host>) {
70 let dir = tempfile::tempdir().unwrap();
71 let host = Arc::new(Host::new(HostConfig::new(dir.path())).unwrap());
72 (dir, host)
73 }
74
75 #[derive(Serialize)]
79 struct EchoOut {
80 echoed: String,
81 }
82 impl ToolOutput for EchoOut {
83 fn to_prompt_text(&self) -> String {
84 self.echoed.clone()
85 }
86 }
87
88 struct Echo;
89 #[async_trait]
90 impl Tool for Echo {
91 type Args = Value;
92 type Output = EchoOut;
93 fn kind(&self) -> ToolKind {
94 ToolKind::Shell
95 }
96 fn description(&self) -> &str {
97 "echo"
98 }
99 async fn run(&self, _ctx: &ToolCtx, args: Value) -> Result<EchoOut, ToolError> {
100 Ok(EchoOut {
101 echoed: args.to_string(),
102 })
103 }
104 }
105
106 struct FakePack;
107 impl Pack for FakePack {
108 fn name(&self) -> &'static str {
109 "fake"
110 }
111 fn register(&self, _host: &Arc<Host>, registry: &mut Registry) {
112 registry.register("alpha", Echo);
113 registry.register("beta", Echo);
114 }
115 fn preamble(&self, _ctx: &PackContext) -> Vec<Message> {
116 vec![Message {
117 role: Role::System,
118 content: vec![ContentBlock::Text {
119 text: "fake pack".into(),
120 }],
121 }]
122 }
123 }
124
125 struct DupPack;
126 impl Pack for DupPack {
127 fn name(&self) -> &'static str {
128 "dup"
129 }
130 fn register(&self, _host: &Arc<Host>, registry: &mut Registry) {
131 registry.register("alpha", Echo);
132 registry.register("alpha", Echo); }
134 fn preamble(&self, _ctx: &PackContext) -> Vec<Message> {
135 Vec::new()
136 }
137 }
138
139 fn ctx(headless: bool) -> PackContext {
140 PackContext {
141 cwd: PathBuf::from("/repo"),
142 os: "linux".into(),
143 shell: "/bin/bash".into(),
144 date: "2026-07-18".into(),
145 headless,
146 strip_identity: false,
147 }
148 }
149
150 #[test]
153 fn resolve_grok_returns_grok_pack() {
154 assert_eq!(resolve("grok").unwrap().name(), "grok");
155 }
156
157 #[test]
158 fn available_lists_grok() {
159 assert_eq!(available(), vec!["grok"]);
160 }
161
162 #[test]
163 fn unknown_harness_errors_clearly() {
164 let err = resolve("gpt").err().expect("unknown harness");
165 let msg = err.to_string();
166 assert!(msg.contains("gpt"), "{msg}");
167 assert!(msg.contains("grok"), "names the available packs: {msg}");
168 assert_eq!(err.requested, "gpt");
169 }
170
171 #[test]
174 fn pack_builds_expected_specs() {
175 let (_dir, host) = test_host();
176 let registry = FakePack.build_registry(&host);
177 assert!(registry.contains("alpha"));
178 assert!(registry.contains("beta"));
179 let specs = registry.specs();
180 let mut names: Vec<&str> = specs.iter().map(|s| s.name.as_str()).collect();
181 names.sort_unstable();
182 assert_eq!(names, vec!["alpha", "beta"]);
183 let alpha = specs.iter().find(|s| s.name == "alpha").unwrap();
185 assert_eq!(alpha.description, "echo");
186 }
187
188 #[tokio::test]
189 async fn pack_routes_to_impl() {
190 let (_dir, host) = test_host();
191 let registry = FakePack.build_registry(&host);
192 let tool_ctx = ToolCtx::new(
193 PathBuf::from("/repo"),
194 "c1".into(),
195 PathBuf::from("/repo"),
196 CancellationToken::new(),
197 );
198 let dispatched = registry
199 .dispatch("alpha", json!({ "x": 1 }), &tool_ctx)
200 .await;
201 assert!(dispatched.record.ok, "routes to the registered impl");
202 assert_eq!(dispatched.record.name, "alpha");
203 assert_eq!(dispatched.record.output["echoed"], json!(r#"{"x":1}"#));
205 }
206
207 #[test]
208 #[should_panic(expected = "duplicate tool registration")]
209 fn duplicate_registration_panics() {
210 let (_dir, host) = test_host();
211 let _ = DupPack.build_registry(&host);
212 }
213
214 #[test]
219 fn grok_tools_expose_kinds_for_the_approval_seam() {
220 use locode_tools::ToolKind;
221 let (_dir, host) = test_host();
222 let registry = resolve("grok").unwrap().build_registry(&host);
223 for (tool, kind) in [
224 ("run_terminal_cmd", ToolKind::Shell),
225 ("read_file", ToolKind::Read),
226 ("search_replace", ToolKind::Edit),
227 ("grep", ToolKind::Grep),
228 ("list_dir", ToolKind::Glob),
229 ] {
230 assert_eq!(registry.kind_of(tool), Some(kind), "kind_of({tool})");
231 }
232 assert_eq!(registry.kind_of("no_such_tool"), None);
233 }
234
235 #[test]
236 fn grok_registers_real_tools_and_preamble() {
237 let (_dir, host) = test_host();
238 let pack = resolve("grok").unwrap();
239 let registry = pack.build_registry(&host);
240 for tool in [
241 "run_terminal_cmd",
242 "read_file",
243 "search_replace",
244 "grep",
245 "list_dir",
246 ] {
247 assert!(registry.contains(tool), "grok pack registers {tool}");
248 }
249
250 let headless = pack.preamble(&ctx(true));
252 assert_eq!(headless.len(), 2);
253 assert_eq!(headless[0].role, Role::System);
254 assert_eq!(headless[1].role, Role::User);
255 let interactive = pack.preamble(&ctx(false));
256 assert_ne!(
257 headless, interactive,
258 "headless branch changes the identity"
259 );
260 }
261}