Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion crates/toolpath-claude/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1688,7 +1688,11 @@ mod tests {
let tool_step = &path.steps[1];
let ch = &tool_step.change["/src/login.rs"];
let raw = ch.raw.as_deref().expect("edit tool should emit unified diff");
assert!(raw.contains("--- a//src/login.rs"), "{}", raw);
// Leading `/` is stripped from the header so `a/`/`b/` don't double up
// (git-style prefixes already denote the repo root). See #36.
assert!(raw.contains("--- a/src/login.rs"), "{}", raw);
assert!(raw.contains("+++ b/src/login.rs"), "{}", raw);
assert!(!raw.contains("a//"), "header should not double-slash: {}", raw);
assert!(raw.contains("-validate_token()"), "{}", raw);
assert!(raw.contains("+validate_token_v2()"), "{}", raw);

Expand Down
39 changes: 38 additions & 1 deletion crates/toolpath-convo/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,11 +407,17 @@ pub fn file_write_diff(
///
/// Always emits a `--- a/{path}` / `+++ b/{path}` header even when one side is
/// empty so downstream renderers can anchor the change to the file it touched.
///
/// Any leading `/` on `path` is stripped before splicing into the header —
/// git-style `a/` and `b/` prefixes already denote the repo root, so an
/// absolute path like `/abs/file.rs` would otherwise emit `--- a//abs/file.rs`,
/// which breaks `patch(1)` and other consumers that parse the header.
pub fn unified_diff(path: &str, before: &str, after: &str) -> String {
use similar::TextDiff;
let diff = TextDiff::from_lines(before, after);
let display = path.trim_start_matches('/');
let mut out = String::new();
out.push_str(&format!("--- a/{path}\n+++ b/{path}\n"));
out.push_str(&format!("--- a/{display}\n+++ b/{display}\n"));
out.push_str(
&diff
.unified_diff()
Expand Down Expand Up @@ -712,6 +718,37 @@ mod tests {
assert!(!raw.contains("something else entirely"));
}

#[test]
fn test_unified_diff_strips_leading_slash_on_absolute_path() {
// Regression for #36: headers for absolute paths must not contain `a//`.
let raw = unified_diff("/abs/path.rs", "a\n", "b\n");
assert!(
raw.contains("--- a/abs/path.rs\n"),
"missing stripped --- header: {raw}"
);
assert!(
raw.contains("+++ b/abs/path.rs\n"),
"missing stripped +++ header: {raw}"
);
assert!(
!raw.contains("a//"),
"header should not contain doubled slash: {raw}"
);
assert!(
!raw.contains("b//"),
"header should not contain doubled slash: {raw}"
);
}

#[test]
fn test_unified_diff_preserves_relative_path() {
// Relative paths (no leading slash) are unchanged — only a single
// leading `/` is stripped.
let raw = unified_diff("src/login.rs", "a\n", "b\n");
assert!(raw.contains("--- a/src/login.rs\n"), "{raw}");
assert!(raw.contains("+++ b/src/login.rs\n"), "{raw}");
}

#[test]
fn test_tool_use_multiedit_emits_per_hunk_diff() {
let mut turn = base_turn("t1", Role::Assistant);
Expand Down
Loading