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。
This commit is contained in:
commit
d5dac5a6fc
140 files changed
+26497
No files matched your search
@@ -0,0 +1,140 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user