// SPDX-License-Identifier: EUPL-1.2 package agentic import ( "context" "net/http" "net/http/httptest" "strings" "testing" "time" core "dappco.re/go/core" "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) { w.Write([]byte(core.JSONMarshalString([]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) { w.Write([]byte(core.JSONMarshalString([]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) { w.Write([]byte(core.JSONMarshalString([]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) { w.Write([]byte(core.JSONMarshalString([]map[string]any{}))) }) srv := httptest.NewServer(mux) t.Cleanup(srv.Close) return srv } // --- scan --- func TestScan_Scan_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", backoff: make(map[string]time.Time), failCount: make(map[string]int), } _, out, err := s.scan(context.Background(), nil, ScanInput{Org: "core"}) require.NoError(t, err) assert.True(t, out.Success) assert.Greater(t, out.Count, 0) // Verify issues contain repos from mock server repos := make(map[string]bool) for _, iss := range out.Issues { repos[iss.Repo] = true } assert.True(t, repos["go-io"] || repos["go-log"], "should contain issues from mock repos") } func TestScan_AllRepos_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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_WithLimit_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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_DefaultLabels_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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_CustomLabels_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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_Deduplicates_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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_NoToken_Bad(t *testing.T) { s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), 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 TestScan_ListRepoIssues_Good_ReturnsIssues(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forgeURL: srv.URL, forgeToken: "test-token", 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 TestScan_ListRepoIssues_Good_EmptyResult(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forgeURL: srv.URL, forgeToken: "test-token", 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 TestScan_ListRepoIssues_Good_AssigneeExtracted(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forgeURL: srv.URL, forgeToken: "test-token", 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 TestScan_ListRepoIssues_Good_EncodesLabelQuery(t *testing.T) { expectedLabel := "bug+urgent & review" srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, "/api/v1/repos/core/go-io/issues", r.URL.Path) assert.Equal(t, expectedLabel, r.URL.Query().Get("labels")) w.Write([]byte(core.JSONMarshalString([]map[string]any{}))) })) 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), } issues, err := s.listRepoIssues(context.Background(), "core", "go-io", expectedLabel) require.NoError(t, err) assert.Empty(t, issues) } func TestScan_ListRepoIssues_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), } _, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic") assert.Error(t, err) } func TestScan_ListRepoIssues_Bad_InvalidJSON(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte("not json")) })) 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.listRepoIssues(context.Background(), "core", "go-io", "agentic") assert.Error(t, err) assert.Contains(t, err.Error(), "parse issues response") } // --- scan Bad/Ugly --- func TestScan_Scan_Bad(t *testing.T) { // Forge returns error for org repos 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{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", backoff: make(map[string]time.Time), failCount: make(map[string]int), } _, _, err := s.scan(context.Background(), nil, ScanInput{}) assert.Error(t, err) } func TestScan_Scan_Ugly(t *testing.T) { // Org with no repos srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if strings.Contains(r.URL.Path, "/orgs/") { w.Write([]byte(core.JSONMarshalString([]map[string]any{}))) return } w.WriteHeader(404) })) t.Cleanup(srv.Close) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", 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.Equal(t, 0, out.Count) } // --- listOrgRepos Good/Bad/Ugly --- func TestScan_ListOrgRepos_Good(t *testing.T) { srv := mockScanServer(t) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", backoff: make(map[string]time.Time), failCount: make(map[string]int), } repos, err := s.listOrgRepos(context.Background(), "core") require.NoError(t, err) assert.Len(t, repos, 3) assert.Contains(t, repos, "go-io") assert.Contains(t, repos, "go-log") assert.Contains(t, repos, "agent") } func TestScan_ListOrgRepos_Bad(t *testing.T) { // Forge returns error 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{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", backoff: make(map[string]time.Time), failCount: make(map[string]int), } _, err := s.listOrgRepos(context.Background(), "core") assert.Error(t, err) } func TestScan_ListOrgRepos_Ugly(t *testing.T) { // Empty org name srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(core.JSONMarshalString([]map[string]any{}))) })) t.Cleanup(srv.Close) s := &PrepSubsystem{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forge: forge.NewForge(srv.URL, "test-token"), forgeURL: srv.URL, forgeToken: "test-token", backoff: make(map[string]time.Time), failCount: make(map[string]int), } repos, err := s.listOrgRepos(context.Background(), "") require.NoError(t, err) assert.Empty(t, repos) } // --- listRepoIssues Ugly --- func TestScan_ListRepoIssues_Ugly(t *testing.T) { // Issues with very long titles srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { longTitle := strings.Repeat("Very Long Issue Title ", 50) w.Write([]byte(core.JSONMarshalString([]map[string]any{ { "number": 1, "title": longTitle, "labels": []map[string]any{{"name": "agentic"}}, "assignee": nil, "html_url": "https://forge.lthn.ai/core/go-io/issues/1", }, }))) })) 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), } issues, err := s.listRepoIssues(context.Background(), "core", "go-io", "agentic") require.NoError(t, err) assert.Len(t, issues, 1) assert.True(t, len(issues[0].Title) > 100) } func TestScan_ListRepoIssues_Good_URLRewrite(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(core.JSONMarshalString([]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{ ServiceRuntime: core.NewServiceRuntime(testCore, AgentOptions{}), forgeURL: srv.URL, forgeToken: "test-token", 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) }