diff --git a/src/auto-reply/reply/commands-approve.test.ts b/src/auto-reply/reply/commands-approve.test.ts index bd84b4ff3a0..07589c17a58 100644 --- a/src/auto-reply/reply/commands-approve.test.ts +++ b/src/auto-reply/reply/commands-approve.test.ts @@ -25,6 +25,38 @@ vi.mock("../../globals.js", () => ({ logVerbose: vi.fn(), })); +function requireRecord(value: unknown, label: string): Record { + expect(value).toBeTypeOf("object"); + expect(value).not.toBeNull(); + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`expected ${label} to be an object`); + } + return value as Record; +} + +function gatewayRequest(callIndex = 0) { + const call = callGatewayMock.mock.calls[callIndex] as unknown[] | undefined; + expect(call).toBeDefined(); + if (!call) { + throw new Error(`expected gateway call ${callIndex}`); + } + return requireRecord(call[0], `gateway call ${callIndex} request`); +} + +function expectGatewayResolveCall(params: { + callIndex?: number; + method: string; + id: string; + decision?: string; +}) { + const request = gatewayRequest(params.callIndex ?? 0); + expect(request.method).toBe(params.method); + expect(request.params).toEqual({ + id: params.id, + decision: params.decision ?? "allow-once", + }); +} + function normalizeDiscordDirectApproverId(value: string | number): string | undefined { const normalized = String(value) .trim() @@ -468,12 +500,7 @@ describe("handleApproveCommand", () => { expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc" }); }); it("accepts bare approve text for Slack-style manual approvals", async () => { @@ -496,12 +523,7 @@ describe("handleApproveCommand", () => { expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc" }); }); it("accepts Telegram /approve from configured approvers even when chat access is otherwise blocked", async () => { @@ -516,12 +538,7 @@ describe("handleApproveCommand", () => { const result = await handleApproveCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc12345" }); }); it("honors the configured default account for omitted-account /approve auth", async () => { @@ -563,12 +580,7 @@ describe("handleApproveCommand", () => { const result = await handleApproveCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc12345" }); }); it("accepts Signal /approve from configured approvers even when chat access is otherwise blocked", async () => { @@ -594,12 +606,7 @@ describe("handleApproveCommand", () => { const result = await handleApproveCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc12345" }); }); it("does not treat implicit default approval auth as a bypass for unauthorized senders", async () => { @@ -706,12 +713,7 @@ describe("handleApproveCommand", () => { const result = await handleApproveCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc12345" }); }); it("accepts Telegram /approve from exec target recipients when native approvals are disabled", async () => { @@ -744,12 +746,7 @@ describe("handleApproveCommand", () => { const result = await handleApproveCommand(params, true); expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "exec.approval.resolve", id: "abc12345" }); }); it("requires configured Discord approvers for exec approvals", async () => { @@ -800,13 +797,11 @@ describe("handleApproveCommand", () => { expect(result?.shouldContinue, testCase.name).toBe(false); expect(result?.reply?.text, testCase.name).toContain(testCase.expectedText); expect(callGatewayMock, testCase.name).toHaveBeenCalledTimes(testCase.expectedGatewayCalls); - if ("expectedMethod" in testCase) { - expect(callGatewayMock, testCase.name).toHaveBeenCalledWith( - expect.objectContaining({ - method: testCase.expectedMethod, - params: { id: "abc12345", decision: "allow-once" }, - }), - ); + if ("expectedMethod" in testCase && testCase.expectedMethod) { + expectGatewayResolveCall({ + method: testCase.expectedMethod, + id: "abc12345", + }); } } }); @@ -859,13 +854,11 @@ describe("handleApproveCommand", () => { expect(result?.shouldContinue).toBe(false); expect(result?.reply?.text).toContain("Approval allow-once submitted"); expect(callGatewayMock).toHaveBeenCalledTimes(2); - expect(callGatewayMock).toHaveBeenNthCalledWith( - 2, - expect.objectContaining({ - method: "plugin.approval.resolve", - params: { id: "legacy-plugin-123", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ + callIndex: 1, + method: "plugin.approval.resolve", + id: "legacy-plugin-123", + }); }); it("returns the underlying not-found error for plugin-only approval routing", async () => { @@ -911,12 +904,7 @@ describe("handleApproveCommand", () => { expect(result?.reply?.text).toContain("Failed to submit approval"); expect(result?.reply?.text).toContain("unknown or expired approval id"); expect(callGatewayMock).toHaveBeenCalledTimes(1); - expect(callGatewayMock).toHaveBeenCalledWith( - expect.objectContaining({ - method: "plugin.approval.resolve", - params: { id: "abc123", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "plugin.approval.resolve", id: "abc123" }); }); it("requires configured Discord approvers for plugin approvals", async () => { @@ -952,12 +940,7 @@ describe("handleApproveCommand", () => { expect(result?.reply?.text, testCase.name).toContain(testCase.expectedText); expect(callGatewayMock, testCase.name).toHaveBeenCalledTimes(testCase.expectedGatewayCalls); if (testCase.expectedGatewayCalls > 0) { - expect(callGatewayMock, testCase.name).toHaveBeenCalledWith( - expect.objectContaining({ - method: "plugin.approval.resolve", - params: { id: "plugin:abc123", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ method: "plugin.approval.resolve", id: "plugin:abc123" }); } } }); @@ -1064,12 +1047,11 @@ describe("handleApproveCommand", () => { testCase.expectedGatewayCalls, ); if (testCase.expectedGatewayCalls > 0) { - expect(callGatewayMock, String(testCase.scopes)).toHaveBeenLastCalledWith( - expect.objectContaining({ - method: "exec.approval.resolve", - params: { id: "abc", decision: "allow-once" }, - }), - ); + expectGatewayResolveCall({ + callIndex: callGatewayMock.mock.calls.length - 1, + method: "exec.approval.resolve", + id: "abc", + }); } } });