diff --git a/src/agents/subagent-registry.persistence.resume.test.ts b/src/agents/subagent-registry.persistence.resume.test.ts index f789c815a79..f5a3b89d0bd 100644 --- a/src/agents/subagent-registry.persistence.resume.test.ts +++ b/src/agents/subagent-registry.persistence.resume.test.ts @@ -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>; - } - } catch { - // ignore - } - return {} as Record>; - }; - 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(); diff --git a/src/agents/subagent-registry.persistence.test-support.ts b/src/agents/subagent-registry.persistence.test-support.ts new file mode 100644 index 00000000000..035d4f4e7cc --- /dev/null +++ b/src/agents/subagent-registry.persistence.test-support.ts @@ -0,0 +1,74 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { vi } from "vitest"; + +type SessionStore = Record>; + +export function resolveSubagentSessionStorePath(stateDir: string, agentId: string): string { + return path.join(stateDir, "agents", agentId, "sessions", "sessions.json"); +} + +export async function readSubagentSessionStore(storePath: string): Promise { + 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 { + 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 { + 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 = {}, +): Record { + 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, + }; +} diff --git a/src/agents/subagent-registry.persistence.test.ts b/src/agents/subagent-registry.persistence.test.ts index 05fa09316d0..d98ac955f25 100644 --- a/src/agents/subagent-registry.persistence.test.ts +++ b/src/agents/subagent-registry.persistence.test.ts @@ -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>; - } - } catch { - // ignore - } - return {} as Record>; - }; - 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) => { @@ -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);