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.
This commit is contained in:
Peter Steinberger
2026-08-19 23:36:33 -07:00
parent dc59703a16
commit e294c154a6
3 changed files with 44 additions and 40 deletions
@@ -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<LoadTranscriptEvents>(),
}));
vi.mock("../config/sessions/session-accessor.sqlite-read.js", async (importOriginal) => {
const actual =
await importOriginal<typeof import("../config/sessions/session-accessor.sqlite-read.js")>();
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<LoadTranscriptEvents> {
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();
+24 -22
View File
@@ -99,12 +99,10 @@ const dashboardTitleGenerationMocks = vi.hoisted(() => ({
}));
const dashboardTitleScheduleMocks = vi.hoisted(() => ({
actual: undefined as ScheduleChatDashboardSessionTitle | undefined,
schedule: vi.fn<ScheduleChatDashboardSessionTitle>(),
}));
const sessionTranscriptReaderMocks = vi.hoisted(() => ({
actual: undefined as ReadSessionMessageCountAsync | undefined,
readCount: vi.fn<ReadSessionMessageCountAsync>(),
}));
@@ -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<typeof import("./server-methods/chat-send-background.js")>();
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<typeof import("./session-transcript-readers.js")>();
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<ScheduleChatDashboardSessionTitle> {
const actual = await vi.importActual<typeof import("./server-methods/chat-send-background.js")>(
"./server-methods/chat-send-background.js",
);
return actual.scheduleChatDashboardSessionTitle;
}
async function actualSessionMessageCountReader(): Promise<ReadSessionMessageCountAsync> {
const actual = await vi.importActual<typeof import("./session-transcript-readers.js")>(
"./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<string> {
+8 -7
View File
@@ -51,8 +51,6 @@ const { sendDurableMessageBatch } = vi.hoisted(() => ({
vi.mock("../config/sessions/session-accessor.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../config/sessions/session-accessor.js")>();
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 () => {