diff --git a/pkg/agentic/remote_dispatch_test.go b/pkg/agentic/remote_dispatch_test.go new file mode 100644 index 0000000..95532f3 --- /dev/null +++ b/pkg/agentic/remote_dispatch_test.go @@ -0,0 +1,186 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// --- dispatchRemote --- + +func TestDispatchRemote_Bad_MissingHost(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + _, _, err := s.dispatchRemote(context.Background(), nil, RemoteDispatchInput{ + Repo: "go-io", Task: "do it", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "host is required") +} + +func TestDispatchRemote_Bad_MissingRepo(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + _, _, err := s.dispatchRemote(context.Background(), nil, RemoteDispatchInput{ + Host: "charon", Task: "do it", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "repo is required") +} + +func TestDispatchRemote_Bad_MissingTask(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + _, _, err := s.dispatchRemote(context.Background(), nil, RemoteDispatchInput{ + Host: "charon", Repo: "go-io", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "task is required") +} + +func TestDispatchRemote_Good_FullRoundtrip(t *testing.T) { + callCount := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + w.Header().Set("Mcp-Session-Id", "test-session") + w.Header().Set("Content-Type", "text/event-stream") + + switch callCount { + case 1: + // Initialize response + fmt.Fprintf(w, "data: {\"result\":{}}\n\n") + case 2: + // Initialized notification — just accept + w.WriteHeader(200) + case 3: + // Tool call response + result := map[string]any{ + "result": map[string]any{ + "content": []map[string]any{ + {"text": `{"success":true,"agent":"codex","repo":"go-io","workspace_dir":"/ws/go-io","pid":12345}`}, + }, + }, + } + data, _ := json.Marshal(result) + fmt.Fprintf(w, "data: %s\n\n", data) + } + })) + t.Cleanup(srv.Close) + + // Override resolveHost to use our test server + t.Setenv("AGENT_TOKEN_TESTHOST", "test-token") + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + _, out, err := s.dispatchRemote(context.Background(), nil, RemoteDispatchInput{ + Host: srv.Listener.Addr().String(), + Repo: "go-io", + Task: "Fix tests", + }) + require.NoError(t, err) + assert.True(t, out.Success) + assert.Equal(t, "go-io", out.Repo) +} + +func TestDispatchRemote_Bad_InitFails(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(500) + })) + t.Cleanup(srv.Close) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + _, _, err := s.dispatchRemote(context.Background(), nil, RemoteDispatchInput{ + Host: srv.Listener.Addr().String(), + Repo: "go-io", + Task: "test", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "MCP initialize failed") +} + +// --- statusRemote --- + +func TestStatusRemote_Bad_MissingHost(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + _, _, err := s.statusRemote(context.Background(), nil, RemoteStatusInput{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "host is required") +} + +func TestStatusRemote_Good_Unreachable(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + // Use a port that's not listening + _, out, err := s.statusRemote(context.Background(), nil, RemoteStatusInput{ + Host: "127.0.0.1:1", + }) + assert.NoError(t, err) // returns output, not error + assert.Contains(t, out.Error, "unreachable") +} + +func TestStatusRemote_Good_FullRoundtrip(t *testing.T) { + callCount := 0 + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + callCount++ + w.Header().Set("Mcp-Session-Id", "test-session") + w.Header().Set("Content-Type", "text/event-stream") + + switch callCount { + case 1: + fmt.Fprintf(w, "data: {\"result\":{}}\n\n") + case 2: + w.WriteHeader(200) + case 3: + result := map[string]any{ + "result": map[string]any{ + "content": []map[string]any{ + {"text": `{"total":5,"running":2,"completed":3,"failed":0}`}, + }, + }, + } + data, _ := json.Marshal(result) + fmt.Fprintf(w, "data: %s\n\n", data) + } + })) + t.Cleanup(srv.Close) + + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + + _, out, err := s.statusRemote(context.Background(), nil, RemoteStatusInput{ + Host: srv.Listener.Addr().String(), + }) + require.NoError(t, err) + assert.True(t, out.Success) + assert.Equal(t, 5, out.Stats.Total) + assert.Equal(t, 2, out.Stats.Running) +} diff --git a/pkg/agentic/render_plan_test.go b/pkg/agentic/render_plan_test.go new file mode 100644 index 0000000..0fa9de9 --- /dev/null +++ b/pkg/agentic/render_plan_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: EUPL-1.2 + +package agentic + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// --- renderPlan --- + +func TestRenderPlan_Good_BugFix(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + result := s.renderPlan("bug-fix", nil, "Fix the auth bug") + if result == "" { + t.Skip("bug-fix template not available in embedded assets") + } + assert.Contains(t, result, "Bug Fix") + assert.Contains(t, result, "Fix the auth bug") + assert.Contains(t, result, "Phase 1") + assert.Contains(t, result, "Investigation") +} + +func TestRenderPlan_Good_WithVariables(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + vars := map[string]string{ + "bug_description": "Login fails on mobile", + "location": "pkg/auth/handler.go", + } + result := s.renderPlan("bug-fix", vars, "Fix login") + if result == "" { + t.Skip("bug-fix template not available") + } + assert.Contains(t, result, "Fix login") +} + +func TestRenderPlan_Bad_UnknownTemplate(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + result := s.renderPlan("nonexistent-template-slug", nil, "task") + assert.Empty(t, result) +} + +func TestRenderPlan_Good_NoTask(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + result := s.renderPlan("bug-fix", nil, "") + if result == "" { + t.Skip("template not available") + } + assert.NotContains(t, result, "**Task:**") +} + +func TestRenderPlan_Good_NewFeature(t *testing.T) { + s := &PrepSubsystem{ + backoff: make(map[string]time.Time), + failCount: make(map[string]int), + } + result := s.renderPlan("new-feature", nil, "Add caching") + if result == "" { + t.Skip("new-feature template not available") + } + assert.Contains(t, result, "Add caching") +}