File size: 9,222 Bytes
76289e7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
// 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 });
    });
  });
});