Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 23 additions & 7 deletions src/server/claimServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,14 +200,30 @@ class ClaimReviewAcceptButton extends Button {
return
}

await recordClaimDecision({
userId,
guildId,
status: "accepted",
decidedById: interaction.user?.id
}).catch((error) => {
try {
await recordClaimDecision({
userId,
guildId,
status: "accepted",
decidedById: interaction.user?.id
})
} catch (error) {
console.error("Failed to record accepted claim:", error)
})
await interaction.reply({
components: [
new Container(
[
new TextDisplay("### Could not record claim"),
new TextDisplay(
"The clawtributors role was added, but the claim decision could not be saved. Ask a moderator to retry or update the claim record."
)
],
{ accentColor: "#f85149" }
)
]
})
return
}

const user = await interaction.client.fetchUser(userId).catch(() => null)
await user?.send({
Expand Down
126 changes: 126 additions & 0 deletions tests/claimAcceptPersist.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { type ButtonInteraction, serializePayload } from "@buape/carbon"
import { describe, expect, it, spyOn } from "bun:test"
import * as claimRequests from "../src/data/claimRequests.js"
import { setRuntimeEnv } from "../src/runtime/env.js"
import { claimReviewComponents } from "../src/server/claimServer.js"

const flattenComponents = (component: unknown): Record<string, unknown>[] => {
if (!component || typeof component !== "object") {
return []
}

const record = component as Record<string, unknown>
const children = Array.isArray(record.components)
? record.components.flatMap(flattenComponents)
: []

return [record, ...children]
}

const replyText = (replies: unknown[]) =>
replies
.flatMap((reply) => flattenComponents(serializePayload(reply)))
.map((component) => component.content)
.filter((content): content is string => typeof content === "string")
.join("\n")

const runAccept = async () => {
const replies: unknown[] = []
const dms: unknown[] = []
const announcements: unknown[] = []
const patches: unknown[] = []
const acceptButton = claimReviewComponents[0]
const interaction = {
user: { id: "reviewer-1" },
message: {
id: "review-message-1",
channelId: "review-channel-1",
rawData: { components: [] }
},
reply: async (payload: unknown) => {
replies.push(payload)
},
client: {
fetchUser: async () => ({
send: async (payload: unknown) => {
dms.push(payload)
}
}),
fetchChannel: async () => ({
send: async (payload: unknown) => {
announcements.push(payload)
}
}),
rest: {
patch: async (...args: unknown[]) => {
patches.push(args)
}
}
}
} as unknown as ButtonInteraction

await acceptButton.run(interaction, {
userId: "suser-1",
guildId: "sguild-1"
})

return { replies, dms, announcements, patches, text: replyText(replies) }
}

describe("claim review accept persist", () => {
it("does not report accepted when decision persist fails", async () => {
setRuntimeEnv({ DISCORD_BOT_TOKEN: "test-token" } as Env)
const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(
async () => new Response(null, { status: 204 })
)
const persistSpy = spyOn(
claimRequests,
"recordClaimDecision"
).mockImplementation(async () => {
throw new Error("database unavailable")
})
const consoleError = spyOn(console, "error").mockImplementation(() => {})

try {
const result = await runAccept()

expect(persistSpy).toHaveBeenCalledTimes(1)
expect(result.replies).toHaveLength(1)
expect(result.text.toLowerCase()).not.toContain("claim accepted")
expect(result.text.toLowerCase()).not.toContain("has been given the role")
expect(result.dms).toHaveLength(0)
expect(result.announcements).toHaveLength(0)
expect(result.patches).toHaveLength(0)
expect(result.text).toContain("Could not record claim")
} finally {
consoleError.mockRestore()
persistSpy.mockRestore()
fetchSpy.mockRestore()
}
})

it("reports accepted after the decision is recorded", async () => {
setRuntimeEnv({ DISCORD_BOT_TOKEN: "test-token" } as Env)
const fetchSpy = spyOn(globalThis, "fetch").mockImplementation(
async () => new Response(null, { status: 204 })
)
const persistSpy = spyOn(
claimRequests,
"recordClaimDecision"
).mockImplementation(async () => {})
const consoleError = spyOn(console, "error").mockImplementation(() => {})

try {
const result = await runAccept()

expect(persistSpy).toHaveBeenCalledTimes(1)
expect(result.text.toLowerCase()).toContain("claim accepted")
expect(result.dms).toHaveLength(1)
expect(result.announcements).toHaveLength(1)
} finally {
consoleError.mockRestore()
persistSpy.mockRestore()
fetchSpy.mockRestore()
}
})
})