mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 22:44:13 +08:00
- Added IMPORTANT note explaining direct prompts are user instructions that take precedence - Updated the direct instruction notice to be marked as CRITICAL and HIGH PRIORITY - These changes make it clearer that direct prompts override other context
1128 lines
36 KiB
TypeScript
1128 lines
36 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { describe, test, expect } from "bun:test";
|
|
import {
|
|
generatePrompt,
|
|
getEventTypeAndContext,
|
|
buildAllowedToolsString,
|
|
buildDisallowedToolsString,
|
|
} from "../src/create-prompt";
|
|
import type { PreparedContext } from "../src/create-prompt";
|
|
|
|
describe("generatePrompt", () => {
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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);
|
|
|
|
// 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");
|
|
});
|
|
});
|