test: merge signal typing-read-receipt coverage into inbound contract suite

This commit is contained in:
Peter Steinberger
2026-02-22 13:24:53 +00:00
parent a395479d8b
commit 494bb685f8
2 changed files with 85 additions and 128 deletions

View File

@@ -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");
});
});

View File

@@ -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<void> };
}) => {
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<typeof import("../../auto-reply/dispatch.js")>();
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),
);
});
});