mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* feat(tools): retain bounded structured file diffs * feat(acp): emit structured file changes in tool updates * fix(acp): harden structured file diff transport * fix(tools): reject unsafe rich diff previews * fix(acp): preserve safe rich diff semantics * fix(tools): bind structured diffs to committed changes * fix(acp): preserve structured diff identity * fix(tools): close rich diff race gaps * fix(tools): report partial patch paths from workspace * fix(tools): finalize structured diff evidence * fix(tools): separate unverified patch writes * test(tools): cover failed formatter recovery * fix(acp): serialize file deletions as nullable diffs * fix(tools): recreate files after formatter failure * test(acp): require nullable diff fields on wire
187 lines
7.3 KiB
Go
187 lines
7.3 KiB
Go
package tools
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestBoundedFileDiffRefusesPartialOrBinaryContent(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "a.txt")
|
|
if diff, ok := boundedFileDiff(path, "old", "new", true, true); !ok || diff.Path != path || !diff.OldExists || !diff.NewExists || diff.OldText != "old" || diff.NewText != "new" {
|
|
t.Fatalf("small text diff = %#v, %t", diff, ok)
|
|
}
|
|
for _, tc := range []struct {
|
|
name string
|
|
old string
|
|
new string
|
|
}{
|
|
{"unchanged", "same", "same"},
|
|
{"binary old", string([]byte{0xff}), "text"},
|
|
{"binary new", "text", string([]byte{0xff})},
|
|
{"nul old", "token=sk-proj-abc\x00def", "text"},
|
|
{"escape new", "text", "token=sk-proj-abc\x1bdef"},
|
|
{"c1 old", "token=sk-proj-abc\u0085def", "text"},
|
|
{"zero width space", "token=sk-proj-abc\u200bdef", "text"},
|
|
{"zero width joiner", "token=sk-proj-abc\u200ddef", "text"},
|
|
{"byte order mark", "token=sk-proj-abc\ufeffdef", "text"},
|
|
{"soft hyphen", "token=sk-proj-abc\u00addef", "text"},
|
|
{"non breaking space", "token=sk-proj-abc\u00a0def", "text"},
|
|
{"too large", strings.Repeat("a", maxToolPreviewBytes+1), "b"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if diff, ok := boundedFileDiff(path, tc.old, tc.new, true, true); ok || diff != (FileDiff{}) {
|
|
t.Fatalf("unexpected diff = %#v, %t", diff, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBoundedFileDiffPreservesEmptyFileOperations(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "empty.txt")
|
|
for _, tc := range []struct {
|
|
name string
|
|
oldExists, newExists bool
|
|
}{
|
|
{name: "create", newExists: true},
|
|
{name: "delete", oldExists: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
diff, ok := boundedFileDiff(path, "", "", tc.oldExists, tc.newExists)
|
|
if !ok || diff.OldExists != tc.oldExists || diff.NewExists != tc.newExists {
|
|
t.Fatalf("empty %s = %#v, %t", tc.name, diff, ok)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBoundedUnifiedDiffRejectsUnsafeRichText(t *testing.T) {
|
|
if got := boundedUnifiedDiff("safe.txt", "before\n", "after\n"); got == "" {
|
|
t.Fatal("safe unified diff was unexpectedly omitted")
|
|
}
|
|
for _, content := range []string{
|
|
"token=sk-proj-abc\x00def",
|
|
"token=sk-proj-abc\x1bdef",
|
|
"token=sk-proj-abc\u200bdef",
|
|
string([]byte{0xff}),
|
|
} {
|
|
if got := boundedUnifiedDiff("secret.txt", content, "safe\n"); got != "" {
|
|
t.Fatalf("unsafe rich diff = %q", got)
|
|
}
|
|
}
|
|
|
|
old := strings.Repeat("unchanged\n", 12) + "form\ffeed\n" + strings.Repeat("unchanged\n", 12)
|
|
updated := strings.Replace(old, "unchanged\n", "changed\n", 1)
|
|
if got := boundedUnifiedDiff("safe-hunk.txt", old, updated); got == "" || strings.Contains(got, "\f") {
|
|
t.Fatalf("safe hunk near unrelated unsafe text = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestStructuredPatchFileDiffsPreserveEmptyOperationsAndResultBudget(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
changes := []structuredPatchChange{
|
|
{kind: structuredPatchAdd, to: structuredPatchTarget{absolute: filepath.Join(root, "created"), relative: "created"}},
|
|
{kind: structuredPatchDelete, from: structuredPatchTarget{absolute: filepath.Join(root, "deleted"), relative: "deleted"}},
|
|
{kind: structuredPatchUpdate, from: structuredPatchTarget{absolute: filepath.Join(root, "from"), relative: "from"}, to: structuredPatchTarget{absolute: filepath.Join(root, "to"), relative: "to"}},
|
|
}
|
|
diffs := fileDiffsFromStructuredPatch(".", changes)
|
|
if len(diffs) != 4 {
|
|
t.Fatalf("empty create/delete/move diffs = %#v", diffs)
|
|
}
|
|
for _, diff := range diffs {
|
|
if diff.Path == "" || (!diff.OldExists && !diff.NewExists) {
|
|
t.Fatalf("invalid diff = %#v", diff)
|
|
}
|
|
}
|
|
|
|
large := strings.Repeat("x", maxToolResultFileDiffBytes/3+1)
|
|
budgeted := fileDiffsFromStructuredPatch(".", []structuredPatchChange{
|
|
{kind: structuredPatchAdd, to: structuredPatchTarget{absolute: filepath.Join(root, "one")}, after: large},
|
|
{kind: structuredPatchAdd, to: structuredPatchTarget{absolute: filepath.Join(root, "two")}, after: "tiny"},
|
|
{kind: structuredPatchAdd, to: structuredPatchTarget{absolute: filepath.Join(root, "three")}, after: large},
|
|
{kind: structuredPatchAdd, to: structuredPatchTarget{absolute: filepath.Join(root, "four")}, after: large},
|
|
})
|
|
if len(budgeted) != 3 || filepath.Base(budgeted[0].Path) != "one" || filepath.Base(budgeted[1].Path) != "two" || filepath.Base(budgeted[2].Path) != "three" {
|
|
t.Fatalf("ordered aggregate file-diff budget = %#v", budgeted)
|
|
}
|
|
}
|
|
|
|
func TestStructuredPatchFileDiffsKeepEligibleSameBasenameSibling(t *testing.T) {
|
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
diffs := fileDiffsFromStructuredPatch(".", []structuredPatchChange{
|
|
{
|
|
kind: structuredPatchAdd,
|
|
to: structuredPatchTarget{absolute: filepath.Join(root, "a.go"), relative: "a.go"},
|
|
after: strings.Repeat("x", maxToolPreviewBytes+1),
|
|
},
|
|
{
|
|
kind: structuredPatchAdd,
|
|
to: structuredPatchTarget{absolute: filepath.Join(root, "sub", "a.go"), relative: filepath.Join("sub", "a.go")},
|
|
after: "package sub\n",
|
|
},
|
|
})
|
|
if len(diffs) != 1 || diffs[0].Path != filepath.Join(root, "sub", "a.go") {
|
|
t.Fatalf("same-basename rich diffs = %#v", diffs)
|
|
}
|
|
}
|
|
|
|
func TestBoundedDiffPreservesOrdinaryUnicodeButRejectsObfuscatedSecrets(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "unicode.txt")
|
|
for name, content := range map[string]string{
|
|
"family emoji": "family: 👨👩👧👦\n",
|
|
"emoji variation selector": "heart: ❤️\n",
|
|
"combining grapheme joiner": "ordinary\u034fprose\n",
|
|
"nonbreaking space": "ordinary\u00a0prose\n",
|
|
"byte order mark": "\ufeffdocument\n",
|
|
"soft hyphen": "co\u00adoperate\n",
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if _, ok := boundedFileDiff(path, "before\n", content, true, true); !ok {
|
|
t.Fatalf("ordinary Unicode content was rejected: %q", content)
|
|
}
|
|
if preview := boundedUnifiedDiff("unicode.txt", "before\n", content); preview == "" {
|
|
t.Fatal("ordinary Unicode preview was omitted")
|
|
}
|
|
})
|
|
}
|
|
|
|
secret := "sk-ant-api03-AAAABBBBCCCCDDDDEEEEFFFFGGGG"
|
|
for name, separator := range map[string]string{
|
|
"zero width space": "\u200b",
|
|
"zero width joiner": "\u200d",
|
|
"byte order mark": "\ufeff",
|
|
"soft hyphen": "\u00ad",
|
|
"nonbreaking space": "\u00a0",
|
|
"combining grapheme joiner": "\u034f",
|
|
"variation selector": "\ufe0f",
|
|
} {
|
|
t.Run("split "+name, func(t *testing.T) {
|
|
obfuscated := secret[:20] + separator + secret[20:]
|
|
if _, ok := boundedFileDiff(path, "before\n", obfuscated, true, true); ok {
|
|
t.Fatalf("obfuscated credential produced a rich diff: %q", obfuscated)
|
|
}
|
|
if preview := boundedUnifiedDiff("secret.txt", "before\n", obfuscated); preview != "" {
|
|
t.Fatalf("obfuscated credential produced preview: %q", preview)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriteFileMarksSuppressedObfuscatedSecretAsRedacted(t *testing.T) {
|
|
root := t.TempDir()
|
|
secret := "sk-ant-api03-AAAABBBBCCCCDDDDEEEEFFFFGGGG"
|
|
obfuscated := secret[:20] + "\u200b" + secret[20:]
|
|
result := NewScopedWriteFileTool(root, nil).Run(t.Context(), map[string]any{
|
|
"path": "secret.txt", "content": obfuscated,
|
|
})
|
|
if result.Status != StatusOK || len(result.ChangedFiles) != 1 || len(result.FileDiffs) != 0 || !result.Redacted {
|
|
t.Fatalf("obfuscated-secret result = status=%s changed=%#v diffs=%#v redacted=%t", result.Status, result.ChangedFiles, result.FileDiffs, result.Redacted)
|
|
}
|
|
}
|