mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-01-22 14:24:13 +08:00
Consolidates the two-step architecture (prepare → run) into a single step to eliminate file-based and output-based communication. Changes: - Add base-action/src/lib.ts with exports for main action import - Modify run-claude-sdk.ts to accept prompt string directly and return result - Add generatePromptContent() that returns prompt without file I/O - Update mode prepare() to return promptContent in result - Create src/entrypoints/run.ts as unified entry point - Update action.yml to use single Run Claude Code step - Update output references from steps.prepare to steps.claude-code Benefits: - No file I/O for prompt - data stays in memory - No step output parsing - direct function returns - Simpler debugging - single entry point - Faster execution - no subprocess overhead - Type safety - TypeScript across the boundary 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
230 lines
7.2 KiB
TypeScript
230 lines
7.2 KiB
TypeScript
import {
|
|
describe,
|
|
test,
|
|
expect,
|
|
beforeEach,
|
|
afterEach,
|
|
spyOn,
|
|
mock,
|
|
} from "bun:test";
|
|
import { agentMode } from "../../src/modes/agent";
|
|
import type { GitHubContext } from "../../src/github/context";
|
|
import { createMockContext, createMockAutomationContext } from "../mockContext";
|
|
import * as core from "@actions/core";
|
|
import * as gitConfig from "../../src/github/operations/git-config";
|
|
|
|
describe("Agent Mode", () => {
|
|
let mockContext: GitHubContext;
|
|
let exportVariableSpy: any;
|
|
let setOutputSpy: any;
|
|
let configureGitAuthSpy: any;
|
|
|
|
beforeEach(() => {
|
|
mockContext = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
exportVariableSpy = spyOn(core, "exportVariable").mockImplementation(
|
|
() => {},
|
|
);
|
|
setOutputSpy = spyOn(core, "setOutput").mockImplementation(() => {});
|
|
// Mock configureGitAuth to prevent actual git commands from running
|
|
configureGitAuthSpy = spyOn(
|
|
gitConfig,
|
|
"configureGitAuth",
|
|
).mockImplementation(async () => {
|
|
// Do nothing - prevent actual git config modifications
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
exportVariableSpy?.mockClear();
|
|
setOutputSpy?.mockClear();
|
|
configureGitAuthSpy?.mockClear();
|
|
exportVariableSpy?.mockRestore();
|
|
setOutputSpy?.mockRestore();
|
|
configureGitAuthSpy?.mockRestore();
|
|
});
|
|
|
|
test("agent mode has correct properties", () => {
|
|
expect(agentMode.name).toBe("agent");
|
|
expect(agentMode.description).toBe(
|
|
"Direct automation mode for explicit prompts",
|
|
);
|
|
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 when prompt is provided", () => {
|
|
// Should NOT trigger for automation events without prompt
|
|
const workflowDispatchContext = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
expect(agentMode.shouldTrigger(workflowDispatchContext)).toBe(false);
|
|
|
|
const scheduleContext = createMockAutomationContext({
|
|
eventName: "schedule",
|
|
});
|
|
expect(agentMode.shouldTrigger(scheduleContext)).toBe(false);
|
|
|
|
const repositoryDispatchContext = createMockAutomationContext({
|
|
eventName: "repository_dispatch",
|
|
});
|
|
expect(agentMode.shouldTrigger(repositoryDispatchContext)).toBe(false);
|
|
|
|
// Should NOT trigger for entity events without prompt
|
|
const entityEvents = [
|
|
"issue_comment",
|
|
"pull_request",
|
|
"pull_request_review",
|
|
"issues",
|
|
] as const;
|
|
|
|
entityEvents.forEach((eventName) => {
|
|
const contextNoPrompt = createMockContext({ eventName });
|
|
expect(agentMode.shouldTrigger(contextNoPrompt)).toBe(false);
|
|
});
|
|
|
|
// Should trigger for ANY event when prompt is provided
|
|
const allEvents = [
|
|
"workflow_dispatch",
|
|
"repository_dispatch",
|
|
"schedule",
|
|
"issue_comment",
|
|
"pull_request",
|
|
"pull_request_review",
|
|
"issues",
|
|
] as const;
|
|
|
|
allEvents.forEach((eventName) => {
|
|
const contextWithPrompt =
|
|
eventName === "workflow_dispatch" ||
|
|
eventName === "repository_dispatch" ||
|
|
eventName === "schedule"
|
|
? createMockAutomationContext({
|
|
eventName,
|
|
inputs: { prompt: "Do something" },
|
|
})
|
|
: createMockContext({
|
|
eventName,
|
|
inputs: { prompt: "Do something" },
|
|
});
|
|
expect(agentMode.shouldTrigger(contextWithPrompt)).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("prepare method passes through claude_args", async () => {
|
|
// Clear any previous calls before this test
|
|
exportVariableSpy.mockClear();
|
|
setOutputSpy.mockClear();
|
|
|
|
const contextWithCustomArgs = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
|
|
// Save original env vars and set test values
|
|
const originalHeadRef = process.env.GITHUB_HEAD_REF;
|
|
const originalRefName = process.env.GITHUB_REF_NAME;
|
|
delete process.env.GITHUB_HEAD_REF;
|
|
delete process.env.GITHUB_REF_NAME;
|
|
|
|
// Set CLAUDE_ARGS environment variable
|
|
process.env.CLAUDE_ARGS = "--model claude-sonnet-4 --max-turns 10";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getAuthenticated: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345 },
|
|
}),
|
|
),
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345 },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
const result = await agentMode.prepare({
|
|
context: contextWithCustomArgs,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
});
|
|
|
|
// Verify claude_args includes user args (no MCP config in agent mode without allowed tools)
|
|
const callArgs = setOutputSpy.mock.calls[0];
|
|
expect(callArgs[0]).toBe("claude_args");
|
|
expect(callArgs[1]).toBe("--model claude-sonnet-4 --max-turns 10");
|
|
expect(callArgs[1]).not.toContain("--mcp-config");
|
|
|
|
// Verify return structure - should use "main" as fallback when no env vars set
|
|
expect(result).toEqual({
|
|
commentId: undefined,
|
|
branchInfo: {
|
|
baseBranch: "main",
|
|
currentBranch: "main",
|
|
claudeBranch: undefined,
|
|
},
|
|
mcpConfig: expect.any(String),
|
|
promptContent: expect.any(String),
|
|
});
|
|
|
|
// Clean up
|
|
delete process.env.CLAUDE_ARGS;
|
|
if (originalHeadRef !== undefined)
|
|
process.env.GITHUB_HEAD_REF = originalHeadRef;
|
|
if (originalRefName !== undefined)
|
|
process.env.GITHUB_REF_NAME = originalRefName;
|
|
});
|
|
|
|
test("prepare method creates prompt file with correct content", async () => {
|
|
const contextWithPrompts = createMockAutomationContext({
|
|
eventName: "workflow_dispatch",
|
|
});
|
|
// In v1-dev, we only have the unified prompt field
|
|
contextWithPrompts.inputs.prompt = "Custom prompt content";
|
|
|
|
const mockOctokit = {
|
|
rest: {
|
|
users: {
|
|
getAuthenticated: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345 },
|
|
}),
|
|
),
|
|
getByUsername: mock(() =>
|
|
Promise.resolve({
|
|
data: { login: "test-user", id: 12345 },
|
|
}),
|
|
),
|
|
},
|
|
},
|
|
} as any;
|
|
await agentMode.prepare({
|
|
context: contextWithPrompts,
|
|
octokit: mockOctokit,
|
|
githubToken: "test-token",
|
|
});
|
|
|
|
// Note: We can't easily test file creation in this unit test,
|
|
// but we can verify the method completes without errors
|
|
// With our conditional MCP logic, agent mode with no allowed tools
|
|
// should not include any MCP config
|
|
const callArgs = setOutputSpy.mock.calls[0];
|
|
expect(callArgs[0]).toBe("claude_args");
|
|
// Should be empty or just whitespace when no MCP servers are included
|
|
expect(callArgs[1]).not.toContain("--mcp-config");
|
|
});
|
|
});
|