diff --git a/src/gateway/server-methods/plugin-approval.test.ts b/src/gateway/server-methods/plugin-approval.test.ts index 84ff0d7659e0..cbdde5106745 100644 --- a/src/gateway/server-methods/plugin-approval.test.ts +++ b/src/gateway/server-methods/plugin-approval.test.ts @@ -8,6 +8,50 @@ function createManager() { return new ExecApprovalManager(); } +function createLogGatewayMock() { + return { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }; +} + +function createApprovalContext( + params: { + broadcast?: ReturnType; + hasExecApprovalClients?: GatewayRequestHandlerOptions["context"]["hasExecApprovalClients"]; + } = {}, +): GatewayRequestHandlerOptions["context"] { + return { + broadcast: params.broadcast ?? vi.fn(), + logGateway: createLogGatewayMock(), + hasExecApprovalClients: params.hasExecApprovalClients ?? (() => true), + } as unknown as GatewayRequestHandlerOptions["context"]; +} + +function createClient( + params: { + connId?: string; + clientId?: string; + displayName?: string; + deviceId?: string; + scopes?: string[]; + } = {}, +): GatewayRequestHandlerOptions["client"] { + const connect: Record = { + client: { + id: params.clientId ?? "test-client", + displayName: params.displayName ?? "Test Client", + }, + }; + if (params.deviceId) { + connect.device = { id: params.deviceId }; + } + if (params.scopes) { + connect.scopes = params.scopes; + } + return { + connId: params.connId ?? "conn-test-client", + connect, + } as unknown as GatewayRequestHandlerOptions["client"]; +} + function createMockOptions( method: string, params: Record, @@ -16,29 +60,16 @@ function createMockOptions( return { req: { method, params, id: "req-1" }, params, - client: { - connId: "conn-test-client", - connect: { - client: { id: "test-client", displayName: "Test Client" }, - }, - }, + client: createClient(), isWebchatConnect: () => false, respond: vi.fn(), - context: { - broadcast: vi.fn(), - logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, - hasExecApprovalClients: () => true, - }, + context: createApprovalContext(), ...overrides, } as unknown as GatewayRequestHandlerOptions; } function createNoExecApprovalContext(): GatewayRequestHandlerOptions["context"] { - return { - broadcast: vi.fn(), - logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, - hasExecApprovalClients: () => false, - } as unknown as GatewayRequestHandlerOptions["context"]; + return createApprovalContext({ hasExecApprovalClients: () => false }); } type MockCallSource = { @@ -59,15 +90,15 @@ function requireArray(value: unknown, label: string): unknown[] { return value as unknown[]; } -function mockCall(source: MockCallSource, index: number, label: string) { - const call = source.mock.calls[index]; +function mockCall(source: unknown, index: number, label: string) { + const call = (source as MockCallSource).mock.calls[index]; if (!call) { throw new Error(`Expected ${label}`); } return call; } -function responseCall(source: MockCallSource, index = 0) { +function responseCall(source: unknown, index = 0) { const call = mockCall(source, index, `response call ${index}`); return { ok: call[0], @@ -76,16 +107,17 @@ function responseCall(source: MockCallSource, index = 0) { }; } -function responseResult(source: MockCallSource, index = 0) { +function responseResult(source: unknown, index = 0) { return requireRecord(responseCall(source, index).result, `response result ${index}`); } -function responseError(source: MockCallSource, index = 0) { +function responseError(source: unknown, index = 0) { return requireRecord(responseCall(source, index).error, `response error ${index}`); } -function acceptedResult(source: MockCallSource) { - const call = Array.from(source.mock.calls).find((candidate) => { +function acceptedResult(source: unknown) { + const callSource = source as MockCallSource; + const call = Array.from(callSource.mock.calls).find((candidate) => { const result = candidate[1]; return typeof result === "object" && result !== null && "status" in result ? (result as Record).status === "accepted" @@ -97,12 +129,74 @@ function acceptedResult(source: MockCallSource) { return requireRecord(call[1], "accepted response result"); } -function acceptedApprovalId(source: MockCallSource) { +function acceptedApprovalId(source: unknown) { const id = acceptedResult(source).id; expect(id, "accepted approval id").toBeTypeOf("string"); return id as string; } +function expectResponseOk(source: unknown, index = 0) { + const call = responseCall(source, index); + expect(call.ok).toBe(true); + expect(call.error).toBeUndefined(); + return requireRecord(call.result, `response result ${index}`); +} + +function expectResponseRejected(source: unknown, index = 0) { + expect(responseCall(source, index).ok).toBe(false); + return responseError(source, index); +} + +async function waitForAcceptedApproval(respond: unknown) { + await vi.waitFor(() => { + const accepted = acceptedResult(respond); + expect(accepted.status).toBe("accepted"); + expect(accepted.id).toBeTypeOf("string"); + }); + return acceptedApprovalId(respond); +} + +function createOwnedClient(owner: "owner" | "other" = "owner") { + return createClient({ + connId: `conn-${owner}`, + clientId: `client-${owner}`, + deviceId: `device-${owner}`, + }); +} + +function registerApproval( + approvalManager: ExecApprovalManager, + params: { + title?: string; + description?: string; + id?: string; + allowedDecisions?: PluginApprovalRequestPayload["allowedDecisions"]; + } = {}, +) { + const request = { + title: params.title ?? "T", + description: params.description ?? "D", + ...(params.allowedDecisions ? { allowedDecisions: params.allowedDecisions } : {}), + }; + const record = params.id + ? approvalManager.create(request, 60_000, params.id) + : approvalManager.create(request, 60_000); + void approvalManager.register(record, 60_000); + return record; +} + +function registerOwnedApproval( + approvalManager: ExecApprovalManager, + params: { title: string; id?: string; owner?: "owner" | "other" }, +) { + const record = registerApproval(approvalManager, { title: params.title, id: params.id }); + const owner = params.owner ?? "owner"; + record.requestedByDeviceId = `device-${owner}`; + record.requestedByConnId = `conn-${owner}`; + record.requestedByClientId = `client-${owner}`; + return record; +} + function expectPluginApprovalId(value: unknown, label: string): string { expect(value, label).toBeTypeOf("string"); if (typeof value !== "string") { @@ -120,11 +214,7 @@ function expectPluginApprovalId(value: unknown, label: string): string { } function broadcastCall(opts: GatewayRequestHandlerOptions, index = 0) { - const call = mockCall( - opts.context.broadcast as unknown as MockCallSource, - index, - "broadcast call", - ); + const call = mockCall(opts.context.broadcast, index, "broadcast call"); return { event: call?.[0], payload: requireRecord(call?.[1], "broadcast payload"), @@ -137,6 +227,25 @@ const invalidParamMethodCases = [ { method: "plugin.approval.resolve" }, ] as const; +const invalidRequestCases = [ + { + name: "invalid severity value", + params: { title: "T", description: "D", severity: "extreme" }, + }, + { + name: "title exceeding max length", + params: { title: "x".repeat(81), description: "D" }, + }, + { + name: "description exceeding max length", + params: { title: "T", description: "x".repeat(257) }, + }, + { + name: "timeoutMs exceeding max", + params: { title: "T", description: "D", timeoutMs: 700_000 }, + }, +] as const; + describe("createPluginApprovalHandlers", () => { let manager: ExecApprovalManager; @@ -163,9 +272,8 @@ describe("createPluginApprovalHandlers", () => { const handlers = createPluginApprovalHandlers(manager); const opts = createMockOptions(method, {}); await handlers[method](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseCall(opts.respond as unknown as MockCallSource).result).toBeUndefined(); - expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string"); + expect(responseCall(opts.respond).result).toBeUndefined(); + expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string"); }); }); @@ -188,12 +296,7 @@ describe("createPluginApprovalHandlers", () => { // Instead, let it run and resolve the approval after the accepted response. const handlerPromise = handlers["plugin.approval.request"](opts); - // Wait for the twoPhase "accepted" response - await vi.waitFor(() => { - const accepted = acceptedResult(respond as unknown as MockCallSource); - expect(accepted.status).toBe("accepted"); - expect(accepted.id).toBeTypeOf("string"); - }); + const approvalId = await waitForAcceptedApproval(respond); const requestedBroadcast = broadcastCall(opts); expect(requestedBroadcast.event).toBe("plugin.approval.requested"); @@ -201,18 +304,15 @@ describe("createPluginApprovalHandlers", () => { expect(requestedBroadcast.options).toEqual({ dropIfSlow: true }); // Resolve the approval so the handler can complete - const approvalId = acceptedApprovalId(respond as unknown as MockCallSource); expect(manager.getSnapshot(approvalId)?.requestedByClientId).toBe("test-client"); manager.resolve(approvalId, "allow-once"); await handlerPromise; // Final response with decision - const finalResult = responseResult(respond as unknown as MockCallSource, 1); - expect(responseCall(respond as unknown as MockCallSource, 1).ok).toBe(true); + const finalResult = expectResponseOk(respond, 1); expect(finalResult.id).toBe(approvalId); expect(finalResult.decision).toBe("allow-once"); - expect(responseCall(respond as unknown as MockCallSource, 1).error).toBeUndefined(); }); it("expires immediately when no approval route", async () => { @@ -228,9 +328,7 @@ describe("createPluginApprovalHandlers", () => { }, ); await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true); - expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBeNull(); - expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined(); + expect(expectResponseOk(opts.respond).decision).toBeNull(); }); it("passes caller connId to hasExecApprovalClients to exclude self", async () => { @@ -240,15 +338,12 @@ describe("createPluginApprovalHandlers", () => { "plugin.approval.request", { title: "T", description: "D" }, { - client: { + client: createClient({ connId: "backend-conn-42", - connect: { client: { id: "test", displayName: "Test" } }, - } as unknown as GatewayRequestHandlerOptions["client"], - context: { - broadcast: vi.fn(), - logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, - hasExecApprovalClients, - } as unknown as GatewayRequestHandlerOptions["context"], + clientId: "test", + displayName: "Test", + }), + context: createApprovalContext({ hasExecApprovalClients }), }, ); await handlers["plugin.approval.request"](opts); @@ -271,23 +366,12 @@ describe("createPluginApprovalHandlers", () => { }, { respond, - context: { - broadcast: vi.fn(), - logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, - hasExecApprovalClients: () => false, - } as unknown as GatewayRequestHandlerOptions["context"], + context: createApprovalContext({ hasExecApprovalClients: () => false }), }, ); const requestPromise = handlers["plugin.approval.request"](opts); - - await vi.waitFor(() => { - const accepted = acceptedResult(respond as unknown as MockCallSource); - expect(accepted.status).toBe("accepted"); - expect(accepted.id).toBeTypeOf("string"); - }); - - const approvalId = acceptedApprovalId(respond as unknown as MockCallSource); + const approvalId = await waitForAcceptedApproval(respond); manager.resolve(approvalId, "allow-once"); await requestPromise; @@ -296,50 +380,11 @@ describe("createPluginApprovalHandlers", () => { } }); - it("rejects invalid severity value", async () => { + it.each(invalidRequestCases)("rejects $name", async ({ params }) => { const handlers = createPluginApprovalHandlers(manager); - const opts = createMockOptions("plugin.approval.request", { - title: "T", - description: "D", - severity: "extreme", - }); + const opts = createMockOptions("plugin.approval.request", params); await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string"); - }); - - it("rejects title exceeding max length", async () => { - const handlers = createPluginApprovalHandlers(manager); - const opts = createMockOptions("plugin.approval.request", { - title: "x".repeat(81), - description: "D", - }); - await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string"); - }); - - it("rejects description exceeding max length", async () => { - const handlers = createPluginApprovalHandlers(manager); - const opts = createMockOptions("plugin.approval.request", { - title: "T", - description: "x".repeat(257), - }); - await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string"); - }); - - it("rejects timeoutMs exceeding max", async () => { - const handlers = createPluginApprovalHandlers(manager); - const opts = createMockOptions("plugin.approval.request", { - title: "T", - description: "D", - timeoutMs: 700_000, - }); - await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).code).toBeTypeOf("string"); + expect(expectResponseRejected(opts.respond).code).toBeTypeOf("string"); }); it("generates plugin-prefixed IDs", async () => { @@ -350,15 +395,11 @@ describe("createPluginApprovalHandlers", () => { { title: "T", description: "D" }, { respond, - context: { - broadcast: vi.fn(), - logGateway: { error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }, - hasExecApprovalClients: () => false, - } as unknown as GatewayRequestHandlerOptions["context"], + context: createApprovalContext({ hasExecApprovalClients: () => false }), }, ); await handlers["plugin.approval.request"](opts); - const result = responseResult(respond as unknown as MockCallSource); + const result = responseResult(respond); expectPluginApprovalId(result?.id, "generated plugin approval id"); }); @@ -390,10 +431,8 @@ describe("createPluginApprovalHandlers", () => { description: "D", }); await handlers["plugin.approval.request"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).message).toContain( - "unexpected property", - ); + expect(responseCall(opts.respond).ok).toBe(false); + expect(responseError(opts.respond).message).toContain("unexpected property"); }); it("stores scoped allowed decisions on plugin approval requests", async () => { @@ -411,13 +450,7 @@ describe("createPluginApprovalHandlers", () => { ); const handlerPromise = handlers["plugin.approval.request"](opts); - await vi.waitFor(() => { - const accepted = acceptedResult(respond as unknown as MockCallSource); - expect(accepted.status).toBe("accepted"); - expect(accepted.id).toBeTypeOf("string"); - }); - - const approvalId = acceptedApprovalId(respond as unknown as MockCallSource); + const approvalId = await waitForAcceptedApproval(respond); expect(manager.getSnapshot(approvalId)?.request.allowedDecisions).toEqual([ "allow-once", "deny", @@ -442,30 +475,23 @@ describe("createPluginApprovalHandlers", () => { ); const handlerPromise = handlers["plugin.approval.request"](requestOpts); - await vi.waitFor(() => { - const accepted = acceptedResult(respond as unknown as MockCallSource); - expect(accepted.status).toBe("accepted"); - expect(accepted.id).toBeTypeOf("string"); - }); + const approvalId = await waitForAcceptedApproval(respond); const listRespond = vi.fn(); await handlers["plugin.approval.list"]( createMockOptions("plugin.approval.list", {}, { respond: listRespond }), ); - expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true); - const approvals = requireArray( - responseCall(listRespond as unknown as MockCallSource).result, - "approval list", - ); + const listCall = responseCall(listRespond); + expect(listCall.ok).toBe(true); + expect(listCall.error).toBeUndefined(); + const approvals = requireArray(listCall.result, "approval list"); expect(approvals).toHaveLength(1); const approval = requireRecord(approvals[0], "approval"); const listedApprovalId = expectPluginApprovalId(approval.id, "listed approval id"); const request = requireRecord(approval.request, "approval request"); expect(request.title).toBe("Sensitive action"); expect(request.description).toBe("Desc"); - expect(responseCall(listRespond as unknown as MockCallSource).error).toBeUndefined(); - const approvalId = acceptedApprovalId(respond as unknown as MockCallSource); expect(listedApprovalId).toBe(approvalId); manager.resolve(approvalId, "allow-once"); await handlerPromise; @@ -473,21 +499,12 @@ describe("createPluginApprovalHandlers", () => { it("lists only plugin approvals owned by the caller", async () => { const handlers = createPluginApprovalHandlers(manager); - const visible = manager.create( - { title: "Visible", description: "D" }, - 60_000, - "plugin:visible", - ); - visible.requestedByDeviceId = "device-owner"; - visible.requestedByConnId = "conn-owner"; - visible.requestedByClientId = "client-owner"; - void manager.register(visible, 60_000); - - const hidden = manager.create({ title: "Hidden", description: "D" }, 60_000, "plugin:hidden"); - hidden.requestedByDeviceId = "device-other"; - hidden.requestedByConnId = "conn-other"; - hidden.requestedByClientId = "client-other"; - void manager.register(hidden, 60_000); + registerOwnedApproval(manager, { title: "Visible", id: "plugin:visible" }); + registerOwnedApproval(manager, { + title: "Hidden", + id: "plugin:hidden", + owner: "other", + }); const listRespond = vi.fn(); await handlers["plugin.approval.list"]( @@ -496,22 +513,14 @@ describe("createPluginApprovalHandlers", () => { {}, { respond: listRespond, - client: { - connId: "conn-owner", - connect: { - client: { id: "client-owner" }, - device: { id: "device-owner" }, - }, - } as unknown as GatewayRequestHandlerOptions["client"], + client: createOwnedClient(), }, ), ); - expect(responseCall(listRespond as unknown as MockCallSource).ok).toBe(true); - const approvals = requireArray( - responseCall(listRespond as unknown as MockCallSource).result, - "approval list", - ); + const listCall = responseCall(listRespond); + expect(listCall.ok).toBe(true); + const approvals = requireArray(listCall.result, "approval list"); expect(approvals.map((entry) => requireRecord(entry, "approval").id)).toEqual([ "plugin:visible", ]); @@ -523,89 +532,67 @@ describe("createPluginApprovalHandlers", () => { const handlers = createPluginApprovalHandlers(manager); const opts = createMockOptions("plugin.approval.waitDecision", {}); await handlers["plugin.approval.waitDecision"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).message).toContain( - "id is required", - ); + expect(expectResponseRejected(opts.respond).message).toContain("id is required"); }); it("returns not found for unknown id", async () => { const handlers = createPluginApprovalHandlers(manager); const opts = createMockOptions("plugin.approval.waitDecision", { id: "unknown" }); await handlers["plugin.approval.waitDecision"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).message).toContain( - "expired or not found", - ); + expect(expectResponseRejected(opts.respond).message).toContain("expired or not found"); }); it("returns not found for approvals hidden from the caller", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create({ title: "T", description: "D" }, 60_000); - record.requestedByDeviceId = "device-owner"; - record.requestedByConnId = "conn-owner"; - record.requestedByClientId = "client-owner"; - void manager.register(record, 60_000); + const record = registerOwnedApproval(manager, { title: "T" }); manager.resolve(record.id, "allow-once"); const opts = createMockOptions( "plugin.approval.waitDecision", { id: record.id }, { - client: { + client: createClient({ connId: "conn-other", - connect: { - client: { id: "client-other" }, - device: { id: "device-other" }, - scopes: ["operator.approvals"], - }, - } as unknown as GatewayRequestHandlerOptions["client"], + clientId: "client-other", + deviceId: "device-other", + scopes: ["operator.approvals"], + }), }, ); await handlers["plugin.approval.waitDecision"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).message).toContain( - "expired or not found", - ); + expect(expectResponseRejected(opts.respond).message).toContain("expired or not found"); }); it("returns decision when resolved", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create({ title: "T", description: "D" }, 60_000); - void manager.register(record, 60_000); + const record = registerApproval(manager); // Resolve before waiting manager.resolve(record.id, "allow-once"); const opts = createMockOptions("plugin.approval.waitDecision", { id: record.id }); await handlers["plugin.approval.waitDecision"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(true); - expect(responseResult(opts.respond as unknown as MockCallSource).id).toBe(record.id); - expect(responseResult(opts.respond as unknown as MockCallSource).decision).toBe("allow-once"); - expect(responseCall(opts.respond as unknown as MockCallSource).error).toBeUndefined(); + const result = expectResponseOk(opts.respond); + expect(result.id).toBe(record.id); + expect(result.decision).toBe("allow-once"); }); }); describe("plugin.approval.resolve", () => { it("rejects invalid decision", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create({ title: "T", description: "D" }, 60_000); - void manager.register(record, 60_000); + const record = registerApproval(manager); const opts = createMockOptions("plugin.approval.resolve", { id: record.id, decision: "invalid", }); await handlers["plugin.approval.resolve"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - expect(responseError(opts.respond as unknown as MockCallSource).message).toBe( - "invalid decision", - ); + expect(expectResponseRejected(opts.respond).message).toBe("invalid decision"); }); it("resolves a pending approval", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create({ title: "T", description: "D" }, 60_000); - void manager.register(record, 60_000); + const record = registerApproval(manager); const opts = createMockOptions("plugin.approval.resolve", { id: record.id, @@ -622,33 +609,17 @@ describe("createPluginApprovalHandlers", () => { it("resolves only plugin approvals owned by the caller", async () => { const handlers = createPluginApprovalHandlers(manager); - const visible = manager.create( - { title: "Visible", description: "D" }, - 60_000, - "plugin:abcd-visible", - ); - visible.requestedByDeviceId = "device-owner"; - visible.requestedByConnId = "conn-owner"; - visible.requestedByClientId = "client-owner"; - void manager.register(visible, 60_000); + const visible = registerOwnedApproval(manager, { + title: "Visible", + id: "plugin:abcd-visible", + }); + const hidden = registerOwnedApproval(manager, { + title: "Hidden", + id: "plugin:abcd-hidden", + owner: "other", + }); - const hidden = manager.create( - { title: "Hidden", description: "D" }, - 60_000, - "plugin:abcd-hidden", - ); - hidden.requestedByDeviceId = "device-other"; - hidden.requestedByConnId = "conn-other"; - hidden.requestedByClientId = "client-other"; - void manager.register(hidden, 60_000); - - const ownerClient = { - connId: "conn-owner", - connect: { - client: { id: "client-owner" }, - device: { id: "device-owner" }, - }, - } as unknown as GatewayRequestHandlerOptions["client"]; + const ownerClient = createOwnedClient(); const resolveRespond = vi.fn(); await handlers["plugin.approval.resolve"]( createMockOptions( @@ -681,8 +652,7 @@ describe("createPluginApprovalHandlers", () => { }, ), ); - expect(responseCall(hiddenRespond as unknown as MockCallSource).ok).toBe(false); - const error = responseError(hiddenRespond as unknown as MockCallSource); + const error = expectResponseRejected(hiddenRespond); expect(error.code).toBe("INVALID_REQUEST"); expect(error.message).toBe("unknown or expired approval id"); expect(manager.getSnapshot(hidden.id)?.decision).toBeUndefined(); @@ -690,23 +660,16 @@ describe("createPluginApprovalHandlers", () => { it("rejects decisions outside plugin approval allowed decisions", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create( - { - title: "T", - description: "D", - allowedDecisions: ["allow-once", "deny"], - }, - 60_000, - ); - void manager.register(record, 60_000); + const record = registerApproval(manager, { + allowedDecisions: ["allow-once", "deny"], + }); const opts = createMockOptions("plugin.approval.resolve", { id: record.id, decision: "allow-always", }); await handlers["plugin.approval.resolve"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - const error = responseError(opts.respond as unknown as MockCallSource); + const error = expectResponseRejected(opts.respond); expect(error.code).toBe("INVALID_REQUEST"); expect(error.message).toBe("allow-always is unavailable for this plugin approval"); expect(error.details).toEqual({ allowedDecisions: ["allow-once", "deny"] }); @@ -720,8 +683,7 @@ describe("createPluginApprovalHandlers", () => { decision: "allow-once", }); await handlers["plugin.approval.resolve"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - const error = responseError(opts.respond as unknown as MockCallSource); + const error = expectResponseRejected(opts.respond); expect(error.code).toBe("INVALID_REQUEST"); expect(error.message).toContain("unknown or expired"); expect(requireRecord(error.details, "error details").reason).toBe("APPROVAL_NOT_FOUND"); @@ -729,8 +691,7 @@ describe("createPluginApprovalHandlers", () => { it("accepts unique short id prefixes", async () => { const handlers = createPluginApprovalHandlers(manager); - const record = manager.create({ title: "T", description: "D" }, 60_000, "abcdef-1234"); - void manager.register(record, 60_000); + const record = registerApproval(manager, { id: "abcdef-1234" }); const opts = createMockOptions("plugin.approval.resolve", { id: "abcdef", @@ -743,18 +704,15 @@ describe("createPluginApprovalHandlers", () => { it("does not leak candidate ids when prefixes are ambiguous", async () => { const handlers = createPluginApprovalHandlers(manager); - const recordA = manager.create({ title: "A", description: "D" }, 60_000, "plugin:abc-1111"); - const recordB = manager.create({ title: "B", description: "D" }, 60_000, "plugin:abc-2222"); - void manager.register(recordA, 60_000); - void manager.register(recordB, 60_000); + registerApproval(manager, { title: "A", id: "plugin:abc-1111" }); + registerApproval(manager, { title: "B", id: "plugin:abc-2222" }); const opts = createMockOptions("plugin.approval.resolve", { id: "plugin:abc", decision: "deny", }); await handlers["plugin.approval.resolve"](opts); - expect(responseCall(opts.respond as unknown as MockCallSource).ok).toBe(false); - const error = responseError(opts.respond as unknown as MockCallSource); + const error = expectResponseRejected(opts.respond); expect(error.code).toBe("INVALID_REQUEST"); expect(error.message).toBe("unknown or expired approval id"); });