agent/pkg/agentic/scan_test.go
Snider 537226bd4d feat: AX v0.8.0 upgrade — Core features + quality gates
AX Quality Gates (RFC-025):
- Eliminate os/exec from all test + production code (12+ files)
- Eliminate encoding/json from all test files (15 files, 66 occurrences)
- Eliminate os from all test files except TestMain (Go runtime contract)
- Eliminate path/filepath, net/url from all files
- String concat: 39 violations replaced with core.Concat()
- Test naming AX-7: 264 test functions renamed across all 6 packages
- Example test 1:1 coverage complete

Core Features Adopted:
- Task Composition: agent.completion pipeline (QA → PR → Verify → Ingest → Poke)
- PerformAsync: completion pipeline runs with WaitGroup + progress tracking
- Config: agents.yaml loaded once, feature flags (auto-qa/pr/merge/ingest)
- Named Locks: c.Lock("drain") for queue serialisation
- Registry: workspace state with cross-package QUERY access
- QUERY: c.QUERY(WorkspaceQuery{Status: "running"}) for cross-service queries
- Action descriptions: 25+ Actions self-documenting
- Data mounts: prompts/tasks/flows/personas/workspaces via c.Data()
- Content Actions: agentic.prompt/task/flow/persona callable via IPC
- Drive endpoints: forge + brain registered with tokens
- Drive REST helpers: DriveGet/DrivePost/DriveDo for Drive-aware HTTP
- HandleIPCEvents: auto-discovered by WithService (no manual wiring)
- Entitlement: frozen-queue gate on write Actions
- CLI dispatch: workspace dispatch wired to real dispatch method
- CLI: --quiet/-q and --debug/-d global flags
- CLI: banner, version, check (with service/action/command counts), env
- main.go: minimal — 5 services + c.Run(), no os import
- cmd tests: 84.2% coverage (was 0%)

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-26 06:38:02 +00:00

449 lines
13 KiB
Go

// 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_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)
}
// --- 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)
}