use supercode::tools::{
ApplyPatchTool, PersistentShellTool, ReadFileTool, SandboxPolicy, Tool, ToolContext,
WriteFileTool,
};
const MAX_READ_BYTES: usize = 400_000;
fn ctx() -> (ToolContext, std::path::PathBuf) {
use std::sync::atomic::{AtomicU64, Ordering};
static N: AtomicU64 = AtomicU64::new(0);
let dir = std::env::temp_dir().join(format!(
"sc-tools-{}-{}",
std::process::id(),
N.fetch_add(1, Ordering::SeqCst)
));
std::fs::create_dir_all(&dir).unwrap();
(ToolContext::new(dir.clone()), dir)
}
#[tokio::test]
async fn apply_patch_add_update_delete_move() {
let (ctx, dir) = ctx();
let tool = ApplyPatchTool;
std::fs::write(dir.join("keep.rs"), "fn main() {\n old();\n}\n").unwrap();
std::fs::write(dir.join("gone.txt"), "bye").unwrap();
let patch = "\
*** Begin Patch
*** Add File: src/new.rs
+pub fn added() -> u8 {
+ 42
+}
*** Update File: keep.rs
@@
fn main() {
- old();
+ fresh();
}
*** Delete File: gone.txt
*** End Patch
";
let out = tool
.execute(serde_json::json!({ "patch": patch }), &ctx)
.await
.unwrap();
assert!(out.contains("A src/new.rs"), "{out}");
assert!(out.contains("U keep.rs"), "{out}");
assert!(out.contains("D gone.txt"), "{out}");
let added = std::fs::read_to_string(dir.join("src/new.rs")).unwrap();
assert!(added.contains("pub fn added") && added.contains("42"));
let updated = std::fs::read_to_string(dir.join("keep.rs")).unwrap();
assert!(updated.contains("fresh();") && !updated.contains("old();"));
assert!(updated.contains("fn main()"));
assert!(!dir.join("gone.txt").exists());
let mv = "\
*** Begin Patch
*** Update File: keep.rs
*** Move to: renamed.rs
@@
- fresh();
+ fresher();
*** End Patch
";
let out = tool
.execute(serde_json::json!({ "patch": mv }), &ctx)
.await
.unwrap();
assert!(out.contains("M keep.rs -> renamed.rs"), "{out}");
assert!(!dir.join("keep.rs").exists());
assert!(std::fs::read_to_string(dir.join("renamed.rs"))
.unwrap()
.contains("fresher();"));
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn apply_patch_reports_unmatched_hunk() {
let (ctx, dir) = ctx();
std::fs::write(dir.join("f.txt"), "hello world").unwrap();
let tool = ApplyPatchTool;
let patch = "\
*** Begin Patch
*** Update File: f.txt
@@
-not present
+replacement
*** End Patch
";
let err = tool
.execute(serde_json::json!({ "patch": patch }), &ctx)
.await
.unwrap_err()
.to_string();
assert!(err.contains("did not match"), "{err}");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn apply_patch_errors_on_ambiguous_hunk_without_anchor() {
let (ctx, dir) = ctx();
let tool = ApplyPatchTool;
let original = "def foo():\n return 1\n\ndef bar():\n return 1\n";
std::fs::write(dir.join("a.py"), original).unwrap();
let patch = "\
*** Begin Patch
*** Update File: a.py
@@
- return 1
+ return 2
*** End Patch
";
let err = tool
.execute(serde_json::json!({ "patch": patch }), &ctx)
.await
.unwrap_err()
.to_string();
assert!(err.contains("matches file contents"), "{err}");
assert!(err.contains('2'), "{err}");
assert!(
err.contains("context") || err.contains("@@ anchor"),
"{err}"
);
let after = std::fs::read_to_string(dir.join("a.py")).unwrap();
assert_eq!(after, original, "ambiguous hunk must not modify the file");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn apply_patch_is_registered_as_a_builtin() {
use supercode::ToolRegistry;
let reg = ToolRegistry::with_builtins();
assert!(
reg.get("apply_patch").is_some(),
"apply_patch must be a built-in tool"
);
assert!(
reg.get("shell").is_some(),
"persistent shell must be a built-in tool"
);
}
#[tokio::test]
async fn persistent_shell_keeps_state_across_calls() {
let (ctx, dir) = ctx();
let sh = PersistentShellTool::default();
let r = sh
.execute(serde_json::json!({"command": "MYVAR=hello123"}), &ctx)
.await
.unwrap();
assert!(r.contains("exit code: 0"), "{r}");
let r = sh
.execute(serde_json::json!({"command": "echo $MYVAR"}), &ctx)
.await
.unwrap();
assert!(r.contains("hello123"), "env var did not persist: {r}");
std::fs::create_dir_all(dir.join("sub")).unwrap();
sh.execute(serde_json::json!({"command": "cd sub"}), &ctx)
.await
.unwrap();
let r = sh
.execute(serde_json::json!({"command": "pwd"}), &ctx)
.await
.unwrap();
assert!(r.contains("/sub"), "cwd did not persist: {r}");
let r = sh
.execute(serde_json::json!({"command": "false"}), &ctx)
.await
.unwrap();
assert!(r.contains("exit code: 1"), "{r}");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn persistent_shell_sentinel_cannot_be_spoofed() {
let (ctx, dir) = ctx();
let sh = PersistentShellTool::default();
let r = sh
.execute(
serde_json::json!({"command": "echo '__SC_SHELL_DONE__ 0'; (exit 7)"}),
&ctx,
)
.await
.unwrap();
assert!(
r.contains("exit code: 7"),
"spoofed exit code accepted: {r}"
);
assert!(
r.contains("__SC_SHELL_DONE__ 0"),
"echoed sentinel text missing from output: {r}"
);
let r = sh
.execute(serde_json::json!({"command": "echo still-alive"}), &ctx)
.await
.unwrap();
assert!(r.contains("exit code: 0"), "{r}");
assert!(r.contains("still-alive"), "{r}");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn sandbox_read_only_and_workspace_write() {
let (mut ctx, dir) = ctx();
let w = WriteFileTool;
ctx.sandbox = SandboxPolicy::ReadOnly;
let err = w
.execute(serde_json::json!({"path": "a.txt", "content": "x"}), &ctx)
.await
.unwrap_err()
.to_string();
assert!(err.contains("read-only"), "{err}");
assert!(!dir.join("a.txt").exists());
ctx.sandbox = SandboxPolicy::WorkspaceWrite;
w.execute(
serde_json::json!({"path": "inside.txt", "content": "ok"}),
&ctx,
)
.await
.unwrap();
assert_eq!(
std::fs::read_to_string(dir.join("inside.txt")).unwrap(),
"ok"
);
let outside = std::env::temp_dir().join(format!("sc-escape-{}.txt", std::process::id()));
let err = w
.execute(
serde_json::json!({"path": outside.to_string_lossy(), "content": "no"}),
&ctx,
)
.await
.unwrap_err()
.to_string();
assert!(err.contains("outside the workspace"), "{err}");
assert!(!outside.exists());
let err = w
.execute(
serde_json::json!({"path": "../escape.txt", "content": "no"}),
&ctx,
)
.await
.unwrap_err()
.to_string();
assert!(err.contains("outside the workspace"), "{err}");
ctx.sandbox = SandboxPolicy::DangerFullAccess;
w.execute(
serde_json::json!({"path": outside.to_string_lossy(), "content": "yes"}),
&ctx,
)
.await
.unwrap();
assert!(outside.exists());
std::fs::remove_file(&outside).ok();
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
#[cfg(unix)]
async fn sandbox_workspace_write_rejects_a_symlink_escape() {
let (mut ctx, dir) = ctx();
let outside = std::env::temp_dir().join(format!(
"sc-symlink-escape-outside-{}-{}",
std::process::id(),
std::line!()
));
std::fs::create_dir_all(&outside).unwrap();
std::os::unix::fs::symlink(&outside, dir.join("link")).unwrap();
ctx.sandbox = SandboxPolicy::WorkspaceWrite;
let w = WriteFileTool;
let err = w
.execute(
serde_json::json!({"path": "link/passwd", "content": "PWNED"}),
&ctx,
)
.await
.unwrap_err()
.to_string();
assert!(err.contains("outside the workspace"), "{err}");
assert!(
!outside.join("passwd").exists(),
"the write must never actually land outside the workspace root via the symlink"
);
std::fs::remove_dir_all(&dir).ok();
std::fs::remove_dir_all(&outside).ok();
}
#[tokio::test]
async fn sandbox_blocks_apply_patch_writes() {
let (mut ctx, dir) = ctx();
ctx.sandbox = SandboxPolicy::ReadOnly;
let tool = ApplyPatchTool;
let patch = "*** Begin Patch\n*** Add File: blocked.rs\n+x\n*** End Patch\n";
let err = tool
.execute(serde_json::json!({"patch": patch}), &ctx)
.await
.unwrap_err()
.to_string();
assert!(err.contains("read-only"), "{err}");
assert!(!dir.join("blocked.rs").exists());
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn persistent_shell_write_stdin() {
let (ctx, dir) = ctx();
let sh = PersistentShellTool::default();
let out = sh
.execute(
serde_json::json!({"write_stdin": "echo from_stdin_pipe\n"}),
&ctx,
)
.await
.unwrap();
assert!(out.contains("from_stdin_pipe"), "write_stdin output: {out}");
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn update_plan_tool() {
use supercode::tools::UpdatePlanTool;
let (ctx, dir) = ctx();
let tool = UpdatePlanTool::default();
let out = tool
.execute(
serde_json::json!({"plan": [
{"step": "read code", "status": "completed"},
{"step": "fix bug", "status": "in_progress"},
{"step": "add test"}
]}),
&ctx,
)
.await
.unwrap();
assert!(out.contains("[x] read code"));
assert!(out.contains("[~] fix bug"));
assert!(out.contains("[ ] add test"));
assert_eq!(tool.current().len(), 3);
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn bash_os_sandbox_blocks_writes() {
use supercode::tools::BashTool;
let (mut ctx, dir) = ctx();
let tool = BashTool::default();
ctx.sandbox = SandboxPolicy::ReadOnly;
let out = tool
.execute(
serde_json::json!({"command": "echo hi > blocked.txt; echo done"}),
&ctx,
)
.await
.unwrap();
assert!(
!dir.join("blocked.txt").exists(),
"read-only sandbox must block the write: {out}"
);
ctx.sandbox = SandboxPolicy::WorkspaceWrite;
tool.execute(serde_json::json!({"command": "echo hi > inside.txt"}), &ctx)
.await
.unwrap();
assert!(
dir.join("inside.txt").exists(),
"workspace-write must allow writes inside cwd"
);
let outside = std::env::temp_dir().join(format!("sc-bash-escape-{}.txt", std::process::id()));
let _ = std::fs::remove_file(&outside);
let cmd = format!("echo hi > {}", outside.display());
tool.execute(serde_json::json!({"command": cmd}), &ctx)
.await
.unwrap();
assert!(
!outside.exists(),
"workspace-write must block writes outside cwd"
);
ctx.sandbox = SandboxPolicy::DangerFullAccess;
tool.execute(serde_json::json!({"command": "echo hi > free.txt"}), &ctx)
.await
.unwrap();
assert!(dir.join("free.txt").exists());
std::fs::remove_dir_all(&dir).ok();
}
#[cfg(target_os = "macos")]
#[tokio::test]
async fn persistent_shell_os_sandbox_blocks_writes() {
let (mut ctx, dir) = ctx();
ctx.sandbox = SandboxPolicy::ReadOnly;
let sh = PersistentShellTool::default();
let out = sh
.execute(
serde_json::json!({"command": "echo hi > shell_blocked.txt; echo done"}),
&ctx,
)
.await
.unwrap();
assert!(
!dir.join("shell_blocked.txt").exists(),
"read-only sandbox must block the persistent shell's write: {out}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn apply_patch_at_at_anchor_disambiguates_and_locates_insertions() {
let (ctx, dir) = ctx();
let tool = ApplyPatchTool;
std::fs::write(
dir.join("a.py"),
"def foo():\n return 1\n\ndef bar():\n return 1\n",
)
.unwrap();
let patch = "\
*** Begin Patch
*** Update File: a.py
@@ def bar():
- return 1
+ return 2
*** End Patch
";
tool.execute(serde_json::json!({ "patch": patch }), &ctx)
.await
.unwrap();
let after = std::fs::read_to_string(dir.join("a.py")).unwrap();
assert_eq!(
after, "def foo():\n return 1\n\ndef bar():\n return 2\n",
"anchor must target bar, leaving foo untouched: {after:?}"
);
let ins = "\
*** Begin Patch
*** Update File: a.py
@@ def foo():
+ # hello
*** End Patch
";
tool.execute(serde_json::json!({ "patch": ins }), &ctx)
.await
.unwrap();
let after = std::fs::read_to_string(dir.join("a.py")).unwrap();
assert!(
after.starts_with("def foo():\n # hello\n return 1\n"),
"insertion must follow the anchor line, not append at EOF: {after:?}"
);
let bad = "\
*** Begin Patch
*** Update File: a.py
@@ def nope():
- return 1
+ return 9
*** End Patch
";
assert!(tool
.execute(serde_json::json!({ "patch": bad }), &ctx)
.await
.is_err());
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn read_file_oversized_no_slice_truncates_with_notice_instead_of_erroring() {
let (ctx, dir) = ctx();
let tool = ReadFileTool;
let line = "the quick brown fox jumps over the lazy dog\n";
let mut content = String::with_capacity(MAX_READ_BYTES + 50_000 + line.len());
while content.len() < MAX_READ_BYTES + 50_000 {
content.push_str(line);
}
let total = content.len();
std::fs::write(dir.join("big.txt"), &content).unwrap();
let out = tool
.execute(serde_json::json!({ "path": "big.txt" }), &ctx)
.await
.expect("oversized read_file must succeed, not error");
assert!(
out.starts_with("[read_file: file is "),
"missing truncation notice: {:?}",
&out[..out.len().min(120)]
);
assert!(
out.contains(&format!("{total} bytes")),
"notice must state the true file size: {out}"
);
assert!(
out.contains("Pass offset/limit to read more."),
"notice must hint at offset/limit: {out}"
);
assert!(
out.contains("the quick brown fox"),
"must contain the head of the file"
);
assert!(
out.len() <= MAX_READ_BYTES + 200,
"output should stay close to MAX_READ_BYTES, got {} bytes",
out.len()
);
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn read_file_offset_limit_still_works_on_oversized_file_with_no_notice() {
let (ctx, dir) = ctx();
let tool = ReadFileTool;
let mut content = String::new();
for i in 1..=60_000 {
content.push_str(&format!("line {i}\n"));
}
assert!(
content.len() > MAX_READ_BYTES,
"fixture must exceed the cap"
);
std::fs::write(dir.join("big.txt"), &content).unwrap();
let out = tool
.execute(
serde_json::json!({ "path": "big.txt", "offset": 5, "limit": 3 }),
&ctx,
)
.await
.unwrap();
assert_eq!(out, "line 5\nline 6\nline 7");
assert!(
!out.contains("[read_file:"),
"sliced reads must not carry a truncation notice: {out}"
);
std::fs::remove_dir_all(&dir).ok();
}
#[tokio::test]
async fn read_file_small_file_returns_full_content_with_no_notice() {
let (ctx, dir) = ctx();
let tool = ReadFileTool;
let content = "hello\nworld\n";
std::fs::write(dir.join("small.txt"), content).unwrap();
let out = tool
.execute(serde_json::json!({ "path": "small.txt" }), &ctx)
.await
.unwrap();
assert_eq!(out, content, "small file must round-trip byte-for-byte");
assert!(
!out.contains("[read_file:"),
"small file must not carry a truncation notice: {out}"
);
std::fs::remove_dir_all(&dir).ok();
}