From 507bf55eb5857c4e66bfa4e01c3738a3a1c20380 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 24 Mar 2026 23:31:07 +0000 Subject: [PATCH] =?UTF-8?q?test(agentic):=20add=20scan=5Ftest.go=20?= =?UTF-8?q?=E2=80=94=20Forge=20issue=20scanning=20with=20mock=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- pkg/agentic/scan_test.go | 282 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) create mode 100644 pkg/agentic/scan_test.go diff --git a/pkg/agentic/scan_test.go b/pkg/agentic/scan_test.go new file mode 100644 index 0000000..77e506a --- /dev/null +++ b/pkg/agentic/scan_test.go @@ -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) +}