openclaw / src /plugin-state /plugin-state-store.expiry.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw History Blame Contribute Delete
9.17 kB
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import {
createOpenClawTestState,
type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import {
countPluginStateLiveEntries,
createPluginStateKeyedStore,
createPluginStateSyncKeyedStore,
resetPluginStateStoreForTests,
sweepExpiredPluginStateEntries,
} from "./plugin-state-store.js";
import {
clearPluginStateStoreForTests,
seedPluginStateEntriesForTests,
setMaxPluginStateEntriesPerPluginForTests,
} from "./plugin-state-store.test-helpers.js";
let testState: OpenClawTestState | undefined;
beforeAll(async () => {
testState = await createOpenClawTestState({ label: "plugin-state-expiry" });
});
beforeEach(() => {
testState?.applyEnv();
clearPluginStateStoreForTests();
});
afterEach(() => {
vi.useRealTimers();
setMaxPluginStateEntriesPerPluginForTests(undefined);
resetPluginStateStoreForTests({ closeDatabase: false });
});
afterAll(async () => {
resetPluginStateStoreForTests();
await testState?.cleanup();
});
describe("plugin state expiry cleanup", () => {
it.each(["sync", "async"] as const)(
"counts live %s rows without deleting expired rows",
async (mode) => {
const now = mode === "sync" ? 1_000 : Date.now();
if (mode === "sync") {
vi.useFakeTimers();
vi.setSystemTime(now);
}
const scope = { pluginId: "discord", namespace: "count-expiry" };
seedPluginStateEntriesForTests([
{ ...scope, key: "permanent", value: 1 },
{
...scope,
key: "future",
value: 2,
expiresAt: now + (mode === "sync" ? 200 : 86_400_000),
},
{ ...scope, key: "boundary", value: 3, expiresAt: now },
{ ...scope, key: "expired", value: 4, expiresAt: now - 1 },
{ ...scope, namespace: "sibling", key: "permanent", value: 5 },
{ ...scope, pluginId: "telegram", key: "permanent", value: 6 },
]);
const options = { namespace: scope.namespace, maxEntries: 10 };
const createStore =
mode === "sync" ? createPluginStateSyncKeyedStore : createPluginStateKeyedStore;
const store = createStore(scope.pluginId, options);
expect(await store.count()).toBe(2);
if (mode === "sync") {
vi.setSystemTime(now + 200);
} else {
seedPluginStateEntriesForTests([{ ...scope, key: "future", value: 2, expiresAt: now - 1 }]);
}
expect(await store.count()).toBe(1);
expect(sweepExpiredPluginStateEntries()).toBe(3);
},
);
it("rechecks expiry time and newly written rows after an empty namespace cleanup", () => {
vi.useFakeTimers();
vi.setSystemTime(1_000);
const scope = { pluginId: "discord", namespace: "fresh-expiry" };
const store = createPluginStateSyncKeyedStore(scope.pluginId, {
namespace: scope.namespace,
maxEntries: 10,
});
seedPluginStateEntriesForTests([{ ...scope, key: "future", value: 1, expiresAt: 1_200 }]);
store.register("permanent", 2);
expect(sweepExpiredPluginStateEntries()).toBe(0);
vi.setSystemTime(1_200);
store.register("permanent", 3);
expect(sweepExpiredPluginStateEntries()).toBe(0);
expect(store.lookup("future")).toBeUndefined();
seedPluginStateEntriesForTests([
{ ...scope, key: "new-expired", value: 4, expiresAt: 1_100 },
{ ...scope, namespace: "sibling", key: "expired", value: 5, expiresAt: 1_100 },
]);
store.register("permanent", 6);
expect(sweepExpiredPluginStateEntries()).toBe(1);
expect(store.entries()).toEqual([{ key: "permanent", value: 6, createdAt: 1_200 }]);
});
it("registerIfAbsent replaces an expired target beyond the namespace cleanup batch", async () => {
const now = Date.now();
seedPluginStateEntriesForTests([
...Array.from({ length: 1_025 }, (_, index) => ({
pluginId: "discord",
namespace: "claims-batched-expiry",
key: `expired-${String(index).padStart(4, "0")}`,
value: { index },
createdAt: index,
expiresAt: now - 100,
})),
{
pluginId: "discord",
namespace: "claims-batched-expiry",
key: "zz-target",
value: { version: 1 },
createdAt: 5_000,
expiresAt: now - 100,
},
]);
const store = createPluginStateKeyedStore<{ version: number }>("discord", {
namespace: "claims-batched-expiry",
maxEntries: 10,
});
await expect(store.registerIfAbsent("zz-target", { version: 2 })).resolves.toBe(true);
await expect(store.lookup("zz-target")).resolves.toEqual({ version: 2 });
expect(sweepExpiredPluginStateEntries()).toBe(1);
});
it("sweeps expired plugin state in bounded batches without touching live rows", async () => {
vi.useFakeTimers();
vi.setSystemTime(3_000);
seedPluginStateEntriesForTests([
...Array.from({ length: 2_050 }, (_, index) => ({
pluginId: index % 2 === 0 ? "discord" : "telegram",
namespace: "batched-expiry",
key: `expired-${index}`,
value: { index },
expiresAt: 1_000 + Math.floor(index / 2),
})),
{
pluginId: "discord",
namespace: "batched-expiry",
key: "permanent",
value: { durable: true },
},
{
pluginId: "discord",
namespace: "batched-expiry",
key: "live",
value: { live: true },
expiresAt: 4_000,
},
{
pluginId: "sibling-plugin",
namespace: "batched-expiry",
key: "permanent",
value: { sibling: true },
},
]);
expect(sweepExpiredPluginStateEntries()).toBe(1_024);
expect(sweepExpiredPluginStateEntries()).toBe(1_024);
expect(sweepExpiredPluginStateEntries()).toBe(2);
expect(sweepExpiredPluginStateEntries()).toBe(0);
const store = createPluginStateSyncKeyedStore("discord", {
namespace: "batched-expiry",
maxEntries: 10,
});
const sibling = createPluginStateSyncKeyedStore("sibling-plugin", {
namespace: "batched-expiry",
maxEntries: 10,
});
expect(store.lookup("permanent")).toEqual({ durable: true });
expect(store.lookup("live")).toEqual({ live: true });
expect(sibling.lookup("permanent")).toEqual({ sibling: true });
});
it.each(["register", "update"] as const)(
"bounds expired namespace cleanup during %s without touching sibling rows",
async (operation) => {
vi.useFakeTimers();
vi.setSystemTime(1_200);
seedPluginStateEntriesForTests([
...Array.from({ length: 1_031 }, (_, index) => ({
pluginId: "discord",
namespace: "namespace-batched-expiry",
key: `expired-${index}`,
value: { index },
expiresAt: 1_100,
})),
{
pluginId: "discord",
namespace: "namespace-batched-expiry",
key: "permanent",
value: { durable: true },
},
{
pluginId: "discord",
namespace: "sibling-namespace",
key: "expired",
value: { sibling: true },
expiresAt: 1_100,
},
{
pluginId: "sibling-plugin",
namespace: "namespace-batched-expiry",
key: "expired",
value: { sibling: true },
expiresAt: 1_100,
},
]);
const store = createPluginStateSyncKeyedStore<{ durable?: boolean; fresh?: boolean }>(
"discord",
{ namespace: "namespace-batched-expiry", maxEntries: 10 },
);
if (operation === "register") {
store.register("fresh", { fresh: true });
} else {
expect(store.update?.("fresh", () => ({ fresh: true }))).toBe(true);
}
expect(store.lookup("fresh")).toEqual({ fresh: true });
expect(store.lookup("permanent")).toEqual({ durable: true });
expect(sweepExpiredPluginStateEntries()).toBe(9);
},
);
it("rolls back bounded expiry cleanup when the enclosing namespace write fails", async () => {
const now = Date.now();
setMaxPluginStateEntriesPerPluginForTests(2);
seedPluginStateEntriesForTests([
...Array.from({ length: 1_031 }, (_, index) => ({
pluginId: "discord",
namespace: "rollback-expiry",
key: `expired-${index}`,
value: { index },
expiresAt: now - 100,
})),
{
pluginId: "discord",
namespace: "durable-sibling",
key: "first",
value: { durable: 1 },
},
{
pluginId: "discord",
namespace: "durable-sibling",
key: "second",
value: { durable: 2 },
},
]);
const store = createPluginStateKeyedStore("discord", {
namespace: "rollback-expiry",
maxEntries: 10,
});
await expect(store.register("fresh", { fresh: true })).rejects.toMatchObject({
code: "PLUGIN_STATE_LIMIT_EXCEEDED",
});
expect(sweepExpiredPluginStateEntries()).toBe(1_024);
expect(sweepExpiredPluginStateEntries()).toBe(7);
await expect(store.lookup("fresh")).resolves.toBeUndefined();
expect(countPluginStateLiveEntries("discord")).toBe(2);
});
});