package github import ( "context" "encoding/json" "net/http" "testing" "github.com/github/github-mcp-server/internal/toolsnaps" "github.com/github/github-mcp-server/pkg/translations" "github.com/google/go-github/v82/github" "github.com/google/jsonschema-go/jsonschema" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // Tests for consolidated actions tools func Test_ActionsList(t *testing.T) { // Verify tool definition once toolDef := ActionsList(translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) assert.Equal(t, "actions_list", toolDef.Tool.Name) assert.NotEmpty(t, toolDef.Tool.Description) inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, inputSchema.Properties, "method") assert.Contains(t, inputSchema.Properties, "owner") assert.Contains(t, inputSchema.Properties, "repo") assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo"}) } func Test_ActionsList_ListWorkflows(t *testing.T) { toolDef := ActionsList(translations.NullTranslationHelper) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string }{ { name: "successful workflow list", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsWorkflowsByOwnerByRepo: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { workflows := &github.Workflows{ TotalCount: github.Ptr(2), Workflows: []*github.Workflow{ { ID: github.Ptr(int64(1)), Name: github.Ptr("CI"), Path: github.Ptr(".github/workflows/ci.yml"), State: github.Ptr("active"), }, { ID: github.Ptr(int64(2)), Name: github.Ptr("Deploy"), Path: github.Ptr(".github/workflows/deploy.yml"), State: github.Ptr("active"), }, }, } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(workflows) }), }), requestArgs: map[string]any{ "method": "list_workflows", "owner": "owner", "repo": "repo", }, expectError: false, }, { name: "missing required parameter method", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), requestArgs: map[string]any{ "owner": "owner", "repo": "repo", }, expectError: true, expectedErrMsg: "missing required parameter: method", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.Equal(t, tc.expectError, result.IsError) textContent := getTextResult(t, result) if tc.expectedErrMsg != "" { assert.Equal(t, tc.expectedErrMsg, textContent.Text) return } 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) }) } } func Test_ActionsList_ListWorkflowRuns(t *testing.T) { toolDef := ActionsList(translations.NullTranslationHelper) t.Run("successful workflow runs list", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsWorkflowsRunsByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { runs := &github.WorkflowRuns{ TotalCount: github.Ptr(1), WorkflowRuns: []*github.WorkflowRun{ { ID: github.Ptr(int64(123)), Name: github.Ptr("CI"), Status: github.Ptr("completed"), Conclusion: github.Ptr("success"), }, }, } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(runs) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "list_workflow_runs", "owner": "owner", "repo": "repo", "resource_id": "ci.yml", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) var response github.WorkflowRuns err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) assert.NotNil(t, response.TotalCount) }) t.Run("list all workflow runs without resource_id", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsRunsByOwnerByRepo: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { runs := &github.WorkflowRuns{ TotalCount: github.Ptr(2), WorkflowRuns: []*github.WorkflowRun{ { ID: github.Ptr(int64(123)), Name: github.Ptr("CI"), Status: github.Ptr("completed"), Conclusion: github.Ptr("success"), }, { ID: github.Ptr(int64(456)), Name: github.Ptr("Deploy"), Status: github.Ptr("in_progress"), Conclusion: nil, }, }, } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(runs) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "list_workflow_runs", "owner": "owner", "repo": "repo", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) var response github.WorkflowRuns err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) assert.Equal(t, 2, *response.TotalCount) }) } func Test_ActionsGet(t *testing.T) { // Verify tool definition once toolDef := ActionsGet(translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) assert.Equal(t, "actions_get", toolDef.Tool.Name) assert.NotEmpty(t, toolDef.Tool.Description) inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, inputSchema.Properties, "method") assert.Contains(t, inputSchema.Properties, "owner") assert.Contains(t, inputSchema.Properties, "repo") assert.Contains(t, inputSchema.Properties, "resource_id") assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo", "resource_id"}) } func Test_ActionsGet_GetWorkflow(t *testing.T) { toolDef := ActionsGet(translations.NullTranslationHelper) t.Run("successful workflow get", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsWorkflowsByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { workflow := &github.Workflow{ ID: github.Ptr(int64(1)), Name: github.Ptr("CI"), Path: github.Ptr(".github/workflows/ci.yml"), State: github.Ptr("active"), } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(workflow) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "get_workflow", "owner": "owner", "repo": "repo", "resource_id": "ci.yml", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) var response github.Workflow err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) assert.NotNil(t, response.ID) assert.Equal(t, "CI", *response.Name) }) } func Test_ActionsGet_GetWorkflowRun(t *testing.T) { toolDef := ActionsGet(translations.NullTranslationHelper) t.Run("successful workflow run get", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsRunsByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { run := &github.WorkflowRun{ ID: github.Ptr(int64(12345)), Name: github.Ptr("CI"), Status: github.Ptr("completed"), Conclusion: github.Ptr("success"), } w.WriteHeader(http.StatusOK) _ = json.NewEncoder(w).Encode(run) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "get_workflow_run", "owner": "owner", "repo": "repo", "resource_id": "12345", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.False(t, result.IsError) textContent := getTextResult(t, result) var response github.WorkflowRun err = json.Unmarshal([]byte(textContent.Text), &response) require.NoError(t, err) assert.NotNil(t, response.ID) assert.Equal(t, int64(12345), *response.ID) }) } func Test_ActionsRunTrigger(t *testing.T) { // Verify tool definition once toolDef := ActionsRunTrigger(translations.NullTranslationHelper) require.NoError(t, toolsnaps.Test(toolDef.Tool.Name, toolDef.Tool)) assert.Equal(t, "actions_run_trigger", toolDef.Tool.Name) assert.NotEmpty(t, toolDef.Tool.Description) inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, inputSchema.Properties, "method") assert.Contains(t, inputSchema.Properties, "owner") assert.Contains(t, inputSchema.Properties, "repo") assert.Contains(t, inputSchema.Properties, "workflow_id") assert.Contains(t, inputSchema.Properties, "ref") assert.Contains(t, inputSchema.Properties, "run_id") assert.ElementsMatch(t, inputSchema.Required, []string{"method", "owner", "repo"}) } func Test_ActionsRunTrigger_RunWorkflow(t *testing.T) { toolDef := ActionsRunTrigger(translations.NullTranslationHelper) tests := []struct { name string mockedClient *http.Client requestArgs map[string]any expectError bool expectedErrMsg string }{ { name: "successful workflow run", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposActionsWorkflowsDispatchesByOwnerByRepoByWorkflowID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) }), }), requestArgs: map[string]any{ "method": "run_workflow", "owner": "owner", "repo": "repo", "workflow_id": "12345", "ref": "main", }, expectError: false, }, { name: "missing required parameter workflow_id", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), requestArgs: map[string]any{ "method": "run_workflow", "owner": "owner", "repo": "repo", "ref": "main", }, expectError: true, expectedErrMsg: "workflow_id is required for run_workflow action", }, { name: "missing required parameter ref", mockedClient: MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}), requestArgs: map[string]any{ "method": "run_workflow", "owner": "owner", "repo": "repo", "workflow_id": "12345", }, expectError: true, expectedErrMsg: "ref is required for run_workflow action", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { client := github.NewClient(tc.mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(tc.requestArgs) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.Equal(t, tc.expectError, result.IsError) textContent := getTextResult(t, result) if tc.expectedErrMsg != "" { assert.Equal(t, tc.expectedErrMsg, textContent.Text) return } 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"]) }) } } func Test_ActionsRunTrigger_CancelWorkflowRun(t *testing.T) { toolDef := ActionsRunTrigger(translations.NullTranslationHelper) t.Run("successful workflow run cancellation", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposActionsRunsCancelByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusAccepted) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "cancel_workflow_run", "owner": "owner", "repo": "repo", "run_id": float64(12345), }) result, err := handler(ContextWithDeps(context.Background(), deps), &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, "Workflow run has been cancelled", response["message"]) }) t.Run("conflict when cancelling a workflow run", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ PostReposActionsRunsCancelByOwnerByRepoByRunID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusConflict) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "cancel_workflow_run", "owner": "owner", "repo": "repo", "run_id": float64(12345), }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.True(t, result.IsError) textContent := getTextResult(t, result) assert.Contains(t, textContent.Text, "failed to cancel workflow run") }) t.Run("missing run_id for non-run_workflow methods", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{}) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "method": "cancel_workflow_run", "owner": "owner", "repo": "repo", }) result, err := handler(ContextWithDeps(context.Background(), deps), &request) require.NoError(t, err) require.True(t, result.IsError) textContent := getTextResult(t, result) assert.Equal(t, "missing required parameter: run_id", textContent.Text) }) } func Test_ActionsGetJobLogs(t *testing.T) { // Verify tool definition once toolDef := ActionsGetJobLogs(translations.NullTranslationHelper) // Note: consolidated ActionsGetJobLogs has same tool name "get_job_logs" as the individual tool // but with different descriptions. We skip toolsnap validation here since the individual // tool's toolsnap already exists and is tested in Test_GetJobLogs. // The consolidated tool has FeatureFlagEnable set, so only one will be active at a time. assert.Equal(t, "get_job_logs", toolDef.Tool.Name) assert.NotEmpty(t, toolDef.Tool.Description) inputSchema := toolDef.Tool.InputSchema.(*jsonschema.Schema) assert.Contains(t, inputSchema.Properties, "owner") assert.Contains(t, inputSchema.Properties, "repo") assert.Contains(t, inputSchema.Properties, "job_id") assert.Contains(t, inputSchema.Properties, "run_id") assert.Contains(t, inputSchema.Properties, "failed_only") assert.Contains(t, inputSchema.Properties, "return_content") assert.ElementsMatch(t, inputSchema.Required, []string{"owner", "repo"}) } func Test_ActionsGetJobLogs_SingleJob(t *testing.T) { toolDef := ActionsGetJobLogs(translations.NullTranslationHelper) t.Run("successful single job logs with URL", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ GetReposActionsJobsLogsByOwnerByRepoByJobID: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Location", "https://github.com/logs/job/123") w.WriteHeader(http.StatusFound) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, ContentWindowSize: 5000, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "owner": "owner", "repo": "repo", "job_id": float64(123), }) result, err := handler(ContextWithDeps(context.Background(), deps), &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.Contains(t, response, "logs_url") assert.Equal(t, "Job logs are available for download", response["message"]) }) } func Test_ActionsGetJobLogs_FailedJobs(t *testing.T) { toolDef := ActionsGetJobLogs(translations.NullTranslationHelper) t.Run("successful failed jobs logs", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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) }), 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) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, ContentWindowSize: 5000, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "owner": "owner", "repo": "repo", "run_id": float64(456), "failed_only": true, }) result, err := handler(ContextWithDeps(context.Background(), deps), &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(456), response["run_id"]) assert.Contains(t, response, "logs") assert.Contains(t, response["message"], "Retrieved logs for") }) t.Run("no failed jobs found", func(t *testing.T) { mockedClient := MockHTTPClientWithHandlers(map[string]http.HandlerFunc{ 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) }), }) client := github.NewClient(mockedClient) deps := BaseDeps{ Client: client, ContentWindowSize: 5000, } handler := toolDef.Handler(deps) request := createMCPRequest(map[string]any{ "owner": "owner", "repo": "repo", "run_id": float64(456), "failed_only": true, }) result, err := handler(ContextWithDeps(context.Background(), deps), &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, "No failed jobs found in this workflow run", response["message"]) }) }