test: deduplicate session target discovery cases

This commit is contained in:
Peter Steinberger 2026-03-13 16:35:18 +00:00
parent a4525b721e
commit 1ff8de3a8a
1 changed files with 104 additions and 201 deletions

View File

@ -15,6 +15,58 @@ async function resolveRealStorePath(sessionsDir: string): Promise<string> {
return fsSync.realpathSync.native(path.join(sessionsDir, "sessions.json"));
}
async function createAgentSessionStores(
root: string,
agentIds: string[],
): Promise<Record<string, string>> {
const storePaths: Record<string, string> = {};
for (const agentId of agentIds) {
const sessionsDir = path.join(root, "agents", agentId, "sessions");
await fs.mkdir(sessionsDir, { recursive: true });
await fs.writeFile(path.join(sessionsDir, "sessions.json"), "{}", "utf8");
storePaths[agentId] = await resolveRealStorePath(sessionsDir);
}
return storePaths;
}
function createCustomRootCfg(customRoot: string, defaultAgentId = "ops"): OpenClawConfig {
return {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: defaultAgentId, default: true }],
},
};
}
function expectTargetsToContainStores(
targets: Array<{ agentId: string; storePath: string }>,
stores: Record<string, string>,
): void {
expect(targets).toEqual(
expect.arrayContaining(
Object.entries(stores).map(([agentId, storePath]) => ({
agentId,
storePath,
})),
),
);
}
const discoveryResolvers = [
{
label: "async",
resolve: async (cfg: OpenClawConfig, env: NodeJS.ProcessEnv) =>
await resolveAllAgentSessionStoreTargets(cfg, { env }),
},
{
label: "sync",
resolve: async (cfg: OpenClawConfig, env: NodeJS.ProcessEnv) =>
resolveAllAgentSessionStoreTargetsSync(cfg, { env }),
},
] as const;
describe("resolveSessionStoreTargets", () => {
it("resolves all configured agent stores", () => {
const cfg: OpenClawConfig = {
@ -83,97 +135,39 @@ describe("resolveAllAgentSessionStoreTargets", () => {
it("includes discovered on-disk agent stores alongside configured targets", async () => {
await withTempHome(async (home) => {
const stateDir = path.join(home, ".openclaw");
const opsSessionsDir = path.join(stateDir, "agents", "ops", "sessions");
const retiredSessionsDir = path.join(stateDir, "agents", "retired", "sessions");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.mkdir(retiredSessionsDir, { recursive: true });
await fs.writeFile(path.join(opsSessionsDir, "sessions.json"), "{}", "utf8");
await fs.writeFile(path.join(retiredSessionsDir, "sessions.json"), "{}", "utf8");
const storePaths = await createAgentSessionStores(stateDir, ["ops", "retired"]);
const cfg: OpenClawConfig = {
agents: {
list: [{ id: "ops", default: true }],
},
};
const opsStorePath = await resolveRealStorePath(opsSessionsDir);
const retiredStorePath = await resolveRealStorePath(retiredSessionsDir);
const targets = await resolveAllAgentSessionStoreTargets(cfg, { env: process.env });
expect(targets).toEqual(
expect.arrayContaining([
{
agentId: "ops",
storePath: opsStorePath,
},
{
agentId: "retired",
storePath: retiredStorePath,
},
]),
);
expect(targets.filter((target) => target.storePath === opsStorePath)).toHaveLength(1);
expectTargetsToContainStores(targets, storePaths);
expect(targets.filter((target) => target.storePath === storePaths.ops)).toHaveLength(1);
});
});
it("discovers retired agent stores under a configured custom session root", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
const opsSessionsDir = path.join(customRoot, "agents", "ops", "sessions");
const retiredSessionsDir = path.join(customRoot, "agents", "retired", "sessions");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.mkdir(retiredSessionsDir, { recursive: true });
await fs.writeFile(path.join(opsSessionsDir, "sessions.json"), "{}", "utf8");
await fs.writeFile(path.join(retiredSessionsDir, "sessions.json"), "{}", "utf8");
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "ops", default: true }],
},
};
const opsStorePath = await resolveRealStorePath(opsSessionsDir);
const retiredStorePath = await resolveRealStorePath(retiredSessionsDir);
const storePaths = await createAgentSessionStores(customRoot, ["ops", "retired"]);
const cfg = createCustomRootCfg(customRoot);
const targets = await resolveAllAgentSessionStoreTargets(cfg, { env: process.env });
expect(targets).toEqual(
expect.arrayContaining([
{
agentId: "ops",
storePath: opsStorePath,
},
{
agentId: "retired",
storePath: retiredStorePath,
},
]),
);
expect(targets.filter((target) => target.storePath === opsStorePath)).toHaveLength(1);
expectTargetsToContainStores(targets, storePaths);
expect(targets.filter((target) => target.storePath === storePaths.ops)).toHaveLength(1);
});
});
it("keeps the actual on-disk store path for discovered retired agents", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
const opsSessionsDir = path.join(customRoot, "agents", "ops", "sessions");
const retiredSessionsDir = path.join(customRoot, "agents", "Retired Agent", "sessions");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.mkdir(retiredSessionsDir, { recursive: true });
await fs.writeFile(path.join(opsSessionsDir, "sessions.json"), "{}", "utf8");
await fs.writeFile(path.join(retiredSessionsDir, "sessions.json"), "{}", "utf8");
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "ops", default: true }],
},
};
const retiredStorePath = await resolveRealStorePath(retiredSessionsDir);
const storePaths = await createAgentSessionStores(customRoot, ["ops", "Retired Agent"]);
const cfg = createCustomRootCfg(customRoot);
const targets = await resolveAllAgentSessionStoreTargets(cfg, { env: process.env });
@ -181,7 +175,7 @@ describe("resolveAllAgentSessionStoreTargets", () => {
expect.arrayContaining([
expect.objectContaining({
agentId: "retired-agent",
storePath: retiredStorePath,
storePath: storePaths["Retired Agent"],
}),
]),
);
@ -223,73 +217,52 @@ describe("resolveAllAgentSessionStoreTargets", () => {
});
});
it("skips unreadable or invalid discovery roots when other roots are still readable", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
await fs.mkdir(customRoot, { recursive: true });
await fs.writeFile(path.join(customRoot, "agents"), "not-a-directory", "utf8");
for (const resolver of discoveryResolvers) {
it(`skips unreadable or invalid discovery roots when other roots are still readable (${resolver.label})`, async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
await fs.mkdir(customRoot, { recursive: true });
await fs.writeFile(path.join(customRoot, "agents"), "not-a-directory", "utf8");
const envStateDir = path.join(home, "env-state");
const mainSessionsDir = path.join(envStateDir, "agents", "main", "sessions");
const retiredSessionsDir = path.join(envStateDir, "agents", "retired", "sessions");
await fs.mkdir(mainSessionsDir, { recursive: true });
await fs.mkdir(retiredSessionsDir, { recursive: true });
await fs.writeFile(path.join(mainSessionsDir, "sessions.json"), "{}", "utf8");
await fs.writeFile(path.join(retiredSessionsDir, "sessions.json"), "{}", "utf8");
const envStateDir = path.join(home, "env-state");
const storePaths = await createAgentSessionStores(envStateDir, ["main", "retired"]);
const cfg = createCustomRootCfg(customRoot, "main");
const env = {
...process.env,
OPENCLAW_STATE_DIR: envStateDir,
};
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "main", default: true }],
},
};
const env = {
...process.env,
OPENCLAW_STATE_DIR: envStateDir,
};
const retiredStorePath = await resolveRealStorePath(retiredSessionsDir);
await expect(resolveAllAgentSessionStoreTargets(cfg, { env })).resolves.toEqual(
expect.arrayContaining([
{
agentId: "retired",
storePath: retiredStorePath,
},
]),
);
});
});
it("skips symlinked discovered stores under templated agents roots", async () => {
await withTempHome(async (home) => {
if (process.platform === "win32") {
return;
}
const customRoot = path.join(home, "custom-state");
const opsSessionsDir = path.join(customRoot, "agents", "ops", "sessions");
const leakedFile = path.join(home, "outside.json");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.writeFile(leakedFile, JSON.stringify({ leak: { secret: "x" } }), "utf8");
await fs.symlink(leakedFile, path.join(opsSessionsDir, "sessions.json"));
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "ops", default: true }],
},
};
const targets = await resolveAllAgentSessionStoreTargets(cfg, { env: process.env });
expect(targets).not.toContainEqual({
agentId: "ops",
storePath: expect.stringContaining(path.join("ops", "sessions", "sessions.json")),
await expect(resolver.resolve(cfg, env)).resolves.toEqual(
expect.arrayContaining([
{
agentId: "retired",
storePath: storePaths.retired,
},
]),
);
});
});
});
it(`skips symlinked discovered stores under templated agents roots (${resolver.label})`, async () => {
await withTempHome(async (home) => {
if (process.platform === "win32") {
return;
}
const customRoot = path.join(home, "custom-state");
const opsSessionsDir = path.join(customRoot, "agents", "ops", "sessions");
const leakedFile = path.join(home, "outside.json");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.writeFile(leakedFile, JSON.stringify({ leak: { secret: "x" } }), "utf8");
await fs.symlink(leakedFile, path.join(opsSessionsDir, "sessions.json"));
const targets = await resolver.resolve(createCustomRootCfg(customRoot), process.env);
expect(targets).not.toContainEqual({
agentId: "ops",
storePath: expect.stringContaining(path.join("ops", "sessions", "sessions.json")),
});
});
});
}
it("skips discovered directories that only normalize into the default main agent", async () => {
await withTempHome(async (home) => {
@ -315,73 +288,3 @@ describe("resolveAllAgentSessionStoreTargets", () => {
});
});
});
describe("resolveAllAgentSessionStoreTargetsSync", () => {
it("skips unreadable or invalid discovery roots when other roots are still readable", async () => {
await withTempHome(async (home) => {
const customRoot = path.join(home, "custom-state");
await fs.mkdir(customRoot, { recursive: true });
await fs.writeFile(path.join(customRoot, "agents"), "not-a-directory", "utf8");
const envStateDir = path.join(home, "env-state");
const mainSessionsDir = path.join(envStateDir, "agents", "main", "sessions");
const retiredSessionsDir = path.join(envStateDir, "agents", "retired", "sessions");
await fs.mkdir(mainSessionsDir, { recursive: true });
await fs.mkdir(retiredSessionsDir, { recursive: true });
await fs.writeFile(path.join(mainSessionsDir, "sessions.json"), "{}", "utf8");
await fs.writeFile(path.join(retiredSessionsDir, "sessions.json"), "{}", "utf8");
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "main", default: true }],
},
};
const env = {
...process.env,
OPENCLAW_STATE_DIR: envStateDir,
};
const retiredStorePath = await resolveRealStorePath(retiredSessionsDir);
expect(resolveAllAgentSessionStoreTargetsSync(cfg, { env })).toEqual(
expect.arrayContaining([
{
agentId: "retired",
storePath: retiredStorePath,
},
]),
);
});
});
it("skips symlinked discovered stores under templated agents roots", async () => {
await withTempHome(async (home) => {
if (process.platform === "win32") {
return;
}
const customRoot = path.join(home, "custom-state");
const opsSessionsDir = path.join(customRoot, "agents", "ops", "sessions");
const leakedFile = path.join(home, "outside.json");
await fs.mkdir(opsSessionsDir, { recursive: true });
await fs.writeFile(leakedFile, JSON.stringify({ leak: { secret: "x" } }), "utf8");
await fs.symlink(leakedFile, path.join(opsSessionsDir, "sessions.json"));
const cfg: OpenClawConfig = {
session: {
store: path.join(customRoot, "agents", "{agentId}", "sessions", "sessions.json"),
},
agents: {
list: [{ id: "ops", default: true }],
},
};
const targets = resolveAllAgentSessionStoreTargetsSync(cfg, { env: process.env });
expect(targets).not.toContainEqual({
agentId: "ops",
storePath: expect.stringContaining(path.join("ops", "sessions", "sessions.json")),
});
});
});
});