diff --git a/src/signal/monitor.event-handler.typing-read-receipts.test.ts b/src/signal/monitor.event-handler.typing-read-receipts.test.ts deleted file mode 100644 index adc2e77b63a..00000000000 --- a/src/signal/monitor.event-handler.typing-read-receipts.test.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; -import { - createBaseSignalEventHandlerDeps, - createSignalReceiveEvent, -} from "./monitor/event-handler.test-harness.js"; - -const sendTypingMock = vi.fn(); -const sendReadReceiptMock = vi.fn(); -const dispatchInboundMessageMock = vi.fn( - async (params: { - replyOptions?: { onReplyStart?: () => void }; - dispatcher?: { sendFinalReply?: (payload: { text: string }) => void }; - }) => { - await Promise.resolve(params.replyOptions?.onReplyStart?.()); - return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; - }, -); - -vi.mock("./send.js", () => ({ - sendMessageSignal: vi.fn(), - sendTypingSignal: sendTypingMock, - sendReadReceiptSignal: sendReadReceiptMock, -})); - -vi.mock("../auto-reply/dispatch.js", () => ({ - dispatchInboundMessage: dispatchInboundMessageMock, - dispatchInboundMessageWithDispatcher: dispatchInboundMessageMock, - dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessageMock, -})); - -vi.mock("../pairing/pairing-store.js", () => ({ - readChannelAllowFromStore: vi.fn().mockResolvedValue([]), - upsertChannelPairingRequest: vi.fn(), -})); - -describe("signal event handler typing + read receipts", () => { - beforeEach(() => { - vi.useRealTimers(); - sendTypingMock.mockClear().mockResolvedValue(true); - sendReadReceiptMock.mockClear().mockResolvedValue(true); - dispatchInboundMessageMock.mockClear(); - }); - - it("sends typing + read receipt for allowed DMs", async () => { - const { createSignalEventHandler } = await import("./monitor/event-handler.js"); - const handler = createSignalEventHandler( - createBaseSignalEventHandlerDeps({ - cfg: { - messages: { inbound: { debounceMs: 0 } }, - channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } }, - }, - account: "+15550009999", - blockStreaming: false, - historyLimit: 0, - groupHistories: new Map(), - sendReadReceipts: true, - }), - ); - - await handler( - createSignalReceiveEvent({ - dataMessage: { - message: "hi", - }, - }), - ); - - expect(sendTypingMock).toHaveBeenCalledWith("+15550001111", expect.any(Object)); - expect(sendReadReceiptMock).toHaveBeenCalledWith( - "signal:+15550001111", - 1700000000000, - expect.any(Object), - ); - }); - - it("prefixes group bodies with sender label", async () => { - let capturedBody = ""; - dispatchInboundMessageMock.mockImplementationOnce( - async (params: { dispatcher?: { sendFinalReply?: (payload: { text: string }) => void } }) => { - const ctx = params as { ctx?: { Body?: string } }; - capturedBody = ctx.ctx?.Body ?? ""; - params.dispatcher?.sendFinalReply?.({ text: "ok" }); - return { queuedFinal: true, counts: { tool: 0, block: 0, final: 1 } }; - }, - ); - - const { createSignalEventHandler } = await import("./monitor/event-handler.js"); - const handler = createSignalEventHandler( - createBaseSignalEventHandlerDeps({ - cfg: { - channels: { signal: {} }, - } as never, - account: "+15550009999", - blockStreaming: false, - historyLimit: 0, - groupHistories: new Map(), - allowFrom: [], - groupAllowFrom: [], - sendReadReceipts: false, - }), - ); - - await handler( - createSignalReceiveEvent({ - dataMessage: { - message: "hello", - groupInfo: { groupId: "group-1", groupName: "Test Group" }, - }, - }), - ); - - expect(dispatchInboundMessageMock).toHaveBeenCalled(); - expect(capturedBody).toContain("Alice (+15550001111): hello"); - }); -}); diff --git a/src/signal/monitor/event-handler.inbound-contract.test.ts b/src/signal/monitor/event-handler.inbound-contract.test.ts index 910e177a5c0..82abd3917c2 100644 --- a/src/signal/monitor/event-handler.inbound-contract.test.ts +++ b/src/signal/monitor/event-handler.inbound-contract.test.ts @@ -1,16 +1,63 @@ -import { describe, expect, it } from "vitest"; -import { inboundCtxCapture as capture } from "../../../test/helpers/inbound-contract-dispatch-mock.js"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import { expectInboundContextContract } from "../../../test/helpers/inbound-contract.js"; +import type { MsgContext } from "../../auto-reply/templating.js"; import { createSignalEventHandler } from "./event-handler.js"; import { createBaseSignalEventHandlerDeps, createSignalReceiveEvent, } from "./event-handler.test-harness.js"; -describe("signal createSignalEventHandler inbound contract", () => { - it("passes a finalized MsgContext to dispatchInboundMessage", async () => { - capture.ctx = undefined; +const { sendTypingMock, sendReadReceiptMock, dispatchInboundMessageMock, capture } = vi.hoisted( + () => { + const captureState: { ctx: MsgContext | undefined } = { ctx: undefined }; + return { + sendTypingMock: vi.fn(), + sendReadReceiptMock: vi.fn(), + dispatchInboundMessageMock: vi.fn( + async (params: { + ctx: MsgContext; + replyOptions?: { onReplyStart?: () => void | Promise }; + }) => { + captureState.ctx = params.ctx; + await Promise.resolve(params.replyOptions?.onReplyStart?.()); + return { queuedFinal: false, counts: { tool: 0, block: 0, final: 0 } }; + }, + ), + capture: captureState, + }; + }, +); +vi.mock("../send.js", () => ({ + sendMessageSignal: vi.fn(), + sendTypingSignal: sendTypingMock, + sendReadReceiptSignal: sendReadReceiptMock, +})); + +vi.mock("../../auto-reply/dispatch.js", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + dispatchInboundMessage: dispatchInboundMessageMock, + dispatchInboundMessageWithDispatcher: dispatchInboundMessageMock, + dispatchInboundMessageWithBufferedDispatcher: dispatchInboundMessageMock, + }; +}); + +vi.mock("../../pairing/pairing-store.js", () => ({ + readChannelAllowFromStore: vi.fn().mockResolvedValue([]), + upsertChannelPairingRequest: vi.fn(), +})); + +describe("signal createSignalEventHandler inbound contract", () => { + beforeEach(() => { + capture.ctx = undefined; + sendTypingMock.mockReset().mockResolvedValue(true); + sendReadReceiptMock.mockReset().mockResolvedValue(true); + dispatchInboundMessageMock.mockClear(); + }); + + it("passes a finalized MsgContext to dispatchInboundMessage", async () => { const handler = createSignalEventHandler( createBaseSignalEventHandlerDeps({ // oxlint-disable-next-line typescript/no-explicit-any @@ -31,7 +78,7 @@ describe("signal createSignalEventHandler inbound contract", () => { expect(capture.ctx).toBeTruthy(); expectInboundContextContract(capture.ctx!); - const contextWithBody = capture.ctx as unknown as { Body?: string }; + const contextWithBody = capture.ctx!; // Sender should appear as prefix in group messages (no redundant [from:] suffix) expect(String(contextWithBody.Body ?? "")).toContain("Alice"); expect(String(contextWithBody.Body ?? "")).toMatch(/Alice.*:/); @@ -39,8 +86,6 @@ describe("signal createSignalEventHandler inbound contract", () => { }); it("normalizes direct chat To/OriginatingTo targets to canonical Signal ids", async () => { - capture.ctx = undefined; - const handler = createSignalEventHandler( createBaseSignalEventHandlerDeps({ // oxlint-disable-next-line typescript/no-explicit-any @@ -62,13 +107,40 @@ describe("signal createSignalEventHandler inbound contract", () => { ); expect(capture.ctx).toBeTruthy(); - const context = capture.ctx as unknown as { - ChatType?: string; - To?: string; - OriginatingTo?: string; - }; + const context = capture.ctx!; expect(context.ChatType).toBe("direct"); expect(context.To).toBe("+15550002222"); expect(context.OriginatingTo).toBe("+15550002222"); }); + + it("sends typing + read receipt for allowed DMs", async () => { + const handler = createSignalEventHandler( + createBaseSignalEventHandlerDeps({ + cfg: { + messages: { inbound: { debounceMs: 0 } }, + channels: { signal: { dmPolicy: "open", allowFrom: ["*"] } }, + }, + account: "+15550009999", + blockStreaming: false, + historyLimit: 0, + groupHistories: new Map(), + sendReadReceipts: true, + }), + ); + + await handler( + createSignalReceiveEvent({ + dataMessage: { + message: "hi", + }, + }), + ); + + expect(sendTypingMock).toHaveBeenCalledWith("+15550001111", expect.any(Object)); + expect(sendReadReceiptMock).toHaveBeenCalledWith( + "signal:+15550001111", + 1700000000000, + expect.any(Object), + ); + }); });