#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ToolInput {
pub flags: serde_json::Map<String, serde_json::Value>,
pub args: Vec<String>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, schemars::JsonSchema)]
pub struct ToolOutput {
pub stdout: String,
pub stderr: String,
pub exit_code: i32,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::{from_value, json, to_value};
#[test]
fn test_tool_input_default() {
let input = ToolInput::default();
assert!(input.flags.is_empty());
assert!(input.args.is_empty());
}
#[test]
fn test_tool_input_wire_shape() {
let input = ToolInput::default();
let json = to_value(&input).expect("serialize");
let obj = json.as_object().expect("is object");
assert_eq!(obj.len(), 2, "ToolInput must have exactly 2 fields");
assert!(obj.contains_key("flags"), "flags field must be present");
assert!(obj.contains_key("args"), "args field must be present");
assert!(
obj["flags"]
.as_object()
.expect("flags is object")
.is_empty(),
"flags must be an empty object"
);
assert!(
obj["args"].as_array().expect("args is array").is_empty(),
"args must be an empty array"
);
}
#[test]
fn test_tool_input_serializes_to_pinned_shape() {
let input = ToolInput {
flags: [
("log-level".to_string(), json!("debug")),
("output".to_string(), json!("results.json")),
]
.iter()
.cloned()
.collect(),
args: vec!["file1.txt".to_string(), "file2.txt".to_string()],
};
let expected = json!({
"flags": { "log-level": "debug", "output": "results.json" },
"args": ["file1.txt", "file2.txt"],
});
let actual = to_value(&input).expect("serialize");
assert_eq!(actual, expected, "serialized shape must match contract");
let parsed: ToolInput = from_value(expected).expect("deserialize");
assert_eq!(parsed.flags, input.flags, "flags must parse back");
assert_eq!(parsed.args, input.args, "args must parse back");
}
#[test]
fn test_tool_output_wire_shape() {
let output = ToolOutput {
stdout: String::new(),
stderr: String::new(),
exit_code: 0,
};
let json = to_value(&output).expect("serialize");
let obj = json.as_object().expect("is object");
assert_eq!(obj.len(), 3, "ToolOutput must have exactly 3 fields");
assert!(obj.contains_key("stdout"), "stdout field must be present");
assert!(obj.contains_key("stderr"), "stderr field must be present");
assert!(
obj.contains_key("exit_code"),
"exit_code field must be present"
);
assert!(obj["stdout"].is_string(), "stdout must serialize as string");
assert!(obj["stderr"].is_string(), "stderr must serialize as string");
assert!(
obj["exit_code"].is_i64(),
"exit_code must serialize as integer"
);
}
#[test]
fn test_tool_output_serializes_to_pinned_shape() {
let output = ToolOutput {
stdout: "Operation succeeded\n".to_string(),
stderr: "Warning: deprecated flag used\n".to_string(),
exit_code: 0,
};
let expected = json!({
"stdout": "Operation succeeded\n",
"stderr": "Warning: deprecated flag used\n",
"exit_code": 0,
});
let actual = to_value(&output).expect("serialize");
assert_eq!(actual, expected, "serialized shape must match contract");
let parsed: ToolOutput = from_value(expected).expect("deserialize");
assert_eq!(parsed.stdout, output.stdout);
assert_eq!(parsed.stderr, output.stderr);
assert_eq!(parsed.exit_code, output.exit_code);
}
#[test]
fn test_tool_output_negative_exit_code_preserves_sign() {
let expected = json!({
"stdout": "",
"stderr": "Process killed by signal\n",
"exit_code": -1,
});
let parsed: ToolOutput = from_value(expected.clone()).expect("deserialize");
assert_eq!(parsed.exit_code, -1, "-1 sentinel must deserialize");
let actual = to_value(&parsed).expect("serialize");
assert_eq!(actual, expected, "-1 sentinel must re-serialize");
}
}