1147 lines
36 KiB
Go
1147 lines
36 KiB
Go
package github
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/github/github-mcp-server/pkg/translations"
|
|
"github.com/google/go-github/v72/github"
|
|
"github.com/migueleliasweb/go-github-mock/src/mock"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func Test_ListWorkflows(t *testing.T) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := ListWorkflows(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "list_workflows", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "perPage")
|
|
assert.Contains(t, tool.InputSchema.Properties, "page")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter owner",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"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 := ListWorkflows(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 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) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := RunWorkflow(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "run_workflow", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
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{"owner", "repo", "workflow_id", "ref"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"workflow_id": "12345",
|
|
"ref": "main",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter workflow_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"ref": "main",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: workflow_id",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Setup client with mock
|
|
client := github.NewClient(tc.mockedClient)
|
|
_, handler := RunWorkflow(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 RunWorkflow function 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{
|
|
"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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"workflow_id": "12345",
|
|
"ref": "main",
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter workflow_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"ref": "main",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: workflow_id",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Setup client with mock
|
|
client := github.NewClient(tc.mockedClient)
|
|
_, handler := RunWorkflow(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_CancelWorkflowRun(t *testing.T) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := CancelWorkflowRun(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "cancel_workflow_run", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "run_id")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"})
|
|
|
|
tests := []struct {
|
|
name string
|
|
mockedClient *http.Client
|
|
requestArgs map[string]any
|
|
expectError bool
|
|
expectedErrMsg string
|
|
}{
|
|
{
|
|
name: "successful workflow run cancellation",
|
|
mockedClient: mock.NewMockedHTTPClient(
|
|
mock.WithRequestMatch(
|
|
mock.EndpointPattern{
|
|
Pattern: "/repos/owner/repo/actions/runs/12345/cancel",
|
|
Method: "POST",
|
|
},
|
|
"", // Empty response body for 202 Accepted
|
|
),
|
|
),
|
|
requestArgs: map[string]any{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(12345),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter run_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"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 := CancelWorkflowRun(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.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 cancelled", response["message"])
|
|
assert.Equal(t, float64(12345), response["run_id"])
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_ListWorkflowRunArtifacts(t *testing.T) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := ListWorkflowRunArtifacts(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "list_workflow_run_artifacts", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
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, "perPage")
|
|
assert.Contains(t, tool.InputSchema.Properties, "page")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(12345),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter run_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"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 := ListWorkflowRunArtifacts(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 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) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := DownloadWorkflowRunArtifact(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "download_workflow_run_artifact", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "artifact_id")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "artifact_id"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"artifact_id": float64(123),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter artifact_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "missing required parameter: artifact_id",
|
|
},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Setup client with mock
|
|
client := github.NewClient(tc.mockedClient)
|
|
_, handler := DownloadWorkflowRunArtifact(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.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) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := DeleteWorkflowRunLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "delete_workflow_run_logs", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "run_id")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(12345),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter run_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"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 := DeleteWorkflowRunLogs(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) {
|
|
// Verify tool definition once
|
|
mockClient := github.NewClient(nil)
|
|
tool, _ := GetWorkflowRunUsage(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "get_workflow_run_usage", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "run_id")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"owner", "repo", "run_id"})
|
|
|
|
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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(12345),
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "missing required parameter run_id",
|
|
mockedClient: mock.NewMockedHTTPClient(),
|
|
requestArgs: map[string]any{
|
|
"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 := GetWorkflowRunUsage(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 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, _ := GetJobLogs(stubGetClientFn(mockClient), translations.NullTranslationHelper)
|
|
|
|
assert.Equal(t, "get_job_logs", tool.Name)
|
|
assert.NotEmpty(t, tool.Description)
|
|
assert.Contains(t, tool.InputSchema.Properties, "owner")
|
|
assert.Contains(t, tool.InputSchema.Properties, "repo")
|
|
assert.Contains(t, tool.InputSchema.Properties, "job_id")
|
|
assert.Contains(t, tool.InputSchema.Properties, "run_id")
|
|
assert.Contains(t, tool.InputSchema.Properties, "failed_only")
|
|
assert.Contains(t, tool.InputSchema.Properties, "return_content")
|
|
assert.ElementsMatch(t, tool.InputSchema.Required, []string{"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{
|
|
"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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(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{
|
|
"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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"failed_only": true,
|
|
},
|
|
expectError: true,
|
|
expectedErrMsg: "run_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{
|
|
"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{
|
|
"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{
|
|
"owner": "owner",
|
|
"repo": "repo",
|
|
"run_id": float64(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 := GetJobLogs(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.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 := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper)
|
|
|
|
request := createMCPRequest(map[string]any{
|
|
"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 := GetJobLogs(stubGetClientFn(client), translations.NullTranslationHelper)
|
|
|
|
request := createMCPRequest(map[string]any{
|
|
"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(1), 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
|
|
}
|