From f80bf9fb4efe6534d4ca67063661e3b142844212 Mon Sep 17 00:00:00 2001 From: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:30:09 +0000 Subject: [PATCH 1/2] fix: route clawtributor claim pings to community moderators Use the canonical moderation notification role for the claim card and its allowed mentions. Leave review configuration and existing authorization behavior unchanged. Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com> --- src/server/claimServer.ts | 7 +- tests/claimReviewNotification.test.ts | 103 ++++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 3 deletions(-) create mode 100644 tests/claimReviewNotification.test.ts diff --git a/src/server/claimServer.ts b/src/server/claimServer.ts index 25cf9f9..542f473 100644 --- a/src/server/claimServer.ts +++ b/src/server/claimServer.ts @@ -17,6 +17,7 @@ import { TextInput, TextInputStyle } from "@buape/carbon" +import { formSettings } from "../../forms.config.js" import { createClaimRequest, deleteClaimRequest, @@ -27,7 +28,7 @@ import { import { getRuntimeEnv } from "../runtime/env.js" const clawtributorsRoleId = "1458375944111915051" -const claimReviewRoleId = "1477360613125787678" +const claimReviewPingRoleId = formSettings.reviewPingRoleId const claimReviewChannelId = "1503772785120383057" const clawtributorsAnnouncementChannelId = "1458141495701012561" const githubOwner = "openclaw" @@ -949,7 +950,7 @@ const handleClaimCallback = async (request: Request, client: Client) => { components: [ new Container( [ - new TextDisplay(`-# <@&${claimReviewRoleId}>`), + new TextDisplay(`-# <@&${claimReviewPingRoleId}>`), new TextDisplay("### Clawtributor Claim Request"), new TextDisplay( `- User: <@${payload.userId}>\n- ID: ${payload.userId}\n- GitHub: [@${qualifyingSummary.username}]()\n- Merged PRs: **${qualifyingSummary.totalCount}**` @@ -967,7 +968,7 @@ const handleClaimCallback = async (request: Request, client: Client) => { ) ], allowedMentions: { - roles: [claimReviewRoleId], + roles: [claimReviewPingRoleId], users: [] } }) diff --git a/tests/claimReviewNotification.test.ts b/tests/claimReviewNotification.test.ts new file mode 100644 index 0000000..72c6a00 --- /dev/null +++ b/tests/claimReviewNotification.test.ts @@ -0,0 +1,103 @@ +import { type Client, MessageFlags, serializePayload } from "@buape/carbon" +import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test" +import { formSettings } from "../forms.config.js" +import * as claimRequests from "../src/data/claimRequests.js" +import { getRuntimeEnv, setRuntimeEnv } from "../src/runtime/env.js" +import { createClaimUrl, registerClaimRoutes } from "../src/server/claimServer.js" + +const moderationPingRoleId = "1546936406272778271" +const communityTeamRoleId = "1477360613125787678" +const reviewChannelId = "1503772785120383057" + +const submitClaim = async () => { + const messages: ReturnType[] = [] + const fetchedChannels: string[] = [] + const client = { + routes: [], + fetchChannel: async (channelId: string) => { + fetchedChannels.push(channelId) + return { + send: async (payload: Parameters[0]) => { + messages.push(serializePayload(payload)) + return { id: "review-message-1", startThread: async () => null } + } + } + } + } as unknown as Client + registerClaimRoutes(client) + const callback = client.routes.find((route) => route.path === "/claim/callback")! + const claimUrl = new URL(await createClaimUrl("applicant-1", "guild-1")) + const callbackUrl = new URL("/claim/callback", claimUrl) + callbackUrl.searchParams.set("state", claimUrl.searchParams.get("state")!) + callbackUrl.searchParams.set("code", "test-code") + const response = await callback.handler(new Request(callbackUrl)) + expect(response.status).toBe(200) + expect(await response.text()).toContain("Claim submitted") + expect(fetchedChannels).toEqual([reviewChannelId]) + expect(messages).toHaveLength(1) + return messages[0] +} + +describe("claim review notification routing", () => { + let previousEnv: ReturnType + let restoreMocks: Array<() => void> + + beforeEach(() => { + try { previousEnv = getRuntimeEnv() } catch { previousEnv = {} as Env } + setRuntimeEnv({ + BASE_URL: "https://hermit.example", + DEPLOY_SECRET: "test-only-secret", + DISCORD_CLIENT_ID: "test-client" + } as Env) + const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(async (input, init) => { + const url = String(input) + if (url === "https://discord.com/api/v10/oauth2/token" && init?.method === "POST") { + return Response.json({ access_token: "test-access-token" }) + } + if (url === "https://discord.com/api/v10/users/@me") { + return Response.json({ id: "applicant-1" }) + } + if (url === "https://discord.com/api/v10/users/@me/connections") { + return Response.json([{ type: "github", name: "applicant", verified: true }]) + } + if (url.startsWith("https://api.github.com/search/issues?")) { + return Response.json({ total_count: 1, items: [] }) + } + throw new Error(`Unexpected request (including role grants): ${init?.method ?? "GET"} ${url}`) + }) + const getSpy = spyOn(claimRequests, "getClaimRequest").mockResolvedValue(null) + const createSpy = spyOn(claimRequests, "createClaimRequest").mockResolvedValue({ + created: true, + claimRequest: { id: 1 } as NonNullable>> + }) + const markSpy = spyOn(claimRequests, "markClaimRequestSubmitted").mockResolvedValue(undefined) + restoreMocks = [fetchSpy, getSpy, createSpy, markSpy].map((mock) => () => mock.mockRestore()) + }) + + afterEach(() => { + for (const restore of restoreMocks) restore() + setRuntimeEnv(previousEnv) + }) + + it("displays Community Moderation Pings, not Community Team, in the submitted Carbon card", async () => { + const message = await submitClaim() + expect(message.flags).toBe(MessageFlags.IsComponentsV2) + expect(JSON.stringify(message.components)).toContain(`<@&${moderationPingRoleId}>`) + expect(JSON.stringify(message.components)).not.toContain(`<@&${communityTeamRoleId}>`) + }) + + it("allows only the moderation role ping and suppresses applicant mentions", async () => { + const message = await submitClaim() + expect(message.allowed_mentions).toEqual({ roles: [moderationPingRoleId], users: [] }) + }) + + it("retains Community Team review configuration and the existing claim controls", async () => { + const message = await submitClaim() + expect(formSettings.reviewRoleId).toBe(communityTeamRoleId) + expect(formSettings.reviewPingRoleId).toBe(moderationPingRoleId) + expect(formSettings.reviewRoleId).not.toBe(formSettings.reviewPingRoleId) + const components = JSON.stringify(message.components) + expect(components).toContain("claim-review-accept:userId=sapplicant-1;guildId=sguild-1") + expect(components).toContain("claim-review-reject:userId=sapplicant-1;guildId=sguild-1") + }) +}) From 9b8cede7c0ad0f21a633adb7ed69a3b25aa3de10 Mon Sep 17 00:00:00 2001 From: roboclaw-bot <309084314+roboclaw-bot@users.noreply.github.com> Date: Fri, 11 Sep 2026 14:30:17 +0000 Subject: [PATCH 2/2] fix: clawtributor claims ping Community Team instead of moderation subscribers OpenClaw-Publication: 78cbd5da-f9dd-45db-8926-2c0021688ee5 Co-authored-by: hannesrudolph <49103247+hannesrudolph@users.noreply.github.com>