go-agent/jobrunner/forgejo/source_extra_test.go
Snider 61e01bfdf1 feat: initial go-agent — agentci + jobrunner + plugins marketplace
Consolidates three codebases into a single agent orchestration repo:

- agentci (from go-scm): Clotho dual-run verification, agent config,
  SSH security (sanitisation, secure commands, token masking)
- jobrunner (from go-scm): Poll-dispatch-report pipeline with 7 handlers
  (dispatch, completion, auto-merge, publish draft, dismiss reviews,
  send fix command, tick parent epic)
- plugins marketplace (from agentic/plugins): 27 Claude/Codex/Gemini
  plugins with shared MCP server

All 150+ tests passing across 6 packages.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-21 15:47:19 +00:00

320 lines
8.4 KiB
Go

package forgejo
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"forge.lthn.ai/core/go-agent/jobrunner"
)
func TestForgejoSource_Poll_Good_InvalidRepo(t *testing.T) {
// Invalid repo format should be logged and skipped, not error.
s := New(Config{Repos: []string{"invalid-no-slash"}}, nil)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
assert.Empty(t, signals)
}
func TestForgejoSource_Poll_Good_MultipleRepos(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(path, "/issues"):
// Return one epic per repo.
issues := []map[string]any{
{
"number": 1,
"body": "- [ ] #2\n",
"labels": []map[string]string{{"name": "epic"}},
"state": "open",
},
}
_ = json.NewEncoder(w).Encode(issues)
case strings.Contains(path, "/pulls"):
prs := []map[string]any{
{
"number": 10,
"body": "Fixes #2",
"state": "open",
"mergeable": true,
"merged": false,
"head": map[string]string{"sha": "abc", "ref": "fix", "label": "fix"},
},
}
_ = json.NewEncoder(w).Encode(prs)
case strings.Contains(path, "/status"):
_ = json.NewEncoder(w).Encode(map[string]any{
"state": "success",
"total_count": 1,
"statuses": []any{},
})
default:
w.WriteHeader(http.StatusOK)
}
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"org-a/repo-1", "org-b/repo-2"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
assert.Len(t, signals, 2)
}
func TestForgejoSource_Poll_Good_NeedsCoding(t *testing.T) {
// When a child issue has no linked PR but is assigned, NeedsCoding should be true.
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(path, "/issues/5"):
// Child issue with assignee.
_ = json.NewEncoder(w).Encode(map[string]any{
"number": 5,
"title": "Implement feature",
"body": "Please implement this.",
"state": "open",
"assignees": []map[string]any{{"login": "darbs-claude", "username": "darbs-claude"}},
})
case strings.Contains(path, "/issues"):
issues := []map[string]any{
{
"number": 1,
"body": "- [ ] #5\n",
"labels": []map[string]string{{"name": "epic"}},
"state": "open",
},
}
_ = json.NewEncoder(w).Encode(issues)
case strings.Contains(path, "/pulls"):
// No PRs linked.
_ = json.NewEncoder(w).Encode([]any{})
default:
w.WriteHeader(http.StatusOK)
}
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"test-org/test-repo"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
require.Len(t, signals, 1)
sig := signals[0]
assert.True(t, sig.NeedsCoding)
assert.Equal(t, "darbs-claude", sig.Assignee)
assert.Equal(t, "Implement feature", sig.IssueTitle)
assert.Equal(t, "Please implement this.", sig.IssueBody)
assert.Equal(t, 5, sig.ChildNumber)
}
func TestForgejoSource_Poll_Good_MergedPR(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(path, "/issues"):
issues := []map[string]any{
{
"number": 1,
"body": "- [ ] #3\n",
"labels": []map[string]string{{"name": "epic"}},
"state": "open",
},
}
_ = json.NewEncoder(w).Encode(issues)
case strings.Contains(path, "/pulls"):
prs := []map[string]any{
{
"number": 20,
"body": "Fixes #3",
"state": "closed",
"mergeable": false,
"merged": true,
"head": map[string]string{"sha": "merged123", "ref": "fix", "label": "fix"},
},
}
_ = json.NewEncoder(w).Encode(prs)
case strings.Contains(path, "/status"):
_ = json.NewEncoder(w).Encode(map[string]any{
"state": "success",
"total_count": 1,
"statuses": []any{},
})
default:
w.WriteHeader(http.StatusOK)
}
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"org/repo"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
require.Len(t, signals, 1)
assert.Equal(t, "MERGED", signals[0].PRState)
assert.Equal(t, "UNKNOWN", signals[0].Mergeable)
}
func TestForgejoSource_Poll_Good_NoHeadSHA(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(path, "/issues"):
issues := []map[string]any{
{
"number": 1,
"body": "- [ ] #3\n",
"labels": []map[string]string{{"name": "epic"}},
"state": "open",
},
}
_ = json.NewEncoder(w).Encode(issues)
case strings.Contains(path, "/pulls"):
prs := []map[string]any{
{
"number": 20,
"body": "Fixes #3",
"state": "open",
"mergeable": true,
"merged": false,
// No head field.
},
}
_ = json.NewEncoder(w).Encode(prs)
default:
w.WriteHeader(http.StatusOK)
}
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"org/repo"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
require.Len(t, signals, 1)
// Without head SHA, check status stays PENDING.
assert.Equal(t, "PENDING", signals[0].CheckStatus)
}
func TestForgejoSource_Report_Good_Nil(t *testing.T) {
s := New(Config{}, nil)
err := s.Report(context.Background(), nil)
assert.NoError(t, err)
}
func TestForgejoSource_Report_Good_Failed(t *testing.T) {
var capturedBody string
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var body map[string]string
_ = json.NewDecoder(r.Body).Decode(&body)
capturedBody = body["body"]
_ = json.NewEncoder(w).Encode(map[string]any{"id": 1})
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{}, client)
result := &jobrunner.ActionResult{
Action: "dispatch",
RepoOwner: "org",
RepoName: "repo",
EpicNumber: 1,
ChildNumber: 2,
PRNumber: 3,
Success: false,
Error: "transfer failed",
}
err := s.Report(context.Background(), result)
require.NoError(t, err)
assert.Contains(t, capturedBody, "failed")
assert.Contains(t, capturedBody, "transfer failed")
}
func TestForgejoSource_Poll_Good_APIErrors(t *testing.T) {
// When the issues API fails, poll should continue with other repos.
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"org/repo"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
assert.Empty(t, signals)
}
func TestForgejoSource_Poll_Good_EmptyRepos(t *testing.T) {
s := New(Config{Repos: []string{}}, nil)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
assert.Empty(t, signals)
}
func TestForgejoSource_Poll_Good_NonEpicIssues(t *testing.T) {
srv := httptest.NewServer(withVersion(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
w.Header().Set("Content-Type", "application/json")
switch {
case strings.Contains(path, "/issues"):
// Issues without the "epic" label.
issues := []map[string]any{
{
"number": 1,
"body": "- [ ] #2\n",
"labels": []map[string]string{{"name": "bug"}},
"state": "open",
},
}
_ = json.NewEncoder(w).Encode(issues)
default:
w.WriteHeader(http.StatusOK)
}
})))
defer srv.Close()
client := newTestClient(t, srv.URL)
s := New(Config{Repos: []string{"org/repo"}}, client)
signals, err := s.Poll(context.Background())
require.NoError(t, err)
assert.Empty(t, signals, "non-epic issues should not generate signals")
}