



























@@ -53,6 +53,52 @@ beforeEach(() => {
5353__resetDiscordDirectoryCacheForTest();
5454});
555556+function isRecord(value: unknown): value is Record<string, unknown> {
57+return typeof value === "object" && value !== null && !Array.isArray(value);
58+}
59+60+function requireRecord(value: unknown, label: string): Record<string, unknown> {
61+if (!isRecord(value)) {
62+throw new Error(`expected ${label} to be an object`);
63+}
64+return value;
65+}
66+67+function requireArray(value: unknown, label: string): unknown[] {
68+if (!Array.isArray(value)) {
69+throw new Error(`expected ${label} to be an array`);
70+}
71+return value;
72+}
73+74+function expectRecordFields(value: unknown, label: string, expected: Record<string, unknown>) {
75+const record = requireRecord(value, label);
76+for (const [key, expectedValue] of Object.entries(expected)) {
77+expect(record[key]).toEqual(expectedValue);
78+}
79+}
80+81+function requireRestOptions(mock: ReturnType<typeof vi.fn>, callIndex: number) {
82+return requireRecord(mock.mock.calls[callIndex]?.[1], "Discord REST options");
83+}
84+85+function requireRestBody(mock: ReturnType<typeof vi.fn>, callIndex = 0) {
86+return requireRecord(requireRestOptions(mock, callIndex).body, "Discord REST body");
87+}
88+89+function expectSingleReceiptPart(receipt: unknown, expected: Record<string, unknown>) {
90+const receiptRecord = requireRecord(receipt, "send receipt");
91+const parts = requireArray(receiptRecord.parts, "send receipt parts");
92+expect(parts).toHaveLength(1);
93+expectRecordFields(parts[0], "send receipt part", expected);
94+}
95+96+function expectBodyFileName(body: unknown, expectedName: string) {
97+const files = requireArray(requireRecord(body, "Discord REST body").files, "Discord files");
98+expect(files).toHaveLength(1);
99+expectRecordFields(files[0], "Discord file", { name: expectedName });
100+}
101+56102describe("resolveDiscordTargetChannelId", () => {
57103it("creates a DM channel for user targets", async () => {
58104const { rest, postMock } = makeDiscordRest();
@@ -139,16 +185,15 @@ describe("sendMessageDiscord", () => {
139185token: "t",
140186cfg: DISCORD_TEST_CFG,
141187});
142-expect(res).toMatchObject({ messageId: "msg1", channelId: "789" });
143-expect(res.receipt).toMatchObject({
188+expect(res.messageId).toBe("msg1");
189+expect(res.channelId).toBe("789");
190+expectRecordFields(res.receipt, "send receipt", {
144191primaryPlatformMessageId: "msg1",
145192platformMessageIds: ["msg1"],
146-parts: [expect.objectContaining({ platformMessageId: "msg1", kind: "text" })],
147193});
148-expect(postMock).toHaveBeenCalledWith(
149-Routes.channelMessages("789"),
150-expect.objectContaining({ body: { content: "hello world" } }),
151-);
194+expectSingleReceiptPart(res.receipt, { platformMessageId: "msg1", kind: "text" });
195+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
196+expect(requireRestBody(postMock).content).toBe("hello world");
152197});
153198154199it("rewrites cached @username mentions to id-based mentions", async () => {
@@ -169,10 +214,8 @@ describe("sendMessageDiscord", () => {
169214cfg: DISCORD_TEST_CFG,
170215accountId: "default",
171216});
172-expect(postMock).toHaveBeenCalledWith(
173-Routes.channelMessages("789"),
174-expect.objectContaining({ body: { content: "ping <@123456789012345678>" } }),
175-);
217+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
218+expect(requireRestBody(postMock).content).toBe("ping <@123456789012345678>");
176219});
177220178221it("rewrites configured @username aliases to id-based mentions", async () => {
@@ -197,10 +240,8 @@ describe("sendMessageDiscord", () => {
197240} as never,
198241accountId: "default",
199242});
200-expect(postMock).toHaveBeenCalledWith(
201-Routes.channelMessages("789"),
202-expect.objectContaining({ body: { content: "ping <@123456789012345678>" } }),
203-);
243+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
244+expect(requireRestBody(postMock).content).toBe("ping <@123456789012345678>");
204245});
205246206247it("uses configured defaultAccount for cached mention rewriting when accountId is omitted", async () => {
@@ -231,10 +272,8 @@ describe("sendMessageDiscord", () => {
231272},
232273} as never,
233274});
234-expect(postMock).toHaveBeenCalledWith(
235-Routes.channelMessages("789"),
236-expect.objectContaining({ body: { content: "ping <@222333444555666777>" } }),
237-);
275+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
276+expect(requireRestBody(postMock).content).toBe("ping <@222333444555666777>");
238277});
239278240279it("auto-creates a forum thread when target is a Forum channel", async () => {
@@ -250,22 +289,19 @@ describe("sendMessageDiscord", () => {
250289token: "t",
251290cfg: DISCORD_TEST_CFG,
252291});
253-expect(res).toMatchObject({ messageId: "starter1", channelId: "thread1" });
254-expect(res.receipt).toMatchObject({
292+expect(res.messageId).toBe("starter1");
293+expect(res.channelId).toBe("thread1");
294+expectRecordFields(res.receipt, "send receipt", {
255295threadId: "thread1",
256296platformMessageIds: ["starter1"],
257-parts: [expect.objectContaining({ platformMessageId: "starter1", kind: "text" })],
258297});
298+expectSingleReceiptPart(res.receipt, { platformMessageId: "starter1", kind: "text" });
259299// Should POST to threads route, not channelMessages.
260-expect(postMock).toHaveBeenCalledWith(
261-Routes.threads("forum1"),
262-expect.objectContaining({
263-body: {
264-name: "Discussion topic",
265-message: { content: "Discussion topic\nBody of the post" },
266-},
267-}),
268-);
300+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.threads("forum1"));
301+expect(requireRestBody(postMock)).toEqual({
302+name: "Discussion topic",
303+message: { content: "Discussion topic\nBody of the post" },
304+});
269305});
270306271307it("posts media as a follow-up message in forum channels", async () => {
@@ -276,31 +312,20 @@ describe("sendMessageDiscord", () => {
276312cfg: DISCORD_TEST_CFG,
277313mediaUrl: "file:///tmp/photo.jpg",
278314});
279-expect(res).toMatchObject({ messageId: "starter1", channelId: "thread1" });
280-expect(res.receipt).toMatchObject({
315+expect(res.messageId).toBe("starter1");
316+expect(res.channelId).toBe("thread1");
317+expectRecordFields(res.receipt, "send receipt", {
281318threadId: "thread1",
282319platformMessageIds: ["starter1"],
283-parts: [expect.objectContaining({ platformMessageId: "starter1", kind: "media" })],
284-});
285-expect(postMock).toHaveBeenNthCalledWith(
286-1,
287-Routes.threads("forum1"),
288-expect.objectContaining({
289-body: {
290-name: "Topic",
291-message: { content: "Topic" },
292-},
293-}),
294-);
295-expect(postMock).toHaveBeenNthCalledWith(
296-2,
297-Routes.channelMessages("thread1"),
298-expect.objectContaining({
299-body: expect.objectContaining({
300-files: [expect.objectContaining({ name: "photo.jpg" })],
301-}),
302-}),
303-);
320+});
321+expectSingleReceiptPart(res.receipt, { platformMessageId: "starter1", kind: "media" });
322+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.threads("forum1"));
323+expect(requireRestBody(postMock, 0)).toEqual({
324+name: "Topic",
325+message: { content: "Topic" },
326+});
327+expect(postMock.mock.calls[1]?.[0]).toBe(Routes.channelMessages("thread1"));
328+expectBodyFileName(requireRestBody(postMock, 1), "photo.jpg");
304329});
305330306331it("chunks long forum posts into follow-up messages", async () => {
@@ -329,16 +354,10 @@ describe("sendMessageDiscord", () => {
329354token: "t",
330355cfg: DISCORD_TEST_CFG,
331356});
332-expect(postMock).toHaveBeenNthCalledWith(
333-1,
334-Routes.userChannels(),
335-expect.objectContaining({ body: { recipient_id: "123" } }),
336-);
337-expect(postMock).toHaveBeenNthCalledWith(
338-2,
339-Routes.channelMessages("chan1"),
340-expect.objectContaining({ body: { content: "hiya" } }),
341-);
357+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.userChannels());
358+expect(requireRestBody(postMock, 0).recipient_id).toBe("123");
359+expect(postMock.mock.calls[1]?.[0]).toBe(Routes.channelMessages("chan1"));
360+expect(requireRestBody(postMock, 1).content).toBe("hiya");
342361expect(res.channelId).toBe("chan1");
343362});
344363@@ -445,18 +464,11 @@ describe("sendMessageDiscord", () => {
445464mediaUrl: "file:///tmp/photo.jpg",
446465});
447466expect(res.messageId).toBe("msg");
448-expect(postMock).toHaveBeenCalledWith(
449-Routes.channelMessages("789"),
450-expect.objectContaining({
451-body: expect.objectContaining({
452-files: [expect.objectContaining({ name: "photo.jpg" })],
453-}),
454-}),
455-);
456-expect(loadWebMedia).toHaveBeenCalledWith(
457-"file:///tmp/photo.jpg",
458-expect.objectContaining({ maxBytes: 100 * 1024 * 1024 }),
459-);
467+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
468+expectBodyFileName(requireRestBody(postMock), "photo.jpg");
469+expect(loadWebMedia).toHaveBeenCalledWith("file:///tmp/photo.jpg", {
470+maxBytes: 100 * 1024 * 1024,
471+});
460472});
461473462474it("passes mediaAccess workspaceDir when loading relative media attachments", async () => {
@@ -473,12 +485,11 @@ describe("sendMessageDiscord", () => {
473485},
474486});
475487476-expect(loadWebMedia).toHaveBeenCalledWith(
477-"chart.png",
478-expect.objectContaining({
479-workspaceDir: "/tmp/agent-workspace",
480-}),
488+const mediaOptions = requireRecord(
489+vi.mocked(loadWebMedia).mock.calls[0]?.[1],
490+"media load options",
481491);
492+expect(mediaOptions.workspaceDir).toBe("/tmp/agent-workspace");
482493});
483494484495it("prefers the caller-provided filename for media attachments", async () => {
@@ -493,14 +504,8 @@ describe("sendMessageDiscord", () => {
493504filename: "renderable.png",
494505});
495506496-expect(postMock).toHaveBeenCalledWith(
497-Routes.channelMessages("789"),
498-expect.objectContaining({
499-body: expect.objectContaining({
500-files: [expect.objectContaining({ name: "renderable.png" })],
501-}),
502-}),
503-);
507+expect(postMock.mock.calls[0]?.[0]).toBe(Routes.channelMessages("789"));
508+expectBodyFileName(requireRestBody(postMock), "renderable.png");
504509});
505510506511it("uses configured discord mediaMaxMb for uploads", async () => {
@@ -520,10 +525,9 @@ describe("sendMessageDiscord", () => {
520525},
521526});
522527523-expect(loadWebMedia).toHaveBeenCalledWith(
524-"file:///tmp/photo.jpg",
525-expect.objectContaining({ maxBytes: 32 * 1024 * 1024 }),
526-);
528+expect(loadWebMedia).toHaveBeenCalledWith("file:///tmp/photo.jpg", {
529+maxBytes: 32 * 1024 * 1024,
530+});
527531});
528532529533it("sends media with empty text without content field", async () => {
@@ -889,10 +893,8 @@ describe("edit/delete message helpers", () => {
889893{ content: "hello" },
890894{ rest, token: "t", cfg: DISCORD_TEST_CFG },
891895);
892-expect(patchMock).toHaveBeenCalledWith(
893-Routes.channelMessage("chan1", "m1"),
894-expect.objectContaining({ body: { content: "hello" } }),
895-);
896+expect(patchMock.mock.calls[0]?.[0]).toBe(Routes.channelMessage("chan1", "m1"));
897+expect(requireRestBody(patchMock).content).toBe("hello");
896898});
897899898900it("deletes message", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。