Files
Agent-System/internal/agent/backend_test.go
T
weichenandCursor 98619c7b56 feat(agent): classify settled-failure as bounded retryable
Treat process-0 plus terminated/Connection error/auto_retry_end as a
provider transient. Permanent missing deliverables stay fail-closed and
are never reconstructed from truncated write arguments.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 19:16:46 +00:00

266 lines
7.5 KiB
Go

package agent_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
"chat-agent-system/internal/agent"
"chat-agent-system/internal/config"
)
func TestStubBackendExecution(t *testing.T) {
tmpDir := t.TempDir()
cfg := &config.Config{AgentBackend: "stub"}
backend := agent.NewBackend("stub", cfg)
if backend.Name() != "stub" {
t.Errorf("expected stub backend, got %s", backend.Name())
}
req := agent.Request{
WorkDir: tmpDir,
Prompt: "请编写用户初始需求收集表",
Targets: []string{"output.md"},
Timeout: 5 * time.Second,
}
res, err := backend.Execute(context.Background(), req)
if err != nil {
t.Fatalf("stub execute failed: %v", err)
}
if !res.Success {
t.Fatalf("expected success")
}
outputPath := filepath.Join(tmpDir, "output.md")
if _, err := os.Stat(outputPath); err != nil {
t.Fatalf("expected output.md to exist: %v", err)
}
}
func TestPiAgentRemoteRunner(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/run" {
http.NotFound(w, r)
return
}
var payload map[string]any
_ = json.NewDecoder(r.Body).Decode(&payload)
resp := map[string]any{
"ok": true,
"patches": []map[string]string{
{
"rel_path": "output.md",
"content": "# Remote Generated Deliverable\nValidated from Container Runner",
},
},
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}))
defer ts.Close()
tmpDir := t.TempDir()
cfg := &config.Config{
AgentBackend: "pi-agent",
PiAgentRunnerURL: ts.URL,
PiAgentModel: "MiniMax-M3",
PiAgentProvider: "minimax",
AgentTimeout: 5 * time.Second,
}
backend := agent.NewBackend("pi-agent", cfg)
res, err := backend.Execute(context.Background(), agent.Request{
WorkDir: tmpDir,
Prompt: "Generate remote test document",
Targets: []string{"output.md"},
})
if err != nil {
t.Fatalf("remote execute returned unexpected error: %v", err)
}
if !res.Success {
t.Fatalf("expected success, got error: %s", res.ErrorReason)
}
if string(res.Patches["output.md"]) != "# Remote Generated Deliverable\nValidated from Container Runner" {
t.Errorf("unexpected content: %s", string(res.Patches["output.md"]))
}
// Verify local file written
localContent, err := os.ReadFile(filepath.Join(tmpDir, "output.md"))
if err != nil {
t.Fatalf("expected local file written: %v", err)
}
if string(localContent) != "# Remote Generated Deliverable\nValidated from Container Runner" {
t.Errorf("unexpected local file content: %s", string(localContent))
}
}
func TestPiAgentRemoteRunner_PathAliasesAndMissing(t *testing.T) {
run := func(mode, want string, expectOK bool) {
t.Helper()
dir := filepath.Join(t.TempDir(), "tasks", "01_draft")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/run" {
http.NotFound(w, r)
return
}
var patches []map[string]string
switch mode {
case "abs":
patches = []map[string]string{{
"rel_path": filepath.Join(dir, "output.md"),
"content": "# from absolute rel_path\n",
}}
case "nested":
patches = []map[string]string{{
"rel_path": "tasks/01_draft/output.md",
"content": "# from nested rel_path\n",
}}
case "dot":
patches = []map[string]string{{
"rel_path": "./output.md",
"content": "# from dot rel_path\n",
}}
default:
patches = []map[string]string{{
"rel_path": "execution.log",
"content": "Outcome: SUCCESS\n",
}}
}
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "patches": patches})
}))
defer ts.Close()
cfg := &config.Config{
AgentBackend: "pi-agent",
PiAgentRunnerURL: ts.URL,
PiAgentModel: "MiniMax-M3",
PiAgentProvider: "minimax",
AgentTimeout: 5 * time.Second,
}
res, err := agent.NewBackend("pi-agent", cfg).Execute(context.Background(), agent.Request{
WorkDir: dir,
Prompt: "draft",
Targets: []string{"output.md"},
})
if err != nil {
t.Fatalf("mode %s unexpected err: %v", mode, err)
}
if expectOK {
if !res.Success {
t.Fatalf("mode %s want success, got %s", mode, res.ErrorReason)
}
got, rerr := os.ReadFile(filepath.Join(dir, "output.md"))
if rerr != nil {
t.Fatalf("mode %s missing file: %v", mode, rerr)
}
if string(got) != want {
t.Fatalf("mode %s content %q", mode, got)
}
return
}
if res.Success {
t.Fatalf("mode %s must fail closed", mode)
}
if !strings.Contains(res.ErrorReason, "missing target deliverable output.md") {
t.Fatalf("mode %s reason %q", mode, res.ErrorReason)
}
if _, serr := os.Stat(filepath.Join(dir, "output.md")); serr == nil {
t.Fatalf("mode %s must not invent output.md", mode)
}
}
run("abs", "# from absolute rel_path\n", true)
run("nested", "# from nested rel_path\n", true)
run("dot", "# from dot rel_path\n", true)
run("missing", "", false)
}
func TestPiAgentRemoteRunner_PropagatesRetryable(t *testing.T) {
dir := t.TempDir()
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": false,
"retryable": true,
"fallback_reason": "Connection error.",
"patches": []any{},
})
}))
defer ts.Close()
res, err := agent.NewBackend("pi-agent", &config.Config{
AgentBackend: "pi-agent",
PiAgentRunnerURL: ts.URL,
PiAgentModel: "MiniMax-M3",
PiAgentProvider: "minimax",
AgentTimeout: 5 * time.Second,
}).Execute(context.Background(), agent.Request{
WorkDir: dir,
Prompt: "draft",
Targets: []string{"output.md"},
})
if err != nil {
t.Fatal(err)
}
if res.Success || !res.Retryable {
t.Fatalf("want retryable failure, success=%v retryable=%v reason=%s", res.Success, res.Retryable, res.ErrorReason)
}
if _, serr := os.Stat(filepath.Join(dir, "output.md")); serr == nil {
t.Fatal("must not invent output.md")
}
}
func TestPiAgentRemoteRunner_OldSettleLieIsRetryable(t *testing.T) {
dir := t.TempDir()
log := "Outcome: SUCCESS\n" +
`{"type":"message_end","message":{"stopReason":"error","errorMessage":"terminated"}}` + "\n" +
`{"type":"auto_retry_end","success":false,"attempt":3,"finalError":"Connection error."}` + "\n" +
`{"type":"agent_settled"}` + "\n"
if err := os.WriteFile(filepath.Join(dir, "execution.log"), []byte(log), 0o644); err != nil {
t.Fatal(err)
}
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
_ = json.NewEncoder(w).Encode(map[string]any{
"ok": true,
"patches": []map[string]string{{
"rel_path": "execution.log",
"content": "Outcome: SUCCESS\n",
}},
})
}))
defer ts.Close()
res, err := agent.NewBackend("pi-agent", &config.Config{
AgentBackend: "pi-agent",
PiAgentRunnerURL: ts.URL,
PiAgentModel: "MiniMax-M3",
PiAgentProvider: "minimax",
AgentTimeout: 5 * time.Second,
}).Execute(context.Background(), agent.Request{
WorkDir: dir,
Prompt: "draft",
Targets: []string{"output.md"},
})
if err != nil {
t.Fatal(err)
}
if res.Success {
t.Fatal("process-0 settle without output.md must not succeed")
}
if !res.Retryable {
t.Fatalf("old runner lie + provider trace must be retryable, reason=%s", res.ErrorReason)
}
if _, serr := os.Stat(filepath.Join(dir, "output.md")); serr == nil {
t.Fatal("must not reconstruct output.md from truncated write args")
}
}