/** * Tests shared approval helpers used by gateway method handlers. */ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { GATEWAY_CLIENT_IDS } from "../../../packages/gateway-protocol/src/client-info.js"; import { createDeferredCore } from "../../shared/deferred.js"; import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; import { ExecApprovalManager } from "../exec-approval-manager.js"; import { createTestApprovalManager } from "../exec-approval-manager.test-support.js"; import { bindApprovalReviewerDeviceIds, handleApprovalResolve, handleApprovalWaitDecision, handlePendingApprovalRequest, isApprovalRecordVisibleToClient, registerPendingApprovalRecord, } from "./approval-shared.js"; import type { GatewayClient, GatewayRequestContext } from "./types.js"; const hasApprovalTurnSourceRouteMock = vi.hoisted(() => vi.fn(() => true)); const prepareApprovalChannelCustodyMock = vi.hoisted(() => vi.fn()); vi.mock("../../infra/approval-turn-source.js", () => ({ hasApprovalTurnSourceRoute: hasApprovalTurnSourceRouteMock, })); vi.mock("../approval-channel-custody.js", () => ({ prepareApprovalChannelCustody: prepareApprovalChannelCustodyMock, })); type ApprovalClientLookup = NonNullable; function createApprovalClient(params: { connId: string; clientId: string; deviceId?: string; scopes?: string[]; approvalRuntime?: boolean; }): GatewayClient { return { connId: params.connId, connect: { client: { id: params.clientId }, device: params.deviceId ? { id: params.deviceId } : undefined, scopes: params.scopes ?? ["operator.approvals"], }, ...(params.approvalRuntime ? { internal: { approvalRuntime: true } } : {}), } as GatewayClient; } function createApprovalClientLookup(clients: GatewayClient[]): ApprovalClientLookup { return (opts = {}) => new Set( clients .filter((client) => { if (opts.excludeConnId && client.connId === opts.excludeConnId) { return false; } return opts.filter?.(client, opts.record) ?? true; }) .map((client) => client.connId) .filter((connId): connId is string => typeof connId === "string" && connId.length > 0), ); } describe("handlePendingApprovalRequest", () => { afterEach(() => { hasApprovalTurnSourceRouteMock.mockClear(); }); it.for([ { name: "allows operator.admin clients to see requester-bound approvals", recordId: "approval-admin-visible", requestedBy: { requestedByDeviceId: "device-owner", requestedByConnId: "conn-owner", requestedByClientId: "client-owner", }, client: { connId: "conn-admin", clientId: "client-admin", deviceId: "device-admin", scopes: ["operator.admin"], }, expected: true, }, { name: "does not allow approval-scoped clients to see no-device gateway-client approvals from another connection", recordId: "approval-gateway-client-visible", requestedBy: { requestedByConnId: "conn-gateway", requestedByClientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, }, client: { connId: "conn-mobile", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }, expected: false, }, ...[ ["Control UI", GATEWAY_CLIENT_IDS.CONTROL_UI], ["WebChat UI", GATEWAY_CLIENT_IDS.WEBCHAT_UI], ["WebChat", GATEWAY_CLIENT_IDS.WEBCHAT], ].map(([label, clientId]) => ({ name: `does not allow approval-scoped clients to see no-device ${label} approvals from another connection`, recordId: `approval-${clientId}-visible`, requestedBy: { requestedByConnId: "conn-browser-ui", requestedByClientId: clientId, }, client: { connId: "conn-mobile", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }, expected: false, })), { name: "does not allow approval-scoped clients to see device-bound gateway-client approvals from another device", recordId: "approval-gateway-device-visible", requestedBy: { requestedByDeviceId: "device-gateway", requestedByConnId: "conn-gateway", requestedByClientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, }, client: { connId: "conn-mobile", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.approvals"], }, expected: false, }, { name: "allows gateway-client approval runtimes to see requester-bound approvals", recordId: "approval-delivery-runtime-visible", requestedBy: { requestedByDeviceId: "device-owner", requestedByConnId: "conn-owner", requestedByClientId: "client-owner", }, client: { connId: "conn-delivery-runtime", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, scopes: ["operator.approvals"], approvalRuntime: true, }, expected: true, }, { name: "does not trust gateway-client ids without the approval runtime marker", recordId: "approval-delivery-runtime-spoof-hidden", requestedBy: { requestedByDeviceId: "device-owner", requestedByConnId: "conn-owner", requestedByClientId: "client-owner", }, client: { connId: "conn-spoofed-runtime", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, scopes: ["operator.approvals"], }, expected: false, }, { name: "does not widen non-gateway no-device approvals to matching client ids", recordId: "approval-other-client-hidden", requestedBy: { requestedByConnId: "conn-requester", requestedByClientId: "client-owner", }, client: { connId: "conn-mobile", clientId: "client-owner", scopes: ["operator.approvals"], }, expected: false, }, ])("$name", ({ recordId, requestedBy, client, expected }, testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create({ command: "echo ok" }, 60_000, recordId); Object.assign(record, requestedBy); expect( isApprovalRecordVisibleToClient({ record, client: createApprovalClient(client), }), ).toBe(expected); }); it("allows approval-scoped reviewer devices to see approvals requested by the backend runtime", (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-reviewer-device-visible", ); record.requestedByDeviceId = "device-gateway-runtime"; record.requestedByConnId = "conn-gateway-runtime"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; bindApprovalReviewerDeviceIds({ record, deviceIds: [" device-mobile ", "device-mobile"], }); expect(record.approvalReviewerDeviceIds).toEqual(["device-mobile"]); expect( isApprovalRecordVisibleToClient({ record, client: createApprovalClient({ connId: "conn-mobile", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.approvals"], }), }), ).toBe(true); }); it("does not allow reviewer devices without approval scope to see approvals", (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-reviewer-device-scope-hidden", ); record.requestedByDeviceId = "device-gateway-runtime"; record.requestedByConnId = "conn-gateway-runtime"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; bindApprovalReviewerDeviceIds({ record, deviceIds: ["device-mobile"] }); expect( isApprovalRecordVisibleToClient({ record, client: createApprovalClient({ connId: "conn-mobile", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.read"], }), }), ).toBe(false); }); it("does not widen explicitly reviewer-bound approvals to another device", (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-reviewer-device-isolated", ); bindApprovalReviewerDeviceIds({ record, deviceIds: ["device-mobile"] }); expect( isApprovalRecordVisibleToClient({ record, client: createApprovalClient({ connId: "conn-other", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-other", scopes: ["operator.approvals"], }), }), ).toBe(false); }); it.for([ { name: "reports an active approval client instead of the manual turn-source route", route: "client", id: "approval-with-client", request: { command: "echo ok", turnSourceChannel: "feishu", turnSourceAccountId: "work" }, }, { name: "counts an instance-local approval subscriber as a delivery route", route: "subscriber", id: "approval-internal-route", request: { command: "echo ok" }, }, { name: "checks plugin turn-source routes with plugin approval kind", route: "plugin", id: "plugin-turn-source-kind", request: { command: "plugin approval", title: "Plugin approval", description: "Review the plugin action", turnSourceChannel: "whatsapp", turnSourceAccountId: "default", }, }, { name: "keeps register-only approval requests pending without a delivery route", route: "register-only", id: "approval-register-only", request: { command: "echo ok" }, }, ] as const)("$name", async ({ route, id, request }, testContext) => { if (route === "register-only") { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); } const manager = createTestApprovalManager(testContext, { approvalKind: route === "plugin" ? "plugin" : "exec", }); const record = manager.create(request, 60_000, id); void manager.register(record, 60_000); const responseSent = createDeferredCore(); const respond = vi.fn(() => responseSent.resolve()); const publishRequested = vi.fn(() => 1); const getApprovalClientConnIds = vi.fn(() => new Set()); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast: vi.fn(), hasExecApprovalClients: () => route === "client", ...(route === "subscriber" ? { approvalEvents: { publishRequested, publishResolved: vi.fn() } } : {}), ...(route === "plugin" ? { broadcastToConnIds: vi.fn(), getApprovalClientConnIds } : {}), } as unknown as GatewayRequestContext, requestEventName: route === "plugin" ? "plugin.approval.requested" : "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, approvalKind: route === "plugin" ? "plugin" : undefined, requireDeliveryRoute: route === "register-only" ? false : undefined, deliverRequest: () => false, }); try { await Promise.race([responseSent.promise, requestPromise]); if (route === "client") { expect(hasApprovalTurnSourceRouteMock).not.toHaveBeenCalled(); } else if (route === "subscriber") { expect(publishRequested).toHaveBeenCalledWith( "exec", expect.objectContaining({ id: record.id }), ); } else if (route === "plugin") { expect(getApprovalClientConnIds).toHaveBeenCalledWith( expect.objectContaining({ approvalKind: "plugin" }), ); expect(hasApprovalTurnSourceRouteMock).toHaveBeenCalledWith({ turnSourceChannel: "whatsapp", turnSourceAccountId: "default", approvalKind: "plugin", }); } else { expect(manager.getSnapshot(record.id)?.resolvedAtMs).toBeUndefined(); } expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id, status: "accepted", ...(route === "register-only" ? {} : { deliveryRoute: route === "plugin" ? "turn-source" : "approval-client" }), }), undefined, ); expect(manager.resolve(record.id, "allow-once")).toBe(true); await requestPromise; if (route === "register-only") { expect(manager.getSnapshot(record.id)?.resolvedBy).not.toBe("no-approval-route"); expect(respond).toHaveBeenLastCalledWith( true, expect.objectContaining({ id, decision: "allow-once" }), undefined, ); } } finally { manager.resolve(record.id, "deny"); await Promise.allSettled([requestPromise, manager.drain()]); } }); it("targets requested approval events to visible approval clients when available", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-visible", ); record.requestedByDeviceId = "device-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(["conn-owner-approval"]); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", }), createApprovalClient({ connId: "conn-other-approval", clientId: "client-other", deviceId: "device-other", }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-requester", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-visible" }), visibleConnIds, { dropIfSlow: true }, ); expect(manager.resolve(record.id, "allow-once")).toBe(true); await requestPromise; }); it("routes backend-runtime approval events to the authorized approval reviewer device", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-reviewer-device-event", ); record.requestedByDeviceId = "device-gateway-runtime"; record.requestedByConnId = "conn-gateway-runtime"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; bindApprovalReviewerDeviceIds({ record, deviceIds: ["device-mobile"] }); void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(["conn-mobile-approval"]); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", }), createApprovalClient({ connId: "conn-other-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-other", }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-gateway-runtime", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-reviewer-device-event" }), visibleConnIds, { dropIfSlow: true }, ); expect(manager.resolve(record.id, "allow-once")).toBe(true); await requestPromise; }); it("targets requester-bound approval events to gateway-client approval runtimes", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-delivery-runtime", ); record.requestedByDeviceId = "device-owner"; record.requestedByConnId = "conn-owner"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(["conn-delivery-runtime"]); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-delivery-runtime", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, approvalRuntime: true, }), createApprovalClient({ connId: "conn-other-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-other", }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-owner", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-delivery-runtime" }), visibleConnIds, { dropIfSlow: true }, ); expect(manager.resolve(record.id, "allow-once")).toBe(true); await requestPromise; }); it("does not target no-device gateway-client approvals to unrelated approval-scoped clients", async (testContext) => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-gateway-mobile", ); record.requestedByConnId = "conn-gateway"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-gateway", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, }), createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-gateway", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-gateway-mobile" }), visibleConnIds, { dropIfSlow: true }, ); await requestPromise; expect(manager.getSnapshot(record.id)?.resolvedBy).toBe("no-approval-route"); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: "approval-gateway-mobile", decision: null }), undefined, ); }); it("returns a concurrent first answer when no-route denial loses after delivery", async (testContext) => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-no-route-race", ); void manager.register(record, 60_000); const respond = vi.fn(); const { promise: delivery, resolve: finishDelivery } = createDeferredCore(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast: vi.fn(), hasExecApprovalClients: () => false, } as unknown as GatewayRequestContext, clientConnId: "conn-requester", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => delivery, }); await Promise.resolve(); expect(manager.resolve(record.id, "allow-once", "control-ui")).toBe(true); finishDelivery(false); await requestPromise; expect(manager.getSnapshot(record.id)).toMatchObject({ decision: "allow-once", resolvedBy: "control-ui", }); expect(respond).toHaveBeenCalledTimes(1); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: record.id, decision: "allow-once" }), undefined, ); }); it("retains a concurrent approval binding through a delayed requester handoff", async (testContext) => { vi.useFakeTimers(); try { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-delayed-handoff", ); void manager.register(record, 60_000); const respond = vi.fn(); const afterDecision = vi.fn(); const { promise: delivery, resolve: finishDelivery } = createDeferredCore(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast: vi.fn(), hasExecApprovalClients: () => false, } as unknown as GatewayRequestContext, requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => delivery, afterDecision, }); await Promise.resolve(); expect(manager.resolve(record.id, "allow-once", "control-ui")).toBe(true); await vi.advanceTimersByTimeAsync(16_000); expect(manager.getLiveSnapshot(record.id)).toMatchObject({ decision: "allow-once", resolvedBy: "control-ui", }); expect(afterDecision).not.toHaveBeenCalled(); finishDelivery(true); await requestPromise; expect(afterDecision).toHaveBeenCalledWith( "allow-once", expect.objectContaining({ id: record.id }), ); expect(respond).toHaveBeenLastCalledWith( true, expect.objectContaining({ id: record.id, decision: "allow-once" }), undefined, ); expect(manager.getLiveSnapshot(record.id)).not.toBeNull(); await vi.advanceTimersByTimeAsync(14_999); expect(manager.getLiveSnapshot(record.id)).not.toBeNull(); await vi.advanceTimersByTimeAsync(1); expect(manager.getLiveSnapshot(record.id)).toBeNull(); } finally { vi.useRealTimers(); } }); it("expires suppressed requests instead of retaining a hidden turn-source route", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo cron", turnSourceChannel: "discord", turnSourceAccountId: "default", }, 60_000, "approval-suppressed-turn-source", ); void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const deliverRequest = vi.fn(() => true); await handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, hasExecApprovalClients: () => true, } as unknown as GatewayRequestContext, requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, suppressDelivery: true, deliverRequest, }); expect(broadcast).not.toHaveBeenCalled(); expect(deliverRequest).not.toHaveBeenCalled(); expect(hasApprovalTurnSourceRouteMock).not.toHaveBeenCalled(); expect(manager.getSnapshot(record.id)).toMatchObject({ resolvedBy: "no-approval-route", terminalReason: "no-route", }); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: record.id, decision: null }), undefined, ); }); it("keeps clients-only requests pending for approval clients without chat delivery", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo cron", turnSourceChannel: "discord", turnSourceAccountId: "default", }, 60_000, "approval-clients-only-pending", ); void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const deliverRequest = vi.fn(() => true); const publishRequested = vi.fn(() => 1); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, hasExecApprovalClients: () => true, approvalEvents: { publishRequested, publishResolved: vi.fn() }, } as unknown as GatewayRequestContext, requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverToApprovalClientsOnly: true, deliverRequest, }); await Promise.resolve(); expect(broadcast).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: record.id }), expect.objectContaining({ dropIfSlow: true }), ); expect(deliverRequest).not.toHaveBeenCalled(); expect(publishRequested).not.toHaveBeenCalled(); expect(hasApprovalTurnSourceRouteMock).not.toHaveBeenCalled(); expect(manager.getSnapshot(record.id)?.resolvedAtMs).toBeUndefined(); expect(manager.resolve(record.id, "allow-once")).toBe(true); await requestPromise; expect(respond).toHaveBeenLastCalledWith( true, expect.objectContaining({ id: record.id, decision: "allow-once" }), undefined, ); }); it("expires clients-only requests as no-route when no approval client is connected", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo cron", turnSourceChannel: "discord", turnSourceAccountId: "default", }, 60_000, "approval-clients-only-no-route", ); void manager.register(record, 60_000); const respond = vi.fn(); const afterDecision = vi.fn(); const deliverRequest = vi.fn(() => true); const publishRequested = vi.fn(() => 1); await handlePendingApprovalRequest({ manager, record, respond, context: { broadcast: vi.fn(), hasExecApprovalClients: () => false, approvalEvents: { publishRequested, publishResolved: vi.fn() }, } as unknown as GatewayRequestContext, requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverToApprovalClientsOnly: true, deliverRequest, afterDecision, }); expect(afterDecision).not.toHaveBeenCalled(); expect(deliverRequest).not.toHaveBeenCalled(); expect(publishRequested).not.toHaveBeenCalled(); expect(hasApprovalTurnSourceRouteMock).not.toHaveBeenCalled(); expect(manager.getSnapshot(record.id)).toMatchObject({ resolvedBy: "no-approval-route", terminalReason: "no-route", }); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: record.id, decision: null }), undefined, ); }); it("does not target no-device browser UI approvals to unrelated approval-scoped clients", async (testContext) => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-control-ui-mobile", ); record.requestedByConnId = "conn-control-ui"; record.requestedByClientId = GATEWAY_CLIENT_IDS.CONTROL_UI; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-control-ui", clientId: GATEWAY_CLIENT_IDS.CONTROL_UI, }), createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-control-ui", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-control-ui-mobile" }), visibleConnIds, { dropIfSlow: true }, ); await requestPromise; expect(manager.getSnapshot(record.id)?.resolvedBy).toBe("no-approval-route"); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: "approval-control-ui-mobile", decision: null }), undefined, ); }); it("does not target device-bound gateway-client approvals to unrelated approval-scoped clients", async (testContext) => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-gateway-device-mobile", ); record.requestedByDeviceId = "device-gateway"; record.requestedByConnId = "conn-gateway"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-gateway", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, deviceId: "device-gateway", }), createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.approvals"], }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-gateway", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-gateway-device-mobile" }), visibleConnIds, { dropIfSlow: true }, ); await requestPromise; expect(manager.getSnapshot(record.id)?.resolvedBy).toBe("no-approval-route"); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: "approval-gateway-device-mobile", decision: null }), undefined, ); }); it("does not target no-device approvals by self-declared client id", async (testContext) => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-no-device", ); record.requestedByConnId = "conn-requester"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(); const requestPromise = handlePendingApprovalRequest({ manager, record, respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-requester", clientId: "client-owner", }), createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", }), createApprovalClient({ connId: "conn-other-approval", clientId: "client-other", }), ]), ), hasExecApprovalClients: vi.fn(() => { throw new Error("expected visibility-filtered approval client lookup"); }), } as unknown as GatewayRequestContext, clientConnId: "conn-requester", requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, }); await Promise.resolve(); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.requested", expect.objectContaining({ id: "approval-no-device" }), visibleConnIds, { dropIfSlow: true }, ); await requestPromise; expect(manager.getSnapshot(record.id)?.resolvedBy).toBe("no-approval-route"); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: "approval-no-device", decision: null }), undefined, ); }); it("does not resolve no-device approvals by self-declared client id", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-no-device-resolve", ); record.requestedByConnId = "conn-requester"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); const respond = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast: vi.fn(), broadcastToConnIds: vi.fn(), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-other", clientId: "client-owner", scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "INVALID_REQUEST", message: "unknown or expired approval id", }), ); expect(manager.getSnapshot(record.id)?.decision).toBeUndefined(); }); it("does not wait on decisions for approvals hidden from the caller", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-wait-hidden", ); record.requestedByDeviceId = "device-owner"; record.requestedByConnId = "conn-owner"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); expect(manager.resolve(record.id, "allow-once")).toBe(true); const respond = vi.fn(); await handleApprovalWaitDecision({ manager, inputId: record.id, respond, client: createApprovalClient({ connId: "conn-other", clientId: "client-other", deviceId: "device-other", scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "INVALID_REQUEST", message: "approval expired or not found", }), ); }); it("allows visible callers to wait for approval decisions", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-wait-visible", ); record.requestedByDeviceId = "device-owner"; record.requestedByConnId = "conn-owner"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); expect(manager.resolve(record.id, "deny")).toBe(true); const respond = vi.fn(); await handleApprovalWaitDecision({ manager, inputId: record.id, respond, client: createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: "approval-wait-visible", decision: "deny", }), undefined, ); }); it("releases run-aborted waiters without changing timeout terminal state", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-wait-terminal-")); const manager = new ExecApprovalManager({ approvalKind: "exec", persistence: { runtimeEpoch: "approval-shared-wait-terminal", databaseOptions: { path: path.join(tempDir, "state.sqlite") }, }, }); const record = manager.create( { command: "echo ok", runId: "run-aborted" }, 60_000, "approval-wait-run-aborted", ); record.requestedByDeviceId = "device-owner"; record.requestedByConnId = "conn-owner"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const waiting = handleApprovalWaitDecision({ manager, inputId: record.id, respond, client: createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", scopes: ["operator.approvals"], }), }); expect( manager.forceDenyDetailed( record.id, "run-aborted", { kind: "system", id: null }, "cancelled", ), ).toMatchObject({ outcome: "denied" }); await waiting; expect(respond).toHaveBeenCalledWith( true, expect.objectContaining({ id: record.id, decision: null, terminalReason: "run-aborted", }), undefined, ); const timeoutRecord = manager.create( { command: "echo later", runId: "run-timeout" }, 60_000, "approval-wait-timeout", ); timeoutRecord.requestedByDeviceId = "device-owner"; timeoutRecord.requestedByConnId = "conn-owner"; timeoutRecord.requestedByClientId = "client-owner"; void manager.register(timeoutRecord, 60_000); const timeoutRespond = vi.fn(); const timeoutWaiting = handleApprovalWaitDecision({ manager, inputId: timeoutRecord.id, respond: timeoutRespond, client: createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", }), }); expect(manager.expire(timeoutRecord.id)).toBe(true); await timeoutWaiting; expect(timeoutRespond).toHaveBeenCalledWith( true, expect.objectContaining({ id: timeoutRecord.id, decision: null, terminalReason: "timeout", }), undefined, ); const allowedRecord = manager.create( { command: "echo allowed", runId: "run-allowed-before-abort" }, 60_000, "approval-wait-allowed-before-abort", ); allowedRecord.requestedByDeviceId = "device-owner"; allowedRecord.requestedByConnId = "conn-owner"; allowedRecord.requestedByClientId = "client-owner"; void manager.register(allowedRecord, 60_000); expect(manager.resolve(allowedRecord.id, "allow-once")).toBe(true); const allowedRespond = vi.fn(); await handleApprovalWaitDecision({ manager, inputId: allowedRecord.id, respond: allowedRespond, client: createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", }), resolveTerminalReason: () => "run-aborted", }); expect(allowedRespond).toHaveBeenCalledWith( true, expect.objectContaining({ id: allowedRecord.id, decision: "allow-once", terminalReason: "run-aborted", }), undefined, ); closeOpenClawStateDatabaseForTest(); fs.rmSync(tempDir, { force: true, recursive: true }); }); it("does not allow approval-scoped clients to resolve no-device gateway-client approvals from another connection", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-gateway-resolve", ); record.requestedByConnId = "conn-gateway"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), ]), ), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "INVALID_REQUEST", message: "unknown or expired approval id", }), ); expect(manager.getSnapshot(record.id)?.decision).toBeUndefined(); }); it("does not allow approval-scoped clients to resolve no-device browser UI approvals from another connection", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-control-ui-resolve", ); record.requestedByConnId = "conn-control-ui"; record.requestedByClientId = GATEWAY_CLIENT_IDS.CONTROL_UI; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), ]), ), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "INVALID_REQUEST", message: "unknown or expired approval id", }), ); expect(manager.getSnapshot(record.id)?.decision).toBeUndefined(); }); it("does not allow approval-scoped clients to resolve device-bound gateway-client approvals from another device", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-gateway-device-resolve", ); record.requestedByDeviceId = "device-gateway"; record.requestedByConnId = "conn-gateway"; record.requestedByClientId = GATEWAY_CLIENT_IDS.GATEWAY_CLIENT; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.approvals"], }), ]), ), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-mobile-approval", clientId: GATEWAY_CLIENT_IDS.IOS_APP, deviceId: "device-mobile", scopes: ["operator.approvals"], }), }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "INVALID_REQUEST", message: "unknown or expired approval id", }), ); expect(manager.getSnapshot(record.id)?.decision).toBeUndefined(); }); it("allows gateway-client approval runtimes to resolve requester-bound approvals", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-delivery-runtime-resolve", ); record.requestedByDeviceId = "device-owner"; record.requestedByConnId = "conn-owner"; record.requestedByClientId = "client-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-delivery-runtime", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, scopes: ["operator.approvals"], approvalRuntime: true, }), ]), ), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-delivery-runtime", clientId: GATEWAY_CLIENT_IDS.GATEWAY_CLIENT, scopes: ["operator.approvals"], approvalRuntime: true, }), }); expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined); expect(manager.getSnapshot(record.id)?.decision).toBe("allow-once"); }); it("filters legacy prefix candidates by channel custody before resolving", async (testContext) => { const manager = createTestApprovalManager(testContext); const owned = manager.create({ command: "owned" }, 60_000, "approval-prefix-owned"); const foreign = manager.create({ command: "foreign" }, 60_000, "approval-prefix-foreign"); void manager.register(owned, 60_000); void manager.register(foreign, 60_000); prepareApprovalChannelCustodyMock.mockReturnValueOnce({ resolverId: "telegram:ops", authorizes: (request: { request: { command: string } }) => request.request.command === "owned", }); const respond = vi.fn(); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: "approval-prefix", decision: "deny", reviewer: { channel: "telegram", accountId: "ops", senderId: "owner" }, respond, context: { broadcast: vi.fn(), broadcastToConnIds: vi.fn(), getRuntimeConfig: () => ({}), } as unknown as GatewayRequestContext, client: null, exposeAmbiguousPrefixError: true, }); expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined); expect(manager.getSnapshot(owned.id)?.decision).toBe("deny"); expect(manager.getSnapshot(foreign.id)?.decision).toBeUndefined(); }); it("targets resolved approval events to visible approval clients when available", async (testContext) => { const manager = createTestApprovalManager(testContext); const record = manager.create( { command: "echo ok", }, 60_000, "approval-resolved-visible", ); record.requestedByDeviceId = "device-owner"; void manager.register(record, 60_000); const respond = vi.fn(); const broadcast = vi.fn(); const broadcastToConnIds = vi.fn(); const visibleConnIds = new Set(["conn-owner-approval"]); await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "allow-once", respond, context: { broadcast, broadcastToConnIds, getApprovalClientConnIds: vi.fn( createApprovalClientLookup([ createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", }), createApprovalClient({ connId: "conn-other-approval", clientId: "client-other", deviceId: "device-other", }), ]), ), } as unknown as GatewayRequestContext, client: createApprovalClient({ connId: "conn-owner-approval", clientId: "client-owner", deviceId: "device-owner", }), }); expect(respond).toHaveBeenCalledWith(true, { ok: true }, undefined); expect(broadcast).not.toHaveBeenCalled(); expect(broadcastToConnIds).toHaveBeenCalledWith( "exec.approval.resolved", expect.objectContaining({ id: "approval-resolved-visible" }), visibleConnIds, { dropIfSlow: true }, ); }); it("sanitizes durable registration failures while retaining server diagnostics", () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-register-failure-")); const databasePath = path.join(tempDir, "state.sqlite"); fs.mkdirSync(databasePath); const manager = new ExecApprovalManager({ approvalKind: "exec", persistence: { runtimeEpoch: "approval-shared-register-failure", databaseOptions: { path: databasePath }, }, }); const record = manager.create({ command: "echo safe" }, 60_000, "registration-failure"); const respond = vi.fn(); const logError = vi.fn(); try { expect( registerPendingApprovalRecord({ manager, record, timeoutMs: 60_000, respond, context: { logGateway: { error: logError } } as unknown as GatewayRequestContext, }), ).toBeUndefined(); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "UNAVAILABLE", message: "approval request unavailable" }), ); expect(JSON.stringify(respond.mock.calls)).not.toContain(databasePath); expect(logError).toHaveBeenCalledTimes(1); } finally { closeOpenClawStateDatabaseForTest(); fs.rmSync(tempDir, { force: true, recursive: true }); } }); it("sanitizes a no-route storage failure while failing the waiter closed", async () => { hasApprovalTurnSourceRouteMock.mockReturnValueOnce(false); const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-route-failure-")); const databasePath = path.join(tempDir, "state.sqlite"); const manager = new ExecApprovalManager({ approvalKind: "exec", persistence: { runtimeEpoch: "approval-shared-route-failure", databaseOptions: { path: databasePath }, }, }); const record = manager.create({ command: "echo safe" }, 60_000, "route-failure"); const decisionPromise = manager.register(record, 60_000); const afterDecision = vi.fn(); closeOpenClawStateDatabaseForTest(); fs.rmSync(databasePath, { force: true }); fs.mkdirSync(databasePath); const respond = vi.fn(); const logError = vi.fn(); try { await handlePendingApprovalRequest({ manager, record, respond, context: { broadcast: vi.fn(), hasExecApprovalClients: () => false, logGateway: { error: logError }, } as unknown as GatewayRequestContext, requestEventName: "exec.approval.requested", requestEvent: { id: record.id, request: record.request, createdAtMs: record.createdAtMs, expiresAtMs: record.expiresAtMs, }, twoPhase: true, deliverRequest: () => false, afterDecision, }); await manager.drain(); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "UNAVAILABLE", message: "approval request unavailable" }), ); expect(respond).toHaveBeenCalledOnce(); expect(afterDecision).not.toHaveBeenCalled(); expect(JSON.stringify(respond.mock.calls)).not.toContain(databasePath); expect(logError).toHaveBeenCalledTimes(1); await expect(decisionPromise).resolves.toBe("deny"); } finally { await manager.drain(); closeOpenClawStateDatabaseForTest(); fs.rmSync(tempDir, { force: true, recursive: true }); } }); it("sanitizes durable resolve failures while failing the waiter closed", async () => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-approval-resolve-failure-")); const databasePath = path.join(tempDir, "state.sqlite"); const manager = new ExecApprovalManager({ approvalKind: "exec", persistence: { runtimeEpoch: "approval-shared-resolve-failure", databaseOptions: { path: databasePath }, }, }); const record = manager.create({ command: "echo safe" }, 60_000, "resolve-failure"); const decisionPromise = manager.register(record, 60_000); closeOpenClawStateDatabaseForTest(); fs.rmSync(databasePath, { force: true }); fs.mkdirSync(databasePath); const respond = vi.fn(); const logError = vi.fn(); try { await handleApprovalResolve({ approvalKind: "exec", manager, inputId: record.id, decision: "deny", respond, context: { broadcast: vi.fn(), broadcastToConnIds: vi.fn(), logGateway: { error: logError }, } as unknown as GatewayRequestContext, client: null, }); expect(respond).toHaveBeenCalledWith( false, undefined, expect.objectContaining({ code: "UNAVAILABLE", message: "approval resolve unavailable" }), ); expect(JSON.stringify(respond.mock.calls)).not.toContain(databasePath); expect(logError).toHaveBeenCalledTimes(1); await expect(decisionPromise).resolves.toBe("deny"); } finally { closeOpenClawStateDatabaseForTest(); fs.rmSync(tempDir, { force: true, recursive: true }); } }); }); /* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */