mirror of
https://gitea.com/gitea/gitea-mcp.git
synced 2026-08-28 11:07:44 +00:00
feat(actions): add wait_for_pr_checks method to actions_run_read
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)
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user