

























@@ -44,6 +44,53 @@ function downloadRequest(
4444return request as { filePathHint?: string; url?: string };
4545}
464647+type ScheduledTimer = {
48+callback: () => unknown;
49+handle: ReturnType<typeof setTimeout>;
50+};
51+52+function resolveActiveScheduledTimersForDelay(
53+setTimeoutSpy: ReturnType<typeof vi.spyOn>,
54+clearTimeoutSpy: ReturnType<typeof vi.spyOn>,
55+delayMs: number,
56+): ScheduledTimer[] {
57+const clearedHandles = new Set(
58+(clearTimeoutSpy.mock.calls as Array<Parameters<typeof clearTimeout>>).map(
59+([handle]) => handle,
60+),
61+);
62+return (setTimeoutSpy.mock.calls as Array<Parameters<typeof setTimeout>>).flatMap(
63+(call, index) => {
64+if (call[1] !== delayMs) {
65+return [];
66+}
67+const handle = setTimeoutSpy.mock.results[index]?.value as ReturnType<typeof setTimeout>;
68+if (clearedHandles.has(handle) || typeof call[0] !== "function") {
69+return [];
70+}
71+return [{ callback: call[0] as () => unknown, handle }];
72+},
73+);
74+}
75+76+async function flushActiveScheduledTimersForDelay(params: {
77+setTimeoutSpy: ReturnType<typeof vi.spyOn>;
78+clearTimeoutSpy: ReturnType<typeof vi.spyOn>;
79+delayMs: number;
80+expectedCount: number;
81+}) {
82+const timers = resolveActiveScheduledTimersForDelay(
83+params.setTimeoutSpy,
84+params.clearTimeoutSpy,
85+params.delayMs,
86+);
87+expect(timers).toHaveLength(params.expectedCount);
88+for (const timer of timers) {
89+clearTimeout(timer.handle);
90+await timer.callback();
91+}
92+}
93+4794describe("telegram inbound media", () => {
4895// Parallel vitest shards can make this suite slower than the standalone run.
4996const INBOUND_MEDIA_TEST_TIMEOUT_MS = process.platform === "win32" ? 120_000 : 90_000;
@@ -346,6 +393,13 @@ describe("telegram media groups", () => {
346393const runtimeError = vi.fn();
347394const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
348395const fetchSpy = mockTelegramPngDownload();
396+let nextTimerHandle = 1;
397+const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(() => {
398+const handle = nextTimerHandle;
399+nextTimerHandle += 1;
400+return handle as unknown as ReturnType<typeof setTimeout>;
401+});
402+const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
349403350404try {
351405for (const scenario of [
@@ -354,7 +408,7 @@ describe("telegram media groups", () => {
354408{
355409chat: { id: 42, type: "private" as const },
356410from: { id: 777, is_bot: false, first_name: "Ada" },
357-message_id: 1,
411+message_id: 101,
358412caption: "Here are my photos",
359413date: 1736380800,
360414media_group_id: "album123",
@@ -364,7 +418,7 @@ describe("telegram media groups", () => {
364418{
365419chat: { id: 42, type: "private" as const },
366420from: { id: 777, is_bot: false, first_name: "Ada" },
367-message_id: 2,
421+message_id: 102,
368422date: 1736380801,
369423media_group_id: "album123",
370424photo: [{ file_id: "photo2" }],
@@ -383,7 +437,7 @@ describe("telegram media groups", () => {
383437{
384438chat: { id: 42, type: "private" as const },
385439from: { id: 777, is_bot: false, first_name: "Ada" },
386-message_id: 11,
440+message_id: 111,
387441caption: "Album A",
388442date: 1736380800,
389443media_group_id: "albumA",
@@ -393,7 +447,7 @@ describe("telegram media groups", () => {
393447{
394448chat: { id: 42, type: "private" as const },
395449from: { id: 777, is_bot: false, first_name: "Ada" },
396-message_id: 12,
450+message_id: 112,
397451caption: "Album B",
398452date: 1736380801,
399453media_group_id: "albumB",
@@ -407,6 +461,8 @@ describe("telegram media groups", () => {
407461]) {
408462replySpy.mockClear();
409463runtimeError.mockClear();
464+setTimeoutSpy.mockClear();
465+clearTimeoutSpy.mockClear();
410466411467await Promise.all(
412468scenario.messages.map((message) =>
@@ -419,62 +475,65 @@ describe("telegram media groups", () => {
419475);
420476421477expect(replySpy).not.toHaveBeenCalled();
422-await vi.waitFor(
423-() => {
424-expect(replySpy).toHaveBeenCalledTimes(scenario.expectedReplyCount);
425-},
426-{ timeout: MEDIA_GROUP_WAIT_TIMEOUT_MS, interval: 2 },
427-);
478+await flushActiveScheduledTimersForDelay({
479+ setTimeoutSpy,
480+ clearTimeoutSpy,
481+delayMs: TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs,
482+expectedCount: scenario.expectedReplyCount,
483+});
484+expect(replySpy).toHaveBeenCalledTimes(scenario.expectedReplyCount);
428485429486expect(runtimeError).not.toHaveBeenCalled();
430487scenario.assert(replySpy);
431488}
432489} finally {
490+setTimeoutSpy.mockRestore();
491+clearTimeoutSpy.mockRestore();
433492fetchSpy.mockRestore();
434493}
435494},
436495MEDIA_GROUP_TEST_TIMEOUT_MS,
437496);
438497439498it(
440-"flushes same-id forum topic media groups in parallel",
499+"buffers same-id forum topic media groups independently",
441500async () => {
442501const originalLoadConfig = telegramBotDepsForTest.getRuntimeConfig;
443502telegramBotDepsForTest.getRuntimeConfig = (() => ({
444503channels: {
445504telegram: {
446505dmPolicy: "open",
447506allowFrom: ["*"],
507+groupAllowFrom: ["777"],
448508groupPolicy: "open",
449-groups: { "*": { requireMention: false } },
509+groups: {
510+"-10042": { allowFrom: ["777"], groupPolicy: "open", requireMention: false },
511+},
450512},
451513},
452514})) as typeof telegramBotDepsForTest.getRuntimeConfig;
453515454516const runtimeError = vi.fn();
455517const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
456518const fetchSpy = mockTelegramPngDownload();
457-let releaseFirstReply: (() => void) | undefined;
458-const firstReplyStarted = new Promise<void>((resolve) => {
459-replySpy.mockImplementationOnce(async (_ctx, opts?: { onReplyStart?: () => unknown }) => {
460-await opts?.onReplyStart?.();
461-resolve();
462-await new Promise<void>((release) => {
463-releaseFirstReply = release;
464-});
465-return undefined;
466-});
519+let nextTimerHandle = 1;
520+const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout").mockImplementation(() => {
521+const handle = nextTimerHandle;
522+nextTimerHandle += 1;
523+return handle as unknown as ReturnType<typeof setTimeout>;
467524});
525+const clearTimeoutSpy = vi.spyOn(globalThis, "clearTimeout");
468526469527try {
470528await Promise.all([
471529handler({
472530message: {
473531chat: { id: -10042, type: "supergroup" as const, is_forum: true },
474532from: { id: 777, is_bot: false, first_name: "Ada" },
475-message_id: 31,
533+message_id: 131,
476534message_thread_id: 101,
477-caption: "Topic one album",
535+is_topic_message: true,
536+caption: "@openclaw_bot Topic one album",
478537date: 1736380800,
479538media_group_id: "album-shared-by-telegram",
480539photo: [{ file_id: "topic1photo" }],
@@ -486,9 +545,10 @@ describe("telegram media groups", () => {
486545message: {
487546chat: { id: -10042, type: "supergroup" as const, is_forum: true },
488547from: { id: 777, is_bot: false, first_name: "Ada" },
489-message_id: 32,
548+message_id: 132,
490549message_thread_id: 202,
491-caption: "Topic two album",
550+is_topic_message: true,
551+caption: "@openclaw_bot Topic two album",
492552date: 1736380801,
493553media_group_id: "album-shared-by-telegram",
494554photo: [{ file_id: "topic2photo" }],
@@ -498,15 +558,17 @@ describe("telegram media groups", () => {
498558}),
499559]);
500560501-await firstReplyStarted;
502-expect(replySpy).toHaveBeenCalledTimes(1);
503-await vi.waitFor(
504-() => {
505-expect(replySpy).toHaveBeenCalledTimes(2);
506-},
507-{ timeout: MEDIA_GROUP_WAIT_TIMEOUT_MS, interval: 2 },
561+const timers = resolveActiveScheduledTimersForDelay(
562+setTimeoutSpy,
563+clearTimeoutSpy,
564+TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs,
508565);
509-566+expect(timers).toHaveLength(2);
567+for (const timer of timers) {
568+clearTimeout(timer.handle);
569+await timer.callback();
570+}
571+expect(replySpy).toHaveBeenCalledTimes(2);
510572const firstPayload = replyPayload(replySpy, 0);
511573const secondPayload = replyPayload(replySpy, 1);
512574expect([firstPayload.Body, secondPayload.Body]).toEqual(
@@ -519,7 +581,15 @@ describe("telegram media groups", () => {
519581expect(secondPayload.MediaPaths).toHaveLength(1);
520582expect(runtimeError).not.toHaveBeenCalled();
521583} finally {
522-releaseFirstReply?.();
584+for (const timer of resolveActiveScheduledTimersForDelay(
585+setTimeoutSpy,
586+clearTimeoutSpy,
587+TELEGRAM_TEST_TIMINGS.mediaGroupFlushMs,
588+)) {
589+clearTimeout(timer.handle);
590+}
591+setTimeoutSpy.mockRestore();
592+clearTimeoutSpy.mockRestore();
523593fetchSpy.mockRestore();
524594telegramBotDepsForTest.getRuntimeConfig = originalLoadConfig;
525595}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。