-
Notifications
You must be signed in to change notification settings - Fork 388
Preserve allowlisted mentions in add_comment sanitization (@copilot regression)
#32683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a0b9a6e
1450a2b
76a1ca4
e7bc146
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2431,6 +2431,114 @@ describe("add_comment", () => { | |
| expect(capturedBody).toContain("`@copilot`"); | ||
| }); | ||
|
|
||
| it("should preserve @copilot mention when mentions allowlist includes copilot", async () => { | ||
| const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); | ||
|
|
||
| mockContext.payload = { | ||
| pull_request: { | ||
| number: 8535, | ||
| user: { login: "PRAuthor", type: "User" }, | ||
| }, | ||
| }; | ||
|
|
||
| let capturedBody = null; | ||
| mockGithub.rest.issues.createComment = async params => { | ||
| capturedBody = params.body; | ||
| return { | ||
| data: { | ||
| id: 12345, | ||
| html_url: "https://github.com/owner/repo/issues/8535#issuecomment-12345", | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { allowed: ["@copilot"] } }); })()`); | ||
|
|
||
| const message = { | ||
| type: "add_comment", | ||
| body: "@copilot review all comments", | ||
| }; | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The new test only exercises it("should preserve `@copilot` mention when allowlist entry omits @ prefix", async () => {
// ... same setup ...
const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { allowed: ["copilot"] } }); })()`)
const result = await handler({ type: "add_comment", body: "`@copilot` review" }, {})
expect(capturedBody).toContain("`@copilot`")
expect(capturedBody).not.toContain("`@copilot`")
}) |
||
| const result = await handler(message, {}); | ||
|
|
||
| expect(result.success).toBe(true); | ||
| expect(capturedBody).toBeDefined(); | ||
| expect(capturedBody).toContain("@copilot"); | ||
| expect(capturedBody).not.toContain("`@copilot`"); | ||
| }); | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] The deduplication path (where an alias appears in both it("should not duplicate alias when it appears in both parentAuthors and mentions.allowed", async () => {
// payload where PR author is also in mentions.allowed
mockContext.payload = {
pull_request: { number: 1, user: { login: "copilot", type: "User" } },
}
const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { allowed: ["`@copilot`"] } }); })()`)
const result = await handler({ type: "add_comment", body: "`@copilot` thanks" }, {})
// `@copilot` should appear exactly once, not escaped
expect(capturedBody.match(/`@copilot/g`)).toHaveLength(1)
}) |
||
| it("should escape all mentions when mentions.enabled is false", async () => { | ||
| const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); | ||
|
|
||
| mockContext.payload = { | ||
| pull_request: { | ||
| number: 8535, | ||
| user: { login: "PRAuthor", type: "User" }, | ||
| }, | ||
| }; | ||
|
|
||
| let capturedBody = null; | ||
| mockGithub.rest.issues.createComment = async params => { | ||
| capturedBody = params.body; | ||
| return { | ||
| data: { | ||
| id: 12345, | ||
| html_url: "https://github.com/owner/repo/issues/8535#issuecomment-12345", | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: { enabled: false, allowed: ["@copilot"] } }); })()`); | ||
|
|
||
| const message = { | ||
| type: "add_comment", | ||
| body: "@copilot ping @PRAuthor", | ||
| }; | ||
|
|
||
| const result = await handler(message, {}); | ||
|
|
||
| expect(result.success).toBe(true); | ||
| expect(capturedBody).toBeDefined(); | ||
| expect(capturedBody).toContain("`@copilot`"); | ||
| expect(capturedBody).toContain("`@PRAuthor`"); | ||
| }); | ||
|
|
||
| it("should escape all mentions when mentions is false", async () => { | ||
| const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); | ||
|
|
||
| mockContext.payload = { | ||
| pull_request: { | ||
| number: 8535, | ||
| user: { login: "PRAuthor", type: "User" }, | ||
| }, | ||
| }; | ||
|
|
||
| let capturedBody = null; | ||
| mockGithub.rest.issues.createComment = async params => { | ||
| capturedBody = params.body; | ||
| return { | ||
| data: { | ||
| id: 12345, | ||
| html_url: "https://github.com/owner/repo/issues/8535#issuecomment-12345", | ||
| }, | ||
| }; | ||
| }; | ||
|
|
||
| const handler = await eval(`(async () => { ${addCommentScript}; return await main({ mentions: false }); })()`); | ||
|
|
||
| const message = { | ||
| type: "add_comment", | ||
| body: "@copilot ping @PRAuthor", | ||
| }; | ||
|
|
||
| const result = await handler(message, {}); | ||
|
|
||
| expect(result.success).toBe(true); | ||
| expect(capturedBody).toBeDefined(); | ||
| expect(capturedBody).toContain("`@copilot`"); | ||
| expect(capturedBody).toContain("`@PRAuthor`"); | ||
| }); | ||
|
|
||
| it("should fetch and preserve issue author for explicit item_number", async () => { | ||
| const addCommentScript = fs.readFileSync(path.join(__dirname, "add_comment.cjs"), "utf8"); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -299,6 +299,12 @@ async function loadHandlers(config, prReviewBuffer) { | |
| // Call the factory function with config to get the message handler | ||
| const handlerConfig = { ...(config[type] || {}) }; | ||
|
|
||
| // Pass top-level mentions policy through to add_comment so the handler can | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/diagnose] The // Merge top-level keys that the handler config doesn't already override
const handlerConfig = { ...config, ...(config[type] || {}) };This follows the same shallow-merge pattern already used elsewhere and removes the need for the handler-type guard. Worth considering if a second handler ever needs a top-level field. |
||
| // preserve the same allowed mention aliases used during collection. | ||
| if (type === "add_comment" && handlerConfig.mentions == null && config.mentions != null) { | ||
| handlerConfig.mentions = config.mentions; | ||
| } | ||
|
Comment on lines
+302
to
+306
|
||
|
|
||
| // Inject shared PR review buffer into handlers that need it | ||
| if (PR_REVIEW_HANDLER_TYPES.has(type)) { | ||
| handlerConfig._prReviewBuffer = prReviewBuffer; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/diagnose] The PR description references a
mergeUniqueAliaseshelper but the implementation inlines the two-loop deduplication instead. The inline approach works correctly, but the logic inadd_comment.cjs(lines 562–585) is non-trivial enough that a named, exported helper would:Not blocking — just noting the gap between the stated design and what landed.