diff --git a/src/gateway/server.sessions.compaction.test.ts b/src/gateway/server.sessions.compaction.test.ts index 6852170f523d..6ec824c6a6e6 100644 --- a/src/gateway/server.sessions.compaction.test.ts +++ b/src/gateway/server.sessions.compaction.test.ts @@ -2,6 +2,7 @@ import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { expect, test, vi } from "vitest"; +import type { SessionCompactionCheckpoint } from "../config/sessions.js"; import { withEnvAsync } from "../test-utils/env.js"; import { embeddedRunMock, @@ -21,7 +22,68 @@ import { directSessionReq, } from "./test/server-sessions.test-helpers.js"; -const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness(); +const { createSessionStoreDir, createSelectedGlobalSessionStore, openClient } = + setupGatewaySessionsTestHarness(); + +type CheckpointFixture = Awaited>; + +function compactionCheckpointEntry( + fixture: CheckpointFixture, + options: { + checkpointId: string; + sessionKey: string; + createdAt: number; + reason: SessionCompactionCheckpoint["reason"]; + summary: string; + tokensBefore?: number; + tokensAfter?: number; + }, +) { + return { + checkpointId: options.checkpointId, + sessionKey: options.sessionKey, + sessionId: fixture.sessionId, + createdAt: options.createdAt, + reason: options.reason, + summary: options.summary, + ...(options.tokensBefore === undefined ? {} : { tokensBefore: options.tokensBefore }), + ...(options.tokensAfter === undefined ? {} : { tokensAfter: options.tokensAfter }), + firstKeptEntryId: fixture.preCompactionLeafId, + preCompaction: { + sessionId: fixture.sessionId, + leafId: fixture.preCompactionLeafId, + }, + postCompaction: { + sessionId: fixture.sessionId, + sessionFile: fixture.sessionFile, + leafId: fixture.postCompactionLeafId, + entryId: fixture.postCompactionLeafId, + }, + }; +} + +function isCompactOperationEvent(message: unknown, phase: "start" | "end") { + const candidate = message as { + event?: unknown; + payload?: { operation?: unknown; phase?: unknown }; + type?: unknown; + }; + return ( + candidate.type === "event" && + candidate.event === "session.operation" && + candidate.payload?.operation === "compact" && + candidate.payload?.phase === phase + ); +} + +function expectMainCompactionResult( + compacted: { ok?: boolean; payload?: { compacted?: boolean; key?: string } | null }, + expectedCompacted: boolean, +) { + expect(compacted.ok).toBe(true); + expect(compacted.payload?.key).toBe("agent:main:main"); + expect(compacted.payload?.compacted).toBe(expectedCompacted); +} test("sessions.compaction.* lists checkpoints and branches or restores from compacted transcripts", async () => { const { dir, storePath } = await createSessionStoreDir(); @@ -29,34 +91,21 @@ test("sessions.compaction.* lists checkpoints and branches or restores from comp expect((await fs.readdir(dir)).some((file) => file.includes(".checkpoint."))).toBe(false); const checkpointEntryCount = fixture.session.getEntries().length; const checkpointCreatedAt = Date.now(); + const checkpointEntry = compactionCheckpointEntry(fixture, { + checkpointId: "checkpoint-1", + sessionKey: "agent:main:main", + createdAt: checkpointCreatedAt, + reason: "manual", + summary: "checkpoint summary", + tokensBefore: 123, + tokensAfter: 45, + }); const { SessionManager } = await getSessionManagerModule(); await writeSessionStore({ entries: { main: sessionStoreEntry(fixture.sessionId, { sessionFile: fixture.sessionFile, - compactionCheckpoints: [ - { - checkpointId: "checkpoint-1", - sessionKey: "agent:main:main", - sessionId: fixture.sessionId, - createdAt: checkpointCreatedAt, - reason: "manual", - tokensBefore: 123, - tokensAfter: 45, - summary: "checkpoint summary", - firstKeptEntryId: fixture.preCompactionLeafId, - preCompaction: { - sessionId: fixture.sessionId, - leafId: fixture.preCompactionLeafId, - }, - postCompaction: { - sessionId: fixture.sessionId, - sessionFile: fixture.sessionFile, - leafId: fixture.postCompactionLeafId, - entryId: fixture.postCompactionLeafId, - }, - }, - ], + compactionCheckpoints: [checkpointEntry], }), }, }); @@ -101,27 +150,7 @@ test("sessions.compaction.* lists checkpoints and branches or restores from comp expect(listedCheckpoints.ok).toBe(true); expect(listedCheckpoints.payload?.key).toBe("agent:main:main"); expect(listedCheckpoints.payload?.checkpoints).toHaveLength(1); - expect(listedCheckpoints.payload?.checkpoints[0]).toEqual({ - checkpointId: "checkpoint-1", - sessionKey: "agent:main:main", - sessionId: fixture.sessionId, - createdAt: checkpointCreatedAt, - reason: "manual", - summary: "checkpoint summary", - tokensBefore: 123, - tokensAfter: 45, - firstKeptEntryId: fixture.preCompactionLeafId, - preCompaction: { - sessionId: fixture.sessionId, - leafId: fixture.preCompactionLeafId, - }, - postCompaction: { - sessionId: fixture.sessionId, - sessionFile: fixture.sessionFile, - leafId: fixture.postCompactionLeafId, - entryId: fixture.postCompactionLeafId, - }, - }); + expect(listedCheckpoints.payload?.checkpoints[0]).toEqual(checkpointEntry); const checkpoint = await rpcReq<{ ok: true; @@ -277,13 +306,7 @@ test("sessions.compaction.* lists checkpoints and branches or restores from comp }); test("sessions.compaction.* scopes selected global checkpoints to the requested agent", async () => { - const { dir } = await createSessionStoreDir(); - const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); - testState.sessionStorePath = storeTemplate; - testState.sessionConfig = { scope: "global" }; - testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; - const mainStorePath = storeTemplate.replace("{agentId}", "main"); - const workStorePath = storeTemplate.replace("{agentId}", "work"); + const { mainStorePath, workStorePath } = await createSelectedGlobalSessionStore(); const workDir = path.dirname(workStorePath); await fs.mkdir(path.dirname(mainStorePath), { recursive: true }); await fs.mkdir(workDir, { recursive: true }); @@ -291,6 +314,13 @@ test("sessions.compaction.* scopes selected global checkpoints to the requested await fs.writeFile(mainSessionFile, `${JSON.stringify({ role: "user", content: "main" })}\n`); const fixture = await createCheckpointFixture(workDir, { legacyPreCompactionSnapshot: false }); const checkpointCreatedAt = Date.now(); + const checkpointEntry = compactionCheckpointEntry(fixture, { + checkpointId: "checkpoint-work", + sessionKey: "global", + createdAt: checkpointCreatedAt, + reason: "manual", + summary: "work checkpoint", + }); await fs.writeFile( mainStorePath, JSON.stringify( @@ -305,27 +335,7 @@ test("sessions.compaction.* scopes selected global checkpoints to the requested { global: sessionStoreEntry(fixture.sessionId, { sessionFile: fixture.sessionFile, - compactionCheckpoints: [ - { - checkpointId: "checkpoint-work", - sessionKey: "global", - sessionId: fixture.sessionId, - createdAt: checkpointCreatedAt, - reason: "manual", - summary: "work checkpoint", - firstKeptEntryId: fixture.preCompactionLeafId, - preCompaction: { - sessionId: fixture.sessionId, - leafId: fixture.preCompactionLeafId, - }, - postCompaction: { - sessionId: fixture.sessionId, - sessionFile: fixture.sessionFile, - leafId: fixture.postCompactionLeafId, - entryId: fixture.postCompactionLeafId, - }, - }, - ], + compactionCheckpoints: [checkpointEntry], }), }, null, @@ -410,22 +420,8 @@ test("sessions.compact without maxLines runs embedded manual compaction for chec const { ws } = await openClient(); await rpcReq(ws, "sessions.subscribe", {}); - const startEventPromise = onceMessage( - ws, - (message) => - message.type === "event" && - message.event === "session.operation" && - (message.payload as { operation?: unknown; phase?: unknown })?.operation === "compact" && - (message.payload as { operation?: unknown; phase?: unknown })?.phase === "start", - ); - const endEventPromise = onceMessage( - ws, - (message) => - message.type === "event" && - message.event === "session.operation" && - (message.payload as { operation?: unknown; phase?: unknown })?.operation === "compact" && - (message.payload as { operation?: unknown; phase?: unknown })?.phase === "end", - ); + const startEventPromise = onceMessage(ws, (message) => isCompactOperationEvent(message, "start")); + const endEventPromise = onceMessage(ws, (message) => isCompactOperationEvent(message, "end")); const compacted = await rpcReq<{ ok: true; key: string; @@ -435,9 +431,7 @@ test("sessions.compact without maxLines runs embedded manual compaction for chec key: "main", }); - expect(compacted.ok).toBe(true); - expect(compacted.payload?.key).toBe("agent:main:main"); - expect(compacted.payload?.compacted).toBe(true); + expectMainCompactionResult(compacted, true); const startEvent = await startEventPromise; const endEvent = await endEventPromise; const startPayload = startEvent.payload as { @@ -568,14 +562,7 @@ test("sessions.compact treats Codex native compaction start as pending, not comp const { ws } = await openClient(); await rpcReq(ws, "sessions.subscribe", {}); - const endEventPromise = onceMessage( - ws, - (message) => - message.type === "event" && - message.event === "session.operation" && - (message.payload as { operation?: unknown; phase?: unknown })?.operation === "compact" && - (message.payload as { operation?: unknown; phase?: unknown })?.phase === "end", - ); + const endEventPromise = onceMessage(ws, (message) => isCompactOperationEvent(message, "end")); const compacted = await rpcReq<{ ok: true; @@ -586,9 +573,7 @@ test("sessions.compact treats Codex native compaction start as pending, not comp key: "main", }); - expect(compacted.ok).toBe(true); - expect(compacted.payload?.key).toBe("agent:main:main"); - expect(compacted.payload?.compacted).toBe(false); + expectMainCompactionResult(compacted, false); expect(compacted.payload?.result?.details).toMatchObject({ backend: "codex-app-server", threadId: "thread-1", diff --git a/src/gateway/server.sessions.create.test.ts b/src/gateway/server.sessions.create.test.ts index 5a90854217e4..fdbbc9add179 100644 --- a/src/gateway/server.sessions.create.test.ts +++ b/src/gateway/server.sessions.create.test.ts @@ -10,7 +10,8 @@ import { sessionLifecycleHookMocks, } from "./test/server-sessions.test-helpers.js"; -const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness(); +const { createSessionStoreDir, createSelectedGlobalSessionStore, openClient } = + setupGatewaySessionsTestHarness(); function requireNonEmptyString(value: string | undefined, label: string): string { if (!value) { @@ -302,13 +303,7 @@ test("sessions.create preserves global and unknown sentinel keys", async () => { }); test("sessions.create stores selected global sessions in the requested agent store", async () => { - const { dir } = await createSessionStoreDir(); - const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); - const mainStorePath = storeTemplate.replace("{agentId}", "main"); - const workStorePath = storeTemplate.replace("{agentId}", "work"); - testState.sessionStorePath = storeTemplate; - testState.sessionConfig = { scope: "global" }; - testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; + const { mainStorePath, workStorePath } = await createSelectedGlobalSessionStore(); const broadcastToConnIds = vi.fn(); const created = await directSessionReq<{ @@ -350,13 +345,7 @@ test("sessions.create stores selected global sessions in the requested agent sto }); test("sessions.create loads selected global parent from the requested agent store", async () => { - const { dir } = await createSessionStoreDir(); - const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); - const mainStorePath = storeTemplate.replace("{agentId}", "main"); - const workStorePath = storeTemplate.replace("{agentId}", "work"); - testState.sessionStorePath = storeTemplate; - testState.sessionConfig = { scope: "global" }; - testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; + const { mainStorePath, workStorePath } = await createSelectedGlobalSessionStore(); try { await writeSessionStore({ storePath: mainStorePath, @@ -430,10 +419,7 @@ test("sessions.create loads selected global parent from the requested agent stor }); test("sessions.get reads selected global messages from the requested agent store", async () => { - const { dir } = await createSessionStoreDir(); - const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); - const mainStorePath = storeTemplate.replace("{agentId}", "main"); - const workStorePath = storeTemplate.replace("{agentId}", "work"); + const { mainStorePath, workStorePath } = await createSelectedGlobalSessionStore(); const mainTranscriptPath = path.join(path.dirname(mainStorePath), "sess-main-global.jsonl"); const workTranscriptPath = path.join(path.dirname(workStorePath), "sess-work-global.jsonl"); await fs.mkdir(path.dirname(mainTranscriptPath), { recursive: true }); @@ -448,9 +434,6 @@ test("sessions.get reads selected global messages from the requested agent store `${JSON.stringify({ type: "message", id: "work-msg", message: { role: "user", content: "work global" } })}\n`, "utf-8", ); - testState.sessionStorePath = storeTemplate; - testState.sessionConfig = { scope: "global" }; - testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; try { await writeSessionStore({ storePath: mainStorePath, @@ -487,11 +470,7 @@ test("sessions.get reads selected global messages from the requested agent store }); test("sessions.create sends selected global initial tasks to the requested agent", async () => { - const { dir } = await createSessionStoreDir(); - const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); - testState.sessionStorePath = storeTemplate; - testState.sessionConfig = { scope: "global" }; - testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; + const { mainStorePath, workStorePath } = await createSelectedGlobalSessionStore(); const { ws } = await openClient(); const created = await rpcReq<{ @@ -510,8 +489,6 @@ test("sessions.create sends selected global initial tasks to the requested agent const runId = requireNonEmptyString(created.payload?.runId, "selected global run id"); const wait = await rpcReq(ws, "agent.wait", { runId, timeoutMs: 1_000 }); expect(wait.ok).toBe(true); - const workStorePath = storeTemplate.replace("{agentId}", "work"); - const mainStorePath = storeTemplate.replace("{agentId}", "main"); const workStore = JSON.parse(await fs.readFile(workStorePath, "utf-8")) as Record< string, { sessionFile?: string } diff --git a/src/gateway/test/server-sessions.test-helpers.ts b/src/gateway/test/server-sessions.test-helpers.ts index 4518c470feaf..e924cb175034 100644 --- a/src/gateway/test/server-sessions.test-helpers.ts +++ b/src/gateway/test/server-sessions.test-helpers.ts @@ -320,6 +320,20 @@ export function setupGatewaySessionsTestHarness() { return { dir, storePath }; } + async function createSelectedGlobalSessionStore() { + const { dir } = await createSessionStoreDir(); + const storeTemplate = path.join(dir, "{agentId}", "sessions.json"); + testState.sessionStorePath = storeTemplate; + testState.sessionConfig = { scope: "global" }; + testState.agentsConfig = { list: [{ id: "main", default: true }, { id: "work" }] }; + return { + dir, + storeTemplate, + mainStorePath: storeTemplate.replace("{agentId}", "main"), + workStorePath: storeTemplate.replace("{agentId}", "work"), + }; + } + async function seedActiveMainSession() { const { dir, storePath } = await createSessionStoreDir(); await writeSingleLineSession(dir, "sess-main", "hello"); @@ -333,6 +347,7 @@ export function setupGatewaySessionsTestHarness() { return { createSessionStoreDir, + createSelectedGlobalSessionStore, getHarness: requireHarness, openClient, seedActiveMainSession,