chore: format gitea branch

This commit is contained in:
Mark Wylde
2026-07-26 21:26:22 +01:00
parent c30a6c8c23
commit 40b8061da5
11 changed files with 455 additions and 442 deletions

View File

@@ -54,24 +54,24 @@ jobs:
## Inputs ## Inputs
| Input | Description | Required | Default | | Input | Description | Required | Default |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- | | ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- |
| `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - | | `anthropic_api_key` | Anthropic API key (required for direct API, not needed for Bedrock/Vertex) | No\* | - |
| `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | - | | `claude_code_oauth_token` | Claude Code OAuth token (alternative to anthropic_api_key) | No | - |
| `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - | | `direct_prompt` | Direct prompt for Claude to execute automatically without needing a trigger (for automated workflows) | No | - |
| `timeout_minutes` | Timeout in minutes for execution | No | `30` | | `timeout_minutes` | Timeout in minutes for execution | No | `30` |
| `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - | | `gitea_token` | Gitea token for Claude to operate with. **Only include this if you're connecting a custom GitHub app of your own!** | No | - |
| `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - | | `model` | Model to use (provider-specific format required for Bedrock/Vertex) | No | - |
| `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - | | `anthropic_model` | **DEPRECATED**: Use `model` instead. Kept for backward compatibility. | No | - |
| `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` | | `use_bedrock` | Use Amazon Bedrock with OIDC authentication instead of direct Anthropic API | No | `false` |
| `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` | | `use_vertex` | Use Google Vertex AI with OIDC authentication instead of direct Anthropic API | No | `false` |
| `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" | | `allowed_tools` | Additional tools for Claude to use (the base GitHub tools will always be included) | No | "" |
| `disallowed_tools` | Tools that Claude should never use | No | "" | | `disallowed_tools` | Tools that Claude should never use | No | "" |
| `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" | | `custom_instructions` | Additional custom instructions to include in the prompt for Claude | No | "" |
| `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue and PR assignment | No | - | | `assignee_trigger` | The assignee username that triggers the action (e.g. @claude). Only used for issue and PR assignment | No | - |
| `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` | | `trigger_phrase` | The trigger phrase to look for in comments, issue/PR bodies, and issue titles | No | `@claude` |
| `claude_git_name` | Git user.name for commits made by Claude | No | `Claude` | | `claude_git_name` | Git user.name for commits made by Claude | No | `Claude` |
| `claude_git_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` | | `claude_git_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` |
\*Required when using direct Anthropic API (default and when not using Bedrock or Vertex) \*Required when using direct Anthropic API (default and when not using Bedrock or Vertex)
@@ -102,6 +102,7 @@ When running Gitea in containers, the action may generate links using internal c
``` ```
**How it works:** **How it works:**
- The action first checks for `GITEA_SERVER_URL` (user-configurable) - The action first checks for `GITEA_SERVER_URL` (user-configurable)
- Falls back to `GITHUB_SERVER_URL` (automatically set by Gitea Actions) - Falls back to `GITHUB_SERVER_URL` (automatically set by Gitea Actions)
- Uses `https://github.com` as final fallback - Uses `https://github.com` as final fallback
@@ -570,9 +571,11 @@ You can authenticate with Claude using any of these methods:
If you have access to [Claude Code](https://claude.ai/code), you can use OAuth authentication instead of an API key: If you have access to [Claude Code](https://claude.ai/code), you can use OAuth authentication instead of an API key:
1. **Generate OAuth Token**: run the following command and follow instructions: 1. **Generate OAuth Token**: run the following command and follow instructions:
``` ```
claude setup-token claude setup-token
``` ```
This will generate an OAuth token that you can use for authentication. This will generate an OAuth token that you can use for authentication.
2. **Add Token to Repository**: Add the generated token as a repository secret named `CLAUDE_CODE_OAUTH_TOKEN`. 2. **Add Token to Repository**: Add the generated token as a repository secret named `CLAUDE_CODE_OAUTH_TOKEN`.

View File

@@ -189,7 +189,6 @@ runs:
echo "$CLAUDE_DIR" >> "$GITHUB_PATH" echo "$CLAUDE_DIR" >> "$GITHUB_PATH"
fi fi
# TODO pass claude_code_executable as input and use it here # TODO pass claude_code_executable as input and use it here
- name: Run Claude Code - name: Run Claude Code
id: claude-code id: claude-code
if: steps.prepare.outputs.contains_trigger == 'true' if: steps.prepare.outputs.contains_trigger == 'true'

View File

@@ -27,7 +27,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean {
} }
// Check for assignee trigger // Check for assignee trigger
if (isIssuesEvent(context) && (context.eventAction === "assigned" || context.eventAction === "opened")) { if (
isIssuesEvent(context) &&
(context.eventAction === "assigned" || context.eventAction === "opened")
) {
// Remove @ symbol from assignee_trigger if present // Remove @ symbol from assignee_trigger if present
let triggerUser = assigneeTrigger?.replace(/^@/, "") || ""; let triggerUser = assigneeTrigger?.replace(/^@/, "") || "";
const assigneeUsername = context.payload.issue.assignee?.login || ""; const assigneeUsername = context.payload.issue.assignee?.login || "";

View File

@@ -97,7 +97,9 @@ describe("checkAndDeleteEmptyBranch", () => {
}); });
test("falls back to branch link when API call fails", async () => { test("falls back to branch link when API call fails", async () => {
const client = createMockClient({ error: Object.assign(new Error("boom"), { status: 500 }) }); const client = createMockClient({
error: Object.assign(new Error("boom"), { status: 500 }),
});
const result = await checkAndDeleteEmptyBranch( const result = await checkAndDeleteEmptyBranch(
client, client,
"owner", "owner",

View File

@@ -140,8 +140,7 @@ describe("updateCommentBody", () => {
it("removes old branch links from body", () => { it("removes old branch links from body", () => {
const input = { const input = {
...baseInput, ...baseInput,
currentBody: currentBody: `Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)`,
`Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)` ,
branchName: "new-branch-name", branchName: "new-branch-name",
}; };

View File

@@ -63,21 +63,33 @@ describe("GITEA_SERVER_URL configuration", () => {
process.env.GITEA_SERVER_URL = "https://gitea.example.com"; process.env.GITEA_SERVER_URL = "https://gitea.example.com";
// Clear module cache and re-import // Clear module cache and re-import
delete require.cache[require.resolve("../src/github/operations/comments/common")]; delete require.cache[
const { createJobRunLink } = await import("../src/github/operations/comments/common"); require.resolve("../src/github/operations/comments/common")
];
const { createJobRunLink } = await import(
"../src/github/operations/comments/common"
);
const link = createJobRunLink("owner", "repo", "123"); const link = createJobRunLink("owner", "repo", "123");
expect(link).toBe("[View job run](https://gitea.example.com/owner/repo/actions/runs/123)"); expect(link).toBe(
"[View job run](https://gitea.example.com/owner/repo/actions/runs/123)",
);
}); });
it("should create correct branch links with custom GITEA_SERVER_URL", async () => { it("should create correct branch links with custom GITEA_SERVER_URL", async () => {
process.env.GITEA_SERVER_URL = "https://gitea.example.com"; process.env.GITEA_SERVER_URL = "https://gitea.example.com";
// Clear module cache and re-import // Clear module cache and re-import
delete require.cache[require.resolve("../src/github/operations/comments/common")]; delete require.cache[
const { createBranchLink } = await import("../src/github/operations/comments/common"); require.resolve("../src/github/operations/comments/common")
];
const { createBranchLink } = await import(
"../src/github/operations/comments/common"
);
const link = createBranchLink("owner", "repo", "feature-branch"); const link = createBranchLink("owner", "repo", "feature-branch");
expect(link).toBe("\n[View branch](https://gitea.example.com/owner/repo/src/branch/feature-branch/)"); expect(link).toBe(
"\n[View branch](https://gitea.example.com/owner/repo/src/branch/feature-branch/)",
);
}); });
}); });

View File

@@ -40,7 +40,12 @@ describe("downloadCommentImages", () => {
}, },
]; ];
const result = await downloadCommentImages(noopClient, "owner", "repo", comments); const result = await downloadCommentImages(
noopClient,
"owner",
"repo",
comments,
);
expect(result.size).toBe(0); expect(result.size).toBe(0);
expect(consoleLogSpy).toHaveBeenCalled(); expect(consoleLogSpy).toHaveBeenCalled();

View File

@@ -52,7 +52,9 @@ describe("checkWritePermissions", () => {
}); });
test("returns true immediately in Gitea environments", async () => { test("returns true immediately in Gitea environments", async () => {
const client = { api: { getBaseUrl: () => "https://gitea.example.com/api/v1" } } as any; const client = {
api: { getBaseUrl: () => "https://gitea.example.com/api/v1" },
} as any;
const result = await checkWritePermissions(client, baseContext); const result = await checkWritePermissions(client, baseContext);
expect(result).toBe(true); expect(result).toBe(true);

View File

@@ -70,11 +70,7 @@ describe("parseEnvVarsWithContext", () => {
}); });
test("should allow missing CLAUDE_BRANCH and omit it from event data", () => { test("should allow missing CLAUDE_BRANCH and omit it from event data", () => {
const result = prepareContext( const result = prepareContext(mockIssueCommentContext, "12345", "main");
mockIssueCommentContext,
"12345",
"main",
);
if ( if (
result.eventData.eventName === "issue_comment" && result.eventData.eventName === "issue_comment" &&

View File

@@ -417,9 +417,7 @@ describe("checkContainsTrigger", () => {
body: "This PR fixes a bug", body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z", created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" }, user: { login: "testuser" },
requested_reviewers: [ requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
{ login: "claude", id: 1, type: "User" },
],
}, },
} as unknown as PullRequestEvent, } as unknown as PullRequestEvent,
inputs: { inputs: {
@@ -491,9 +489,7 @@ describe("checkContainsTrigger", () => {
body: "This PR fixes a bug", body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z", created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" }, user: { login: "testuser" },
requested_reviewers: [ requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
{ login: "claude", id: 1, type: "User" },
],
}, },
} as unknown as PullRequestEvent, } as unknown as PullRequestEvent,
inputs: { inputs: {
@@ -516,426 +512,422 @@ describe("checkContainsTrigger", () => {
}); });
}); });
it("should return true when PR has trigger user as requested reviewer for synchronized event", () => { it("should return true when PR has trigger user as requested reviewer for synchronized event", () => {
const context = createMockContext({ const context = createMockContext({
eventName: "pull_request", eventName: "pull_request",
eventAction: "synchronized", eventAction: "synchronized",
isPR: true, isPR: true,
payload: { payload: {
action: "synchronized", action: "synchronized",
pull_request: { pull_request: {
number: 123, number: 123,
title: "Test PR", title: "Test PR",
body: "This PR fixes a bug", body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z", created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" }, user: { login: "testuser" },
requested_reviewers: [ requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
{ login: "claude", id: 1, type: "User" }, requested_teams: [],
],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
}, },
}); } as unknown as PullRequestEvent,
expect(checkContainsTrigger(context)).toBe(true); inputs: {
}); mode: "tag",
triggerPhrase: "@claude",
it("should return false when PR has no matching requested reviewers", () => { assigneeTrigger: "",
const context = createMockContext({ labelTrigger: "",
eventName: "pull_request", directPrompt: "",
eventAction: "opened", overridePrompt: "",
isPR: true, allowedTools: [],
payload: { disallowedTools: [],
action: "opened", customInstructions: "",
pull_request: { branchPrefix: "claude/",
number: 123, useStickyComment: false,
title: "Test PR", additionalPermissions: new Map(),
body: "This PR fixes a bug", useCommitSigning: false,
created_at: "2023-01-01T00:00:00Z", },
user: { login: "testuser" },
requested_reviewers: [
{ login: "other-reviewer", id: 2, type: "User" },
],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle trigger phrase without @ symbol", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [
{ login: "claude", id: 1, type: "User" },
],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "claude", // No @ symbol
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
});
it("should handle empty requested_reviewers and requested_teams arrays", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle missing requested_reviewers and requested_teams fields", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
// requested_reviewers and requested_teams are undefined
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
}); });
expect(checkContainsTrigger(context)).toBe(true);
}); });
describe("comment trigger", () => { it("should return false when PR has no matching requested reviewers", () => {
it("should return true for issue_comment with trigger phrase", () => { const context = createMockContext({
const context = mockIssueCommentContext; eventName: "pull_request",
expect(checkContainsTrigger(context)).toBe(true); eventAction: "opened",
}); isPR: true,
payload: {
it("should return true for pull_request_review_comment with trigger phrase", () => { action: "opened",
const context = mockPullRequestReviewCommentContext; pull_request: {
expect(checkContainsTrigger(context)).toBe(true); number: 123,
}); title: "Test PR",
body: "This PR fixes a bug",
it("should return true for pull_request_review with submitted action and trigger phrase", () => { created_at: "2023-01-01T00:00:00Z",
const context = mockPullRequestReviewContext; user: { login: "testuser" },
expect(checkContainsTrigger(context)).toBe(true); requested_reviewers: [
}); { login: "other-reviewer", id: 2, type: "User" },
],
it("should return true for pull_request_review with edited action and trigger phrase", () => { requested_teams: [],
const context = {
...mockPullRequestReviewContext,
eventAction: "edited",
payload: {
...mockPullRequestReviewContext.payload,
action: "edited",
}, },
} as ParsedGitHubContext; } as unknown as PullRequestEvent,
expect(checkContainsTrigger(context)).toBe(true); inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
}); });
expect(checkContainsTrigger(context)).toBe(false);
});
it("should return false for pull_request_review with different action", () => { it("should handle trigger phrase without @ symbol", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "claude", // No @ symbol
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
});
it("should handle empty requested_reviewers and requested_teams arrays", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [],
requested_teams: [],
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle missing requested_reviewers and requested_teams fields", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "opened",
isPR: true,
payload: {
action: "opened",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
// requested_reviewers and requested_teams are undefined
},
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
});
describe("comment trigger", () => {
it("should return true for issue_comment with trigger phrase", () => {
const context = mockIssueCommentContext;
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return true for pull_request_review_comment with trigger phrase", () => {
const context = mockPullRequestReviewCommentContext;
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return true for pull_request_review with submitted action and trigger phrase", () => {
const context = mockPullRequestReviewContext;
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return true for pull_request_review with edited action and trigger phrase", () => {
const context = {
...mockPullRequestReviewContext,
eventAction: "edited",
payload: {
...mockPullRequestReviewContext.payload,
action: "edited",
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return false for pull_request_review with different action", () => {
const context = {
...mockPullRequestReviewContext,
eventAction: "dismissed",
payload: {
...mockPullRequestReviewContext.payload,
action: "dismissed",
review: {
...(mockPullRequestReviewContext.payload as PullRequestReviewEvent)
.review,
body: "/claude please review this PR",
},
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle pull_request_review with punctuation", () => {
const baseContext = {
...mockPullRequestReviewContext,
inputs: {
...mockPullRequestReviewContext.inputs,
triggerPhrase: "@claude",
},
};
const testCases = [
{ commentBody: "@claude, please review", expected: true },
{ commentBody: "@claude. fix this", expected: true },
{ commentBody: "@claude!", expected: true },
{ commentBody: "claude@example.com", expected: false },
{ commentBody: "claudette", expected: false },
];
testCases.forEach(({ commentBody, expected }) => {
const context = { const context = {
...mockPullRequestReviewContext, ...baseContext,
eventAction: "dismissed",
payload: { payload: {
...mockPullRequestReviewContext.payload, ...baseContext.payload,
action: "dismissed",
review: { review: {
...(mockPullRequestReviewContext.payload as PullRequestReviewEvent) ...(baseContext.payload as PullRequestReviewEvent).review,
.review, body: commentBody,
body: "/claude please review this PR",
}, },
}, },
} as ParsedGitHubContext; } as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(false); expect(checkContainsTrigger(context)).toBe(expected);
});
it("should handle pull_request_review with punctuation", () => {
const baseContext = {
...mockPullRequestReviewContext,
inputs: {
...mockPullRequestReviewContext.inputs,
triggerPhrase: "@claude",
},
};
const testCases = [
{ commentBody: "@claude, please review", expected: true },
{ commentBody: "@claude. fix this", expected: true },
{ commentBody: "@claude!", expected: true },
{ commentBody: "claude@example.com", expected: false },
{ commentBody: "claudette", expected: false },
];
testCases.forEach(({ commentBody, expected }) => {
const context = {
...baseContext,
payload: {
...baseContext.payload,
review: {
...(baseContext.payload as PullRequestReviewEvent).review,
body: commentBody,
},
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(expected);
});
});
it("should handle comment trigger with punctuation", () => {
const baseContext = {
...mockIssueCommentContext,
inputs: {
...mockIssueCommentContext.inputs,
triggerPhrase: "@claude",
},
};
const testCases = [
{ commentBody: "@claude, please review", expected: true },
{ commentBody: "@claude. fix this", expected: true },
{ commentBody: "@claude!", expected: true },
{ commentBody: "claude@example.com", expected: false },
{ commentBody: "claudette", expected: false },
];
testCases.forEach(({ commentBody, expected }) => {
const context = {
...baseContext,
payload: {
...baseContext.payload,
comment: {
...(baseContext.payload as IssueCommentEvent).comment,
body: commentBody,
},
},
} as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(expected);
});
}); });
}); });
describe("pull request review_requested action", () => { it("should handle comment trigger with punctuation", () => {
it("should return true when trigger user is requested as reviewer", () => { const baseContext = {
const context = createMockContext({ ...mockIssueCommentContext,
eventName: "pull_request", inputs: {
eventAction: "review_requested", ...mockIssueCommentContext.inputs,
isPR: true, triggerPhrase: "@claude",
payload: { },
action: "review_requested", };
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
requested_teams: [],
},
requested_reviewer: { login: "claude", id: 1, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
});
it("should return false when different user is requested as reviewer", () => { const testCases = [
const context = createMockContext({ { commentBody: "@claude, please review", expected: true },
eventName: "pull_request", { commentBody: "@claude. fix this", expected: true },
eventAction: "review_requested", { commentBody: "@claude!", expected: true },
isPR: true, { commentBody: "claude@example.com", expected: false },
payload: { { commentBody: "claudette", expected: false },
action: "review_requested", ];
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "john", id: 2, type: "User" }],
requested_teams: [],
},
requested_reviewer: { login: "john", id: 2, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle trigger phrase without @ symbol", () => { testCases.forEach(({ commentBody, expected }) => {
const context = createMockContext({ const context = {
eventName: "pull_request", ...baseContext,
eventAction: "review_requested",
isPR: true,
payload: { payload: {
action: "review_requested", ...baseContext.payload,
pull_request: { comment: {
number: 123, ...(baseContext.payload as IssueCommentEvent).comment,
title: "Test PR", body: commentBody,
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
requested_teams: [],
}, },
requested_reviewer: { login: "claude", id: 1, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "claude", // no @ symbol
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
}, },
}); } as ParsedGitHubContext;
expect(checkContainsTrigger(context)).toBe(true); expect(checkContainsTrigger(context)).toBe(expected);
}); });
}); });
});
describe("non-matching events", () => { describe("pull request review_requested action", () => {
it("should return false for non-matching event type", () => { it("should return true when trigger user is requested as reviewer", () => {
const context = createMockContext({ const context = createMockContext({
eventName: "push", eventName: "pull_request",
eventAction: "created", eventAction: "review_requested",
payload: {} as any, isPR: true,
}); payload: {
expect(checkContainsTrigger(context)).toBe(false); action: "review_requested",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
requested_teams: [],
},
requested_reviewer: { login: "claude", id: 1, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
}); });
expect(checkContainsTrigger(context)).toBe(true);
}); });
it("should return false when different user is requested as reviewer", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "review_requested",
isPR: true,
payload: {
action: "review_requested",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "john", id: 2, type: "User" }],
requested_teams: [],
},
requested_reviewer: { login: "john", id: 2, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "@claude",
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(false);
});
it("should handle trigger phrase without @ symbol", () => {
const context = createMockContext({
eventName: "pull_request",
eventAction: "review_requested",
isPR: true,
payload: {
action: "review_requested",
pull_request: {
number: 123,
title: "Test PR",
body: "This PR fixes a bug",
created_at: "2023-01-01T00:00:00Z",
user: { login: "testuser" },
requested_reviewers: [{ login: "claude", id: 1, type: "User" }],
requested_teams: [],
},
requested_reviewer: { login: "claude", id: 1, type: "User" },
} as unknown as PullRequestEvent,
inputs: {
mode: "tag",
triggerPhrase: "claude", // no @ symbol
assigneeTrigger: "",
labelTrigger: "",
directPrompt: "",
overridePrompt: "",
allowedTools: [],
disallowedTools: [],
customInstructions: "",
branchPrefix: "claude/",
useStickyComment: false,
additionalPermissions: new Map(),
useCommitSigning: false,
},
});
expect(checkContainsTrigger(context)).toBe(true);
});
});
describe("non-matching events", () => {
it("should return false for non-matching event type", () => {
const context = createMockContext({
eventName: "push",
eventAction: "created",
payload: {} as any,
});
expect(checkContainsTrigger(context)).toBe(false);
});
});
describe("escapeRegExp", () => { describe("escapeRegExp", () => {
it("should escape special regex characters", () => { it("should escape special regex characters", () => {
expect(escapeRegExp(".*+?^${}()|[]\\")).toBe( expect(escapeRegExp(".*+?^${}()|[]\\")).toBe(