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") } }