



























@@ -445,25 +445,87 @@ function getReactionEmojis(): string[] {
445445).map((call) => call[2]);
446446}
447447448-function expectAckReactionRuntimeOptions(params?: {
449-accountId?: string;
450-ackReaction?: string;
451-removeAckAfterReply?: boolean;
452-}) {
448+function requireRecord(value: unknown, label: string): Record<string, unknown> {
449+expect(typeof value).toBe("object");
450+expect(value).not.toBeNull();
451+if (typeof value !== "object" || value === null) {
452+throw new Error(`${label} was not an object`);
453+}
454+return value as Record<string, unknown>;
455+}
456+457+function expectRecordFields(record: Record<string, unknown>, fields: Record<string, unknown>) {
458+for (const [key, value] of Object.entries(fields)) {
459+expect(record[key]).toEqual(value);
460+}
461+}
462+463+function expectAckReactionRuntimeOptions(
464+options: unknown,
465+params?: {
466+accountId?: string;
467+ackReaction?: string;
468+removeAckAfterReply?: boolean;
469+},
470+) {
471+const optionRecord = requireRecord(options, "reaction runtime options");
472+requireRecord(optionRecord.rest, "reaction REST client");
473+if (params?.accountId) {
474+expect(optionRecord.accountId).toBe(params.accountId);
475+}
453476const messages: Record<string, unknown> = {};
454477if (params?.ackReaction) {
455478messages.ackReaction = params.ackReaction;
456479}
457480if (params?.removeAckAfterReply !== undefined) {
458481messages.removeAckAfterReply = params.removeAckAfterReply;
459482}
460-return expect.objectContaining({
461-rest: expect.anything(),
462- ...(Object.keys(messages).length > 0
463- ? { cfg: expect.objectContaining({ messages: expect.objectContaining(messages) }) }
464- : {}),
465- ...(params?.accountId ? { accountId: params.accountId } : {}),
466-});
483+if (Object.keys(messages).length > 0) {
484+const cfg = requireRecord(optionRecord.cfg, "reaction config");
485+expectRecordFields(requireRecord(cfg.messages, "reaction message config"), messages);
486+}
487+}
488+489+function requireReactionCall(
490+mock: typeof sendMocks.reactMessageDiscord | typeof sendMocks.removeReactionDiscord,
491+index: number,
492+) {
493+const call = mock.mock.calls[index] as unknown[] | undefined;
494+expect(call).toBeDefined();
495+if (!call) {
496+throw new Error(`missing reaction call ${index + 1}`);
497+}
498+return call;
499+}
500+501+function expectReactionCallAt(
502+mock: typeof sendMocks.reactMessageDiscord | typeof sendMocks.removeReactionDiscord,
503+index: number,
504+emoji: string,
505+params?: {
506+accountId?: string;
507+ackReaction?: string;
508+removeAckAfterReply?: boolean;
509+channelId?: string;
510+messageId?: string;
511+},
512+) {
513+const call = requireReactionCall(mock, index);
514+expect(call[0]).toBe(params?.channelId ?? "c1");
515+expect(call[1]).toBe(params?.messageId ?? "m1");
516+expect(call[2]).toBe(emoji);
517+expectAckReactionRuntimeOptions(call[3], params);
518+}
519+520+function expectReactionCallsContain(channelId: string, messageId: string, emoji: string) {
521+const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
522+[string, string, string]
523+>;
524+const hasCall = calls.some(
525+([actualChannelId, actualMessageId, actualEmoji]) =>
526+actualChannelId === channelId && actualMessageId === messageId && actualEmoji === emoji,
527+);
528+expect(hasCall).toBe(true);
467529}
468530469531function expectReactAckCallAt(
@@ -477,13 +539,7 @@ function expectReactAckCallAt(
477539removeAckAfterReply?: boolean;
478540},
479541) {
480-expect(sendMocks.reactMessageDiscord).toHaveBeenNthCalledWith(
481-index + 1,
482-params?.channelId ?? "c1",
483-params?.messageId ?? "m1",
484-emoji,
485-expectAckReactionRuntimeOptions(params),
486-);
542+expectReactionCallAt(sendMocks.reactMessageDiscord, index, emoji, params);
487543}
488544489545function expectRemoveAckCallAt(
@@ -497,13 +553,7 @@ function expectRemoveAckCallAt(
497553removeAckAfterReply?: boolean;
498554},
499555) {
500-expect(sendMocks.removeReactionDiscord).toHaveBeenNthCalledWith(
501-index + 1,
502-params?.channelId ?? "c1",
503-params?.messageId ?? "m1",
504-emoji,
505-expectAckReactionRuntimeOptions(params),
506-);
556+expectReactionCallAt(sendMocks.removeReactionDiscord, index, emoji, params);
507557}
508558509559function createMockDraftStreamForTest() {
@@ -512,13 +562,20 @@ function createMockDraftStreamForTest() {
512562return draftStream;
513563}
514564565+function expectPreviewEditContent(content: string) {
566+const call = editMessageDiscord.mock.calls[0] as unknown[] | undefined;
567+expect(call).toBeDefined();
568+if (!call) {
569+throw new Error("missing preview edit call");
570+}
571+expect(call[0]).toBe("c1");
572+expect(call[1]).toBe("preview-1");
573+expect(call[2]).toEqual({ content });
574+requireRecord(requireRecord(call[3], "preview edit options").rest, "preview edit REST client");
575+}
576+515577function expectSinglePreviewEdit() {
516-expect(editMessageDiscord).toHaveBeenCalledWith(
517-"c1",
518-"preview-1",
519-{ content: "Hello\nWorld" },
520-expect.objectContaining({ rest: expect.anything() }),
521-);
578+expectPreviewEditContent("Hello\nWorld");
522579expect(deliverDiscordReply).not.toHaveBeenCalled();
523580}
524581@@ -601,12 +658,13 @@ describe("processDiscordMessage ack reactions", () => {
601658await runProcessDiscordMessage(ctx);
602659603660expect(sendMocks.reactMessageDiscord).toHaveBeenCalled();
604-expect(sendMocks.reactMessageDiscord.mock.calls[0]?.[3]).toEqual(
605-expect.objectContaining({ rest: feedbackRest }),
606-);
607-expect(deliverDiscordReply).toHaveBeenCalledWith(
608-expect.objectContaining({ rest: deliveryRest }),
661+const feedbackOptions = requireRecord(
662+sendMocks.reactMessageDiscord.mock.calls[0]?.[3],
663+"feedback reaction options",
609664);
665+expect(feedbackOptions.rest).toBe(feedbackRest);
666+const deliveryParams = requireRecord(deliverDiscordReply.mock.calls[0]?.[0], "delivery params");
667+expect(deliveryParams.rest).toBe(deliveryRest);
610668expect(feedbackRest).not.toBe(deliveryRest);
611669});
612670@@ -669,12 +727,9 @@ describe("processDiscordMessage ack reactions", () => {
669727await runProcessDiscordMessage(ctx);
670728await vi.runAllTimersAsync();
671729672-const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
673-[string, string, string]
674->;
675-expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "📈"]));
676-expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", "✉️"]));
677-expect(calls).toContainEqual(expect.arrayContaining(["c1", "m1", DEFAULT_EMOJIS.done]));
730+expectReactionCallsContain("c1", "m1", "📈");
731+expectReactionCallsContain("c1", "m1", "✉️");
732+expectReactionCallsContain("c1", "m1", DEFAULT_EMOJIS.done);
678733});
679734680735it("resolves tracked reaction to targets like the Discord reaction action", async () => {
@@ -702,16 +757,20 @@ describe("processDiscordMessage ack reactions", () => {
702757await runProcessDiscordMessage(ctx);
703758await vi.runAllTimersAsync();
704759705-expect(discordTargetMocks.resolveDiscordTargetChannelId).toHaveBeenCalledWith(
706-"user:u1",
707-expect.objectContaining({ accountId: "default" }),
760+const resolveCall = discordTargetMocks.resolveDiscordTargetChannelId.mock.calls[0] as
761+| unknown[]
762+| undefined;
763+expect(resolveCall).toBeDefined();
764+if (!resolveCall) {
765+throw new Error("missing Discord target resolve call");
766+}
767+expect(resolveCall[0]).toBe("user:u1");
768+expect(requireRecord(resolveCall[1], "Discord target resolve options").accountId).toBe(
769+"default",
708770);
709-const calls = sendMocks.reactMessageDiscord.mock.calls as unknown as Array<
710-[string, string, string]
711->;
712-expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", "📈"]));
713-expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", "✉️"]));
714-expect(calls).toContainEqual(expect.arrayContaining(["dm-u1", "m1", DEFAULT_EMOJIS.done]));
771+expectReactionCallsContain("dm-u1", "m1", "📈");
772+expectReactionCallsContain("dm-u1", "m1", "✉️");
773+expectReactionCallsContain("dm-u1", "m1", DEFAULT_EMOJIS.done);
715774});
716775717776it("shows stall emojis for long no-progress runs", async () => {
@@ -911,7 +970,7 @@ describe("processDiscordMessage session routing", () => {
911970912971await runProcessDiscordMessage(ctx);
913972914-expect(getLastDispatchCtx()).toMatchObject({
973+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
915974BodyForAgent: "hello from discord voice",
916975CommandBody: "hello from discord voice",
917976Transcript: "hello from discord voice",
@@ -939,7 +998,7 @@ describe("processDiscordMessage session routing", () => {
939998to: "user:U1",
940999accountId: "default",
9411000});
942-expect(getLastDispatchCtx()).toMatchObject({
1001+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
9431002ChatType: "direct",
9441003From: "discord:U1",
9451004To: "user:U1",
@@ -978,16 +1037,22 @@ describe("processDiscordMessage session routing", () => {
97810379791038await runProcessDiscordMessage(ctx);
9801039981-expect(getLastRouteUpdate()).toMatchObject({
1040+expectRecordFields(requireRecord(getLastRouteUpdate(), "last route update"), {
9821041sessionKey: "agent:main:main",
9831042channel: "discord",
9841043to: "user:222",
9851044accountId: "default",
986-mainDmOwnerPin: {
1045+});
1046+expectRecordFields(
1047+requireRecord(
1048+requireRecord(getLastRouteUpdate(), "last route update").mainDmOwnerPin,
1049+"main DM owner pin",
1050+),
1051+{
9871052ownerRecipient: "111",
9881053senderRecipient: "222",
9891054},
990-});
1055+);
9911056});
99210579931058it("stores group lastRoute with channel target", async () => {
@@ -1016,7 +1081,7 @@ describe("processDiscordMessage session routing", () => {
1016108110171082await runProcessDiscordMessage(ctx);
101810831019-expect(getLastDispatchReplyOptions()).toMatchObject({
1084+expectRecordFields(requireRecord(getLastDispatchReplyOptions(), "dispatch reply options"), {
10201085sourceReplyDeliveryMode: "message_tool_only",
10211086disableBlockStreaming: true,
10221087});
@@ -1098,7 +1163,7 @@ describe("processDiscordMessage session routing", () => {
1098116310991164await runProcessDiscordMessage(ctx);
110011651101-expect(getLastDispatchCtx()).toMatchObject({
1166+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
11021167MessageSid: "orig-123",
11031168MessageSidFull: "proxy-456",
11041169});
@@ -1169,7 +1234,7 @@ describe("processDiscordMessage session routing", () => {
1169123411701235await runProcessDiscordMessage(ctx);
117112361172-expect(getLastDispatchCtx()).toMatchObject({
1237+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
11731238SessionKey: "agent:main:subagent:child",
11741239MessageThreadId: "thread-1",
11751240});
@@ -1202,7 +1267,7 @@ describe("processDiscordMessage session routing", () => {
1202126712031268await runProcessDiscordMessage(ctx);
120412691205-expect(getLastDispatchCtx()).toMatchObject({
1270+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
12061271SessionKey: "agent:main:discord:channel:thread-1",
12071272MessageThreadId: "thread-1",
12081273ModelParentSessionKey: "agent:main:discord:channel:parent-1",
@@ -1246,7 +1311,7 @@ describe("processDiscordMessage session routing", () => {
12461311await runProcessDiscordMessage(ctx);
1247131212481313expect(rest.get).toHaveBeenCalled();
1249-expect(getLastDispatchCtx()).toMatchObject({
1314+expectRecordFields(requireRecord(getLastDispatchCtx(), "dispatch context"), {
12501315SessionKey: threadSessionKey,
12511316MessageThreadId: "thread-1",
12521317});
@@ -1320,12 +1385,7 @@ describe("processDiscordMessage draft streaming", () => {
1320138513211386expect(draftStream.update).toHaveBeenCalledWith(expect.stringContaining("Exec"));
13221387expect(draftStream.update).toHaveBeenCalledWith(expect.stringContaining("exec done"));
1323-expect(editMessageDiscord).toHaveBeenCalledWith(
1324-"c1",
1325-"preview-1",
1326-{ content: "done" },
1327-expect.objectContaining({ rest: expect.anything() }),
1328-);
1388+expectPreviewEditContent("done");
13291389expect(deliverDiscordReply).not.toHaveBeenCalled();
13301390});
13311391@@ -1365,12 +1425,7 @@ describe("processDiscordMessage draft streaming", () => {
1365142513661426await runProcessDiscordMessage(ctx);
136714271368-expect(editMessageDiscord).toHaveBeenCalledWith(
1369-"c1",
1370-"preview-1",
1371-{ content: longReply },
1372-expect.objectContaining({ rest: expect.anything() }),
1373-);
1428+expectPreviewEditContent(longReply);
13741429expect(deliverDiscordReply).not.toHaveBeenCalled();
13751430});
13761431@@ -1535,9 +1590,12 @@ describe("processDiscordMessage draft streaming", () => {
15351590expect(draftStream.update).toHaveBeenCalledTimes(1);
15361591expect(draftStream.update).toHaveBeenCalledWith("Shelling");
15371592expect(draftStream.flush).toHaveBeenCalledTimes(1);
1538-expect(dispatchInboundMessage.mock.calls[0]?.[0]?.replyOptions).toMatchObject({
1539-suppressDefaultToolProgressMessages: true,
1540-});
1593+expect(
1594+requireRecord(
1595+dispatchInboundMessage.mock.calls[0]?.[0]?.replyOptions,
1596+"dispatch reply options",
1597+).suppressDefaultToolProgressMessages,
1598+).toBe(true);
15411599});
1542160015431601it("does not start Discord progress drafts for text-only accepted turns", async () => {
@@ -1588,12 +1646,7 @@ describe("processDiscordMessage draft streaming", () => {
1588164615891647expect(draftStream.update).toHaveBeenCalledWith("Shelling\n🛠️ Exec\n• exec done");
15901648expect(deliverDiscordReply).not.toHaveBeenCalled();
1591-expect(editMessageDiscord).toHaveBeenCalledWith(
1592-"c1",
1593-"preview-1",
1594-{ content: "done" },
1595-expect.objectContaining({ rest: expect.anything() }),
1596-);
1649+expectPreviewEditContent("done");
15971650});
1598165115991652it("uses raw tool-progress detail in Discord progress drafts", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。