项目文件夹

文件
2026-07-13 13:00:08 +08:00

139 行
5.0 KiB
Go

package agent
import (
"context"
"strings"
"sync/atomic"
"testing"
"reasonix/internal/event"
"reasonix/internal/provider"
"reasonix/internal/tool"
)
func TestAnchorEditRequiresReadAfterSameTurnWrite(t *testing.T) {
var editCalls int32
reg := tool.NewRegistry()
reg.Add(fakeTool{name: "edit_file", readOnly: false, calls: &editCalls})
args := `{"path":"src/map.html","old_string":"before","new_string":"after"}`
prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
{
toolCallChunk("c1", "edit_file", args),
toolCallChunk("c2", "edit_file", args),
{Type: provider.ChunkDone},
},
{{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}},
}}
a := New(prov, reg, NewSession(""), Options{}, event.Discard)
if err := a.Run(context.Background(), "edit the map"); err != nil {
t.Fatalf("Run: %v", err)
}
if got := atomic.LoadInt32(&editCalls); got != 1 {
t.Fatalf("edit_file executed %d times, want only the first call", got)
}
results := toolResults(a.session, "edit_file")
if len(results) != 2 {
t.Fatalf("tool results = %d, want 2", len(results))
}
last := results[len(results)-1]
for _, want := range []string{"[fresh read required]", "read_file", "multi_edit"} {
if !strings.Contains(last, want) {
t.Fatalf("blocked result should mention %q, got %q", want, last)
}
}
}
func TestAnchorEditAllowedAfterFreshRead(t *testing.T) {
var editCalls int32
var readCalls int32
reg := tool.NewRegistry()
reg.Add(fakeTool{name: "edit_file", readOnly: false, calls: &editCalls})
reg.Add(fakeTool{name: "read_file", readOnly: true, calls: &readCalls})
prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
{
toolCallChunk("c1", "edit_file", `{"path":"src/map.html","old_string":"before","new_string":"after"}`),
toolCallChunk("c2", "read_file", `{"path":"src/map.html"}`),
toolCallChunk("c3", "edit_file", `{"path":"src/map.html","old_string":"current","new_string":"final"}`),
{Type: provider.ChunkDone},
},
{{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}},
}}
a := New(prov, reg, NewSession(""), Options{}, event.Discard)
if err := a.Run(context.Background(), "edit the map with a read between edits"); err != nil {
t.Fatalf("Run: %v", err)
}
if got := atomic.LoadInt32(&readCalls); got != 1 {
t.Fatalf("read_file executed %d times, want 1", got)
}
if got := atomic.LoadInt32(&editCalls); got != 2 {
t.Fatalf("edit_file executed %d times, want 2 after fresh read", got)
}
if last := lastToolResult(a.session, "edit_file"); strings.Contains(last, "[fresh read required]") {
t.Fatalf("fresh read should allow the second edit, got %q", last)
}
}
func TestAnchorEditStillRequiresReadAfterWindowedRead(t *testing.T) {
var editCalls int32
var readCalls int32
reg := tool.NewRegistry()
reg.Add(fakeTool{name: "edit_file", readOnly: false, calls: &editCalls})
reg.Add(fakeTool{name: "read_file", readOnly: true, calls: &readCalls})
prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
{
toolCallChunk("c1", "edit_file", `{"path":"src/map.html","old_string":"before","new_string":"after"}`),
toolCallChunk("c2", "read_file", `{"path":"src/map.html","offset":400,"limit":20}`),
toolCallChunk("c3", "edit_file", `{"path":"src/map.html","old_string":"current","new_string":"final"}`),
{Type: provider.ChunkDone},
},
{{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}},
}}
a := New(prov, reg, NewSession(""), Options{}, event.Discard)
if err := a.Run(context.Background(), "edit the map with a narrow read between edits"); err != nil {
t.Fatalf("Run: %v", err)
}
if got := atomic.LoadInt32(&readCalls); got != 1 {
t.Fatalf("read_file executed %d times, want 1", got)
}
if got := atomic.LoadInt32(&editCalls); got != 1 {
t.Fatalf("edit_file executed %d times, want only the first call", got)
}
if last := lastToolResult(a.session, "edit_file"); !strings.Contains(last, "[fresh read required]") {
t.Fatalf("windowed read should not allow the second edit, got %q", last)
}
}
func TestMultiEditAllowedAfterSameTurnWrite(t *testing.T) {
var editCalls int32
var multiCalls int32
reg := tool.NewRegistry()
reg.Add(fakeTool{name: "edit_file", readOnly: false, calls: &editCalls})
reg.Add(fakeTool{name: "multi_edit", readOnly: false, calls: &multiCalls})
prov := &scriptedProvider{name: "p", turns: [][]provider.Chunk{
{
toolCallChunk("c1", "edit_file", `{"path":"src/map.html","old_string":"before","new_string":"after"}`),
toolCallChunk("c2", "multi_edit", `{"path":"src/map.html","edits":[{"old_string":"current","new_string":"final"}]}`),
{Type: provider.ChunkDone},
},
{{Type: provider.ChunkText, Text: "done"}, {Type: provider.ChunkDone}},
}}
a := New(prov, reg, NewSession(""), Options{}, event.Discard)
if err := a.Run(context.Background(), "edit the map atomically"); err != nil {
t.Fatalf("Run: %v", err)
}
if got := atomic.LoadInt32(&editCalls); got != 1 {
t.Fatalf("edit_file executed %d times, want 1", got)
}
if got := atomic.LoadInt32(&multiCalls); got != 1 {
t.Fatalf("multi_edit executed %d times, want 1", got)
}
}