mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
BREAKING CHANGES: - Remove review mode entirely - now handled via slash commands in agent mode - Remove all deprecated backward compatibility fields (mode, anthropic_model, override_prompt, direct_prompt) - Simplify mode detection: prompt overrides everything, then @claude mentions trigger tag mode, default is agent mode - Remove slash command resolution from GitHub Action - Claude Code handles natively - Remove variable substitution - prompts passed through as-is Architecture changes: - Only two modes now: tag (for @claude mentions) and agent (everything else) - Agent mode is the default for all events including PRs - Users configure behavior via prompts/slash commands (e.g. /review) - GitHub Action is now a thin wrapper that passes prompts to Claude Code - Mode names changed: 'experimental-review' → removed entirely This aligns with the philosophy that the GitHub Action should do minimal work and delegate to Claude Code for all intelligent behavior.
681 lines
21 KiB
TypeScript
681 lines
21 KiB
TypeScript
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
|
|
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
|
|
import * as core from "@actions/core";
|
|
import type { ParsedGitHubContext } from "../src/github/context";
|
|
|
|
describe("prepareMcpConfig", () => {
|
|
let consoleInfoSpy: any;
|
|
let consoleWarningSpy: any;
|
|
let setFailedSpy: any;
|
|
let processExitSpy: any;
|
|
|
|
// Create a mock context for tests
|
|
const mockContext: ParsedGitHubContext = {
|
|
runId: "test-run-id",
|
|
eventName: "issue_comment",
|
|
eventAction: "created",
|
|
repository: {
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
full_name: "test-owner/test-repo",
|
|
},
|
|
actor: "test-actor",
|
|
payload: {} as any,
|
|
entityNumber: 123,
|
|
isPR: false,
|
|
inputs: {
|
|
prompt: "",
|
|
triggerPhrase: "@claude",
|
|
assigneeTrigger: "",
|
|
labelTrigger: "",
|
|
allowedTools: [],
|
|
disallowedTools: [],
|
|
customInstructions: "",
|
|
branchPrefix: "",
|
|
useStickyComment: false,
|
|
additionalPermissions: new Map(),
|
|
useCommitSigning: false,
|
|
},
|
|
};
|
|
|
|
const mockPRContext: ParsedGitHubContext = {
|
|
...mockContext,
|
|
eventName: "pull_request",
|
|
isPR: true,
|
|
entityNumber: 456,
|
|
};
|
|
|
|
const mockContextWithSigning: ParsedGitHubContext = {
|
|
...mockContext,
|
|
inputs: {
|
|
...mockContext.inputs,
|
|
useCommitSigning: true,
|
|
},
|
|
};
|
|
|
|
const mockPRContextWithSigning: ParsedGitHubContext = {
|
|
...mockPRContext,
|
|
inputs: {
|
|
...mockPRContext.inputs,
|
|
useCommitSigning: true,
|
|
},
|
|
};
|
|
|
|
beforeEach(() => {
|
|
consoleInfoSpy = spyOn(core, "info").mockImplementation(() => {});
|
|
consoleWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
|
|
setFailedSpy = spyOn(core, "setFailed").mockImplementation(() => {});
|
|
processExitSpy = spyOn(process, "exit").mockImplementation(() => {
|
|
throw new Error("Process exit");
|
|
});
|
|
|
|
// Set up required environment variables
|
|
if (!process.env.GITHUB_ACTION_PATH) {
|
|
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
|
}
|
|
});
|
|
|
|
afterEach(() => {
|
|
consoleInfoSpy.mockRestore();
|
|
consoleWarningSpy.mockRestore();
|
|
setFailedSpy.mockRestore();
|
|
processExitSpy.mockRestore();
|
|
});
|
|
|
|
test("should return comment server when commit signing is disabled", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: mockContext,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
expect(parsed.mcpServers.github_comment.env.GITHUB_TOKEN).toBe(
|
|
"test-token",
|
|
);
|
|
expect(parsed.mcpServers.github_comment.env.REPO_OWNER).toBe("test-owner");
|
|
expect(parsed.mcpServers.github_comment.env.REPO_NAME).toBe("test-repo");
|
|
});
|
|
|
|
test("should return file ops server when commit signing is enabled", async () => {
|
|
const contextWithSigning = {
|
|
...mockContext,
|
|
inputs: {
|
|
...mockContext.inputs,
|
|
useCommitSigning: true,
|
|
},
|
|
};
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: contextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops.env.GITHUB_TOKEN).toBe(
|
|
"test-token",
|
|
);
|
|
expect(parsed.mcpServers.github_file_ops.env.REPO_OWNER).toBe("test-owner");
|
|
expect(parsed.mcpServers.github_file_ops.env.REPO_NAME).toBe("test-repo");
|
|
expect(parsed.mcpServers.github_file_ops.env.BRANCH_NAME).toBe(
|
|
"test-branch",
|
|
);
|
|
});
|
|
|
|
test("should include github MCP server when mcp__github__ tools are allowed", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [
|
|
"mcp__github__create_issue",
|
|
"mcp__github_file_ops__commit_files",
|
|
],
|
|
context: mockContext,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
|
expect(parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN).toBe(
|
|
"test-token",
|
|
);
|
|
});
|
|
|
|
test("should not include github MCP server when only file_ops tools are allowed", async () => {
|
|
const contextWithSigning = {
|
|
...mockContext,
|
|
inputs: {
|
|
...mockContext.inputs,
|
|
useCommitSigning: true,
|
|
},
|
|
};
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [
|
|
"mcp__github_file_ops__commit_files",
|
|
"mcp__github_file_ops__update_claude_comment",
|
|
],
|
|
context: contextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should include comment server when no GitHub tools are allowed and signing disabled", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: ["Edit", "Read", "Write"],
|
|
context: mockContext,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
});
|
|
|
|
test("should return base config when additional config is empty string", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: "",
|
|
allowedTools: [],
|
|
context: mockContext,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should return base config when additional config is whitespace only", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: " \n\t ",
|
|
allowedTools: [],
|
|
context: mockContext,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_comment).toBeDefined();
|
|
expect(consoleWarningSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test("should merge valid additional config with base config", async () => {
|
|
const additionalConfig = JSON.stringify({
|
|
mcpServers: {
|
|
custom_server: {
|
|
command: "custom-command",
|
|
args: ["arg1", "arg2"],
|
|
env: {
|
|
CUSTOM_ENV: "custom-value",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: additionalConfig,
|
|
allowedTools: [
|
|
"mcp__github__create_issue",
|
|
"mcp__github_file_ops__commit_files",
|
|
],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
"Merging additional MCP server configuration with built-in servers",
|
|
);
|
|
expect(parsed.mcpServers.github).toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
expect(parsed.mcpServers.custom_server).toBeDefined();
|
|
expect(parsed.mcpServers.custom_server.command).toBe("custom-command");
|
|
expect(parsed.mcpServers.custom_server.args).toEqual(["arg1", "arg2"]);
|
|
expect(parsed.mcpServers.custom_server.env.CUSTOM_ENV).toBe("custom-value");
|
|
});
|
|
|
|
test("should override built-in servers when additional config has same server names", async () => {
|
|
const additionalConfig = JSON.stringify({
|
|
mcpServers: {
|
|
github: {
|
|
command: "overridden-command",
|
|
args: ["overridden-arg"],
|
|
env: {
|
|
OVERRIDDEN_ENV: "overridden-value",
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: additionalConfig,
|
|
allowedTools: [
|
|
"mcp__github__create_issue",
|
|
"mcp__github_file_ops__commit_files",
|
|
],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
"Merging additional MCP server configuration with built-in servers",
|
|
);
|
|
expect(parsed.mcpServers.github.command).toBe("overridden-command");
|
|
expect(parsed.mcpServers.github.args).toEqual(["overridden-arg"]);
|
|
expect(parsed.mcpServers.github.env.OVERRIDDEN_ENV).toBe(
|
|
"overridden-value",
|
|
);
|
|
expect(
|
|
parsed.mcpServers.github.env.GITHUB_PERSONAL_ACCESS_TOKEN,
|
|
).toBeUndefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should merge additional root-level properties", async () => {
|
|
const additionalConfig = JSON.stringify({
|
|
customProperty: "custom-value",
|
|
anotherProperty: {
|
|
nested: "value",
|
|
},
|
|
mcpServers: {
|
|
custom_server: {
|
|
command: "custom",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: additionalConfig,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.customProperty).toBe("custom-value");
|
|
expect(parsed.anotherProperty).toEqual({ nested: "value" });
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.custom_server).toBeDefined();
|
|
});
|
|
|
|
test("should handle invalid JSON gracefully", async () => {
|
|
const invalidJson = "{ invalid json }";
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: invalidJson,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
|
);
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should handle non-object JSON values", async () => {
|
|
const nonObjectJson = JSON.stringify("string value");
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: nonObjectJson,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
|
);
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
|
);
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should handle null JSON value", async () => {
|
|
const nullJson = JSON.stringify(null);
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: nullJson,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("Failed to parse additional MCP config:"),
|
|
);
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining("MCP config must be a valid JSON object"),
|
|
);
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should handle array JSON value", async () => {
|
|
const arrayJson = JSON.stringify([1, 2, 3]);
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: arrayJson,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
// Arrays are objects in JavaScript, so they pass the object check
|
|
// But they'll fail when trying to spread or access mcpServers property
|
|
expect(consoleInfoSpy).toHaveBeenCalledWith(
|
|
"Merging additional MCP server configuration with built-in servers",
|
|
);
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
// The array will be spread into the config (0: 1, 1: 2, 2: 3)
|
|
expect(parsed[0]).toBe(1);
|
|
expect(parsed[1]).toBe(2);
|
|
expect(parsed[2]).toBe(3);
|
|
});
|
|
|
|
test("should merge complex nested configurations", async () => {
|
|
const additionalConfig = JSON.stringify({
|
|
mcpServers: {
|
|
server1: {
|
|
command: "cmd1",
|
|
env: { KEY1: "value1" },
|
|
},
|
|
server2: {
|
|
command: "cmd2",
|
|
env: { KEY2: "value2" },
|
|
},
|
|
github_file_ops: {
|
|
command: "overridden",
|
|
env: { CUSTOM: "value" },
|
|
},
|
|
},
|
|
otherConfig: {
|
|
nested: {
|
|
deeply: "value",
|
|
},
|
|
},
|
|
});
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
additionalMcpConfig: additionalConfig,
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.server1).toBeDefined();
|
|
expect(parsed.mcpServers.server2).toBeDefined();
|
|
expect(parsed.mcpServers.github).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops.command).toBe("overridden");
|
|
expect(parsed.mcpServers.github_file_ops.env.CUSTOM).toBe("value");
|
|
expect(parsed.otherConfig.nested.deeply).toBe("value");
|
|
});
|
|
|
|
test("should preserve GITHUB_ACTION_PATH in file_ops server args", async () => {
|
|
const oldEnv = process.env.GITHUB_ACTION_PATH;
|
|
process.env.GITHUB_ACTION_PATH = "/test/action/path";
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_file_ops.args[1]).toBe(
|
|
"/test/action/path/src/mcp/github-file-ops-server.ts",
|
|
);
|
|
|
|
process.env.GITHUB_ACTION_PATH = oldEnv;
|
|
});
|
|
|
|
test("should use process.cwd() when GITHUB_WORKSPACE is not set", async () => {
|
|
const oldEnv = process.env.GITHUB_WORKSPACE;
|
|
delete process.env.GITHUB_WORKSPACE;
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_file_ops.env.REPO_DIR).toBe(process.cwd());
|
|
|
|
process.env.GITHUB_WORKSPACE = oldEnv;
|
|
});
|
|
|
|
test("should include github_ci server when context.isPR is true and actions:read permission is granted", async () => {
|
|
const oldEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
|
|
|
|
const contextWithPermissions = {
|
|
...mockPRContext,
|
|
inputs: {
|
|
...mockPRContext.inputs,
|
|
additionalPermissions: new Map([["actions", "read"]]),
|
|
useCommitSigning: true,
|
|
},
|
|
};
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: contextWithPermissions,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_ci).toBeDefined();
|
|
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
|
expect(parsed.mcpServers.github_ci.env.PR_NUMBER).toBe("456");
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = oldEnv;
|
|
});
|
|
|
|
test("should not include github_ci server when context.isPR is false", async () => {
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: mockContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
});
|
|
|
|
test("should not include github_ci server when actions:read permission is not granted", async () => {
|
|
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: mockPRContextWithSigning,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_ci).not.toBeDefined();
|
|
expect(parsed.mcpServers.github_file_ops).toBeDefined();
|
|
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
|
});
|
|
|
|
test("should parse additional_permissions with multiple lines correctly", async () => {
|
|
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
|
|
|
|
const contextWithPermissions = {
|
|
...mockPRContext,
|
|
inputs: {
|
|
...mockPRContext.inputs,
|
|
additionalPermissions: new Map([
|
|
["actions", "read"],
|
|
["future", "permission"],
|
|
]),
|
|
},
|
|
};
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: contextWithPermissions,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_ci).toBeDefined();
|
|
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
|
|
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
|
});
|
|
|
|
test("should warn when actions:read is requested but token lacks permission", async () => {
|
|
const oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = "invalid-token";
|
|
|
|
const contextWithPermissions = {
|
|
...mockPRContext,
|
|
inputs: {
|
|
...mockPRContext.inputs,
|
|
additionalPermissions: new Map([["actions", "read"]]),
|
|
},
|
|
};
|
|
|
|
const result = await prepareMcpConfig({
|
|
githubToken: "test-token",
|
|
owner: "test-owner",
|
|
repo: "test-repo",
|
|
branch: "test-branch",
|
|
baseBranch: "main",
|
|
allowedTools: [],
|
|
context: contextWithPermissions,
|
|
});
|
|
|
|
const parsed = JSON.parse(result);
|
|
expect(parsed.mcpServers.github_ci).toBeDefined();
|
|
expect(consoleWarningSpy).toHaveBeenCalledWith(
|
|
expect.stringContaining(
|
|
"The github_ci MCP server requires 'actions: read' permission",
|
|
),
|
|
);
|
|
|
|
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
|
|
});
|
|
});
|