import { describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { PluginLruCache, createConfigScopedPromiseLoader, resolveConfigScopedRuntimeCacheValue, type ConfigScopedRuntimeCache, } from "./plugin-cache-primitives.js"; describe("PluginLruCache", () => { it("evicts the least recently used entry", () => { const cache = new PluginLruCache(2); cache.set("", "empty"); cache.set("a", "alpha"); cache.set("b", "bravo"); expect(cache.get("a")).toBe("alpha"); cache.set("c", "charlie"); expect(cache.get("b")).toBeUndefined(); expect(cache.get("a")).toBe("alpha"); expect(cache.get("c")).toBe("charlie"); }); it("returns hit state for cached null values", () => { const cache = new PluginLruCache(2); cache.set("missing", null); expect(cache.getResult("missing")).toEqual({ hit: true, value: null }); expect(cache.getResult("unknown")).toEqual({ hit: false }); }); it("resizes and falls back to the default max entry count", () => { const cache = new PluginLruCache(2); cache.setMaxEntriesForTest(1.9); cache.set("a", "alpha"); cache.set("b", "bravo"); expect(cache.maxEntries).toBe(1); expect(cache.size).toBe(1); expect(cache.get("a")).toBeUndefined(); cache.setMaxEntriesForTest(); expect(cache.maxEntries).toBe(2); }); }); describe("resolveConfigScopedRuntimeCacheValue", () => { it("caches values by config object and key", () => { const cache: ConfigScopedRuntimeCache = new WeakMap(); const config = {} as OpenClawConfig; const load = vi.fn(() => ["loaded"]); expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "demo", load })).toEqual([ "loaded", ]); expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "demo", load })).toEqual([ "loaded", ]); expect(load).toHaveBeenCalledOnce(); }); it("does not cache values without a config owner", () => { const cache: ConfigScopedRuntimeCache = new WeakMap(); const load = vi.fn(() => "loaded"); expect(resolveConfigScopedRuntimeCacheValue({ cache, key: "demo", load })).toBe("loaded"); expect(resolveConfigScopedRuntimeCacheValue({ cache, key: "demo", load })).toBe("loaded"); expect(load).toHaveBeenCalledTimes(2); }); it("caches undefined values by key", () => { const cache: ConfigScopedRuntimeCache = new WeakMap(); const config = {} as OpenClawConfig; const load = vi.fn(() => undefined); expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "missing", load })).toBe( undefined, ); expect(resolveConfigScopedRuntimeCacheValue({ cache, config, key: "missing", load })).toBe( undefined, ); expect(load).toHaveBeenCalledOnce(); }); }); describe("createConfigScopedPromiseLoader", () => { it("dedupes concurrent default loads", async () => { let calls = 0; const loader = createConfigScopedPromiseLoader(async () => `loaded-${++calls}`); await expect(Promise.all([loader.load(), loader.load()])).resolves.toEqual([ "loaded-1", "loaded-1", ]); await expect(loader.load()).resolves.toBe("loaded-1"); expect(calls).toBe(1); }); it("caches loads by config object", async () => { const firstConfig = { plugins: { load: { disabled: true } } } as OpenClawConfig; const secondConfig = { plugins: { load: { disabled: false } } } as OpenClawConfig; const load = vi.fn(async (config?: OpenClawConfig) => config === firstConfig ? "first" : "second", ); const loader = createConfigScopedPromiseLoader(load); await expect(loader.load(firstConfig)).resolves.toBe("first"); await expect(loader.load(firstConfig)).resolves.toBe("first"); await expect(loader.load(secondConfig)).resolves.toBe("second"); expect(load).toHaveBeenCalledTimes(2); }); it("evicts rejected loads so retries can recover", async () => { const config = {} as OpenClawConfig; let calls = 0; const loader = createConfigScopedPromiseLoader(async () => { calls += 1; if (calls === 1) { throw new Error("transient"); } return "recovered"; }); await expect(loader.load(config)).rejects.toThrow("transient"); await expect(loader.load(config)).resolves.toBe("recovered"); expect(calls).toBe(2); }); it("clears default and config-scoped entries", async () => { const config = {} as OpenClawConfig; let calls = 0; const loader = createConfigScopedPromiseLoader( async (owner?: OpenClawConfig) => `${owner ? "config" : "default"}-${++calls}`, ); await expect(loader.load()).resolves.toBe("default-1"); await expect(loader.load(config)).resolves.toBe("config-2"); loader.clear(); await expect(loader.load()).resolves.toBe("default-3"); await expect(loader.load(config)).resolves.toBe("config-4"); }); });