Fix Discord /codex_resume picker expiration (#51260)

Merged via squash.

Prepared head SHA: 76eb184dbe
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Co-authored-by: huntharo <5617868+huntharo@users.noreply.github.com>
Reviewed-by: @huntharo
This commit is contained in:
Harold Hunt
2026-03-21 12:59:21 -04:00
committed by GitHub
parent f4227e2787
commit e24bf22f98
15 changed files with 534 additions and 104 deletions

View File

@@ -9,6 +9,7 @@ export type { DiscordConfig, DiscordPluralKitConfig } from "../config/types.disc
export type { InspectedDiscordAccount } from "../../extensions/discord/api.js";
export type { ResolvedDiscordAccount } from "../../extensions/discord/api.js";
export type { DiscordSendComponents, DiscordSendEmbeds } from "../../extensions/discord/api.js";
export type { DiscordComponentMessageSpec } from "../../extensions/discord/api.js";
export type {
ThreadBindingManager,
ThreadBindingRecord,
@@ -64,8 +65,10 @@ export {
} from "./status-helpers.js";
export {
buildDiscordComponentMessage,
createDiscordActionGate,
listDiscordAccountIds,
resolveDiscordAccount,
resolveDefaultDiscordAccountId,
} from "../../extensions/discord/api.js";
export { inspectDiscordAccount } from "../../extensions/discord/api.js";
@@ -105,6 +108,8 @@ export {
createScheduledEventDiscord,
createThreadDiscord,
deleteChannelDiscord,
editDiscordComponentMessage,
registerBuiltDiscordComponentMessage,
deleteMessageDiscord,
editChannelDiscord,
editMessageDiscord,

View File

@@ -9,6 +9,7 @@ const require = createRequire(import.meta.url);
const rootSdk = require("./root-alias.cjs") as Record<string, unknown>;
const rootAliasPath = fileURLToPath(new URL("./root-alias.cjs", import.meta.url));
const rootAliasSource = fs.readFileSync(rootAliasPath, "utf-8");
const packageJsonPath = fileURLToPath(new URL("../../package.json", import.meta.url));
type EmptySchema = {
safeParse: (value: unknown) =>
@@ -196,6 +197,14 @@ describe("plugin-sdk root alias", () => {
expect(rootSdk.__esModule).toBe(true);
});
it("publishes the Discord plugin-sdk subpath", () => {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as {
exports?: Record<string, unknown>;
};
expect(packageJson.exports?.["./plugin-sdk/discord"]).toBeDefined();
});
it("preserves reflection semantics for lazily resolved exports", { timeout: 240_000 }, () => {
expect("resolveControlCommandGate" in rootSdk).toBe(true);
expect("onDiagnosticEvent" in rootSdk).toBe(true);

View File

@@ -97,7 +97,6 @@ describe("plugin-sdk subpath exports", () => {
expect(pluginSdkSubpaths).not.toContain("bluebubbles");
expect(pluginSdkSubpaths).not.toContain("compat");
expect(pluginSdkSubpaths).not.toContain("device-pair");
expect(pluginSdkSubpaths).not.toContain("discord");
expect(pluginSdkSubpaths).not.toContain("feishu");
expect(pluginSdkSubpaths).not.toContain("google");
expect(pluginSdkSubpaths).not.toContain("googlechat");
@@ -132,7 +131,6 @@ describe("plugin-sdk subpath exports", () => {
expect(pluginSdkSubpaths).not.toContain("secret-input-runtime");
expect(pluginSdkSubpaths).not.toContain("secret-input-schema");
expect(pluginSdkSubpaths).not.toContain("zai");
expect(pluginSdkSubpaths).not.toContain("discord-core");
expect(pluginSdkSubpaths).not.toContain("slack-core");
expect(pluginSdkSubpaths).not.toContain("provider-model-definitions");
});
@@ -220,6 +218,14 @@ describe("plugin-sdk subpath exports", () => {
expect(typeof runtimeSdk.createLoggerBackedRuntime).toBe("function");
});
it("exports Discord component helpers from the dedicated subpath", async () => {
const discordSdk = await import("openclaw/plugin-sdk/discord");
expect(typeof discordSdk.buildDiscordComponentMessage).toBe("function");
expect(typeof discordSdk.editDiscordComponentMessage).toBe("function");
expect(typeof discordSdk.registerBuiltDiscordComponentMessage).toBe("function");
expect(typeof discordSdk.resolveDiscordAccount).toBe("function");
});
it("exports channel identity and session helpers from stronger existing homes", () => {
expect(typeof routingSdk.normalizeMessageChannel).toBe("function");
expect(typeof routingSdk.resolveGatewayMessageChannel).toBe("function");

View File

@@ -313,6 +313,58 @@ describe("plugin interactive handlers", () => {
});
});
it("acknowledges matched Discord interactions before awaiting plugin handlers", async () => {
const callOrder: string[] = [];
const handler = vi.fn(async () => {
callOrder.push("handler");
expect(callOrder).toEqual(["ack", "handler"]);
return { handled: true };
});
expect(
registerPluginInteractiveHandler("codex-plugin", {
channel: "discord",
namespace: "codex",
handler,
}),
).toEqual({ ok: true });
await expect(
dispatchPluginInteractiveHandler({
channel: "discord",
data: "codex:approve:thread-1",
interactionId: "ix-ack-1",
ctx: {
accountId: "default",
interactionId: "ix-ack-1",
conversationId: "channel-1",
parentConversationId: "parent-1",
guildId: "guild-1",
senderId: "user-1",
senderUsername: "ada",
auth: { isAuthorizedSender: true },
interaction: {
kind: "button",
messageId: "message-1",
},
},
respond: {
acknowledge: vi.fn(async () => {}),
reply: vi.fn(async () => {}),
followUp: vi.fn(async () => {}),
editMessage: vi.fn(async () => {}),
clearComponents: vi.fn(async () => {}),
},
onMatched: async () => {
callOrder.push("ack");
},
}),
).resolves.toEqual({
matched: true,
handled: true,
duplicate: false,
});
});
it("routes Slack interactions by namespace and dedupes interaction ids", async () => {
const handler = vi.fn(async () => ({ handled: true }));
expect(

View File

@@ -168,6 +168,7 @@ export async function dispatchPluginInteractiveHandler(params: {
clearButtons: () => Promise<void>;
deleteMessage: () => Promise<void>;
};
onMatched?: () => Promise<void> | void;
}): Promise<InteractiveDispatchResult>;
export async function dispatchPluginInteractiveHandler(params: {
channel: "discord";
@@ -175,6 +176,7 @@ export async function dispatchPluginInteractiveHandler(params: {
interactionId: string;
ctx: DiscordInteractiveDispatchContext;
respond: PluginInteractiveDiscordHandlerContext["respond"];
onMatched?: () => Promise<void> | void;
}): Promise<InteractiveDispatchResult>;
export async function dispatchPluginInteractiveHandler(params: {
channel: "slack";
@@ -182,6 +184,7 @@ export async function dispatchPluginInteractiveHandler(params: {
interactionId: string;
ctx: SlackInteractiveDispatchContext;
respond: PluginInteractiveSlackHandlerContext["respond"];
onMatched?: () => Promise<void> | void;
}): Promise<InteractiveDispatchResult>;
export async function dispatchPluginInteractiveHandler(params: {
channel: "telegram" | "discord" | "slack";
@@ -205,6 +208,7 @@ export async function dispatchPluginInteractiveHandler(params: {
}
| PluginInteractiveDiscordHandlerContext["respond"]
| PluginInteractiveSlackHandlerContext["respond"];
onMatched?: () => Promise<void> | void;
}): Promise<InteractiveDispatchResult> {
const match = resolveNamespaceMatch(params.channel, params.data);
if (!match) {
@@ -217,6 +221,8 @@ export async function dispatchPluginInteractiveHandler(params: {
return { matched: true, handled: true, duplicate: true };
}
await params.onMatched?.();
let result:
| ReturnType<PluginInteractiveTelegramHandlerRegistration["handler"]>
| ReturnType<PluginInteractiveDiscordHandlerRegistration["handler"]>