mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 10:34:44 +00:00
refactor: share selected global session test setup
This commit is contained in:
@@ -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<ReturnType<typeof createCheckpointFixture>>;
|
||||
|
||||
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",
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user