use agtrace_types::{ExecuteArgs, FileReadArgs};
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyPatchArgs {
pub raw: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedPatch {
pub operation: PatchOperation,
pub file_path: String,
pub raw_patch: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatchOperation {
Add,
Update,
}
impl ApplyPatchArgs {
pub fn parse(&self) -> Result<ParsedPatch, String> {
let raw = &self.raw;
for line in raw.lines() {
if let Some(path) = line.strip_prefix("*** Add File: ") {
return Ok(ParsedPatch {
operation: PatchOperation::Add,
file_path: path.trim().to_string(),
raw_patch: raw.clone(),
});
}
if let Some(path) = line.strip_prefix("*** Update File: ") {
return Ok(ParsedPatch {
operation: PatchOperation::Update,
file_path: path.trim().to_string(),
raw_patch: raw.clone(),
});
}
}
Err("Failed to parse patch: no file path header found".to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShellArgs {
pub command: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timeout_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub workdir: Option<String>,
}
impl ShellArgs {
pub fn to_execute_args(&self) -> ExecuteArgs {
let command_str = self.command.join(" ");
let mut extra = json!({});
if let Some(workdir) = &self.workdir {
extra["workdir"] = json!(workdir);
}
ExecuteArgs {
command: Some(command_str),
description: None,
timeout: self.timeout_ms,
extra,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReadMcpResourceArgs {
pub server: String,
pub uri: String,
}
impl ReadMcpResourceArgs {
pub fn to_file_read_args(&self) -> FileReadArgs {
let mut extra = json!({});
extra["server"] = json!(&self.server);
FileReadArgs {
file_path: Some(self.uri.clone()),
path: None,
pattern: None,
extra,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_add_file_patch() {
let args = ApplyPatchArgs {
raw: r#"*** Begin Patch
*** Add File: docs/test.md
+# Test Document
+
+This is a test.
*** End Patch"#
.to_string(),
};
let parsed = args.parse().unwrap();
assert_eq!(parsed.operation, PatchOperation::Add);
assert_eq!(parsed.file_path, "docs/test.md");
}
#[test]
fn test_parse_update_file_patch() {
let args = ApplyPatchArgs {
raw: r#"*** Begin Patch
*** Update File: src/lib.rs
@@
fn example() {
- old_code()
+ new_code()
}
*** End Patch"#
.to_string(),
};
let parsed = args.parse().unwrap();
assert_eq!(parsed.operation, PatchOperation::Update);
assert_eq!(parsed.file_path, "src/lib.rs");
}
#[test]
fn test_parse_invalid_patch() {
let args = ApplyPatchArgs {
raw: "*** Begin Patch\nno header\n*** End Patch".to_string(),
};
assert!(args.parse().is_err());
}
#[test]
fn test_shell_args_to_execute_args() {
let shell_args = ShellArgs {
command: vec!["bash".to_string(), "-lc".to_string(), "ls".to_string()],
timeout_ms: Some(10000),
workdir: Some("/path/to/dir".to_string()),
};
let execute_args = shell_args.to_execute_args();
assert_eq!(execute_args.command, Some("bash -lc ls".to_string()));
assert_eq!(execute_args.timeout, Some(10000));
assert_eq!(
execute_args.extra.get("workdir"),
Some(&json!("/path/to/dir"))
);
}
#[test]
fn test_shell_args_without_optional_fields() {
let shell_args = ShellArgs {
command: vec!["echo".to_string(), "hello".to_string()],
timeout_ms: None,
workdir: None,
};
let execute_args = shell_args.to_execute_args();
assert_eq!(execute_args.command, Some("echo hello".to_string()));
assert_eq!(execute_args.timeout, None);
assert_eq!(execute_args.extra, json!({}));
}
#[test]
fn test_read_mcp_resource_args_to_file_read_args() {
let mcp_args = ReadMcpResourceArgs {
server: "local".to_string(),
uri: "/foo-bar-hoge-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/AGENTS.md".to_string(),
};
let file_read_args = mcp_args.to_file_read_args();
assert_eq!(
file_read_args.file_path,
Some("/foo-bar-hoge-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/AGENTS.md".to_string())
);
assert_eq!(file_read_args.path, None);
assert_eq!(file_read_args.pattern, None);
assert_eq!(file_read_args.extra.get("server"), Some(&json!("local")));
}
}