mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
fix(discord): ignore stale exec approval clicks
This commit is contained in:
12
CHANGELOG.md
12
CHANGELOG.md
@@ -6,6 +6,18 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
## 2026.4.26
|
||||
|
||||
### Fixes
|
||||
|
||||
- Discord: keep late clicks on already-resolved exec approval buttons quiet when elevated mode auto-resolved the request, while still surfacing real approval submission failures. Fixes #66906. Thanks @rlerikse.
|
||||
- Gateway/Tailscale: let Tailscale-authenticated Control UI operator sessions with browser device identity skip the device-pairing round trip while still rejecting device-less and node-role connections. Refs #71986. Thanks @jokedul.
|
||||
- Doctor: honor `OPENCLAW_SERVICE_REPAIR_POLICY=external` by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd.
|
||||
- Agents/Discord: keep raw `Agent failed before reply` runner failures out of Discord group/channel chats and show detailed runner errors in direct chats only when `/verbose` is enabled. Thanks @codex.
|
||||
- Package: include patched dependency files in the published npm package so downstream installs can resolve `patchedDependencies`. (#69224) Thanks @gucasbrg and @vincentkoc.
|
||||
- Plugins/channels: treat malformed bundled channel plugin loaders that return `undefined` as unavailable instead of crashing config and help paths. Fixes #69044. Thanks @frankhli843 and @vincentkoc.
|
||||
- Scripts/watch: show corrupted dependency package-config recovery guidance when `gateway:watch` fails during watcher startup, without double-logging unrelated import failures. (#58780) Thanks @roytong9 and @vincentkoc.
|
||||
- Signal: read signal-cli RPC, health checks, and SSE events through Node's HTTP client so Node 24/25 fetch regressions do not break Signal sends or inbound events. Fixes #51716 and #53040. Thanks @Barukimang, @minupla, and @vincentkoc.
|
||||
- Skills/Docker: run npm-backed skill dependency installs with an OpenClaw-managed user prefix so non-root Docker images do not write to `/usr/local`. Fixes #59601. Thanks @chanjarster and @vincentkoc.
|
||||
|
||||
## 2026.4.25
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -87,7 +87,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
const interaction = createInteraction();
|
||||
const button = new ExecApprovalButton({
|
||||
getApprovers: () => ["123"],
|
||||
resolveApproval: async () => true,
|
||||
resolveApproval: async () => ({ ok: true }),
|
||||
});
|
||||
|
||||
await button.run(interaction, { id: "", action: "" });
|
||||
@@ -102,7 +102,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
const interaction = createInteraction({ userId: "999" });
|
||||
const button = new ExecApprovalButton({
|
||||
getApprovers: () => ["123"],
|
||||
resolveApproval: async () => true,
|
||||
resolveApproval: async () => ({ ok: true }),
|
||||
});
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "allow-once" });
|
||||
@@ -115,7 +115,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
|
||||
it("acknowledges and resolves valid approval clicks", async () => {
|
||||
const interaction = createInteraction();
|
||||
const resolveApproval = vi.fn(async () => true);
|
||||
const resolveApproval = vi.fn(async () => ({ ok: true }) as const);
|
||||
const button = new ExecApprovalButton({
|
||||
getApprovers: () => ["123"],
|
||||
resolveApproval,
|
||||
@@ -132,7 +132,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
const interaction = createInteraction();
|
||||
const button = new ExecApprovalButton({
|
||||
getApprovers: () => ["123"],
|
||||
resolveApproval: async () => false,
|
||||
resolveApproval: async () => ({ ok: false, reason: "error" }),
|
||||
});
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "deny" });
|
||||
@@ -144,6 +144,19 @@ describe("discord exec approval monitor helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps already-resolved approval clicks quiet", async () => {
|
||||
const interaction = createInteraction();
|
||||
const button = new ExecApprovalButton({
|
||||
getApprovers: () => ["123"],
|
||||
resolveApproval: async () => ({ ok: false, reason: "not-found" }),
|
||||
});
|
||||
|
||||
await button.run(interaction, { id: "abc", action: "allow-once" });
|
||||
|
||||
expect(interaction.acknowledge).toHaveBeenCalled();
|
||||
expect(interaction.followUp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("builds button context from config and routes resolution over gateway", async () => {
|
||||
const cfg = buildConfig({ enabled: true, approvers: ["123"] });
|
||||
resolveApprovalOverGatewayMock.mockResolvedValue(undefined);
|
||||
@@ -155,7 +168,7 @@ describe("discord exec approval monitor helpers", () => {
|
||||
});
|
||||
|
||||
expect(ctx.getApprovers()).toEqual(["123"]);
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toBe(true);
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toEqual({ ok: true });
|
||||
expect(resolveApprovalOverGatewayMock).toHaveBeenCalledWith({
|
||||
cfg,
|
||||
approvalId: "abc",
|
||||
@@ -173,6 +186,41 @@ describe("discord exec approval monitor helpers", () => {
|
||||
config: { enabled: true, approvers: ["123"] },
|
||||
});
|
||||
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toBe(false);
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "error",
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies structured approval-not-found gateway errors as stale clicks", async () => {
|
||||
const err = Object.assign(new Error("unknown or expired approval id"), {
|
||||
gatewayCode: "INVALID_REQUEST",
|
||||
details: { reason: "APPROVAL_NOT_FOUND" },
|
||||
});
|
||||
resolveApprovalOverGatewayMock.mockRejectedValue(err);
|
||||
const ctx = createDiscordExecApprovalButtonContext({
|
||||
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
|
||||
accountId: "default",
|
||||
config: { enabled: true, approvers: ["123"] },
|
||||
});
|
||||
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "not-found",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps message-only approval-not-found errors visible", async () => {
|
||||
resolveApprovalOverGatewayMock.mockRejectedValue(new Error("unknown or expired approval id"));
|
||||
const ctx = createDiscordExecApprovalButtonContext({
|
||||
cfg: buildConfig({ enabled: true, approvers: ["123"] }),
|
||||
accountId: "default",
|
||||
config: { enabled: true, approvers: ["123"] },
|
||||
});
|
||||
|
||||
await expect(ctx.resolveApproval("abc", "allow-once")).resolves.toEqual({
|
||||
ok: false,
|
||||
reason: "error",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -53,9 +53,30 @@ export function parseExecApprovalData(
|
||||
|
||||
export type ExecApprovalButtonContext = {
|
||||
getApprovers: () => string[];
|
||||
resolveApproval: (approvalId: string, decision: ExecApprovalDecision) => Promise<boolean>;
|
||||
resolveApproval: (
|
||||
approvalId: string,
|
||||
decision: ExecApprovalDecision,
|
||||
) => Promise<ExecApprovalResolveResult>;
|
||||
};
|
||||
|
||||
type ExecApprovalResolveResult = { ok: true } | { ok: false; reason: "error" | "not-found" };
|
||||
|
||||
function isStructuredApprovalNotFoundError(err: unknown): boolean {
|
||||
if (!err || typeof err !== "object") {
|
||||
return false;
|
||||
}
|
||||
const record = err as {
|
||||
gatewayCode?: unknown;
|
||||
details?: { reason?: unknown } | null;
|
||||
};
|
||||
if (record.gatewayCode === "APPROVAL_NOT_FOUND") {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
record.gatewayCode === "INVALID_REQUEST" && record.details?.reason === "APPROVAL_NOT_FOUND"
|
||||
);
|
||||
}
|
||||
|
||||
export class ExecApprovalButton extends Button {
|
||||
label = "execapproval";
|
||||
customId = "execapproval:seed=1";
|
||||
@@ -100,8 +121,8 @@ export class ExecApprovalButton extends Button {
|
||||
await interaction.acknowledge();
|
||||
} catch {}
|
||||
|
||||
const ok = await this.ctx.resolveApproval(parsed.approvalId, parsed.action);
|
||||
if (!ok) {
|
||||
const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.action);
|
||||
if (!result.ok && result.reason !== "not-found") {
|
||||
try {
|
||||
await interaction.followUp({
|
||||
content: `Failed to submit approval decision for **${decisionLabel}**. The request may have expired or already been resolved.`,
|
||||
@@ -138,9 +159,12 @@ export function createDiscordExecApprovalButtonContext(params: {
|
||||
gatewayUrl: params.gatewayUrl,
|
||||
clientDisplayName: `Discord approval (${params.accountId})`,
|
||||
});
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
return { ok: true };
|
||||
} catch (err) {
|
||||
return {
|
||||
ok: false,
|
||||
reason: isStructuredApprovalNotFoundError(err) ? "not-found" : "error",
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user