mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-23 06:54:13 +08:00
* feat: add agent mode for automation scenarios - Add agent mode that always triggers without checking for mentions - Implement Mode interface with support for mode-specific tool configuration - Add getAllowedTools() and getDisallowedTools() methods to Mode interface - Simplify tests by combining related test cases - Update documentation and examples to include agent mode - Fix TypeScript imports to prevent circular dependencies Agent mode is designed for automation and workflow_dispatch scenarios where Claude should always run without requiring trigger phrases. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Minor update to readme (from @main to @beta) * Since workflow_dispatch isn't in the base action, update the examples accordingly * minor formatting issue * Update to say beta instead of main * Fix missed tracking comment to be false * add schedule & workflow dispatch paths. Also make prepare logic conditional * tests * Add test workflow for workflow_dispatch functionality * Update workflow to use correct branch reference * remove test workflow dispatch file * minor lint update * update workflow dispatch agent example * minor lint update * refactor: simplify prepare logic with mode-specific implementations * ensure tag mode can't work with workflow dispatch and schedule tasks * simplify: remove workflow_dispatch/schedule from create-prompt - Remove workflow_dispatch and schedule event handling from create-prompt since agent mode doesn't use the standard prompt generation flow - Enforce mode compatibility at selection time in the registry instead of runtime validation in tag mode - Add explanatory comment in agent mode about why prompt file is needed - Update tests to reflect simplified event handling This reduces code duplication and makes the separation between tag mode (entity-based events) and agent mode (automation events) clearer. * simplify PR by making agent mode only work with workflow dispatch and schedule events * remove unnecessary changes * remove unnecessary changes from PR - Revert update-comment-link.ts changes (agent mode doesn't use this) - Revert create-initial.ts changes (agent mode doesn't create comments) - Remove unused default-branch.ts file - Revert install-mcp-server.ts changes (agent mode uses minimal MCP) These files are only used by tag mode for entity-based events, not needed for workflow_dispatch/schedule support via agent mode. * fix: handle optional entityNumber for TypeScript - Add runtime checks in files that require entityNumber - These files are only used by tag mode which always has entityNumber - Agent mode (workflow_dispatch/schedule) doesn't use these files * linting update * refactor: implement discriminated unions for GitHub contexts Split ParsedGitHubContext into entity-specific and automation contexts: - ParsedGitHubContext: For entity events (issues/PRs) with required entityNumber and isPR - AutomationContext: For workflow_dispatch/schedule events without entity fields - GitHubContext: Union type for all contexts This eliminates ~20 null checks throughout the codebase and provides better type safety. Entity-specific code paths are now guaranteed to have the required fields. Co-Authored-By: Claude <noreply@anthropic.com> * update comment * More robust type checking * refactor: improve discriminated union implementation based on review feedback - Use eventName checks instead of 'in' operator for more robust type guards - Remove unnecessary type assertions - TypeScript's control flow analysis works correctly - Remove redundant runtime checks for entityNumber and isPR - Simplify code by using context directly after type guard Co-Authored-By: Claude <noreply@anthropic.com> * some structural simplification * refactor: further simplify discriminated union implementation - Add event name constants to reduce duplication - Derive EntityEventName and AutomationEventName types from constants - Use isAutomationContext consistently in agent mode and registry - Simplify parseGitHubContext by removing redundant type assertions - Extract payload casts to variables for cleaner code Co-Authored-By: Claude <noreply@anthropic.com> * bun format * specify the type * minor linting update again --------- Co-authored-by: km-anthropic <km-anthropic@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
303 lines
9.8 KiB
TypeScript
303 lines
9.8 KiB
TypeScript
#!/usr/bin/env bun
|
|
|
|
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
import { prepareContext } from "../src/create-prompt";
|
|
import {
|
|
createMockContext,
|
|
mockIssueOpenedContext,
|
|
mockIssueAssignedContext,
|
|
mockIssueCommentContext,
|
|
mockPullRequestCommentContext,
|
|
mockPullRequestReviewContext,
|
|
mockPullRequestReviewCommentContext,
|
|
} from "./mockContext";
|
|
|
|
const BASE_ENV = {
|
|
CLAUDE_COMMENT_ID: "12345",
|
|
GITHUB_TOKEN: "test-token",
|
|
};
|
|
|
|
describe("parseEnvVarsWithContext", () => {
|
|
let originalEnv: typeof process.env;
|
|
|
|
beforeEach(() => {
|
|
originalEnv = { ...process.env };
|
|
process.env = {};
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
});
|
|
|
|
describe("issue_comment event", () => {
|
|
describe("on issue", () => {
|
|
beforeEach(() => {
|
|
process.env = {
|
|
...BASE_ENV,
|
|
BASE_BRANCH: "main",
|
|
CLAUDE_BRANCH: "claude/issue-67890-20240101-1200",
|
|
};
|
|
});
|
|
|
|
test("should parse issue_comment event correctly", () => {
|
|
const result = prepareContext(
|
|
mockIssueCommentContext,
|
|
"12345",
|
|
"main",
|
|
"claude/issue-67890-20240101-1200",
|
|
);
|
|
|
|
expect(result.repository).toBe("test-owner/test-repo");
|
|
expect(result.claudeCommentId).toBe("12345");
|
|
expect(result.triggerPhrase).toBe("@claude");
|
|
expect(result.triggerUsername).toBe("contributor-user");
|
|
expect(result.eventData.eventName).toBe("issue_comment");
|
|
expect(result.eventData.isPR).toBe(false);
|
|
if (
|
|
result.eventData.eventName === "issue_comment" &&
|
|
!result.eventData.isPR
|
|
) {
|
|
expect(result.eventData.issueNumber).toBe("55");
|
|
expect(result.eventData.commentId).toBe("12345678");
|
|
expect(result.eventData.claudeBranch).toBe(
|
|
"claude/issue-67890-20240101-1200",
|
|
);
|
|
expect(result.eventData.baseBranch).toBe("main");
|
|
expect(result.eventData.commentBody).toBe(
|
|
"@claude can you help explain how to configure the logging system?",
|
|
);
|
|
}
|
|
});
|
|
|
|
test("should throw error when CLAUDE_BRANCH is missing", () => {
|
|
expect(() =>
|
|
prepareContext(mockIssueCommentContext, "12345", "main"),
|
|
).toThrow("CLAUDE_BRANCH is required for issue_comment event");
|
|
});
|
|
|
|
test("should throw error when BASE_BRANCH is missing", () => {
|
|
expect(() =>
|
|
prepareContext(
|
|
mockIssueCommentContext,
|
|
"12345",
|
|
undefined,
|
|
"claude/issue-67890-20240101-1200",
|
|
),
|
|
).toThrow("BASE_BRANCH is required for issue_comment event");
|
|
});
|
|
});
|
|
|
|
describe("on PR", () => {
|
|
test("should parse PR issue_comment event correctly", () => {
|
|
process.env = BASE_ENV;
|
|
const result = prepareContext(mockPullRequestCommentContext, "12345");
|
|
|
|
expect(result.eventData.eventName).toBe("issue_comment");
|
|
expect(result.eventData.isPR).toBe(true);
|
|
expect(result.triggerUsername).toBe("reviewer-user");
|
|
if (
|
|
result.eventData.eventName === "issue_comment" &&
|
|
result.eventData.isPR
|
|
) {
|
|
expect(result.eventData.prNumber).toBe("789");
|
|
expect(result.eventData.commentId).toBe("87654321");
|
|
expect(result.eventData.commentBody).toBe(
|
|
"/claude please review the changes and ensure we're not introducing any new memory issues",
|
|
);
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("pull_request_review event", () => {
|
|
test("should parse pull_request_review event correctly", () => {
|
|
process.env = BASE_ENV;
|
|
const result = prepareContext(mockPullRequestReviewContext, "12345");
|
|
|
|
expect(result.eventData.eventName).toBe("pull_request_review");
|
|
expect(result.eventData.isPR).toBe(true);
|
|
expect(result.triggerUsername).toBe("senior-developer");
|
|
if (result.eventData.eventName === "pull_request_review") {
|
|
expect(result.eventData.prNumber).toBe("321");
|
|
expect(result.eventData.commentBody).toBe(
|
|
"@claude can you check if the error handling is comprehensive enough in this PR?",
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("pull_request_review_comment event", () => {
|
|
test("should parse pull_request_review_comment event correctly", () => {
|
|
process.env = BASE_ENV;
|
|
const result = prepareContext(
|
|
mockPullRequestReviewCommentContext,
|
|
"12345",
|
|
);
|
|
|
|
expect(result.eventData.eventName).toBe("pull_request_review_comment");
|
|
expect(result.eventData.isPR).toBe(true);
|
|
expect(result.triggerUsername).toBe("code-reviewer");
|
|
if (result.eventData.eventName === "pull_request_review_comment") {
|
|
expect(result.eventData.prNumber).toBe("999");
|
|
expect(result.eventData.commentId).toBe("99988877");
|
|
expect(result.eventData.commentBody).toBe(
|
|
"/claude is this the most efficient way to implement this algorithm?",
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("issues event", () => {
|
|
beforeEach(() => {
|
|
process.env = {
|
|
...BASE_ENV,
|
|
BASE_BRANCH: "main",
|
|
CLAUDE_BRANCH: "claude/issue-42-20240101-1200",
|
|
};
|
|
});
|
|
|
|
test("should parse issue opened event correctly", () => {
|
|
const result = prepareContext(
|
|
mockIssueOpenedContext,
|
|
"12345",
|
|
"main",
|
|
"claude/issue-42-20240101-1200",
|
|
);
|
|
|
|
expect(result.eventData.eventName).toBe("issues");
|
|
expect(result.eventData.isPR).toBe(false);
|
|
expect(result.triggerUsername).toBe("john-doe");
|
|
if (
|
|
result.eventData.eventName === "issues" &&
|
|
result.eventData.eventAction === "opened"
|
|
) {
|
|
expect(result.eventData.issueNumber).toBe("42");
|
|
expect(result.eventData.baseBranch).toBe("main");
|
|
expect(result.eventData.claudeBranch).toBe(
|
|
"claude/issue-42-20240101-1200",
|
|
);
|
|
}
|
|
});
|
|
|
|
test("should parse issue assigned event correctly", () => {
|
|
const result = prepareContext(
|
|
mockIssueAssignedContext,
|
|
"12345",
|
|
"main",
|
|
"claude/issue-123-20240101-1200",
|
|
);
|
|
|
|
expect(result.eventData.eventName).toBe("issues");
|
|
expect(result.eventData.isPR).toBe(false);
|
|
expect(result.triggerUsername).toBe("jane-smith");
|
|
if (
|
|
result.eventData.eventName === "issues" &&
|
|
result.eventData.eventAction === "assigned"
|
|
) {
|
|
expect(result.eventData.issueNumber).toBe("123");
|
|
expect(result.eventData.baseBranch).toBe("main");
|
|
expect(result.eventData.claudeBranch).toBe(
|
|
"claude/issue-123-20240101-1200",
|
|
);
|
|
expect(result.eventData.assigneeTrigger).toBe("@claude-bot");
|
|
}
|
|
});
|
|
|
|
test("should throw error when CLAUDE_BRANCH is missing for issues", () => {
|
|
expect(() =>
|
|
prepareContext(mockIssueOpenedContext, "12345", "main"),
|
|
).toThrow("CLAUDE_BRANCH is required for issues event");
|
|
});
|
|
|
|
test("should throw error when BASE_BRANCH is missing for issues", () => {
|
|
expect(() =>
|
|
prepareContext(
|
|
mockIssueOpenedContext,
|
|
"12345",
|
|
undefined,
|
|
"claude/issue-42-20240101-1200",
|
|
),
|
|
).toThrow("BASE_BRANCH is required for issues event");
|
|
});
|
|
|
|
test("should allow issue assigned event with direct_prompt and no assigneeTrigger", () => {
|
|
const contextWithDirectPrompt = createMockContext({
|
|
...mockIssueAssignedContext,
|
|
inputs: {
|
|
...mockIssueAssignedContext.inputs,
|
|
assigneeTrigger: "", // No assignee trigger
|
|
directPrompt: "Please assess this issue", // But direct prompt is provided
|
|
},
|
|
});
|
|
|
|
const result = prepareContext(
|
|
contextWithDirectPrompt,
|
|
"12345",
|
|
"main",
|
|
"claude/issue-123-20240101-1200",
|
|
);
|
|
|
|
expect(result.eventData.eventName).toBe("issues");
|
|
expect(result.eventData.isPR).toBe(false);
|
|
expect(result.directPrompt).toBe("Please assess this issue");
|
|
if (
|
|
result.eventData.eventName === "issues" &&
|
|
result.eventData.eventAction === "assigned"
|
|
) {
|
|
expect(result.eventData.issueNumber).toBe("123");
|
|
expect(result.eventData.assigneeTrigger).toBeUndefined();
|
|
}
|
|
});
|
|
|
|
test("should throw error when neither assigneeTrigger nor directPrompt provided for issue assigned event", () => {
|
|
const contextWithoutTriggers = createMockContext({
|
|
...mockIssueAssignedContext,
|
|
inputs: {
|
|
...mockIssueAssignedContext.inputs,
|
|
assigneeTrigger: "", // No assignee trigger
|
|
directPrompt: "", // No direct prompt
|
|
},
|
|
});
|
|
|
|
expect(() =>
|
|
prepareContext(
|
|
contextWithoutTriggers,
|
|
"12345",
|
|
"main",
|
|
"claude/issue-123-20240101-1200",
|
|
),
|
|
).toThrow("ASSIGNEE_TRIGGER is required for issue assigned event");
|
|
});
|
|
});
|
|
|
|
describe("optional fields", () => {
|
|
test("should include custom instructions when provided", () => {
|
|
process.env = BASE_ENV;
|
|
const contextWithCustomInstructions = createMockContext({
|
|
...mockPullRequestCommentContext,
|
|
inputs: {
|
|
...mockPullRequestCommentContext.inputs,
|
|
customInstructions: "Be concise",
|
|
},
|
|
});
|
|
const result = prepareContext(contextWithCustomInstructions, "12345");
|
|
|
|
expect(result.customInstructions).toBe("Be concise");
|
|
});
|
|
|
|
test("should include allowed tools when provided", () => {
|
|
process.env = BASE_ENV;
|
|
const contextWithAllowedTools = createMockContext({
|
|
...mockPullRequestCommentContext,
|
|
inputs: {
|
|
...mockPullRequestCommentContext.inputs,
|
|
allowedTools: ["Tool1", "Tool2"],
|
|
},
|
|
});
|
|
const result = prepareContext(contextWithAllowedTools, "12345");
|
|
|
|
expect(result.allowedTools).toBe("Tool1,Tool2");
|
|
});
|
|
});
|
|
});
|