From c37f65a449d96e06bfd35fddc0d6b326e2ededae Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 16 Feb 2026 17:06:20 +0000 Subject: [PATCH] refactor(tests): share harnesses for cli and monitor fixtures --- src/cli/qr-cli.test.ts | 68 +++++--------- src/discord/monitor.test.ts | 55 ++++------- src/infra/fetch.test.ts | 76 ++++++--------- src/tui/tui.submit-handler.test.ts | 92 +++++-------------- ...ssages-from-senders-allowfrom-list.test.ts | 83 ++++++----------- 5 files changed, 119 insertions(+), 255 deletions(-) diff --git a/src/cli/qr-cli.test.ts b/src/cli/qr-cli.test.ts index 361186e21f..78fdf1ecb9 100644 --- a/src/cli/qr-cli.test.ts +++ b/src/cli/qr-cli.test.ts @@ -27,6 +27,25 @@ vi.mock("qrcode-terminal", () => ({ const { registerQrCli } = await import("./qr-cli.js"); +function createRemoteQrConfig(params?: { withTailscale?: boolean }) { + return { + gateway: { + ...(params?.withTailscale ? { tailscale: { mode: "serve" } } : {}), + remote: { url: "wss://remote.example.com:444", token: "remote-tok" }, + auth: { mode: "token", token: "local-tok" }, + }, + plugins: { + entries: { + "device-pair": { + config: { + publicUrl: "wss://wrong.example.com:443", + }, + }, + }, + }, + }; +} + describe("registerQrCli", () => { beforeEach(() => { vi.clearAllMocks(); @@ -124,21 +143,7 @@ describe("registerQrCli", () => { }); it("uses gateway.remote.url when --remote is set (ignores device-pair publicUrl)", async () => { - loadConfig.mockReturnValue({ - gateway: { - remote: { url: "wss://remote.example.com:444", token: "remote-tok" }, - auth: { mode: "token", token: "local-tok" }, - }, - plugins: { - entries: { - "device-pair": { - config: { - publicUrl: "wss://wrong.example.com:443", - }, - }, - }, - }, - }); + loadConfig.mockReturnValue(createRemoteQrConfig()); const program = new Command(); registerQrCli(program); @@ -152,21 +157,7 @@ describe("registerQrCli", () => { }); it("reports gateway.remote.url as source in --remote json output", async () => { - loadConfig.mockReturnValue({ - gateway: { - remote: { url: "wss://remote.example.com:444", token: "remote-tok" }, - auth: { mode: "token", token: "local-tok" }, - }, - plugins: { - entries: { - "device-pair": { - config: { - publicUrl: "wss://wrong.example.com:443", - }, - }, - }, - }, - }); + loadConfig.mockReturnValue(createRemoteQrConfig()); const program = new Command(); registerQrCli(program); @@ -202,22 +193,7 @@ describe("registerQrCli", () => { }); it("prefers gateway.remote.url over tailscale when --remote is set", async () => { - loadConfig.mockReturnValue({ - gateway: { - tailscale: { mode: "serve" }, - remote: { url: "wss://remote.example.com:444", token: "remote-tok" }, - auth: { mode: "token", token: "local-tok" }, - }, - plugins: { - entries: { - "device-pair": { - config: { - publicUrl: "wss://wrong.example.com:443", - }, - }, - }, - }, - }); + loadConfig.mockReturnValue(createRemoteQrConfig({ withTailscale: true })); runCommandWithTimeout.mockResolvedValue({ code: 0, stdout: '{"Self":{"DNSName":"ts-host.tailnet.ts.net."}}', diff --git a/src/discord/monitor.test.ts b/src/discord/monitor.test.ts index e170387508..476405eb73 100644 --- a/src/discord/monitor.test.ts +++ b/src/discord/monitor.test.ts @@ -38,6 +38,22 @@ const makeEntries = ( return out; }; +function createAutoThreadMentionContext() { + const guildInfo: DiscordGuildEntryResolved = { + requireMention: true, + channels: { + general: { allow: true, autoThread: true }, + }, + }; + const channelConfig = resolveDiscordChannelConfig({ + guildInfo, + channelId: "1", + channelName: "General", + channelSlug: "general", + }); + return { guildInfo, channelConfig }; +} + describe("registerDiscordListener", () => { class FakeListener {} @@ -402,18 +418,7 @@ describe("discord mention gating", () => { }); it("does not require mention inside autoThread threads", () => { - const guildInfo: DiscordGuildEntryResolved = { - requireMention: true, - channels: { - general: { allow: true, autoThread: true }, - }, - }; - const channelConfig = resolveDiscordChannelConfig({ - guildInfo, - channelId: "1", - channelName: "General", - channelSlug: "general", - }); + const { guildInfo, channelConfig } = createAutoThreadMentionContext(); expect( resolveDiscordShouldRequireMention({ isGuildMessage: true, @@ -427,18 +432,7 @@ describe("discord mention gating", () => { }); it("requires mention inside user-created threads with autoThread enabled", () => { - const guildInfo: DiscordGuildEntryResolved = { - requireMention: true, - channels: { - general: { allow: true, autoThread: true }, - }, - }; - const channelConfig = resolveDiscordChannelConfig({ - guildInfo, - channelId: "1", - channelName: "General", - channelSlug: "general", - }); + const { guildInfo, channelConfig } = createAutoThreadMentionContext(); expect( resolveDiscordShouldRequireMention({ isGuildMessage: true, @@ -452,18 +446,7 @@ describe("discord mention gating", () => { }); it("requires mention when thread owner is unknown", () => { - const guildInfo: DiscordGuildEntryResolved = { - requireMention: true, - channels: { - general: { allow: true, autoThread: true }, - }, - }; - const channelConfig = resolveDiscordChannelConfig({ - guildInfo, - channelId: "1", - channelName: "General", - channelSlug: "general", - }); + const { guildInfo, channelConfig } = createAutoThreadMentionContext(); expect( resolveDiscordShouldRequireMention({ isGuildMessage: true, diff --git a/src/infra/fetch.test.ts b/src/infra/fetch.test.ts index 4af18b6a9e..b1ce6f383e 100644 --- a/src/infra/fetch.test.ts +++ b/src/infra/fetch.test.ts @@ -1,6 +1,31 @@ import { describe, expect, it, vi } from "vitest"; import { resolveFetch, wrapFetchWithAbortSignal } from "./fetch.js"; +function createForeignSignalHarness() { + let abortHandler: (() => void) | null = null; + const removeEventListener = vi.fn((event: string, handler: () => void) => { + if (event === "abort" && abortHandler === handler) { + abortHandler = null; + } + }); + + const fakeSignal = { + aborted: false, + addEventListener: (event: string, handler: () => void) => { + if (event === "abort") { + abortHandler = handler; + } + }, + removeEventListener, + } as AbortSignal; + + return { + fakeSignal, + removeEventListener, + triggerAbort: () => abortHandler?.(), + }; +} + describe("wrapFetchWithAbortSignal", () => { it("adds duplex for requests with a body", async () => { let seenInit: RequestInit | undefined; @@ -25,27 +50,14 @@ describe("wrapFetchWithAbortSignal", () => { const wrapped = wrapFetchWithAbortSignal(fetchImpl); - let abortHandler: (() => void) | null = null; - const fakeSignal = { - aborted: false, - addEventListener: (event: string, handler: () => void) => { - if (event === "abort") { - abortHandler = handler; - } - }, - removeEventListener: (event: string, handler: () => void) => { - if (event === "abort" && abortHandler === handler) { - abortHandler = null; - } - }, - } as AbortSignal; + const { fakeSignal, triggerAbort } = createForeignSignalHarness(); const promise = wrapped("https://example.com", { signal: fakeSignal }); expect(fetchImpl).toHaveBeenCalledOnce(); expect(seenSignal).toBeInstanceOf(AbortSignal); expect(seenSignal).not.toBe(fakeSignal); - abortHandler?.(); + triggerAbort(); expect(seenSignal?.aborted).toBe(true); await promise; @@ -64,22 +76,7 @@ describe("wrapFetchWithAbortSignal", () => { ); const wrapped = wrapFetchWithAbortSignal(fetchImpl); - let abortHandler: (() => void) | null = null; - const removeEventListener = vi.fn((event: string, handler: () => void) => { - if (event === "abort" && abortHandler === handler) { - abortHandler = null; - } - }); - - const fakeSignal = { - aborted: false, - addEventListener: (event: string, handler: () => void) => { - if (event === "abort") { - abortHandler = handler; - } - }, - removeEventListener, - } as AbortSignal; + const { fakeSignal, removeEventListener } = createForeignSignalHarness(); try { await expect(wrapped("https://example.com", { signal: fakeSignal })).rejects.toBe(fetchError); @@ -100,22 +97,7 @@ describe("wrapFetchWithAbortSignal", () => { }); const wrapped = wrapFetchWithAbortSignal(fetchImpl); - let abortHandler: (() => void) | null = null; - const removeEventListener = vi.fn((event: string, handler: () => void) => { - if (event === "abort" && abortHandler === handler) { - abortHandler = null; - } - }); - - const fakeSignal = { - aborted: false, - addEventListener: (event: string, handler: () => void) => { - if (event === "abort") { - abortHandler = handler; - } - }, - removeEventListener, - } as AbortSignal; + const { fakeSignal, removeEventListener } = createForeignSignalHarness(); expect(() => wrapped("https://example.com", { signal: fakeSignal })).toThrow(syncError); expect(removeEventListener).toHaveBeenCalledOnce(); diff --git a/src/tui/tui.submit-handler.test.ts b/src/tui/tui.submit-handler.test.ts index f33d1af13d..dc337ad294 100644 --- a/src/tui/tui.submit-handler.test.ts +++ b/src/tui/tui.submit-handler.test.ts @@ -5,22 +5,26 @@ import { shouldEnableWindowsGitBashPasteFallback, } from "./tui.js"; +function createSubmitHarness() { + const editor = { + setText: vi.fn(), + addToHistory: vi.fn(), + }; + const handleCommand = vi.fn(); + const sendMessage = vi.fn(); + const handleBangLine = vi.fn(); + const onSubmit = createEditorSubmitHandler({ + editor, + handleCommand, + sendMessage, + handleBangLine, + }); + return { editor, handleCommand, sendMessage, handleBangLine, onSubmit }; +} + describe("createEditorSubmitHandler", () => { it("routes lines starting with ! to handleBangLine", () => { - const editor = { - setText: vi.fn(), - addToHistory: vi.fn(), - }; - const handleCommand = vi.fn(); - const sendMessage = vi.fn(); - const handleBangLine = vi.fn(); - - const onSubmit = createEditorSubmitHandler({ - editor, - handleCommand, - sendMessage, - handleBangLine, - }); + const { handleCommand, sendMessage, handleBangLine, onSubmit } = createSubmitHarness(); onSubmit("!ls"); @@ -31,20 +35,7 @@ describe("createEditorSubmitHandler", () => { }); it("treats a lone ! as a normal message", () => { - const editor = { - setText: vi.fn(), - addToHistory: vi.fn(), - }; - const handleCommand = vi.fn(); - const sendMessage = vi.fn(); - const handleBangLine = vi.fn(); - - const onSubmit = createEditorSubmitHandler({ - editor, - handleCommand, - sendMessage, - handleBangLine, - }); + const { sendMessage, handleBangLine, onSubmit } = createSubmitHarness(); onSubmit("!"); @@ -54,20 +45,7 @@ describe("createEditorSubmitHandler", () => { }); it("does not treat leading whitespace before ! as a bang command", () => { - const editor = { - setText: vi.fn(), - addToHistory: vi.fn(), - }; - const handleCommand = vi.fn(); - const sendMessage = vi.fn(); - const handleBangLine = vi.fn(); - - const onSubmit = createEditorSubmitHandler({ - editor, - handleCommand, - sendMessage, - handleBangLine, - }); + const { editor, sendMessage, handleBangLine, onSubmit } = createSubmitHarness(); onSubmit(" !ls"); @@ -77,20 +55,7 @@ describe("createEditorSubmitHandler", () => { }); it("trims normal messages before sending and adding to history", () => { - const editor = { - setText: vi.fn(), - addToHistory: vi.fn(), - }; - const handleCommand = vi.fn(); - const sendMessage = vi.fn(); - const handleBangLine = vi.fn(); - - const onSubmit = createEditorSubmitHandler({ - editor, - handleCommand, - sendMessage, - handleBangLine, - }); + const { editor, sendMessage, onSubmit } = createSubmitHarness(); onSubmit(" hello "); @@ -99,20 +64,7 @@ describe("createEditorSubmitHandler", () => { }); it("preserves internal newlines for multiline messages", () => { - const editor = { - setText: vi.fn(), - addToHistory: vi.fn(), - }; - const handleCommand = vi.fn(); - const sendMessage = vi.fn(); - const handleBangLine = vi.fn(); - - const onSubmit = createEditorSubmitHandler({ - editor, - handleCommand, - sendMessage, - handleBangLine, - }); + const { editor, handleCommand, sendMessage, handleBangLine, onSubmit } = createSubmitHarness(); onSubmit("Line 1\nLine 2\nLine 3"); diff --git a/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts b/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts index 175bd66e10..828236a2e7 100644 --- a/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts +++ b/src/web/monitor-inbox.allows-messages-from-senders-allowfrom-list.test.ts @@ -16,6 +16,27 @@ const DEFAULT_MESSAGES_CFG = { responsePrefix: undefined, } as const; +function createAllowListConfig(allowFrom: string[]) { + return { + channels: { + whatsapp: { + allowFrom, + }, + }, + messages: DEFAULT_MESSAGES_CFG, + }; +} + +async function openInboxMonitor(onMessage = vi.fn()) { + const listener = await monitorWebInbox({ + verbose: false, + accountId: DEFAULT_ACCOUNT_ID, + authDir: getAuthDir(), + onMessage, + }); + return { onMessage, listener, sock: getSock() }; +} + async function expectOutboundDmSkipsPairing(params: { selfChatMode: boolean; messageId: string; @@ -73,27 +94,9 @@ describe("web monitor inbox", () => { installWebMonitorInboxUnitTestHooks(); it("allows messages from senders in allowFrom list", async () => { - mockLoadConfig.mockReturnValue({ - channels: { - whatsapp: { - // Allow +999 - allowFrom: ["+111", "+999"], - }, - }, - messages: { - messagePrefix: undefined, - responsePrefix: undefined, - }, - }); + mockLoadConfig.mockReturnValue(createAllowListConfig(["+111", "+999"])); - const onMessage = vi.fn(); - const listener = await monitorWebInbox({ - verbose: false, - accountId: DEFAULT_ACCOUNT_ID, - authDir: getAuthDir(), - onMessage, - }); - const sock = getSock(); + const { onMessage, listener, sock } = await openInboxMonitor(); const upsert = { type: "notify", @@ -124,27 +127,9 @@ describe("web monitor inbox", () => { it("allows same-phone messages even if not in allowFrom", async () => { // Same-phone mode: when from === selfJid, should always be allowed // This allows users to message themselves even with restrictive allowFrom - mockLoadConfig.mockReturnValue({ - channels: { - whatsapp: { - // Only allow +111, but self is +123 - allowFrom: ["+111"], - }, - }, - messages: { - messagePrefix: undefined, - responsePrefix: undefined, - }, - }); + mockLoadConfig.mockReturnValue(createAllowListConfig(["+111"])); - const onMessage = vi.fn(); - const listener = await monitorWebInbox({ - verbose: false, - accountId: DEFAULT_ACCOUNT_ID, - authDir: getAuthDir(), - onMessage, - }); - const sock = getSock(); + const { onMessage, listener, sock } = await openInboxMonitor(); // Message from self (sock.user.id is "123@s.whatsapp.net" in mock) const upsert = { @@ -176,14 +161,7 @@ describe("web monitor inbox", () => { .mockResolvedValueOnce({ code: "PAIRCODE", created: true }) .mockResolvedValueOnce({ code: "PAIRCODE", created: false }); - const onMessage = vi.fn(); - const listener = await monitorWebInbox({ - verbose: false, - accountId: DEFAULT_ACCOUNT_ID, - authDir: getAuthDir(), - onMessage, - }); - const sock = getSock(); + const { onMessage, listener, sock } = await openInboxMonitor(); // Message from someone else should be blocked const upsertBlocked = { @@ -280,14 +258,7 @@ describe("web monitor inbox", () => { }); it("handles append messages by marking them read but skipping auto-reply", async () => { - const onMessage = vi.fn(); - const listener = await monitorWebInbox({ - verbose: false, - accountId: DEFAULT_ACCOUNT_ID, - authDir: getAuthDir(), - onMessage, - }); - const sock = getSock(); + const { onMessage, listener, sock } = await openInboxMonitor(); const upsert = { type: "append",