diff --git a/src/slack/monitor/slash.command-arg-menus.test.ts b/src/slack/monitor/slash.command-arg-menus.test.ts index 600102b98..159319479 100644 --- a/src/slack/monitor/slash.command-arg-menus.test.ts +++ b/src/slack/monitor/slash.command-arg-menus.test.ts @@ -1,43 +1,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { registerSlackMonitorSlashCommands } from "./slash.js"; +import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.js"; -const dispatchMock = vi.fn(); -const readAllowFromStoreMock = vi.fn(); -const upsertPairingRequestMock = vi.fn(); -const resolveAgentRouteMock = vi.fn(); - -vi.mock("../../auto-reply/reply/provider-dispatcher.js", () => ({ - dispatchReplyWithDispatcher: (...args: unknown[]) => dispatchMock(...args), -})); - -vi.mock("../../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), - upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), -})); - -vi.mock("../../routing/resolve-route.js", () => ({ - resolveAgentRoute: (...args: unknown[]) => resolveAgentRouteMock(...args), -})); - -vi.mock("../../agents/identity.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveEffectiveMessagesConfig: () => ({ responsePrefix: "" }), - }; -}); +const { dispatchMock } = getSlackSlashMocks(); beforeEach(() => { - dispatchMock.mockReset().mockResolvedValue({ counts: { final: 1, tool: 0, block: 0 } }); - readAllowFromStoreMock.mockReset().mockResolvedValue([]); - upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); - resolveAgentRouteMock.mockReset().mockReturnValue({ - agentId: "main", - sessionKey: "session:1", - accountId: "acct", - }); + resetSlackSlashMocks(); }); +async function registerCommands(ctx: unknown, account: unknown) { + const { registerSlackMonitorSlashCommands } = await import("./slash.js"); + registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); +} + function encodeValue(parts: { command: string; arg: string; value: string; userId: string }) { return [ "cmdarg", @@ -99,7 +73,7 @@ function createHarness() { describe("Slack native command argument menus", () => { it("shows a button menu when required args are omitted", async () => { const { commands, ctx, account } = createHarness(); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const handler = commands.get("/usage"); if (!handler) { @@ -130,7 +104,7 @@ describe("Slack native command argument menus", () => { it("dispatches the command when a menu button is clicked", async () => { const { actions, ctx, account } = createHarness(); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const handler = actions.get("openclaw_cmdarg"); if (!handler) { @@ -158,7 +132,7 @@ describe("Slack native command argument menus", () => { it("rejects menu clicks from other users", async () => { const { actions, ctx, account } = createHarness(); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const handler = actions.get("openclaw_cmdarg"); if (!handler) { @@ -188,7 +162,7 @@ describe("Slack native command argument menus", () => { it("falls back to postEphemeral with token when respond is unavailable", async () => { const { actions, postEphemeral, ctx, account } = createHarness(); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const handler = actions.get("openclaw_cmdarg"); if (!handler) { @@ -212,7 +186,7 @@ describe("Slack native command argument menus", () => { it("treats malformed percent-encoding as an invalid button (no throw)", async () => { const { actions, postEphemeral, ctx, account } = createHarness(); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const handler = actions.get("openclaw_cmdarg"); if (!handler) { diff --git a/src/slack/monitor/slash.policy.test.ts b/src/slack/monitor/slash.policy.test.ts index 48e48bdf8..108ed91f2 100644 --- a/src/slack/monitor/slash.policy.test.ts +++ b/src/slack/monitor/slash.policy.test.ts @@ -1,43 +1,17 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { registerSlackMonitorSlashCommands } from "./slash.js"; +import { getSlackSlashMocks, resetSlackSlashMocks } from "./slash.test-harness.js"; -const dispatchMock = vi.fn(); -const readAllowFromStoreMock = vi.fn(); -const upsertPairingRequestMock = vi.fn(); -const resolveAgentRouteMock = vi.fn(); - -vi.mock("../../auto-reply/reply/provider-dispatcher.js", () => ({ - dispatchReplyWithDispatcher: (...args: unknown[]) => dispatchMock(...args), -})); - -vi.mock("../../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: (...args: unknown[]) => readAllowFromStoreMock(...args), - upsertChannelPairingRequest: (...args: unknown[]) => upsertPairingRequestMock(...args), -})); - -vi.mock("../../routing/resolve-route.js", () => ({ - resolveAgentRoute: (...args: unknown[]) => resolveAgentRouteMock(...args), -})); - -vi.mock("../../agents/identity.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveEffectiveMessagesConfig: () => ({ responsePrefix: "" }), - }; -}); +const { dispatchMock } = getSlackSlashMocks(); beforeEach(() => { - dispatchMock.mockReset().mockResolvedValue({ counts: { final: 1, tool: 0, block: 0 } }); - readAllowFromStoreMock.mockReset().mockResolvedValue([]); - upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); - resolveAgentRouteMock.mockReset().mockReturnValue({ - agentId: "main", - sessionKey: "session:1", - accountId: "acct", - }); + resetSlackSlashMocks(); }); +async function registerCommands(ctx: unknown, account: unknown) { + const { registerSlackMonitorSlashCommands } = await import("./slash.js"); + registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); +} + function createHarness(overrides?: { groupPolicy?: "open" | "allowlist"; channelsConfig?: Record; @@ -136,7 +110,7 @@ describe("slack slash commands channel policy", () => { channelId: "C_UNLISTED", channelName: "unlisted", }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, @@ -159,7 +133,7 @@ describe("slack slash commands channel policy", () => { channelId: "C_DENIED", channelName: "denied", }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, @@ -183,7 +157,7 @@ describe("slack slash commands channel policy", () => { channelId: "C_UNLISTED", channelName: "unlisted", }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, @@ -209,7 +183,7 @@ describe("slack slash commands access groups", () => { channelName: "unknown", resolveChannelName: async () => ({}), }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, @@ -233,7 +207,7 @@ describe("slack slash commands access groups", () => { channelName: "notdirectmessage", resolveChannelName: async () => ({}), }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, @@ -260,7 +234,7 @@ describe("slack slash commands access groups", () => { channelName: "directmessage", resolveChannelName: async () => ({ name: "directmessage", type: "im" }), }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); await runSlashHandler({ commands, @@ -286,7 +260,7 @@ describe("slack slash commands access groups", () => { channelName: "private", resolveChannelName: async () => ({}), }); - registerSlackMonitorSlashCommands({ ctx: ctx as never, account: account as never }); + await registerCommands(ctx, account); const { respond } = await runSlashHandler({ commands, diff --git a/src/slack/monitor/slash.test-harness.ts b/src/slack/monitor/slash.test-harness.ts new file mode 100644 index 000000000..20f4e65af --- /dev/null +++ b/src/slack/monitor/slash.test-harness.ts @@ -0,0 +1,44 @@ +import { vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + dispatchMock: vi.fn(), + readAllowFromStoreMock: vi.fn(), + upsertPairingRequestMock: vi.fn(), + resolveAgentRouteMock: vi.fn(), +})); + +vi.mock("../../auto-reply/reply/provider-dispatcher.js", () => ({ + dispatchReplyWithDispatcher: (...args: unknown[]) => mocks.dispatchMock(...args), +})); + +vi.mock("../../pairing/pairing-store.js", () => ({ + readChannelAllowFromStore: (...args: unknown[]) => mocks.readAllowFromStoreMock(...args), + upsertChannelPairingRequest: (...args: unknown[]) => mocks.upsertPairingRequestMock(...args), +})); + +vi.mock("../../routing/resolve-route.js", () => ({ + resolveAgentRoute: (...args: unknown[]) => mocks.resolveAgentRouteMock(...args), +})); + +vi.mock("../../agents/identity.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + resolveEffectiveMessagesConfig: () => ({ responsePrefix: "" }), + }; +}); + +export function getSlackSlashMocks() { + return mocks; +} + +export function resetSlackSlashMocks() { + mocks.dispatchMock.mockReset().mockResolvedValue({ counts: { final: 1, tool: 0, block: 0 } }); + mocks.readAllowFromStoreMock.mockReset().mockResolvedValue([]); + mocks.upsertPairingRequestMock.mockReset().mockResolvedValue({ code: "PAIRCODE", created: true }); + mocks.resolveAgentRouteMock.mockReset().mockReturnValue({ + agentId: "main", + sessionKey: "session:1", + accountId: "acct", + }); +}