Compare commits

...

4 Commits

Author SHA1 Message Date
claude[bot]
6325e51611 fix: rewrite branch.test.ts to match actual implementation
- Remove tests for non-existent cleanupBranch function
- Fix types: use Octokits instead of Octokit, ParsedGitHubContext instead of EntityContext
- Fix assertions: test actual BranchInfo return type (baseBranch, claudeBranch, currentBranch)
- Add comprehensive test coverage for:
  - Issue branch creation (default/custom base branch, commit signing)
  - PR branch handling (open/closed/merged states)
  - Error handling (branch not found, repository fetch failures)
  - Branch naming (kubernetes-compatible, custom prefixes)
- Use proper Bun test mocking patterns instead of Jest
- Match function signatures and behavior from actual setupBranch implementation

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

Co-authored-by: kashyap murali <km-anthropic@users.noreply.github.com>
2025-09-10 04:42:56 +00:00
km-anthropic
266d8536dc Add branch operation tests
- Add comprehensive test suite for branch operations
- Cover setupBranch and cleanupBranch functions
- Include tests for PR and issue contexts
- Add error handling tests
2025-09-09 21:20:38 -07:00
km-anthropic
550d5c7843 fix: use agent mode for issues events with explicit prompts
Fixes #528 - Issues events now correctly use agent mode when an explicit
prompt is provided in the workflow YAML, matching the behavior of PR events.

Previously, issues events would always use tag mode (with tracking comments)
even when a prompt was provided, creating inconsistent behavior compared to
pull request events which correctly used agent mode for automation.

The fix adds a check for explicit prompts before checking for triggers,
ensuring consistent mode selection across all event types.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-03 18:57:11 -07:00
GitHub Actions
9365bbe4af chore: bump Claude Code version to 1.0.103 2025-09-03 23:44:36 +00:00
5 changed files with 211 additions and 2 deletions

View File

@@ -162,7 +162,7 @@ runs:
# Install Claude Code if no custom executable is provided # Install Claude Code if no custom executable is provided
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.102 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
echo "$HOME/.local/bin" >> "$GITHUB_PATH" echo "$HOME/.local/bin" >> "$GITHUB_PATH"
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"

View File

@@ -99,7 +99,7 @@ runs:
run: | run: |
if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then if [ -z "${{ inputs.path_to_claude_code_executable }}" ]; then
echo "Installing Claude Code..." echo "Installing Claude Code..."
curl -fsSL https://claude.ai/install.sh | bash -s 1.0.102 curl -fsSL https://claude.ai/install.sh | bash -s 1.0.103
else else
echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}" echo "Using custom Claude Code executable: ${{ inputs.path_to_claude_code_executable }}"
# Add the directory containing the custom executable to PATH # Add the directory containing the custom executable to PATH

View File

