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