From 5dea88db4c871e1e4f4c40be487f826b0ce5411e Mon Sep 17 00:00:00 2001 From: Maxwell Calkin Date: Sun, 8 Mar 2026 11:52:00 -0400 Subject: [PATCH] fix: allow trigger_phrase after punctuation, not just whitespace (#941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The trigger phrase regex used `(^|\s)` as the leading boundary, which only matched after start-of-string or whitespace. This caused silent failures when users wrote the trigger phrase after common punctuation like parentheses, quotes, angle brackets, or colons — e.g. `(@claude)`, `"@claude"`, `>@claude`, `cc:@claude`. Replace `(^|\s)` with `(? --- src/github/validation/trigger.ts | 16 +-- test/trigger-validation.test.ts | 163 +++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+), 8 deletions(-) diff --git a/src/github/validation/trigger.ts b/src/github/validation/trigger.ts index 74b385d8d..1f7e6159b 100644 --- a/src/github/validation/trigger.ts +++ b/src/github/validation/trigger.ts @@ -48,9 +48,9 @@ export function checkContainsTrigger(context: ParsedGitHubContext): boolean { if (isIssuesEvent(context) && context.eventAction === "opened") { const issueBody = context.payload.issue.body || ""; const issueTitle = context.payload.issue.title || ""; - // Check for exact match with word boundaries or punctuation + // Check for exact match - trigger phrase must not be preceded by a word character const regex = new RegExp( - `(^|\\s)${escapeRegExp(triggerPhrase)}([\\s.,!?;:]|$)`, + `(? { expect(escapeRegExp("test[123]")).toBe("test\\[123\\]"); }); }); + +describe("trigger phrase preceded by non-whitespace characters (#941)", () => { + describe("issue body - non-whitespace leading boundaries", () => { + const baseContext = { + ...mockIssueOpenedContext, + inputs: { + ...mockIssueOpenedContext.inputs, + triggerPhrase: "@claude", + }, + }; + + function makeIssueContext(body: string): ParsedGitHubContext { + return { + ...baseContext, + payload: { + ...baseContext.payload, + issue: { + ...(baseContext.payload as IssuesEvent).issue, + body, + }, + }, + } as ParsedGitHubContext; + } + + it("should trigger when preceded by parenthesis", () => { + expect(checkContainsTrigger(makeIssueContext("(@claude) can you check?"))).toBe(true); + }); + + it("should trigger when preceded by double quote", () => { + expect(checkContainsTrigger(makeIssueContext('"@claude can you check?"'))).toBe(true); + }); + + it("should trigger when preceded by single quote", () => { + expect(checkContainsTrigger(makeIssueContext("'@claude can you check?'"))).toBe(true); + }); + + it("should trigger when preceded by angle bracket (blockquote)", () => { + expect(checkContainsTrigger(makeIssueContext(">@claude can you check?"))).toBe(true); + }); + + it("should trigger when preceded by colon", () => { + expect(checkContainsTrigger(makeIssueContext("cc:@claude please review"))).toBe(true); + }); + + it("should trigger when preceded by backtick", () => { + expect(checkContainsTrigger(makeIssueContext("`@claude`"))).toBe(true); + }); + + it("should trigger when preceded by slash", () => { + expect(checkContainsTrigger(makeIssueContext("/@claude help"))).toBe(true); + }); + + it("should trigger when preceded by hyphen/dash", () => { + expect(checkContainsTrigger(makeIssueContext("-@claude fix it"))).toBe(true); + }); + + it("should still trigger at start of string", () => { + expect(checkContainsTrigger(makeIssueContext("@claude help me"))).toBe(true); + }); + + it("should still trigger after whitespace", () => { + expect(checkContainsTrigger(makeIssueContext("hey @claude help"))).toBe(true); + }); + + it("should still reject when preceded by word characters", () => { + expect(checkContainsTrigger(makeIssueContext("email@claude.com"))).toBe(false); + }); + + it("should still reject when trigger is part of a longer word", () => { + expect(checkContainsTrigger(makeIssueContext("claudette helped"))).toBe(false); + }); + + it("should reject when preceded by digits", () => { + expect(checkContainsTrigger(makeIssueContext("123@claude"))).toBe(false); + }); + + it("should reject when preceded by underscore", () => { + expect(checkContainsTrigger(makeIssueContext("_@claude"))).toBe(false); + }); + }); + + describe("comment body - non-whitespace leading boundaries", () => { + const baseContext = { + ...mockIssueCommentContext, + inputs: { + ...mockIssueCommentContext.inputs, + triggerPhrase: "@claude", + }, + }; + + function makeCommentContext(body: string): ParsedGitHubContext { + return { + ...baseContext, + payload: { + ...baseContext.payload, + comment: { + ...(baseContext.payload as IssueCommentEvent).comment, + body, + }, + }, + } as ParsedGitHubContext; + } + + it("should trigger when preceded by parenthesis", () => { + expect(checkContainsTrigger(makeCommentContext("(@claude) can you check?"))).toBe(true); + }); + + it("should trigger when preceded by double quote", () => { + expect(checkContainsTrigger(makeCommentContext('"@claude help"'))).toBe(true); + }); + + it("should trigger when preceded by angle bracket", () => { + expect(checkContainsTrigger(makeCommentContext(">@claude review this"))).toBe(true); + }); + + it("should trigger when preceded by colon", () => { + expect(checkContainsTrigger(makeCommentContext("cc:@claude"))).toBe(true); + }); + + it("should still reject email-like patterns", () => { + expect(checkContainsTrigger(makeCommentContext("user@claude.com"))).toBe(false); + }); + }); + + describe("PR review - non-whitespace leading boundaries", () => { + const baseContext = { + ...mockPullRequestReviewContext, + inputs: { + ...mockPullRequestReviewContext.inputs, + triggerPhrase: "@claude", + }, + }; + + function makeReviewContext(body: string): ParsedGitHubContext { + return { + ...baseContext, + payload: { + ...baseContext.payload, + review: { + ...(baseContext.payload as PullRequestReviewEvent).review, + body, + }, + }, + } as ParsedGitHubContext; + } + + it("should trigger when preceded by parenthesis", () => { + expect(checkContainsTrigger(makeReviewContext("(@claude) please review"))).toBe(true); + }); + + it("should trigger when preceded by double quote", () => { + expect(checkContainsTrigger(makeReviewContext('"@claude review"'))).toBe(true); + }); + + it("should trigger when preceded by single quote", () => { + expect(checkContainsTrigger(makeReviewContext("'@claude review'"))).toBe(true); + }); + + it("should still reject email-like patterns", () => { + expect(checkContainsTrigger(makeReviewContext("admin@claude.io"))).toBe(false); + }); + }); +});