import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearSessionStoreCacheForTest, loadSessionStore, type SessionEntry, saveSessionStore, } from "./sessions.js"; describe("Session Store Cache", () => { let testDir: string; let storePath: string; beforeEach(() => { // Create a temporary directory for test testDir = path.join(os.tmpdir(), `session-cache-test-${Date.now()}`); fs.mkdirSync(testDir, { recursive: true }); storePath = path.join(testDir, "sessions.json"); // Clear cache before each test clearSessionStoreCacheForTest(); // Reset environment variable delete process.env.CLAWDBOT_SESSION_CACHE_TTL_MS; }); afterEach(() => { // Clean up test directory if (fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); } clearSessionStoreCacheForTest(); delete process.env.CLAWDBOT_SESSION_CACHE_TTL_MS; }); it("should load session store from disk on first call", async () => { const testStore: Record = { "session:1": { sessionId: "id-1", updatedAt: Date.now(), displayName: "Test Session 1", }, }; // Write test data await saveSessionStore(storePath, testStore); // Load it const loaded = loadSessionStore(storePath); expect(loaded).toEqual(testStore); }); it("should cache session store on first load when file is unchanged", async () => { const testStore: Record = { "session:1": { sessionId: "id-1", updatedAt: Date.now(), displayName: "Test Session 1", }, }; await saveSessionStore(storePath, testStore); const readSpy = vi.spyOn(fs, "readFileSync"); // First load - from disk const loaded1 = loadSessionStore(storePath); expect(loaded1).toEqual(testStore); // Second load - should return cached data (no extra disk read) const loaded2 = loadSessionStore(storePath); expect(loaded2).toEqual(testStore); expect(readSpy).toHaveBeenCalledTimes(1); readSpy.mockRestore(); }); it("should refresh cache when store file changes on disk", async () => { const testStore: Record = { "session:1": { sessionId: "id-1", updatedAt: Date.now(), displayName: "Test Session 1", }, }; await saveSessionStore(storePath, testStore); // First load - from disk const loaded1 = loadSessionStore(storePath); expect(loaded1).toEqual(testStore); // Modify file on disk while cache is valid const modifiedStore: Record = { "session:99": { sessionId: "id-99", updatedAt: Date.now() }, }; fs.writeFileSync(storePath, JSON.stringify(modifiedStore, null, 2)); const bump = new Date(Date.now() + 2000); fs.utimesSync(storePath, bump, bump); // Second load - should return the updated store const loaded2 = loadSessionStore(storePath); expect(loaded2).toEqual(modifiedStore); }); it("should invalidate cache on write", async () => { const testStore: Record = { "session:1": { sessionId: "id-1", updatedAt: Date.now(), displayName: "Test Session 1", }, }; await saveSessionStore(storePath, testStore); // Load - should cache const loaded1 = loadSessionStore(storePath); expect(loaded1).toEqual(testStore); // Update store const updatedStore: Record = { "session:1": { ...testStore["session:1"], displayName: "Updated Session 1", }, }; // Save - should invalidate cache await saveSessionStore(storePath, updatedStore); // Load again - should get new data from disk const loaded2 = loadSessionStore(storePath); expect(loaded2["session:1"].displayName).toBe("Updated Session 1"); }); it("should respect CLAWDBOT_SESSION_CACHE_TTL_MS=0 to disable cache", async () => { process.env.CLAWDBOT_SESSION_CACHE_TTL_MS = "0"; clearSessionStoreCacheForTest(); const testStore: Record = { "session:1": { sessionId: "id-1", updatedAt: Date.now(), displayName: "Test Session 1", }, }; await saveSessionStore(storePath, testStore); // First load const loaded1 = loadSessionStore(storePath); expect(loaded1).toEqual(testStore); // Modify file on disk const modifiedStore: Record = { "session:2": { sessionId: "id-2", updatedAt: Date.now(), displayName: "Test Session 2", }, }; fs.writeFileSync(storePath, JSON.stringify(modifiedStore, null, 2)); // Second load - should read from disk (cache disabled) const loaded2 = loadSessionStore(storePath); expect(loaded2).toEqual(modifiedStore); // Should be modified, not cached }); it("should handle non-existent store gracefully", () => { const nonExistentPath = path.join(testDir, "non-existent.json"); // Should return empty store const loaded = loadSessionStore(nonExistentPath); expect(loaded).toEqual({}); }); it("should handle invalid JSON gracefully", async () => { // Write invalid JSON fs.writeFileSync(storePath, "not valid json {"); // Should return empty store const loaded = loadSessionStore(storePath); expect(loaded).toEqual({}); }); });