forge_agent/translate/
commands.rs1use serde_json::Value;
5
6use forge_primitives::AvailableCommand;
7
8pub fn map_available_commands_from_json(arr: &[Value]) -> Vec<AvailableCommand> {
15 arr.iter()
16 .filter_map(|entry| {
17 if let Some(name) = entry.as_str() {
18 if name.is_empty() {
19 return None;
20 }
21 return Some(AvailableCommand {
22 name: name.to_owned(),
23 description: String::new(),
24 input_hint: None,
25 });
26 }
27 let obj = entry.as_object()?;
28 let name = obj.get("name")?.as_str().filter(|s| !s.is_empty())?.to_owned();
29 let description =
30 obj.get("description").and_then(Value::as_str).unwrap_or_default().to_owned();
31 let input_hint = obj
32 .get("argumentHint")
33 .and_then(Value::as_str)
34 .filter(|s| !s.is_empty())
35 .map(str::to_owned);
36 Some(AvailableCommand { name, description, input_hint })
37 })
38 .collect()
39}
40
41pub fn slash_name(name: &str) -> String {
46 if name.is_empty() || name.starts_with('/') { name.to_owned() } else { format!("/{name}") }
47}
48
49#[cfg(test)]
50mod tests {
51 use super::{map_available_commands_from_json, slash_name};
52 use forge_primitives::AvailableCommand;
53 use serde_json::{Value, json};
54
55 fn parse(entries: &Value) -> Vec<AvailableCommand> {
56 map_available_commands_from_json(entries.as_array().expect("an array"))
57 }
58
59 #[test]
62 fn a_bare_name_gains_the_slash_it_is_typed_with() {
63 assert_eq!(slash_name("doctor"), "/doctor");
64 }
65
66 #[test]
69 fn a_name_that_already_carries_one_is_unchanged() {
70 assert_eq!(slash_name("/doctor"), "/doctor");
71 }
72
73 #[test]
77 fn an_empty_name_stays_empty() {
78 assert_eq!(slash_name(""), "");
79 }
80
81 #[test]
84 fn bare_name_entries_map_to_commands_without_descriptions() {
85 let commands = parse(&json!(["/help", "memory"]));
86
87 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
88 assert_eq!(names, vec!["/help", "memory"], "both entries survive, in wire order");
89 assert!(commands.iter().all(|c| c.description.is_empty()), "and carry no description");
90 assert!(commands.iter().all(|c| c.input_hint.is_none()), "nor an input hint");
91 }
92
93 #[test]
96 fn object_entries_carry_their_description_and_argument_hint() {
97 let commands = parse(&json!([
98 {"name": "/model", "description": "Pick a model", "argumentHint": "<id>"},
99 ]));
100
101 assert_eq!(commands.len(), 1);
102 assert_eq!(commands[0].description, "Pick a model");
103 assert_eq!(commands[0].input_hint.as_deref(), Some("<id>"));
104 }
105
106 #[test]
109 fn an_empty_argument_hint_collapses_to_none() {
110 let commands = parse(&json!([
111 {"name": "/clear", "description": "Clear", "argumentHint": ""},
112 {"name": "/compact"},
113 ]));
114
115 assert_eq!(commands.len(), 2);
116 assert!(commands[0].input_hint.is_none(), "an empty hint is no hint");
117 assert!(commands[1].input_hint.is_none(), "and a missing one is the same");
118 assert!(commands[1].description.is_empty(), "a missing description is empty, not absent");
119 }
120
121 #[test]
124 fn entries_without_a_name_are_dropped() {
125 let commands = parse(&json!([
126 "",
127 {"description": "no name here"},
128 {"name": ""},
129 42,
130 "/kept",
131 ]));
132
133 let names: Vec<&str> = commands.iter().map(|c| c.name.as_str()).collect();
134 assert_eq!(names, vec!["/kept"], "only the entry with a name survives: {names:?}");
135 }
136}