@@ -0,0 +1,178 @@
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
import { mock } from "bun:test";
import { setupBranch, type BranchInfo } from "../branch";
import type { Octokits } from "../../api/client";
import type { FetchDataResult } from "../../data/fetcher";
import type { ParsedGitHubContext } from "../../context";
import type { GitHubPullRequest, GitHubIssue } from "../../types";
// Mock process.exit to prevent tests from actually exiting
const mockExit = mock(() => {});
const originalExit = process.exit;
describe("setupBranch", () => {
let mockOctokits: Octokits;
let mockContext: ParsedGitHubContext;
let mockGithubData: FetchDataResult;
beforeEach(() => {
// Replace process.exit temporarily
(process as any).exit = mockExit;
mockExit.mockClear();
// Simple mock objects
mockOctokits = {
rest: {
repos: {
get: mock(() => Promise.resolve({ data: { default_branch: "main" } })),
},
git: {
getRef: mock(() => Promise.resolve({
data: { object: { sha: "abc123def456" } }
})),
},
},
graphql: mock(() => Promise.resolve({})),
} as any;
mockContext = {
repository: {
owner: "test-owner",
repo: "test-repo",
full_name: "test-owner/test-repo",
},
isPR: false,
entityNumber: 123,
inputs: {
branchPrefix: "claude/",
useCommitSigning: false,
},
} as ParsedGitHubContext;
// Default mock data for issues
mockGithubData = {
contextData: {
title: "Test Issue",
body: "Test issue body",
state: "OPEN",
} as GitHubIssue,
comments: [],
changedFiles: [],
changedFilesWithSHA: [],
reviewData: null,
};
});
afterEach(() => {
// Restore original process.exit
process.exit = originalExit;
});
describe("Issue branch creation", () => {
test("should create new branch for issue using default branch as source", async () => {
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("main");
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
expect(result.currentBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
});
test("should use provided base branch as source", async () => {
mockContext.inputs.baseBranch = "develop";
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("develop");
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
});
test("should handle commit signing mode", async () => {
mockContext.inputs.useCommitSigning = true;
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("main");
expect(result.currentBranch).toBe("main"); // Should stay on source branch
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
});
});
describe("PR branch handling", () => {
beforeEach(() => {
mockContext.isPR = true;
mockGithubData.contextData = {
title: "Test PR",
body: "Test PR body",
state: "OPEN",
baseRefName: "main",
headRefName: "feature/test",
commits: { totalCount: 5 },
} as GitHubPullRequest;
});
test("should checkout existing PR branch for open PR", async () => {
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("main");
expect(result.currentBranch).toBe("feature/test");
expect(result.claudeBranch).toBeUndefined(); // No claude branch for open PRs
});
test("should create new branch for closed PR", async () => {
const closedPR = mockGithubData.contextData as GitHubPullRequest;
closedPR.state = "CLOSED";
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("main");
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
expect(result.currentBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
});
test("should create new branch for merged PR", async () => {
const mergedPR = mockGithubData.contextData as GitHubPullRequest;
mergedPR.state = "MERGED";
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.baseBranch).toBe("main");
expect(result.claudeBranch).toMatch(/^claude\/pr-123-\d{8}-\d{4}$/);
});
});
describe("Error handling", () => {
test("should exit with code 1 when source branch doesn't exist", async () => {
mockOctokits.rest.git.getRef = mock(() => Promise.reject(new Error("Branch not found")));
await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(mockExit).toHaveBeenCalledWith(1);
});
test("should exit with code 1 when repository fetch fails", async () => {
mockOctokits.rest.repos.get = mock(() => Promise.reject(new Error("Repository not found")));
await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(mockExit).toHaveBeenCalledWith(1);
});
});
describe("Branch naming", () => {
test("should generate kubernetes-compatible branch names", async () => {
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
// Branch name should be lowercase, use hyphens, and include timestamp
expect(result.claudeBranch).toMatch(/^claude\/issue-123-\d{8}-\d{4}$/);
expect(result.claudeBranch?.length).toBeLessThanOrEqual(50);
});
test("should use custom branch prefix", async () => {
mockContext.inputs.branchPrefix = "ai/";
const result = await setupBranch(mockOctokits, mockGithubData, mockContext);
expect(result.claudeBranch).toMatch(/^ai\/issue-123-\d{8}-\d{4}$/);
});
});
});

View File

@@ -44,6 +44,10 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
// Issue events // Issue events
if (isEntityContext(context) && isIssuesEvent(context)) { if (isEntityContext(context) && isIssuesEvent(context)) {
// If prompt is provided, use agent mode (same as PR events)
if (context.inputs.prompt) {
return "agent";
}
// Check for @claude mentions or labels/assignees // Check for @claude mentions or labels/assignees
if (checkContainsTrigger(context)) { if (checkContainsTrigger(context)) {
return "tag"; return "tag";

View File

@@ -113,6 +113,33 @@ describe("detectMode with enhanced routing", () => {
expect(detectMode(context)).toBe("agent"); expect(detectMode(context)).toBe("agent");
}); });
it("should use agent mode for issues with explicit prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "Test issue" } } as any,
entityNumber: 1,
isPR: false,
inputs: { ...baseContext.inputs, prompt: "Analyze this issue" },
};
expect(detectMode(context)).toBe("agent");
});
it("should use tag mode for issues with @claude mention and no prompt", () => {
const context: GitHubContext = {
...baseContext,
eventName: "issues",
eventAction: "opened",
payload: { issue: { number: 1, body: "@claude help" } } as any,
entityNumber: 1,
isPR: false,
};
expect(detectMode(context)).toBe("tag");
});
}); });
describe("Comment Events (unchanged behavior)", () => { describe("Comment Events (unchanged behavior)", () => {