test: clear discord message process broad matchers

This commit is contained in:
Peter Steinberger
2026-05-10 12:46:42 +01:00
parent b0da65dc39
commit 1429f9a181

View File

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