Files
claude-code-action/test/create-prompt.test.ts
Chris Lloyd a328bf4b16 feat: enforce MCP-only commits in remote agent mode for enhanced security
Remote agent mode now exclusively uses MCP tools for all commit operations,
eliminating the security risks associated with direct git command execution.

## Key Changes

### Security Enhancements
- **Removed git authentication setup**: No longer configures local git credentials
- **Eliminated dangerous git tools**: Blocked `git commit`, `git add`, `git push`, `git config`, `git rm`
- **Enforced API-based commits**: All commits go through GitHub API with proper authentication
- **Maintained read-only git access**: Preserved safe tools like `git status`, `git diff`, `git log`

### Implementation Details
- **New specialized function**: `buildRemoteAgentAllowedToolsString()` replaces general tool builder
- **Simplified system prompts**: Removed conditional logic since MCP is always used
- **Cleaner codebase**: Eliminated git configuration complexity for remote agents

### Tool Changes
**Added (always present):**
- `mcp__github_file_ops__commit_files` - Atomic multi-file commits via GitHub API
- `mcp__github_file_ops__delete_files` - File deletion via GitHub API

**Removed (security risks):**
- `Bash(git commit:*)` - Direct git commits
- `Bash(git add:*)` - Git staging
- `Bash(git push:*)` - Direct git pushes
- `Bash(git config:*)` - Git configuration
- `Bash(git rm:*)` - Git file removal

**Preserved (safe operations):**
- `Bash(git status:*)` - Repository status
- `Bash(git diff:*)` - Change inspection
- `Bash(git log:*)` - History viewing

## Testing
- Added comprehensive test suite for `buildRemoteAgentAllowedToolsString()`
- Verified security boundaries prevent dangerous tool inclusion
- Ensured custom tools and GitHub Actions integration still work
- All existing functionality preserved through MCP layer

## Benefits
- **Enhanced Security**: All commits are signed and authenticated via GitHub API
- **Consistent Attribution**: Proper commit authorship through GitHub's systems
- **Audit Trail**: Complete tracking of all repository modifications
- **Reduced Attack Surface**: No local git configuration or direct repository access

Remote agent mode is now significantly more secure while maintaining full
functionality through the existing MCP infrastructure.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-21 20:20:39 -07:00

1264 lines
42 KiB
TypeScript

