esengine--deepseek-reasonix
139 行
5.0 KiB
Go
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)
|
|
}
|
|
}
|