From 8dba458cfdb55ec77f602abfdd72edbb40fc7dfd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 1 Aug 2026 12:18:53 -0700 Subject: [PATCH] test(gateway): dedupe chat server fixtures --- .../server.chat.gateway-server-chat-b.test.ts | 1546 +++++------------ 1 file changed, 420 insertions(+), 1126 deletions(-) diff --git a/src/gateway/server.chat.gateway-server-chat-b.test.ts b/src/gateway/server.chat.gateway-server-chat-b.test.ts index e98448051155..bd78f857f3ec 100644 --- a/src/gateway/server.chat.gateway-server-chat-b.test.ts +++ b/src/gateway/server.chat.gateway-server-chat-b.test.ts @@ -138,11 +138,7 @@ async function withGatewayChatHarness( options?: { headers?: Record }, ) { const ws = await harness.openWs(options?.headers); - const createSessionDir = async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - return sessionDir; - }; + const createSessionDir = async () => openDirectChatSession().sessionDir; try { await run({ ws, createSessionDir }); @@ -160,14 +156,10 @@ function testSessionFilePath(sessionDir: string, sessionId: string): string { return path.join(sessionDir, `${sessionId}.jsonl`); } -async function writeMainSessionStore(_sessionDir?: string, sessionId = "sess-main") { - await writeSessionStore({ - entries: { - main: { - sessionId, - updatedAt: futureFixtureUpdatedAt(), - }, - }, +async function writeMainSessionStore(sessionId = "sess-main") { + await writeStoredMainSession({ + sessionId, + updatedAt: futureFixtureUpdatedAt(), }); } @@ -198,7 +190,6 @@ async function writeGatewayConfig(config: Record) { } async function writeMainSessionTranscript( - _sessionDir: string, events: unknown[], sessionId = "sess-main", opts?: { @@ -226,18 +217,90 @@ async function writeMainSessionTranscript( } } -async function withDirectChatSession(run: (sessionDir: string) => Promise) { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); +async function withDirectChatSession( + run: (sessionDir: string, storePath: string) => Promise, +) { + const { sessionDir, storePath } = openDirectChatSession(); try { - await run(sessionDir); + await run(sessionDir, storePath); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } } +type StoredSessionEntry = Parameters[0]["entries"][string]; + +function openDirectChatSession() { + const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const storePath = path.join(sessionDir, "sessions.json"); + testState.sessionStorePath = storePath; + return { sessionDir, storePath }; +} + +function resetDirectChatSession() { + dispatchInboundMessageMock.mockReset(); + testState.sessionStorePath = undefined; + clearConfigCache(); +} + +async function writeStoredMainSession(entry: StoredSessionEntry = {}) { + await writeSessionStore({ + entries: { + main: { + sessionId: "sess-main", + updatedAt: Date.now(), + ...entry, + }, + }, + }); +} + +type DirectChatMethod = "chat.abort" | "chat.history" | "chat.send" | "chat.startup"; + +async function callDirectChatHandler( + method: DirectChatMethod, + options: GatewayRequestHandlerOptions, +) { + const { chatHandlers } = await import("./server-methods/chat.js"); + await expectDefined(chatHandlers[method], `${method} test invariant`)(options); +} + +type DirectChatCallOptions = Omit< + GatewayRequestHandlerOptions, + "client" | "isWebchatConnect" | "req" +> & { + id: string; + client?: GatewayRequestHandlerOptions["client"]; + isWebchatConnect?: GatewayRequestHandlerOptions["isWebchatConnect"]; + req?: GatewayRequestHandlerOptions["req"]; +}; + +async function callDirectChat(method: DirectChatMethod, options: DirectChatCallOptions) { + const { client, id, isWebchatConnect, req, ...handlerOptions } = options; + await callDirectChatHandler(method, { + ...handlerOptions, + req: req ?? { type: "req", id, method, params: options.params }, + client: client ?? null, + isWebchatConnect: isWebchatConnect ?? (() => false), + }); +} + +function createControlUiClient( + scopes = ["operator.write", "operator.admin"], + properties: Record = {}, +) { + return { + ...properties, + connect: { + client: { + id: GATEWAY_CLIENT_NAMES.CONTROL_UI, + mode: GATEWAY_CLIENT_MODES.WEBCHAT, + }, + scopes, + }, + } as never; +} + async function sendControlUiChat(params: { authenticatedUserId?: string; authenticatedUserProfile?: { @@ -268,19 +331,12 @@ async function sendControlUiChat(params: { params: requestParams, }, params: requestParams, - client: { + client: createControlUiClient(undefined, { ...(params.authenticatedUserId ? { authenticatedUserId: params.authenticatedUserId } : {}), ...(params.authenticatedUserProfile ? { authenticatedUserProfile: params.authenticatedUserProfile } : {}), - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write", "operator.admin"], - }, - } as never, + }), isWebchatConnect: () => true, respond: params.respond, context: params.context, @@ -290,26 +346,15 @@ async function sendControlUiChat(params: { await handleChatSend(options, params.onAdmissionOwned); return; } - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )(options); + await callDirectChatHandler("chat.send", options); } test("chat.send replays a cached result after the session is archived", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { dispatchInboundMessageMock.mockClear(); - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - archivedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + archivedAt: Date.now(), }); const context = createDirectChatContext(); const runId = "idem-archived-cached-result"; @@ -321,20 +366,14 @@ test("chat.send replays a cached result after the session is archived", async () }); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown; meta?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ + await callDirectChat("chat.send", { + id: "cached", req: { type: "req", id: "cached", method: "chat.send" }, params: { sessionKey: "main", message: "retry completed send", idempotencyKey: runId, }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error, meta) => { responses.push({ ok, payload, error, meta }); }) as RespondFn, @@ -351,9 +390,7 @@ test("chat.send replays a cached result after the session is archived", async () ]); expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); @@ -434,7 +471,7 @@ async function prepareMainHistoryHarness(params: { }) { await connectOk(params.ws); const sessionDir = await params.createSessionDir(); - await writeMainSessionStore(sessionDir, params.sessionId); + await writeMainSessionStore(params.sessionId); return sessionDir; } @@ -442,10 +479,9 @@ describe("gateway server chat", () => { test.each(["chat.history", "chat.startup"] as const)( "%s replays the active plan snapshot in inFlightRun", async (method) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeMainSessionStore(sessionDir); + await writeMainSessionStore(); const context = createDirectChatContext(); const controller = new AbortController(); context.chatAbortControllers.set("run-active", { @@ -463,16 +499,9 @@ describe("gateway server chat", () => { steps: [{ step: "Reconnect clients", status: "in_progress" }], }; const responses: Array<{ ok: boolean; payload?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers[method], - `${method} test invariant`, - )({ - req: { type: "req", id: method, method, params: { sessionKey: "main" } }, + await callDirectChat(method, { + id: method, params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload) => responses.push({ ok, payload })) as RespondFn, context, }); @@ -497,21 +526,14 @@ describe("gateway server chat", () => { ); test("chat.history returns catalog-backed session metadata with history", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); testState.agentConfig = { model: { primary: "test-provider/catalog-model" }, }; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "test-provider", - model: "catalog-model", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "test-provider", + model: "catalog-model", }); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; const config = { @@ -538,21 +560,9 @@ describe("gateway server chat", () => { routeVariants: catalog, }), }); - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.history"], - 'chatHandlers["chat.history"] test invariant', - )({ - req: { - type: "req", - id: "history-no-catalog", - method: "chat.history", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.history", { + id: "history-no-catalog", params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); }) as RespondFn, @@ -602,20 +612,15 @@ describe("gateway server chat", () => { test("chat.history exposes persisted and synthetic session metadata for startup hydration", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const updatedAt = Date.now(); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt, - modelProvider: "openai", - model: "gpt-5", - contextTokens: 128_000, - }, - }, + await writeStoredMainSession({ + updatedAt, + modelProvider: "openai", + model: "gpt-5", + contextTokens: 128_000, }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "persisted metadata", { timestamp: updatedAt }), ]); @@ -692,19 +697,14 @@ describe("gateway server chat", () => { }, }); await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const updatedAt = Date.now(); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt, - modelProvider: "openai", - model: "gpt-5", - }, - }, + await writeStoredMainSession({ + updatedAt, + modelProvider: "openai", + model: "gpt-5", }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "startup hydrate", { timestamp: updatedAt }), ]); @@ -751,8 +751,7 @@ describe("gateway server chat", () => { }); test("chat.startup memoizes config projections and invalidates them on config change", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); + openDirectChatSession(); testState.agentConfig = { model: { primary: "test-provider/memo-model" }, models: { "test-provider/memo-model": {} }, @@ -781,22 +780,11 @@ describe("gateway server chat", () => { }; }), }); - const { chatHandlers } = await import("./server-methods/chat.js"); const requestStartup = async () => { const responses: Array<{ ok: boolean; payload?: unknown }> = []; - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: `startup-memo-${responses.length}`, - method: "chat.startup", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.startup", { + id: `startup-memo-${responses.length}`, params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload) => responses.push({ ok, payload })) as RespondFn, context, }); @@ -852,28 +840,15 @@ describe("gateway server chat", () => { routeVariants: [], })), }); - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); await writeSessionStore({ entries: { "agent:work:main": { sessionId: "sess-work", updatedAt: Date.now() } }, }); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-fallback-owner", - method: "chat.startup", - params: { sessionKey: "agent:work:main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-fallback-owner", params: { sessionKey: "agent:work:main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => responses.push({ ok, payload, error })) as RespondFn, context, }); @@ -913,28 +888,15 @@ describe("gateway server chat", () => { }; }), }); - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); await writeSessionStore({ entries: { main: { sessionId: "sess-main", updatedAt: Date.now() } }, }); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-config-advanced", - method: "chat.startup", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-config-advanced", params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => responses.push({ ok, payload, error })) as RespondFn, context, }); @@ -979,28 +941,15 @@ describe("gateway server chat", () => { }; }), }); - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); await writeSessionStore({ entries: { main: { sessionId: "sess-main", updatedAt: Date.now() } }, }); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-config-equivalent", - method: "chat.startup", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-config-equivalent", params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => responses.push({ ok, payload, error })) as RespondFn, context, }); @@ -1025,18 +974,11 @@ describe("gateway server chat", () => { }); test("chat.startup does not wait for slow optional model catalog metadata", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "test-provider", - model: "slow-catalog-model", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "test-provider", + model: "slow-catalog-model", }); const catalog = createDeferred< @@ -1049,21 +991,9 @@ describe("gateway server chat", () => { .mockReturnValue(catalog.promise), getRuntimeConfig: () => ({}), }); - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-slow-catalog", - method: "chat.startup", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-slow-catalog", params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); }) as RespondFn, @@ -1089,18 +1019,11 @@ describe("gateway server chat", () => { }); test("chat.history degrades promptly when the optional model catalog is slow", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "test-provider", - model: "slow-catalog-model", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "test-provider", + model: "slow-catalog-model", }); const catalog = createDeferred< @@ -1113,21 +1036,9 @@ describe("gateway server chat", () => { .mockReturnValue(catalog.promise), getRuntimeConfig: () => ({}), }); - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.history"], - 'chatHandlers["chat.history"] test invariant', - )({ - req: { - type: "req", - id: "history-slow-catalog", - method: "chat.history", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.history", { + id: "history-slow-catalog", params: { sessionKey: "main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => responses.push({ ok, payload, error })) as RespondFn, context, }); @@ -1161,9 +1072,8 @@ describe("gateway server chat", () => { }, }, async (state) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); const config = { agents: { defaults: { @@ -1307,21 +1217,9 @@ describe("gateway server chat", () => { selectedProfileId: "openai:api", selectedRoute: { authRequirement: "api-key" }, }); - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-dual-route-catalog", - method: "chat.startup", - params: { sessionKey: "agent:work:main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-dual-route-catalog", params: { sessionKey: "agent:work:main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); }) as RespondFn, @@ -1373,19 +1271,9 @@ describe("gateway server chat", () => { ["agent:work:legacy-auto", "platform"], ].entries()) { responses.length = 0; - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: `startup-preferred-route-${index}`, - method: "chat.startup", - params: { sessionKey }, - }, + await callDirectChat("chat.startup", { + id: `startup-preferred-route-${index}`, params: { sessionKey }, - client: null, - isWebchatConnect: () => false, respond: ((ok, responsePayload, error) => { responses.push({ ok, payload: responsePayload, error }); }) as RespondFn, @@ -1453,9 +1341,8 @@ describe("gateway server chat", () => { }); test("chat.startup scopes metadata to agent session keys without explicit agentId", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); await writeSessionStore({ entries: { "agent:work:main": { @@ -1530,21 +1417,9 @@ describe("gateway server chat", () => { }), getRuntimeConfig: () => config, }); - const { chatHandlers } = await import("./server-methods/chat.js"); - - await expectDefined( - chatHandlers["chat.startup"], - 'chatHandlers["chat.startup"] test invariant', - )({ - req: { - type: "req", - id: "startup-agent-scoped-metadata", - method: "chat.startup", - params: { sessionKey: "agent:work:main" }, - }, + await callDirectChat("chat.startup", { + id: "startup-agent-scoped-metadata", params: { sessionKey: "agent:work:main" }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); }) as RespondFn, @@ -1648,19 +1523,12 @@ describe("gateway server chat", () => { }); test("chat.send returns in_flight when duplicate attachment send wins parsing race", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const dispatchRelease = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "test-provider", - model: "vision-model", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "test-provider", + model: "vision-model", }); const firstCatalogSnapshot = @@ -1692,16 +1560,10 @@ describe("gateway server chat", () => { }, ], }; - const { chatHandlers } = await import("./server-methods/chat.js"); const callSend = (id: string) => - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id, method: "chat.send", params }, + callDirectChat("chat.send", { + id, params, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ id, ok, payload, error }); }) as RespondFn, @@ -1748,29 +1610,20 @@ describe("gateway server chat", () => { }, FAST_WAIT_OPTS); } finally { dispatchRelease.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.abort cancels chat.send during attachment preparation before ACK", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const firstCatalogSnapshot = createDeferred< Awaited> >(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "test-provider", - model: "vision-model", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "test-provider", + model: "vision-model", }); const sendResponses: Array<{ @@ -1809,16 +1662,11 @@ describe("gateway server chat", () => { scopes: ["operator.write"], }, } as never; - const { chatHandlers } = await import("./server-methods/chat.js"); const first = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "first", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "first", params, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ id: "first", ok, payload, error }); }) as RespondFn, @@ -1830,19 +1678,10 @@ describe("gateway server chat", () => { expect(context.chatAbortControllers.has("idem-attachment-abort")).toBe(true); }, FAST_WAIT_OPTS); - await expectDefined( - chatHandlers["chat.abort"], - 'chatHandlers["chat.abort"] test invariant', - )({ - req: { - type: "req", - id: "abort", - method: "chat.abort", - params: { sessionKey: "main", runId: "idem-attachment-abort" }, - }, + await callDirectChat("chat.abort", { + id: "abort", params: { sessionKey: "main", runId: "idem-attachment-abort" }, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { abortResponses.push({ ok, payload, error }); }) as RespondFn, @@ -1858,14 +1697,10 @@ describe("gateway server chat", () => { ]); expect(context.chatAbortControllers.has("idem-attachment-abort")).toBe(false); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "retry", method: "chat.send", params }, + await callDirectChat("chat.send", { + id: "retry", params, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ id: "retry", ok, payload, error }); }) as RespondFn, @@ -1919,28 +1754,18 @@ describe("gateway server chat", () => { expect(context.removeChatRun).toHaveBeenCalledTimes(1); } finally { firstCatalogSnapshot.resolve(createChatVisionModelCatalogSnapshot()); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.abort cancels chat.send while lifecycle admission waits", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const { storePath } = openDirectChatSession(); const releaseMutation = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const mutationStarted = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ - scope: testState.sessionStorePath, + scope: storePath, identities: ["sess-main"], run: async () => { mutationStarted.resolve(); @@ -1972,16 +1797,11 @@ describe("gateway server chat", () => { scopes: ["operator.write"], }, } as never; - const { chatHandlers } = await import("./server-methods/chat.js"); const send = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "send", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "send", params, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ ok, payload, error }); }) as RespondFn, @@ -1995,14 +1815,10 @@ describe("gateway server chat", () => { expect(context.chatAbortControllers.has(runId)).toBe(false); const retryResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "retry", method: "chat.send", params }, + await callDirectChat("chat.send", { + id: "retry", params, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { retryResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2017,19 +1833,10 @@ describe("gateway server chat", () => { ]); expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); - await expectDefined( - chatHandlers["chat.abort"], - 'chatHandlers["chat.abort"] test invariant', - )({ - req: { - type: "req", - id: "abort", - method: "chat.abort", - params: { sessionKey: "main", runId }, - }, + await callDirectChat("chat.abort", { + id: "abort", params: { sessionKey: "main", runId }, client, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { abortResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2065,28 +1872,18 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { releaseMutation.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send rejects stale lifecycle work after admission waits", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const { storePath } = openDirectChatSession(); const releaseMutation = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const mutationStarted = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ - scope: testState.sessionStorePath, + scope: storePath, identities: ["sess-main"], run: async () => { mutationStarted.resolve(); @@ -2103,16 +1900,10 @@ describe("gateway server chat", () => { message: "do not resume after restart", idempotencyKey: runId, }; - const { chatHandlers } = await import("./server-methods/chat.js"); const send = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "send", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "send", params, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2146,26 +1937,16 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { releaseMutation.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send does not recreate a session deleted while admission waits", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const performDeletion = createDeferred(); let mutation: Promise | undefined; try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const [{ deleteSessionEntryLifecycle }, { loadSessionEntry: loadGatewaySessionEntry }] = await Promise.all([ import("../config/sessions/session-accessor.js"), @@ -2220,16 +2001,10 @@ describe("gateway server chat", () => { message: "do not recreate the deleted session", idempotencyKey: runId, }; - const { chatHandlers } = await import("./server-methods/chat.js"); const send = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "send", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "send", params, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error, meta) => { sendResponses.push({ ok, payload, error, meta }); }) as RespondFn, @@ -2259,28 +2034,20 @@ describe("gateway server chat", () => { } finally { performDeletion.resolve(); await Promise.allSettled(mutation ? [mutation] : []); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send does not enter a replacement session after reset while admission waits", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const { storePath } = openDirectChatSession(); const releaseMutation = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-before-reset", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + sessionId: "sess-before-reset", }); const mutationStarted = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ - scope: testState.sessionStorePath, + scope: storePath, identities: ["agent:main:main", "sess-before-reset"], run: async () => { mutationStarted.resolve(); @@ -2297,16 +2064,10 @@ describe("gateway server chat", () => { message: "do not enter the replacement session", idempotencyKey: runId, }; - const { chatHandlers } = await import("./server-methods/chat.js"); const send = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "send", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "send", params, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2317,13 +2078,8 @@ describe("gateway server chat", () => { expect(context.dedupe.has(pendingChatSendDedupeKey(runId))).toBe(true); }, FAST_WAIT_OPTS); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-after-reset", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + sessionId: "sess-after-reset", }); releaseMutation.resolve(); await mutation; @@ -2338,29 +2094,19 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { releaseMutation.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send does not consume a replacement pending reservation", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + const { storePath } = openDirectChatSession(); const releaseMutation = createDeferred(); const releaseTerminalMutation = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const mutationStarted = createDeferred(); const mutation = runExclusiveSessionLifecycleMutation({ - scope: testState.sessionStorePath, + scope: storePath, identities: ["sess-main"], run: async () => { mutationStarted.resolve(); @@ -2378,16 +2124,10 @@ describe("gateway server chat", () => { message: "only the replacement may run", idempotencyKey: runId, }; - const { chatHandlers } = await import("./server-methods/chat.js"); const send = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "send", method: "chat.send", params }, + callDirectChat("chat.send", { + id: "send", params, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { sendResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2427,7 +2167,7 @@ describe("gateway server chat", () => { const terminalMutationStarted = createDeferred(); const terminalMutation = runExclusiveSessionLifecycleMutation({ - scope: testState.sessionStorePath, + scope: storePath, identities: ["sess-main"], run: async () => { terminalMutationStarted.resolve(); @@ -2444,14 +2184,9 @@ describe("gateway server chat", () => { }; const terminalResponses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; const terminalSend = Promise.resolve( - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { type: "req", id: "terminal-send", method: "chat.send", params: terminalParams }, + callDirectChat("chat.send", { + id: "terminal-send", params: terminalParams, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { terminalResponses.push({ ok, payload, error }); }) as RespondFn, @@ -2481,18 +2216,15 @@ describe("gateway server chat", () => { } finally { releaseMutation.resolve(); releaseTerminalMutation.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test.each(configuredImageModelCases)( "chat.send preserves text-only image uploads as MediaPaths even with configured imageModel: $id", async ({ id, imageModel }) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); testState.agentConfig = { model: { primary: "anthropic/claude-opus-4-6", @@ -2503,15 +2235,9 @@ describe("gateway server chat", () => { "anthropic/claude-opus-4-6": {}, }, }; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - modelProvider: "anthropic", - model: "claude-opus-4-6", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + modelProvider: "anthropic", + model: "claude-opus-4-6", }); const context = createDirectChatContext({ @@ -2561,30 +2287,9 @@ describe("gateway server chat", () => { }; }); - const { chatHandlers } = await import("./server-methods/chat.js"); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: `configured-image-model-${id}`, - method: "chat.send", - params: { - sessionKey: "main", - message: "see image", - idempotencyKey: `idem-configured-image-model-${id}`, - attachments: [ - { - type: "image", - mimeType: "image/png", - fileName: "dot.png", - content: pngB64, - }, - ], - }, - }, + await callDirectChat("chat.send", { + id: `configured-image-model-${id}`, params: { sessionKey: "main", message: "see image", @@ -2598,8 +2303,6 @@ describe("gateway server chat", () => { }, ], }, - client: null, - isWebchatConnect: () => false, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); }) as RespondFn, @@ -2627,19 +2330,11 @@ describe("gateway server chat", () => { ); test("chat.send durably admits a restart-safe Control UI turn before ACK", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const dispatchRelease = createDeferred(); try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); dispatchInboundMessageMock.mockImplementationOnce(async () => dispatchRelease.promise); @@ -2702,24 +2397,15 @@ describe("gateway server chat", () => { ); } finally { dispatchRelease.resolve(undefined); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send persists optional connection identity per turn", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); const send = async (params: { @@ -2810,32 +2496,22 @@ describe("gateway server chat", () => { ]), ); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send preserves a terminal source claim before admitting the next turn", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const dispatchRelease = createDeferred(); const priorRunId = "idem-prior-terminal-claim"; const nextRunId = "idem-after-terminal-claim"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - abortedLastRun: false, - restartRecoveryDeliveryRunId: priorRunId, - restartRecoveryDeliverySourceRunId: priorRunId, - restartRecoveryTerminalRunIds: ["idem-older-terminal-claim"], - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", + abortedLastRun: false, + restartRecoveryDeliveryRunId: priorRunId, + restartRecoveryDeliverySourceRunId: priorRunId, + restartRecoveryTerminalRunIds: ["idem-older-terminal-claim"], }); const context = createDirectChatContext(); dispatchInboundMessageMock.mockImplementationOnce(async () => dispatchRelease.promise); @@ -2896,26 +2572,17 @@ describe("gateway server chat", () => { ); } finally { dispatchRelease.resolve(undefined); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send runs an admission-owned callback for only one concurrent retry", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const dispatchRelease = createDeferred(); const runId = "idem-concurrent-admission-owner"; try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); dispatchInboundMessageMock.mockImplementationOnce(async () => dispatchRelease.promise); @@ -2952,27 +2619,18 @@ describe("gateway server chat", () => { ); } finally { dispatchRelease.resolve(undefined); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.abort still sees a replacement while its admission callback is running", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const callbackEntered = createDeferred(); const releaseCallback = createDeferred(); const runId = "idem-visible-during-admission-callback"; try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext({ chatQueuedTurns: new Map() }); const sendResponses: Array<{ ok: boolean; payload?: unknown }> = []; @@ -2991,27 +2649,10 @@ describe("gateway server chat", () => { expect(context.chatAbortControllers.get(runId)?.controlUiVisible).not.toBe(false); const abortResponses: Array<{ ok: boolean; payload?: unknown }> = []; - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.abort"], - 'chatHandlers["chat.abort"] test invariant', - )({ - req: { - type: "req", - id: "abort-visible-replacement", - method: "chat.abort", - params: { sessionKey: "main" }, - }, + await callDirectChat("chat.abort", { + id: "abort-visible-replacement", params: { sessionKey: "main" }, - client: { - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write", "operator.admin"], - }, - } as never, + client: createControlUiClient(), isWebchatConnect: () => true, respond: ((ok, payload) => abortResponses.push({ ok, payload })) as RespondFn, context, @@ -3040,9 +2681,7 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { releaseCallback.resolve(undefined); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); @@ -3050,22 +2689,14 @@ describe("gateway server chat", () => { { caseName: "tombstones an explicit abort", retryable: false, stopReason: "rpc" }, { caseName: "retains a restart interruption", retryable: true, stopReason: "restart" }, ])("chat.send $caseName during SQLite admission", async ({ retryable, stopReason }) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const runId = `idem-restart-safe-abort-${stopReason}`; const lockEntered = createDeferred(); const releaseLock = createDeferred(); let lockPromise: Promise | undefined; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const scope = { agentId: "main", @@ -3205,18 +2836,14 @@ describe("gateway server chat", () => { } finally { releaseLock.resolve(undefined); await lockPromise?.catch(() => undefined); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send keeps a durable Control UI retry pending when recovery remains abandoned", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const idempotencyKey = "idem-restart-safe-duplicate"; try { - testState.sessionStorePath = storePath; await writeSessionStore({ entries: {} }); await replaceSessionEntry( { sessionKey: "main", storePath }, @@ -3283,29 +2910,19 @@ describe("gateway server chat", () => { }); } finally { restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockClear(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send retires a durable retry after recovery re-dispatch succeeds", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const idempotencyKey = "idem-restart-safe-recovered-retry"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "running", - abortedLastRun: true, - restartRecoveryDeliveryRunId: "recovery-run", - restartRecoveryDeliverySourceRunId: idempotencyKey, - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "running", + abortedLastRun: true, + restartRecoveryDeliveryRunId: "recovery-run", + restartRecoveryDeliverySourceRunId: idempotencyKey, }); restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockImplementationOnce( async ({ sessionKey, storePath: recoveryStorePath }) => { @@ -3341,28 +2958,18 @@ describe("gateway server chat", () => { }); } finally { restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockClear(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send suppresses a durable retry settled while lifecycle admission waits", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const idempotencyKey = "idem-recovery-settled-during-admission"; const releaseMutation = createDeferred(); let mutation: Promise | undefined; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const mutationStarted = createDeferred(); mutation = runExclusiveSessionLifecycleMutation({ @@ -3407,30 +3014,20 @@ describe("gateway server chat", () => { } finally { releaseMutation.resolve(); await Promise.allSettled(mutation ? [mutation] : []); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send does not re-dispatch an archived durable recovery claim", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + openDirectChatSession(); const idempotencyKey = "idem-restart-safe-archived-retry"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - archivedAt: Date.now(), - status: "running", - abortedLastRun: true, - restartRecoveryDeliveryRunId: "recovery-run", - restartRecoveryDeliverySourceRunId: idempotencyKey, - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + archivedAt: Date.now(), + status: "running", + abortedLastRun: true, + restartRecoveryDeliveryRunId: "recovery-run", + restartRecoveryDeliverySourceRunId: idempotencyKey, }); const context = createDirectChatContext(); const responses: Array<{ error?: unknown; ok: boolean; payload?: unknown }> = []; @@ -3453,29 +3050,19 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockClear(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send stops automatic retry when durable recovery ownership changes", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + openDirectChatSession(); const idempotencyKey = "idem-restart-safe-replaced-retry"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "running", - abortedLastRun: true, - restartRecoveryDeliveryRunId: "recovery-run", - restartRecoveryDeliverySourceRunId: idempotencyKey, - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "running", + abortedLastRun: true, + restartRecoveryDeliveryRunId: "recovery-run", + restartRecoveryDeliverySourceRunId: idempotencyKey, }); restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockImplementationOnce( async ({ sessionKey, storePath: recoveryStorePath }) => { @@ -3508,9 +3095,7 @@ describe("gateway server chat", () => { expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { restartRecoveryMocks.retryRestartAbortedMainSessionRecovery.mockClear(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); @@ -3518,21 +3103,13 @@ describe("gateway server chat", () => { { caseName: "settled recovery", status: "done" as const, abortedLastRun: false }, { caseName: "unresumable recovery", status: "failed" as const, abortedLastRun: true }, ])("chat.send suppresses a Control UI retry after $caseName", async (terminal) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + openDirectChatSession(); const idempotencyKey = `idem-${terminal.status}-recovery`; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: terminal.status, - abortedLastRun: terminal.abortedLastRun, - restartRecoveryTerminalRunIds: [idempotencyKey], - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: terminal.status, + abortedLastRun: terminal.abortedLastRun, + restartRecoveryTerminalRunIds: [idempotencyKey], }); const context = createDirectChatContext(); const responses: Array<{ ok: boolean; payload?: unknown }> = []; @@ -3552,26 +3129,16 @@ describe("gateway server chat", () => { ]); expect(dispatchInboundMessageMock).not.toHaveBeenCalled(); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send releases an unadopted durable claim after dispatch rejection", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const runId = "idem-restart-safe-dispatch-error"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); const responses: Array<{ ok: boolean; payload?: unknown }> = []; @@ -3649,26 +3216,16 @@ describe("gateway server chat", () => { )?.replyOptions?.suppressNextUserMessagePersistence, ).toBe(true); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send releases a durable claim after synchronous post-admission failure", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const runId = "idem-restart-safe-setup-error"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); const responses: Array<{ ok: boolean; payload?: unknown }> = []; @@ -3697,26 +3254,16 @@ describe("gateway server chat", () => { expect(failed?.restartRecoveryDeliveryRunId).toBe(runId); expect(failed?.restartRecoveryDeliverySourceRunId).toBe(runId); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send leaves a post-admission routing rejection retryable", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); const runId = "idem-restart-safe-routing-change"; try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", }); const context = createDirectChatContext(); const initialRuntimeConfig = getRuntimeConfig(); @@ -3779,9 +3326,7 @@ describe("gateway server chat", () => { )?.replyOptions?.suppressNextUserMessagePersistence, ).toBe(true); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); @@ -3807,19 +3352,12 @@ describe("gateway server chat", () => { entry: { abortedLastRun: true }, }, ])("chat.send leaves $caseName outside restart-safe admission", async ({ entry, runId }) => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); - const storePath = path.join(sessionDir, "sessions.json"); + const { storePath } = openDirectChatSession(); try { - testState.sessionStorePath = storePath; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - status: "done", - ...entry, - updatedAt: Date.now(), - }, - }, + await writeStoredMainSession({ + status: "done", + ...entry, + updatedAt: Date.now(), }); const context = createDirectChatContext(); dispatchInboundMessageMock.mockResolvedValueOnce(undefined); @@ -3846,25 +3384,15 @@ describe("gateway server chat", () => { FAST_WAIT_OPTS, ); } finally { - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send keeps matching WebChat text sends distinct by idempotency key", async () => { - const sessionDir = autoCleanupTempDirs.make("openclaw-gw-"); + openDirectChatSession(); const dispatchRelease = createDeferred(); try { - testState.sessionStorePath = path.join(sessionDir, "sessions.json"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = []; const context = createDirectChatContext({ @@ -3873,51 +3401,30 @@ describe("gateway server chat", () => { }); dispatchInboundMessageMock.mockImplementation(async () => dispatchRelease.promise); - const { chatHandlers } = await import("./server-methods/chat.js"); const callSend = ( id: string, idempotencyKey: string, systemProvenanceReceipt?: string, thinking = "low", - ) => - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id, - method: "chat.send", - params: { - sessionKey: "main", - message: "?", - idempotencyKey, - thinking, - ...(systemProvenanceReceipt ? { systemProvenanceReceipt } : {}), - }, - }, - params: { - sessionKey: "main", - message: "?", - idempotencyKey, - thinking, - ...(systemProvenanceReceipt ? { systemProvenanceReceipt } : {}), - }, - client: { - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write", "operator.admin"], - }, - } as never, + ) => { + const params = { + sessionKey: "main", + message: "?", + idempotencyKey, + thinking, + ...(systemProvenanceReceipt ? { systemProvenanceReceipt } : {}), + }; + return callDirectChat("chat.send", { + id, + params, + client: createControlUiClient(), isWebchatConnect: () => true, respond: ((ok, payload, error) => { responses.push({ id, ok, payload, error }); }) as RespondFn, context, }); + }; const first = Promise.resolve(callSend("first", "idem-active-a")); await waitForFast( @@ -4060,22 +3567,13 @@ describe("gateway server chat", () => { }, FAST_WAIT_OPTS); } finally { dispatchRelease.resolve(); - dispatchInboundMessageMock.mockReset(); - testState.sessionStorePath = undefined; - clearConfigCache(); + resetDirectChatSession(); } }); test("chat.send can suppress command interpretation for slash-prefixed system turns", async () => { await withDirectChatSession(async () => { - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = []; const context = createDirectChatContext({ @@ -4084,37 +3582,15 @@ describe("gateway server chat", () => { }); dispatchInboundMessageMock.mockResolvedValue({}); - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "suppressed-command", - method: "chat.send", - params: { - sessionKey: "main", - message: "/reset examples", - suppressCommandInterpretation: true, - idempotencyKey: "idem-suppressed-command", - }, - }, + await callDirectChat("chat.send", { + id: "suppressed-command", params: { sessionKey: "main", message: "/reset examples", suppressCommandInterpretation: true, idempotencyKey: "idem-suppressed-command", }, - client: { - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write", "operator.admin"], - }, - } as never, + client: createControlUiClient(), isWebchatConnect: () => true, respond: ((ok, payload, error) => { responses.push({ id: "suppressed-command", ok, payload, error }); @@ -4158,14 +3634,7 @@ describe("gateway server chat", () => { test("chat.send starts the next WebChat turn after the prior internal run finishes", async () => { await withDirectChatSession(async () => { - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const responses: Array<{ id: string; ok: boolean; payload?: unknown; error?: unknown }> = []; const context = createDirectChatContext({ @@ -4174,36 +3643,15 @@ describe("gateway server chat", () => { }); dispatchInboundMessageMock.mockResolvedValue(undefined); - const { chatHandlers } = await import("./server-methods/chat.js"); const callSend = (id: string, message: string, idempotencyKey: string) => - expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id, - method: "chat.send", - params: { - sessionKey: "main", - message, - idempotencyKey, - }, - }, + callDirectChat("chat.send", { + id, params: { sessionKey: "main", message, idempotencyKey, }, - client: { - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write"], - }, - } as never, + client: createControlUiClient(["operator.write"]), isWebchatConnect: () => true, respond: ((ok, payload, error) => { responses.push({ id, ok, payload, error }); @@ -4267,14 +3715,7 @@ describe("gateway server chat", () => { test("chat.send terminalizes the client run when a followup is queued", async () => { await withDirectChatSession(async () => { - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const broadcast = vi.fn((_event: string, _payload: unknown) => undefined); const context = createDirectChatContext({ @@ -4293,21 +3734,8 @@ describe("gateway server chat", () => { return {}; }); - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "queued-followup", - method: "chat.send", - params: { - sessionKey: "main", - message: "queued prompt", - idempotencyKey: "idem-queued-followup", - }, - }, + await callDirectChat("chat.send", { + id: "queued-followup", params: { sessionKey: "main", message: "queued prompt", @@ -4358,20 +3786,8 @@ describe("gateway server chat", () => { context.dedupe.delete("chat:idem-queued-followup"); const replayRespond = vi.fn() as RespondFn; - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "queued-followup-replay", - method: "chat.send", - params: { - sessionKey: "main", - message: "queued prompt", - idempotencyKey: "idem-queued-followup", - }, - }, + await callDirectChat("chat.send", { + id: "queued-followup-replay", params: { sessionKey: "main", message: "queued prompt", @@ -4415,20 +3831,8 @@ describe("gateway server chat", () => { failedDispatchLifecycle?.onDeferred?.(); throw new Error("post-enqueue bookkeeping failed"); }); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "queued-followup-post-error", - method: "chat.send", - params: { - sessionKey: "main", - message: "accepted before dispatch error", - idempotencyKey: "idem-queued-followup-post-error", - }, - }, + await callDirectChat("chat.send", { + id: "queued-followup-post-error", params: { sessionKey: "main", message: "accepted before dispatch error", @@ -4472,14 +3876,7 @@ describe("gateway server chat", () => { test("chat.send emits operator-only post-ACK server timing milestones", async () => { await withDirectChatSession(async () => { - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; const broadcastToConnIds = vi.fn(); @@ -4499,36 +3896,14 @@ describe("gateway server chat", () => { return {}; }); - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "operator-timing", - method: "chat.send", - params: { - sessionKey: "main", - message: "measure", - idempotencyKey: "idem-server-timing", - }, - }, + await callDirectChat("chat.send", { + id: "operator-timing", params: { sessionKey: "main", message: "measure", idempotencyKey: "idem-server-timing", }, - client: { - connId: "conn-control-ui", - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write"], - }, - } as never, + client: createControlUiClient(["operator.write"], { connId: "conn-control-ui" }), isWebchatConnect: () => true, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); @@ -4598,14 +3973,7 @@ describe("gateway server chat", () => { test("chat.send emits first-assistant timing for direct final replies", async () => { await withDirectChatSession(async () => { - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); + await writeStoredMainSession({}); const responses: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = []; const broadcast = vi.fn(); @@ -4632,36 +4000,14 @@ describe("gateway server chat", () => { return {}; }); - const { chatHandlers } = await import("./server-methods/chat.js"); - await expectDefined( - chatHandlers["chat.send"], - 'chatHandlers["chat.send"] test invariant', - )({ - req: { - type: "req", - id: "operator-direct-timing", - method: "chat.send", - params: { - sessionKey: "main", - message: "measure direct", - idempotencyKey: "idem-direct-server-timing", - }, - }, + await callDirectChat("chat.send", { + id: "operator-direct-timing", params: { sessionKey: "main", message: "measure direct", idempotencyKey: "idem-direct-server-timing", }, - client: { - connId: "conn-control-ui", - connect: { - client: { - id: GATEWAY_CLIENT_NAMES.CONTROL_UI, - mode: GATEWAY_CLIENT_MODES.WEBCHAT, - }, - scopes: ["operator.write"], - }, - } as never, + client: createControlUiClient(["operator.write"], { connId: "conn-control-ui" }), isWebchatConnect: () => true, respond: ((ok, payload, error) => { responses.push({ ok, payload, error }); @@ -4786,19 +4132,15 @@ describe("gateway server chat", () => { ); setTestEnvValue("HOME", homeDir); try { - await writeSessionStore({ - entries: { - main: { - sessionId, - sessionFile: testSessionFilePath(sessionDir, sessionId), - updatedAt: futureFixtureUpdatedAt(), - modelProvider: "claude-cli", - model: "claude-sonnet-4-6", - cliSessionBindings: { - "claude-cli": { - sessionId: cliSessionId, - }, - }, + await writeStoredMainSession({ + sessionId, + sessionFile: testSessionFilePath(sessionDir, sessionId), + updatedAt: futureFixtureUpdatedAt(), + modelProvider: "claude-cli", + model: "claude-sonnet-4-6", + cliSessionBindings: { + "claude-cli": { + sessionId: cliSessionId, }, }, }); @@ -4866,20 +4208,15 @@ describe("gateway server chat", () => { ); setTestEnvValue("HOME", homeDir); try { - await writeSessionStore({ - entries: { - main: { - sessionId, - sessionFile: testSessionFilePath(sessionDir, sessionId), - updatedAt: futureFixtureUpdatedAt(), - modelProvider: "claude-cli", - model: "claude-sonnet-4-6", - cliSessionBindings: { "claude-cli": { sessionId: cliSessionId } }, - }, - }, + await writeStoredMainSession({ + sessionId, + sessionFile: testSessionFilePath(sessionDir, sessionId), + updatedAt: futureFixtureUpdatedAt(), + modelProvider: "claude-cli", + model: "claude-sonnet-4-6", + cliSessionBindings: { "claude-cli": { sessionId: cliSessionId } }, }); await writeMainSessionTranscript( - sessionDir, Array.from({ length: 70 }, (_, index) => JSON.stringify({ message: { @@ -4932,22 +4269,17 @@ describe("gateway server chat", () => { const homeEnvSnapshot = captureEnv(["HOME"]); setTestEnvValue("HOME", path.join(sessionDir, "empty-home")); try { - await writeSessionStore({ - entries: { - main: { - sessionId, - sessionFile: testSessionFilePath(sessionDir, sessionId), - updatedAt: futureFixtureUpdatedAt(), - modelProvider: "claude-cli", - model: "claude-sonnet-4-6", - cliSessionBindings: { - "claude-cli": { sessionId: "missing-cli-session" }, - }, - }, + await writeStoredMainSession({ + sessionId, + sessionFile: testSessionFilePath(sessionDir, sessionId), + updatedAt: futureFixtureUpdatedAt(), + modelProvider: "claude-cli", + model: "claude-sonnet-4-6", + cliSessionBindings: { + "claude-cli": { sessionId: "missing-cli-session" }, }, }); await writeMainSessionTranscript( - sessionDir, Array.from({ length: 5 }, (_, index) => JSON.stringify({ message: { @@ -5026,18 +4358,14 @@ describe("gateway server chat", () => { ); setTestEnvValue("HOME", homeDir); try { - await writeSessionStore({ - entries: { - main: { - sessionId, - sessionFile: testSessionFilePath(sessionDir, sessionId), - updatedAt: futureFixtureUpdatedAt(), - modelProvider: "claude-cli", - model: "claude-sonnet-4-6", - cliSessionBindings: { - "claude-cli": { sessionId: cliSessionId }, - }, - }, + await writeStoredMainSession({ + sessionId, + sessionFile: testSessionFilePath(sessionDir, sessionId), + updatedAt: futureFixtureUpdatedAt(), + modelProvider: "claude-cli", + model: "claude-sonnet-4-6", + cliSessionBindings: { + "claude-cli": { sessionId: cliSessionId }, }, }); // The two import copies are the oldest local records; 45 newer @@ -5045,7 +4373,6 @@ describe("gateway server chat", () => { // messages), so the tail merge incorporates the import while the full // read dedupes everything. This layout used to recurse forever. await writeMainSessionTranscript( - sessionDir, [ createTextTranscriptEvent("user", "dup user question", { timestamp: dupBaseMs }), createTextTranscriptEvent("assistant", "dup assistant reply", { @@ -5086,18 +4413,12 @@ describe("gateway server chat", () => { test("chat.history overreads one local message to drop stale announce pairs at the limit boundary", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const sessionStartedAt = Date.parse("2026-05-23T04:02:30.000Z"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - sessionStartedAt, - }, - }, + await writeStoredMainSession({ + sessionStartedAt, }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ type: "session", version: 1, id: "sess-main" }), JSON.stringify({ timestamp: "2026-05-16T16:00:31.000Z", @@ -5140,23 +4461,17 @@ describe("gateway server chat", () => { test("chat.history does not surface an older stale assistant when overreading for pair context", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const sessionStartedAt = Date.parse("2026-05-23T04:02:30.000Z"); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - sessionStartedAt, - }, - }, + await writeStoredMainSession({ + sessionStartedAt, }); const announce = { kind: "inter_session", sourceSessionKey: "agent:main:subagent:child", sourceTool: "subagent_announce", }; - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ type: "session", version: 1, id: "sess-main" }), JSON.stringify({ timestamp: "2026-05-16T16:00:29.000Z", @@ -5210,23 +4525,17 @@ describe("gateway server chat", () => { test("chat.history offset pages overread context before filtering stale announce replies", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const sessionStartedAt = Date.parse("2026-05-23T04:02:30.000Z"); const announce = { kind: "inter_session", sourceSessionKey: "agent:main:subagent:child", sourceTool: "subagent_announce", }; - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - sessionStartedAt, - }, - }, + await writeStoredMainSession({ + sessionStartedAt, }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ timestamp: "2026-05-23T04:03:10.000Z", message: { @@ -5280,16 +4589,9 @@ describe("gateway server chat", () => { test("chat.history offset pages preserve a hidden heartbeat boundary from overread context", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: Date.now(), - }, - }, - }); - await writeMainSessionTranscript(sessionDir, [ + await createSessionDir(); + await writeStoredMainSession({}); + await writeMainSessionTranscript([ JSON.stringify({ message: { role: "user", @@ -5597,10 +4899,10 @@ describe("gateway server chat", () => { test("chat.history hard-caps single oversized nested payloads", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const historyMaxBytes = getMaxChatHistoryMessagesBytes(); const hugeNestedText = "n".repeat(300_000); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ id: "msg-huge", message: { @@ -5630,7 +4932,7 @@ describe("gateway server chat", () => { test("chat.history keeps recent messages within the production byte budget", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const historyMaxBytes = getMaxChatHistoryMessagesBytes(); const baseText = "s".repeat(100_000); const lines = Array.from({ length: 70 }, (_, index) => @@ -5658,7 +4960,7 @@ describe("gateway server chat", () => { }), ); - await writeMainSessionTranscript(sessionDir, lines); + await writeMainSessionTranscript(lines); const messages = await fetchHistoryMessages(ws, { maxChars: 100_000 }); const serialized = JSON.stringify(messages); @@ -5671,9 +4973,9 @@ describe("gateway server chat", () => { test("chat.history advances past an oversized newest record when the tail parses empty", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const historyMaxBytes = getMaxChatHistoryMessagesBytes(); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "reachable older message", { timestamp: Date.now() }), JSON.stringify({ message: { @@ -5710,10 +5012,10 @@ describe("gateway server chat", () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); await writeMainSessionStore(); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ message: { role: "assistant", @@ -5768,9 +5070,9 @@ describe("gateway server chat", () => { test("chat.history preserves canonical parallel tool calls and bounded result diffs", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const fullDiff = `-12 old line\n+12 ${"new line ".repeat(20)}`; - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ message: { role: "assistant", @@ -5841,7 +5143,7 @@ describe("gateway server chat", () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); await writeMainSessionStore(); const lines = [ @@ -5870,7 +5172,7 @@ describe("gateway server chat", () => { }), createTextTranscriptEvent("assistant", " keep padded ", { timestamp: Date.now() + 3 }), ]; - await writeMainSessionTranscript(sessionDir, lines); + await writeMainSessionTranscript(lines); const messages = await fetchHistoryMessages(ws); expect(messages.length).toBe(4); @@ -5892,8 +5194,8 @@ describe("gateway server chat", () => { test("chat.history keeps visible assistant progress text from mixed tool-use transcript messages", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await prepareMainHistoryHarness({ ws, createSessionDir }); + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "fix it", { timestamp: 1 }), JSON.stringify({ message: { @@ -5946,8 +5248,8 @@ describe("gateway server chat", () => { test("chat.history applies RPC maxChars", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await prepareMainHistoryHarness({ ws, createSessionDir }); + await writeMainSessionTranscript([ createTextTranscriptEvent("assistant", "abcdefghij", { timestamp: Date.now() }), ]); @@ -5983,8 +5285,8 @@ describe("gateway server chat", () => { test("chat.message.get returns the full projected message for a truncated history row", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await prepareMainHistoryHarness({ ws, createSessionDir }); + await writeMainSessionTranscript([ createTextTranscriptEvent("assistant", "abcdefghij", { id: "msg-full-assistant" }), ]); @@ -6042,7 +5344,7 @@ describe("gateway server chat", () => { }, }); await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); await writeSessionStore({ agentId: "work", entries: { @@ -6050,7 +5352,6 @@ describe("gateway server chat", () => { }, }); await writeMainSessionTranscript( - sessionDir, [ createTextTranscriptEvent("assistant", "global agent content", { id: "msg-global-agent", @@ -6100,9 +5401,9 @@ describe("gateway server chat", () => { test("chat.message.get returns active SQLite oversized transcript entries", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const oversizedText = "x".repeat(300 * 1024); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("assistant", oversizedText, { id: "msg-oversized-sqlite" }), ]); @@ -6118,7 +5419,7 @@ describe("gateway server chat", () => { test("chat.message.get does not return inactive branch entries", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "question", { id: "msg-root", parentId: null }), createTextTranscriptEvent("assistant", "stale branch", { id: "msg-stale", @@ -6171,14 +5472,14 @@ describe("gateway server chat", () => { test("chat.message.get does not return pre-session announce pairs hidden by history", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await connectOk(ws); - const sessionDir = await createSessionDir(); + await createSessionDir(); const sessionStartedAt = Date.now(); await writeSessionStore({ entries: { main: { sessionId: "sess-main", updatedAt: Date.now(), sessionStartedAt }, }, }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ id: "msg-announce", message: { @@ -6216,8 +5517,8 @@ describe("gateway server chat", () => { test("chat.history still drops assistant NO_REPLY entries before truncation", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await prepareMainHistoryHarness({ ws, createSessionDir }); + await writeMainSessionTranscript([ createTextTranscriptEvent("assistant", "NO_REPLY", { timestamp: Date.now() }), ]); @@ -6228,11 +5529,11 @@ describe("gateway server chat", () => { test("chat.history backfills visible messages when raw tail is mostly silent", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const silentTail = Array.from({ length: 24 }, (_, index) => createTextTranscriptEvent("assistant", "NO_REPLY", { timestamp: Date.now() + index + 2 }), ); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "visible question", { timestamp: Date.now() }), createTextTranscriptEvent("assistant", "visible answer", { timestamp: Date.now() + 1 }), ...silentTail, @@ -6248,7 +5549,7 @@ describe("gateway server chat", () => { test("chat.history returns retryable unavailable while a dirty projection rebuilds", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ message: { role: "user", content: "ready after rebuild" } }), ]); const databaseOptions = { @@ -6276,8 +5577,8 @@ describe("gateway server chat", () => { test("chat.history offset pagination advances from the projected first-page boundary", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await prepareMainHistoryHarness({ ws, createSessionDir }); + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "oldest question", { timestamp: Date.now() }), createTextTranscriptEvent("assistant", "oldest answer", { timestamp: Date.now() + 1 }), createTextTranscriptEvent("user", "visible boundary", { timestamp: Date.now() + 2 }), @@ -6322,9 +5623,8 @@ describe("gateway server chat", () => { test("chat.history first-page metadata pages backward without overlaps or gaps", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); await writeMainSessionTranscript( - sessionDir, Array.from({ length: 7 }, (_, index) => JSON.stringify({ message: { @@ -6439,7 +5739,7 @@ describe("gateway server chat", () => { test("chat.history centers a bounded page around a message id", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ JSON.stringify({ type: "model_change", provider: "mock", modelId: "mock" }), JSON.stringify({ type: "thinking_level_change", thinkingLevel: "off" }), ]); @@ -6497,14 +5797,9 @@ describe("gateway server chat", () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { await prepareMainHistoryHarness({ ws, createSessionDir }); const currentSessionStartedAt = Date.now(); - await writeSessionStore({ - entries: { - main: { - sessionId: "sess-main", - updatedAt: futureFixtureUpdatedAt(), - sessionStartedAt: currentSessionStartedAt, - }, - }, + await writeStoredMainSession({ + updatedAt: futureFixtureUpdatedAt(), + sessionStartedAt: currentSessionStartedAt, }); const storePath = testState.sessionStorePath; if (!storePath) { @@ -6594,10 +5889,9 @@ describe("gateway server chat", () => { test("chat.history offset pagination advances from the final budgeted page", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const messageCount = 70; await writeMainSessionTranscript( - sessionDir, Array.from({ length: messageCount }, (_, index) => JSON.stringify({ message: { @@ -6633,7 +5927,7 @@ describe("gateway server chat", () => { test("chat.history advances past a replay boundary that cannot fit all projected siblings", async () => { await withGatewayChatHarness(async ({ ws, createSessionDir }) => { - const sessionDir = await prepareMainHistoryHarness({ ws, createSessionDir }); + await prepareMainHistoryHarness({ ws, createSessionDir }); const projectedSiblingCount = 70; const captured: DiagnosticPayloadLargeEvent[] = []; const unsubscribe = onDiagnosticEvent((event) => { @@ -6642,7 +5936,7 @@ describe("gateway server chat", () => { } }); try { - await writeMainSessionTranscript(sessionDir, [ + await writeMainSessionTranscript([ createTextTranscriptEvent("user", "reachable older message", { timestamp: Date.now() }), JSON.stringify({ message: {