From 1fd9fe2b33a99225571bf4462bb92bf36148bc90 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 31 May 2026 03:30:35 -0400 Subject: [PATCH] fix(ci): isolate timer-sensitive tests --- src/agents/cli-runner.binding-flush.test.ts | 19 +++++++++++++++++-- src/agents/model-catalog-browse.test.ts | 11 +++++++++-- src/channels/typing.test.ts | 12 +++++++++++- .../runner.skip-tiny-audio.test.ts | 10 +++++++--- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/agents/cli-runner.binding-flush.test.ts b/src/agents/cli-runner.binding-flush.test.ts index 3193e0ce872..9425c1f35aa 100644 --- a/src/agents/cli-runner.binding-flush.test.ts +++ b/src/agents/cli-runner.binding-flush.test.ts @@ -9,10 +9,13 @@ describe("isCliBindingFlushed", () => { const workspaceDir = "/tmp/openclaw-workspace"; beforeEach(() => { + vi.useRealTimers(); restoreCliRunnerTestDeps(); }); afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); restoreCliRunnerTestDeps(); }); @@ -34,14 +37,21 @@ describe("isCliBindingFlushed", () => { }); it("retries up to three times before giving up", async () => { + vi.useFakeTimers(); const probe = vi.fn(async () => false); setCliRunnerTestDeps({ claudeCliSessionTranscriptHasContent: probe }); - expect(await isCliBindingFlushed("sid-cold", "claude-cli", workspaceDir)).toBe(false); + const resultPromise = isCliBindingFlushed("sid-cold", "claude-cli", workspaceDir); + await vi.advanceTimersByTimeAsync(0); + await vi.advanceTimersByTimeAsync(50); + await vi.advanceTimersByTimeAsync(150); + + await expect(resultPromise).resolves.toBe(false); expect(probe).toHaveBeenCalledTimes(3); }); it("succeeds when the transcript becomes visible on a later retry", async () => { + vi.useFakeTimers(); let calls = 0; const probe = vi.fn(async () => { calls += 1; @@ -49,7 +59,11 @@ describe("isCliBindingFlushed", () => { }); setCliRunnerTestDeps({ claudeCliSessionTranscriptHasContent: probe }); - expect(await isCliBindingFlushed("sid-late", "claude-cli", workspaceDir)).toBe(true); + const resultPromise = isCliBindingFlushed("sid-late", "claude-cli", workspaceDir); + await vi.advanceTimersByTimeAsync(0); + await vi.advanceTimersByTimeAsync(50); + + await expect(resultPromise).resolves.toBe(true); expect(probe).toHaveBeenCalledTimes(2); }); @@ -72,6 +86,7 @@ describe("isCliBindingFlushed", () => { expect(errored).not.toHaveBeenCalled(); expect(probe).toHaveBeenCalledTimes(3); } finally { + vi.clearAllTimers(); vi.useRealTimers(); } }); diff --git a/src/agents/model-catalog-browse.test.ts b/src/agents/model-catalog-browse.test.ts index b4c540c78ec..1784a90d548 100644 --- a/src/agents/model-catalog-browse.test.ts +++ b/src/agents/model-catalog-browse.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { loadModelCatalogForBrowse } from "./model-catalog-browse.js"; @@ -24,7 +24,12 @@ function config(params: { providerWildcard?: boolean } = {}): OpenClawConfig { } describe("loadModelCatalogForBrowse", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + afterEach(() => { + vi.clearAllTimers(); vi.useRealTimers(); }); @@ -65,6 +70,7 @@ describe("loadModelCatalogForBrowse", () => { }); it("returns an empty catalog when read-only catalog loading times out", async () => { + vi.useFakeTimers(); const onTimeout = vi.fn(); const loadCatalog = vi.fn( () => @@ -80,9 +86,10 @@ describe("loadModelCatalogForBrowse", () => { onTimeout, }); + await vi.advanceTimersByTimeAsync(5); await expect(resultPromise).resolves.toEqual([]); expect(onTimeout).toHaveBeenCalledExactlyOnceWith(5); - await new Promise((resolve) => setTimeout(resolve, 15)); + await vi.advanceTimersByTimeAsync(10); }); it("uses the default timeout when timeoutMs is non-finite", async () => { diff --git a/src/channels/typing.test.ts b/src/channels/typing.test.ts index 3fb9e109251..4e1db7be43e 100644 --- a/src/channels/typing.test.ts +++ b/src/channels/typing.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js"; import { createTypingCallbacks } from "./typing.js"; @@ -16,6 +16,7 @@ async function withFakeTimers(run: () => Promise) { try { await run(); } finally { + vi.clearAllTimers(); vi.useRealTimers(); } } @@ -56,6 +57,15 @@ function createTypingHarness(overrides: TypingCallbackOverrides = {}) { } describe("createTypingCallbacks", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + }); + it("invokes start on reply start", async () => { const { start, onStartError, callbacks } = createTypingHarness(); diff --git a/src/media-understanding/runner.skip-tiny-audio.test.ts b/src/media-understanding/runner.skip-tiny-audio.test.ts index 843995eaa37..11048af982d 100644 --- a/src/media-understanding/runner.skip-tiny-audio.test.ts +++ b/src/media-understanding/runner.skip-tiny-audio.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; import type { MsgContext } from "../auto-reply/templating.js"; import type { OpenClawConfig } from "../config/types.js"; import { MIN_AUDIO_FILE_BYTES } from "./defaults.js"; @@ -78,6 +78,10 @@ async function runAudioCapabilityWithTranscriber(params: { } describe("runCapability skips tiny audio files", () => { + beforeEach(() => { + vi.useRealTimers(); + }); + it("skips audio transcription when file is smaller than MIN_AUDIO_FILE_BYTES", async () => { await withAudioFixture({ filePrefix: "openclaw-tiny-audio", @@ -172,7 +176,7 @@ describe("runCapability skips tiny audio files", () => { media, cache, transcribeAudio: async () => { - throw new Error("upstream 500"); + throw Object.assign(new Error("HTTP 400 validation failed"), { status: 400 }); }, }); @@ -189,7 +193,7 @@ describe("runCapability skips tiny audio files", () => { throw new Error("expected failed audio decision attempt"); } expect(attempt.outcome).toBe("failed"); - expect(attempt.reason).toContain("upstream 500"); + expect(attempt.reason).toContain("HTTP 400 validation failed"); }, }); });