From 40b8061da5fef68fbad6f5f6cecdf9550325987b Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Sun, 26 Jul 2026 21:26:22 +0100 Subject: [PATCH] chore: format gitea branch --- README.md | 39 +- action.yml | 3 +- examples/gitea-custom-url.yml | 2 +- src/github/validation/trigger.ts | 5 +- test/branch-cleanup.test.ts | 4 +- test/comment-logic.test.ts | 3 +- test/gitea-server-url.test.ts | 34 +- test/image-downloader.test.ts | 7 +- test/permissions.test.ts | 4 +- test/prepare-context.test.ts | 6 +- test/trigger-validation.test.ts | 790 +++++++++++++++---------------- 11 files changed, 455 insertions(+), 442 deletions(-) diff --git a/README.md b/README.md index 0e7c32f..0ded3b9 100644 --- a/README.md +++ b/README.md @@ -54,24 +54,24 @@ jobs: ## Inputs -| Input | Description | Required | Default | -| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- | -| `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 | - | -| `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` | -| `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 | - | -| `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_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 | "" | -| `disallowed_tools` | Tools that Claude should never use | 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 | - | -| `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_email` | Git user.email for commits made by Claude | No | `claude@anthropic.com` | +| Input | Description | Required | Default | +| ------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------- | ---------------------- | +| `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 | - | +| `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` | +| `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 | - | +| `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_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 | "" | +| `disallowed_tools` | Tools that Claude should never use | 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 | - | +| `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_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) @@ -102,6 +102,7 @@ When running Gitea in containers, the action may generate links using internal c ``` **How it works:** + - The action first checks for `GITEA_SERVER_URL` (user-configurable) - Falls back to `GITHUB_SERVER_URL` (automatically set by Gitea Actions) - 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: 1. **Generate OAuth Token**: run the following command and follow instructions: + ``` claude setup-token ``` + 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`. diff --git a/action.yml b/action.yml index 663cd5f..ec2e069 100644 --- a/action.yml +++ b/action.yml @@ -189,7 +189,6 @@ runs: echo "$CLAUDE_DIR" >> "$GITHUB_PATH" fi # TODO pass claude_code_executable as input and use it here - - name: Run Claude Code id: claude-code if: steps.prepare.outputs.contains_trigger == 'true' @@ -225,7 +224,7 @@ runs: USE_VERTEX: ${{ inputs.use_vertex }} ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} CLAUDE_CODE_OAUTH_TOKEN: ${{ inputs.claude_code_oauth_token }} - + # New settings support SETTINGS: ${{ inputs.settings }} SYSTEM_PROMPT: ${{ inputs.system_prompt }} diff --git a/examples/gitea-custom-url.yml b/examples/gitea-custom-url.yml index 92051e7..f4ec4e0 100644 --- a/examples/gitea-custom-url.yml +++ b/examples/gitea-custom-url.yml @@ -18,6 +18,6 @@ jobs: # Set this to your public Gitea URL to override the internal container URL # This ensures that links in comments point to the correct public URL GITEA_SERVER_URL: https://gitea.example.com - + # Note: GITHUB_SERVER_URL is automatically set by Gitea Actions to the internal URL # but it will be overridden by GITEA_SERVER_URL if set above diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 94be73c..1a31eee 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -27,7 +27,10 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { } // 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 let triggerUser = assigneeTrigger?.replace(/^@/, "") || ""; const assigneeUsername = context.payload.issue.assignee?.login || ""; diff --git a/test/branch-cleanup.test.ts b/test/branch-cleanup.test.ts index 99730a3..f495c40 100644 --- a/test/branch-cleanup.test.ts +++ b/test/branch-cleanup.test.ts @@ -97,7 +97,9 @@ describe("checkAndDeleteEmptyBranch", () => { }); 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( client, "owner", diff --git a/test/comment-logic.test.ts b/test/comment-logic.test.ts index 7599715..2793557 100644 --- a/test/comment-logic.test.ts +++ b/test/comment-logic.test.ts @@ -140,8 +140,7 @@ describe("updateCommentBody", () => { it("removes old branch links from body", () => { const input = { ...baseInput, - currentBody: - `Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)` , + currentBody: `Some comment with [View branch](${BRANCH_BASE_URL}/branch-name)`, branchName: "new-branch-name", }; diff --git a/test/gitea-server-url.test.ts b/test/gitea-server-url.test.ts index fd3adb4..fb0e2b9 100644 --- a/test/gitea-server-url.test.ts +++ b/test/gitea-server-url.test.ts @@ -8,7 +8,7 @@ describe("GITEA_SERVER_URL configuration", () => { process.env = { ...originalEnv }; delete process.env.GITEA_SERVER_URL; delete process.env.GITHUB_SERVER_URL; - + // Clear module cache to force re-evaluation delete require.cache[require.resolve("../src/github/api/config")]; }); @@ -61,23 +61,35 @@ describe("GITEA_SERVER_URL configuration", () => { it("should create correct job run links with custom GITEA_SERVER_URL", async () => { process.env.GITEA_SERVER_URL = "https://gitea.example.com"; - + // Clear module cache and re-import - delete require.cache[require.resolve("../src/github/operations/comments/common")]; - const { createJobRunLink } = await import("../src/github/operations/comments/common"); - + delete require.cache[ + require.resolve("../src/github/operations/comments/common") + ]; + const { createJobRunLink } = await import( + "../src/github/operations/comments/common" + ); + 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 () => { process.env.GITEA_SERVER_URL = "https://gitea.example.com"; - + // Clear module cache and re-import - delete require.cache[require.resolve("../src/github/operations/comments/common")]; - const { createBranchLink } = await import("../src/github/operations/comments/common"); - + delete require.cache[ + require.resolve("../src/github/operations/comments/common") + ]; + const { createBranchLink } = await import( + "../src/github/operations/comments/common" + ); + 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/)", + ); }); }); diff --git a/test/image-downloader.test.ts b/test/image-downloader.test.ts index 6488e65..a8f4274 100644 --- a/test/image-downloader.test.ts +++ b/test/image-downloader.test.ts @@ -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(consoleLogSpy).toHaveBeenCalled(); diff --git a/test/permissions.test.ts b/test/permissions.test.ts index 10e244e..3113df8 100644 --- a/test/permissions.test.ts +++ b/test/permissions.test.ts @@ -52,7 +52,9 @@ describe("checkWritePermissions", () => { }); 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); expect(result).toBe(true); diff --git a/test/prepare-context.test.ts b/test/prepare-context.test.ts index c205eb4..5aad950 100644 --- a/test/prepare-context.test.ts +++ b/test/prepare-context.test.ts @@ -70,11 +70,7 @@ describe("parseEnvVarsWithContext", () => { }); test("should allow missing CLAUDE_BRANCH and omit it from event data", () => { - const result = prepareContext( - mockIssueCommentContext, - "12345", - "main", - ); + const result = prepareContext(mockIssueCommentContext, "12345", "main"); if ( result.eventData.eventName === "issue_comment" && diff --git a/test/trigger-validation.test.ts b/test/trigger-validation.test.ts index 7b67cef..4ed0578 100644 --- a/test/trigger-validation.test.ts +++ b/test/trigger-validation.test.ts @@ -417,9 +417,7 @@ describe("checkContainsTrigger", () => { 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_reviewers: [{ login: "claude", id: 1, type: "User" }], }, } as unknown as PullRequestEvent, inputs: { @@ -491,9 +489,7 @@ describe("checkContainsTrigger", () => { 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_reviewers: [{ login: "claude", id: 1, type: "User" }], }, } as unknown as PullRequestEvent, inputs: { @@ -516,426 +512,422 @@ describe("checkContainsTrigger", () => { }); }); - it("should return true when PR has trigger user as requested reviewer for synchronized event", () => { - const context = createMockContext({ - eventName: "pull_request", - eventAction: "synchronized", - isPR: true, - payload: { - action: "synchronized", - 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", - assigneeTrigger: "", - labelTrigger: "", - directPrompt: "", - overridePrompt: "", - allowedTools: [], - disallowedTools: [], - customInstructions: "", - branchPrefix: "claude/", - useStickyComment: false, - additionalPermissions: new Map(), - useCommitSigning: false, + it("should return true when PR has trigger user as requested reviewer for synchronized event", () => { + const context = createMockContext({ + eventName: "pull_request", + eventAction: "synchronized", + isPR: true, + payload: { + action: "synchronized", + 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: [], }, - }); - expect(checkContainsTrigger(context)).toBe(true); - }); - - it("should return false when PR has no matching requested reviewers", () => { - 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: "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); + } 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); }); - 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", + it("should return false when PR has no matching requested reviewers", () => { + 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: "other-reviewer", id: 2, type: "User" }, + ], + requested_teams: [], }, - } as ParsedGitHubContext; - expect(checkContainsTrigger(context)).toBe(true); + } 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 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 = { - ...mockPullRequestReviewContext, - eventAction: "dismissed", + ...baseContext, payload: { - ...mockPullRequestReviewContext.payload, - action: "dismissed", + ...baseContext.payload, review: { - ...(mockPullRequestReviewContext.payload as PullRequestReviewEvent) - .review, - body: "/claude please review this PR", + ...(baseContext.payload as PullRequestReviewEvent).review, + body: commentBody, }, }, } 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 = { - ...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); - }); + expect(checkContainsTrigger(context)).toBe(expected); }); }); - describe("pull request review_requested action", () => { - it("should return true when trigger 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: "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 handle comment trigger with punctuation", () => { + const baseContext = { + ...mockIssueCommentContext, + inputs: { + ...mockIssueCommentContext.inputs, + triggerPhrase: "@claude", + }, + }; - 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); - }); + 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 }, + ]; - it("should handle trigger phrase without @ symbol", () => { - const context = createMockContext({ - eventName: "pull_request", - eventAction: "review_requested", - isPR: true, + testCases.forEach(({ commentBody, expected }) => { + const context = { + ...baseContext, 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: [], + ...baseContext.payload, + comment: { + ...(baseContext.payload as IssueCommentEvent).comment, + body: commentBody, }, - 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); + } as ParsedGitHubContext; + expect(checkContainsTrigger(context)).toBe(expected); }); }); +}); - 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("pull request review_requested action", () => { + it("should return true when trigger 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: "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", () => { it("should escape special regex characters", () => { expect(escapeRegExp(".*+?^${}()|[]\\")).toBe(