Compare commits

..

3 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
4 changed files with 209 additions and 259 deletions

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

@@ -1,259 +0,0 @@
import { describe, test, expect } from "bun:test";
import { checkContainsTrigger } from "../trigger";
import type { ParsedGitHubContext } from "../../context";
describe("Trigger Validation", () => {
const createMockContext = (overrides = {}): ParsedGitHubContext => ({
eventName: "issue_comment",
eventAction: "created",
repository: {
owner: "test-owner",
repo: "test-repo",
full_name: "test-owner/test-repo",
},
actor: "testuser",
entityNumber: 42,
isPR: false,
runId: "test-run-id",
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
prompt: "",
trackProgress: false,
},
payload: {
comment: {
body: "Test comment",
id: 12345,
},
},
...overrides,
} as ParsedGitHubContext);
describe("checkContainsTrigger", () => {
test("should detect @claude mentions", () => {
const context = createMockContext({
payload: {
comment: { body: "Hey @claude can you fix this?", id: 12345 },
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should detect Claude mentions case-insensitively", () => {
// Testing multiple case variations
const contexts = [
createMockContext({
payload: { comment: { body: "Hey @Claude please help", id: 12345 } },
}),
createMockContext({
payload: { comment: { body: "Hey @CLAUDE please help", id: 12345 } },
}),
createMockContext({
payload: { comment: { body: "Hey @ClAuDe please help", id: 12345 } },
}),
];
// Note: The actual function is case-sensitive, it looks for exact match
contexts.forEach(context => {
const result = checkContainsTrigger(context);
expect(result).toBe(false); // @claude is case-sensitive
});
});
test("should not trigger on partial matches", () => {
const context = createMockContext({
payload: {
comment: { body: "Emailed @claudette about this", id: 12345 },
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(false);
});
test("should handle claude mentions in code blocks", () => {
// Testing mentions inside code blocks - they SHOULD trigger
// The regex checks for word boundaries, not markdown context
const context = createMockContext({
payload: {
comment: {
body: "Here's an example:\n```\n@claude fix this\n```",
id: 12345
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true); // Mentions in code blocks do trigger
});
test("should detect trigger in issue body for opened events", () => {
const context = createMockContext({
eventName: "issues",
eventAction: "opened",
payload: {
action: "opened",
issue: {
body: "@claude implement this feature",
title: "New feature",
number: 42
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should handle multiple mentions", () => {
const context = createMockContext({
payload: {
comment: { body: "@claude and @claude should both work", id: 12345 },
},
});
// Multiple mentions in same comment should trigger (only needs one match)
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should handle null/undefined comment body", () => {
const contextNull = createMockContext({
payload: { comment: { body: null } },
});
const contextUndefined = createMockContext({
payload: { comment: { body: undefined } },
});
expect(checkContainsTrigger(contextNull)).toBe(false);
expect(checkContainsTrigger(contextUndefined)).toBe(false);
});
test("should handle empty comment body", () => {
const context = createMockContext({
payload: { comment: { body: "" } },
});
const result = checkContainsTrigger(context);
expect(result).toBe(false);
});
test("should detect trigger in pull request body", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
body: "@claude please review this PR",
title: "Feature update",
number: 42
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should detect trigger in pull request review", () => {
const context = createMockContext({
eventName: "pull_request_review",
eventAction: "submitted",
isPR: true,
payload: {
action: "submitted",
review: {
body: "@claude can you fix this issue?",
id: 999
},
pull_request: {
number: 42
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should detect trigger when assigned to specified user", () => {
const context = createMockContext({
eventName: "issues",
eventAction: "assigned",
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "claude-bot",
labelTrigger: "",
prompt: "",
trackProgress: false,
},
payload: {
action: "assigned",
issue: {
number: 42,
body: "Some issue",
title: "Title"
},
assignee: {
login: "claude-bot"
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should detect trigger when labeled with specified label", () => {
const context = createMockContext({
eventName: "issues",
eventAction: "labeled",
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "needs-claude",
prompt: "",
trackProgress: false,
},
payload: {
action: "labeled",
issue: {
number: 42,
body: "Some issue",
title: "Title"
},
label: {
name: "needs-claude"
},
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
test("should always trigger when prompt is provided", () => {
const context = createMockContext({
inputs: {
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
prompt: "Fix all the bugs",
trackProgress: false,
},
payload: {
comment: { body: "No trigger phrase here", id: 12345 },
},
});
const result = checkContainsTrigger(context);
expect(result).toBe(true);
});
});
});

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)", () => {