diff --git a/src/auto-reply/reply/followup-runner.test.ts b/src/auto-reply/reply/followup-runner.test.ts index 305aceb1280..80c18095a34 100644 --- a/src/auto-reply/reply/followup-runner.test.ts +++ b/src/auto-reply/reply/followup-runner.test.ts @@ -20,9 +20,6 @@ let resolveQueuedReplyExecutionConfigActual: | undefined; let createFollowupRunner: typeof import("./followup-runner.js").createFollowupRunner; let clearRuntimeConfigSnapshot: typeof import("../../config/config.js").clearRuntimeConfigSnapshot; -let loadSessionStore: typeof import("../../config/sessions/store.js").loadSessionStore; -let saveSessionStore: typeof import("../../config/sessions/store.js").saveSessionStore; -let clearSessionStoreCacheForTest: typeof import("../../config/sessions/store.js").clearSessionStoreCacheForTest; let clearFollowupQueue: typeof import("./queue.js").clearFollowupQueue; let enqueueFollowupRun: typeof import("./queue.js").enqueueFollowupRun; let sessionRunAccounting: typeof import("./session-run-accounting.js"); @@ -37,7 +34,7 @@ const FOLLOWUP_TEST_QUEUES = new Map< lastRun?: FollowupRun["run"]; } >(); -const FOLLOWUP_TEST_SESSION_STORES = new Map>(); +const FOLLOWUP_TEST_SESSION_STORES = new Set>(); function debugFollowupTest(message: string): void { if (!FOLLOWUP_DEBUG) { @@ -105,10 +102,9 @@ function expectNoBlockReplyTextIncludes( } function registerFollowupTestSessionStore( - storePath: string, sessionStore: Record, ): void { - FOLLOWUP_TEST_SESSION_STORES.set(storePath, sessionStore); + FOLLOWUP_TEST_SESSION_STORES.add(sessionStore); } async function incrementRunCompactionCountForFollowupTest( @@ -261,12 +257,16 @@ function refreshQueuedFollowupSessionForFollowupTest(params: { async function persistRunSessionUsageForFollowupTest( params: Parameters[0], ): Promise { - const { storePath, sessionKey } = params; - if (!storePath || !sessionKey) { + const { sessionKey } = params; + if (!sessionKey) { + return; + } + const store = Array.from(FOLLOWUP_TEST_SESSION_STORES).find((candidate) => + Object.hasOwn(candidate, sessionKey), + ); + if (!store) { return; } - const registeredStore = FOLLOWUP_TEST_SESSION_STORES.get(storePath); - const store = registeredStore ?? loadSessionStore(storePath); const entry = store[sessionKey]; if (!entry) { return; @@ -294,10 +294,6 @@ async function persistRunSessionUsageForFollowupTest( nextEntry.totalTokens = promptTokens > 0 ? promptTokens : undefined; nextEntry.totalTokensFresh = promptTokens > 0; store[sessionKey] = nextEntry; - if (registeredStore) { - return; - } - await saveSessionStore(storePath, store); } async function loadFreshFollowupRunnerModuleForTest() { @@ -307,12 +303,6 @@ async function loadFreshFollowupRunnerModuleForTest() { "../../agents/model-fallback.js", async () => await import("../../test-utils/model-fallback.mock.js"), ); - vi.doMock("../../agents/session-write-lock.js", () => ({ - acquireSessionWriteLock: vi.fn(async () => ({ - release: async () => {}, - })), - resolveSessionLockMaxHoldFromTimeout: vi.fn(() => 1), - })); vi.doMock("../../agents/pi-embedded.js", () => ({ abortEmbeddedPiRun: vi.fn(async () => false), compactEmbeddedPiSession: (params: unknown) => compactEmbeddedPiSessionMock(params), @@ -400,8 +390,6 @@ async function loadFreshFollowupRunnerModuleForTest() { ({ createFollowupRunner } = await import("./followup-runner.js")); ({ clearRuntimeConfigSnapshot, setRuntimeConfigSnapshot } = await import("../../config/config.js")); - ({ clearSessionStoreCacheForTest, loadSessionStore, saveSessionStore } = - await import("../../config/sessions/store.js")); ({ clearFollowupQueue, enqueueFollowupRun } = await import("./queue.js")); sessionRunAccounting = await import("./session-run-accounting.js"); ({ createMockFollowupRun, createMockTypingController } = await import("./test-helpers.js")); @@ -465,7 +453,7 @@ afterEach(() => { FOLLOWUP_TEST_SESSION_STORES.clear(); vi.clearAllTimers(); vi.useRealTimers(); - clearSessionStoreCacheForTest(); + vi.unstubAllEnvs(); if (!FOLLOWUP_DEBUG) { return; } @@ -704,10 +692,6 @@ describe("createFollowupRunner runtime config", () => { describe("createFollowupRunner compaction", () => { it("adds verbose auto-compaction notice and tracks count", async () => { - const storePath = path.join( - await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-")), - "sessions.json", - ); const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now(), @@ -716,7 +700,7 @@ describe("createFollowupRunner compaction", () => { main: sessionEntry, }; const onBlockReply = vi.fn(async () => {}); - registerFollowupTestSessionStore(storePath, sessionStore); + registerFollowupTestSessionStore(sessionStore); mockCompactionRun({ willRetry: true, @@ -730,7 +714,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry, sessionStore, sessionKey: "main", - storePath, defaultModel: "anthropic/claude-opus-4-6", }); @@ -749,20 +732,17 @@ describe("createFollowupRunner compaction", () => { }); it("tracks auto-compaction from embedded result metadata even when no compaction event is emitted", async () => { - const storePath = path.join( - await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-meta-")), - "sessions.json", - ); + const sessionsDir = await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-meta-")); const sessionEntry: SessionEntry = { sessionId: "session", - sessionFile: path.join(path.dirname(storePath), "session.jsonl"), + sessionFile: path.join(sessionsDir, "session.jsonl"), updatedAt: Date.now(), }; const sessionStore: Record = { main: sessionEntry, }; const onBlockReply = vi.fn(async () => {}); - registerFollowupTestSessionStore(storePath, sessionStore); + registerFollowupTestSessionStore(sessionStore); runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], @@ -782,7 +762,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry, sessionStore, sessionKey: "main", - storePath, defaultModel: "anthropic/claude-opus-4-6", }); @@ -800,24 +779,21 @@ describe("createFollowupRunner compaction", () => { expect(sessionStore.main.compactionCount).toBe(2); expect(sessionStore.main.sessionId).toBe("session-rotated"); expect(await normalizeComparablePath(sessionStore.main.sessionFile ?? "")).toBe( - await normalizeComparablePath(path.join(path.dirname(storePath), "session-rotated.jsonl")), + await normalizeComparablePath(path.join(sessionsDir, "session-rotated.jsonl")), ); }); it("refreshes queued followup runs to the rotated transcript", async () => { - const storePath = path.join( - await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-queue-")), - "sessions.json", - ); + const sessionsDir = await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-queue-")); const sessionEntry: SessionEntry = { sessionId: "session", - sessionFile: path.join(path.dirname(storePath), "session.jsonl"), + sessionFile: path.join(sessionsDir, "session.jsonl"), updatedAt: Date.now(), }; const sessionStore: Record = { main: sessionEntry, }; - registerFollowupTestSessionStore(storePath, sessionStore); + registerFollowupTestSessionStore(sessionStore); runEmbeddedPiAgentMock.mockResolvedValueOnce({ payloads: [{ text: "final" }], @@ -837,7 +813,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry, sessionStore, sessionKey: "main", - storePath, defaultModel: "anthropic/claude-opus-4-6", }); @@ -845,7 +820,7 @@ describe("createFollowupRunner compaction", () => { prompt: "next", run: { sessionId: "session", - sessionFile: path.join(path.dirname(storePath), "session.jsonl"), + sessionFile: path.join(sessionsDir, "session.jsonl"), }, }); const queueSettings: QueueSettings = { mode: "queue" }; @@ -855,7 +830,7 @@ describe("createFollowupRunner compaction", () => { run: { verboseLevel: "on", sessionId: "session", - sessionFile: path.join(path.dirname(storePath), "session.jsonl"), + sessionFile: path.join(sessionsDir, "session.jsonl"), }, }); @@ -863,15 +838,11 @@ describe("createFollowupRunner compaction", () => { expect(queuedNext.run.sessionId).toBe("session-rotated"); expect(await normalizeComparablePath(queuedNext.run.sessionFile)).toBe( - await normalizeComparablePath(path.join(path.dirname(storePath), "session-rotated.jsonl")), + await normalizeComparablePath(path.join(sessionsDir, "session-rotated.jsonl")), ); }); it("does not count failed compaction end events in followup runs", async () => { - const storePath = path.join( - await fs.mkdtemp(path.join(tmpdir(), "openclaw-compaction-failed-")), - "sessions.json", - ); const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now(), @@ -880,7 +851,7 @@ describe("createFollowupRunner compaction", () => { main: sessionEntry, }; const onBlockReply = vi.fn(async () => {}); - registerFollowupTestSessionStore(storePath, sessionStore); + registerFollowupTestSessionStore(sessionStore); const runner = createFollowupRunner({ opts: { onBlockReply }, @@ -889,7 +860,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry, sessionStore, sessionKey: "main", - storePath, defaultModel: "anthropic/claude-opus-4-6", }); @@ -925,7 +895,6 @@ describe("createFollowupRunner compaction", () => { it("injects the post-compaction refresh prompt before followup runs after preflight compaction", async () => { const workspaceDir = await fs.mkdtemp(path.join(tmpdir(), "openclaw-preflight-followup-")); - const storePath = path.join(workspaceDir, "sessions.json"); const transcriptPath = path.join(workspaceDir, "session.jsonl"); await fs.writeFile( transcriptPath, @@ -961,7 +930,7 @@ describe("createFollowupRunner compaction", () => { const sessionStore: Record = { main: sessionEntry, }; - registerFollowupTestSessionStore(storePath, sessionStore); + registerFollowupTestSessionStore(sessionStore); compactEmbeddedPiSessionMock.mockResolvedValueOnce({ ok: true, @@ -979,7 +948,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry?: SessionEntry; sessionStore?: Record; sessionKey?: string; - storePath?: string; }) => { await compactEmbeddedPiSessionMock({ sessionFile: transcriptPath, @@ -1001,16 +969,6 @@ describe("createFollowupRunner compaction", () => { if (params.sessionKey && params.sessionStore) { params.sessionStore[params.sessionKey] = updatedEntry; } - if (params.storePath && params.sessionKey) { - const registeredStore = FOLLOWUP_TEST_SESSION_STORES.get(params.storePath); - if (registeredStore) { - registeredStore[params.sessionKey] = updatedEntry; - } else { - const store = loadSessionStore(params.storePath); - store[params.sessionKey] = updatedEntry; - await saveSessionStore(params.storePath, store); - } - } } return updatedEntry; }, @@ -1034,7 +992,6 @@ describe("createFollowupRunner compaction", () => { sessionEntry, sessionStore, sessionKey: "main", - storePath, defaultModel: "anthropic/claude-opus-4-6", agentCfgContextTokens: 100_000, }); @@ -1128,11 +1085,10 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry: SessionEntry; sessionStore: Record; sessionKey: string; - storePath: string; }> = {}, ) { - if (overrides.storePath && overrides.sessionStore) { - registerFollowupTestSessionStore(overrides.storePath, overrides.sessionStore); + if (overrides.sessionStore) { + registerFollowupTestSessionStore(overrides.sessionStore); } return createFollowupRunner({ opts: { onBlockReply }, @@ -1142,7 +1098,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry: overrides.sessionEntry, sessionStore: overrides.sessionStore, sessionKey: overrides.sessionKey, - storePath: overrides.storePath, }); } @@ -1153,7 +1108,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry: SessionEntry; sessionStore: Record; sessionKey: string; - storePath: string; }>; }) { const onBlockReply = createAsyncReplySpy(); @@ -1175,7 +1129,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { } it("persists usage even when replies are suppressed", async () => { - const storePath = "/tmp/openclaw-followup-usage.json"; const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; @@ -1212,14 +1165,12 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry, sessionStore, sessionKey, - storePath, }, queued: baseQueuedRun("slack"), }); expect(onBlockReply).not.toHaveBeenCalled(); const persistCall = requireMockCallArg(persistSpy, 0); - expect(persistCall.storePath).toBe(storePath); expect(persistCall.sessionKey).toBe(sessionKey); expect(persistCall.modelUsed).toBe("claude-opus-4-6"); expect(persistCall.providerUsed).toBe("anthropic"); @@ -1232,7 +1183,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { }); it("passes queued config into usage persistence during drained followups", async () => { - const storePath = "/tmp/openclaw-followup-usage-cfg.json"; const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; @@ -1262,7 +1212,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry, sessionStore, sessionKey, - storePath, }); await expect( @@ -1276,14 +1225,12 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { ).resolves.toBeUndefined(); const persistCall = requireMockCallArg(persistSpy, 0); - expect(persistCall.storePath).toBe(storePath); expect(persistCall.sessionKey).toBe(sessionKey); expect(persistCall.cfg).toBe(cfg); persistSpy.mockRestore(); }); it("uses providerUsed for snapshot freshness when agent metadata overrides the run provider", async () => { - const storePath = "/tmp/openclaw-followup-usage-provider.json"; const sessionKey = "main"; const sessionEntry: SessionEntry = { sessionId: "session", updatedAt: Date.now() }; const sessionStore: Record = { [sessionKey]: sessionEntry }; @@ -1308,7 +1255,6 @@ describe("createFollowupRunner messaging delivery and dedupe", () => { sessionEntry, sessionStore, sessionKey, - storePath, }); await expect(