agent/pkg/agentic/epic_test.go
Snider aafa63818f fix: remove dead client field from PrepSubsystem + test literals
client *http.Client removed — all HTTP routes through transport.go.
75 test struct literals cleaned, 3 test assertions updated.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 01:32:17 +00:00

448 lines
12 KiB
Go

// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"time"
core "dappco.re/go/core"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockForgeServer creates an httptest server that handles Forge API calls
// for issues and labels. Returns the server and a counter of issues created.
func mockForgeServer(t *testing.T) (*httptest.Server, *atomic.Int32) {
t.Helper()
issueCounter := &atomic.Int32{}
mux := http.NewServeMux()
// Create issue
mux.HandleFunc("/api/v1/repos/", func(w http.ResponseWriter, r *http.Request) {
// Route based on method + path suffix
if r.Method == "POST" && pathEndsWith(r.URL.Path, "/issues") {
num := int(issueCounter.Add(1))
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"number": num,
"html_url": "https://forge.test/core/test-repo/issues/" + itoa(num),
})
return
}
// Create/list labels
if pathEndsWith(r.URL.Path, "/labels") {
if r.Method == "GET" {
json.NewEncoder(w).Encode([]map[string]any{
{"id": 1, "name": "agentic"},
{"id": 2, "name": "bug"},
})
return
}
if r.Method == "POST" {
w.WriteHeader(201)
json.NewEncoder(w).Encode(map[string]any{
"id": issueCounter.Load() + 100,
})
return
}
}
// List issues (for scan)
if r.Method == "GET" && pathEndsWith(r.URL.Path, "/issues") {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 1,
"title": "Test issue",
"labels": []map[string]any{{"name": "agentic"}},
"assignee": nil,
"html_url": "https://forge.test/core/test-repo/issues/1",
},
})
return
}
// Issue labels (for verify)
if r.Method == "POST" && containsStr(r.URL.Path, "/labels") {
w.WriteHeader(200)
return
}
// PR merge
if r.Method == "POST" && containsStr(r.URL.Path, "/merge") {
w.WriteHeader(200)
return
}
// Issue comments
if r.Method == "POST" && containsStr(r.URL.Path, "/comments") {
w.WriteHeader(201)
return
}
w.WriteHeader(404)
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv, issueCounter
}
func pathEndsWith(path, suffix string) bool {
if len(path) < len(suffix) {
return false
}
return path[len(path)-len(suffix):] == suffix
}
func containsStr(s, sub string) bool {
for i := 0; i+len(sub) <= len(s); i++ {
if s[i:i+len(sub)] == sub {
return true
}
}
return false
}
func itoa(n int) string {
if n == 0 {
return "0"
}
digits := make([]byte, 0, 10)
for n > 0 {
digits = append([]byte{byte('0' + n%10)}, digits...)
n /= 10
}
return string(digits)
}
// newTestSubsystem creates a PrepSubsystem wired to a mock Forge server.
func newTestSubsystem(t *testing.T, srv *httptest.Server) *PrepSubsystem {
t.Helper()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
brainURL: srv.URL,
brainKey: "test-brain-key",
codePath: t.TempDir(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
return s
}
// --- createIssue ---
func TestEpic_CreateIssue_Good_Success(t *testing.T) {
srv, counter := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "Fix the bug", "Description", []int64{1})
require.NoError(t, err)
assert.Equal(t, 1, child.Number)
assert.Equal(t, "Fix the bug", child.Title)
assert.Contains(t, child.URL, "issues/1")
assert.Equal(t, int32(1), counter.Load())
}
func TestEpic_CreateIssue_Good_NoLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "No labels task", "", nil)
require.NoError(t, err)
assert.Equal(t, "No labels task", child.Title)
}
func TestEpic_CreateIssue_Good_WithBody(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
child, err := s.createIssue(context.Background(), "core", "test-repo", "Task with body", "Detailed description", []int64{1, 2})
require.NoError(t, err)
assert.NotZero(t, child.Number)
}
func TestEpic_CreateIssue_Bad_ServerDown(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close() // immediately close
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forgeURL: srv.URL,
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.createIssue(context.Background(), "core", "test-repo", "Title", "", nil)
assert.Error(t, err)
}
func TestEpic_CreateIssue_Bad_Non201Response(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forgeURL: srv.URL,
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.createIssue(context.Background(), "core", "test-repo", "Title", "", nil)
assert.Error(t, err)
}
// --- resolveLabelIDs ---
func TestEpic_ResolveLabelIDs_Good_ExistingLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"agentic", "bug"})
assert.Len(t, ids, 2)
assert.Contains(t, ids, int64(1))
assert.Contains(t, ids, int64(2))
}
func TestEpic_ResolveLabelIDs_Good_NewLabel(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// "new-label" doesn't exist in mock, so it will be created
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"new-label"})
assert.NotEmpty(t, ids)
}
func TestEpic_ResolveLabelIDs_Good_EmptyNames(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", nil)
assert.Nil(t, ids)
}
func TestEpic_ResolveLabelIDs_Bad_ServerError(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(500)
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forgeURL: srv.URL,
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"agentic"})
assert.Nil(t, ids)
}
// --- createLabel ---
func TestEpic_CreateLabel_Good_Known(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
id := s.createLabel(context.Background(), "core", "test-repo", "agentic")
assert.NotZero(t, id)
}
func TestEpic_CreateLabel_Good_Unknown(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// Unknown label uses default colour
id := s.createLabel(context.Background(), "core", "test-repo", "custom-label")
assert.NotZero(t, id)
}
func TestEpic_CreateLabel_Bad_ServerDown(t *testing.T) {
srv := httptest.NewServer(http.NotFoundHandler())
srv.Close()
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forgeURL: srv.URL,
forgeToken: "test-token",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
id := s.createLabel(context.Background(), "core", "test-repo", "agentic")
assert.Zero(t, id)
}
// --- createEpic (validation only, not full dispatch) ---
func TestEpic_CreateEpic_Bad_NoTitle(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Tasks: []string{"Task 1"},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "title is required")
}
func TestEpic_CreateEpic_Bad_NoTasks(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Epic Title",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "at least one task")
}
func TestEpic_CreateEpic_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}),
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Epic",
Tasks: []string{"Task"},
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
func TestEpic_CreateEpic_Good_WithTasks(t *testing.T) {
srv, counter := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Test Epic",
Tasks: []string{"Task 1", "Task 2"},
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.NotZero(t, out.EpicNumber)
assert.Len(t, out.Children, 2)
assert.Equal(t, "Task 1", out.Children[0].Title)
assert.Equal(t, "Task 2", out.Children[1].Title)
// 2 children + 1 epic = 3 issues
assert.Equal(t, int32(3), counter.Load())
}
func TestEpic_CreateEpic_Good_WithLabels(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Labelled Epic",
Tasks: []string{"Do it"},
Labels: []string{"bug"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestEpic_CreateEpic_Good_AgenticLabelAutoAdded(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// No labels specified — "agentic" should be auto-added
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "Auto-labelled",
Tasks: []string{"Task"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestEpic_CreateEpic_Good_AgenticLabelNotDuplicated(t *testing.T) {
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
// agentic already present — should not be duplicated
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: "With agentic",
Tasks: []string{"Task"},
Labels: []string{"agentic"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
// --- Ugly tests ---
func TestEpic_CreateEpic_Ugly(t *testing.T) {
// Very long title/description
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
longTitle := strings.Repeat("Very Long Epic Title ", 50)
longBody := strings.Repeat("Detailed description of the epic work to be done. ", 100)
_, out, err := s.createEpic(context.Background(), nil, EpicInput{
Repo: "test-repo",
Title: longTitle,
Body: longBody,
Tasks: []string{"Task 1"},
})
require.NoError(t, err)
assert.True(t, out.Success)
assert.NotZero(t, out.EpicNumber)
}
func TestEpic_CreateIssue_Ugly(t *testing.T) {
// Issue with HTML in body
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
htmlBody := "<h1>Issue</h1><p>This has <b>bold</b> and <script>alert('xss')</script></p>"
child, err := s.createIssue(context.Background(), "core", "test-repo", "HTML Issue", htmlBody, []int64{1})
require.NoError(t, err)
assert.Equal(t, "HTML Issue", child.Title)
assert.NotZero(t, child.Number)
}
func TestEpic_ResolveLabelIDs_Ugly(t *testing.T) {
// Label names with special chars
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
ids := s.resolveLabelIDs(context.Background(), "core", "test-repo", []string{"bug/fix", "feature:new", "label with spaces"})
// These will all be created as new labels since they don't match existing ones
assert.NotNil(t, ids)
}
func TestEpic_CreateLabel_Ugly(t *testing.T) {
// Label with unicode name
srv, _ := mockForgeServer(t)
s := newTestSubsystem(t, srv)
id := s.createLabel(context.Background(), "core", "test-repo", "\u00e9nhancement-\u00fc\u00f1ic\u00f6de")
assert.NotZero(t, id)
}