Fix Signal quote debounce target selection

This commit is contained in:
Codex CLI Audit 2026-03-08 17:49:43 -04:00 committed by Joey Krug
parent a51a1c6333
commit ebbf92259b
4 changed files with 66 additions and 6 deletions

View File

@ -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) {

View File

@ -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();

View File

@ -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) => {

View File

@ -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",