1use atd_protocol::AtdError;
4use atd_sdk::AtdClient;
5use std::io::Write;
6
7use crate::cli::SchemaArgs;
8
9pub async fn run(
10 client: &AtdClient,
11 args: SchemaArgs,
12 out: &mut impl Write,
13) -> Result<(), AtdError> {
14 let def = client.describe(&args.tool_id).await?;
15
16 let json = if args.json {
17 serde_json::to_string(&def)
18 } else {
19 serde_json::to_string_pretty(&def)
20 }
21 .map_err(|e| AtdError::ProtocolError {
22 expected: "serializable ToolDefinition".into(),
23 got: format!("serde error: {e}"),
24 })?;
25 writeln!(out, "{json}").ok();
26 Ok(())
27}
28
29#[cfg(test)]
30mod tests {
31 use super::*;
32 use atd_sdk::Endpoint;
33 use tokio::io::{AsyncReadExt, AsyncWriteExt};
34 use tokio::net::UnixListener;
35
36 fn sample_tool_def() -> serde_json::Value {
37 serde_json::json!({
38 "id": "anos:fs.read",
39 "name": "Read File",
40 "description": "Read a file from disk.",
41 "version": "0.1.0",
42 "capability": {"domain": "fs", "actions": ["read"], "tags": [], "intent_examples": []},
43 "input_schema": {"type": "object"},
44 "output_schema": {"type": "string"},
45 "bindings": [{"protocol": "Cli", "config": {}}],
46 "safety": {"level": "Read", "dry_run": false, "side_effects": [], "data_sensitivity": null},
47 "resources": {"timeout_ms": 1000, "max_concurrent": 1, "rate_limit_per_min": null, "estimated_tokens": null},
48 "trust": {"publisher": "anos", "trust_level": "L2Tested", "signature": null},
49 "visibility": "read"
50 })
51 }
52
53 async fn spawn_fake_server() -> std::path::PathBuf {
54 let dir = tempfile::tempdir().unwrap();
55 let path = dir.path().join("s.sock");
56 let listener = UnixListener::bind(&path).unwrap();
57 std::mem::forget(dir);
58
59 let ret = path.clone();
60 tokio::spawn(async move {
61 while let Ok((stream, _)) = listener.accept().await {
62 tokio::spawn(async move {
63 let (mut r, mut w) = stream.into_split();
64 loop {
65 let mut lb = [0u8; 4];
66 if r.read_exact(&mut lb).await.is_err() {
67 return;
68 }
69 let n = u32::from_be_bytes(lb) as usize;
70 let mut buf = vec![0u8; n];
71 if r.read_exact(&mut buf).await.is_err() {
72 return;
73 }
74 let req: serde_json::Value = serde_json::from_slice(&buf).unwrap();
75 let reply: serde_json::Value = match req["type"].as_str() {
76 Some("ping") => serde_json::json!({"type":"pong"}),
77 Some("tool_schema") => serde_json::json!({
78 "type":"tool_schema","schema": sample_tool_def(),
79 }),
80 _ => serde_json::json!({"type":"error","message":"no"}),
81 };
82 let body = serde_json::to_vec(&reply).unwrap();
83 if w.write_all(&(body.len() as u32).to_be_bytes())
84 .await
85 .is_err()
86 {
87 return;
88 }
89 if w.write_all(&body).await.is_err() {
90 return;
91 }
92 let _ = w.flush().await;
93 }
94 });
95 }
96 });
97 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
98 ret
99 }
100
101 #[tokio::test]
102 async fn schema_pretty_by_default_has_newlines_and_indent() {
103 let sock = spawn_fake_server().await;
104 let client = AtdClient::connect(Endpoint::unix(sock)).await.unwrap();
105 let mut out: Vec<u8> = Vec::new();
106 run(
107 &client,
108 SchemaArgs {
109 tool_id: "anos:fs.read".into(),
110 json: false,
111 },
112 &mut out,
113 )
114 .await
115 .unwrap();
116 let s = String::from_utf8(out).unwrap();
117 assert!(s.contains("\n"));
118 assert!(
119 s.contains(" \"id\""),
120 "pretty output should have indented keys"
121 );
122 assert!(s.contains("anos:fs.read"));
123 }
124
125 #[tokio::test]
126 async fn schema_json_flag_emits_compact_single_line() {
127 let sock = spawn_fake_server().await;
128 let client = AtdClient::connect(Endpoint::unix(sock)).await.unwrap();
129 let mut out: Vec<u8> = Vec::new();
130 run(
131 &client,
132 SchemaArgs {
133 tool_id: "anos:fs.read".into(),
134 json: true,
135 },
136 &mut out,
137 )
138 .await
139 .unwrap();
140 let s = String::from_utf8(out).unwrap();
141 let trimmed = s.trim_end_matches('\n');
142 assert!(
143 !trimmed.contains('\n'),
144 "json output should be one line, got: {s}"
145 );
146 let v: serde_json::Value = serde_json::from_str(trimmed).unwrap();
147 assert_eq!(v["id"], "anos:fs.read");
148 }
149}