| |
|
|
| 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(); |
| }); |
|
|
| |
| |
| |
| 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(); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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 }); |
|
|
| |
| |
| 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 }); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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 }); |
|
|
| |
| 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, |
| }, |
| ]); |
|
|
| |
| await expect(store.lookup("short")).resolves.toBeUndefined(); |
| await expect(store.lookup("long")).resolves.toEqual({ v: 2 }); |
|
|
| |
| const swept = sweepExpiredPluginStateEntries(); |
| expect(swept).toBe(1); |
|
|
| |
| const remaining = await store.entries(); |
| expect(remaining).toHaveLength(1); |
| expect(expectDefined(remaining[0], "remaining[0] test invariant").key).toBe("long"); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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" }); |
|
|
| |
| await pluginA.clear(); |
| await expect(pluginA.lookup("same")).resolves.toBeUndefined(); |
| await expect(pluginB.lookup("same")).resolves.toEqual({ owner: "b" }); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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, |
| }); |
| |
| 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(); |
| }); |
| }); |
| }); |
|
|
| |
| |
| |
| 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([]); |
|
|
| |
| 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 }); |
|
|
| |
| closePluginStateDatabase(); |
| await expect(store.lookup("k")).resolves.toEqual({ v: 1 }); |
|
|
| |
| closePluginStateDatabase(); |
| await expect(store.lookup("k")).resolves.toEqual({ v: 1 }); |
|
|
| |
| await store.register("k", { v: 2 }); |
| await expect(store.lookup("k")).resolves.toEqual({ v: 2 }); |
| }); |
| }); |
| }); |
|
|