mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-07 22:44:16 +00:00
refactor(channels): dedupe monitor message test flows
This commit is contained in:
@@ -119,6 +119,13 @@ vi.mock("../../config/sessions.js", () => ({
|
||||
const { processDiscordMessage } = await import("./message-handler.process.js");
|
||||
|
||||
const createBaseContext = createBaseDiscordMessageContext;
|
||||
const BASE_CHANNEL_ROUTE = {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:discord:channel:c1",
|
||||
mainSessionKey: "agent:main:main",
|
||||
} as const;
|
||||
|
||||
function mockDispatchSingleBlockReply(payload: { text: string; isReasoning?: boolean }) {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
@@ -127,6 +134,10 @@ function mockDispatchSingleBlockReply(payload: { text: string; isReasoning?: boo
|
||||
});
|
||||
}
|
||||
|
||||
function createNoQueuedDispatchResult() {
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
}
|
||||
|
||||
async function processStreamOffDiscordMessage() {
|
||||
const ctx = await createBaseContext({ discordConfig: { streamMode: "off" } });
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -144,10 +155,7 @@ beforeEach(() => {
|
||||
recordInboundSession.mockClear();
|
||||
readSessionUpdatedAt.mockClear();
|
||||
resolveStorePath.mockClear();
|
||||
dispatchInboundMessage.mockResolvedValue({
|
||||
queuedFinal: false,
|
||||
counts: { final: 0, tool: 0, block: 0 },
|
||||
});
|
||||
dispatchInboundMessage.mockResolvedValue(createNoQueuedDispatchResult());
|
||||
recordInboundSession.mockResolvedValue(undefined);
|
||||
readSessionUpdatedAt.mockReturnValue(undefined);
|
||||
resolveStorePath.mockReturnValue("/tmp/openclaw-discord-process-test-sessions.json");
|
||||
@@ -193,6 +201,28 @@ async function runInPartialStreamMode(): Promise<void> {
|
||||
await runProcessDiscordMessage(ctx);
|
||||
}
|
||||
|
||||
function getReactionEmojis(): string[] {
|
||||
return (
|
||||
sendMocks.reactMessageDiscord.mock.calls as unknown as Array<[unknown, unknown, string]>
|
||||
).map((call) => call[2]);
|
||||
}
|
||||
|
||||
function createMockDraftStreamForTest() {
|
||||
const draftStream = createMockDraftStream();
|
||||
createDiscordDraftStream.mockReturnValueOnce(draftStream);
|
||||
return draftStream;
|
||||
}
|
||||
|
||||
function expectSinglePreviewEdit() {
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "Hello\nWorld" },
|
||||
{ rest: {} },
|
||||
);
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
describe("processDiscordMessage ack reactions", () => {
|
||||
it("skips ack reactions for group-mentions when mentions are not required", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
@@ -245,7 +275,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
await params?.replyOptions?.onToolStart?.({ name: "exec" });
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext();
|
||||
@@ -253,9 +283,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
|
||||
const emojis = (
|
||||
sendMocks.reactMessageDiscord.mock.calls as unknown as Array<[unknown, unknown, string]>
|
||||
).map((call) => call[2]);
|
||||
const emojis = getReactionEmojis();
|
||||
expect(emojis).toContain("👀");
|
||||
expect(emojis).toContain(DEFAULT_EMOJIS.done);
|
||||
expect(emojis).not.toContain(DEFAULT_EMOJIS.thinking);
|
||||
@@ -270,7 +298,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
});
|
||||
dispatchInboundMessage.mockImplementationOnce(async () => {
|
||||
await dispatchGate;
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext();
|
||||
@@ -293,7 +321,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
it("applies status reaction emoji/timing overrides from config", async () => {
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onReasoningStream?.();
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createBaseContext({
|
||||
@@ -312,9 +340,7 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
await processDiscordMessage(ctx as any);
|
||||
|
||||
const emojis = (
|
||||
sendMocks.reactMessageDiscord.mock.calls as unknown as Array<[unknown, unknown, string]>
|
||||
).map((call) => call[2]);
|
||||
const emojis = getReactionEmojis();
|
||||
expect(emojis).toContain("🟦");
|
||||
expect(emojis).toContain("🏁");
|
||||
});
|
||||
@@ -347,13 +373,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
it("stores group lastRoute with channel target", async () => {
|
||||
const ctx = await createBaseContext({
|
||||
baseSessionKey: "agent:main:discord:channel:c1",
|
||||
route: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:discord:channel:c1",
|
||||
mainSessionKey: "agent:main:main",
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -389,13 +409,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
threadChannel: { id: "thread-1", name: "subagent-thread" },
|
||||
boundSessionKey: "agent:main:subagent:child",
|
||||
threadBindings,
|
||||
route: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: "default",
|
||||
sessionKey: "agent:main:discord:channel:c1",
|
||||
mainSessionKey: "agent:main:main",
|
||||
},
|
||||
route: BASE_CHANNEL_ROUTE,
|
||||
});
|
||||
|
||||
// oxlint-disable-next-line typescript/no-explicit-any
|
||||
@@ -446,26 +460,12 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
it("finalizes via preview edit when final fits one chunk", async () => {
|
||||
await runSingleChunkFinalScenario({ streamMode: "partial", maxLinesPerMessage: 5 });
|
||||
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "Hello\nWorld" },
|
||||
{ rest: {} },
|
||||
);
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
expectSinglePreviewEdit();
|
||||
});
|
||||
|
||||
it("accepts streaming=true alias for partial preview mode", async () => {
|
||||
await runSingleChunkFinalScenario({ streaming: true, maxLinesPerMessage: 5 });
|
||||
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "Hello\nWorld" },
|
||||
{ rest: {} },
|
||||
);
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
expectSinglePreviewEdit();
|
||||
});
|
||||
|
||||
it("falls back to standard send when final needs multiple chunks", async () => {
|
||||
@@ -508,12 +508,11 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
});
|
||||
|
||||
it("streams block previews using draft chunking", async () => {
|
||||
const draftStream = createMockDraftStream();
|
||||
createDiscordDraftStream.mockReturnValueOnce(draftStream);
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onPartialReply?.({ text: "HelloWorld" });
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createBlockModeContext();
|
||||
@@ -526,13 +525,12 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
});
|
||||
|
||||
it("forces new preview messages on assistant boundaries in block mode", async () => {
|
||||
const draftStream = createMockDraftStream();
|
||||
createDiscordDraftStream.mockReturnValueOnce(draftStream);
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onPartialReply?.({ text: "Hello" });
|
||||
await params?.replyOptions?.onAssistantMessageStart?.();
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
const ctx = await createBlockModeContext();
|
||||
@@ -544,14 +542,13 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
});
|
||||
|
||||
it("strips reasoning tags from partial stream updates", async () => {
|
||||
const draftStream = createMockDraftStream();
|
||||
createDiscordDraftStream.mockReturnValueOnce(draftStream);
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onPartialReply?.({
|
||||
text: "<thinking>Let me think about this</thinking>\nThe answer is 42",
|
||||
});
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
await runInPartialStreamMode();
|
||||
@@ -563,14 +560,13 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
});
|
||||
|
||||
it("skips pure-reasoning partial updates without updating draft", async () => {
|
||||
const draftStream = createMockDraftStream();
|
||||
createDiscordDraftStream.mockReturnValueOnce(draftStream);
|
||||
const draftStream = createMockDraftStreamForTest();
|
||||
|
||||
dispatchInboundMessage.mockImplementationOnce(async (params?: DispatchInboundParams) => {
|
||||
await params?.replyOptions?.onPartialReply?.({
|
||||
text: "Reasoning:\nThe user asked about X so I need to consider Y",
|
||||
});
|
||||
return { queuedFinal: false, counts: { final: 0, tool: 0, block: 0 } };
|
||||
return createNoQueuedDispatchResult();
|
||||
});
|
||||
|
||||
await runInPartialStreamMode();
|
||||
|
||||
@@ -106,6 +106,50 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
}
|
||||
|
||||
async function runChannelMessageEvent(
|
||||
text: string,
|
||||
overrides: Partial<SlackMessageEvent> = {},
|
||||
): Promise<void> {
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent({
|
||||
text,
|
||||
channel_type: "channel",
|
||||
...overrides,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function setHistoryCaptureConfig(channels: Record<string, unknown>) {
|
||||
slackTestState.config = {
|
||||
messages: { ackReactionScope: "group-mentions" },
|
||||
channels: {
|
||||
slack: {
|
||||
historyLimit: 5,
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function captureReplyContexts<T extends Record<string, unknown>>() {
|
||||
const contexts: T[] = [];
|
||||
replyMock.mockImplementation(async (ctx: unknown) => {
|
||||
contexts.push((ctx ?? {}) as T);
|
||||
return undefined;
|
||||
});
|
||||
return contexts;
|
||||
}
|
||||
|
||||
async function runMonitoredSlackMessages(events: SlackMessageEvent[]) {
|
||||
const { controller, run } = startSlackMonitor(monitorSlackProvider);
|
||||
const handler = await getSlackHandlerOrThrow("message");
|
||||
for (const event of events) {
|
||||
await handler({ event });
|
||||
}
|
||||
await stopSlackMonitor({ controller, run });
|
||||
}
|
||||
|
||||
function setPairingOnlyDirectMessages() {
|
||||
const currentConfig = slackTestState.config as {
|
||||
channels?: { slack?: Record<string, unknown> };
|
||||
@@ -122,6 +166,61 @@ describe("monitorSlackProvider tool results", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function setOpenChannelDirectMessages(params?: {
|
||||
bindings?: Array<Record<string, unknown>>;
|
||||
groupPolicy?: "open";
|
||||
includeAckReactionConfig?: boolean;
|
||||
replyToMode?: "off" | "all" | "first";
|
||||
threadInheritParent?: boolean;
|
||||
}) {
|
||||
const slackChannelConfig: Record<string, unknown> = {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
...(params?.groupPolicy ? { groupPolicy: params.groupPolicy } : {}),
|
||||
...(params?.replyToMode ? { replyToMode: params.replyToMode } : {}),
|
||||
...(params?.threadInheritParent ? { thread: { inheritParent: true } } : {}),
|
||||
};
|
||||
slackTestState.config = {
|
||||
messages: params?.includeAckReactionConfig
|
||||
? {
|
||||
responsePrefix: "PFX",
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "group-mentions",
|
||||
}
|
||||
: { responsePrefix: "PFX" },
|
||||
channels: { slack: slackChannelConfig },
|
||||
...(params?.bindings ? { bindings: params.bindings } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function getFirstReplySessionCtx(): {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
ThreadStarterBody?: string;
|
||||
ThreadLabel?: string;
|
||||
} {
|
||||
return (replyMock.mock.calls[0]?.[0] ?? {}) as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
ThreadStarterBody?: string;
|
||||
ThreadLabel?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function expectSingleSendWithThread(threadTs: string | undefined) {
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs });
|
||||
}
|
||||
|
||||
async function runDefaultMessageAndExpectSentText(expectedText: string) {
|
||||
replyMock.mockResolvedValue({ text: expectedText.replace(/^PFX /, "") });
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent(),
|
||||
});
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][1]).toBe(expectedText);
|
||||
}
|
||||
|
||||
it("skips socket startup when Slack channel is disabled", async () => {
|
||||
slackTestState.config = {
|
||||
channels: {
|
||||
@@ -149,14 +248,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
|
||||
it("skips tool summaries with responsePrefix", async () => {
|
||||
replyMock.mockResolvedValue({ text: "final reply" });
|
||||
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent(),
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][1]).toBe("PFX final reply");
|
||||
await runDefaultMessageAndExpectSentText("PFX final reply");
|
||||
});
|
||||
|
||||
it("drops events with mismatched api_app_id", async () => {
|
||||
@@ -213,127 +305,56 @@ describe("monitorSlackProvider tool results", () => {
|
||||
},
|
||||
};
|
||||
|
||||
replyMock.mockResolvedValue({ text: "final reply" });
|
||||
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent(),
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][1]).toBe("final reply");
|
||||
await runDefaultMessageAndExpectSentText("final reply");
|
||||
});
|
||||
|
||||
it("preserves RawBody without injecting processed room history", async () => {
|
||||
slackTestState.config = {
|
||||
messages: { ackReactionScope: "group-mentions" },
|
||||
channels: {
|
||||
slack: {
|
||||
historyLimit: 5,
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { "*": { requireMention: false } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
let capturedCtx: { Body?: string; RawBody?: string; CommandBody?: string } = {};
|
||||
replyMock.mockImplementation(async (ctx: unknown) => {
|
||||
capturedCtx = ctx ?? {};
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { controller, run } = startSlackMonitor(monitorSlackProvider);
|
||||
const handler = await getSlackHandlerOrThrow("message");
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U1",
|
||||
text: "first",
|
||||
ts: "123",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
user: "U2",
|
||||
text: "second",
|
||||
ts: "124",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await stopSlackMonitor({ controller, run });
|
||||
setHistoryCaptureConfig({ "*": { requireMention: false } });
|
||||
const capturedCtx = captureReplyContexts<{
|
||||
Body?: string;
|
||||
RawBody?: string;
|
||||
CommandBody?: string;
|
||||
}>();
|
||||
await runMonitoredSlackMessages([
|
||||
makeSlackMessageEvent({ user: "U1", text: "first", ts: "123", channel_type: "channel" }),
|
||||
makeSlackMessageEvent({ user: "U2", text: "second", ts: "124", channel_type: "channel" }),
|
||||
]);
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(2);
|
||||
expect(capturedCtx.Body).not.toContain(HISTORY_CONTEXT_MARKER);
|
||||
expect(capturedCtx.Body).not.toContain(CURRENT_MESSAGE_MARKER);
|
||||
expect(capturedCtx.Body).not.toContain("first");
|
||||
expect(capturedCtx.RawBody).toBe("second");
|
||||
expect(capturedCtx.CommandBody).toBe("second");
|
||||
const latestCtx = capturedCtx.at(-1) ?? {};
|
||||
expect(latestCtx.Body).not.toContain(HISTORY_CONTEXT_MARKER);
|
||||
expect(latestCtx.Body).not.toContain(CURRENT_MESSAGE_MARKER);
|
||||
expect(latestCtx.Body).not.toContain("first");
|
||||
expect(latestCtx.RawBody).toBe("second");
|
||||
expect(latestCtx.CommandBody).toBe("second");
|
||||
});
|
||||
|
||||
it("scopes thread history to the thread by default", async () => {
|
||||
slackTestState.config = {
|
||||
messages: { ackReactionScope: "group-mentions" },
|
||||
channels: {
|
||||
slack: {
|
||||
historyLimit: 5,
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: true } },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const capturedCtx: Array<{ Body?: string }> = [];
|
||||
replyMock.mockImplementation(async (ctx: unknown) => {
|
||||
capturedCtx.push(ctx ?? {});
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const { controller, run } = startSlackMonitor(monitorSlackProvider);
|
||||
const handler = await getSlackHandlerOrThrow("message");
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
setHistoryCaptureConfig({ C1: { allow: true, requireMention: true } });
|
||||
const capturedCtx = captureReplyContexts<{ Body?: string }>();
|
||||
await runMonitoredSlackMessages([
|
||||
makeSlackMessageEvent({
|
||||
user: "U1",
|
||||
text: "thread-a-one",
|
||||
ts: "200",
|
||||
thread_ts: "100",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
}),
|
||||
makeSlackMessageEvent({
|
||||
user: "U1",
|
||||
text: "<@bot-user> thread-a-two",
|
||||
ts: "201",
|
||||
thread_ts: "100",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await handler({
|
||||
event: {
|
||||
type: "message",
|
||||
}),
|
||||
makeSlackMessageEvent({
|
||||
user: "U2",
|
||||
text: "<@bot-user> thread-b-one",
|
||||
ts: "301",
|
||||
thread_ts: "300",
|
||||
channel: "C1",
|
||||
channel_type: "channel",
|
||||
},
|
||||
});
|
||||
|
||||
await stopSlackMonitor({ controller, run });
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(2);
|
||||
expect(capturedCtx[0]?.Body).toContain("thread-a-one");
|
||||
@@ -438,13 +459,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
|
||||
it("treats control commands as mentions for group bypass", async () => {
|
||||
replyMock.mockResolvedValue({ text: "ok" });
|
||||
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent({
|
||||
text: "/elevated off",
|
||||
channel_type: "channel",
|
||||
}),
|
||||
});
|
||||
await runChannelMessageEvent("/elevated off");
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
expect(firstReplyCtx().WasMentioned).toBe(true);
|
||||
@@ -452,25 +467,14 @@ describe("monitorSlackProvider tool results", () => {
|
||||
|
||||
it("threads replies when incoming message is in a thread", async () => {
|
||||
replyMock.mockResolvedValue({ text: "thread reply" });
|
||||
slackTestState.config = {
|
||||
messages: {
|
||||
responsePrefix: "PFX",
|
||||
ackReaction: "👀",
|
||||
ackReactionScope: "group-mentions",
|
||||
},
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
groupPolicy: "open",
|
||||
replyToMode: "off",
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
},
|
||||
},
|
||||
};
|
||||
setOpenChannelDirectMessages({
|
||||
includeAckReactionConfig: true,
|
||||
groupPolicy: "open",
|
||||
replyToMode: "off",
|
||||
});
|
||||
await runChannelThreadReplyEvent();
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: "111.222" });
|
||||
expectSingleSendWithThread("111.222");
|
||||
});
|
||||
|
||||
it("ignores replyToId directive when replyToMode is off", async () => {
|
||||
@@ -497,8 +501,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: undefined });
|
||||
expectSingleSendWithThread(undefined);
|
||||
});
|
||||
|
||||
it("keeps replyToId directive threading when replyToMode is all", async () => {
|
||||
@@ -511,8 +514,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: "555" });
|
||||
expectSingleSendWithThread("555");
|
||||
});
|
||||
|
||||
it("reacts to mention-gated room messages when ackReaction is enabled", async () => {
|
||||
@@ -581,8 +583,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
setDirectMessageReplyMode("all");
|
||||
await runDirectMessageEvent("123");
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: "123" });
|
||||
expectSingleSendWithThread("123");
|
||||
});
|
||||
|
||||
it("treats parent_user_id as a thread reply even when thread_ts matches ts", async () => {
|
||||
@@ -596,27 +597,14 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
const ctx = getFirstReplySessionCtx();
|
||||
expect(ctx.SessionKey).toBe("agent:main:main:thread:123");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
});
|
||||
|
||||
it("keeps thread parent inheritance opt-in", async () => {
|
||||
replyMock.mockResolvedValue({ text: "thread reply" });
|
||||
|
||||
slackTestState.config = {
|
||||
messages: { responsePrefix: "PFX" },
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
thread: { inheritParent: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
setOpenChannelDirectMessages({ threadInheritParent: true });
|
||||
|
||||
await runSlackMessageOnce(monitorSlackProvider, {
|
||||
event: makeSlackMessageEvent({
|
||||
@@ -626,10 +614,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
const ctx = getFirstReplySessionCtx();
|
||||
expect(ctx.SessionKey).toBe("agent:main:slack:channel:c1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBe("agent:main:slack:channel:c1");
|
||||
});
|
||||
@@ -649,25 +634,12 @@ describe("monitorSlackProvider tool results", () => {
|
||||
});
|
||||
}
|
||||
|
||||
slackTestState.config = {
|
||||
messages: { responsePrefix: "PFX" },
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
},
|
||||
},
|
||||
};
|
||||
setOpenChannelDirectMessages();
|
||||
|
||||
await runChannelThreadReplyEvent();
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
ThreadStarterBody?: string;
|
||||
ThreadLabel?: string;
|
||||
};
|
||||
const ctx = getFirstReplySessionCtx();
|
||||
expect(ctx.SessionKey).toBe("agent:main:slack:channel:c1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
expect(ctx.ThreadStarterBody).toContain("starter message");
|
||||
@@ -676,16 +648,9 @@ describe("monitorSlackProvider tool results", () => {
|
||||
|
||||
it("scopes thread session keys to the routed agent", async () => {
|
||||
replyMock.mockResolvedValue({ text: "ok" });
|
||||
slackTestState.config = {
|
||||
messages: { responsePrefix: "PFX" },
|
||||
channels: {
|
||||
slack: {
|
||||
dm: { enabled: true, policy: "open", allowFrom: ["*"] },
|
||||
channels: { C1: { allow: true, requireMention: false } },
|
||||
},
|
||||
},
|
||||
setOpenChannelDirectMessages({
|
||||
bindings: [{ agentId: "support", match: { channel: "slack", teamId: "T1" } }],
|
||||
};
|
||||
});
|
||||
|
||||
const client = getSlackClient();
|
||||
if (client?.auth?.test) {
|
||||
@@ -703,10 +668,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
await runChannelThreadReplyEvent();
|
||||
|
||||
expect(replyMock).toHaveBeenCalledTimes(1);
|
||||
const ctx = replyMock.mock.calls[0]?.[0] as {
|
||||
SessionKey?: string;
|
||||
ParentSessionKey?: string;
|
||||
};
|
||||
const ctx = getFirstReplySessionCtx();
|
||||
expect(ctx.SessionKey).toBe("agent:support:slack:channel:c1:thread:111.222");
|
||||
expect(ctx.ParentSessionKey).toBeUndefined();
|
||||
});
|
||||
@@ -716,8 +678,7 @@ describe("monitorSlackProvider tool results", () => {
|
||||
setDirectMessageReplyMode("off");
|
||||
await runDirectMessageEvent("789");
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: undefined });
|
||||
expectSingleSendWithThread(undefined);
|
||||
});
|
||||
|
||||
it("threads first reply when replyToMode is first and message is not threaded", async () => {
|
||||
@@ -725,8 +686,6 @@ describe("monitorSlackProvider tool results", () => {
|
||||
setDirectMessageReplyMode("first");
|
||||
await runDirectMessageEvent("789");
|
||||
|
||||
expect(sendMock).toHaveBeenCalledTimes(1);
|
||||
// First reply starts a thread under the incoming message
|
||||
expect(sendMock.mock.calls[0][2]).toMatchObject({ threadTs: "789" });
|
||||
expectSingleSendWithThread("789");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,141 +2,152 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { buildTelegramMessageContextForTest } from "./bot-message-context.test-harness.js";
|
||||
|
||||
const transcribeFirstAudioMock = vi.fn();
|
||||
const DEFAULT_MODEL = "anthropic/claude-opus-4-5";
|
||||
const DEFAULT_WORKSPACE = "/tmp/openclaw";
|
||||
const DEFAULT_MENTION_PATTERN = "\\bbot\\b";
|
||||
|
||||
vi.mock("../media-understanding/audio-preflight.js", () => ({
|
||||
transcribeFirstAudio: (...args: unknown[]) => transcribeFirstAudioMock(...args),
|
||||
}));
|
||||
|
||||
async function buildGroupVoiceContext(params: {
|
||||
messageId: number;
|
||||
chatId: number;
|
||||
title: string;
|
||||
date: number;
|
||||
fromId: number;
|
||||
firstName: string;
|
||||
fileId: string;
|
||||
mediaPath: string;
|
||||
groupDisableAudioPreflight?: boolean;
|
||||
topicDisableAudioPreflight?: boolean;
|
||||
}) {
|
||||
const groupConfig = {
|
||||
requireMention: true,
|
||||
...(params.groupDisableAudioPreflight === undefined
|
||||
? {}
|
||||
: { disableAudioPreflight: params.groupDisableAudioPreflight }),
|
||||
};
|
||||
const topicConfig =
|
||||
params.topicDisableAudioPreflight === undefined
|
||||
? undefined
|
||||
: { disableAudioPreflight: params.topicDisableAudioPreflight };
|
||||
|
||||
return buildTelegramMessageContextForTest({
|
||||
message: {
|
||||
message_id: params.messageId,
|
||||
chat: { id: params.chatId, type: "supergroup", title: params.title },
|
||||
date: params.date,
|
||||
text: undefined,
|
||||
from: { id: params.fromId, first_name: params.firstName },
|
||||
voice: { file_id: params.fileId },
|
||||
},
|
||||
allMedia: [{ path: params.mediaPath, contentType: "audio/ogg" }],
|
||||
options: { forceWasMentioned: true },
|
||||
cfg: {
|
||||
agents: { defaults: { model: DEFAULT_MODEL, workspace: DEFAULT_WORKSPACE } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: [DEFAULT_MENTION_PATTERN] } },
|
||||
},
|
||||
resolveGroupActivation: () => true,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig,
|
||||
topicConfig,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function expectTranscriptRendered(
|
||||
ctx: Awaited<ReturnType<typeof buildGroupVoiceContext>>,
|
||||
transcript: string,
|
||||
) {
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.BodyForAgent).toBe(transcript);
|
||||
expect(ctx?.ctxPayload?.Body).toContain(transcript);
|
||||
expect(ctx?.ctxPayload?.Body).not.toContain("<media:audio>");
|
||||
}
|
||||
|
||||
function expectAudioPlaceholderRendered(ctx: Awaited<ReturnType<typeof buildGroupVoiceContext>>) {
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(ctx?.ctxPayload?.Body).toContain("<media:audio>");
|
||||
}
|
||||
|
||||
describe("buildTelegramMessageContext audio transcript body", () => {
|
||||
it("uses preflight transcript as BodyForAgent for mention-gated group voice messages", async () => {
|
||||
transcribeFirstAudioMock.mockResolvedValueOnce("hey bot please help");
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: {
|
||||
message_id: 1,
|
||||
chat: { id: -1001234567890, type: "supergroup", title: "Test Group" },
|
||||
date: 1700000000,
|
||||
text: undefined,
|
||||
from: { id: 42, first_name: "Alice" },
|
||||
voice: { file_id: "voice-1" },
|
||||
},
|
||||
allMedia: [{ path: "/tmp/voice.ogg", contentType: "audio/ogg" }],
|
||||
options: { forceWasMentioned: true },
|
||||
cfg: {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bbot\\b"] } },
|
||||
},
|
||||
resolveGroupActivation: () => true,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true },
|
||||
topicConfig: undefined,
|
||||
}),
|
||||
const ctx = await buildGroupVoiceContext({
|
||||
messageId: 1,
|
||||
chatId: -1001234567890,
|
||||
title: "Test Group",
|
||||
date: 1700000000,
|
||||
fromId: 42,
|
||||
firstName: "Alice",
|
||||
fileId: "voice-1",
|
||||
mediaPath: "/tmp/voice.ogg",
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
|
||||
expect(ctx?.ctxPayload?.BodyForAgent).toBe("hey bot please help");
|
||||
expect(ctx?.ctxPayload?.Body).toContain("hey bot please help");
|
||||
expect(ctx?.ctxPayload?.Body).not.toContain("<media:audio>");
|
||||
expectTranscriptRendered(ctx, "hey bot please help");
|
||||
});
|
||||
|
||||
it("skips preflight transcription when disableAudioPreflight is true", async () => {
|
||||
transcribeFirstAudioMock.mockClear();
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: {
|
||||
message_id: 2,
|
||||
chat: { id: -1001234567891, type: "supergroup", title: "Test Group 2" },
|
||||
date: 1700000100,
|
||||
text: undefined,
|
||||
from: { id: 43, first_name: "Bob" },
|
||||
voice: { file_id: "voice-2" },
|
||||
},
|
||||
allMedia: [{ path: "/tmp/voice2.ogg", contentType: "audio/ogg" }],
|
||||
options: { forceWasMentioned: true },
|
||||
cfg: {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bbot\\b"] } },
|
||||
},
|
||||
resolveGroupActivation: () => true,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true, disableAudioPreflight: true },
|
||||
topicConfig: undefined,
|
||||
}),
|
||||
const ctx = await buildGroupVoiceContext({
|
||||
messageId: 2,
|
||||
chatId: -1001234567891,
|
||||
title: "Test Group 2",
|
||||
date: 1700000100,
|
||||
fromId: 43,
|
||||
firstName: "Bob",
|
||||
fileId: "voice-2",
|
||||
mediaPath: "/tmp/voice2.ogg",
|
||||
groupDisableAudioPreflight: true,
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(transcribeFirstAudioMock).not.toHaveBeenCalled();
|
||||
expect(ctx?.ctxPayload?.Body).toContain("<media:audio>");
|
||||
expectAudioPlaceholderRendered(ctx);
|
||||
});
|
||||
|
||||
it("uses topic disableAudioPreflight=false to override group disableAudioPreflight=true", async () => {
|
||||
transcribeFirstAudioMock.mockResolvedValueOnce("topic override transcript");
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: {
|
||||
message_id: 3,
|
||||
chat: { id: -1001234567892, type: "supergroup", title: "Test Group 3" },
|
||||
date: 1700000200,
|
||||
text: undefined,
|
||||
from: { id: 44, first_name: "Cara" },
|
||||
voice: { file_id: "voice-3" },
|
||||
},
|
||||
allMedia: [{ path: "/tmp/voice3.ogg", contentType: "audio/ogg" }],
|
||||
options: { forceWasMentioned: true },
|
||||
cfg: {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bbot\\b"] } },
|
||||
},
|
||||
resolveGroupActivation: () => true,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true, disableAudioPreflight: true },
|
||||
topicConfig: { disableAudioPreflight: false },
|
||||
}),
|
||||
const ctx = await buildGroupVoiceContext({
|
||||
messageId: 3,
|
||||
chatId: -1001234567892,
|
||||
title: "Test Group 3",
|
||||
date: 1700000200,
|
||||
fromId: 44,
|
||||
firstName: "Cara",
|
||||
fileId: "voice-3",
|
||||
mediaPath: "/tmp/voice3.ogg",
|
||||
groupDisableAudioPreflight: true,
|
||||
topicDisableAudioPreflight: false,
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(transcribeFirstAudioMock).toHaveBeenCalledTimes(1);
|
||||
expect(ctx?.ctxPayload?.BodyForAgent).toBe("topic override transcript");
|
||||
expect(ctx?.ctxPayload?.Body).toContain("topic override transcript");
|
||||
expect(ctx?.ctxPayload?.Body).not.toContain("<media:audio>");
|
||||
expectTranscriptRendered(ctx, "topic override transcript");
|
||||
});
|
||||
|
||||
it("uses topic disableAudioPreflight=true to override group disableAudioPreflight=false", async () => {
|
||||
transcribeFirstAudioMock.mockClear();
|
||||
|
||||
const ctx = await buildTelegramMessageContextForTest({
|
||||
message: {
|
||||
message_id: 4,
|
||||
chat: { id: -1001234567893, type: "supergroup", title: "Test Group 4" },
|
||||
date: 1700000300,
|
||||
text: undefined,
|
||||
from: { id: 45, first_name: "Dan" },
|
||||
voice: { file_id: "voice-4" },
|
||||
},
|
||||
allMedia: [{ path: "/tmp/voice4.ogg", contentType: "audio/ogg" }],
|
||||
options: { forceWasMentioned: true },
|
||||
cfg: {
|
||||
agents: { defaults: { model: "anthropic/claude-opus-4-5", workspace: "/tmp/openclaw" } },
|
||||
channels: { telegram: {} },
|
||||
messages: { groupChat: { mentionPatterns: ["\\bbot\\b"] } },
|
||||
},
|
||||
resolveGroupActivation: () => true,
|
||||
resolveGroupRequireMention: () => true,
|
||||
resolveTelegramGroupConfig: () => ({
|
||||
groupConfig: { requireMention: true, disableAudioPreflight: false },
|
||||
topicConfig: { disableAudioPreflight: true },
|
||||
}),
|
||||
const ctx = await buildGroupVoiceContext({
|
||||
messageId: 4,
|
||||
chatId: -1001234567893,
|
||||
title: "Test Group 4",
|
||||
date: 1700000300,
|
||||
fromId: 45,
|
||||
firstName: "Dan",
|
||||
fileId: "voice-4",
|
||||
mediaPath: "/tmp/voice4.ogg",
|
||||
groupDisableAudioPreflight: false,
|
||||
topicDisableAudioPreflight: true,
|
||||
});
|
||||
|
||||
expect(ctx).not.toBeNull();
|
||||
expect(transcribeFirstAudioMock).not.toHaveBeenCalled();
|
||||
expect(ctx?.ctxPayload?.Body).toContain("<media:audio>");
|
||||
expectAudioPlaceholderRendered(ctx);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ describe("web monitor inbox", () => {
|
||||
const sock = getSock();
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
return { onMessage, listener };
|
||||
return { onMessage, listener, sock };
|
||||
}
|
||||
|
||||
function expectSingleGroupMessage(
|
||||
@@ -44,10 +44,7 @@ describe("web monitor inbox", () => {
|
||||
}
|
||||
|
||||
it("captures media path for image messages", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
const { onMessage, listener, sock } = await runSingleUpsertAndCapture({
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
@@ -56,10 +53,7 @@ describe("web monitor inbox", () => {
|
||||
messageTimestamp: 1_700_000_100,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
});
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -116,10 +110,7 @@ describe("web monitor inbox", () => {
|
||||
const logPath = path.join(os.tmpdir(), `openclaw-log-test-${crypto.randomUUID()}.log`);
|
||||
setLoggerOverride({ level: "trace", file: logPath });
|
||||
|
||||
const onMessage = vi.fn();
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
const { listener } = await runSingleUpsertAndCapture({
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
@@ -129,10 +120,7 @@ describe("web monitor inbox", () => {
|
||||
pushName: "Tester",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
@@ -147,10 +135,7 @@ describe("web monitor inbox", () => {
|
||||
});
|
||||
|
||||
it("includes participant when marking group messages read", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
const { listener, sock } = await runSingleUpsertAndCapture({
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
@@ -163,10 +148,7 @@ describe("web monitor inbox", () => {
|
||||
message: { conversation: "group ping" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
});
|
||||
|
||||
expect(sock.readMessages).toHaveBeenCalledWith([
|
||||
{
|
||||
@@ -180,10 +162,7 @@ describe("web monitor inbox", () => {
|
||||
});
|
||||
|
||||
it("passes through group messages with participant metadata", async () => {
|
||||
const onMessage = vi.fn();
|
||||
const listener = await openMonitor(onMessage);
|
||||
const sock = getSock();
|
||||
const upsert = {
|
||||
const { onMessage, listener } = await runSingleUpsertAndCapture({
|
||||
type: "notify",
|
||||
messages: [
|
||||
{
|
||||
@@ -203,10 +182,7 @@ describe("web monitor inbox", () => {
|
||||
messageTimestamp: 1_700_000_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
sock.ev.emit("messages.upsert", upsert);
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
});
|
||||
|
||||
expect(onMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
|
||||
Reference in New Issue
Block a user