Why: native must compile without agent-intent, and the two execution packages were the same contract under two names. Evidence: GOWORK=off go test -count=1 ./... passed, including schema.TestRequestAndResultRoundTrip and the native adapter tests after Options was split into embedded sub-structs. Impact: adapters/intentgate is gone, go.mod no longer requires agent-intent, and the nested gitlink stays for the release task. Callers of Options composite literals name the sub-struct. Refs: D1 Co-authored-by: Cursor <cursoragent@cursor.com>
142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
package native
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"agent-native/exec"
|
|
"agent-native/runtime/llm"
|
|
)
|
|
|
|
// script 按序回放假助手轮次(对齐 runtime/loop_test 的伪造方式)。
|
|
func script(turns ...llm.Message) llm.StreamFunc {
|
|
var mu sync.Mutex
|
|
i := 0
|
|
return func(ctx context.Context, model string, messages []llm.Message, tools []llm.ToolDecl, opts llm.StreamOptions) <-chan llm.Event {
|
|
ch := make(chan llm.Event, 4)
|
|
go func() {
|
|
defer close(ch)
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if i >= len(turns) {
|
|
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: llm.Message{Role: llm.RoleAssistant, Content: "done", StopReason: llm.StopEnd}}
|
|
return
|
|
}
|
|
msg := turns[i]
|
|
i++
|
|
if msg.Content != "" {
|
|
ch <- llm.Event{Kind: llm.KindTextDelta, Text: msg.Content}
|
|
}
|
|
ch <- llm.Event{Kind: llm.KindMessageEnd, Message: msg}
|
|
}()
|
|
return ch
|
|
}
|
|
}
|
|
|
|
func TestBackendWorkspaceProfileWritesDeliverable(t *testing.T) {
|
|
dir := t.TempDir()
|
|
b := New(Options{
|
|
ModelOptions: ModelOptions{
|
|
Model: "fake",
|
|
Stream: script(
|
|
llm.Message{Role: llm.RoleAssistant, StopReason: llm.StopToolCalls, ToolCalls: []llm.ToolCall{{
|
|
ID: "c1", Name: "write_file", Arguments: `{"path":"output.md","content":"# 交付\n\n工具写入的交付物内容。"}`,
|
|
}}},
|
|
llm.Message{Role: llm.RoleAssistant, Content: "已写入 output.md", StopReason: llm.StopEnd},
|
|
),
|
|
},
|
|
})
|
|
var events []exec.ProcessEvent
|
|
res, err := b.Execute(context.Background(), exec.Request{
|
|
WorkDir: dir, Prompt: "写交付物", Instructions: "你是测试角色",
|
|
Targets: []string{"output.md"}, OnEvent: func(ev exec.ProcessEvent) { events = append(events, ev) },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Success {
|
|
t.Fatalf("want success, got %+v", res)
|
|
}
|
|
data, err := os.ReadFile(filepath.Join(dir, "output.md"))
|
|
if err != nil || !strings.Contains(string(data), "交付物内容") {
|
|
t.Fatalf("deliverable missing: %v %q", err, data)
|
|
}
|
|
if res.Patches["output.md"] == nil {
|
|
t.Fatal("patches missing output.md")
|
|
}
|
|
for _, f := range []string{"TASK.md", "AGENTS.md", "INSTRUCTIONS.md", "execution.log"} {
|
|
if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
|
|
t.Errorf("workspace file missing: %s", f)
|
|
}
|
|
}
|
|
sawTool := false
|
|
for _, ev := range events {
|
|
if ev.Type == "tool_execution_start" && ev.Tool == "write_file" {
|
|
sawTool = true
|
|
}
|
|
}
|
|
if !sawTool {
|
|
t.Errorf("tool process events not forwarded: %+v", events)
|
|
}
|
|
}
|
|
|
|
func TestBackendReasoningProfileWritesText(t *testing.T) {
|
|
dir := t.TempDir()
|
|
b := New(Options{
|
|
ModelOptions: ModelOptions{
|
|
Model: "fake",
|
|
Stream: script(
|
|
llm.Message{Role: llm.RoleAssistant, Content: "纯文本交付内容(无工具)。", StopReason: llm.StopEnd},
|
|
),
|
|
},
|
|
})
|
|
res, err := b.Execute(context.Background(), exec.Request{
|
|
WorkDir: dir, Prompt: "直接回答", ToolProfile: exec.ToolProfileReasoning,
|
|
Targets: []string{"notes.md"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !res.Success {
|
|
t.Fatalf("want success, got %+v", res)
|
|
}
|
|
data, _ := os.ReadFile(filepath.Join(dir, "notes.md"))
|
|
if !strings.Contains(string(data), "纯文本交付内容") {
|
|
t.Fatalf("reasoning text not written: %q", data)
|
|
}
|
|
}
|
|
|
|
func TestBackendFailClosedWithoutConfig(t *testing.T) {
|
|
b := New(Options{})
|
|
res, err := b.Execute(context.Background(), exec.Request{WorkDir: t.TempDir(), Targets: []string{"x.md"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if res.Success || !strings.Contains(res.ErrorReason, "requires") {
|
|
t.Fatalf("want fail-closed config error, got %+v", res)
|
|
}
|
|
res2, _ := New(Options{
|
|
ModelOptions: ModelOptions{
|
|
APIBase: "http://x",
|
|
APIKey: "k",
|
|
},
|
|
}).Execute(context.Background(),
|
|
exec.Request{WorkDir: t.TempDir(), Targets: []string{"x.md"}})
|
|
if res2.Success || !strings.Contains(res2.ErrorReason, "model") {
|
|
t.Fatalf("want model required error, got %+v", res2)
|
|
}
|
|
res3, _ := New(Options{
|
|
ModelOptions: ModelOptions{
|
|
Stream: script(),
|
|
},
|
|
}).Execute(context.Background(),
|
|
exec.Request{WorkDir: t.TempDir(), Targets: []string{"x.md"}, ToolProfile: "bogus"})
|
|
if res3.Success || !strings.Contains(res3.ErrorReason, "tool profile") {
|
|
t.Fatalf("want profile error, got %+v", res3)
|
|
}
|
|
}
|