mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 18:24:47 +00:00
test: clear discord message process broad matchers
This commit is contained in:
@@ -445,11 +445,34 @@ function getReactionEmojis(): string[] {
|
||||
).map((call) => call[2]);
|
||||
}
|
||||
|
||||
function expectAckReactionRuntimeOptions(params?: {
|
||||
accountId?: string;
|
||||
ackReaction?: string;
|
||||
removeAckAfterReply?: boolean;
|
||||
}) {
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
expect(typeof value).toBe("object");
|
||||
expect(value).not.toBeNull();
|
||||
if (typeof value !== "object" || value === null) {
|
||||
throw new Error(`${label} was not an object`);
|
||||
}
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
|
||||
for (const [key, value] of Object.entries(fields)) {
|
||||
expect(record[key]).toEqual(value);
|
||||
}
|
||||
}
|
||||
|
||||
function expectAckReactionRuntimeOptions(
|
||||
options: unknown,
|
||||
params?: {
|
||||
accountId?: string;
|
||||
ackReaction?: string;
|
||||
removeAckAfterReply?: boolean;
|
||||
},
|
||||
) {
|
||||
const optionRecord = requireRecord(options, "reaction runtime options");
|
||||
requireRecord(optionRecord.rest, "reaction REST client");
|
||||
if (params?.accountId) {
|
||||
expect(optionRecord.accountId).toBe(params.accountId);
|
||||
}
|
||||
const messages: Record<string, unknown> = {};
|
||||
if (params?.ackReaction) {
|
||||
messages.ackReaction = params.ackReaction;
|
||||
@@ -457,13 +480,52 @@ function expectAckReactionRuntimeOptions(params?: {
|
||||
if (params?.removeAckAfterReply !== undefined) {
|
||||
messages.removeAckAfterReply = params.removeAckAfterReply;
|
||||
}
|
||||
return expect.objectContaining({
|
||||
rest: expect.anything(),
|
||||
...(Object.keys(messages).length > 0
|
||||
? { cfg: expect.objectContaining({ messages: expect.objectContaining(messages) }) }
|
||||
: {}),
|
||||
...(params?.accountId ? { accountId: params.accountId } : {}),
|
||||
});
|
||||
if (Object.keys(messages).length > 0) {
|
||||
const cfg = requireRecord(optionRecord.cfg, "reaction config");
|
||||
expectRecordFields(requireRecord(cfg.messages, "reaction message config"), messages);
|
||||
}
|
||||
}
|
||||
|
||||
function requireReactionCall(
|
||||
mock: typeof sendMocks.reactMessageDiscord | typeof sendMocks.removeReactionDiscord,
|
||||
index: number,
|
||||
) {
|
||||
const call = mock.mock.calls[index] as unknown[] | undefined;
|
||||
expect(call).toBeDefined();
|
||||
if (!call) {
|
||||
throw new Error(`missing reaction call ${index + 1}`);
|
||||
}
|
||||
return call;
|
||||
}
|
||||
|
||||
function expectReactionCallAt(
|
||||
mock: typeof sendMocks.reactMessageDiscord | typeof sendMocks.removeReactionDiscord,
|
||||
index: number,
|
||||
emoji: string,
|
||||
params?: {
|
||||
accountId?: string;
|
||||
ackReaction?: string;
|
||||
removeAckAfterReply?: boolean;
|
||||
channelId?: string;
|
||||
messageId?: string;
|
||||
},
|
||||
) {
|
||||
const call = requireReactionCall(mock, index);
|
||||
expect(call[0]).toBe(params?.channelId ?? "c1");
|
||||
expect(call[1]).toBe(params?.messageId ?? "m1");
|
||||
expect(call[2]).toBe(emoji);
|
||||
expectAckReactionRuntimeOptions(call[3], params);
|
||||
}
|
||||
|
||||
function expectReactionCallsContain(channelId: string, messageId: string, emoji: string) {
|
||||
const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
|
||||
[string, string, string]
|
||||
>;
|
||||
const hasCall = calls.some(
|
||||
([actualChannelId, actualMessageId, actualEmoji]) =>
|
||||
actualChannelId === channelId && actualMessageId === messageId && actualEmoji === emoji,
|
||||
);
|
||||
expect(hasCall).toBe(true);
|
||||
}
|
||||
|
||||
function expectReactAckCallAt(
|
||||
@@ -477,13 +539,7 @@ function expectReactAckCallAt(
|
||||
removeAckAfterReply?: boolean;
|
||||
},
|
||||
) {
|
||||
expect(sendMocks.reactMessageDiscord).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
params?.channelId ?? "c1",
|
||||
params?.messageId ?? "m1",
|
||||
emoji,
|
||||
expectAckReactionRuntimeOptions(params),
|
||||
);
|
||||
expectReactionCallAt(sendMocks.reactMessageDiscord, index, emoji, params);
|
||||
}
|
||||
|
||||
function expectRemoveAckCallAt(
|
||||
@@ -497,13 +553,7 @@ function expectRemoveAckCallAt(
|
||||
removeAckAfterReply?: boolean;
|
||||
},
|
||||
) {
|
||||
expect(sendMocks.removeReactionDiscord).toHaveBeenNthCalledWith(
|
||||
index + 1,
|
||||
params?.channelId ?? "c1",
|
||||
params?.messageId ?? "m1",
|
||||
emoji,
|
||||
expectAckReactionRuntimeOptions(params),
|
||||
);
|
||||
expectReactionCallAt(sendMocks.removeReactionDiscord, index, emoji, params);
|
||||
}
|
||||
|
||||
function createMockDraftStreamForTest() {
|
||||
@@ -512,13 +562,20 @@ function createMockDraftStreamForTest() {
|
||||
return draftStream;
|
||||
}
|
||||
|
||||
function expectPreviewEditContent(content: string) {
|
||||
const call = editMessageDiscord.mock.calls[0] as unknown[] | undefined;
|
||||
expect(call).toBeDefined();
|
||||
if (!call) {
|
||||
throw new Error("missing preview edit call");
|
||||
}
|
||||
expect(call[0]).toBe("c1");
|
||||
expect(call[1]).toBe("preview-1");
|
||||
expect(call[2]).toEqual({ content });
|
||||
requireRecord(requireRecord(call[3], "preview edit options").rest, "preview edit REST client");
|
||||
}
|
||||
|
||||
function expectSinglePreviewEdit() {
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "Hello\nWorld" },
|
||||
expect.objectContaining({ rest: expect.anything() }),
|
||||
);
|
||||
expectPreviewEditContent("Hello\nWorld");
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
@@ -601,12 +658,13 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(sendMocks.reactMessageDiscord).toHaveBeenCalled();
|
||||
expect(sendMocks.reactMessageDiscord.mock.calls[0]?.[3]).toEqual(
|
||||
expect.objectContaining({ rest: feedbackRest }),
|
||||
);
|
||||
expect(deliverDiscordReply).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ rest: deliveryRest }),
|
||||
const feedbackOptions = requireRecord(
|
||||
sendMocks.reactMessageDiscord.mock.calls[0]?.[3],
|
||||
"feedback reaction options",
|
||||
);
|
||||
expect(feedbackOptions.rest).toBe(feedbackRest);
|
||||
const deliveryParams = requireRecord(deliverDiscordReply.mock.calls[0]?.[0], "delivery params");
|
||||
expect(deliveryParams.rest).toBe(deliveryRest);
|
||||
expect(feedbackRest).not.toBe(deliveryRest);
|
||||
});
|
||||
|
||||
@@ -669,12 +727,9 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
await runProcessDiscordMessage(ctx);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
|
||||
[string, string, string]
|
||||
>;
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "📈"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "✉️"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", DEFAULT_EMOJIS.done]));
|
||||
expectReactionCallsContain("c1", "m1", "📈");
|
||||
expectReactionCallsContain("c1", "m1", "✉️");
|
||||
expectReactionCallsContain("c1", "m1", DEFAULT_EMOJIS.done);
|
||||
});
|
||||
|
||||
it("resolves tracked reaction to targets like the Discord reaction action", async () => {
|
||||
@@ -702,16 +757,20 @@ describe("processDiscordMessage ack reactions", () => {
|
||||
await runProcessDiscordMessage(ctx);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(discordTargetMocks.resolveDiscordTargetChannelId).toHaveBeenCalledWith(
|
||||
"user:u1",
|
||||
expect.objectContaining({ accountId: "default" }),
|
||||
const resolveCall = discordTargetMocks.resolveDiscordTargetChannelId.mock.calls[0] as
|
||||
| unknown[]
|
||||
| undefined;
|
||||
expect(resolveCall).toBeDefined();
|
||||
if (!resolveCall) {
|
||||
throw new Error("missing Discord target resolve call");
|
||||
}
|
||||
expect(resolveCall[0]).toBe("user:u1");
|
||||
expect(requireRecord(resolveCall[1], "Discord target resolve options").accountId).toBe(
|
||||
"default",
|
||||
);
|
||||
const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
|
||||
[string, string, string]
|
||||
>;
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", "📈"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", "✉️"]));
|
||||
expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", DEFAULT_EMOJIS.done]));
|
||||
expectReactionCallsContain("dm-u1", "m1", "📈");
|
||||
expectReactionCallsContain("dm-u1", "m1", "✉️");
|
||||
expectReactionCallsContain("dm-u1", "m1", DEFAULT_EMOJIS.done);
|
||||
});
|
||||
|
||||
it("shows stall emojis for long no-progress runs", async () => {
|
||||
@@ -911,7 +970,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
BodyForAgent: "hello from discord voice",
|
||||
CommandBody: "hello from discord voice",
|
||||
Transcript: "hello from discord voice",
|
||||
@@ -939,7 +998,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
to: "user:U1",
|
||||
accountId: "default",
|
||||
});
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
ChatType: "direct",
|
||||
From: "discord:U1",
|
||||
To: "user:U1",
|
||||
@@ -978,16 +1037,22 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastRouteUpdate()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastRouteUpdate(), "last route update"), {
|
||||
sessionKey: "agent:main:main",
|
||||
channel: "discord",
|
||||
to: "user:222",
|
||||
accountId: "default",
|
||||
mainDmOwnerPin: {
|
||||
});
|
||||
expectRecordFields(
|
||||
requireRecord(
|
||||
requireRecord(getLastRouteUpdate(), "last route update").mainDmOwnerPin,
|
||||
"main DM owner pin",
|
||||
),
|
||||
{
|
||||
ownerRecipient: "111",
|
||||
senderRecipient: "222",
|
||||
},
|
||||
});
|
||||
);
|
||||
});
|
||||
|
||||
it("stores group lastRoute with channel target", async () => {
|
||||
@@ -1016,7 +1081,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchReplyOptions()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchReplyOptions(), "dispatch reply options"), {
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
disableBlockStreaming: true,
|
||||
});
|
||||
@@ -1098,7 +1163,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
MessageSid: "orig-123",
|
||||
MessageSidFull: "proxy-456",
|
||||
});
|
||||
@@ -1169,7 +1234,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
SessionKey: "agent:main:subagent:child",
|
||||
MessageThreadId: "thread-1",
|
||||
});
|
||||
@@ -1202,7 +1267,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
SessionKey: "agent:main:discord:channel:thread-1",
|
||||
MessageThreadId: "thread-1",
|
||||
ModelParentSessionKey: "agent:main:discord:channel:parent-1",
|
||||
@@ -1246,7 +1311,7 @@ describe("processDiscordMessage session routing", () => {
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(rest.get).toHaveBeenCalled();
|
||||
expect(getLastDispatchCtx()).toMatchObject({
|
||||
expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
|
||||
SessionKey: threadSessionKey,
|
||||
MessageThreadId: "thread-1",
|
||||
});
|
||||
@@ -1320,12 +1385,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith(expect.stringContaining("Exec"));
|
||||
expect(draftStream.update).toHaveBeenCalledWith(expect.stringContaining("exec done"));
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "done" },
|
||||
expect.objectContaining({ rest: expect.anything() }),
|
||||
);
|
||||
expectPreviewEditContent("done");
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1365,12 +1425,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
await runProcessDiscordMessage(ctx);
|
||||
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: longReply },
|
||||
expect.objectContaining({ rest: expect.anything() }),
|
||||
);
|
||||
expectPreviewEditContent(longReply);
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -1535,9 +1590,12 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
expect(draftStream.update).toHaveBeenCalledTimes(1);
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Shelling");
|
||||
expect(draftStream.flush).toHaveBeenCalledTimes(1);
|
||||
expect(dispatchInboundMessage.mock.calls[0]?.[0]?.replyOptions).toMatchObject({
|
||||
suppressDefaultToolProgressMessages: true,
|
||||
});
|
||||
expect(
|
||||
requireRecord(
|
||||
dispatchInboundMessage.mock.calls[0]?.[0]?.replyOptions,
|
||||
"dispatch reply options",
|
||||
).suppressDefaultToolProgressMessages,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("does not start Discord progress drafts for text-only accepted turns", async () => {
|
||||
@@ -1588,12 +1646,7 @@ describe("processDiscordMessage draft streaming", () => {
|
||||
|
||||
expect(draftStream.update).toHaveBeenCalledWith("Shelling\n🛠️ Exec\n• exec done");
|
||||
expect(deliverDiscordReply).not.toHaveBeenCalled();
|
||||
expect(editMessageDiscord).toHaveBeenCalledWith(
|
||||
"c1",
|
||||
"preview-1",
|
||||
{ content: "done" },
|
||||
expect.objectContaining({ rest: expect.anything() }),
|
||||
);
|
||||
expectPreviewEditContent("done");
|
||||
});
|
||||
|
||||
it("uses raw tool-progress detail in Discord progress drafts", async () => {
|
||||
|
||||
Reference in New Issue
Block a user