openclaw / src /plugin-state /plugin-state-store.persistence.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw
History Blame Contribute Delete
9.22 kB
// Plugin state store E2E tests cover persisted plugin state across runtime calls.
import { expectDefined } from "@openclaw/normalization-core";
import { afterEach, describe, expect, it, vi } from "vitest";
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import {
createPluginStateKeyedStore,
createPluginStateSyncKeyedStore,
resetPluginStateStoreForTests,
sweepExpiredPluginStateEntries,
} from "./plugin-state-store.js";
import { closePluginStateDatabase } from "./plugin-state-store.sqlite.js";
import {
probePluginStateStore,
seedPluginStateEntriesForTests,
} from "./plugin-state-store.test-helpers.js";
afterEach(() => {
vi.useRealTimers();
resetPluginStateStoreForTests();
});
// ---------------------------------------------------------------------------
// Runtime smoke
// ---------------------------------------------------------------------------
describe("runtime smoke", () => {
it("writes and reads a value", async () => {
await withOpenClawTestState({ label: "e2e-smoke-rw" }, async () => {
const store = createPluginStateKeyedStore<{ msg: string }>("fixture-plugin", {
namespace: "data",
maxEntries: 10,
});
await store.register("greeting", { msg: "hello" });
await expect(store.lookup("greeting")).resolves.toEqual({ msg: "hello" });
});
});
it("consumes a value exactly once", async () => {
await withOpenClawTestState({ label: "e2e-smoke-consume" }, async () => {
const store = createPluginStateKeyedStore<{ token: string }>("fixture-plugin", {
namespace: "tokens",
maxEntries: 10,
});
await store.register("one-shot", { token: "abc123" });
const first = await store.consume("one-shot");
expect(first).toEqual({ token: "abc123" });
const second = await store.consume("one-shot");
expect(second).toBeUndefined();
await expect(store.lookup("one-shot")).resolves.toBeUndefined();
});
});
});
// ---------------------------------------------------------------------------
// Persistence
// ---------------------------------------------------------------------------
describe("persistence", () => {
it("survives close and reopen of the store", async () => {
await withOpenClawTestState({ label: "e2e-persist" }, async () => {
const storeA = createPluginStateKeyedStore<{ persisted: boolean }>("fixture-plugin", {
namespace: "durable",
maxEntries: 10,
});
await storeA.register("key1", { persisted: true });
await storeA.register("key2", { persisted: true });
// Tear down the cached DB handle and option signatures – simulates
// a full gateway restart while the on-disk DB survives.
resetPluginStateStoreForTests();
const storeB = createPluginStateKeyedStore<{ persisted: boolean }>("fixture-plugin", {
namespace: "durable",
maxEntries: 10,
});
await expect(storeB.lookup("key1")).resolves.toEqual({ persisted: true });
await expect(storeB.lookup("key2")).resolves.toEqual({ persisted: true });
});
});
});
// ---------------------------------------------------------------------------
// TTL
// ---------------------------------------------------------------------------
describe("TTL", () => {
it("hides expired values and sweep removes the row", async () => {
await withOpenClawTestState({ label: "e2e-ttl" }, async () => {
const store = createPluginStateKeyedStore<{ v: number }>("fixture-plugin", {
namespace: "ttl-test",
maxEntries: 10,
});
await store.register("short", { v: 1 }, { ttlMs: 24 * 60 * 60_000 });
await store.register("long", { v: 2 }, { ttlMs: 48 * 60 * 60_000 });
// Before expiry – both visible.
await expect(store.lookup("short")).resolves.toEqual({ v: 1 });
await expect(store.lookup("long")).resolves.toEqual({ v: 2 });
seedPluginStateEntriesForTests([
{
pluginId: "fixture-plugin",
namespace: "ttl-test",
key: "short",
value: { v: 1 },
expiresAt: Date.now() - 1,
},
]);
// Expired value is invisible to reads.
await expect(store.lookup("short")).resolves.toBeUndefined();
await expect(store.lookup("long")).resolves.toEqual({ v: 2 });
// Sweep physically removes the expired row.
const swept = sweepExpiredPluginStateEntries();
expect(swept).toBe(1);
// After sweep the entry list contains only the long-lived record.
const remaining = await store.entries();
expect(remaining).toHaveLength(1);
expect(expectDefined(remaining[0], "remaining[0] test invariant").key).toBe("long");
});
});
});
// ---------------------------------------------------------------------------
// Isolation
// ---------------------------------------------------------------------------
describe("isolation", () => {
it("segregates plugins sharing namespace and key", async () => {
await withOpenClawTestState({ label: "e2e-isolation" }, async () => {
const pluginA = createPluginStateKeyedStore<{ owner: string }>("plugin-a", {
namespace: "x",
maxEntries: 10,
});
const pluginB = createPluginStateKeyedStore<{ owner: string }>("plugin-b", {
namespace: "x",
maxEntries: 10,
});
await pluginA.register("same", { owner: "a" });
await pluginB.register("same", { owner: "b" });
await expect(pluginA.lookup("same")).resolves.toEqual({ owner: "a" });
await expect(pluginB.lookup("same")).resolves.toEqual({ owner: "b" });
// Clearing one plugin's namespace does not affect the other.
await pluginA.clear();
await expect(pluginA.lookup("same")).resolves.toBeUndefined();
await expect(pluginB.lookup("same")).resolves.toEqual({ owner: "b" });
});
});
});
// ---------------------------------------------------------------------------
// Limits
// ---------------------------------------------------------------------------
describe("limits", () => {
it.each(["async", "sync"])("enforces the 1 MiB boundary across %s writes", async (mode) => {
await withOpenClawTestState({ label: "e2e-limit" }, async () => {
const createStore =
mode === "async"
? createPluginStateKeyedStore<string>
: createPluginStateSyncKeyedStore<string>;
const store = createStore("fixture-plugin", {
namespace: "size",
maxEntries: 10,
});
// JSON.stringify wraps a string in quotes (+2 bytes).
const boundary = "x".repeat(1_048_574);
const oversize = `${boundary}x`;
const update = expectDefined(store.update, "keyed store update support");
await store.register("registered", boundary);
expect(await store.registerIfAbsent("claimed", boundary)).toBe(true);
await store.register("updated", "before");
expect(await update("updated", () => boundary)).toBe(true);
for (const write of [
() => store.register("registered", oversize),
() => store.registerIfAbsent("rejected", oversize),
() => update("updated", () => oversize),
]) {
await expect(async () => {
await write();
}).rejects.toMatchObject({
code: "PLUGIN_STATE_LIMIT_EXCEEDED",
});
}
resetPluginStateStoreForTests();
for (const key of ["registered", "claimed", "updated"]) {
expect(await store.lookup(key)).toBe(boundary);
}
expect(await store.lookup("rejected")).toBeUndefined();
});
});
});
// ---------------------------------------------------------------------------
// Failure safety
// ---------------------------------------------------------------------------
describe("failure safety", () => {
it("probe returns redacted diagnostics without leaking stored values", async () => {
await withOpenClawTestState({ label: "e2e-fail-probe" }, async () => {
const result = probePluginStateStore();
expect(result.ok).toBe(true);
expect(result.databasePath).toContain("openclaw.sqlite");
expect(result.steps.length).toBeGreaterThanOrEqual(4);
const failedSteps = result.steps.filter((step) => !step.ok);
expect(failedSteps).toEqual([]);
// The probe's temporary stored value must not leak into the result.
const serialised = JSON.stringify(result);
expect(serialised).not.toContain("probe-value");
});
});
it("close and reopen cycle is clean", async () => {
await withOpenClawTestState({ label: "e2e-fail-reopen" }, async () => {
const store = createPluginStateKeyedStore<{ v: number }>("fixture-plugin", {
namespace: "reopen",
maxEntries: 10,
});
await store.register("k", { v: 1 });
// First close.
closePluginStateDatabase();
await expect(store.lookup("k")).resolves.toEqual({ v: 1 });
// Second close (idempotent).
closePluginStateDatabase();
await expect(store.lookup("k")).resolves.toEqual({ v: 1 });
// Write after reopen.
await store.register("k", { v: 2 });
await expect(store.lookup("k")).resolves.toEqual({ v: 2 });
});
});
});