mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 19:40:41 +00:00
149 lines
4.9 KiB
TypeScript
149 lines
4.9 KiB
TypeScript
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<string>(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<string | null>(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<string>(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<string[]> = 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<string> = 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<string | undefined> = 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");
|
|
});
|
|
});
|