Files
github--github-mcp-server/pkg/github/actions_test.go
2025-11-20 16:50:17 +00:00

2179 lines
66 KiB
Go

package github
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"os"
"regexp"
"runtime"
"runtime/debug"
"strings"
"testing"
"github.com/github/github-mcp-server/internal/profiler"
buffer "github.com/github/github-mcp-server/pkg/buffer"
"github.com/github/github-mcp-server/pkg/translations"
"github.com/google/go-github/v79/github"
"github.com/migueleliasweb/go-github-mock/src/mock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_ActionsList_ListWorkflows(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful workflow listing",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsByOwnerByRepo,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
workflows := &github.Workflows{
TotalCount: github.Ptr(2),
Workflows: []*github.Workflow{
{
ID: github.Ptr(int64(123)),
Name: github.Ptr("CI"),
Path: github.Ptr(".github/workflows/ci.yml"),
State: github.Ptr("active"),
CreatedAt: &github.Timestamp{},
UpdatedAt: &github.Timestamp{},
URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/123"),
HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/ci.yml"),
BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/CI/badge.svg"),
NodeID: github.Ptr("W_123"),
},
{
ID: github.Ptr(int64(456)),
Name: github.Ptr("Deploy"),
Path: github.Ptr(".github/workflows/deploy.yml"),
State: github.Ptr("active"),
CreatedAt: &github.Timestamp{},
UpdatedAt: &github.Timestamp{},
URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/workflows/456"),
HTMLURL: github.Ptr("https://github.com/owner/repo/actions/workflows/deploy.yml"),
BadgeURL: github.Ptr("https://github.com/owner/repo/workflows/Deploy/badge.svg"),
NodeID: github.Ptr("W_456"),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(workflows)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflows.String(),
"owner": "owner",
"repo": "repo",
},
expectError: false,
},
{
name: "missing required parameter owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflows.String(),
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: owner",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response github.Workflows
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response.TotalCount)
assert.Greater(t, *response.TotalCount, 0)
assert.NotEmpty(t, response.Workflows)
})
}
}
func Test_RunWorkflow(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful workflow run",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeRunWorkflow.String(),
"owner": "owner",
"repo": "repo",
"workflow_id": "12345",
"ref": "main",
},
expectError: false,
},
{
name: "missing required parameter workflow_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeRunWorkflow.String(),
"owner": "owner",
"repo": "repo",
"ref": "main",
},
expectError: true,
expectedErrMsg: "workflow_id is required for run_workflow action",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, "Workflow run has been queued", response["message"])
assert.Contains(t, response, "workflow_type")
})
}
}
func Test_RunWorkflow_WithFilename(t *testing.T) {
// Test the unified ActionsRunTrigger tool with filenames
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful workflow run by filename",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeRunWorkflow.String(),
"owner": "owner",
"repo": "repo",
"workflow_id": "ci.yml",
"ref": "main",
},
expectError: false,
},
{
name: "successful workflow run by numeric ID as string",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeRunWorkflow.String(),
"owner": "owner",
"repo": "repo",
"workflow_id": "12345",
"ref": "main",
},
expectError: false,
},
{
name: "missing required parameter workflow_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeRunWorkflow.String(),
"owner": "owner",
"repo": "repo",
"ref": "main",
},
expectError: true,
expectedErrMsg: "workflow_id is required for run_workflow action",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, "Workflow run has been queued", response["message"])
assert.Contains(t, response, "workflow_type")
})
}
}
func Test_ActionsRunTrigger(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := ActionsRunTrigger(stubGetClientFn(mockClient), translations.NullTranslationHelper)
assert.Equal(t, "actions_run_trigger", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "action")
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "run_id")
assert.Contains(t, tool.InputSchema.Properties, "workflow_id")
assert.Contains(t, tool.InputSchema.Properties, "ref")
assert.Contains(t, tool.InputSchema.Properties, "inputs")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"action", "owner", "repo"})
}
func Test_CancelWorkflowRun(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful workflow run cancellation",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
Method: "POST",
},
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusAccepted)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeCancelWorkflowRun.String(),
"owner": "owner",
"repo": "repo",
"run_id": float64(12345),
},
expectError: false,
},
{
name: "conflict when cancelling a workflow run",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
Method: "POST",
},
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusConflict)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeCancelWorkflowRun.String(),
"owner": "owner",
"repo": "repo",
"run_id": float64(12345),
},
expectError: true,
expectedErrMsg: "failed to cancel workflow run",
},
{
name: "missing required parameter run_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeCancelWorkflowRun.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: run_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, textContent.Text, tc.expectedErrMsg)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, "Workflow run has been cancelled", response["message"])
assert.Equal(t, float64(12345), response["run_id"])
})
}
}
func Test_ListWorkflowRunArtifacts(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful artifacts listing",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
artifacts := &github.ArtifactList{
TotalCount: github.Ptr(int64(2)),
Artifacts: []*github.Artifact{
{
ID: github.Ptr(int64(1)),
NodeID: github.Ptr("A_1"),
Name: github.Ptr("build-artifacts"),
SizeInBytes: github.Ptr(int64(1024)),
URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1"),
ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/1/zip"),
Expired: github.Ptr(false),
CreatedAt: &github.Timestamp{},
UpdatedAt: &github.Timestamp{},
ExpiresAt: &github.Timestamp{},
WorkflowRun: &github.ArtifactWorkflowRun{
ID: github.Ptr(int64(12345)),
RepositoryID: github.Ptr(int64(1)),
HeadRepositoryID: github.Ptr(int64(1)),
HeadBranch: github.Ptr("main"),
HeadSHA: github.Ptr("abc123"),
},
},
{
ID: github.Ptr(int64(2)),
NodeID: github.Ptr("A_2"),
Name: github.Ptr("test-results"),
SizeInBytes: github.Ptr(int64(512)),
URL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2"),
ArchiveDownloadURL: github.Ptr("https://api.github.com/repos/owner/repo/actions/artifacts/2/zip"),
Expired: github.Ptr(false),
CreatedAt: &github.Timestamp{},
UpdatedAt: &github.Timestamp{},
ExpiresAt: &github.Timestamp{},
WorkflowRun: &github.ArtifactWorkflowRun{
ID: github.Ptr(int64(12345)),
RepositoryID: github.Ptr(int64(1)),
HeadRepositoryID: github.Ptr(int64(1)),
HeadBranch: github.Ptr("main"),
HeadSHA: github.Ptr("abc123"),
},
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(artifacts)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowArtifacts.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "12345",
},
expectError: false,
},
{
name: "missing required parameter resource_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowArtifacts.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: fmt.Sprintf("missing required parameter for action %s: resource_id", actionsActionTypeListWorkflowArtifacts.String()),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response github.ArtifactList
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response.TotalCount)
assert.Greater(t, *response.TotalCount, int64(0))
assert.NotEmpty(t, response.Artifacts)
})
}
}
func Test_DownloadWorkflowRunArtifact(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful artifact download URL",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/owner/repo/actions/artifacts/123/zip",
Method: "GET",
},
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
// GitHub returns a 302 redirect to the download URL
w.Header().Set("Location", "https://api.github.com/repos/owner/repo/actions/artifacts/123/download")
w.WriteHeader(http.StatusFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeDownloadWorkflowArtifact.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "123",
},
expectError: false,
},
{
name: "missing required parameter resource_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeDownloadWorkflowArtifact.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: resource_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Contains(t, response, "download_url")
assert.Contains(t, response, "message")
assert.Equal(t, "Artifact is available for download", response["message"])
assert.Equal(t, float64(123), response["artifact_id"])
})
}
}
func Test_DeleteWorkflowRunLogs(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful logs deletion",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.DeleteReposActionsRunsLogsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNoContent)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeDeleteWorkflowRunLogs.String(),
"owner": "owner",
"repo": "repo",
"run_id": float64(12345),
},
expectError: false,
},
{
name: "missing required parameter run_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeDeleteWorkflowRunLogs.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: run_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsRunTrigger(stubGetClientFn(client), translations.NullTranslationHelper)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, "Workflow run logs have been deleted", response["message"])
assert.Equal(t, float64(12345), response["run_id"])
})
}
}
func Test_GetWorkflowRunUsage(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "successful workflow run usage",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsTimingByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
usage := &github.WorkflowRunUsage{
Billable: &github.WorkflowRunBillMap{
"UBUNTU": &github.WorkflowRunBill{
TotalMS: github.Ptr(int64(120000)),
Jobs: github.Ptr(2),
JobRuns: []*github.WorkflowRunJobRun{
{
JobID: github.Ptr(1),
DurationMS: github.Ptr(int64(60000)),
},
{
JobID: github.Ptr(2),
DurationMS: github.Ptr(int64(60000)),
},
},
},
},
RunDurationMS: github.Ptr(int64(120000)),
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(usage)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRunUsage.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "12345",
},
expectError: false,
},
{
name: "missing required parameter resource_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRunUsage.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: resource_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
// Unmarshal and verify the result
var response github.WorkflowRunUsage
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response.RunDurationMS)
assert.NotNil(t, response.Billable)
})
}
}
func Test_GetJobLogs(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := ActionsGet(stubGetClientFn(mockClient), translations.NullTranslationHelper, 5000)
assert.Equal(t, "actions_get", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "action")
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "resource_id")
assert.Contains(t, tool.InputSchema.Properties, "job_id")
assert.Contains(t, tool.InputSchema.Properties, "failed_only")
assert.Contains(t, tool.InputSchema.Properties, "return_content")
assert.Contains(t, tool.InputSchema.Properties, "tail_lines")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"action", "owner", "repo"})
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
checkResponse func(t *testing.T, response map[string]any)
}{
{
name: "successful single job logs with URL",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Location", "https://github.com/logs/job/123")
w.WriteHeader(http.StatusFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"job_id": float64(123),
},
expectError: false,
checkResponse: func(t *testing.T, response map[string]any) {
assert.Equal(t, float64(123), response["job_id"])
assert.Contains(t, response, "logs_url")
assert.Equal(t, "Job logs are available for download", response["message"])
assert.Contains(t, response, "note")
},
},
{
name: "successful failed jobs logs",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsJobsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jobs := &github.Jobs{
TotalCount: github.Ptr(3),
Jobs: []*github.WorkflowJob{
{
ID: github.Ptr(int64(1)),
Name: github.Ptr("test-job-1"),
Conclusion: github.Ptr("success"),
},
{
ID: github.Ptr(int64(2)),
Name: github.Ptr("test-job-2"),
Conclusion: github.Ptr("failure"),
},
{
ID: github.Ptr(int64(3)),
Name: github.Ptr("test-job-3"),
Conclusion: github.Ptr("failure"),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(jobs)
}),
),
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "https://github.com/logs/job/"+r.URL.Path[len(r.URL.Path)-1:])
w.WriteHeader(http.StatusFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "456",
"failed_only": true,
},
expectError: false,
checkResponse: func(t *testing.T, response map[string]any) {
assert.Equal(t, float64(456), response["run_id"])
assert.Equal(t, float64(3), response["total_jobs"])
assert.Equal(t, float64(2), response["failed_jobs"])
assert.Contains(t, response, "logs")
assert.Equal(t, "Retrieved logs for 2 failed jobs", response["message"])
logs, ok := response["logs"].([]interface{})
assert.True(t, ok)
assert.Len(t, logs, 2)
},
},
{
name: "no failed jobs found",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsJobsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jobs := &github.Jobs{
TotalCount: github.Ptr(2),
Jobs: []*github.WorkflowJob{
{
ID: github.Ptr(int64(1)),
Name: github.Ptr("test-job-1"),
Conclusion: github.Ptr("success"),
},
{
ID: github.Ptr(int64(2)),
Name: github.Ptr("test-job-2"),
Conclusion: github.Ptr("success"),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(jobs)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "456",
"failed_only": true,
},
expectError: false,
checkResponse: func(t *testing.T, response map[string]any) {
assert.Equal(t, "No failed jobs found in this workflow run", response["message"])
assert.Equal(t, float64(456), response["run_id"])
assert.Equal(t, float64(2), response["total_jobs"])
assert.Equal(t, float64(0), response["failed_jobs"])
},
},
{
name: "missing job_id when not using failed_only",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "job_id is required when failed_only is false",
},
{
name: "missing run_id when using failed_only",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"failed_only": true,
},
expectError: true,
expectedErrMsg: "resource_id is required when failed_only is true",
},
{
name: "missing required parameter owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"repo": "repo",
"job_id": float64(123),
},
expectError: true,
expectedErrMsg: "missing required parameter: owner",
},
{
name: "missing required parameter repo",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"job_id": float64(123),
},
expectError: true,
expectedErrMsg: "missing required parameter: repo",
},
{
name: "API error when getting single job logs",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Not Found",
})
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"job_id": float64(999),
},
expectError: true,
},
{
name: "API error when listing workflow jobs for failed_only",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsJobsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
_ = json.NewEncoder(w).Encode(map[string]string{
"message": "Not Found",
})
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "999",
"failed_only": true,
},
expectError: true,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectError {
// For API errors, just verify we got an error
assert.True(t, result.IsError)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
if tc.checkResponse != nil {
tc.checkResponse(t, response)
}
})
}
}
func Test_GetJobLogs_WithContentReturn(t *testing.T) {
// Test the return_content functionality with a mock HTTP server
logContent := "2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully"
// Create a test server to serve log content
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(logContent))
}))
defer testServer.Close()
mockedClient := mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Location", testServer.URL)
w.WriteHeader(http.StatusFound)
}),
),
)
client := github.NewClient(mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"job_id": float64(123),
"return_content": true,
})
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, float64(123), response["job_id"])
assert.Equal(t, logContent, response["logs_content"])
assert.Equal(t, "Job logs content retrieved successfully", response["message"])
assert.NotContains(t, response, "logs_url") // Should not have URL when returning content
}
func Test_GetJobLogs_WithContentReturnAndTailLines(t *testing.T) {
// Test the return_content functionality with a mock HTTP server
logContent := "2023-01-01T10:00:00.000Z Starting job...\n2023-01-01T10:00:01.000Z Running tests...\n2023-01-01T10:00:02.000Z Job completed successfully"
expectedLogContent := "2023-01-01T10:00:02.000Z Job completed successfully"
// Create a test server to serve log content
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(logContent))
}))
defer testServer.Close()
mockedClient := mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Location", testServer.URL)
w.WriteHeader(http.StatusFound)
}),
),
)
client := github.NewClient(mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"job_id": float64(123),
"return_content": true,
"tail_lines": float64(1), // Requesting last 1 line
})
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, float64(123), response["job_id"])
assert.Equal(t, float64(3), response["original_length"])
assert.Equal(t, expectedLogContent, response["logs_content"])
assert.Equal(t, "Job logs content retrieved successfully", response["message"])
assert.NotContains(t, response, "logs_url") // Should not have URL when returning content
}
func Test_GetJobLogs_WithContentReturnAndLargeTailLines(t *testing.T) {
logContent := "Line 1\nLine 2\nLine 3"
expectedLogContent := "Line 1\nLine 2\nLine 3"
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(logContent))
}))
defer testServer.Close()
mockedClient := mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsLogsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Location", testServer.URL)
w.WriteHeader(http.StatusFound)
}),
),
)
client := github.NewClient(mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
request := createMCPRequest(map[string]any{
"action": actionsActionTypeGetWorkflowJobLogs.String(),
"owner": "owner",
"repo": "repo",
"job_id": float64(123),
"return_content": true,
"tail_lines": float64(100),
})
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.False(t, result.IsError)
textContent := getTextResult(t, result)
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, float64(123), response["job_id"])
assert.Equal(t, float64(3), response["original_length"])
assert.Equal(t, expectedLogContent, response["logs_content"])
assert.Equal(t, "Job logs content retrieved successfully", response["message"])
assert.NotContains(t, response, "logs_url")
}
func Test_MemoryUsage_SlidingWindow_vs_NoWindow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping memory profiling test in short mode")
}
const logLines = 100000
const bufferSize = 5000
largeLogContent := strings.Repeat("log line with some content\n", logLines-1) + "final log line"
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(largeLogContent))
}))
defer testServer.Close()
os.Setenv("GITHUB_MCP_PROFILING_ENABLED", "true")
defer os.Unsetenv("GITHUB_MCP_PROFILING_ENABLED")
profiler.InitFromEnv(nil)
ctx := context.Background()
debug.SetGCPercent(-1)
defer debug.SetGCPercent(100)
for i := 0; i < 3; i++ {
runtime.GC()
}
var baselineStats runtime.MemStats
runtime.ReadMemStats(&baselineStats)
profile1, err1 := profiler.ProfileFuncWithMetrics(ctx, "sliding_window", func() (int, int64, error) {
resp1, err := http.Get(testServer.URL)
if err != nil {
return 0, 0, err
}
defer resp1.Body.Close() //nolint:bodyclose
content, totalLines, _, err := buffer.ProcessResponseAsRingBufferToEnd(resp1, bufferSize) //nolint:bodyclose
return totalLines, int64(len(content)), err
})
require.NoError(t, err1)
for i := 0; i < 3; i++ {
runtime.GC()
}
profile2, err2 := profiler.ProfileFuncWithMetrics(ctx, "no_window", func() (int, int64, error) {
resp2, err := http.Get(testServer.URL)
if err != nil {
return 0, 0, err
}
defer resp2.Body.Close() //nolint:bodyclose
allContent, err := io.ReadAll(resp2.Body)
if err != nil {
return 0, 0, err
}
allLines := strings.Split(string(allContent), "\n")
var nonEmptyLines []string
for _, line := range allLines {
if line != "" {
nonEmptyLines = append(nonEmptyLines, line)
}
}
totalLines := len(nonEmptyLines)
var resultLines []string
if totalLines > bufferSize {
resultLines = nonEmptyLines[totalLines-bufferSize:]
} else {
resultLines = nonEmptyLines
}
result := strings.Join(resultLines, "\n")
return totalLines, int64(len(result)), nil
})
require.NoError(t, err2)
assert.Greater(t, profile2.MemoryDelta, profile1.MemoryDelta,
"Sliding window should use less memory than reading all into memory")
assert.Equal(t, profile1.LinesCount, profile2.LinesCount,
"Both approaches should count the same number of input lines")
assert.InDelta(t, profile1.BytesCount, profile2.BytesCount, 100,
"Both approaches should produce similar output sizes (within 100 bytes)")
memoryReduction := float64(profile2.MemoryDelta-profile1.MemoryDelta) / float64(profile2.MemoryDelta) * 100
t.Logf("Memory reduction: %.1f%% (%.2f MB vs %.2f MB)",
memoryReduction,
float64(profile2.MemoryDelta)/1024/1024,
float64(profile1.MemoryDelta)/1024/1024)
t.Logf("Baseline: %d bytes", baselineStats.Alloc)
t.Logf("Sliding window: %s", profile1.String())
t.Logf("No window: %s", profile2.String())
}
func Test_ActionsGet(t *testing.T) {
// Verify tool definition once
mockClient := github.NewClient(nil)
tool, _ := ActionsGet(stubGetClientFn(mockClient), translations.NullTranslationHelper, 5000)
assert.Equal(t, "actions_get", tool.Name)
assert.NotEmpty(t, tool.Description)
assert.Contains(t, tool.InputSchema.Properties, "action")
assert.Contains(t, tool.InputSchema.Properties, "owner")
assert.Contains(t, tool.InputSchema.Properties, "repo")
assert.Contains(t, tool.InputSchema.Properties, "resource_id")
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"action", "owner", "repo"})
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
}{
{
name: "missing required parameter action",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"owner": "owner",
"repo": "repo",
"resource_id": "123",
},
expectError: true,
expectedErrMsg: "missing required parameter: action",
},
{
name: "missing required parameter owner",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflow.String(),
"repo": "repo",
"resource_id": "123",
},
expectError: true,
expectedErrMsg: "missing required parameter: owner",
},
{
name: "missing required parameter repo",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflow.String(),
"owner": "owner",
"resource_id": "123",
},
expectError: true,
expectedErrMsg: "missing required parameter: repo",
},
{
name: "missing required parameter resource_id",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflow.String(),
"owner": "owner",
"repo": "repo",
},
expectError: true,
expectedErrMsg: "missing required parameter: resource_id",
},
{
name: "unknown resource",
mockedClient: mock.NewMockedHTTPClient(),
requestArgs: map[string]any{
"action": "random",
"owner": "owner",
"repo": "repo",
"resource_id": "123",
},
expectError: true,
expectedErrMsg: "unknown action: random",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Equal(t, tc.expectedErrMsg, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_GetWorkflow(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
workflow := &github.Workflow{
ID: github.Ptr(int64(1)),
NodeID: github.Ptr("W_1"),
Name: github.Ptr("CI"),
Path: github.Ptr(".github/workflows/test.yaml"),
State: github.Ptr("active"),
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(workflow)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflow.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
},
expectError: false,
},
{
name: "missing workflow read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflow.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "2",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to get workflow: GET .*/repos/owner/repo/actions/workflows/2: 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
// Unmarshal and verify the result
var response github.Workflow
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.NotNil(t, response.ID)
assert.NotNil(t, response.Name)
assert.NotNil(t, response.Path)
})
}
}
func Test_ActionsResourceRead_GetWorkflowRun(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow run read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
run := &github.WorkflowRun{
ID: github.Ptr(int64(12345)),
RunNumber: github.Ptr(int(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("success"),
WorkflowID: github.Ptr(int64(1)),
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(run)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRun.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "12345",
},
expectError: false,
},
{
name: "missing workflow run read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRun.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to get workflow run: GET .*/repos/owner/repo/actions/runs/99999: 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
// Unmarshal and verify the result
var response map[string]any
err = json.Unmarshal([]byte(textContent.Text), &response)
require.NoError(t, err)
assert.Equal(t, float64(12345), response["id"])
assert.Equal(t, float64(1), response["run_number"])
assert.Equal(t, "completed", response["status"])
assert.Equal(t, "success", response["conclusion"])
assert.Equal(t, float64(1), response["workflow_id"])
})
}
}
func Test_ActionsResourceRead_ListWorkflowRuns(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow runs read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
runs := &github.WorkflowRuns{
TotalCount: github.Ptr(2),
WorkflowRuns: []*github.WorkflowRun{
{
ID: github.Ptr(int64(12345)),
RunNumber: github.Ptr(int(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("success"),
WorkflowID: github.Ptr(int64(1)),
},
{
ID: github.Ptr(int64(12346)),
RunNumber: github.Ptr(int(2)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("failure"),
WorkflowID: github.Ptr(int64(1)),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(runs)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowRuns.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
},
expectError: false,
},
{
name: "successful workflow runs read with filters",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify query parameters
query := r.URL.Query()
assert.Equal(t, "omgitsads", query.Get("actor"))
assert.Equal(t, "completed", query.Get("status"))
runs := &github.WorkflowRuns{
TotalCount: github.Ptr(1),
WorkflowRuns: []*github.WorkflowRun{
{
ID: github.Ptr(int64(12345)),
RunNumber: github.Ptr(int(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("success"),
WorkflowID: github.Ptr(int64(1)),
Actor: &github.User{
Login: github.Ptr("omgitsads"),
},
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(runs)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowRuns.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
"workflow_runs_filter": map[string]any{
"actor": "omgitsads",
"status": "completed",
},
},
expectError: false,
},
{
name: "missing workflow runs read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowRuns.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to list workflow runs: GET .*/repos/owner/repo/actions/workflows/99999/runs.* 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_GetWorkflowJob(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow job read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
job := &github.WorkflowJob{
ID: github.Ptr(int64(12345)),
RunID: github.Ptr(int64(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("success"),
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(job)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJob.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "12345",
},
expectError: false,
},
{
name: "missing workflow job read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsJobsByOwnerByRepoByJobId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowJob.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to get workflow job: GET .*/repos/owner/repo/actions/jobs/99999: 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_ListWorkflowJobs(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow jobs read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsJobsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
jobs := &github.Jobs{
TotalCount: github.Ptr(2),
Jobs: []*github.WorkflowJob{
{
ID: github.Ptr(int64(12345)),
RunID: github.Ptr(int64(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("success"),
},
{
ID: github.Ptr(int64(12346)),
RunID: github.Ptr(int64(1)),
Status: github.Ptr("completed"),
Conclusion: github.Ptr("failure"),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(jobs)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowJobs.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
},
expectError: false,
},
{
name: "missing workflow runs read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsJobsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowJobs.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to list workflow jobs: GET .*/repos/owner/repo/actions/runs/99999/jobs.* 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_DownloadWorkflowArtifact(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow artifact download",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsArtifactsByOwnerByRepoByArtifactIdByArchiveFormat,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusFound)
w.Header().Set("Location", "https://github.com/artifact/download/url")
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeDownloadWorkflowArtifact.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "12345",
},
expectError: false,
},
{
name: "missing workflow artifact download",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsArtifactsByOwnerByRepoByArtifactId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeDownloadWorkflowArtifact.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsg: "failed to get artifact download URL: unexpected status code: 404 Not Found",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_ListWorkflowArtifacts(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow artifacts read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
artifacts := &github.ArtifactList{
TotalCount: github.Ptr(int64(2)),
Artifacts: []*github.Artifact{
{
ID: github.Ptr(int64(12345)),
Name: github.Ptr("artifact-1"),
},
{
ID: github.Ptr(int64(12346)),
Name: github.Ptr("artifact-2"),
},
},
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(artifacts)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowArtifacts.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
},
expectError: false,
},
{
name: "missing workflow artifacts read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsArtifactsByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeListWorkflowArtifacts.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to list workflow run artifacts: GET .*/repos/owner/repo/actions/runs/99999/artifacts.* 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsList(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}
func Test_ActionsResourceRead_GetWorkflowUsage(t *testing.T) {
tests := []struct {
name string
mockedClient *http.Client
requestArgs map[string]any
expectError bool
expectedErrMsg string
expectedErrMsgRegexp *regexp.Regexp
}{
{
name: "successful workflow usage read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsTimingByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
workflowUsage := &github.WorkflowRunUsage{
Billable: &github.WorkflowRunBillMap{
"UBUNTU": &github.WorkflowRunBill{
TotalMS: github.Ptr(int64(60000)),
Jobs: github.Ptr(1),
JobRuns: []*github.WorkflowRunJobRun{
{
JobID: github.Ptr(1),
DurationMS: github.Ptr(int64(600)),
},
},
},
},
RunDurationMS: github.Ptr(int64(60000)),
}
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(workflowUsage)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRunUsage.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "1",
},
expectError: false,
},
{
name: "missing workflow usage read",
mockedClient: mock.NewMockedHTTPClient(
mock.WithRequestMatchHandler(
mock.GetReposActionsRunsTimingByOwnerByRepoByRunId,
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotFound)
}),
),
),
requestArgs: map[string]any{
"action": actionsActionTypeGetWorkflowRunUsage.String(),
"owner": "owner",
"repo": "repo",
"resource_id": "99999",
},
expectError: true,
expectedErrMsgRegexp: regexp.MustCompile(`^failed to get workflow run usage: GET .*/repos/owner/repo/actions/runs/99999/timing.* 404.*$`),
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// Setup client with mock
client := github.NewClient(tc.mockedClient)
_, handler := ActionsGet(stubGetClientFn(client), translations.NullTranslationHelper, 5000)
// Create call request
request := createMCPRequest(tc.requestArgs)
// Call handler
result, err := handler(context.Background(), request)
require.NoError(t, err)
require.Equal(t, tc.expectError, result.IsError)
// Parse the result and get the text content if no error
textContent := getTextResult(t, result)
if tc.expectedErrMsg != "" {
assert.Contains(t, tc.expectedErrMsg, textContent.Text)
return
}
if tc.expectedErrMsgRegexp != nil {
assert.Regexp(t, tc.expectedErrMsgRegexp, textContent.Text)
return
}
})
}
}