openclaw/src/signal/monitor/event-handler.system-messages.test.ts
Dale 2685b11e95 fix(signal): ignore system messages (expiration timer, group permission changes)
signal-cli sends dataMessage envelopes for system events (disappearing
message timer changes, group permission updates) with no user text.
These fell through to the agent, which responded with a confused
'I got a media message' reply.

Add isExpirationUpdate and groupV2Change fields to SignalDataMessage type,
and early-return on these system-only events before dispatch.

Fixes #27615, Fixes #30981
2026-03-16 12:36:37 +00:00

129 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from "vitest";
import { createSignalEventHandler } from "./event-handler.js";
import {
createBaseSignalEventHandlerDeps,
createSignalReceiveEvent,
} from "./event-handler.test-harness.js";
const { sendTypingMock, sendReadReceiptMock, dispatchInboundMessageMock } = vi.hoisted(() => ({
sendTypingMock: vi.fn(),
sendReadReceiptMock: vi.fn(),
dispatchInboundMessageMock: vi.fn(async () => ({
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", 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(),
}));
function makeDeps() {
return createBaseSignalEventHandlerDeps({
// oxlint-disable-next-line typescript/no-explicit-any
cfg: { messages: { inbound: { debounceMs: 0 } } } as any,
historyLimit: 0,
});
}
describe("signal system message filtering", () => {
beforeEach(() => {
sendTypingMock.mockReset().mockResolvedValue(true);
sendReadReceiptMock.mockReset().mockResolvedValue(true);
dispatchInboundMessageMock.mockClear();
});
it("filters expiration timer update (expiresInSeconds, no text)", async () => {
const handler = createSignalEventHandler(makeDeps());
await handler(
createSignalReceiveEvent({
dataMessage: {
message: null,
attachments: [],
expiresInSeconds: 604800,
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
});
it("filters expiration timer update with isExpirationUpdate flag", async () => {
const handler = createSignalEventHandler(makeDeps());
await handler(
createSignalReceiveEvent({
dataMessage: {
message: null,
attachments: [],
isExpirationUpdate: true,
expiresInSeconds: 604800,
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
});
it("filters groupV2Change messages", async () => {
const handler = createSignalEventHandler(makeDeps());
await handler(
createSignalReceiveEvent({
dataMessage: {
message: null,
attachments: [],
groupV2Change: { editor: "+15550001111", changes: [] },
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(dispatchInboundMessageMock).not.toHaveBeenCalled();
});
it("does NOT filter normal message with expiresInSeconds=0", async () => {
const handler = createSignalEventHandler(makeDeps());
await handler(
createSignalReceiveEvent({
dataMessage: {
message: "hello",
attachments: [],
expiresInSeconds: 0,
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(dispatchInboundMessageMock).toHaveBeenCalled();
});
it("does NOT filter message with text even if expiresInSeconds > 0", async () => {
const handler = createSignalEventHandler(makeDeps());
await handler(
createSignalReceiveEvent({
dataMessage: {
message: "hello with timer",
attachments: [],
expiresInSeconds: 604800,
groupInfo: { groupId: "g1", groupName: "Test Group" },
},
}),
);
expect(dispatchInboundMessageMock).toHaveBeenCalled();
});
});