Files
openclaw/src/gateway/server.config-patch.test.ts
Peter Steinberger bb46b79d3c refactor: internalize OpenClaw agent runtime (#85341)
* refactor: extract agent core package

Introduce packages/agent-core as the OpenClaw-owned home for reusable agent loop, harness, session, prompt, and runtime dependency contracts.

* refactor: extract shared llm runtime

Move provider model registries, stream wrappers, OAuth helpers, and LLM utilities into src/llm with plugin-sdk barrels instead of depending on the old embedded runtime layout.

* refactor: remove pi runtime internals

Rename remaining Pi-shaped agent surfaces to OpenClaw agent runtime names, delete obsolete Pi docs and package graph checks, and add the third-party notice for incorporated code.

* refactor: tighten agent session runtime

Make agent-core/runtime dependencies explicit, consolidate compaction and session transcript helpers, and move model/session helpers behind OpenClaw-owned contracts.

* refactor: remove static model and pi auth paths

Drop static model catalogs and Pi auth bridges, move model/provider facts to manifest-owned runtime contracts, and harden internal embedded-agent utilities.

* refactor: remove legacy provider compat paths

* docs: remove agent parity notes

* fix: skip provider wildcard metadata parsing

* refactor: share session extension sdk loading

* refactor: inline acpx proxy error formatter

* refactor: fold edit recovery into edit tool

* fix: accept extension batch separator

* test: align startup provider plugin expectations

* fix: restore provider-scoped release discovery

* test: align static asset packaging expectations

* fix: run static provider catalogs during scoped discovery

* fix: add provider entry catalogs for scoped live discovery

* fix: load lightweight provider catalog entries

* fix: refresh provider-scoped plugin metadata

* fix: keep provider catalog entries on release live path

* fix: keep static manifest models in release live checks

* fix: harden release model discovery

* fix: reduce OpenAI live cache probe reasoning

* fix: disable OpenAI cache probe reasoning

* ci: extend OpenAI gateway live timeout

* fix: extend live gateway model budget

* fix: stabilize release validation regressions

* fix: honor provider aliases in model rows

* fix: stabilize release validation lanes

* fix: stabilize release memory qa

* ci: stabilize release validation lanes

* ci: prefer ipv4 for live docker node calls

* fix: restore shared tool-call stream wrapper

* ci: remove legacy pi test shard alias

* fix: clean up embedded agent test drift

* fix: stabilize runtime alias status

* fix: clean up embedded agent ci drift

* fix: restore release ci invariants

* fix: clean up post-rebase runtime drift

* fix: restore release ci checks

* fix: restore release ci after rebase

* fix: remove stale pi runtime path

* test: align compaction runtime expectations

* test: update plugin prerelease expectations

* fix: handle claude live tool approvals

* fix: stabilize release validation gates

* fix: finish agent runtime import

* test: finish post-rebase agent runtime mocks

* fix: keep codex compaction native

* fix: stabilize codex app-server hook tests

* test: isolate codex diagnostic active run

* test: remove codex diagnostic completion race

# Conflicts:
#	extensions/codex/src/app-server/run-attempt.test.ts

* ci: fix full release manifest performance run id

* refactor: narrow llm plugin sdk boundary

* chore: drop generated google boundary stamps

* fix: repair rebase fallout

* fix: clean up rebased runtime references

* fix: decode codex jwt payloads as base64url

* fix: preserve shipped pi runtime alias

* fix: add scoped sdk virtual modules

* fix: decode llm codex oauth jwt as base64url

* fix: avoid stale vertex adc negative cache

* fix: harden tool arg decoding and codeql path

* fix: keep vertex adc negative checks live

* refactor: consolidate codex jwt and edit helpers

* fix: await codex oauth node runtime imports

* fix: preserve sdk tool and notice contracts

* fix: preserve shipped compat config boundaries

* fix: align codex oauth callback host

* fix: terminate agent-core loop streams on failure

* fix: keep codex oauth callback alive during fallback

* ci: include session tools in critical codeql scans

* fix: keep Cloudflare Anthropic provider auth header

* docs: redirect legacy pi runtime pages

* fix: honor bundled web provider compat discovery

* fix: protect session output spill files

* fix: keep legacy agent dir env blocked

* fix: contain auto-discovered skill symlinks

* fix: harden agent core sdk proxy surfaces

* fix: restore approval reaction sdk compat

* fix: keep live docker runs bounded

* fix: keep codex oauth redirect host aligned

* fix: resolve post-rebase agent runtime drift

* fix: redact anthropic oauth parse failures

* fix: preserve responses strict tool shaping

* fix: repair agent runtime rebase cleanup

* docs: redirect retired parity pages

* fix: bound auto-discovered resources to roots

* fix: repair post-rebase agent test drift

* fix: preserve bundled provider allowlist migration

* fix: preserve manifest-owned provider aliases

* fix: declare photon image dependency

* fix: keep provider headers out of proxy body

* fix: preserve shipped env aliases

* fix: refresh control ui i18n generated state

* fix: quote read fallback paths

* fix: preview edits through configured backend

* test: satisfy core test typecheck

* fix: preserve ZAI usage auth fallback

* test: repair codex diagnostic test

* fix: repair agent runtime rebase drift

* test: finish embedded runner import rename

* fix: repair agent runtime rebase integrations

* test: align compaction oauth fallback expectations

* fix: allow sdk-auth session models

* fix: update doctor tool schema import

* fix: preserve bedrock plugin region

* fix: stream harmony-like prose immediately

* ci: include session runtime in codeql shards

* fix: repair latest rebase integrations

* fix: honor explicit codex websocket transport

* fix: keep openai-compatible credentials provider-scoped

* fix: refresh sdk api baseline after rebase

* fix: route cli runtime aliases through openclaw harness

* test: rename stale harness mock expectation

* test: rename embedded agent overflow calls

* test: clean embedded auth test wording

* test: use openclaw stream types in deepinfra cache test

* fix: refresh sdk api baseline on latest main

* fix: honor bundled discovery compat allowlists

* fix: refresh sdk api baseline after latest rebase

* fix: remove stale rebase imports

* test: rename stale model catalog mock

* test: mock renamed doctor runtime modules

* fix: map canonical kimi env auth

* fix: use internal model registry in bench script

* fix: migrate deepinfra provider catalog entry

* fix: enforce builtin tool suppression

* fix: route compaction auth and proxy payloads safely

* refactor: prune unused llm registry leftovers

* test: update codex hooks session import

* test: fix model picker ci coverage

* test: align model picker auth mock types
2026-05-27 19:24:04 +01:00

669 lines
22 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { resolveDefaultAgentDir } from "../agents/agent-scope.js";
import { AUTH_PROFILE_FILENAME } from "../agents/auth-profiles/constants.js";
import { testing as controlPlaneRateLimitTesting } from "./control-plane-rate-limit.js";
import {
connectOk,
installGatewayTestHooks,
rpcReq,
startServerWithClient,
testState,
writeSessionStore,
} from "./test-helpers.js";
installGatewayTestHooks({ scope: "suite" });
const CONFIG_SECRETREF_RPC_TIMEOUT_MS = 20_000;
let startedServer: Awaited<ReturnType<typeof startServerWithClient>> | null = null;
let sharedTempRoot: string;
function requireWs(): Awaited<ReturnType<typeof startServerWithClient>>["ws"] {
if (!startedServer) {
throw new Error("gateway test server not started");
}
return startedServer.ws;
}
function requireConfigObject(
value: Record<string, unknown> | undefined,
label: string,
): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error(`expected ${label}`);
}
return value;
}
beforeAll(async () => {
sharedTempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-sessions-config-"));
startedServer = await startServerWithClient(undefined, { controlUiEnabled: true });
await connectOk(requireWs());
});
afterAll(async () => {
if (!startedServer) {
return;
}
startedServer.ws.close();
await startedServer.server.close();
startedServer = null;
await fs.rm(sharedTempRoot, { recursive: true, force: true });
});
async function resetTempDir(name: string): Promise<string> {
const dir = path.join(sharedTempRoot, name);
await fs.rm(dir, { recursive: true, force: true });
await fs.mkdir(dir, { recursive: true });
return dir;
}
async function getConfigHash() {
const current = await rpcReq<{
hash?: string;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
return String(current.payload?.hash);
}
async function sendConfigApply(params: { raw: unknown; baseHash?: string }, timeoutMs?: number) {
return await rpcReq(requireWs(), "config.apply", params, timeoutMs);
}
async function expectSchemaLookupInvalid(path: unknown) {
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", { path });
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("invalid config.schema.lookup params");
}
async function writeUnresolvedAuthProfileTokenRef(missingEnvVar: string) {
delete process.env[missingEnvVar];
const authStorePath = path.join(resolveDefaultAgentDir({}), AUTH_PROFILE_FILENAME);
await fs.mkdir(path.dirname(authStorePath), { recursive: true });
await fs.writeFile(
authStorePath,
`${JSON.stringify(
{
version: 1,
profiles: {
"custom:token": {
type: "token",
provider: "custom",
tokenRef: { source: "env", provider: "default", id: missingEnvVar },
},
},
},
null,
2,
)}\n`,
"utf-8",
);
}
beforeEach(() => {
controlPlaneRateLimitTesting.resetControlPlaneRateLimitState();
});
describe("gateway config methods", () => {
it("rejects config.set when SecretRef resolution fails", async () => {
const missingEnvVar = `OPENCLAW_MISSING_SECRETREF_${Date.now()}`;
delete process.env[missingEnvVar];
const current = await rpcReq<{
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const currentConfig = requireConfigObject(current.payload?.config, "current config");
const nextConfig = structuredClone(currentConfig);
const gateway = (nextConfig.gateway ??= {}) as Record<string, unknown>;
gateway.auth = {
mode: "token",
token: { source: "env", provider: "default", id: missingEnvVar },
};
const res = await rpcReq<{ ok?: boolean; error?: { message?: string } }>(
requireWs(),
"config.set",
{
raw: JSON.stringify(nextConfig, null, 2),
baseHash: current.payload?.hash,
},
CONFIG_SECRETREF_RPC_TIMEOUT_MS,
);
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("active SecretRef resolution failed");
const afterHash = await getConfigHash();
expect(afterHash).toBe(current.payload?.hash);
});
it("round-trips config.set and returns the live config path", async () => {
const { createConfigIO } = await import("../config/config.js");
const current = await rpcReq<{
raw?: unknown;
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const currentConfig = requireConfigObject(current.payload?.config, "current config");
const res = await rpcReq<{
ok?: boolean;
path?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.set", {
raw: JSON.stringify(currentConfig, null, 2),
baseHash: current.payload?.hash,
});
expect(res.ok).toBe(true);
expect(res.payload?.path).toBe(createConfigIO().configPath);
requireConfigObject(res.payload?.config, "updated config");
});
it("returns the persisted config from config.set responses", async () => {
const current = await rpcReq<{
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const nextConfig = structuredClone(
requireConfigObject(current.payload?.config, "current config"),
);
delete nextConfig.meta;
const gateway = (nextConfig.gateway ??= {}) as Record<string, unknown>;
gateway.port = 19001;
const res = await rpcReq<{
ok?: boolean;
config?: Record<string, unknown>;
}>(requireWs(), "config.set", {
raw: JSON.stringify(nextConfig, null, 2),
baseHash: current.payload?.hash,
});
expect(res.error).toBeUndefined();
expect(res.ok).toBe(true);
const after = await rpcReq<{
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(after.ok).toBe(true);
expect(res.payload?.config).toEqual(after.payload?.config);
requireConfigObject(res.payload?.config, "response config");
});
it("accepts runtime-shaped config.set when bundled provider baseUrl was only defaulted", async () => {
const { createConfigIO, resetConfigRuntimeState } = await import("../config/config.js");
const configPath = createConfigIO().configPath;
await fs.mkdir(path.dirname(configPath), { recursive: true });
try {
await fs.writeFile(
configPath,
`${JSON.stringify(
{
models: {
providers: {
openai: {
agentRuntime: { id: "openclaw" },
},
},
},
},
null,
2,
)}\n`,
"utf-8",
);
resetConfigRuntimeState();
const current = await rpcReq<{
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const nextConfig = structuredClone(
requireConfigObject(current.payload?.config, "current config"),
);
const providers = ((nextConfig.models as Record<string, unknown>).providers ?? {}) as Record<
string,
Record<string, unknown>
>;
providers.openai ??= {};
providers.openai.baseUrl = "";
providers.openai.models = [];
const gateway = (nextConfig.gateway ??= {}) as Record<string, unknown>;
gateway.port = 19002;
const res = await rpcReq<{
ok?: boolean;
error?: { message?: string };
}>(requireWs(), "config.set", {
raw: JSON.stringify(nextConfig, null, 2),
baseHash: current.payload?.hash,
});
expect(res.error).toBeUndefined();
expect(res.ok).toBe(true);
const persisted = await fs.readFile(configPath, "utf-8");
expect(persisted).toContain('"port": 19002');
expect(persisted).not.toContain('"baseUrl"');
} finally {
await fs.rm(configPath, { force: true });
resetConfigRuntimeState();
}
});
it("redacts browser cdpUrl credentials from config.get responses", async () => {
const { createConfigIO, resetConfigRuntimeState } = await import("../config/config.js");
const configPath = createConfigIO().configPath;
await fs.mkdir(path.dirname(configPath), { recursive: true });
try {
await fs.writeFile(
configPath,
`${JSON.stringify(
{
browser: {
cdpUrl: "https://user:pass@chrome.browserless.io?token=supersecret123",
profiles: {
remote: {
cdpUrl: "https://alice:secret@chrome.remote.example.com?token=profile-secret",
},
local: {
cdpUrl: "ws://127.0.0.1:9222",
},
},
},
},
null,
2,
)}\n`,
"utf-8",
);
resetConfigRuntimeState();
const after = await rpcReq<{
raw?: string | null;
config?: {
browser?: {
cdpUrl?: string;
profiles?: Record<string, { cdpUrl?: string }>;
};
};
}>(requireWs(), "config.get", {});
expect(after.ok).toBe(true);
expect(after.payload?.config?.browser?.cdpUrl).toBe("__OPENCLAW_REDACTED__");
expect(after.payload?.config?.browser?.profiles?.remote?.cdpUrl).toBe(
"__OPENCLAW_REDACTED__",
);
expect(after.payload?.config?.browser?.profiles?.local?.cdpUrl).toBe("ws://127.0.0.1:9222");
if (typeof after.payload?.raw === "string") {
expect(after.payload.raw).toContain("__OPENCLAW_REDACTED__");
expect(after.payload.raw).not.toContain("supersecret123");
expect(after.payload.raw).not.toContain("user:pass@");
expect(after.payload.raw).not.toContain("profile-secret");
expect(after.payload.raw).not.toContain("alice:secret@");
}
} finally {
await fs.rm(configPath, { force: true });
resetConfigRuntimeState();
}
});
it("does not reject config.set for unresolved auth-profile refs outside submitted config", async () => {
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_REF_${Date.now()}`;
await writeUnresolvedAuthProfileTokenRef(missingEnvVar);
const current = await rpcReq<{
hash?: string;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const currentConfig = requireConfigObject(current.payload?.config, "current config");
const res = await rpcReq<{ ok?: boolean; error?: { message?: string } }>(
requireWs(),
"config.set",
{
raw: JSON.stringify(currentConfig, null, 2),
baseHash: current.payload?.hash,
},
);
expect(res.ok).toBe(true);
expect(res.error).toBeUndefined();
});
it("returns config.set validation details in the top-level error message", async () => {
const res = await rpcReq<{
ok?: boolean;
error?: {
message?: string;
};
}>(requireWs(), "config.set", {
raw: JSON.stringify({ gateway: { bind: 123 } }),
baseHash: await getConfigHash(),
});
const error = res.error as
| {
message?: string;
details?: {
issues?: Array<{ path?: string; message?: string }>;
};
}
| undefined;
expect(res.ok).toBe(false);
expect(error?.message ?? "").toContain("invalid config:");
expect(error?.message ?? "").toContain("gateway.bind");
expect(error?.message ?? "").toContain("allowed:");
expect(error?.details?.issues?.[0]?.path).toBe("gateway.bind");
});
it("returns a path-scoped config schema lookup", async () => {
const res = await rpcReq<{
path: string;
hintPath?: string;
children?: Array<{ key: string; path: string; required: boolean; hintPath?: string }>;
schema?: { properties?: unknown };
}>(requireWs(), "config.schema.lookup", {
path: "gateway.auth",
});
expect(res.ok).toBe(true);
expect(res.payload?.path).toBe("gateway.auth");
expect(res.payload?.hintPath).toBe("gateway.auth");
const tokenChild = res.payload?.children?.find((child) => child.key === "token");
expect(tokenChild?.key).toBe("token");
expect(tokenChild?.path).toBe("gateway.auth.token");
expect(tokenChild?.hintPath).toBe("gateway.auth.token");
expect(res.payload?.schema?.properties).toBeUndefined();
});
it("rejects config.schema.lookup when the path is missing", async () => {
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", {
path: "gateway.notReal.path",
});
expect(res.ok).toBe(false);
expect(res.error?.message).toBe("config schema path not found");
});
it.each([
{ name: "rejects config.schema.lookup when the path is only whitespace", path: " " },
{
name: "rejects config.schema.lookup when the path exceeds the protocol limit",
path: `gateway.${"a".repeat(1020)}`,
},
{
name: "rejects config.schema.lookup when the path contains invalid characters",
path: "gateway.auth\nspoof",
},
{
name: "rejects config.schema.lookup when the path is not a string",
path: 42,
},
])("$name", async ({ path }) => {
await expectSchemaLookupInvalid(path);
});
it("rejects prototype-chain config.schema.lookup paths without reflecting them", async () => {
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.schema.lookup", {
path: "constructor",
});
expect(res.ok).toBe(false);
expect(res.error?.message).toBe("config schema path not found");
});
it("returns noop for config.patch when config is unchanged", async () => {
const current = await rpcReq<{
config?: Record<string, unknown>;
hash?: string;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
// Patch with the same config — no actual changes
const res = await rpcReq<{
ok?: boolean;
noop?: boolean;
config?: Record<string, unknown>;
}>(requireWs(), "config.patch", {
raw: JSON.stringify(current.payload?.config ?? {}),
baseHash: current.payload?.hash,
});
expect(res.ok).toBe(true);
expect(res.payload?.noop).toBe(true);
// Config hash should not change (no file write)
const after = await rpcReq<{ hash?: string }>(requireWs(), "config.get", {});
expect(after.payload?.hash).toBe(current.payload?.hash);
});
it("rejects config.patch when raw is null", async () => {
const res = await rpcReq<{ ok?: boolean }>(requireWs(), "config.patch", {
raw: "null",
baseHash: await getConfigHash(),
});
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("raw must be an object");
});
it("rejects config.patch when merged SecretRefs cannot resolve", async () => {
const missingEnvVar = `OPENCLAW_MISSING_SECRETREF_PATCH_${Date.now()}`;
delete process.env[missingEnvVar];
const beforeHash = await getConfigHash();
const res = await rpcReq<{ ok?: boolean; error?: { message?: string } }>(
requireWs(),
"config.patch",
{
raw: JSON.stringify({
gateway: {
auth: {
mode: "token",
token: {
source: "env",
provider: "default",
id: missingEnvVar,
},
},
},
}),
baseHash: beforeHash,
},
CONFIG_SECRETREF_RPC_TIMEOUT_MS,
);
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("active SecretRef resolution failed");
const afterHash = await getConfigHash();
expect(afterHash).toBe(beforeHash);
});
});
describe("gateway config.apply", () => {
it("rejects config.apply when SecretRef resolution fails", async () => {
const missingEnvVar = `OPENCLAW_MISSING_SECRETREF_APPLY_${Date.now()}`;
delete process.env[missingEnvVar];
const current = await rpcReq<{
hash?: string;
raw?: string | null;
config?: Record<string, unknown>;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
expect(typeof current.payload?.hash).toBe("string");
const nextConfig = structuredClone(current.payload?.config ?? {});
const gateway = (nextConfig.gateway ??= {}) as Record<string, unknown>;
gateway.auth = {
mode: "token",
token: { source: "env", provider: "default", id: missingEnvVar },
};
const res = await sendConfigApply(
{
raw: JSON.stringify(nextConfig, null, 2),
baseHash: current.payload?.hash,
},
CONFIG_SECRETREF_RPC_TIMEOUT_MS,
);
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("active SecretRef resolution failed");
const after = await rpcReq<{
hash?: string;
raw?: string | null;
}>(requireWs(), "config.get", {});
expect(after.ok).toBe(true);
expect(after.payload?.hash).toBe(current.payload?.hash);
expect(after.payload?.raw).toBe(current.payload?.raw);
});
it("does not reject config.apply for unresolved auth-profile refs outside submitted config", async () => {
const missingEnvVar = `OPENCLAW_MISSING_AUTH_PROFILE_REF_APPLY_${Date.now()}`;
await writeUnresolvedAuthProfileTokenRef(missingEnvVar);
const current = await rpcReq<{
config?: Record<string, unknown>;
hash?: string;
}>(requireWs(), "config.get", {});
expect(current.ok).toBe(true);
const currentConfig = requireConfigObject(current.payload?.config, "current config");
const res = await sendConfigApply({
raw: JSON.stringify(currentConfig, null, 2),
baseHash: current.payload?.hash,
});
expect(res.ok).toBe(true);
expect(res.error).toBeUndefined();
});
it("rejects invalid raw config", async () => {
const currentHash = await getConfigHash();
const res = await sendConfigApply({ raw: "{", baseHash: currentHash });
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toMatch(/invalid|SyntaxError/i);
});
it("requires raw to be a string", async () => {
const currentHash = await getConfigHash();
const res = await sendConfigApply({
raw: { gateway: { mode: "local" } },
baseHash: currentHash,
});
expect(res.ok).toBe(false);
expect(res.error?.message ?? "").toContain("raw");
});
});
describe("gateway server sessions", () => {
it("filters sessions by agentId", async () => {
const dir = await resetTempDir("agents");
testState.sessionConfig = {
store: path.join(dir, "{agentId}", "sessions.json"),
};
testState.agentsConfig = {
list: [{ id: "home", default: true }, { id: "work" }],
};
const homeDir = path.join(dir, "home");
const workDir = path.join(dir, "work");
await fs.mkdir(homeDir, { recursive: true });
await fs.mkdir(workDir, { recursive: true });
await writeSessionStore({
storePath: path.join(homeDir, "sessions.json"),
agentId: "home",
entries: {
main: {
sessionId: "sess-home-main",
updatedAt: Date.now(),
},
"discord:group:dev": {
sessionId: "sess-home-group",
updatedAt: Date.now() - 1000,
},
},
});
await writeSessionStore({
storePath: path.join(workDir, "sessions.json"),
agentId: "work",
entries: {
main: {
sessionId: "sess-work-main",
updatedAt: Date.now(),
},
},
});
const homeSessions = await rpcReq<{
sessions: Array<{ key: string }>;
}>(requireWs(), "sessions.list", {
includeGlobal: false,
includeUnknown: false,
agentId: "home",
});
expect(homeSessions.ok).toBe(true);
expect(homeSessions.payload?.sessions.map((s) => s.key).toSorted()).toEqual([
"agent:home:discord:group:dev",
"agent:home:main",
]);
const workSessions = await rpcReq<{
sessions: Array<{ key: string }>;
}>(requireWs(), "sessions.list", {
includeGlobal: false,
includeUnknown: false,
agentId: "work",
});
expect(workSessions.ok).toBe(true);
expect(workSessions.payload?.sessions.map((s) => s.key)).toEqual(["agent:work:main"]);
});
it("resolves and patches main alias to default agent main key", async () => {
const dir = await resetTempDir("main-alias");
const storePath = path.join(dir, "sessions.json");
testState.sessionStorePath = storePath;
testState.agentsConfig = { list: [{ id: "ops", default: true }] };
testState.sessionConfig = { mainKey: "work" };
await writeSessionStore({
storePath,
agentId: "ops",
mainKey: "work",
entries: {
main: {
sessionId: "sess-ops-main",
updatedAt: Date.now(),
},
},
});
const resolved = await rpcReq<{ ok: true; key: string }>(requireWs(), "sessions.resolve", {
key: "main",
});
expect(resolved.ok).toBe(true);
expect(resolved.payload?.key).toBe("agent:ops:work");
const patched = await rpcReq<{ ok: true; key: string }>(requireWs(), "sessions.patch", {
key: "main",
thinkingLevel: "medium",
});
expect(patched.ok).toBe(true);
expect(patched.payload?.key).toBe("agent:ops:work");
const stored = JSON.parse(await fs.readFile(storePath, "utf-8")) as Record<
string,
{ thinkingLevel?: string }
>;
expect(stored["agent:ops:work"]?.thinkingLevel).toBe("medium");
expect(stored.main).toBeUndefined();
});
});