mirror of
https://github.com/openclaw/barnacle.git
synced 2026-08-25 11:10:04 -04:00
751 lines
20 KiB
TypeScript
751 lines
20 KiB
TypeScript
import { Database } from "bun:sqlite"
|
|
import {
|
|
afterAll,
|
|
beforeAll,
|
|
describe,
|
|
expect,
|
|
it,
|
|
spyOn
|
|
} from "bun:test"
|
|
import {
|
|
existsSync,
|
|
readFileSync,
|
|
readdirSync
|
|
} from "node:fs"
|
|
import {
|
|
ApplicationCommandType,
|
|
ApplicationIntegrationType,
|
|
type ButtonInteraction,
|
|
type CommandInteraction,
|
|
InteractionContextType,
|
|
PermissionFlagsBits,
|
|
serializePayload
|
|
} from "@buape/carbon"
|
|
import AdminCommand from "../src/commands/admin.js"
|
|
import MaintainerCommand from "../src/commands/maintainer.js"
|
|
import SlapCommand, {
|
|
FishSlapContextCommand
|
|
} from "../src/commands/slap.js"
|
|
import RoleCommand from "../src/commands/role.js"
|
|
import {
|
|
buildSlapIncidentContainer
|
|
} from "../src/components/slapButtons.js"
|
|
import {
|
|
slapConfig,
|
|
slapOutcomesForRarity,
|
|
slapSceneRevision,
|
|
slapSceneUrl,
|
|
slapSceneVariants,
|
|
type SlapOutcome
|
|
} from "../src/config/slap.js"
|
|
import {
|
|
bindSlapMessage,
|
|
createSlapEvent,
|
|
getSlapEvent,
|
|
recordSlapAppeal,
|
|
recordSlapCounter
|
|
} from "../src/data/slapEvents.js"
|
|
import { getPrimaryDb } from "../src/db.js"
|
|
import { setRuntimeEnv } from "../src/runtime/env.js"
|
|
import {
|
|
formatSlapIncidentId,
|
|
generateSlapResult,
|
|
getAppealRuling
|
|
} from "../src/services/slapEngine.js"
|
|
import {
|
|
buildSlapIncidentPayload,
|
|
setSlapImageFetcherForTesting
|
|
} from "../src/services/slapMedia.js"
|
|
import {
|
|
handleSlapAppeal,
|
|
handleSlapBack,
|
|
hasSlapRole
|
|
} from "../src/services/slapInteractions.js"
|
|
import { SqliteD1Database } from "./helpers/sqliteD1.js"
|
|
|
|
const validWebp = new Uint8Array([
|
|
0x52, 0x49, 0x46, 0x46,
|
|
0x04, 0x00, 0x00, 0x00,
|
|
0x57, 0x45, 0x42, 0x50
|
|
])
|
|
|
|
beforeAll(() => {
|
|
setSlapImageFetcherForTesting(
|
|
async () =>
|
|
new Response(validWebp, {
|
|
status: 200,
|
|
headers: { "content-type": "image/webp" }
|
|
})
|
|
)
|
|
})
|
|
|
|
afterAll(() => {
|
|
setSlapImageFetcherForTesting(null)
|
|
})
|
|
|
|
const slapMigrationPaths = readdirSync("drizzle")
|
|
.filter(
|
|
(file) =>
|
|
(file.startsWith("0010_") || file.startsWith("0011_")) &&
|
|
file.endsWith(".sql")
|
|
)
|
|
.sort()
|
|
|
|
if (slapMigrationPaths.length !== 2) {
|
|
throw new Error("Could not find slap and shared cooldown migrations")
|
|
}
|
|
|
|
const applySlapMigration = (database: Database) => {
|
|
for (const migrationPath of slapMigrationPaths) {
|
|
const migration = readFileSync(`drizzle/${migrationPath}`, "utf8")
|
|
for (const statement of migration.split("--> statement-breakpoint")) {
|
|
const trimmed = statement.trim()
|
|
if (trimmed.length > 0) {
|
|
database.run(trimmed)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const testDatabase = () => {
|
|
const owner = new SqliteD1Database()
|
|
applySlapMigration(owner.database)
|
|
setRuntimeEnv({ DB: owner as unknown as D1Database } as Env)
|
|
return { owner, database: getPrimaryDb() }
|
|
}
|
|
|
|
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 payloadText = (payload: unknown) =>
|
|
flattenComponents(serializePayload(payload))
|
|
.map((component) => component.content)
|
|
.filter((content): content is string => typeof content === "string")
|
|
.join("\n")
|
|
|
|
const baseResult = (seed = "test-seed") =>
|
|
generateSlapResult({
|
|
seed,
|
|
actor: { id: "actor-1", bot: false },
|
|
target: { id: "target-1", bot: false }
|
|
})
|
|
|
|
const createEvent = async (
|
|
overrides: Partial<{
|
|
interactionId: string
|
|
guildId: string
|
|
channelId: string
|
|
actorId: string
|
|
targetId: string
|
|
targetIsBot: boolean
|
|
}> = {},
|
|
referenceDate = new Date("2026-07-16T18:00:00.000Z")
|
|
) =>
|
|
createSlapEvent(
|
|
{
|
|
interactionId: "interaction-1",
|
|
guildId: slapConfig.guildId,
|
|
channelId: "channel-1",
|
|
actorId: "actor-1",
|
|
targetId: "target-1",
|
|
targetIsBot: false,
|
|
result: baseResult(overrides.interactionId),
|
|
...overrides
|
|
},
|
|
referenceDate
|
|
)
|
|
|
|
describe("command registration changes", () => {
|
|
it("removes showcase-ban from /role", () => {
|
|
expect(new RoleCommand().subcommands.map((command) => command.name)).toEqual([
|
|
"clawtributor",
|
|
"maintainer-guest"
|
|
])
|
|
})
|
|
|
|
it("keeps only active admin and maintainer commands", () => {
|
|
const admin = new AdminCommand()
|
|
expect(admin.permission).toBe(PermissionFlagsBits.Administrator)
|
|
expect(admin.subcommands.map((command) => command.name)).toEqual([
|
|
"say",
|
|
"inactivity-warn",
|
|
"automod-bypass-toggle"
|
|
])
|
|
expect(new MaintainerCommand().subcommands.map((command) => command.name)).toEqual([
|
|
"whois"
|
|
])
|
|
})
|
|
})
|
|
|
|
describe("slap catalog and engine", () => {
|
|
it("ships the complete fish catalog and copy set", () => {
|
|
expect(slapConfig.fish).toHaveLength(12)
|
|
expect(new Set(slapConfig.fish.map((fish) => fish.rarity))).toEqual(
|
|
new Set(["common", "uncommon", "rare", "epic", "legendary"])
|
|
)
|
|
const imageUrls = slapConfig.fish.flatMap((fish) => {
|
|
return slapOutcomesForRarity(fish.rarity).flatMap((outcome) =>
|
|
slapSceneVariants.map((variant) =>
|
|
slapSceneUrl(fish.slug, outcome, variant)
|
|
)
|
|
)
|
|
})
|
|
|
|
expect(imageUrls).toHaveLength(327)
|
|
expect(new Set(imageUrls).size).toBe(327)
|
|
expect(slapSceneRevision).toMatch(/^[a-f0-9]{40}$/)
|
|
const repositoryRevisionPrefix =
|
|
`https://raw.githubusercontent.com/openclaw/hermit/${slapSceneRevision}/`
|
|
for (const imageUrl of imageUrls) {
|
|
expect(imageUrl.startsWith(repositoryRevisionPrefix)).toBe(true)
|
|
const path = imageUrl.slice(repositoryRevisionPrefix.length)
|
|
expect(path.startsWith("assets/slap/scenes/")).toBe(true)
|
|
expect(existsSync(path)).toBe(true)
|
|
}
|
|
expect(
|
|
Object.values(slapConfig.lines).reduce(
|
|
(total, lines) => total + lines.length,
|
|
0
|
|
)
|
|
).toBe(40)
|
|
})
|
|
|
|
it("is deterministic for the same interaction seed", () => {
|
|
const first = baseResult("stable-interaction")
|
|
const second = baseResult("stable-interaction")
|
|
|
|
expect(second).toEqual(first)
|
|
expect(first.imageUrl).toContain(
|
|
`/assets/slap/scenes/${first.fishSlug}/${first.outcome}-`
|
|
)
|
|
})
|
|
|
|
it("handles self, Hermit, Rock Lobster, and generic bots explicitly", () => {
|
|
const actor = { id: "actor-1", bot: false }
|
|
const outcomeFor = (target: { id: string; bot: boolean }) =>
|
|
generateSlapResult({
|
|
seed: `special:${target.id}`,
|
|
actor,
|
|
target
|
|
}).outcome
|
|
|
|
expect(outcomeFor(actor)).toBe("self")
|
|
expect(
|
|
outcomeFor({ id: slapConfig.hermitUserId, bot: true })
|
|
).toBe("hermit")
|
|
expect(
|
|
outcomeFor({ id: slapConfig.rockLobsterUserId, bot: true })
|
|
).toBe("rock_lobster")
|
|
expect(outcomeFor({ id: "another-bot", bot: true })).toBe("bot")
|
|
})
|
|
})
|
|
|
|
describe("slap event ledger", () => {
|
|
it("creates one durable event and reuses it for interaction retries", async () => {
|
|
const { owner, database } = testDatabase()
|
|
try {
|
|
const first = await createEvent()
|
|
const retry = await createEvent(
|
|
{},
|
|
new Date("2026-07-16T18:00:05.000Z")
|
|
)
|
|
|
|
expect(first.kind).toBe("created")
|
|
expect(retry.kind).toBe("existing")
|
|
if (first.kind !== "cooldown" && retry.kind !== "cooldown") {
|
|
expect(retry.event.id).toBe(first.event.id)
|
|
expect(formatSlapIncidentId(first.event.id)).toBe("FSH-0001")
|
|
}
|
|
expect(
|
|
owner.database
|
|
.query("select count(*) as count from slap_events")
|
|
.get()
|
|
).toEqual({ count: 1 })
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
|
|
it("enforces actor, target, and channel cooldowns independently", async () => {
|
|
const { owner } = testDatabase()
|
|
try {
|
|
await createEvent()
|
|
|
|
const actorCooldown = await createEvent(
|
|
{
|
|
interactionId: "interaction-actor",
|
|
targetId: "target-2",
|
|
channelId: "channel-2"
|
|
},
|
|
new Date("2026-07-16T18:00:05.000Z")
|
|
)
|
|
const targetCooldown = await createEvent(
|
|
{
|
|
interactionId: "interaction-target",
|
|
actorId: "actor-2",
|
|
channelId: "channel-3"
|
|
},
|
|
new Date("2026-07-16T18:00:35.000Z")
|
|
)
|
|
const channelCooldown = await createEvent(
|
|
{
|
|
interactionId: "interaction-channel",
|
|
actorId: "actor-3",
|
|
targetId: "target-3"
|
|
},
|
|
new Date("2026-07-16T18:00:05.000Z")
|
|
)
|
|
const afterCooldowns = await createEvent(
|
|
{
|
|
interactionId: "interaction-later",
|
|
actorId: "actor-4",
|
|
targetId: "target-4",
|
|
channelId: "channel-4"
|
|
},
|
|
new Date("2026-07-16T18:01:31.000Z")
|
|
)
|
|
|
|
expect(actorCooldown).toEqual(
|
|
expect.objectContaining({
|
|
kind: "cooldown",
|
|
cooldowns: [
|
|
expect.objectContaining({ kind: "actor" })
|
|
]
|
|
})
|
|
)
|
|
expect(targetCooldown).toEqual(
|
|
expect.objectContaining({
|
|
kind: "cooldown",
|
|
cooldowns: [
|
|
expect.objectContaining({ kind: "target" })
|
|
]
|
|
})
|
|
)
|
|
expect(channelCooldown).toEqual(
|
|
expect.objectContaining({
|
|
kind: "cooldown",
|
|
cooldowns: [
|
|
expect.objectContaining({ kind: "channel" })
|
|
]
|
|
})
|
|
)
|
|
expect(afterCooldowns.kind).toBe("created")
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
|
|
it("records at most one counter and one stable appeal ruling", async () => {
|
|
const { owner, database } = testDatabase()
|
|
try {
|
|
const creation = await createEvent()
|
|
expect(creation.kind).toBe("created")
|
|
if (creation.kind === "cooldown") {
|
|
return
|
|
}
|
|
const event = creation.event
|
|
const counter = generateSlapResult({
|
|
seed: `counter:${event.interactionId}`,
|
|
actor: { id: event.targetId, bot: false },
|
|
target: { id: event.actorId, bot: false },
|
|
mode: "counter"
|
|
})
|
|
|
|
const firstCounter = await recordSlapCounter(
|
|
event.id,
|
|
event.targetId,
|
|
event.actorId,
|
|
counter,
|
|
new Date("2026-07-16T18:00:10.000Z"),
|
|
database
|
|
)
|
|
const secondCounter = await recordSlapCounter(
|
|
event.id,
|
|
event.targetId,
|
|
event.actorId,
|
|
baseResult("different-counter"),
|
|
new Date("2026-07-16T18:00:11.000Z"),
|
|
database
|
|
)
|
|
const ruling = getAppealRuling(event.id)
|
|
const firstAppeal = await recordSlapAppeal(
|
|
event.id,
|
|
event.targetId,
|
|
ruling,
|
|
new Date("2026-07-16T18:00:12.000Z"),
|
|
database
|
|
)
|
|
const secondAppeal = await recordSlapAppeal(
|
|
event.id,
|
|
event.targetId,
|
|
"Different ruling",
|
|
new Date("2026-07-16T18:00:13.000Z"),
|
|
database
|
|
)
|
|
|
|
expect(firstCounter?.kind).toBe("recorded")
|
|
expect(secondCounter?.kind).toBe("already_recorded")
|
|
expect(secondCounter?.event.counterNarrative).toBe(counter.narrative)
|
|
expect(firstAppeal?.kind).toBe("recorded")
|
|
expect(secondAppeal?.kind).toBe("already_recorded")
|
|
expect(secondAppeal?.event.appealRuling).toBe(ruling)
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("slap Carbon incident card", () => {
|
|
it("uploads artwork to Discord attachments instead of external media", async () => {
|
|
const { owner } = testDatabase()
|
|
try {
|
|
const creation = await createEvent()
|
|
if (creation.kind === "cooldown") {
|
|
throw new Error("Unexpected cooldown")
|
|
}
|
|
const event = creation.event
|
|
const requested: string[] = []
|
|
const payload = await buildSlapIncidentPayload(
|
|
event,
|
|
async (input) => {
|
|
requested.push(input.toString())
|
|
return new Response(validWebp, {
|
|
status: 200,
|
|
headers: { "content-type": "image/webp" }
|
|
})
|
|
}
|
|
)
|
|
const serialized = serializePayload(payload)
|
|
const gallery = flattenComponents(serialized).find(
|
|
(component) => component.type === 12
|
|
) as { items?: Array<{ media?: { url?: string } }> } | undefined
|
|
|
|
expect(requested).toEqual([event.imageUrl])
|
|
expect(payload.files).toHaveLength(1)
|
|
expect(payload.files?.[0]?.name).toBe(
|
|
`slap-${event.id}-initial.webp`
|
|
)
|
|
expect(gallery?.items?.[0]?.media?.url).toBe(
|
|
`attachment://slap-${event.id}-initial.webp`
|
|
)
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
|
|
it("omits broken external media when attachment retrieval fails", async () => {
|
|
const { owner } = testDatabase()
|
|
const consoleError = spyOn(console, "error").mockImplementation(() => {})
|
|
try {
|
|
const creation = await createEvent()
|
|
if (creation.kind === "cooldown") {
|
|
throw new Error("Unexpected cooldown")
|
|
}
|
|
const event = creation.event
|
|
const payload = await buildSlapIncidentPayload(
|
|
event,
|
|
async () => new Response("not found", { status: 404 })
|
|
)
|
|
const serialized = serializePayload(payload)
|
|
const components = flattenComponents(serialized)
|
|
|
|
expect(payload.files).toBeUndefined()
|
|
expect(
|
|
components.some((component) => component.type === 12)
|
|
).toBe(false)
|
|
expect(payloadText(payload)).toContain(
|
|
"Incident artwork is temporarily unavailable."
|
|
)
|
|
} finally {
|
|
consoleError.mockRestore()
|
|
owner.close()
|
|
}
|
|
})
|
|
|
|
it("renders art, metrics, incident controls, and counter state", async () => {
|
|
const { owner, database } = testDatabase()
|
|
try {
|
|
const creation = await createEvent()
|
|
if (creation.kind === "cooldown") {
|
|
throw new Error("Unexpected cooldown")
|
|
}
|
|
const event = creation.event
|
|
const initialPayload = serializePayload({
|
|
components: [buildSlapIncidentContainer(event)]
|
|
}) as { components?: unknown[] }
|
|
const initialComponents = flattenComponents(initialPayload)
|
|
const initialText = payloadText({
|
|
components: [buildSlapIncidentContainer(event)]
|
|
})
|
|
const buttons = initialComponents.filter(
|
|
(component) => component.type === 2
|
|
)
|
|
const gallery = initialComponents.find(
|
|
(component) => component.type === 12
|
|
)
|
|
|
|
expect(initialText).toContain("Fishery Incident FSH-0001")
|
|
expect(initialText).toContain("Impact:")
|
|
expect(gallery).toBeDefined()
|
|
expect(buttons).toEqual([
|
|
expect.objectContaining({
|
|
custom_id: `slap-back:id=${event.id}`,
|
|
disabled: false
|
|
}),
|
|
expect.objectContaining({
|
|
custom_id: `slap-appeal:id=${event.id}`,
|
|
disabled: false
|
|
})
|
|
])
|
|
|
|
const counter = generateSlapResult({
|
|
seed: "counter-card",
|
|
actor: { id: event.targetId, bot: false },
|
|
target: { id: event.actorId, bot: false },
|
|
mode: "counter"
|
|
})
|
|
const recorded = await recordSlapCounter(
|
|
event.id,
|
|
event.targetId,
|
|
event.actorId,
|
|
counter,
|
|
new Date(),
|
|
database
|
|
)
|
|
expect(recorded).not.toBeNull()
|
|
const counterText = payloadText({
|
|
components: [buildSlapIncidentContainer(recorded!.event)]
|
|
})
|
|
expect(counterText).toContain("Counter-filing accepted")
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("/slap and Fish Slap", () => {
|
|
it("register both entry points as guild-only staff commands", () => {
|
|
const slash = new SlapCommand()
|
|
const context = new FishSlapContextCommand()
|
|
|
|
for (const command of [slash, context]) {
|
|
expect(command.contexts).toEqual([InteractionContextType.Guild])
|
|
expect(command.integrationTypes).toEqual([
|
|
ApplicationIntegrationType.GuildInstall
|
|
])
|
|
expect(command.guildIds).toEqual([slapConfig.guildId])
|
|
expect(command.defer).toBe(false)
|
|
}
|
|
expect(context.name).toBe("Fish Slap")
|
|
expect(context.type).toBe(ApplicationCommandType.User)
|
|
})
|
|
|
|
it("authorizes Community Team, Maintainers, and OpenClaw Foundation", () => {
|
|
expect(slapConfig.authorizedRoleIds).toEqual([
|
|
"1477360613125787678",
|
|
"1457214688806047756",
|
|
"1503268035908075590",
|
|
"1509063061598769333"
|
|
])
|
|
for (const roleId of slapConfig.authorizedRoleIds) {
|
|
expect(hasSlapRole([roleId])).toBe(true)
|
|
}
|
|
expect(hasSlapRole(["unrelated-role"])).toBe(false)
|
|
})
|
|
|
|
it("rejects users outside the authorized roles before touching D1", async () => {
|
|
const replies: unknown[] = []
|
|
const interaction = {
|
|
rawData: {
|
|
id: "unauthorized-interaction",
|
|
guild_id: slapConfig.guildId,
|
|
channel_id: "channel-1"
|
|
},
|
|
member: { roles: [{ id: "unrelated-role" }] },
|
|
user: { id: "actor-1" },
|
|
userId: "actor-1",
|
|
options: {
|
|
getUser: () => ({ id: "target-1", bot: false })
|
|
},
|
|
reply: async (payload: unknown) => {
|
|
replies.push(payload)
|
|
}
|
|
} as unknown as CommandInteraction
|
|
|
|
await new SlapCommand().run(interaction)
|
|
|
|
expect(payloadText(replies[0])).toContain(
|
|
"OpenClaw Foundation roles only"
|
|
)
|
|
expect(replies[0]).toEqual(expect.objectContaining({ ephemeral: true }))
|
|
})
|
|
|
|
it("allows Maintainers to publish the canonical incident card", async () => {
|
|
const { owner } = testDatabase()
|
|
try {
|
|
const replies: unknown[] = []
|
|
let deferred = false
|
|
const interaction = {
|
|
rawData: {
|
|
id: "authorized-interaction",
|
|
guild_id: slapConfig.guildId,
|
|
channel_id: "channel-command"
|
|
},
|
|
member: {
|
|
roles: [{ id: slapConfig.authorizedRoleIds[1] }]
|
|
},
|
|
user: { id: "actor-command" },
|
|
userId: "actor-command",
|
|
options: {
|
|
getUser: () => ({ id: "target-command", bot: false })
|
|
},
|
|
defer: async () => {
|
|
deferred = true
|
|
},
|
|
reply: async (payload: unknown) => {
|
|
replies.push(payload)
|
|
return { id: "message-command" }
|
|
}
|
|
} as unknown as CommandInteraction
|
|
|
|
await new SlapCommand().run(interaction)
|
|
|
|
const stored = owner.database
|
|
.query(
|
|
"select message_id as messageId, interaction_id as interactionId from slap_events"
|
|
)
|
|
.get() as { messageId: string; interactionId: string }
|
|
expect(deferred).toBe(true)
|
|
expect(stored).toEqual({
|
|
messageId: "message-command",
|
|
interactionId: "authorized-interaction"
|
|
})
|
|
expect(payloadText(replies[0])).toContain("Fishery Incident FSH-0001")
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("slap buttons", () => {
|
|
it("allows the named target to slap back regardless of roles", async () => {
|
|
const { owner, database } = testDatabase()
|
|
try {
|
|
const creation = await createEvent()
|
|
if (creation.kind === "cooldown") {
|
|
throw new Error("Unexpected cooldown")
|
|
}
|
|
const event = await bindSlapMessage(
|
|
creation.event.id,
|
|
"message-1",
|
|
new Date(),
|
|
database
|
|
)
|
|
expect(event).not.toBeNull()
|
|
|
|
const makeInteraction = (userId: string, roles: string[]) => {
|
|
const replies: unknown[] = []
|
|
const updates: unknown[] = []
|
|
const interaction = {
|
|
rawData: {
|
|
guild_id: slapConfig.guildId,
|
|
channel_id: "channel-1",
|
|
message: { id: "message-1" }
|
|
},
|
|
user: { id: userId },
|
|
userId,
|
|
member: { roles: roles.map((id) => ({ id })) },
|
|
reply: async (payload: unknown) => {
|
|
replies.push(payload)
|
|
},
|
|
update: async (payload: unknown) => {
|
|
updates.push(payload)
|
|
}
|
|
} as unknown as ButtonInteraction
|
|
return { interaction, replies, updates }
|
|
}
|
|
|
|
const wrongUser = makeInteraction("someone-else", [])
|
|
await handleSlapBack(wrongUser.interaction, { id: event!.id })
|
|
expect(payloadText(wrongUser.replies[0])).toContain(
|
|
"Only the named target"
|
|
)
|
|
|
|
const target = makeInteraction(event!.targetId, [])
|
|
await handleSlapBack(target.interaction, { id: event!.id })
|
|
expect(target.replies).toHaveLength(0)
|
|
expect(target.updates).toHaveLength(1)
|
|
expect(payloadText(target.updates[0])).toContain(
|
|
"Counter-filing accepted"
|
|
)
|
|
|
|
const stored = await getSlapEvent(event!.id, database)
|
|
expect(stored?.counteredAt).not.toBeNull()
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
|
|
it("returns a private, stable ruling only to the target", async () => {
|
|
const { owner, database } = testDatabase()
|
|
try {
|
|
const creation = await createEvent()
|
|
if (creation.kind === "cooldown") {
|
|
throw new Error("Unexpected cooldown")
|
|
}
|
|
const event = await bindSlapMessage(
|
|
creation.event.id,
|
|
"message-appeal",
|
|
new Date(),
|
|
database
|
|
)
|
|
const replies: unknown[] = []
|
|
const messageEdits: unknown[] = []
|
|
const interaction = {
|
|
rawData: {
|
|
guild_id: slapConfig.guildId,
|
|
channel_id: "channel-1",
|
|
message: { id: "message-appeal" }
|
|
},
|
|
user: { id: event!.targetId },
|
|
userId: event!.targetId,
|
|
member: { roles: [] },
|
|
message: {
|
|
edit: async (payload: unknown) => {
|
|
messageEdits.push(payload)
|
|
}
|
|
},
|
|
reply: async (payload: unknown) => {
|
|
replies.push(payload)
|
|
},
|
|
update: async () => {}
|
|
} as unknown as ButtonInteraction
|
|
|
|
await handleSlapAppeal(interaction, { id: event!.id })
|
|
|
|
expect(messageEdits).toHaveLength(1)
|
|
expect(replies).toHaveLength(1)
|
|
expect(replies[0]).toEqual(
|
|
expect.objectContaining({ ephemeral: true })
|
|
)
|
|
expect(payloadText(replies[0])).toContain(
|
|
getAppealRuling(event!.id)
|
|
)
|
|
const stored = await getSlapEvent(event!.id, database)
|
|
expect(stored?.appealRuling).toBe(getAppealRuling(event!.id))
|
|
} finally {
|
|
owner.close()
|
|
}
|
|
})
|
|
})
|