mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
fix(discord): recover question feedback after failed acknowledgements (#128293)
This commit is contained in:
committed by
GitHub
parent
bca206d794
commit
663a9d2f15
@@ -1,6 +1,15 @@
|
||||
// Discord question component feedback tests.
|
||||
import { InteractionResponseType, MessageFlags } from "discord-api-types/v10";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ButtonInteraction } from "../internal/discord.js";
|
||||
import {
|
||||
createInteraction as createDiscordInteraction,
|
||||
type ButtonInteraction,
|
||||
} from "../internal/discord.js";
|
||||
import {
|
||||
attachRestMock,
|
||||
createInternalComponentInteractionPayload,
|
||||
createInternalTestClient,
|
||||
} from "../internal/test-builders.test-support.js";
|
||||
import { createDiscordQuestionButton } from "./questions.js";
|
||||
|
||||
type InteractionHarness = {
|
||||
@@ -91,4 +100,99 @@ describe("Discord question button", () => {
|
||||
ephemeral: true,
|
||||
});
|
||||
});
|
||||
|
||||
it.each(
|
||||
[
|
||||
{
|
||||
name: "submitted answer",
|
||||
result: {
|
||||
status: "answered" as const,
|
||||
questionId: "target",
|
||||
optionValue: "Production",
|
||||
},
|
||||
expectedText: "Answer submitted.",
|
||||
},
|
||||
{
|
||||
name: "already answered question",
|
||||
result: { status: "already-terminal" as const, reason: "already-terminal" as const },
|
||||
expectedText: "This question was already answered.",
|
||||
},
|
||||
{
|
||||
name: "question resolution failure",
|
||||
error: new Error("question service unavailable"),
|
||||
expectedText: "Could not submit this answer.",
|
||||
},
|
||||
].flatMap((outcome) => [
|
||||
{ ...outcome, acknowledged: false },
|
||||
{ ...outcome, acknowledged: true },
|
||||
]),
|
||||
)("delivers $name feedback when acknowledgement is $acknowledged", async (testCase) => {
|
||||
const client = createInternalTestClient();
|
||||
const post = vi.fn(async () => undefined);
|
||||
if (!testCase.acknowledged) {
|
||||
post.mockRejectedValueOnce(new Error("temporary connection reset"));
|
||||
}
|
||||
attachRestMock(client, { post });
|
||||
const interaction = createDiscordInteraction(
|
||||
client,
|
||||
createInternalComponentInteractionPayload({
|
||||
id: "interaction-1",
|
||||
token: "interaction-token",
|
||||
user: {
|
||||
id: "user-1",
|
||||
username: "Alice",
|
||||
discriminator: "0",
|
||||
avatar: null,
|
||||
global_name: null,
|
||||
},
|
||||
}),
|
||||
) as ButtonInteraction;
|
||||
const resolveQuestion = vi.fn(async () => {
|
||||
if (testCase.error) {
|
||||
throw testCase.error;
|
||||
}
|
||||
return testCase.result;
|
||||
});
|
||||
const button = createDiscordQuestionButton({
|
||||
cfg: {} as never,
|
||||
accountId: "default",
|
||||
authorizeQuestion: vi.fn(async () => true),
|
||||
resolveQuestion: resolveQuestion as never,
|
||||
});
|
||||
|
||||
await button.run(interaction, {
|
||||
id: "ask_0123456789abcdef0123456789abcdef",
|
||||
i: "1",
|
||||
});
|
||||
|
||||
expect(resolveQuestion).toHaveBeenCalledOnce();
|
||||
expect(post).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"/interactions/interaction-1/interaction-token/callback",
|
||||
{
|
||||
body: { type: InteractionResponseType.DeferredMessageUpdate },
|
||||
},
|
||||
);
|
||||
if (testCase.acknowledged) {
|
||||
expect(post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"/webhooks/app1/interaction-token",
|
||||
{ body: { content: testCase.expectedText, flags: MessageFlags.Ephemeral } },
|
||||
undefined,
|
||||
);
|
||||
expect(interaction.responseState).toBe("deferred-update");
|
||||
} else {
|
||||
expect(post).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
"/interactions/interaction-1/interaction-token/callback",
|
||||
{
|
||||
body: {
|
||||
type: InteractionResponseType.ChannelMessageWithSource,
|
||||
data: { content: testCase.expectedText, flags: MessageFlags.Ephemeral },
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(interaction.responseState).toBe("replied");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,31 +41,28 @@ class QuestionButton extends Button {
|
||||
try {
|
||||
await interaction.acknowledge();
|
||||
} catch {}
|
||||
let result: Awaited<ReturnType<QuestionResolver>>;
|
||||
let content: string;
|
||||
try {
|
||||
result = await this.ctx.resolveQuestion({
|
||||
const result = await this.ctx.resolveQuestion({
|
||||
cfg: this.ctx.cfg,
|
||||
questionId: callback.questionId,
|
||||
optionIndex: callback.optionIndex,
|
||||
senderId: interaction.userId,
|
||||
clientDisplayName: `Discord question (${this.ctx.accountId})`,
|
||||
});
|
||||
content =
|
||||
result.status === "answered" ? "Answer submitted." : "This question was already answered.";
|
||||
} catch {
|
||||
try {
|
||||
await interaction.followUp({ content: "Could not submit this answer.", ephemeral: true });
|
||||
} catch {}
|
||||
return;
|
||||
content = "Could not submit this answer.";
|
||||
}
|
||||
try {
|
||||
await interaction.followUp({
|
||||
content:
|
||||
result.status === "answered"
|
||||
? "Answer submitted."
|
||||
: "This question was already answered.",
|
||||
ephemeral: true,
|
||||
});
|
||||
const feedback = { content, ephemeral: true };
|
||||
// A rejected acknowledgement leaves the initial callback available, not the webhook.
|
||||
await (interaction.responseState === "unacknowledged"
|
||||
? interaction.reply(feedback)
|
||||
: interaction.followUp(feedback));
|
||||
} catch {
|
||||
// Gateway state already committed; receipt delivery is best-effort.
|
||||
// Gateway state may already be committed; receipt delivery is best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user