Files
openclaw/src/gateway/server.sessions.create.test.ts
Peter Steinberger a509c48f0e feat: add core session goals (#87469)
* feat: add core session goals

* feat: polish session goals in tui

* fix: resolve goal tool session stores

* fix: keep get goal read-only

* fix: migrate legacy goal session slots

* fix: persist goal token accounting

* fix: validate goal session rows

* refactor: remove unshipped goal legacy handling

* fix: handle goal commands in local tui

* fix: satisfy goal tool display checks

* fix: reset goal budget on overdue resume

* feat: surface session goals across control surfaces

* test: update gateway protocol test import

* test: align goal fixture types with protocol

* fix: scope selected global transcript usage fallback

* fix: scope selected global web subscriptions

* fix: preserve selected global agent during chat dispatch

* fix: scope chat inject to selected global agents
2026-05-29 22:36:29 +02:00

579 lines
19 KiB
TypeScript

import fs from "node:fs/promises";
import path from "node:path";
import { expect, test, vi } from "vitest";
import { agentDiscoveryMock, rpcReq, testState, writeSessionStore } from "./test-helpers.js";
import {
setupGatewaySessionsTestHarness,
sessionStoreEntry,
directSessionReq,
sessionHookMocks,
sessionLifecycleHookMocks,
} 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();
agentDiscoveryMock.enabled = true;
agentDiscoveryMock.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]?.sessionId).toBe(created.payload?.sessionId);
expect(rawStore[key]?.label).toBe("Dashboard Chat");
expect(rawStore[key]?.providerOverride).toBe("openai");
expect(rawStore[key]?.modelOverride).toBe("gpt-test-a");
expect(rawStore[key]?.parentSessionKey).toBe("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);
const header = JSON.parse(headerLine) as { type?: string; id?: string };
expect(header.type).toBe("session");
expect(header.id).toBe(created.payload?.sessionId);
});
test("sessions.create inherits parent runtime model selection when model is omitted", async () => {
const { storePath } = await createSessionStoreDir();
await writeSessionStore({
entries: {
main: sessionStoreEntry("sess-parent", {
providerOverride: "codex",
modelOverride: "gpt-5.5",
modelOverrideSource: "user",
agentRuntimeOverride: "codex",
modelProvider: "codex",
model: "gpt-5.5",
contextTokens: 272000,
thinkingLevel: "off",
traceLevel: "debug",
authProfileOverride: "codex-oauth",
authProfileOverrideSource: "user",
}),
},
});
const created = await directSessionReq<{
key?: string;
entry?: {
providerOverride?: string;
modelOverride?: string;
modelOverrideSource?: string;
agentRuntimeOverride?: string;
modelProvider?: string;
model?: string;
contextTokens?: number;
thinkingLevel?: string;
traceLevel?: string;
authProfileOverride?: string;
authProfileOverrideSource?: string;
parentSessionKey?: string;
};
}>("sessions.create", {
agentId: "main",
label: "Fresh Chat",
parentSessionKey: "main",
});
expect(created.ok).toBe(true);
expect(created.payload?.entry?.parentSessionKey).toBe("agent:main:main");
expect(created.payload?.entry?.providerOverride).toBe("codex");
expect(created.payload?.entry?.modelOverride).toBe("gpt-5.5");
expect(created.payload?.entry?.modelOverrideSource).toBe("user");
expect(created.payload?.entry?.agentRuntimeOverride).toBe("codex");
expect(created.payload?.entry?.modelProvider).toBe("codex");
expect(created.payload?.entry?.model).toBe("gpt-5.5");
expect(created.payload?.entry?.contextTokens).toBe(272000);
expect(created.payload?.entry?.thinkingLevel).toBe("off");
expect(created.payload?.entry?.traceLevel).toBe("debug");
expect(created.payload?.entry?.authProfileOverride).toBe("codex-oauth");
expect(created.payload?.entry?.authProfileOverrideSource).toBe("user");
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{
providerOverride?: string;
modelOverride?: string;
parentSessionKey?: string;
}
>;
const key = created.payload?.key as string;
expect(rawStore[key]?.providerOverride).toBe("codex");
expect(rawStore[key]?.modelOverride).toBe("gpt-5.5");
expect(rawStore[key]?.parentSessionKey).toBe("agent:main:main");
});
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 replaces a dead main entry with a fresh session id", async () => {
const { storePath } = await createSessionStoreDir();
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
try {
await writeSessionStore({
agentId: "ops",
entries: {
main: {
updatedAt: 1,
label: "Ops Main",
sessionFile: "stale.jsonl",
},
},
});
const created = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: {
label?: string;
sessionFile?: string;
};
}>("sessions.create", {
key: "main",
agentId: "ops",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toBe("agent:ops:main");
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?.entry?.label).toBeUndefined();
expect(created.payload?.entry?.sessionFile).not.toBe("stale.jsonl");
const rawStore = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{
sessionId?: string;
sessionFile?: string;
}
>;
expect(rawStore["agent:ops:main"]?.sessionId).toBe(created.payload?.sessionId);
expect(rawStore["agent:ops:main"]?.sessionFile).not.toBe("stale.jsonl");
} finally {
testState.agentsConfig = undefined;
}
});
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 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 broadcastToConnIds = vi.fn();
const created = await directSessionReq<{
key?: string;
sessionId?: string;
entry?: { sessionFile?: string };
}>(
"sessions.create",
{
key: "global",
agentId: "work",
},
{
context: {
broadcastToConnIds,
getSessionEventSubscriberConnIds: () => new Set(["conn-1"]),
},
},
);
expect(created.ok).toBe(true);
expect(created.payload?.key).toBe("global");
requireNonEmptyString(created.payload?.entry?.sessionFile, "work global session file");
await expect(fs.readFile(mainStorePath, "utf-8")).rejects.toMatchObject({ code: "ENOENT" });
const workStore = JSON.parse(await fs.readFile(workStorePath, "utf-8")) as Record<
string,
{ sessionId?: string }
>;
expect(workStore.global?.sessionId).toBe(created.payload?.sessionId);
expect(broadcastToConnIds).toHaveBeenCalledWith(
"sessions.changed",
expect.objectContaining({ sessionKey: "global", agentId: "work", reason: "create" }),
new Set(["conn-1"]),
{ dropIfSlow: true },
);
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
testState.agentsConfig = undefined;
});
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" }] };
try {
await writeSessionStore({
storePath: mainStorePath,
entries: {
global: sessionStoreEntry("sess-main-parent", {
providerOverride: "codex",
modelOverride: "main-model",
}),
},
});
await writeSessionStore({
storePath: workStorePath,
agentId: "work",
entries: {
global: sessionStoreEntry("sess-work-parent", {
providerOverride: "openai",
modelOverride: "work-model",
thinkingLevel: "high",
}),
},
});
const created = await directSessionReq<{
key?: string;
entry?: {
parentSessionKey?: string;
providerOverride?: string;
modelOverride?: string;
thinkingLevel?: string;
};
}>("sessions.create", {
agentId: "work",
parentSessionKey: "global",
emitCommandHooks: true,
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toMatch(/^agent:work:dashboard:/);
expect(created.payload?.entry?.parentSessionKey).toBe("global");
expect(created.payload?.entry?.providerOverride).toBe("openai");
expect(created.payload?.entry?.modelOverride).toBe("work-model");
expect(created.payload?.entry?.thinkingLevel).toBe("high");
const commandNewEvent = (
sessionHookMocks.triggerInternalHook.mock.calls as unknown as Array<[unknown]>
)
.map((call) => call[0])
.find(
(
event,
): event is {
context?: { sessionEntry?: { sessionId?: string } };
} =>
Boolean(event) &&
typeof event === "object" &&
(event as { type?: unknown }).type === "command" &&
(event as { action?: unknown }).action === "new",
);
expect(commandNewEvent?.context?.sessionEntry?.sessionId).toBe("sess-work-parent");
const [endEvent] = sessionLifecycleHookMocks.runSessionEnd.mock.calls[0] as unknown as [
{ sessionId?: string; sessionKey?: string },
unknown,
];
expect(endEvent.sessionId).toBe("sess-work-parent");
expect(endEvent.sessionKey).toBe("global");
} finally {
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
testState.agentsConfig = undefined;
}
});
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 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 });
await fs.mkdir(path.dirname(workTranscriptPath), { recursive: true });
await fs.writeFile(
mainTranscriptPath,
`${JSON.stringify({ type: "message", id: "main-msg", message: { role: "user", content: "main global" } })}\n`,
"utf-8",
);
await fs.writeFile(
workTranscriptPath,
`${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,
entries: {
global: sessionStoreEntry("sess-main-global", {
sessionFile: mainTranscriptPath,
}),
},
});
await writeSessionStore({
storePath: workStorePath,
agentId: "work",
entries: {
global: sessionStoreEntry("sess-work-global", {
sessionFile: workTranscriptPath,
}),
},
});
const result = await directSessionReq<{ messages?: unknown[] }>("sessions.get", {
key: "global",
agentId: "work",
});
expect(result.ok).toBe(true);
const renderedMessages = JSON.stringify(result.payload?.messages ?? []);
expect(renderedMessages).toContain("work global");
expect(renderedMessages).not.toContain("main global");
} finally {
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
testState.agentsConfig = undefined;
}
});
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 { ws } = await openClient();
const created = await rpcReq<{
key?: string;
runStarted?: boolean;
runId?: string;
}>(ws, "sessions.create", {
key: "global",
agentId: "work",
task: "hello selected global",
});
expect(created.ok).toBe(true);
expect(created.payload?.key).toBe("global");
expect(created.payload?.runStarted).toBe(true);
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 }
>;
const workTranscript = requireNonEmptyString(
workStore.global?.sessionFile,
"selected global transcript",
);
await expect(fs.readFile(workTranscript, "utf-8")).resolves.toContain("hello selected global");
await expect(fs.readFile(mainStorePath, "utf-8")).rejects.toMatchObject({ code: "ENOENT" });
testState.sessionStorePath = undefined;
testState.sessionConfig = undefined;
testState.agentsConfig = undefined;
ws.close();
});
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);
const runId = requireNonEmptyString(created.payload?.runId, "started run id");
expect(created.payload?.messageSeq).toBe(1);
const wait = await rpcReq(ws, "agent.wait", { runId, timeoutMs: 1_000 });
expect(wait.ok).toBe(true);
expect(wait.payload?.status).toBe("ok");
ws.close();
});