test(agentic): add scan_test.go — Forge issue scanning with mock API

Tests scan tool with mockScanServer (org repos, issue listing, dedup),
listRepoIssues (assignee extraction, URL rewriting, error handling).
11 tests covering filtering, limits, labels, and deduplication.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-24 23:31:07 +00:00
parent 0008e269e4
commit 507bf55eb5

282
pkg/agentic/scan_test.go Normal file
View file

@ -0,0 +1,282 @@
// SPDX-License-Identifier: EUPL-1.2
package agentic
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"dappco.re/go/core/forge"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockScanServer creates a server that handles repo listing and issue listing.
func mockScanServer(t *testing.T) *httptest.Server {
t.Helper()
mux := http.NewServeMux()
// List org repos
mux.HandleFunc("/api/v1/orgs/core/repos", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{"name": "go-io", "full_name": "core/go-io"},
{"name": "go-log", "full_name": "core/go-log"},
{"name": "agent", "full_name": "core/agent"},
})
})
// List issues for repos
mux.HandleFunc("/api/v1/repos/core/go-io/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 10,
"title": "Replace fmt.Errorf with E()",
"labels": []map[string]any{{"name": "agentic"}},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-io/issues/10",
},
{
"number": 11,
"title": "Add missing tests",
"labels": []map[string]any{{"name": "agentic"}, {"name": "help-wanted"}},
"assignee": map[string]any{"login": "virgil"},
"html_url": "https://forge.lthn.ai/core/go-io/issues/11",
},
})
})
mux.HandleFunc("/api/v1/repos/core/go-log/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 5,
"title": "Fix log rotation",
"labels": []map[string]any{{"name": "bug"}},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-log/issues/5",
},
})
})
mux.HandleFunc("/api/v1/repos/core/agent/issues", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{})
})
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)
return srv
}
// --- scan ---
func TestScan_Good_AllRepos(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{})
require.NoError(t, err)
assert.True(t, out.Success)
assert.Greater(t, out.Count, 0)
}
func TestScan_Good_WithLimit(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{Limit: 1})
require.NoError(t, err)
assert.True(t, out.Success)
assert.LessOrEqual(t, out.Count, 1)
}
func TestScan_Good_DefaultLabels(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Default labels: agentic, help-wanted, bug
_, out, err := s.scan(context.Background(), nil, ScanInput{})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestScan_Good_CustomLabels(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, out, err := s.scan(context.Background(), nil, ScanInput{
Labels: []string{"bug"},
})
require.NoError(t, err)
assert.True(t, out.Success)
}
func TestScan_Good_Deduplicates(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forge: forge.NewForge(srv.URL, "test-token"),
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
// Two labels that return the same issues — should be deduped
_, out, err := s.scan(context.Background(), nil, ScanInput{
Labels: []string{"agentic", "help-wanted"},
Limit: 50,
})
require.NoError(t, err)
assert.True(t, out.Success)
// Check no duplicates (same repo+number)
seen := make(map[string]bool)
for _, issue := range out.Issues {
key := issue.Repo + "#" + itoa(issue.Number)
assert.False(t, seen[key], "duplicate issue: %s", key)
seen[key] = true
}
}
func TestScan_Bad_NoToken(t *testing.T) {
s := &PrepSubsystem{
forgeToken: "",
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, _, err := s.scan(context.Background(), nil, ScanInput{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "no Forge token")
}
// --- listRepoIssues ---
func TestListRepoIssues_Good_ReturnsIssues(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
require.NoError(t, err)
assert.Len(t, issues, 2)
assert.Equal(t, "go-io", issues[0].Repo)
assert.Equal(t, 10, issues[0].Number)
assert.Contains(t, issues[0].Labels, "agentic")
}
func TestListRepoIssues_Good_EmptyResult(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "agent", "agentic")
require.NoError(t, err)
assert.Empty(t, issues)
}
func TestListRepoIssues_Good_AssigneeExtracted(t *testing.T) {
srv := mockScanServer(t)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
require.NoError(t, err)
require.Len(t, issues, 2)
assert.Equal(t, "", issues[0].Assignee)
assert.Equal(t, "virgil", issues[1].Assignee)
}
func TestListRepoIssues_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{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
_, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic")
assert.Error(t, err)
}
func TestListRepoIssues_Good_URLRewrite(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]map[string]any{
{
"number": 1,
"title": "Test",
"labels": []map[string]any{},
"assignee": nil,
"html_url": "https://forge.lthn.ai/core/go-io/issues/1",
},
})
}))
t.Cleanup(srv.Close)
s := &PrepSubsystem{
forgeURL: srv.URL,
forgeToken: "test-token",
client: srv.Client(),
backoff: make(map[string]time.Time),
failCount: make(map[string]int),
}
issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "")
require.NoError(t, err)
require.Len(t, issues, 1)
// URL should be rewritten to use the mock server URL
assert.Contains(t, issues[0].URL, srv.URL)
}