Skip to main content

atd_cli/
schema.rs

1//! `atd schema` — describe a tool and pretty-print its ToolDefinition.
2
3use 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}