diff --git a/extensions/signal/src/monitor/event-handler.quote.test.ts b/extensions/signal/src/monitor/event-handler.quote.test.ts index ad865251a51..92fb4df489b 100644 --- a/extensions/signal/src/monitor/event-handler.quote.test.ts +++ b/extensions/signal/src/monitor/event-handler.quote.test.ts @@ -345,6 +345,49 @@ describe("signal quote reply handling", () => { expect(String(ctx?.Body ?? "")).toContain("[Quoting +15550002222 id:1700000000001]"); }); + it("does not poison the quote-author cache from attacker-controlled quote metadata", async () => { + const handler = createQuoteHandler(); + + await handler( + createSignalReceiveEvent({ + sourceNumber: "+15550002222", + sourceName: "Bob", + timestamp: 1700000000001, + dataMessage: { + message: "Forwarding this", + groupInfo: { groupId: "g1", groupName: "Test Group" }, + quote: { + id: 1700000000000, + authorNumber: "+15550009999", + text: "Mallory wrote this", + }, + }, + }), + ); + + capturedCtx = undefined; + + await handler( + createSignalReceiveEvent({ + sourceNumber: "+15550003333", + sourceName: "Alice", + timestamp: 1700000000002, + dataMessage: { + message: "Replying to Bob", + groupInfo: { groupId: "g1", groupName: "Test Group" }, + quote: { + id: 1700000000001, + text: "Forwarding this", + }, + }, + }), + ); + + const ctx = getCapturedCtx(); + expect(ctx?.ReplyToSender).toBe("+15550002222"); + expect(String(ctx?.Body ?? "")).toContain("[Quoting +15550002222 id:1700000000001]"); + }); + it("resolves cached uuid senders with a uuid: prefix", async () => { const handler = createQuoteHandler(); const senderUuid = "123e4567-e89b-12d3-a456-426614174000"; diff --git a/src/auto-reply/reply/reply-payloads.test.ts b/src/auto-reply/reply/reply-payloads.test.ts index 8664eec5c72..3156b0096ee 100644 --- a/src/auto-reply/reply/reply-payloads.test.ts +++ b/src/auto-reply/reply/reply-payloads.test.ts @@ -2,10 +2,33 @@ import { describe, expect, it } from "vitest"; import { setActivePluginRegistry } from "../../plugins/runtime.js"; import { createTestRegistry } from "../../test-utils/channel-plugins.js"; import { + applyReplyThreading, filterMessagingToolMediaDuplicates, shouldSuppressMessagingToolReplies, } from "./reply-payloads.js"; +describe("applyReplyThreading", () => { + it("treats whitespace-only replyToId as unset so implicit replies still apply", () => { + const result = applyReplyThreading({ + payloads: [{ text: "hello", replyToId: " \n\t " }], + replyToMode: "all", + currentMessageId: "123", + }); + + expect(result).toEqual([{ text: "hello", replyToId: "123" }]); + }); + + it("preserves explicit null replyToId as do-not-reply", () => { + const result = applyReplyThreading({ + payloads: [{ text: "hello", replyToId: null }], + replyToMode: "all", + currentMessageId: "123", + }); + + expect(result).toEqual([{ text: "hello", replyToId: null }]); + }); +}); + describe("filterMessagingToolMediaDuplicates", () => { it("strips mediaUrl when it matches sentMediaUrls", () => { const result = filterMessagingToolMediaDuplicates({ diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index 01d8367dd78..cec3e001fd4 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vite import { markdownToSignalTextChunks } from "../../../extensions/signal/src/format.js"; import { signalOutbound, + slackOutbound, telegramOutbound, whatsappOutbound, } from "../../../test/channel-outbounds.js"; @@ -1027,6 +1028,45 @@ describe("deliverOutboundPayloads", () => { ); }); + it("keeps inherited Slack thread context across all payloads", async () => { + const sendSlack = vi + .fn() + .mockResolvedValueOnce({ messageId: "sl1", channelId: "C123" }) + .mockResolvedValueOnce({ messageId: "sl2", channelId: "C123" }); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "slack", + plugin: createOutboundTestPlugin({ id: "slack", outbound: slackOutbound }), + source: "test", + }, + ]), + ); + + await deliverOutboundPayloads({ + cfg: { channels: { slack: {} } }, + channel: "slack", + to: "C123", + payloads: [{ text: "first" }, { text: "second" }], + replyToId: "thread-123", + deps: { sendSlack }, + }); + + expect(sendSlack).toHaveBeenCalledTimes(2); + expect(sendSlack).toHaveBeenNthCalledWith( + 1, + "C123", + "first", + expect.objectContaining({ threadTs: "thread-123" }), + ); + expect(sendSlack).toHaveBeenNthCalledWith( + 2, + "C123", + "second", + expect.objectContaining({ threadTs: "thread-123" }), + ); + }); + it("passes normalized payload to onError", async () => { const sendWhatsApp = vi.fn().mockRejectedValue(new Error("boom")); const onError = vi.fn();