From e294c154a6c7aace5ded3425587f191f13ce3e29 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 19 Aug 2026 23:36:33 -0700 Subject: [PATCH] test(gateway): resolve mocked implementations without factory timing `server.sessions.compaction-read-errors` captured the real transcript reader as a side effect of its `vi.mock` factory and consumed it in `beforeEach`. Vitest runs a factory on first import of the mocked module, and the gateway-server project is `isolate: false`, so on a warm module graph the factory can still be unrun when the hook fires. Run 32335933003 hit that on `main`: all five tests failed with `transcript reader mock was not initialized` while the same 24-file shard passes locally in the same order. Resolve the real implementations with `vi.importActual` at use time instead, so there is no state that can be observed before it is written. Applies the same repair to the two siblings sharing the invariant: `server.sessions.create` (which had worked around it by force-importing both mocked modules in `beforeAll`) and `client-voice-session` (whose `beforeEach` silently skipped installing the real append when the capture was missing). `tools.optional` keeps its guard: it evaluates inside the mock implementation, so the factory has necessarily run by then. --- ...er.sessions.compaction-read-errors.test.ts | 23 +++++----- src/gateway/server.sessions.create.test.ts | 46 ++++++++++--------- src/talk/client-voice-session.test.ts | 15 +++--- 3 files changed, 44 insertions(+), 40 deletions(-) diff --git a/src/gateway/server.sessions.compaction-read-errors.test.ts b/src/gateway/server.sessions.compaction-read-errors.test.ts index 56afcc1296a7..e7894b6d76d9 100644 --- a/src/gateway/server.sessions.compaction-read-errors.test.ts +++ b/src/gateway/server.sessions.compaction-read-errors.test.ts @@ -19,30 +19,31 @@ type LoadTranscriptEvents = (typeof import("../config/sessions/session-accessor.sqlite-read.js"))["loadTranscriptEvents"]; const transcriptReads = vi.hoisted(() => ({ - actual: undefined as LoadTranscriptEvents | undefined, load: vi.fn(), })); vi.mock("../config/sessions/session-accessor.sqlite-read.js", async (importOriginal) => { const actual = await importOriginal(); - transcriptReads.actual = actual.loadTranscriptEvents; - transcriptReads.load.mockImplementation(actual.loadTranscriptEvents); return { ...actual, loadTranscriptEvents: transcriptReads.load }; }); const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness(); -function requireTranscriptReader(): LoadTranscriptEvents { - if (!transcriptReads.actual) { - throw new Error("transcript reader mock was not initialized"); - } - return transcriptReads.actual; +// Read the real implementation back here rather than capturing it inside the mock +// factory: Vitest runs that factory on first import of the mocked module, and this +// project is `isolate: false`, so on a warm module graph the factory can still be +// unrun when the first `beforeEach` fires. +async function actualTranscriptReader(): Promise { + const actual = await vi.importActual< + typeof import("../config/sessions/session-accessor.sqlite-read.js") + >("../config/sessions/session-accessor.sqlite-read.js"); + return actual.loadTranscriptEvents; } -beforeEach(() => { +beforeEach(async () => { transcriptReads.load.mockReset(); - transcriptReads.load.mockImplementation(requireTranscriptReader()); + transcriptReads.load.mockImplementation(await actualTranscriptReader()); }); async function seedCompactionSession(params: { @@ -117,7 +118,7 @@ test("sessions.compact reports model compaction transcript re-read failures as u storePath, nativeHarness: true, }); - const events = await requireTranscriptReader()(scope); + const events = await (await actualTranscriptReader())(scope); transcriptReads.load.mockResolvedValueOnce(events).mockRejectedValueOnce(transcriptReadError()); const { ws } = await openClient(); diff --git a/src/gateway/server.sessions.create.test.ts b/src/gateway/server.sessions.create.test.ts index c63af9114a20..d6c73937ac75 100644 --- a/src/gateway/server.sessions.create.test.ts +++ b/src/gateway/server.sessions.create.test.ts @@ -99,12 +99,10 @@ const dashboardTitleGenerationMocks = vi.hoisted(() => ({ })); const dashboardTitleScheduleMocks = vi.hoisted(() => ({ - actual: undefined as ScheduleChatDashboardSessionTitle | undefined, schedule: vi.fn(), })); const sessionTranscriptReaderMocks = vi.hoisted(() => ({ - actual: undefined as ReadSessionMessageCountAsync | undefined, readCount: vi.fn(), })); @@ -136,15 +134,11 @@ vi.mock("../auto-reply/reply/conversation-label-generator.js", () => ({ vi.mock("./server-methods/chat-send-background.js", async (importOriginal) => { const actual = await importOriginal(); - dashboardTitleScheduleMocks.actual = actual.scheduleChatDashboardSessionTitle; - dashboardTitleScheduleMocks.schedule.mockImplementation(actual.scheduleChatDashboardSessionTitle); return { ...actual, scheduleChatDashboardSessionTitle: dashboardTitleScheduleMocks.schedule }; }); vi.mock("./session-transcript-readers.js", async (importOriginal) => { const actual = await importOriginal(); - sessionTranscriptReaderMocks.actual = actual.readSessionMessageCountAsync; - sessionTranscriptReaderMocks.readCount.mockImplementation(actual.readSessionMessageCountAsync); return { ...actual, readSessionMessageCountAsync: sessionTranscriptReaderMocks.readCount }; }); @@ -159,20 +153,32 @@ beforeAll(async () => { gitWorkspaceTemplateRoot = await fs.realpath( await fs.mkdtemp(path.join(await fs.realpath(os.tmpdir()), "openclaw-session-git-template-")), ); - const workspace = createGitWorkspace(gitWorkspaceTemplateRoot); - await Promise.all([ - import("./server-methods/chat-send-background.js"), - import("./session-transcript-readers.js"), - workspace, - ]); - gitWorkspaceTemplate = await workspace; + gitWorkspaceTemplate = await createGitWorkspace(gitWorkspaceTemplateRoot); }); afterAll(async () => { await fs.rm(gitWorkspaceTemplateRoot, { recursive: true, force: true }); }); -beforeEach(() => { +// Read the real implementations back here rather than capturing them inside the +// mock factories: Vitest runs a factory on first import of the mocked module, and +// this project is `isolate: false`, so on a warm module graph a factory can still +// be unrun when the first `beforeEach` fires. +async function actualDashboardTitleScheduler(): Promise { + const actual = await vi.importActual( + "./server-methods/chat-send-background.js", + ); + return actual.scheduleChatDashboardSessionTitle; +} + +async function actualSessionMessageCountReader(): Promise { + const actual = await vi.importActual( + "./session-transcript-readers.js", + ); + return actual.readSessionMessageCountAsync; +} + +beforeEach(async () => { sessionDiffBaselineMocks.captureGate = undefined; sessionDiffBaselineMocks.captureStarted = undefined; sessionDiffBaselineMocks.capture.mockClear(); @@ -182,15 +188,11 @@ beforeEach(() => { dashboardTitleGenerationMocks.generate.mockReset(); dashboardTitleGenerationMocks.generate.mockResolvedValue("Generated Dashboard Title"); dashboardTitleScheduleMocks.schedule.mockReset(); - if (!dashboardTitleScheduleMocks.actual) { - throw new Error("actual dashboard title scheduler was not loaded"); - } - dashboardTitleScheduleMocks.schedule.mockImplementation(dashboardTitleScheduleMocks.actual); + dashboardTitleScheduleMocks.schedule.mockImplementation(await actualDashboardTitleScheduler()); sessionTranscriptReaderMocks.readCount.mockReset(); - if (!sessionTranscriptReaderMocks.actual) { - throw new Error("actual session transcript reader was not loaded"); - } - sessionTranscriptReaderMocks.readCount.mockImplementation(sessionTranscriptReaderMocks.actual); + sessionTranscriptReaderMocks.readCount.mockImplementation( + await actualSessionMessageCountReader(), + ); }); async function makeNonGitTempDir(prefix: string): Promise { diff --git a/src/talk/client-voice-session.test.ts b/src/talk/client-voice-session.test.ts index f479ec5f725f..e9d86c07277a 100644 --- a/src/talk/client-voice-session.test.ts +++ b/src/talk/client-voice-session.test.ts @@ -51,8 +51,6 @@ const { sendDurableMessageBatch } = vi.hoisted(() => ({ vi.mock("../config/sessions/session-accessor.js", async (importOriginal) => { const actual = await importOriginal(); - sessionAccessorMocks.actualAppendTranscriptMessage = actual.appendTranscriptMessage; - sessionAccessorMocks.appendTranscriptMessage.mockImplementation(actual.appendTranscriptMessage); return { ...actual, appendTranscriptMessage: sessionAccessorMocks.appendTranscriptMessage }; }); vi.mock("../channels/message/runtime.js", () => ({ @@ -114,11 +112,14 @@ describe("client voice session", () => { setTestEnvValue("OPENCLAW_STATE_DIR", tempDir); sendDurableMessageBatch.mockReset().mockResolvedValue({ status: "sent" }); sessionAccessorMocks.appendTranscriptMessage.mockReset(); - if (sessionAccessorMocks.actualAppendTranscriptMessage) { - sessionAccessorMocks.appendTranscriptMessage.mockImplementation( - sessionAccessorMocks.actualAppendTranscriptMessage, - ); - } + // Resolve the real append here rather than capturing it inside the mock factory: + // Vitest runs that factory on first import of the mocked module, so on a warm + // module graph it can still be unrun when this hook fires. + const { appendTranscriptMessage } = await vi.importActual< + typeof import("../config/sessions/session-accessor.js") + >("../config/sessions/session-accessor.js"); + sessionAccessorMocks.actualAppendTranscriptMessage = appendTranscriptMessage; + sessionAccessorMocks.appendTranscriptMessage.mockImplementation(appendTranscriptMessage); }); afterEach(async () => {