test(agents): share subagent persistence fixtures

This commit is contained in:
Vincent Koc
2026-04-12 10:52:03 +01:00
parent 12db6dfc8d
commit f80a8e7b6c
3 changed files with 111 additions and 83 deletions

View File

@@ -8,6 +8,10 @@ import {
drainSessionStoreLockQueuesForTest,
} from "../config/sessions/store.js";
import { captureEnv } from "../test-utils/env.js";
import {
createSubagentRegistryTestDeps,
writeSubagentSessionEntry,
} from "./subagent-registry.persistence.test-support.js";
const hoisted = vi.hoisted(() => ({
announceSpy: vi.fn(async () => true),
@@ -71,22 +75,6 @@ describe("subagent registry persistence resume", () => {
const envSnapshot = captureEnv(["OPENCLAW_STATE_DIR"]);
let tempStateDir: string | null = null;
const resolveSessionStorePath = (stateDir: string, agentId: string) =>
path.join(stateDir, "agents", agentId, "sessions", "sessions.json");
const readSessionStore = async (storePath: string) => {
try {
const raw = await fs.readFile(storePath, "utf8");
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, Record<string, unknown>>;
}
} catch {
// ignore
}
return {} as Record<string, Record<string, unknown>>;
};
const writeChildSessionEntry = async (params: {
sessionKey: string;
sessionId?: string;
@@ -95,16 +83,14 @@ describe("subagent registry persistence resume", () => {
if (!tempStateDir) {
throw new Error("tempStateDir not initialized");
}
const storePath = resolveSessionStorePath(tempStateDir, "main");
const store = await readSessionStore(storePath);
store[params.sessionKey] = {
...store[params.sessionKey],
sessionId: params.sessionId ?? `sess-${Date.now()}`,
updatedAt: params.updatedAt ?? Date.now(),
};
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, `${JSON.stringify(store)}\n`, "utf8");
return storePath;
return await writeSubagentSessionEntry({
stateDir: tempStateDir,
agentId: "main",
sessionKey: params.sessionKey,
sessionId: params.sessionId,
updatedAt: params.updatedAt,
defaultSessionId: `sess-${Date.now()}`,
});
};
const flushQueuedRegistryWork = async () => {
@@ -129,19 +115,10 @@ describe("subagent registry persistence resume", () => {
endedAt: 222,
});
mod.__testing.setDepsForTest({
callGateway: vi.mocked(callGatewayModule.callGateway),
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
captureSubagentCompletionReply: vi.fn(async () => undefined),
ensureContextEnginesInitialized: vi.fn(),
ensureRuntimePluginsLoaded: vi.fn(),
loadConfig: vi.fn(() => ({})),
resolveAgentTimeoutMs: vi.fn(() => 100),
resolveContextEngine: vi.fn(async () => ({
info: { id: "test", name: "Test", version: "0.0.1" },
ingest: vi.fn(async () => ({ ingested: false })),
assemble: vi.fn(async ({ messages }) => ({ messages, estimatedTokens: 0 })),
compact: vi.fn(async () => ({ ok: false, compacted: false })),
})),
...createSubagentRegistryTestDeps({
callGateway: vi.mocked(callGatewayModule.callGateway),
captureSubagentCompletionReply: vi.fn(async () => undefined),
}),
});
mod.resetSubagentRegistryForTests({ persist: false });
vi.mocked(agentEventsModule.onAgentEvent).mockReset();

View File

@@ -0,0 +1,74 @@
import fs from "node:fs/promises";
import path from "node:path";
import { vi } from "vitest";
type SessionStore = Record<string, Record<string, unknown>>;
export function resolveSubagentSessionStorePath(stateDir: string, agentId: string): string {
return path.join(stateDir, "agents", agentId, "sessions", "sessions.json");
}
export async function readSubagentSessionStore(storePath: string): Promise<SessionStore> {
try {
const raw = await fs.readFile(storePath, "utf8");
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as SessionStore;
}
} catch {
// ignore
}
return {};
}
export async function writeSubagentSessionEntry(params: {
stateDir: string;
sessionKey: string;
sessionId?: string;
updatedAt?: number;
agentId: string;
defaultSessionId: string;
}): Promise<string> {
const storePath = resolveSubagentSessionStorePath(params.stateDir, params.agentId);
const store = await readSubagentSessionStore(storePath);
store[params.sessionKey] = {
...store[params.sessionKey],
sessionId: params.sessionId ?? params.defaultSessionId,
updatedAt: params.updatedAt ?? Date.now(),
};
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, `${JSON.stringify(store)}\n`, "utf8");
return storePath;
}
export async function removeSubagentSessionEntry(params: {
stateDir: string;
sessionKey: string;
agentId: string;
}): Promise<string> {
const storePath = resolveSubagentSessionStorePath(params.stateDir, params.agentId);
const store = await readSubagentSessionStore(storePath);
delete store[params.sessionKey];
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, `${JSON.stringify(store)}\n`, "utf8");
return storePath;
}
export function createSubagentRegistryTestDeps(
extra: Record<string, unknown> = {},
): Record<string, unknown> {
return {
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
ensureContextEnginesInitialized: vi.fn(),
ensureRuntimePluginsLoaded: vi.fn(),
loadConfig: vi.fn(() => ({})),
resolveAgentTimeoutMs: vi.fn(() => 100),
resolveContextEngine: vi.fn(async () => ({
info: { id: "test", name: "Test", version: "0.0.1" },
ingest: vi.fn(async () => ({ ingested: false })),
assemble: vi.fn(async ({ messages }) => ({ messages, estimatedTokens: 0 })),
compact: vi.fn(async () => ({ ok: false, compacted: false })),
})),
...extra,
};
}

View File

@@ -22,6 +22,12 @@ import {
registerSubagentRun,
resetSubagentRegistryForTests,
} from "./subagent-registry.js";
import {
createSubagentRegistryTestDeps,
readSubagentSessionStore,
removeSubagentSessionEntry,
writeSubagentSessionEntry,
} from "./subagent-registry.persistence.test-support.js";
import {
loadSubagentRegistryFromDisk,
resolveSubagentRegistryPath,
@@ -47,22 +53,6 @@ describe("subagent registry persistence", () => {
return (match?.[1] ?? "main").trim().toLowerCase() || "main";
};
const resolveSessionStorePath = (stateDir: string, agentId: string) =>
path.join(stateDir, "agents", agentId, "sessions", "sessions.json");
const readSessionStore = async (storePath: string) => {
try {
const raw = await fs.readFile(storePath, "utf8");
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
return parsed as Record<string, Record<string, unknown>>;
}
} catch {
// ignore
}
return {} as Record<string, Record<string, unknown>>;
};
const writeChildSessionEntry = async (params: {
sessionKey: string;
sessionId?: string;
@@ -72,16 +62,14 @@ describe("subagent registry persistence", () => {
throw new Error("tempStateDir not initialized");
}
const agentId = resolveAgentIdFromSessionKey(params.sessionKey);
const storePath = resolveSessionStorePath(tempStateDir, agentId);
const store = await readSessionStore(storePath);
store[params.sessionKey] = {
...store[params.sessionKey],
sessionId: params.sessionId ?? `sess-${agentId}-${Date.now()}`,
updatedAt: params.updatedAt ?? Date.now(),
};
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, `${JSON.stringify(store)}\n`, "utf8");
return storePath;
return await writeSubagentSessionEntry({
stateDir: tempStateDir,
agentId,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
updatedAt: params.updatedAt,
defaultSessionId: `sess-${agentId}-${Date.now()}`,
});
};
const removeChildSessionEntry = async (sessionKey: string) => {
@@ -89,12 +77,11 @@ describe("subagent registry persistence", () => {
throw new Error("tempStateDir not initialized");
}
const agentId = resolveAgentIdFromSessionKey(sessionKey);
const storePath = resolveSessionStorePath(tempStateDir, agentId);
const store = await readSessionStore(storePath);
delete store[sessionKey];
await fs.mkdir(path.dirname(storePath), { recursive: true });
await fs.writeFile(storePath, `${JSON.stringify(store)}\n`, "utf8");
return storePath;
return await removeSubagentSessionEntry({
stateDir: tempStateDir,
agentId,
sessionKey,
});
};
const seedChildSessionsForPersistedRuns = async (persisted: Record<string, unknown>) => {
@@ -181,17 +168,7 @@ describe("subagent registry persistence", () => {
beforeEach(() => {
__testing.setDepsForTest({
cleanupBrowserSessionsForLifecycleEnd: vi.fn(async () => {}),
ensureContextEnginesInitialized: vi.fn(),
ensureRuntimePluginsLoaded: vi.fn(),
loadConfig: vi.fn(() => ({})),
resolveAgentTimeoutMs: vi.fn(() => 100),
resolveContextEngine: vi.fn(async () => ({
info: { id: "test", name: "Test", version: "0.0.1" },
ingest: vi.fn(async () => ({ ingested: false })),
assemble: vi.fn(async ({ messages }) => ({ messages, estimatedTokens: 0 })),
compact: vi.fn(async () => ({ ok: false, compacted: false })),
})),
...createSubagentRegistryTestDeps(),
runSubagentAnnounceFlow: announceSpy,
});
vi.mocked(callGateway).mockReset();
@@ -245,7 +222,7 @@ describe("subagent registry persistence", () => {
outcome: { status: "ok" },
} as never);
const store = await readSessionStore(storePath);
const store = await readSubagentSessionStore(storePath);
const persisted = store["agent:main:subagent:timing"];
expect(persisted?.endedAt).toBe(endedAt);
expect(persisted?.runtimeMs).toBe(500);