From ebbf92259bb2153ada4410b4c609739ac615d053 Mon Sep 17 00:00:00 2001 From: Codex CLI Audit Date: Sun, 8 Mar 2026 17:49:43 -0400 Subject: [PATCH] Fix Signal quote debounce target selection --- .../mattermost/src/mattermost/monitor.ts | 2 +- .../src/monitor/event-handler.quote.test.ts | 58 +++++++++++++++++++ .../signal/src/monitor/event-handler.ts | 9 ++- src/plugins/loader.test.ts | 3 +- 4 files changed, 66 insertions(+), 6 deletions(-) diff --git a/extensions/mattermost/src/mattermost/monitor.ts b/extensions/mattermost/src/mattermost/monitor.ts index 958a40de705..03b5c11c9ae 100644 --- a/extensions/mattermost/src/mattermost/monitor.ts +++ b/extensions/mattermost/src/mattermost/monitor.ts @@ -159,7 +159,7 @@ function channelChatType(kind: ChatType): "direct" | "group" | "channel" { export function resolveMattermostReplyRootId(params: { threadRootId?: string; - replyToId?: string; + replyToId?: string | null; }): string | undefined { const threadRootId = params.threadRootId?.trim(); if (threadRootId) { diff --git a/extensions/signal/src/monitor/event-handler.quote.test.ts b/extensions/signal/src/monitor/event-handler.quote.test.ts index 92fb4df489b..81ccdcafd96 100644 --- a/extensions/signal/src/monitor/event-handler.quote.test.ts +++ b/extensions/signal/src/monitor/event-handler.quote.test.ts @@ -101,6 +101,64 @@ describe("signal quote reply handling", () => { expect(String(ctx?.Body ?? "")).toContain("[Quoting +15550003333 id:1700000000000]"); }); + it("uses the latest quote target when debouncing rapid quoted Signal replies", async () => { + vi.useFakeTimers(); + try { + const handler = createQuoteHandler({ + // oxlint-disable-next-line typescript/no-explicit-any + cfg: { messages: { inbound: { debounceMs: 25 } } } as any, + }); + + await handler( + createSignalReceiveEvent({ + sourceNumber: "+15550002222", + sourceName: "Bob", + timestamp: 1700000000001, + dataMessage: { + message: "First chunk", + quote: { + id: 1700000000000, + authorNumber: "+15550003333", + text: "First quoted message", + }, + }, + }), + ); + await handler( + createSignalReceiveEvent({ + sourceNumber: "+15550002222", + sourceName: "Bob", + timestamp: 1700000000002, + dataMessage: { + message: "Second chunk", + quote: { + id: 1700000000009, + authorNumber: "+15550004444", + text: "Second quoted message", + }, + }, + }), + ); + + expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); + + await vi.advanceTimersByTimeAsync(30); + await vi.waitFor(() => { + expect(dispatchInboundMessageMock).toHaveBeenCalledTimes(1); + }); + + const ctx = getCapturedCtx(); + expect(ctx?.BodyForAgent).toBe("First chunk\\nSecond chunk"); + expect(ctx?.ReplyToId).toBe("1700000000009"); + expect(ctx?.ReplyToBody).toBe("Second quoted message"); + expect(ctx?.ReplyToSender).toBe("+15550004444"); + expect(String(ctx?.Body ?? "")).toContain("[Quoting +15550004444 id:1700000000009]"); + expect(String(ctx?.Body ?? "")).not.toContain("[Quoting +15550003333 id:1700000000000]"); + } finally { + vi.useRealTimers(); + } + }); + it("keeps quote-only replies and exposes the replied-message context block", async () => { const handler = createQuoteHandler(); diff --git a/extensions/signal/src/monitor/event-handler.ts b/extensions/signal/src/monitor/event-handler.ts index 5dd2cd5015f..2867dda019d 100644 --- a/extensions/signal/src/monitor/event-handler.ts +++ b/extensions/signal/src/monitor/event-handler.ts @@ -476,8 +476,11 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { if (!combinedText.trim()) { return; } - // Preserve quoteTarget from the earliest entry that has one - const earliestQuoteTarget = entries.find((entry) => entry.quoteTarget)?.quoteTarget; + // Preserve quoteTarget from the latest entry that has one so the reply + // target matches the newest text in the merged body. + const latestQuoteTarget = entries + .toReversed() + .find((entry) => entry.quoteTarget)?.quoteTarget; await handleSignalInboundMessage({ ...last, bodyText: combinedText, @@ -485,7 +488,7 @@ export function createSignalEventHandler(deps: SignalEventHandlerDeps) { mediaType: undefined, mediaPaths: undefined, mediaTypes: undefined, - quoteTarget: earliestQuoteTarget ?? last.quoteTarget, + quoteTarget: latestQuoteTarget ?? last.quoteTarget, }); }, onError: (err) => { diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 8af6cf927d4..9d3a1651781 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { pathToFileURL } from "node:url"; import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; import { emitDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js"; import { withEnv } from "../test-utils/env.js"; @@ -3262,7 +3261,7 @@ module.exports = { expect(record?.status).toBe("loaded"); }); - it("supports legacy plugins importing monolithic plugin-sdk root", async () => { + it("supports legacy plugins importing monolithic plugin-sdk root", () => { useNoBundledPlugins(); const plugin = writePlugin({ id: "legacy-root-import",