Compare commits

..

4 Commits

Author SHA1 Message Date
claude[bot]
66ff608953 Fix trigger validation tests
- Fixed type import: EntityContext -> ParsedGitHubContext
- Fixed mock structure to match actual function signature
- Added comprehensive test coverage for all trigger types
- Fixed payload structure for all test cases
- Added tests for PR body, PR review, assignee, and label triggers
- Fixed import to use bun:test instead of @jest/globals

Co-authored-by: kashyap murali <km-anthropic@users.noreply.github.com>
2025-09-10 05:15:02 +00:00
kashyap murali
1a6e0b6355 Update src/github/validation/__tests__/trigger.test.ts
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-09 22:11:05 -07:00
kashyap murali
7dff9f06f7 Update src/github/validation/__tests__/trigger.test.ts
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-09 22:10:56 -07:00
km-anthropic
f6b1490a80 Add trigger validation tests
- Add comprehensive test coverage for checkContainsTrigger
- Test various mention formats and edge cases
- Cover different GitHub event types
- Include code block and special character handling
2025-09-09 21:50:01 -07:00
4 changed files with 259 additions and 209 deletions

View File

@@ -1,178 +0,0 @@
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

@@ -0,0 +1,259 @@
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,10 +44,6 @@ export function detectMode(context: GitHubContext): AutoDetectedMode {
// Issue events
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
if (checkContainsTrigger(context)) {
return "tag";

View File

@@ -113,33 +113,6 @@ describe("detectMode with enhanced routing", () => {
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)", () => {