From 13cfb77c10f91e32c8a537c20669c8bb51f2f6b3 Mon Sep 17 00:00:00 2001 From: "Jason (Json)" <263060202+fuller-stack-dev@users.noreply.github.com> Date: Tue, 26 May 2026 20:56:30 -0600 Subject: [PATCH] fix: repair local approval resolution (#86771) --- .../discord/src/monitor/exec-approvals.test.ts | 8 ++++++-- .../discord/src/monitor/exec-approvals.ts | 7 +++++-- src/gateway/operator-approvals-client.test.ts | 17 ++++++++++++++++- src/gateway/operator-approvals-client.ts | 5 ++++- 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/extensions/discord/src/monitor/exec-approvals.test.ts b/extensions/discord/src/monitor/exec-approvals.test.ts index 76230533de2..95a22c06196 100644 --- a/extensions/discord/src/monitor/exec-approvals.test.ts +++ b/extensions/discord/src/monitor/exec-approvals.test.ts @@ -144,7 +144,7 @@ describe("discord exec approval monitor helpers", () => { }); }); - it("keeps already-resolved approval clicks quiet", async () => { + it("shows a follow-up for already-resolved approval clicks", async () => { const interaction = createInteraction(); const button = new ExecApprovalButton({ getApprovers: () => ["123"], @@ -154,7 +154,11 @@ describe("discord exec approval monitor helpers", () => { await button.run(interaction, { id: "abc", action: "allow-once" }); expect(interaction.acknowledge).toHaveBeenCalled(); - expect(interaction.followUp).not.toHaveBeenCalled(); + expect(interaction.followUp).toHaveBeenCalledWith({ + content: + "That approval request is no longer pending. It may have expired or already been resolved.", + ephemeral: true, + }); }); it("builds button context from config and routes resolution over gateway", async () => { diff --git a/extensions/discord/src/monitor/exec-approvals.ts b/extensions/discord/src/monitor/exec-approvals.ts index 224c17db92d..bcc17a56926 100644 --- a/extensions/discord/src/monitor/exec-approvals.ts +++ b/extensions/discord/src/monitor/exec-approvals.ts @@ -112,10 +112,13 @@ export class ExecApprovalButton extends Button { } catch {} const result = await this.ctx.resolveApproval(parsed.approvalId, parsed.action); - if (!result.ok && result.reason !== "not-found") { + if (!result.ok) { try { await interaction.followUp({ - content: `Failed to submit approval decision for **${decisionLabel}**. The request may have expired or already been resolved.`, + content: + result.reason === "not-found" + ? `That approval request is no longer pending. It may have expired or already been resolved.` + : `Failed to submit approval decision for **${decisionLabel}**. The request may have expired or already been resolved.`, ephemeral: true, }); } catch {} diff --git a/src/gateway/operator-approvals-client.test.ts b/src/gateway/operator-approvals-client.test.ts index 382cedc3268..a4e8fcaa13d 100644 --- a/src/gateway/operator-approvals-client.test.ts +++ b/src/gateway/operator-approvals-client.test.ts @@ -117,7 +117,7 @@ describe("withOperatorApprovalsGatewayClient", () => { expect(clientState.options?.deviceIdentity).toBeUndefined(); }); - it("omits approval runtime token for explicit gateway URL overrides", async () => { + it("keeps approval runtime token for loopback explicit gateway URL overrides", async () => { await withOperatorApprovalsGatewayClient( { config: {} as never, @@ -127,6 +127,21 @@ describe("withOperatorApprovalsGatewayClient", () => { async () => undefined, ); + expect(typeof clientState.options?.approvalRuntimeToken).toBe("string"); + }); + + it("omits approval runtime token for remote explicit gateway URL overrides", async () => { + bootstrapState.url = "wss://gateway.example/ws"; + + await withOperatorApprovalsGatewayClient( + { + config: {} as never, + gatewayUrl: "wss://gateway.example/ws", + clientDisplayName: "Matrix approval (@owner:example.org)", + }, + async () => undefined, + ); + expect(clientState.options).not.toHaveProperty("approvalRuntimeToken"); }); diff --git a/src/gateway/operator-approvals-client.ts b/src/gateway/operator-approvals-client.ts index d594e74c020..61c1f83ca90 100644 --- a/src/gateway/operator-approvals-client.ts +++ b/src/gateway/operator-approvals-client.ts @@ -44,12 +44,15 @@ export async function createOperatorApprovalsGatewayClient( gatewayUrl: params.gatewayUrl, env: process.env, }); + const shouldSendApprovalRuntimeToken = !params.gatewayUrl || isLoopbackGatewayUrl(bootstrap.url); return new GatewayClient({ url: bootstrap.url, token: bootstrap.auth.token, password: bootstrap.auth.password, - ...(params.gatewayUrl ? {} : { approvalRuntimeToken: getOperatorApprovalRuntimeToken() }), + ...(shouldSendApprovalRuntimeToken + ? { approvalRuntimeToken: getOperatorApprovalRuntimeToken() } + : {}), preauthHandshakeTimeoutMs: bootstrap.preauthHandshakeTimeoutMs, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, clientDisplayName: params.clientDisplayName,