#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import {
generatePrompt,
generateDefaultPrompt,
getEventTypeAndContext,
buildAllowedToolsString,
buildDisallowedToolsString,
buildRemoteAgentAllowedToolsString,
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
import type { Mode } from "../src/modes/types";
describe("generatePrompt", () => {
// Create a mock tag mode that uses the default prompt
const mockTagMode: Mode = {
name: "tag",
description: "Tag mode",
shouldTrigger: () => true,
prepareContext: (context) => ({ mode: "tag", githubContext: context }),
getAllowedTools: () => [],
getDisallowedTools: () => [],
shouldCreateTrackingComment: () => true,
generatePrompt: (context, githubData, useCommitSigning) =>
generateDefaultPrompt(context, githubData, useCommitSigning),
prepare: async () => ({
commentId: 123,
branchInfo: {
baseBranch: "main",
currentBranch: "main",
claudeBranch: undefined,
},
mcpConfig: "{}",
}),
};
const mockGitHubData = {
contextData: {
title: "Test PR",
body: "This is a test PR",
author: { login: "testuser" },
state: "OPEN",
createdAt: "2023-01-01T00:00:00Z",
additions: 15,
deletions: 5,
baseRefName: "main",
headRefName: "feature-branch",
headRefOid: "abc123",
commits: {
totalCount: 2,
nodes: [
{
commit: {
oid: "commit1",
message: "Add feature",
author: {
name: "John Doe",
email: "john@example.com",
},
},
},
],
},
files: {
nodes: [
{
path: "src/file1.ts",
additions: 10,
deletions: 5,
changeType: "MODIFIED",
},
],
},
comments: {
nodes: [
{
id: "comment1",
databaseId: "123456",
body: "First comment",
author: { login: "user1" },
createdAt: "2023-01-01T01:00:00Z",
},
],
},
reviews: {
nodes: [
{
id: "review1",
author: { login: "reviewer1" },
body: "LGTM",
state: "APPROVED",
submittedAt: "2023-01-01T02:00:00Z",
comments: {
nodes: [],
},
},
],
},
},
comments: [
{
id: "comment1",
databaseId: "123456",
body: "First comment",
author: { login: "user1" },
createdAt: "2023-01-01T01:00:00Z",
},
{
id: "comment2",
databaseId: "123457",
body: "@claude help me",
author: { login: "user2" },
createdAt: "2023-01-01T01:30:00Z",
},
],
changedFiles: [],
changedFilesWithSHA: [
{
path: "src/file1.ts",
additions: 10,
deletions: 5,
changeType: "MODIFIED",
sha: "abc123",
},
],
reviewData: {
nodes: [
{
id: "review1",
databaseId: "400001",
author: { login: "reviewer1" },
body: "LGTM",
state: "APPROVED",
submittedAt: "2023-01-01T02:00:00Z",
comments: {
nodes: [],
},
},
],
},
imageUrlMap: new Map<string, string>(),
};
test("should generate prompt for issue_comment event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: false,
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101-1200",
issueNumber: "67890",
commentBody: "@claude please fix this",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>GENERAL_COMMENT</event_type>");
expect(prompt).toContain("<is_pr>false</is_pr>");
expect(prompt).toContain(
"<trigger_context>issue comment with '@claude'</trigger_context>",
);
expect(prompt).toContain("<repository>owner/repo</repository>");
expect(prompt).toContain("<claude_comment_id>12345</claude_comment_id>");
expect(prompt).toContain("<trigger_username>Unknown</trigger_username>");
expect(prompt).toContain("[user1 at 2023-01-01T01:00:00Z]: First comment"); // from formatted comments
expect(prompt).not.toContain("filename\tstatus\tadditions\tdeletions\tsha"); // since it's not a PR
});
test("should generate prompt for pull_request_review event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_review",
isPR: true,
prNumber: "456",
commentBody: "@claude please fix this bug",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
expect(prompt).toContain("<pr_number>456</pr_number>");
expect(prompt).toContain("- src/file1.ts (MODIFIED) +10/-5 SHA: abc123"); // from formatted changed files
expect(prompt).toContain(
"[Review by reviewer1 at 2023-01-01T02:00:00Z]: APPROVED",
); // from review comments
});
test("should generate prompt for issue opened event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "789",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101-1200",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
expect(prompt).toContain(
"<trigger_context>new issue with '@claude' in body</trigger_context>",
);
expect(prompt).toContain(
"[Create a PR](https://github.com/owner/repo/compare/main",
);
expect(prompt).toContain("The target-branch should be 'main'");
});
test("should generate prompt for issue assigned event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "assigned",
isPR: false,
issueNumber: "999",
baseBranch: "develop",
claudeBranch: "claude/issue-999-20240101-1200",
assigneeTrigger: "claude-bot",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
expect(prompt).toContain(
"<trigger_context>issue assigned to 'claude-bot'</trigger_context>",
);
expect(prompt).toContain(
"[Create a PR](https://github.com/owner/repo/compare/develop",
);
});
test("should generate prompt for issue labeled event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "labeled",
isPR: false,
issueNumber: "888",
baseBranch: "main",
claudeBranch: "claude/issue-888-20240101-1200",
labelTrigger: "claude-task",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
expect(prompt).toContain(
"<trigger_context>issue labeled with 'claude-task'</trigger_context>",
);
expect(prompt).toContain(
"[Create a PR](https://github.com/owner/repo/compare/main",
);
});
test("should include direct prompt when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
directPrompt: "Fix the bug in the login form",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "789",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101-1200",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<direct_prompt>");
expect(prompt).toContain("Fix the bug in the login form");
expect(prompt).toContain("</direct_prompt>");
expect(prompt).toContain(
"CRITICAL: Direct user instructions were provided in the <direct_prompt> tag above. These are HIGH PRIORITY instructions that OVERRIDE all other context and MUST be followed exactly as written.",
);
});
test("should generate prompt for pull_request event", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request",
eventAction: "opened",
isPR: true,
prNumber: "999",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
expect(prompt).toContain("<pr_number>999</pr_number>");
expect(prompt).toContain("pull request opened");
});
test("should include custom instructions when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
customInstructions: "Always use TypeScript",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: false,
issueNumber: "123",
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101-1200",
commentBody: "@claude please fix this",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("CUSTOM INSTRUCTIONS:\nAlways use TypeScript");
});
test("should use override_prompt when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt: "Simple prompt for $REPOSITORY PR #$PR_NUMBER",
eventData: {
eventName: "pull_request",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toBe("Simple prompt for owner/repo PR #123");
expect(prompt).not.toContain("You are Claude, an AI assistant");
});
test("should substitute all variables in override_prompt", () => {
const envVars: PreparedContext = {
repository: "test/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
triggerUsername: "john-doe",
overridePrompt: `Repository: $REPOSITORY
PR: $PR_NUMBER
Title: $PR_TITLE
Body: $PR_BODY
Comments: $PR_COMMENTS
Review Comments: $REVIEW_COMMENTS
Changed Files: $CHANGED_FILES
Trigger Comment: $TRIGGER_COMMENT
Username: $TRIGGER_USERNAME
Branch: $BRANCH_NAME
Base: $BASE_BRANCH
Event: $EVENT_TYPE
Is PR: $IS_PR`,
eventData: {
eventName: "pull_request_review_comment",
isPR: true,
prNumber: "456",
commentBody: "Please review this code",
claudeBranch: "feature-branch",
baseBranch: "main",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("Repository: test/repo");
expect(prompt).toContain("PR: 456");
expect(prompt).toContain("Title: Test PR");
expect(prompt).toContain("Body: This is a test PR");
expect(prompt).toContain("Comments: ");
expect(prompt).toContain("Review Comments: ");
expect(prompt).toContain("Changed Files: ");
expect(prompt).toContain("Trigger Comment: Please review this code");
expect(prompt).toContain("Username: john-doe");
expect(prompt).toContain("Branch: feature-branch");
expect(prompt).toContain("Base: main");
expect(prompt).toContain("Event: pull_request_review_comment");
expect(prompt).toContain("Is PR: true");
});
test("should handle override_prompt for issues", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt: "Issue #$ISSUE_NUMBER: $ISSUE_TITLE in $REPOSITORY",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "789",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101-1200",
},
};
const issueGitHubData = {
...mockGitHubData,
contextData: {
title: "Bug: Login form broken",
body: "The login form is not working",
author: { login: "testuser" },
state: "OPEN",
createdAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [],
},
},
};
const prompt = generatePrompt(envVars, issueGitHubData, false, mockTagMode);
expect(prompt).toBe("Issue #789: Bug: Login form broken in owner/repo");
});
test("should handle empty values in override_prompt substitution", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
overridePrompt:
"PR: $PR_NUMBER, Issue: $ISSUE_NUMBER, Comment: $TRIGGER_COMMENT",
eventData: {
eventName: "pull_request",
eventAction: "opened",
isPR: true,
prNumber: "123",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toBe("PR: 123, Issue: , Comment: ");
});
test("should not substitute variables when override_prompt is not provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "123",
baseBranch: "main",
claudeBranch: "claude/issue-123-20240101-1200",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("You are Claude, an AI assistant");
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
});
test("should include trigger username when provided", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
triggerUsername: "johndoe",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: false,
issueNumber: "123",
baseBranch: "main",
claudeBranch: "claude/issue-67890-20240101-1200",
commentBody: "@claude please fix this",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<trigger_username>johndoe</trigger_username>");
// With commit signing disabled, co-author info appears in git commit instructions
expect(prompt).toContain(
"Co-authored-by: johndoe <johndoe@users.noreply.github.com>",
);
});
test("should include PR-specific instructions only for PR events", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_review",
isPR: true,
prNumber: "456",
commentBody: "@claude please fix this",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
// Should NOT contain Issue-specific instructions
expect(prompt).not.toContain("You are already on the correct branch (");
expect(prompt).not.toContain(
"IMPORTANT: You are already on the correct branch (",
);
expect(prompt).not.toContain("Create a PR](https://github.com/");
});
test("should include Issue-specific instructions only for Issue events", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "opened",
isPR: false,
issueNumber: "789",
baseBranch: "main",
claudeBranch: "claude/issue-789-20240101-1200",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain Issue-specific instructions
expect(prompt).toContain(
"You are already on the correct branch (claude/issue-789-20240101-1200)",
);
expect(prompt).toContain(
"IMPORTANT: You are already on the correct branch (claude/issue-789-20240101-1200)",
);
expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain(
"If you created anything in your branch, your comment must include the PR URL",
);
// Should NOT contain PR-specific instructions
expect(prompt).not.toContain(
"Push directly using mcp__github_file_ops__commit_files to the existing branch",
);
expect(prompt).not.toContain(
"Always push to the existing branch when triggered on a PR",
);
});
test("should use actual branch name for issue comments", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: false,
issueNumber: "123",
baseBranch: "main",
claudeBranch: "claude/issue-123-20240101-1200",
commentBody: "@claude please fix this",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain the actual branch name with timestamp
expect(prompt).toContain(
"You are already on the correct branch (claude/issue-123-20240101-1200)",
);
expect(prompt).toContain(
"IMPORTANT: You are already on the correct branch (claude/issue-123-20240101-1200)",
);
expect(prompt).toContain(
"The branch-name is the current branch: claude/issue-123-20240101-1200",
);
});
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-1200",
baseBranch: "main",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain branch-specific instructions like issues
expect(prompt).toContain(
"You are already on the correct branch (claude/pr-456-20240101-1200)",
);
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-1200",
);
expect(prompt).toContain("Reference to the original PR");
expect(prompt).toContain(
"If you created anything in your branch, your comment must include the PR URL",
);
// Should NOT contain open PR instructions
expect(prompt).not.toContain(
"Push directly using mcp__github_file_ops__commit_files to the existing branch",
);
});
test("should handle open PR without 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",
// No claudeBranch or baseBranch for open PRs
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
// Should NOT contain new branch instructions
expect(prompt).not.toContain("Create a PR](https://github.com/");
expect(prompt).not.toContain("You are already on the correct branch");
expect(prompt).not.toContain(
"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-1230",
baseBranch: "develop",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
"You are already on the correct branch (claude/pr-789-20240101-1230)",
);
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-1400",
baseBranch: "main",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
"You are already on the correct branch (claude/pr-999-20240101-1400)",
);
expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain("Reference to the original PR");
expect(prompt).toContain(
"If you created anything in your branch, your comment must include the PR URL",
);
});
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-1500",
baseBranch: "main",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
"You are already on the correct branch (claude/pr-555-20240101-1500)",
);
expect(prompt).toContain("Create a PR](https://github.com/");
expect(prompt).toContain("Reference to the original PR");
});
test("should include git commands when useCommitSigning is false", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: true,
prNumber: "123",
commentBody: "@claude fix the bug",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should have git command instructions
expect(prompt).toContain("Use git commands via the Bash tool");
expect(prompt).toContain("git add");
expect(prompt).toContain("git commit");
expect(prompt).toContain("git push");
// Should use the minimal comment tool
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tool references
expect(prompt).not.toContain("mcp__github_file_ops__commit_files");
});
test("should include commit signing tools when useCommitSigning is true", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issue_comment",
commentId: "67890",
isPR: true,
prNumber: "123",
commentBody: "@claude fix the bug",
},
};
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
// Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files");
expect(prompt).toContain("mcp__github_file_ops__delete_files");
// Comment tool should always be from comment server, not file ops
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
// Should not have git command instructions
expect(prompt).not.toContain("Use git commands via the Bash tool");
});
});
describe("getEventTypeAndContext", () => {
test("should return correct type and context for pull_request_review_comment", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "pull_request_review_comment",
isPR: true,
prNumber: "123",
commentBody: "@claude please fix this",
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("REVIEW_COMMENT");
expect(result.triggerContext).toBe("PR review comment with '@claude'");
});
test("should return correct type and context for issue assigned", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "assigned",
isPR: false,
issueNumber: "999",
baseBranch: "main",
claudeBranch: "claude/issue-999-20240101-1200",
assigneeTrigger: "claude-bot",
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("ISSUE_ASSIGNED");
expect(result.triggerContext).toBe("issue assigned to 'claude-bot'");
});
test("should return correct type and context for issue labeled", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
eventData: {
eventName: "issues",
eventAction: "labeled",
isPR: false,
issueNumber: "888",
baseBranch: "main",
claudeBranch: "claude/issue-888-20240101-1200",
labelTrigger: "claude-task",
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("ISSUE_LABELED");
expect(result.triggerContext).toBe("issue labeled with 'claude-task'");
});
test("should return correct type and context for issue assigned without assigneeTrigger", () => {
const envVars: PreparedContext = {
repository: "owner/repo",
claudeCommentId: "12345",
triggerPhrase: "@claude",
directPrompt: "Please assess this issue",
eventData: {
eventName: "issues",
eventAction: "assigned",
isPR: false,
issueNumber: "999",
baseBranch: "main",
claudeBranch: "claude/issue-999-20240101-1200",
// No assigneeTrigger when using directPrompt
},
};
const result = getEventTypeAndContext(envVars);
expect(result.eventType).toBe("ISSUE_ASSIGNED");
expect(result.triggerContext).toBe("issue assigned event");
});
});
describe("buildAllowedToolsString", () => {
test("should return correct tools for regular events (default no signing)", () => {
const result = buildAllowedToolsString();
// 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");
// Default is no commit signing, so should have specific Bash git commands
expect(result).toContain("Bash(git add:*)");
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("Bash(git push:*)");
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tools
expect(result).not.toContain("mcp__github_file_ops__commit_files");
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should return correct tools with default parameters", () => {
const result = buildAllowedToolsString([], false, false);
// 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");
// Should have specific Bash git commands for non-signing mode
expect(result).toContain("Bash(git add:*)");
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tools
expect(result).not.toContain("mcp__github_file_ops__commit_files");
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should append custom tools when provided", () => {
const customTools = ["Tool1", "Tool2", "Tool3"];
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");
});
test("should include GitHub Actions tools when includeActionsTools is true", () => {
const result = buildAllowedToolsString([], true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
// GitHub Actions tools should be included
expect(result).toContain("mcp__github_ci__get_ci_status");
expect(result).toContain("mcp__github_ci__get_workflow_run_details");
expect(result).toContain("mcp__github_ci__download_job_log");
});
test("should include both custom and Actions tools when both provided", () => {
const customTools = ["Tool1", "Tool2"];
const result = buildAllowedToolsString(customTools, true);
// Base tools should be present
expect(result).toContain("Edit");
// Custom tools should be included
expect(result).toContain("Tool1");
expect(result).toContain("Tool2");
// GitHub Actions tools should be included
expect(result).toContain("mcp__github_ci__get_ci_status");
expect(result).toContain("mcp__github_ci__get_workflow_run_details");
expect(result).toContain("mcp__github_ci__download_job_log");
});
test("should include commit signing tools when useCommitSigning is true", () => {
const result = buildAllowedToolsString([], false, true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
// Commit signing tools should be included
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
// Comment tool should always be from github_comment server
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Bash should NOT be included when using commit signing (except in comment tool name)
expect(result).not.toContain("Bash(");
});
test("should include specific Bash git commands when useCommitSigning is false", () => {
const result = buildAllowedToolsString([], false, false);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
// Specific Bash git commands should be included
expect(result).toContain("Bash(git add:*)");
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("Bash(git push:*)");
expect(result).toContain("Bash(git status:*)");
expect(result).toContain("Bash(git diff:*)");
expect(result).toContain("Bash(git log:*)");
expect(result).toContain("Bash(git rm:*)");
expect(result).toContain("Bash(git config user.name:*)");
expect(result).toContain("Bash(git config user.email:*)");
// Comment tool from minimal server should be included
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Commit signing tools should NOT be included
expect(result).not.toContain("mcp__github_file_ops__commit_files");
expect(result).not.toContain("mcp__github_file_ops__delete_files");
});
test("should handle all combinations of options", () => {
const customTools = ["CustomTool1", "CustomTool2"];
const result = buildAllowedToolsString(customTools, true, false);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Bash(git add:*)");
// Custom tools should be included
expect(result).toContain("CustomTool1");
expect(result).toContain("CustomTool2");
// GitHub Actions tools should be included
expect(result).toContain("mcp__github_ci__get_ci_status");
// Comment tool from minimal server should be included
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Commit signing tools should NOT be included
expect(result).not.toContain("mcp__github_file_ops__commit_files");
});
});
describe("buildDisallowedToolsString", () => {
test("should return base disallowed tools when no custom tools provided", () => {
const result = buildDisallowedToolsString();
// The base disallowed tools should be in the result
expect(result).toContain("WebSearch");
expect(result).toContain("WebFetch");
});
test("should append custom disallowed tools when provided", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const result = buildDisallowedToolsString(customDisallowedTools);
// Base disallowed tools should be present
expect(result).toContain("WebSearch");
// Custom disallowed tools should be appended
expect(result).toContain("BadTool1");
expect(result).toContain("BadTool2");
// Verify format with comma separation
const parts = result.split(",");
expect(parts).toContain("WebSearch");
expect(parts).toContain("BadTool1");
expect(parts).toContain("BadTool2");
});
test("should remove hardcoded disallowed tools if they are in allowed tools", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "SomeOtherTool"];
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,
);
// WebSearch should be removed from disallowed since it's in allowed
expect(result).not.toContain("WebSearch");
// WebFetch should still be disallowed since it's not in allowed
expect(result).toContain("WebFetch");
// Custom disallowed tools should still be present
expect(result).toContain("BadTool1");
expect(result).toContain("BadTool2");
});
test("should remove all hardcoded disallowed tools if they are all in allowed tools", () => {
const allowedTools = ["WebSearch", "WebFetch", "SomeOtherTool"];
const result = buildDisallowedToolsString(undefined, allowedTools);
// Both hardcoded disallowed tools should be removed
expect(result).not.toContain("WebSearch");
expect(result).not.toContain("WebFetch");
// Result should be empty since no custom disallowed tools provided
expect(result).toBe("");
});
test("should handle custom disallowed tools when all hardcoded tools are overridden", () => {
const customDisallowedTools = ["BadTool1", "BadTool2"];
const allowedTools = ["WebSearch", "WebFetch"];
const result = buildDisallowedToolsString(
customDisallowedTools,
allowedTools,
);
// Hardcoded tools should be removed
expect(result).not.toContain("WebSearch");
expect(result).not.toContain("WebFetch");
// Only custom disallowed tools should remain
expect(result).toBe("BadTool1,BadTool2");
});
});
describe("buildRemoteAgentAllowedToolsString", () => {
test("should return correct tools for remote agent mode (always uses commit signing)", () => {
const result = buildRemoteAgentAllowedToolsString();
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
// Comment tool should always be included
expect(result).toContain("mcp__github_comment__update_claude_comment");
// MCP commit signing tools should always be included
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
// Safe git tools should be included
expect(result).toContain("Bash(git status:*)");
expect(result).toContain("Bash(git diff:*)");
expect(result).toContain("Bash(git log:*)");
// Dangerous git tools should NOT be included
expect(result).not.toContain("Bash(git commit:*)");
expect(result).not.toContain("Bash(git add:*)");
expect(result).not.toContain("Bash(git push:*)");
expect(result).not.toContain("Bash(git config");
expect(result).not.toContain("Bash(git rm:*)");
});
test("should include custom tools when provided", () => {
const customTools = ["CustomTool1", "CustomTool2"];
const result = buildRemoteAgentAllowedToolsString(customTools);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
// Custom tools should be included
expect(result).toContain("CustomTool1");
expect(result).toContain("CustomTool2");
// MCP commit signing tools should still be included
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
// Dangerous git tools should still NOT be included
expect(result).not.toContain("Bash(git commit:*)");
expect(result).not.toContain("Bash(git config");
});
test("should include GitHub Actions tools when includeActionsTools is true", () => {
const result = buildRemoteAgentAllowedToolsString([], true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Glob");
// GitHub Actions tools should be included
expect(result).toContain("mcp__github_ci__get_ci_status");
expect(result).toContain("mcp__github_ci__get_workflow_run_details");
expect(result).toContain("mcp__github_ci__download_job_log");
// MCP commit signing tools should still be included
expect(result).toContain("mcp__github_file_ops__commit_files");
expect(result).toContain("mcp__github_file_ops__delete_files");
// Dangerous git tools should still NOT be included
expect(result).not.toContain("Bash(git commit:*)");
expect(result).not.toContain("Bash(git config");
});
test("should include both custom and Actions tools when both provided", () => {
const customTools = ["CustomTool1"];
const result = buildRemoteAgentAllowedToolsString(customTools, true);
// Base tools should be present
expect(result).toContain("Edit");
// Custom tools should be included
expect(result).toContain("CustomTool1");
// GitHub Actions tools should be included
expect(result).toContain("mcp__github_ci__get_ci_status");
// MCP commit signing tools should still be included
expect(result).toContain("mcp__github_file_ops__commit_files");
// Dangerous git tools should still NOT be included
expect(result).not.toContain("Bash(git commit:*)");
expect(result).not.toContain("Bash(git config");
});
test("should never include dangerous git tools regardless of parameters", () => {
const dangerousCustomTools = ["Bash(git commit:*)", "Bash(git config:*)"];
const result = buildRemoteAgentAllowedToolsString(dangerousCustomTools, true);
// The function should still include dangerous tools if explicitly provided in custom tools
// This is by design - if someone explicitly adds them, they should be included
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("Bash(git config:*)");
// But the base function should not add them automatically
const resultWithoutCustom = buildRemoteAgentAllowedToolsString([], true);
expect(resultWithoutCustom).not.toContain("Bash(git commit:*)");
expect(resultWithoutCustom).not.toContain("Bash(git config");
});
});