Merge remote-tracking branch 'origin/main' into support-bot-user

This commit is contained in:
Yuku Kotani
2025-08-06 12:37:39 +09:00
56 changed files with 3400 additions and 1164 deletions

View File

@@ -3,13 +3,37 @@
import { describe, test, expect } from "bun:test";
import {
generatePrompt,
generateDefaultPrompt,
getEventTypeAndContext,
buildAllowedToolsString,
buildDisallowedToolsString,
} 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",
@@ -133,7 +157,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
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>");
@@ -161,7 +185,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>PR_REVIEW</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
@@ -187,7 +211,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_CREATED</event_type>");
expect(prompt).toContain(
@@ -215,7 +239,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_ASSIGNED</event_type>");
expect(prompt).toContain(
@@ -242,7 +266,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>ISSUE_LABELED</event_type>");
expect(prompt).toContain(
@@ -269,13 +293,13 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
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(
"DIRECT INSTRUCTION: A direct instruction was provided and is shown in the <direct_prompt> tag above",
"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.",
);
});
@@ -292,7 +316,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
expect(prompt).toContain("<event_type>PULL_REQUEST</event_type>");
expect(prompt).toContain("<is_pr>true</is_pr>");
@@ -317,11 +341,153 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
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",
@@ -339,7 +505,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
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
@@ -361,7 +527,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -392,7 +558,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain Issue-specific instructions
expect(prompt).toContain(
@@ -431,7 +597,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain the actual branch name with timestamp
expect(prompt).toContain(
@@ -461,7 +627,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain branch-specific instructions like issues
expect(prompt).toContain(
@@ -499,7 +665,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push");
@@ -530,7 +696,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -558,7 +724,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -586,7 +752,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should contain new branch instructions
expect(prompt).toContain(
@@ -610,7 +776,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, false);
const prompt = generatePrompt(envVars, mockGitHubData, false, mockTagMode);
// Should have git command instructions
expect(prompt).toContain("Use git commands via the Bash tool");
@@ -639,7 +805,7 @@ describe("generatePrompt", () => {
},
};
const prompt = generatePrompt(envVars, mockGitHubData, true);
const prompt = generatePrompt(envVars, mockGitHubData, true, mockTagMode);
// Should have commit signing tool instructions
expect(prompt).toContain("mcp__github_file_ops__commit_files");

View File

@@ -252,6 +252,63 @@ describe("formatComments", () => {
`[user1 at 2023-01-01T00:00:00Z]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
);
});
test("filters out minimized comments", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Normal comment",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: false,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
{
id: "3",
databaseId: "100003",
body: "Another normal comment",
author: { login: "user3" },
createdAt: "2023-01-03T00:00:00Z",
},
];
const result = formatComments(comments);
expect(result).toBe(
`[user1 at 2023-01-01T00:00:00Z]: Normal comment\n\n[user3 at 2023-01-03T00:00:00Z]: Another normal comment`,
);
});
test("returns empty string when all comments are minimized", () => {
const comments: GitHubComment[] = [
{
id: "1",
databaseId: "100001",
body: "Minimized comment 1",
author: { login: "user1" },
createdAt: "2023-01-01T00:00:00Z",
isMinimized: true,
},
{
id: "2",
databaseId: "100002",
body: "Minimized comment 2",
author: { login: "user2" },
createdAt: "2023-01-02T00:00:00Z",
isMinimized: true,
},
];
const result = formatComments(comments);
expect(result).toBe("");
});
});
describe("formatReviewComments", () => {
@@ -517,6 +574,159 @@ describe("formatReviewComments", () => {
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body\n [Comment on src/index.ts:42]: Image: ![](https://github.com/user-attachments/assets/test.png)`,
);
});
test("filters out minimized review comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review with mixed comments",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Normal review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized review comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
{
id: "comment3",
databaseId: "200003",
body: "Another normal comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/main.ts",
line: 10,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview with mixed comments\n [Comment on src/index.ts:42]: Normal review comment\n [Comment on src/main.ts:10]: Another normal comment`,
);
});
test("returns review with only body when all review comments are minimized", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "Review body only",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Minimized comment 1",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: true,
},
{
id: "comment2",
databaseId: "200002",
body: "Minimized comment 2",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nReview body only`,
);
});
test("handles multiple reviews with mixed minimized comments", () => {
const reviewData = {
nodes: [
{
id: "review1",
databaseId: "300001",
author: { login: "reviewer1" },
body: "First review",
state: "APPROVED",
submittedAt: "2023-01-01T00:00:00Z",
comments: {
nodes: [
{
id: "comment1",
databaseId: "200001",
body: "Good comment",
author: { login: "reviewer1" },
createdAt: "2023-01-01T00:00:00Z",
path: "src/index.ts",
line: 42,
isMinimized: false,
},
],
},
},
{
id: "review2",
databaseId: "300002",
author: { login: "reviewer2" },
body: "Second review",
state: "COMMENTED",
submittedAt: "2023-01-02T00:00:00Z",
comments: {
nodes: [
{
id: "comment2",
databaseId: "200002",
body: "Spam comment",
author: { login: "reviewer2" },
createdAt: "2023-01-02T00:00:00Z",
path: "src/utils.ts",
line: 15,
isMinimized: true,
},
],
},
},
],
};
const result = formatReviewComments(reviewData);
expect(result).toBe(
`[Review by reviewer1 at 2023-01-01T00:00:00Z]: APPROVED\nFirst review\n [Comment on src/index.ts:42]: Good comment\n\n[Review by reviewer2 at 2023-01-02T00:00:00Z]: COMMENTED\nSecond review`,
);
});
});
describe("formatChangedFiles", () => {

View File

@@ -28,7 +28,7 @@ if __name__ == "__main__":
print(result)
```
*Token usage: 100 input, 75 output*
*Token usage: 150 input, 75 output*
---
@@ -47,7 +47,7 @@ I can see the debug print statement that needs to be removed. Let me fix this by
**→** File successfully edited. The debug print statement has been removed.
*Token usage: 200 input, 50 output*
*Token usage: 300 input, 50 output*
---
@@ -70,7 +70,7 @@ Perfect! I've successfully removed the debug print statement from the function.
**→** Successfully posted review comment to PR #123
*Token usage: 150 input, 80 output*
*Token usage: 225 input, 80 output*
---
@@ -82,7 +82,7 @@ Great! I've successfully completed the requested task:
The debug print statement has been removed as requested by the reviewers.
*Token usage: 180 input, 60 output*
*Token usage: 270 input, 60 output*
---
@@ -91,5 +91,3 @@ The debug print statement has been removed as requested by the reviewers.
Successfully removed debug print statement from file and added review comment to document the change.
**Cost:** $0.0347 | **Duration:** 18.8s

View File

@@ -24,6 +24,7 @@ describe("prepareMcpConfig", () => {
entityNumber: 123,
isPR: false,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
@@ -31,6 +32,7 @@ describe("prepareMcpConfig", () => {
disallowedTools: [],
customInstructions: "",
directPrompt: "",
overridePrompt: "",
branchPrefix: "",
useStickyComment: false,
additionalPermissions: new Map(),
@@ -546,8 +548,8 @@ describe("prepareMcpConfig", () => {
});
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 oldEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const contextWithPermissions = {
...mockPRContext,
@@ -574,7 +576,7 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_ci.env.PR_NUMBER).toBe("456");
expect(parsed.mcpServers.github_file_ops).toBeDefined();
process.env.ACTIONS_TOKEN = oldEnv;
process.env.DEFAULT_WORKFLOW_TOKEN = oldEnv;
});
test("should not include github_ci server when context.isPR is false", async () => {
@@ -594,8 +596,8 @@ describe("prepareMcpConfig", () => {
});
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 oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const result = await prepareMcpConfig({
githubToken: "test-token",
@@ -611,12 +613,12 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_ci).not.toBeDefined();
expect(parsed.mcpServers.github_file_ops).toBeDefined();
process.env.ACTIONS_TOKEN = oldTokenEnv;
process.env.DEFAULT_WORKFLOW_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 oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "workflow-token";
const contextWithPermissions = {
...mockPRContext,
@@ -643,12 +645,12 @@ describe("prepareMcpConfig", () => {
expect(parsed.mcpServers.github_ci).toBeDefined();
expect(parsed.mcpServers.github_ci.env.GITHUB_TOKEN).toBe("workflow-token");
process.env.ACTIONS_TOKEN = oldTokenEnv;
process.env.DEFAULT_WORKFLOW_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 oldTokenEnv = process.env.DEFAULT_WORKFLOW_TOKEN;
process.env.DEFAULT_WORKFLOW_TOKEN = "invalid-token";
const contextWithPermissions = {
...mockPRContext,
@@ -676,6 +678,6 @@ describe("prepareMcpConfig", () => {
),
);
process.env.ACTIONS_TOKEN = oldTokenEnv;
process.env.DEFAULT_WORKFLOW_TOKEN = oldTokenEnv;
});
});

View File

@@ -1,4 +1,7 @@
import type { ParsedGitHubContext } from "../src/github/context";
import type {
ParsedGitHubContext,
AutomationContext,
} from "../src/github/context";
import type {
IssuesEvent,
IssueCommentEvent,
@@ -8,6 +11,7 @@ import type {
} from "@octokit/webhooks-types";
const defaultInputs = {
mode: "tag" as const,
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
@@ -16,6 +20,7 @@ const defaultInputs = {
disallowedTools: [] as string[],
customInstructions: "",
directPrompt: "",
overridePrompt: "",
useBedrock: false,
useVertex: false,
timeoutMinutes: 30,
@@ -37,7 +42,7 @@ export const createMockContext = (
): ParsedGitHubContext => {
const baseContext: ParsedGitHubContext = {
runId: "1234567890",
eventName: "",
eventName: "issue_comment", // Default to a valid entity event
eventAction: "",
repository: defaultRepository,
actor: "test-actor",
@@ -54,6 +59,22 @@ export const createMockContext = (
return { ...baseContext, ...overrides };
};
export const createMockAutomationContext = (
overrides: Partial<AutomationContext> = {},
): AutomationContext => {
const baseContext: AutomationContext = {
runId: "1234567890",
eventName: "workflow_dispatch",
eventAction: undefined,
repository: defaultRepository,
actor: "test-actor",
payload: {} as any,
inputs: defaultInputs,
};
return { ...baseContext, ...overrides };
};
export const mockIssueOpenedContext: ParsedGitHubContext = {
runId: "1234567890",
eventName: "issues",

59
test/modes/agent.test.ts Normal file
View File

@@ -0,0 +1,59 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { agentMode } from "../../src/modes/agent";
import type { GitHubContext } from "../../src/github/context";
import { createMockContext, createMockAutomationContext } from "../mockContext";
describe("Agent Mode", () => {
let mockContext: GitHubContext;
beforeEach(() => {
mockContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
});
test("agent mode has correct properties", () => {
expect(agentMode.name).toBe("agent");
expect(agentMode.description).toBe(
"Automation mode for workflow_dispatch and schedule events",
);
expect(agentMode.shouldCreateTrackingComment()).toBe(false);
expect(agentMode.getAllowedTools()).toEqual([]);
expect(agentMode.getDisallowedTools()).toEqual([]);
});
test("prepareContext returns minimal data", () => {
const context = agentMode.prepareContext(mockContext);
expect(context.mode).toBe("agent");
expect(context.githubContext).toBe(mockContext);
// Agent mode doesn't use comment tracking or branch management
expect(Object.keys(context)).toEqual(["mode", "githubContext"]);
});
test("agent mode only triggers for workflow_dispatch and schedule events", () => {
// Should trigger for automation events
const workflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(true);
const scheduleContext = createMockAutomationContext({
eventName: "schedule",
});
expect(agentMode.shouldTrigger(scheduleContext)).toBe(true);
// Should NOT trigger for entity events
const entityEvents = [
"issue_comment",
"pull_request",
"pull_request_review",
"issues",
] as const;
entityEvents.forEach((eventName) => {
const context = createMockContext({ eventName });
expect(agentMode.shouldTrigger(context)).toBe(false);
});
});
});

View File

@@ -0,0 +1,80 @@
import { describe, test, expect } from "bun:test";
import { getMode, isValidMode } from "../../src/modes/registry";
import type { ModeName } from "../../src/modes/types";
import { tagMode } from "../../src/modes/tag";
import { agentMode } from "../../src/modes/agent";
import { reviewMode } from "../../src/modes/review";
import { createMockContext, createMockAutomationContext } from "../mockContext";
describe("Mode Registry", () => {
const mockContext = createMockContext({
eventName: "issue_comment",
});
const mockWorkflowDispatchContext = createMockAutomationContext({
eventName: "workflow_dispatch",
});
const mockScheduleContext = createMockAutomationContext({
eventName: "schedule",
});
test("getMode returns tag mode for standard events", () => {
const mode = getMode("tag", mockContext);
expect(mode).toBe(tagMode);
expect(mode.name).toBe("tag");
});
test("getMode returns agent mode", () => {
const mode = getMode("agent", mockContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode returns experimental-review mode", () => {
const mode = getMode("experimental-review", mockContext);
expect(mode).toBe(reviewMode);
expect(mode.name).toBe("experimental-review");
});
test("getMode throws error for tag mode with workflow_dispatch event", () => {
expect(() => getMode("tag", mockWorkflowDispatchContext)).toThrow(
"Tag mode cannot handle workflow_dispatch events. Use 'agent' mode for automation events.",
);
});
test("getMode throws error for tag mode with schedule event", () => {
expect(() => getMode("tag", mockScheduleContext)).toThrow(
"Tag mode cannot handle schedule events. Use 'agent' mode for automation events.",
);
});
test("getMode allows agent mode for workflow_dispatch event", () => {
const mode = getMode("agent", mockWorkflowDispatchContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode allows agent mode for schedule event", () => {
const mode = getMode("agent", mockScheduleContext);
expect(mode).toBe(agentMode);
expect(mode.name).toBe("agent");
});
test("getMode throws error for invalid mode", () => {
const invalidMode = "invalid" as unknown as ModeName;
expect(() => getMode(invalidMode, mockContext)).toThrow(
"Invalid mode 'invalid'. Valid modes are: 'tag', 'agent', 'experimental-review'. Please check your workflow configuration.",
);
});
test("isValidMode returns true for all valid modes", () => {
expect(isValidMode("tag")).toBe(true);
expect(isValidMode("agent")).toBe(true);
expect(isValidMode("experimental-review")).toBe(true);
});
test("isValidMode returns false for invalid mode", () => {
expect(isValidMode("invalid")).toBe(false);
});
});

92
test/modes/tag.test.ts Normal file
View File

@@ -0,0 +1,92 @@
import { describe, test, expect, beforeEach } from "bun:test";
import { tagMode } from "../../src/modes/tag";
import type { ParsedGitHubContext } from "../../src/github/context";
import type { IssueCommentEvent } from "@octokit/webhooks-types";
import { createMockContext } from "../mockContext";
describe("Tag Mode", () => {
let mockContext: ParsedGitHubContext;
beforeEach(() => {
mockContext = createMockContext({
eventName: "issue_comment",
isPR: false,
});
});
test("tag mode has correct properties", () => {
expect(tagMode.name).toBe("tag");
expect(tagMode.description).toBe(
"Traditional implementation mode triggered by @claude mentions",
);
expect(tagMode.shouldCreateTrackingComment()).toBe(true);
});
test("shouldTrigger delegates to checkContainsTrigger", () => {
const contextWithTrigger = createMockContext({
eventName: "issue_comment",
isPR: false,
inputs: {
...createMockContext().inputs,
triggerPhrase: "@claude",
},
payload: {
comment: {
body: "Hey @claude, can you help?",
},
} as IssueCommentEvent,
});
expect(tagMode.shouldTrigger(contextWithTrigger)).toBe(true);
const contextWithoutTrigger = createMockContext({
eventName: "issue_comment",
isPR: false,
inputs: {
...createMockContext().inputs,
triggerPhrase: "@claude",
},
payload: {
comment: {
body: "This is just a regular comment",
},
} as IssueCommentEvent,
});
expect(tagMode.shouldTrigger(contextWithoutTrigger)).toBe(false);
});
test("prepareContext includes all required data", () => {
const data = {
commentId: 123,
baseBranch: "main",
claudeBranch: "claude/fix-bug",
};
const context = tagMode.prepareContext(mockContext, data);
expect(context.mode).toBe("tag");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBe(123);
expect(context.baseBranch).toBe("main");
expect(context.claudeBranch).toBe("claude/fix-bug");
});
test("prepareContext works without data", () => {
const context = tagMode.prepareContext(mockContext);
expect(context.mode).toBe("tag");
expect(context.githubContext).toBe(mockContext);
expect(context.commentId).toBeUndefined();
expect(context.baseBranch).toBeUndefined();
expect(context.claudeBranch).toBeUndefined();
});
test("getAllowedTools returns empty array", () => {
expect(tagMode.getAllowedTools()).toEqual([]);
});
test("getDisallowedTools returns empty array", () => {
expect(tagMode.getDisallowedTools()).toEqual([]);
});
});

View File

@@ -60,6 +60,7 @@ describe("checkWritePermissions", () => {
entityNumber: 1,
isPR: false,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
@@ -67,6 +68,7 @@ describe("checkWritePermissions", () => {
disallowedTools: [],
customInstructions: "",
directPrompt: "",
overridePrompt: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),

View File

@@ -299,15 +299,4 @@ describe("parseEnvVarsWithContext", () => {
expect(result.allowedTools).toBe("Tool1,Tool2");
});
});
test("should throw error for unsupported event type", () => {
process.env = BASE_ENV;
const unsupportedContext = createMockContext({
eventName: "unsupported_event",
eventAction: "whatever",
});
expect(() => prepareContext(unsupportedContext, "12345")).toThrow(
"Unsupported event type: unsupported_event",
);
});
});

View File

@@ -28,10 +28,12 @@ describe("checkContainsTrigger", () => {
eventName: "issues",
eventAction: "opened",
inputs: {
mode: "tag",
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "Fix the bug in the login form",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
@@ -60,10 +62,12 @@ describe("checkContainsTrigger", () => {
},
} as IssuesEvent,
inputs: {
mode: "tag",
triggerPhrase: "/claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
@@ -276,10 +280,12 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
@@ -309,10 +315,12 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
@@ -342,10 +350,12 @@ describe("checkContainsTrigger", () => {
},
} as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
@@ -469,17 +479,6 @@ describe("checkContainsTrigger", () => {
});
});
});
describe("non-matching events", () => {
it("should return false for non-matching event type", () => {
const context = createMockContext({
eventName: "push",
eventAction: "created",
payload: {} as any,
});
expect(checkContainsTrigger(context)).toBe(false);
});
});
});
describe("escapeRegExp", () => {