mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-27 10:37:44 +00:00
e885e5a4e1
Add a wait_for_pr_checks method that resolves a pull request's head SHA, then polls its Actions runs until every run reaches a terminal status/conclusion or a timeout elapses, returning the runs and whether the wait timed out. Co-Authored-By: Codet <codet@commitgo.dev> (GPT-5-Codex)
170 lines
4.8 KiB
Go
170 lines
4.8 KiB
Go
package actions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.com/gitea/gitea-mcp/pkg/flag"
|
|
|
|
"github.com/modelcontextprotocol/go-sdk/mcp"
|
|
)
|
|
|
|
func TestAllRunsTerminal(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
runs []map[string]any
|
|
want bool
|
|
}{
|
|
{"no runs", nil, true},
|
|
{"single completed run with conclusion", []map[string]any{{"status": "completed", "conclusion": "success"}}, true},
|
|
{"single running run", []map[string]any{{"status": "running", "conclusion": ""}}, false},
|
|
{"single waiting run", []map[string]any{{"status": "waiting"}}, false},
|
|
{"mixed terminal and running", []map[string]any{
|
|
{"status": "completed", "conclusion": "success"},
|
|
{"status": "running"},
|
|
}, false},
|
|
{"all terminal", []map[string]any{
|
|
{"status": "completed", "conclusion": "failure"},
|
|
{"status": "completed", "conclusion": "cancelled"},
|
|
}, true},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if got := allRunsTerminal(tt.runs); got != tt.want {
|
|
t.Errorf("allRunsTerminal() = %v, want %v", got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWaitForRunsUntilTerminal_ReturnsOnceTerminal(t *testing.T) {
|
|
calls := 0
|
|
fetch := func(ctx context.Context) ([]map[string]any, error) {
|
|
calls++
|
|
if calls < 3 {
|
|
return []map[string]any{{"status": "running"}}, nil
|
|
}
|
|
return []map[string]any{{"status": "completed", "conclusion": "success"}}, nil
|
|
}
|
|
|
|
runs, timedOut, err := waitForRunsUntilTerminal(context.Background(), time.Second, time.Millisecond, fetch)
|
|
if err != nil {
|
|
t.Fatalf("waitForRunsUntilTerminal() error = %v", err)
|
|
}
|
|
if timedOut {
|
|
t.Fatalf("expected timedOut = false")
|
|
}
|
|
if calls != 3 {
|
|
t.Fatalf("expected 3 fetch calls, got %d", calls)
|
|
}
|
|
if len(runs) != 1 || runs[0]["conclusion"] != "success" {
|
|
t.Fatalf("unexpected runs: %v", runs)
|
|
}
|
|
}
|
|
|
|
func TestWaitForRunsUntilTerminal_TimesOut(t *testing.T) {
|
|
fetch := func(ctx context.Context) ([]map[string]any, error) {
|
|
return []map[string]any{{"status": "running"}}, nil
|
|
}
|
|
|
|
runs, timedOut, err := waitForRunsUntilTerminal(context.Background(), 20*time.Millisecond, time.Millisecond, fetch)
|
|
if err != nil {
|
|
t.Fatalf("waitForRunsUntilTerminal() error = %v", err)
|
|
}
|
|
if !timedOut {
|
|
t.Fatalf("expected timedOut = true")
|
|
}
|
|
if len(runs) != 1 {
|
|
t.Fatalf("expected last fetched runs to be returned, got %v", runs)
|
|
}
|
|
}
|
|
|
|
func TestWaitForRunsUntilTerminal_PropagatesFetchError(t *testing.T) {
|
|
wantErr := errors.New("boom")
|
|
fetch := func(ctx context.Context) ([]map[string]any, error) {
|
|
return nil, wantErr
|
|
}
|
|
|
|
_, _, err := waitForRunsUntilTerminal(context.Background(), time.Second, time.Millisecond, fetch)
|
|
if !errors.Is(err, wantErr) {
|
|
t.Fatalf("waitForRunsUntilTerminal() error = %v, want %v", err, wantErr)
|
|
}
|
|
}
|
|
|
|
func Test_waitForPRChecksFn(t *testing.T) {
|
|
const (
|
|
owner = "octo"
|
|
repo = "demo"
|
|
pullNumber = 42
|
|
headSHA = "abc123"
|
|
)
|
|
|
|
var runsRequests int32
|
|
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch {
|
|
case r.URL.Path == fmt.Sprintf("/api/v1/repos/%s/%s/pulls/%d", owner, repo, pullNumber):
|
|
_, _ = fmt.Fprintf(w, `{"head":{"sha":%q}}`, headSHA)
|
|
case r.URL.Path == fmt.Sprintf("/api/v1/repos/%s/%s/actions/runs", owner, repo):
|
|
atomic.AddInt32(&runsRequests, 1)
|
|
if r.URL.Query().Get("head_sha") != headSHA {
|
|
t.Errorf("expected head_sha query param %q, got %q", headSHA, r.URL.Query().Get("head_sha"))
|
|
}
|
|
_, _ = fmt.Fprint(w, `{"workflow_runs":[{"id":1,"status":"completed","conclusion":"success"},{"id":2,"status":"completed","conclusion":"failure"}]}`)
|
|
default:
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
|
|
server := httptest.NewServer(handler)
|
|
defer server.Close()
|
|
|
|
var mu sync.Mutex
|
|
mu.Lock()
|
|
origHost, origToken := flag.Host, flag.Token
|
|
flag.Host, flag.Token = server.URL, ""
|
|
mu.Unlock()
|
|
defer func() {
|
|
mu.Lock()
|
|
flag.Host, flag.Token = origHost, origToken
|
|
mu.Unlock()
|
|
}()
|
|
|
|
args := map[string]any{
|
|
"owner": owner,
|
|
"repo": repo,
|
|
"pull_number": float64(pullNumber),
|
|
}
|
|
|
|
result, err := waitForPRChecksFn(context.Background(), args)
|
|
if err != nil {
|
|
t.Fatalf("waitForPRChecksFn() error = %v", err)
|
|
}
|
|
if atomic.LoadInt32(&runsRequests) != 1 {
|
|
t.Fatalf("expected exactly 1 runs request, got %d", runsRequests)
|
|
}
|
|
|
|
if len(result.Content) == 0 {
|
|
t.Fatalf("expected content in result")
|
|
}
|
|
textContent, ok := result.Content[0].(*mcp.TextContent)
|
|
if !ok {
|
|
t.Fatalf("expected text content, got %T", result.Content[0])
|
|
}
|
|
if !strings.Contains(textContent.Text, headSHA) {
|
|
t.Fatalf("expected result to mention head sha %q, got %s", headSHA, textContent.Text)
|
|
}
|
|
if !strings.Contains(textContent.Text, `"timed_out":false`) {
|
|
t.Fatalf("expected result to report timed_out=false, got %s", textContent.Text)
|
|
}
|
|
}
|