Files
AutoZero/tools/edit_match_test.go
T
Sakurasan d5dac5a6fc feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)
llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、
builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、
编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。

产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt)
留在 AutoGo,宿主接入方式见 MIGRATION.md。
2026-10-26 00:54:46 +08:00

141 lines
4.2 KiB
Go

package tools
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
)
// editIn runs the edit tool against a temp file. It returns the resulting file
// content and the tool error.
func editIn(t *testing.T, content, input string) (string, error) {
t.Helper()
dir := t.TempDir()
p := filepath.Join(dir, "f.txt")
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
reg := NewRegistry()
for _, tl := range BuiltinIn(dir, NewFence(), nil) {
if err := reg.Register(tl); err != nil {
t.Fatal(err)
}
}
e, _ := reg.Get("edit")
input = strings.ReplaceAll(input, "PATH", strings.ReplaceAll(p, `\`, `\\`))
if _, err := e.Run(context.Background(), input); err != nil {
return "", err
}
got, err := os.ReadFile(p)
if err != nil {
t.Fatal(err)
}
return string(got), nil
}
// TestEditRequiresUniqueMatch:多个匹配默认必须报错,不能像以前那样静默改第一个。
func TestEditRequiresUniqueMatch(t *testing.T) {
_, err := editIn(t, "foo\nbar\nfoo\n", `{"path":"PATH","old_string":"foo","new_string":"x"}`)
if err == nil {
t.Fatal("multiple matches must fail without replace_all")
}
if !strings.Contains(err.Error(), "2 times") || !strings.Contains(err.Error(), "replace_all") {
t.Fatalf("error should name the count and the escape hatch, got %q", err)
}
}
func TestEditReplaceAll(t *testing.T) {
got, err := editIn(t, "foo\nbar\nfoo\n", `{"path":"PATH","old_string":"foo","new_string":"x","replace_all":true}`)
if err != nil {
t.Fatal(err)
}
if got != "x\nbar\nx\n" {
t.Fatalf("got %q", got)
}
}
func TestEditUniqueReplacement(t *testing.T) {
got, err := editIn(t, "alpha beta gamma\n", `{"path":"PATH","old_string":"beta","new_string":"B"}`)
if err != nil {
t.Fatal(err)
}
if got != "alpha B gamma\n" {
t.Fatalf("got %q", got)
}
}
// TestEditNormalizesTypographicCharacters:模型从网页复制的弯引号/破折号,
// 用 ASCII 也能命中;写回后文件里是 new_string 的原始字节。
func TestEditNormalizesTypographicCharacters(t *testing.T) {
got, err := editIn(t, "say “hello” — now\n", `{"path":"PATH","old_string":"say \"hello\" - now","new_string":"say \"bye\" - later"}`)
if err != nil {
t.Fatal(err)
}
if got != "say \"bye\" - later\n" {
t.Fatalf("got %q", got)
}
}
// TestEditIgnoresTrailingWhitespace:行尾多/少空格时退到逐行匹配,
// 且替换后不把旧行的多余空白带回来。
func TestEditIgnoresTrailingWhitespace(t *testing.T) {
got, err := editIn(t, "a \nb\t\nc\n", `{"path":"PATH","old_string":"a\nb\n","new_string":"x\ny\n"}`)
if err != nil {
t.Fatal(err)
}
if got != "x\ny\nc\n" {
t.Fatalf("got %q", got)
}
}
// TestEditPreservesCRLF:命中 CRLF 区域时,LF 风格的 new_string 要转成 CRLF。
func TestEditPreservesCRLF(t *testing.T) {
got, err := editIn(t, "one\r\ntwo\r\nthree\r\n", `{"path":"PATH","old_string":"one\ntwo","new_string":"1\n2"}`)
if err != nil {
t.Fatal(err)
}
if got != "1\r\n2\r\nthree\r\n" {
t.Fatalf("got %q", got)
}
}
func TestEditNotFound(t *testing.T) {
_, err := editIn(t, "abc\n", `{"path":"PATH","old_string":"zzz","new_string":"x"}`)
if err == nil || !strings.Contains(err.Error(), "not found") {
t.Fatalf("want not-found error, got %v", err)
}
}
func TestEditRejectsEmptyOldString(t *testing.T) {
_, err := editIn(t, "abc\n", `{"path":"PATH","old_string":"","new_string":"x"}`)
if err == nil || !strings.Contains(err.Error(), "must not be empty") {
t.Fatalf("want empty-old-string error, got %v", err)
}
}
// TestEditAmbiguousDoesNotRecordSnapshot:失败的编辑不能留档。
func TestEditAmbiguousDoesNotRecordSnapshot(t *testing.T) {
dir := t.TempDir()
snap := &recordingSnap{}
reg := NewRegistry()
for _, tl := range BuiltinIn(dir, NewFence(), snap) {
if err := reg.Register(tl); err != nil {
t.Fatal(err)
}
}
e, _ := reg.Get("edit")
p := filepath.Join(dir, "s.txt")
if err := os.WriteFile(p, []byte("x x"), 0o644); err != nil {
t.Fatal(err)
}
if _, err := e.Run(context.Background(),
`{"path":"`+p+`","old_string":"x","new_string":"y"}`); err == nil {
t.Fatal("ambiguous edit must fail")
}
if len(snap.got) != 0 {
t.Fatalf("failed edit recorded a snapshot: %+v", snap.got)
}
}