diff --git a/test/branch-cleanup.test.ts b/test/branch-cleanup.test.ts
index eef7fad..99730a3 100644
--- a/test/branch-cleanup.test.ts
+++ b/test/branch-cleanup.test.ts
@@ -1,50 +1,51 @@
import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
import { checkAndDeleteEmptyBranch } from "../src/github/operations/branch-cleanup";
-import type { Octokits } from "../src/github/api/client";
+import type { GitHubClient } from "../src/github/api/client";
import { GITEA_SERVER_URL } from "../src/github/api/config";
describe("checkAndDeleteEmptyBranch", () => {
let consoleLogSpy: any;
let consoleErrorSpy: any;
+ const originalEnv = { ...process.env };
beforeEach(() => {
- // Spy on console methods
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
+ delete process.env.GITEA_API_URL; // ensure GitHub mode for predictable behaviour
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
+ process.env = { ...originalEnv };
});
- const createMockOctokit = (
- compareResponse?: any,
- deleteRefError?: Error,
- ): Octokits => {
+ const createMockClient = (
+ options: { branchSha?: string; baseSha?: string; error?: Error } = {},
+ ): GitHubClient => {
+ const { branchSha = "branch-sha", baseSha = "base-sha", error } = options;
return {
- rest: {
- repos: {
- compareCommitsWithBasehead: async () => ({
- data: compareResponse || { total_commits: 0 },
- }),
- },
- git: {
- deleteRef: async () => {
- if (deleteRefError) {
- throw deleteRefError;
- }
- return { data: {} };
- },
+ api: {
+ getBranch: async (_owner: string, _repo: string, branch: string) => {
+ if (error) {
+ throw error;
+ }
+ return {
+ data: {
+ commit: {
+ sha: branch.includes("claude/") ? branchSha : baseSha,
+ },
+ },
+ };
},
},
- } as any as Octokits;
+ } as unknown as GitHubClient;
};
- test("should return no branch link and not delete when branch is undefined", async () => {
- const mockOctokit = createMockOctokit();
+ test("returns defaults when no claude branch provided", async () => {
+ const client = createMockClient();
const result = await checkAndDeleteEmptyBranch(
- mockOctokit,
+ client,
"owner",
"repo",
undefined,
@@ -56,94 +57,65 @@ describe("checkAndDeleteEmptyBranch", () => {
expect(consoleLogSpy).not.toHaveBeenCalled();
});
- test("should delete branch and return no link when branch has no commits", async () => {
- const mockOctokit = createMockOctokit({ total_commits: 0 });
+ test("marks branch for deletion when SHAs match", async () => {
+ const client = createMockClient({ branchSha: "same", baseSha: "same" });
const result = await checkAndDeleteEmptyBranch(
- mockOctokit,
+ client,
"owner",
"repo",
- "claude/issue-123-20240101_123456",
+ "claude/issue-123",
"main",
);
expect(result.shouldDeleteBranch).toBe(true);
expect(result.branchLink).toBe("");
expect(consoleLogSpy).toHaveBeenCalledWith(
- "Branch claude/issue-123-20240101_123456 has no commits from Claude, will delete it",
+ "Branch claude/issue-123 has same SHA as base, marking for deletion",
);
expect(consoleLogSpy).toHaveBeenCalledWith(
- "✅ Deleted empty branch: claude/issue-123-20240101_123456",
+ "Skipping branch deletion - not reliably supported across all Git platforms: claude/issue-123",
);
});
- test("should not delete branch and return link when branch has commits", async () => {
- const mockOctokit = createMockOctokit({ total_commits: 3 });
+ test("returns branch link when branch has commits", async () => {
+ const client = createMockClient({ branchSha: "feature", baseSha: "main" });
const result = await checkAndDeleteEmptyBranch(
- mockOctokit,
+ client,
"owner",
"repo",
- "claude/issue-123-20240101_123456",
+ "claude/issue-123",
"main",
);
expect(result.shouldDeleteBranch).toBe(false);
expect(result.branchLink).toBe(
- `\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123-20240101_123456)`,
+ `\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123)`,
);
- expect(consoleLogSpy).not.toHaveBeenCalledWith(
- expect.stringContaining("has no commits"),
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ "Branch claude/issue-123 appears to have commits (different SHA from base)",
);
});
- test("should handle branch comparison errors gracefully", async () => {
- const mockOctokit = {
- rest: {
- repos: {
- compareCommitsWithBasehead: async () => {
- throw new Error("API error");
- },
- },
- git: {
- deleteRef: async () => ({ data: {} }),
- },
- },
- } as any as Octokits;
-
+ test("falls back to branch link when API call fails", async () => {
+ const client = createMockClient({ error: Object.assign(new Error("boom"), { status: 500 }) });
const result = await checkAndDeleteEmptyBranch(
- mockOctokit,
+ client,
"owner",
"repo",
- "claude/issue-123-20240101_123456",
+ "claude/issue-123",
"main",
);
expect(result.shouldDeleteBranch).toBe(false);
expect(result.branchLink).toBe(
- `\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123-20240101_123456)`,
+ `\n[View branch](${GITEA_SERVER_URL}/owner/repo/src/branch/claude/issue-123)`,
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
- "Error checking for commits on Claude branch:",
+ "Error checking branch:",
expect.any(Error),
);
- });
-
- test("should handle branch deletion errors gracefully", async () => {
- const deleteError = new Error("Delete failed");
- const mockOctokit = createMockOctokit({ total_commits: 0 }, deleteError);
-
- const result = await checkAndDeleteEmptyBranch(
- mockOctokit,
- "owner",
- "repo",
- "claude/issue-123-20240101_123456",
- "main",
- );
-
- expect(result.shouldDeleteBranch).toBe(true);
- expect(result.branchLink).toBe("");
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- "Failed to delete branch claude/issue-123-20240101_123456:",
- deleteError,
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ "Assuming branch exists due to non-404 error",
);
});
});
diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts
index ed2afa2..7599715 100644
--- a/test/comment-logic.test.ts
+++ b/test/comment-logic.test.ts
@@ -1,12 +1,28 @@
-import { describe, it, expect } from "bun:test";
+import { describe, it, expect, beforeEach, afterEach } from "bun:test";
import { updateCommentBody } from "../src/github/operations/comment-logic";
describe("updateCommentBody", () => {
+ const GITEA_SERVER_URL = "https://gitea.example.com";
+ const JOB_URL = `${GITEA_SERVER_URL}/owner/repo/actions/runs/123`;
+ const BRANCH_BASE_URL = `${GITEA_SERVER_URL}/owner/repo/src/branch`;
+
+ let originalEnv: NodeJS.ProcessEnv;
+
+ beforeEach(() => {
+ originalEnv = { ...process.env };
+ process.env.GITEA_SERVER_URL = GITEA_SERVER_URL;
+ process.env.GITEA_API_URL = `${GITEA_SERVER_URL}/api/v1`;
+ });
+
+ afterEach(() => {
+ process.env = originalEnv;
+ });
+
const baseInput = {
currentBody: "Initial comment body",
actionFailed: false,
executionDetails: null,
- jobUrl: "https://github.com/owner/repo/actions/runs/123",
+ jobUrl: JOB_URL,
branchName: undefined,
triggerUsername: undefined,
};
@@ -105,20 +121,19 @@ describe("updateCommentBody", () => {
const result = updateCommentBody(input);
expect(result).toContain(
- "• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
+ `• [\`claude/issue-123-20240101_120000\`](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
);
});
it("extracts branch name from branchLink if branchName not provided", () => {
const input = {
...baseInput,
- branchLink:
- "\n[View branch](https://github.com/owner/repo/src/branch/branch-name)",
+ branchLink: `\n[View branch](${BRANCH_BASE_URL}/branch-name)`,
};
const result = updateCommentBody(input);
expect(result).toContain(
- "• [`branch-name`](https://github.com/owner/repo/src/branch/branch-name)",
+ `• [\`branch-name\`](${BRANCH_BASE_URL}/branch-name)`,
);
});
@@ -126,13 +141,13 @@ describe("updateCommentBody", () => {
const input = {
...baseInput,
currentBody:
- "Some comment with [View branch](https://github.com/owner/repo/src/branch/branch-name)",
+ `Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)` ,
branchName: "new-branch-name",
};
const result = updateCommentBody(input);
expect(result).toContain(
- "• [`new-branch-name`](https://github.com/owner/repo/src/branch/new-branch-name)",
+ `• [\`new-branch-name\`](${BRANCH_BASE_URL}/new-branch-name)`,
);
expect(result).not.toContain("View branch");
});
@@ -142,12 +157,12 @@ describe("updateCommentBody", () => {
it("adds PR link to header when provided", () => {
const input = {
...baseInput,
- prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)",
+ prLink: "\n[Create a PR](https://gitea.example.com/owner/repo/pr-url)",
};
const result = updateCommentBody(input);
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/pr-url)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url)",
);
});
@@ -155,12 +170,12 @@ describe("updateCommentBody", () => {
const input = {
...baseInput,
currentBody:
- "Some comment with [Create a PR](https://github.com/owner/repo/pr-url)",
+ "Some comment with [Create a PR](https://gitea.example.com/owner/repo/pr-url)",
};
const result = updateCommentBody(input);
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/pr-url)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url)",
);
// Original Create a PR link is removed from body
expect(result).not.toContain("[Create a PR]");
@@ -170,21 +185,21 @@ describe("updateCommentBody", () => {
const input = {
...baseInput,
currentBody:
- "Some comment with [Create a PR](https://github.com/owner/repo/pr-url-from-body)",
+ "Some comment with [Create a PR](https://gitea.example.com/owner/repo/pr-url-from-body)",
prLink:
- "\n[Create a PR](https://github.com/owner/repo/pr-url-provided)",
+ "\n[Create a PR](https://gitea.example.com/owner/repo/pr-url-provided)",
};
const result = updateCommentBody(input);
// Prefers the link found in content over the provided one
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/pr-url-from-body)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url-from-body)",
);
});
it("handles complex PR URLs with encoded characters", () => {
const complexUrl =
- "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20important%20bug%20fix&body=Fixes%20%23123%0A%0A%23%23%20Description%0AThis%20PR%20fixes%20an%20important%20bug%20that%20was%20causing%20issues%20with%20the%20application.%0A%0AGenerated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
+ "https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20important%20bug%20fix&body=Fixes%20%23123%0A%0A%23%23%20Description%0AThis%20PR%20fixes%20an%20important%20bug%20that%20was%20causing%20issues%20with%20the%20application.%0A%0AGenerated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
const input = {
...baseInput,
currentBody: `Some comment with [Create a PR](${complexUrl})`,
@@ -198,7 +213,7 @@ describe("updateCommentBody", () => {
it("handles PR links with encoded URLs containing parentheses", () => {
const complexUrl =
- "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20bug%20fix&body=Generated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
+ "https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A%20bug%20fix&body=Generated%20with%20%5BClaude%20Code%5D(https%3A%2F%2Fclaude.ai%2Fcode)";
const input = {
...baseInput,
currentBody: `This PR was created.\n\n[Create a PR](${complexUrl})`,
@@ -217,9 +232,9 @@ describe("updateCommentBody", () => {
it("handles PR links with unencoded spaces and special characters", () => {
const unEncodedUrl =
- "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix: update welcome message&body=Generated with [Claude Code](https://claude.ai/code)";
+ "https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix: update welcome message&body=Generated with [Claude Code](https://claude.ai/code)";
const expectedEncodedUrl =
- "https://github.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A+update+welcome+message&body=Generated+with+%5BClaude+Code%5D%28https%3A%2F%2Fclaude.ai%2Fcode%29";
+ "https://gitea.example.com/owner/repo/compare/main...feature-branch?quick_pull=1&title=fix%3A+update+welcome+message&body=Generated+with+%5BClaude+Code%5D%28https%3A%2F%2Fclaude.ai%2Fcode%29";
const input = {
...baseInput,
currentBody: `This PR was created.\n\n[Create a PR](${unEncodedUrl})`,
@@ -235,7 +250,7 @@ describe("updateCommentBody", () => {
it("falls back to prLink parameter when PR link in content cannot be encoded", () => {
const invalidUrl = "not-a-valid-url-at-all";
- const fallbackPrUrl = "https://github.com/owner/repo/pull/123";
+ const fallbackPrUrl = "https://gitea.example.com/owner/repo/pull/123";
const input = {
...baseInput,
currentBody: `This PR was created.\n\n[Create a PR](${invalidUrl})`,
@@ -317,7 +332,7 @@ describe("updateCommentBody", () => {
"Claude Code is working…\n\n### Todo List:\n- [x] Read README.md\n- [x] Add disclaimer",
actionFailed: false,
branchName: "claude-branch-123",
- prLink: "\n[Create a PR](https://github.com/owner/repo/pr-url)",
+ prLink: "\n[Create a PR](https://gitea.example.com/owner/repo/pr-url)",
executionDetails: {
cost_usd: 0.01,
duration_ms: 65000, // 1 minute 5 seconds
@@ -333,7 +348,7 @@ describe("updateCommentBody", () => {
);
expect(result).toContain("—— [View job]");
expect(result).toContain(
- "• [`claude-branch-123`](https://github.com/owner/repo/src/branch/claude-branch-123)",
+ `• [\`claude-branch-123\`](${BRANCH_BASE_URL}/claude-branch-123)`,
);
expect(result).toContain("• [Create PR ➔]");
@@ -358,7 +373,7 @@ describe("updateCommentBody", () => {
const input = {
...baseInput,
currentBody:
- "Claude Code is working…\n\nI've made changes.\n[Create a PR](https://github.com/owner/repo/pr-url-in-content)\n\n@john-doe",
+ "Claude Code is working…\n\nI've made changes.\n[Create a PR](https://gitea.example.com/owner/repo/pr-url-in-content)\n\n@john-doe",
branchName: "feature-branch",
triggerUsername: "john-doe",
};
@@ -367,7 +382,7 @@ describe("updateCommentBody", () => {
// PR link should be moved to header
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/pr-url-in-content)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/pr-url-in-content)",
);
// Original link should be removed from body
expect(result).not.toContain("[Create a PR]");
@@ -383,7 +398,7 @@ describe("updateCommentBody", () => {
currentBody: "Claude Code is working…
",
branchName: "claude/pr-456-20240101_120000",
prLink:
- "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
+ "\n[Create a PR](https://gitea.example.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
triggerUsername: "jane-doe",
};
@@ -391,7 +406,7 @@ describe("updateCommentBody", () => {
// Should include the PR link in the formatted style
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/compare/main...claude/pr-456-20240101_120000)",
);
expect(result).toContain("**Claude finished @jane-doe's task**");
});
@@ -401,20 +416,19 @@ describe("updateCommentBody", () => {
...baseInput,
currentBody: "Claude Code is working…",
branchName: "claude/issue-123-20240101_120000",
- branchLink:
- "\n[View branch](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
+ branchLink: `\n[View branch](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
prLink:
- "\n[Create a PR](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
+ "\n[Create a PR](https://gitea.example.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
};
const result = updateCommentBody(input);
// Should include both links in formatted style
expect(result).toContain(
- "• [`claude/issue-123-20240101_120000`](https://github.com/owner/repo/src/branch/claude/issue-123-20240101_120000)",
+ `• [\`claude/issue-123-20240101_120000\`](${BRANCH_BASE_URL}/claude/issue-123-20240101_120000)`,
);
expect(result).toContain(
- "• [Create PR ➔](https://github.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
+ "• [Create PR ➔](https://gitea.example.com/owner/repo/compare/main...claude/issue-123-20240101_120000)",
);
});
});
diff --git a/test/create-prompt.test.ts b/test/create-prompt.test.ts
index 665a229..3f9e43c 100644
--- a/test/create-prompt.test.ts
+++ b/test/create-prompt.test.ts
@@ -8,7 +8,6 @@ import {
buildDisallowedToolsString,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
-import type { EventData } from "../src/create-prompt/types";
describe("generatePrompt", () => {
const mockGitHubData = {
@@ -134,7 +133,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("GENERAL_COMMENT");
@@ -162,7 +161,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("PR_REVIEW");
expect(prompt).toContain("true");
@@ -188,16 +187,14 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("ISSUE_CREATED");
expect(prompt).toContain(
"new issue with '@claude' in body",
);
- expect(prompt).toContain(
- "[Create a PR](https://github.com/owner/repo/compare/main",
- );
- expect(prompt).toContain("The target-branch should be 'main'");
+ expect(prompt).toContain("mcp__gitea__update_issue_comment");
+ expect(prompt).toContain("mcp__gitea__list_branches");
});
test("should generate prompt for issue assigned event", () => {
@@ -216,15 +213,14 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("ISSUE_ASSIGNED");
expect(prompt).toContain(
"issue assigned to 'claude-bot'",
);
- expect(prompt).toContain(
- "[Create a PR](https://github.com/owner/repo/compare/develop",
- );
+ expect(prompt).toContain("mcp__gitea__list_branches");
+ expect(prompt).toContain("mcp__local_git_ops__checkout_branch");
});
test("should include direct prompt when provided", () => {
@@ -243,7 +239,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("");
expect(prompt).toContain("Fix the bug in the login form");
@@ -266,7 +262,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("PULL_REQUEST");
expect(prompt).toContain("true");
@@ -291,7 +287,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
});
@@ -313,11 +309,11 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
expect(prompt).toContain("johndoe");
expect(prompt).toContain(
- "Co-authored-by: johndoe ",
+ "johndoe",
);
});
@@ -334,7 +330,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
// Should contain PR-specific instructions
expect(prompt).toContain(
@@ -367,19 +363,12 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
// Should contain Issue-specific instructions
- expect(prompt).toContain(
- "You are already on the correct branch (claude/issue-789-20240101_120000)",
- );
- expect(prompt).toContain(
- "IMPORTANT: You are already on the correct branch (claude/issue-789-20240101_120000)",
- );
- expect(prompt).toContain("Create a PR](https://github.com/");
- expect(prompt).toContain(
- "If you created a branch and made changes, your comment must include the PR URL",
- );
+ expect(prompt).toContain("mcp__gitea__update_issue_comment");
+ expect(prompt).toContain("mcp__gitea__list_branches");
+ expect(prompt).toContain("mcp__local_git_ops__checkout_branch");
// Should NOT contain PR-specific instructions
expect(prompt).not.toContain(
@@ -406,54 +395,11 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
- // Should contain the actual branch name with timestamp
- expect(prompt).toContain(
- "You are already on the correct branch (claude/issue-123-20240101_120000)",
- );
- expect(prompt).toContain(
- "IMPORTANT: You are already on the correct branch (claude/issue-123-20240101_120000)",
- );
- expect(prompt).toContain(
- "The branch-name is the current branch: claude/issue-123-20240101_120000",
- );
- });
-
- test("should handle closed PR with new branch", () => {
- const envVars: PreparedContext = {
- repository: "owner/repo",
- claudeCommentId: "12345",
- triggerPhrase: "@claude",
- eventData: {
- eventName: "issue_comment",
- commentId: "67890",
- isPR: true,
- prNumber: "456",
- commentBody: "@claude please fix this",
- claudeBranch: "claude/pr-456-20240101_120000",
- baseBranch: "main",
- },
- };
-
- const prompt = generatePrompt(envVars, mockGitHubData);
-
- // Should contain branch-specific instructions like issues
- expect(prompt).toContain(
- "You are already on the correct branch (claude/pr-456-20240101_120000)",
- );
- expect(prompt).toContain(
- "Create a PR](https://github.com/owner/repo/compare/main",
- );
- expect(prompt).toContain(
- "The branch-name is the current branch: claude/pr-456-20240101_120000",
- );
- expect(prompt).toContain("Reference to the original PR");
-
- // Should NOT contain open PR instructions
- expect(prompt).not.toContain(
- "Commit changes using mcp__local_git_ops__commit_files to the existing branch",
- );
+ // Should surface the issue number and comment metadata
+ expect(prompt).toContain("123");
+ expect(prompt).toContain("12345");
});
test("should handle open PR without new branch", () => {
@@ -471,7 +417,7 @@ describe("generatePrompt", () => {
},
};
- const prompt = generatePrompt(envVars, mockGitHubData);
+ const prompt = generatePrompt(envVars, mockGitHubData, false);
// Should contain open PR instructions
expect(prompt).toContain(
@@ -488,84 +434,6 @@ describe("generatePrompt", () => {
"If you created anything in your branch, your comment must include the PR URL",
);
});
-
- test("should handle PR review on closed PR with new branch", () => {
- const envVars: PreparedContext = {
- repository: "owner/repo",
- claudeCommentId: "12345",
- triggerPhrase: "@claude",
- eventData: {
- eventName: "pull_request_review",
- isPR: true,
- prNumber: "789",
- commentBody: "@claude please update this",
- claudeBranch: "claude/pr-789-20240101_123000",
- baseBranch: "develop",
- },
- };
-
- const prompt = generatePrompt(envVars, mockGitHubData);
-
- // Should contain new branch instructions
- expect(prompt).toContain(
- "You are already on the correct branch (claude/pr-789-20240101_123000)",
- );
- expect(prompt).toContain(
- "Create a PR](https://github.com/owner/repo/compare/develop",
- );
- expect(prompt).toContain("Reference to the original PR");
- });
-
- test("should handle PR review comment on closed PR with new branch", () => {
- const envVars: PreparedContext = {
- repository: "owner/repo",
- claudeCommentId: "12345",
- triggerPhrase: "@claude",
- eventData: {
- eventName: "pull_request_review_comment",
- isPR: true,
- prNumber: "999",
- commentId: "review-comment-123",
- commentBody: "@claude fix this issue",
- claudeBranch: "claude/pr-999-20240101_140000",
- baseBranch: "main",
- },
- };
-
- const prompt = generatePrompt(envVars, mockGitHubData);
-
- // Should contain new branch instructions
- expect(prompt).toContain(
- "You are already on the correct branch (claude/pr-999-20240101_140000)",
- );
- expect(prompt).toContain("Create a PR](https://github.com/");
- expect(prompt).toContain("Reference to the original PR");
- });
-
- test("should handle pull_request event on closed PR with new branch", () => {
- const envVars: PreparedContext = {
- repository: "owner/repo",
- claudeCommentId: "12345",
- triggerPhrase: "@claude",
- eventData: {
- eventName: "pull_request",
- eventAction: "closed",
- isPR: true,
- prNumber: "555",
- claudeBranch: "claude/pr-555-20240101_150000",
- baseBranch: "main",
- },
- };
-
- const prompt = generatePrompt(envVars, mockGitHubData);
-
- // Should contain new branch instructions
- expect(prompt).toContain(
- "You are already on the correct branch (claude/pr-555-20240101_150000)",
- );
- expect(prompt).toContain("Create a PR](https://github.com/");
- expect(prompt).toContain("Reference to the original PR");
- });
});
describe("getEventTypeAndContext", () => {
@@ -612,81 +480,36 @@ describe("getEventTypeAndContext", () => {
});
describe("buildAllowedToolsString", () => {
- test("should return issue comment tool for regular events", () => {
- const mockEventData: EventData = {
- eventName: "issue_comment",
- commentId: "123",
- isPR: true,
- prNumber: "456",
- commentBody: "Test comment",
- };
+ test("should include base tools", () => {
+ const result = buildAllowedToolsString();
- const result = buildAllowedToolsString(mockEventData);
-
- // The base tools should be in the result
expect(result).toContain("Edit");
expect(result).toContain("Glob");
- expect(result).toContain("Grep");
- expect(result).toContain("LS");
- expect(result).toContain("Read");
- expect(result).toContain("Write");
- expect(result).toContain("mcp__github__update_issue_comment");
- expect(result).not.toContain("mcp__github__update_pull_request_comment");
- expect(result).toContain("mcp__local_git_ops__commit_files");
- expect(result).toContain("mcp__local_git_ops__delete_files");
+ expect(result).toContain("mcp__gitea__update_issue_comment");
+ expect(result).toContain("mcp__gitea__update_pull_request_comment");
});
- test("should return PR comment tool for inline review comments", () => {
- const mockEventData: EventData = {
- eventName: "pull_request_review_comment",
- isPR: true,
- prNumber: "456",
- commentBody: "Test review comment",
- commentId: "789",
- };
+ test("should include commit signing tools when enabled", () => {
+ const result = buildAllowedToolsString(undefined, false, true);
- const result = buildAllowedToolsString(mockEventData);
+ expect(result).toContain("mcp__github_file_ops__commit_files");
+ expect(result).toContain("mcp__github_file_ops__delete_files");
+ });
- // The base tools should be in the result
- expect(result).toContain("Edit");
- expect(result).toContain("Glob");
- expect(result).toContain("Grep");
- expect(result).toContain("LS");
- expect(result).toContain("Read");
- expect(result).toContain("Write");
- expect(result).not.toContain("mcp__github__update_issue_comment");
- expect(result).toContain("mcp__github__update_pull_request_comment");
- expect(result).toContain("mcp__local_git_ops__commit_files");
- expect(result).toContain("mcp__local_git_ops__delete_files");
+ test("should include actions tools when actions read permission granted", () => {
+ const result = buildAllowedToolsString([], true, false);
+
+ expect(result).toContain("mcp__github_actions__get_ci_status");
+ expect(result).toContain("mcp__github_actions__download_job_log");
});
test("should append custom tools when provided", () => {
- const mockEventData: EventData = {
- eventName: "issue_comment",
- commentId: "123",
- isPR: true,
- prNumber: "456",
- commentBody: "Test comment",
- };
-
const customTools = "Tool1,Tool2,Tool3";
- const result = buildAllowedToolsString(mockEventData, customTools);
+ const result = buildAllowedToolsString(customTools);
- // Base tools should be present
- expect(result).toContain("Edit");
- expect(result).toContain("Glob");
-
- // Custom tools should be appended
expect(result).toContain("Tool1");
expect(result).toContain("Tool2");
expect(result).toContain("Tool3");
-
- // Verify format with comma separation
- const basePlusCustom = result.split(",");
- expect(basePlusCustom.length).toBeGreaterThan(10); // At least the base tools plus custom
- expect(basePlusCustom).toContain("Tool1");
- expect(basePlusCustom).toContain("Tool2");
- expect(basePlusCustom).toContain("Tool3");
});
});
diff --git a/test/image-downloader.test.ts b/test/image-downloader.test.ts
index 01f30fa..6488e65 100644
--- a/test/image-downloader.test.ts
+++ b/test/image-downloader.test.ts
@@ -1,665 +1,48 @@
+import { describe, test, expect, spyOn, beforeEach, afterEach } from "bun:test";
import {
- describe,
- test,
- expect,
- spyOn,
- beforeEach,
- afterEach,
- jest,
- setSystemTime,
-} from "bun:test";
-import fs from "fs/promises";
-import { downloadCommentImages } from "../src/github/utils/image-downloader";
-import type { CommentWithImages } from "../src/github/utils/image-downloader";
-import type { Octokits } from "../src/github/api/client";
+ downloadCommentImages,
+ type CommentWithImages,
+} from "../src/github/utils/image-downloader";
+
+const noopClient = { api: {} } as any;
describe("downloadCommentImages", () => {
let consoleLogSpy: any;
- let consoleWarnSpy: any;
- let consoleErrorSpy: any;
- let fsMkdirSpy: any;
- let fsWriteFileSpy: any;
- let fetchSpy: any;
beforeEach(() => {
- // Spy on console methods
consoleLogSpy = spyOn(console, "log").mockImplementation(() => {});
- consoleWarnSpy = spyOn(console, "warn").mockImplementation(() => {});
- consoleErrorSpy = spyOn(console, "error").mockImplementation(() => {});
-
- // Spy on fs methods
- fsMkdirSpy = spyOn(fs, "mkdir").mockResolvedValue(undefined);
- fsWriteFileSpy = spyOn(fs, "writeFile").mockResolvedValue(undefined);
-
- // Set fake system time for consistent filenames
- setSystemTime(new Date("2024-01-01T00:00:00.000Z")); // 1704067200000
});
afterEach(() => {
consoleLogSpy.mockRestore();
- consoleWarnSpy.mockRestore();
- consoleErrorSpy.mockRestore();
- fsMkdirSpy.mockRestore();
- fsWriteFileSpy.mockRestore();
- if (fetchSpy) fetchSpy.mockRestore();
- setSystemTime(); // Reset to real time
});
- const createMockOctokit = (): Octokits => {
- return {
- rest: {
- issues: {
- getComment: jest.fn(),
- get: jest.fn(),
- },
- pulls: {
- getReviewComment: jest.fn(),
- getReview: jest.fn(),
- get: jest.fn(),
- },
- },
- } as any as Octokits;
- };
+ test("returns empty map and logs disabled message", async () => {
+ const result = await downloadCommentImages(
+ noopClient,
+ "owner",
+ "repo",
+ [] as CommentWithImages[],
+ );
- test("should create download directory", async () => {
- const mockOctokit = createMockOctokit();
- const comments: CommentWithImages[] = [];
-
- await downloadCommentImages(mockOctokit, "owner", "repo", comments);
-
- expect(fsMkdirSpy).toHaveBeenCalledWith("/tmp/github-images", {
- recursive: true,
- });
+ expect(result.size).toBe(0);
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ "Image downloading temporarily disabled during Octokit migration",
+ );
});
- test("should handle comments without images", async () => {
- const mockOctokit = createMockOctokit();
+ test("ignores provided comments while feature disabled", async () => {
const comments: CommentWithImages[] = [
{
type: "issue_comment",
id: "123",
- body: "This is a comment without images",
+ body: "",
},
];
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
+ const result = await downloadCommentImages(noopClient, "owner", "repo", comments);
expect(result.size).toBe(0);
- expect(consoleLogSpy).not.toHaveBeenCalledWith(
- expect.stringContaining("Found"),
- );
- });
-
- test("should detect and download images from issue comments", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl =
- "https://github.com/user-attachments/assets/test-image.png";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/test.png?jwt=token";
-
- // Mock octokit response
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- // Mock fetch for image download
- const mockArrayBuffer = new ArrayBuffer(8);
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => mockArrayBuffer,
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "123",
- body: `Here's an image: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(mockOctokit.rest.issues.getComment).toHaveBeenCalledWith({
- owner: "owner",
- repo: "repo",
- comment_id: 123,
- mediaType: { format: "full+json" },
- });
-
- expect(fetchSpy).toHaveBeenCalledWith(signedUrl);
- expect(fsWriteFileSpy).toHaveBeenCalledWith(
- "/tmp/github-images/image-1704067200000-0.png",
- Buffer.from(mockArrayBuffer),
- );
-
- expect(result.size).toBe(1);
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.png",
- );
- expect(consoleLogSpy).toHaveBeenCalledWith(
- "Found 1 image(s) in issue_comment 123",
- );
- expect(consoleLogSpy).toHaveBeenCalledWith(`Downloading ${imageUrl}...`);
- expect(consoleLogSpy).toHaveBeenCalledWith(
- "✓ Saved: /tmp/github-images/image-1704067200000-0.png",
- );
- });
-
- test("should handle review comments", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl =
- "https://github.com/user-attachments/assets/review-image.jpg";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/review.jpg?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.pulls.getReviewComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "review_comment",
- id: "456",
- body: `Review comment with image: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(mockOctokit.rest.pulls.getReviewComment).toHaveBeenCalledWith({
- owner: "owner",
- repo: "repo",
- comment_id: 456,
- mediaType: { format: "full+json" },
- });
-
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.jpg",
- );
- });
-
- test("should handle review bodies", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl =
- "https://github.com/user-attachments/assets/review-body.png";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/body.png?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.pulls.getReview = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "review_body",
- id: "789",
- pullNumber: "100",
- body: `Review body: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(mockOctokit.rest.pulls.getReview).toHaveBeenCalledWith({
- owner: "owner",
- repo: "repo",
- pull_number: 100,
- review_id: 789,
- mediaType: { format: "full+json" },
- });
-
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.png",
- );
- });
-
- test("should handle issue bodies", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl =
- "https://github.com/user-attachments/assets/issue-body.gif";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/issue.gif?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.get = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_body",
- issueNumber: "200",
- body: `Issue description: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(mockOctokit.rest.issues.get).toHaveBeenCalledWith({
- owner: "owner",
- repo: "repo",
- issue_number: 200,
- mediaType: { format: "full+json" },
- });
-
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.gif",
- );
- expect(consoleLogSpy).toHaveBeenCalledWith(
- "Found 1 image(s) in issue_body 200",
- );
- });
-
- test("should handle PR bodies", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl = "https://github.com/user-attachments/assets/pr-body.webp";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/pr.webp?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.pulls.get = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "pr_body",
- pullNumber: "300",
- body: `PR description: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(mockOctokit.rest.pulls.get).toHaveBeenCalledWith({
- owner: "owner",
- repo: "repo",
- pull_number: 300,
- mediaType: { format: "full+json" },
- });
-
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.webp",
- );
- expect(consoleLogSpy).toHaveBeenCalledWith(
- "Found 1 image(s) in pr_body 300",
- );
- });
-
- test("should handle multiple images in a single comment", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl1 = "https://github.com/user-attachments/assets/image1.png";
- const imageUrl2 = "https://github.com/user-attachments/assets/image2.jpg";
- const signedUrl1 =
- "https://private-user-images.githubusercontent.com/1.png?jwt=token1";
- const signedUrl2 =
- "https://private-user-images.githubusercontent.com/2.jpg?jwt=token2";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "999",
- body: `Two images:  and `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(fetchSpy).toHaveBeenCalledTimes(2);
- expect(result.size).toBe(2);
- expect(result.get(imageUrl1)).toBe(
- "/tmp/github-images/image-1704067200000-0.png",
- );
- expect(result.get(imageUrl2)).toBe(
- "/tmp/github-images/image-1704067200000-1.jpg",
- );
- expect(consoleLogSpy).toHaveBeenCalledWith(
- "Found 2 image(s) in issue_comment 999",
- );
- });
-
- test("should skip already downloaded images", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl = "https://github.com/user-attachments/assets/duplicate.png";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/dup.png?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "111",
- body: `First: `,
- },
- {
- type: "issue_comment",
- id: "222",
- body: `Second: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(fetchSpy).toHaveBeenCalledTimes(1); // Only downloaded once
- expect(result.size).toBe(1);
- expect(result.get(imageUrl)).toBe(
- "/tmp/github-images/image-1704067200000-0.png",
- );
- });
-
- test("should handle missing HTML body", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl = "https://github.com/user-attachments/assets/missing.png";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: null,
- },
- });
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "333",
- body: `Missing HTML: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(result.size).toBe(0);
- expect(consoleWarnSpy).toHaveBeenCalledWith(
- "No HTML body found for issue_comment 333",
- );
- });
-
- test("should handle fetch errors", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl = "https://github.com/user-attachments/assets/error.png";
- const signedUrl =
- "https://private-user-images.githubusercontent.com/error.png?jwt=token";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: false,
- status: 404,
- statusText: "Not Found",
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "444",
- body: `Error image: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(result.size).toBe(0);
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- `✗ Failed to download ${imageUrl}:`,
- expect.any(Error),
- );
- });
-
- test("should handle API errors gracefully", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl = "https://github.com/user-attachments/assets/api-error.png";
-
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest
- .fn()
- .mockRejectedValue(new Error("API rate limit exceeded"));
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "555",
- body: `API error: `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(result.size).toBe(0);
- expect(consoleErrorSpy).toHaveBeenCalledWith(
- "Failed to process images for issue_comment 555:",
- expect.any(Error),
- );
- });
-
- test("should extract correct file extensions", async () => {
- const mockOctokit = createMockOctokit();
- const extensions = [
- {
- url: "https://github.com/user-attachments/assets/test.png",
- ext: ".png",
- },
- {
- url: "https://github.com/user-attachments/assets/test.jpg",
- ext: ".jpg",
- },
- {
- url: "https://github.com/user-attachments/assets/test.jpeg",
- ext: ".jpeg",
- },
- {
- url: "https://github.com/user-attachments/assets/test.gif",
- ext: ".gif",
- },
- {
- url: "https://github.com/user-attachments/assets/test.webp",
- ext: ".webp",
- },
- {
- url: "https://github.com/user-attachments/assets/test.svg",
- ext: ".svg",
- },
- {
- // default
- url: "https://github.com/user-attachments/assets/no-extension",
- ext: ".png",
- },
- ];
-
- let callIndex = 0;
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- for (const { url, ext } of extensions) {
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: `${1000 + callIndex}`,
- body: `Test: `,
- },
- ];
-
- setSystemTime(new Date(1704067200000 + callIndex));
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
- expect(result.get(url)).toBe(
- `/tmp/github-images/image-${1704067200000 + callIndex}-0${ext}`,
- );
-
- // Reset for next iteration
- fsWriteFileSpy.mockClear();
- callIndex++;
- }
- });
-
- test("should handle mismatched signed URL count", async () => {
- const mockOctokit = createMockOctokit();
- const imageUrl1 = "https://github.com/user-attachments/assets/img1.png";
- const imageUrl2 = "https://github.com/user-attachments/assets/img2.png";
- const signedUrl1 =
- "https://private-user-images.githubusercontent.com/1.png?jwt=token";
-
- // Only one signed URL for two images
- // @ts-expect-error Mock implementation doesn't match full type signature
- mockOctokit.rest.issues.getComment = jest.fn().mockResolvedValue({
- data: {
- body_html: `
`,
- },
- });
-
- fetchSpy = spyOn(global, "fetch").mockResolvedValue({
- ok: true,
- arrayBuffer: async () => new ArrayBuffer(8),
- } as Response);
-
- const comments: CommentWithImages[] = [
- {
- type: "issue_comment",
- id: "666",
- body: `Two images:  `,
- },
- ];
-
- const result = await downloadCommentImages(
- mockOctokit,
- "owner",
- "repo",
- comments,
- );
-
- expect(fetchSpy).toHaveBeenCalledTimes(1);
- expect(result.size).toBe(1);
- expect(result.get(imageUrl1)).toBe(
- "/tmp/github-images/image-1704067200000-0.png",
- );
- expect(result.get(imageUrl2)).toBeUndefined();
+ expect(consoleLogSpy).toHaveBeenCalled();
});
});
diff --git a/test/install-mcp-server.test.ts b/test/install-mcp-server.test.ts
index ac8c11e..4c2b958 100644
--- a/test/install-mcp-server.test.ts
+++ b/test/install-mcp-server.test.ts
@@ -1,682 +1,59 @@
-import { describe, test, expect, beforeEach, afterEach, spyOn } from "bun:test";
+import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { prepareMcpConfig } from "../src/mcp/install-mcp-server";
-import * as core from "@actions/core";
-import type { ParsedGitHubContext } from "../src/github/context";
+
+const originalEnv = { ...process.env };
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: {
- mode: "tag",
- triggerPhrase: "@claude",
- assigneeTrigger: "",
- labelTrigger: "",
- allowedTools: [],
- disallowedTools: [],
- customInstructions: "",
- directPrompt: "",
- overridePrompt: "",
- 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";
- }
+ process.env.GITHUB_ACTION_PATH = "/action/path";
+ process.env.GITHUB_WORKSPACE = "/workspace";
+ process.env.GITEA_API_URL = "https://gitea.example.com/api/v1";
});
afterEach(() => {
- consoleInfoSpy.mockRestore();
- consoleWarningSpy.mockRestore();
- setFailedSpy.mockRestore();
- processExitSpy.mockRestore();
+ process.env = { ...originalEnv };
});
- test("should return comment server when commit signing is disabled", async () => {
+ test("returns base gitea and local git MCP servers", async () => {
const result = await prepareMcpConfig({
- githubToken: "test-token",
- owner: "test-owner",
- repo: "test-repo",
- branch: "test-branch",
- baseBranch: "main",
- allowedTools: [],
- context: mockContext,
+ githubToken: "token",
+ owner: "owner",
+ repo: "repo",
+ branch: "branch",
});
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");
- });
+ expect(Object.keys(parsed.mcpServers)).toEqual(["gitea", "local_git_ops"]);
- 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",
- },
- },
+ expect(parsed.mcpServers.gitea).toEqual({
+ command: "bun",
+ args: ["run", "/action/path/src/mcp/gitea-mcp-server.ts"],
+ env: {
+ GITHUB_TOKEN: "token",
+ REPO_OWNER: "owner",
+ REPO_NAME: "repo",
+ BRANCH_NAME: "branch",
+ REPO_DIR: "/workspace",
+ GITEA_API_URL: "https://gitea.example.com/api/v1",
},
});
- 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.local_git_ops.args[1]).toBe(
+ "/action/path/src/mcp/local-git-ops-server.ts",
);
- 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;
+ test("falls back to process.cwd when workspace not provided", async () => {
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,
+ githubToken: "token",
+ owner: "owner",
+ repo: "repo",
+ branch: "branch",
});
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.ACTIONS_TOKEN;
- process.env.ACTIONS_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.ACTIONS_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.ACTIONS_TOKEN;
- process.env.ACTIONS_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.ACTIONS_TOKEN = oldTokenEnv;
- });
-
- test("should parse additional_permissions with multiple lines correctly", async () => {
- const oldTokenEnv = process.env.ACTIONS_TOKEN;
- process.env.ACTIONS_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.ACTIONS_TOKEN = oldTokenEnv;
- });
-
- test("should warn when actions:read is requested but token lacks permission", async () => {
- const oldTokenEnv = process.env.ACTIONS_TOKEN;
- process.env.ACTIONS_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.ACTIONS_TOKEN = oldTokenEnv;
+ expect(parsed.mcpServers.gitea.env.REPO_DIR).toBe(process.cwd());
});
});
diff --git a/test/permissions.test.ts b/test/permissions.test.ts
index 2caaaf8..10e244e 100644
--- a/test/permissions.test.ts
+++ b/test/permissions.test.ts
@@ -1,169 +1,63 @@
-import { describe, expect, test, spyOn, beforeEach, afterEach } from "bun:test";
+import { describe, expect, test, beforeEach, afterEach, spyOn } from "bun:test";
import * as core from "@actions/core";
import { checkWritePermissions } from "../src/github/validation/permissions";
import type { ParsedGitHubContext } from "../src/github/context";
+const baseContext: ParsedGitHubContext = {
+ runId: "123",
+ eventName: "issue_comment",
+ eventAction: "created",
+ repository: {
+ owner: "owner",
+ repo: "repo",
+ full_name: "owner/repo",
+ },
+ actor: "tester",
+ payload: {
+ action: "created",
+ issue: { number: 1, body: "", title: "", user: { login: "owner" } },
+ comment: { id: 1, body: "@claude ping", user: { login: "tester" } },
+ } as any,
+ entityNumber: 1,
+ isPR: false,
+ inputs: {
+ mode: "tag",
+ triggerPhrase: "@claude",
+ assigneeTrigger: "",
+ labelTrigger: "",
+ allowedTools: [],
+ disallowedTools: [],
+ customInstructions: "",
+ directPrompt: "",
+ overridePrompt: "",
+ branchPrefix: "claude/",
+ useStickyComment: false,
+ additionalPermissions: new Map(),
+ useCommitSigning: false,
+ },
+};
+
describe("checkWritePermissions", () => {
- let coreInfoSpy: any;
- let coreWarningSpy: any;
- let coreErrorSpy: any;
+ let infoSpy: any;
+ const originalEnv = { ...process.env };
beforeEach(() => {
- // Spy on core methods
- coreInfoSpy = spyOn(core, "info").mockImplementation(() => {});
- coreWarningSpy = spyOn(core, "warning").mockImplementation(() => {});
- coreErrorSpy = spyOn(core, "error").mockImplementation(() => {});
+ infoSpy = spyOn(core, "info").mockImplementation(() => {});
+ process.env.GITEA_API_URL = "https://gitea.example.com/api/v1";
});
afterEach(() => {
- coreInfoSpy.mockRestore();
- coreWarningSpy.mockRestore();
- coreErrorSpy.mockRestore();
+ infoSpy.mockRestore();
+ process.env = { ...originalEnv };
});
- const createMockOctokit = (permission: string) => {
- return {
- repos: {
- getCollaboratorPermissionLevel: async () => ({
- data: { permission },
- }),
- },
- } as any;
- };
-
- const createContext = (): ParsedGitHubContext => ({
- runId: "1234567890",
- eventName: "issue_comment",
- eventAction: "created",
- repository: {
- full_name: "test-owner/test-repo",
- owner: "test-owner",
- repo: "test-repo",
- },
- actor: "test-user",
- payload: {
- action: "created",
- issue: {
- number: 1,
- title: "Test Issue",
- body: "Test body",
- user: { login: "test-user" },
- },
- comment: {
- id: 123,
- body: "@claude test",
- user: { login: "test-user" },
- html_url:
- "https://github.com/test-owner/test-repo/issues/1#issuecomment-123",
- },
- } as any,
- entityNumber: 1,
- isPR: false,
- inputs: {
- mode: "tag",
- triggerPhrase: "@claude",
- assigneeTrigger: "",
- labelTrigger: "",
- allowedTools: [],
- disallowedTools: [],
- customInstructions: "",
- directPrompt: "",
- overridePrompt: "",
- branchPrefix: "claude/",
- useStickyComment: false,
- additionalPermissions: new Map(),
- useCommitSigning: false,
- },
- });
-
- test("should return true for admin permissions", async () => {
- const mockOctokit = createMockOctokit("admin");
- const context = createContext();
-
- const result = await checkWritePermissions(mockOctokit, context);
+ test("returns true immediately in Gitea environments", async () => {
+ const client = { api: { getBaseUrl: () => "https://gitea.example.com/api/v1" } } as any;
+ const result = await checkWritePermissions(client, baseContext);
expect(result).toBe(true);
- expect(coreInfoSpy).toHaveBeenCalledWith(
- "Checking permissions for actor: test-user",
+ expect(infoSpy).toHaveBeenCalledWith(
+ "Detected Gitea environment (https://gitea.example.com/api/v1), assuming actor has permissions",
);
- expect(coreInfoSpy).toHaveBeenCalledWith(
- "Permission level retrieved: admin",
- );
- expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: admin");
- });
-
- test("should return true for write permissions", async () => {
- const mockOctokit = createMockOctokit("write");
- const context = createContext();
-
- const result = await checkWritePermissions(mockOctokit, context);
-
- expect(result).toBe(true);
- expect(coreInfoSpy).toHaveBeenCalledWith("Actor has write access: write");
- });
-
- test("should return false for read permissions", async () => {
- const mockOctokit = createMockOctokit("read");
- const context = createContext();
-
- const result = await checkWritePermissions(mockOctokit, context);
-
- expect(result).toBe(false);
- expect(coreWarningSpy).toHaveBeenCalledWith(
- "Actor has insufficient permissions: read",
- );
- });
-
- test("should return false for none permissions", async () => {
- const mockOctokit = createMockOctokit("none");
- const context = createContext();
-
- const result = await checkWritePermissions(mockOctokit, context);
-
- expect(result).toBe(false);
- expect(coreWarningSpy).toHaveBeenCalledWith(
- "Actor has insufficient permissions: none",
- );
- });
-
- test("should throw error when permission check fails", async () => {
- const error = new Error("API error");
- const mockOctokit = {
- repos: {
- getCollaboratorPermissionLevel: async () => {
- throw error;
- },
- },
- } as any;
- const context = createContext();
-
- await expect(checkWritePermissions(mockOctokit, context)).rejects.toThrow(
- "Failed to check permissions for test-user: Error: API error",
- );
-
- expect(coreErrorSpy).toHaveBeenCalledWith(
- "Failed to check permissions: Error: API error",
- );
- });
-
- test("should call API with correct parameters", async () => {
- let capturedParams: any;
- const mockOctokit = {
- repos: {
- getCollaboratorPermissionLevel: async (params: any) => {
- capturedParams = params;
- return { data: { permission: "write" } };
- },
- },
- } as any;
- const context = createContext();
-
- await checkWritePermissions(mockOctokit, context);
-
- expect(capturedParams).toEqual({
- owner: "test-owner",
- repo: "test-repo",
- username: "test-user",
- });
});
});
diff --git a/test/update-claude-comment.test.ts b/test/update-claude-comment.test.ts
index e56c039..0f16713 100644
--- a/test/update-claude-comment.test.ts
+++ b/test/update-claude-comment.test.ts
@@ -1,12 +1,11 @@
import { describe, test, expect, jest, beforeEach } from "bun:test";
-import { Octokit } from "@octokit/rest";
import {
updateClaudeComment,
type UpdateClaudeCommentParams,
} from "../src/github/operations/comments/update-claude-comment";
describe("updateClaudeComment", () => {
- let mockOctokit: Octokit;
+ let mockOctokit: any;
beforeEach(() => {
mockOctokit = {
@@ -18,7 +17,7 @@ describe("updateClaudeComment", () => {
updateReviewComment: jest.fn(),
},
},
- } as any as Octokit;
+ };
});
test("should update issue comment successfully", async () => {
@@ -31,7 +30,6 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -70,7 +68,6 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -109,7 +106,6 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -151,11 +147,9 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockRejectedValue(mockError);
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -195,7 +189,6 @@ describe("updateClaudeComment", () => {
const mockError = new Error("Internal Server Error") as any;
mockError.status = 500;
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockRejectedValue(mockError);
@@ -226,7 +219,6 @@ describe("updateClaudeComment", () => {
test("should propagate error when issue comment update fails", async () => {
const mockError = new Error("Forbidden");
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockRejectedValue(mockError);
@@ -261,7 +253,6 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -294,7 +285,6 @@ describe("updateClaudeComment", () => {
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -345,7 +335,6 @@ const code = "example";
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.issues.updateComment = jest
.fn()
.mockResolvedValue(mockResponse);
@@ -388,7 +377,6 @@ const code = "example";
},
};
- // @ts-expect-error Mock implementation doesn't match full type signature
mockOctokit.rest.pulls.updateReviewComment = jest
.fn()
.mockResolvedValue(mockResponse);