mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-21 01:51:39 -06:00
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:
@@ -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();
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user