Files
openclaw/src/gateway/server.sessions.create.test.ts
2026-05-08 05:28:12 +01:00

230 lines
7.1 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { expect, test } from "vitest";
import { piSdkMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js";
import {
setupGatewaySessionsTestHarness,
sessionStoreEntry,
directSessionReq,
} from "./test/server-sessions.test-helpers.js";
const { createSessionStoreDir, openClient } = setupGatewaySessionsTestHarness();
function requireNonEmptyString(value: string | undefined, label: string): string {
if (!value) {
throw new Error(`expected ${label}`);
}
return value;
}
test("sessions.create stores dashboard session model and parent linkage, and creates a transcript", async () => {
const { dir, storePath } = await createSessionStoreDir();
piSdkMock.enabled = true;
piSdkMock.models = [{ id: "gpt-test-a", name: "A", provider: "openai" }];
await writeSessionStore({
entries: {
main: sessionStoreEntry("sess-parent"),
},
});
const created = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
label?: string;
providerOverride?: string;
modelOverride?: string;
parentSessionKey?: string;
sessionFile?: string;
};
}>("sessions.create", {
agentId: "ops",
label: "Dashboard Chat",
model: "openai/gpt-test-a",
parentSessionKey: "main",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toMatch(/^agent:ops:dashboard:/);
expect(created.payload?.entry?.label).toBe("Dashboard Chat");
expect(created.payload?.entry?.providerOverride).toBe("openai");
expect(created.payload?.entry?.modelOverride).toBe("gpt-test-a");
expect(created.payload?.entry?.parentSessionKey).toBe("agent:main:main");
const sessionFile = requireNonEmptyString(
created.payload?.entry?.sessionFile,
"created session file",
);
expect(created.payload?.sessionId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{
sessionId?: string;
label?: string;
providerOverride?: string;
modelOverride?: string;
parentSessionKey?: string;
sessionFile?: string;
}
>;
const key = created.payload?.key as string;
expect(rawStore[key]).toMatchObject({
sessionId: created.payload?.sessionId,
label: "Dashboard Chat",
providerOverride: "openai",
modelOverride: "gpt-test-a",
parentSessionKey: "agent:main:main",
});
expect(sessionFile).toBe(rawStore[key]?.sessionFile);
const transcriptPath = path.join(dir, `${created.payload?.sessionId}.jsonl`);
const transcript = await fs.readFile(transcriptPath, "utf-8");
const [headerLine] = transcript.trim().split(/\r?\n/, 1);
expect(JSON.parse(headerLine) as { type?: string; id?: string }).toMatchObject({
type: "session",
id: created.payload?.sessionId,
});
});
test("sessions.create accepts an explicit key for persistent dashboard sessions", async () => {
await createSessionStoreDir();
const key = "agent:ops-agent:dashboard:direct:subagent-orchestrator";
const created = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
label?: string;
};
}>("sessions.create", {
key,
label: "Dashboard Orchestrator",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toBe(key);
expect(created.payload?.entry?.label).toBe("Dashboard Orchestrator");
expect(created.payload?.sessionId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
});
test("sessions.create scopes the main alias to the requested agent", async () => {
const { storePath } = await createSessionStoreDir();
const created = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
sessionFile?: string;
};
}>("sessions.create", {
key: "main",
agentId: "longmemeval",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toBe("agent:longmemeval:main");
requireNonEmptyString(created.payload?.entry?.sessionFile, "longmemeval session file");
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{
sessionId?: string;
}
>;
expect(rawStore["agent:longmemeval:main"]?.sessionId).toBe(created.payload?.sessionId);
expect(rawStore["agent:main:main"]).toBeUndefined();
});
test("sessions.create preserves global and unknown sentinel keys", async () => {
const { storePath } = await createSessionStoreDir();
const globalCreated = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
sessionFile?: string;
};
}>("sessions.create", {
key: "global",
agentId: "longmemeval",
});
expect(globalCreated.ok).toBe(true);
expect(globalCreated.payload?.key).toBe("global");
requireNonEmptyString(globalCreated.payload?.entry?.sessionFile, "global session file");
const unknownCreated = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
sessionFile?: string;
};
}>("sessions.create", {
key: "unknown",
agentId: "longmemeval",
});
expect(unknownCreated.ok).toBe(true);
expect(unknownCreated.payload?.key).toBe("unknown");
requireNonEmptyString(unknownCreated.payload?.entry?.sessionFile, "unknown session file");
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{
sessionId?: string;
}
>;
expect(rawStore.global?.sessionId).toBe(globalCreated.payload?.sessionId);
expect(rawStore.unknown?.sessionId).toBe(unknownCreated.payload?.sessionId);
expect(rawStore["agent:longmemeval:global"]).toBeUndefined();
expect(rawStore["agent:longmemeval:unknown"]).toBeUndefined();
});
test("sessions.create rejects unknown parentSessionKey", async () => {
await createSessionStoreDir();
const created = await directSessionReq("sessions.create", {
agentId: "ops",
parentSessionKey: "agent:main:missing",
});
expect(created.ok).toBe(false);
expect((created.error as { message?: string } | undefined)?.message ?? "").toContain(
"unknown parent session",
);
});
test("sessions.create can start the first agent turn from an initial task", async () => {
await createSessionStoreDir();
// Register "ops" so the deleted-agent guard added in #65986 does not
// reject the auto-started chat.send triggered by `task:`.
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
const { ws } = await openClient();
const created = await rpcReq<{
key?: string;
sessionId?: string;
runStarted?: boolean;
runId?: string;
messageSeq?: number;
}>(ws, "sessions.create", {
agentId: "ops",
label: "Dashboard Chat",
task: "hello from create",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toMatch(/^agent:ops:dashboard:/);
expect(created.payload?.sessionId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
expect(created.payload?.runStarted).toBe(true);
requireNonEmptyString(created.payload?.runId, "started run id");
expect(created.payload?.messageSeq).toBe(1);
ws.close();
});