File size: 4,317 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
// Fresh-store reads remain empty after checkpoint bootstrap; missing canonical tables stay errors.
import { DatabaseSync } from "node:sqlite";
import { afterEach, describe, expect, it } from "vitest";
import { OPENCLAW_STATE_SCHEMA_VERSION } from "../state/openclaw-state-db-contract.js";
import { withOpenClawStateStartupMigrationCheckpointDatabase } from "../state/openclaw-state-db.js";
import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js";
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import {
  countPluginStateLiveEntries,
  createPluginStateKeyedStore,
  pluginStateEntriesInKeyRange,
  resetPluginStateStoreForTests,
} from "./plugin-state-store.js";
import { PluginStateStoreError } from "./plugin-state-store.types.js";

afterEach(() => {
  resetPluginStateStoreForTests({ closeDatabase: false });
});

async function expectPluginStateReadFailure(
  promise: Promise<unknown>,
  expected: { operation: "entries" | "lookup" | "count"; path: string },
): Promise<void> {
  let storeError: unknown;
  try {
    await promise;
  } catch (error) {
    storeError = error;
  }
  expect(storeError).toBeInstanceOf(PluginStateStoreError);
  expect(storeError).toMatchObject({
    code: "PLUGIN_STATE_READ_FAILED",
    operation: expected.operation,
    path: expected.path,
  });
}

describe("plugin state fresh-store reads", () => {
  it("initializes an empty canonical plugin-state store before startup checkpoint reads", async () => {
    await withOpenClawTestState(
      { label: "plugin-state-read-only-table-missing", applyEnv: false },
      async (state) => {
        const databasePath = resolveOpenClawStateSqlitePath(state.env);
        withOpenClawStateStartupMigrationCheckpointDatabase(() => undefined, { env: state.env });

        const store = createPluginStateKeyedStore("discord", {
          namespace: "read-only-table-missing",
          maxEntries: 10,
          env: state.env,
        });

        await expect(store.lookup("k")).resolves.toBeUndefined();
        await expect(store.lookupMany(["k"])).resolves.toEqual([{ ok: true, value: undefined }]);
        await expect(store.entries()).resolves.toEqual([]);
        await expect(store.count()).resolves.toBe(0);
        expect(
          await pluginStateEntriesInKeyRange({
            pluginId: "discord",
            namespace: "read-only-table-missing",
            keyStartInclusive: "a",
            keyEndExclusive: "z",
            limit: 1,
            env: state.env,
          }),
        ).toEqual([]);
        expect(countPluginStateLiveEntries("discord", state.env)).toBe(0);

        const verify = new DatabaseSync(databasePath, { readOnly: true });
        try {
          expect(verify.prepare("PRAGMA user_version").get()).toEqual({
            user_version: OPENCLAW_STATE_SCHEMA_VERSION,
          });
          expect(
            verify
              .prepare("SELECT name FROM sqlite_schema WHERE name = 'plugin_state_entries'")
              .get(),
          ).toEqual({ name: "plugin_state_entries" });
        } finally {
          verify.close();
        }
      },
    );
  });

  it("rejects a missing plugin-state table after another state table has been initialized", async () => {
    await withOpenClawTestState(
      { label: "plugin-state-read-only-table-damaged", applyEnv: false },
      async (state) => {
        const databasePath = resolveOpenClawStateSqlitePath(state.env);
        withOpenClawStateStartupMigrationCheckpointDatabase(() => undefined, { env: state.env });
        const database = new DatabaseSync(databasePath);
        database.exec("DROP TABLE plugin_state_entries");
        database.close();

        const store = createPluginStateKeyedStore("discord", {
          namespace: "read-only-table-damaged",
          maxEntries: 10,
          env: state.env,
        });

        await expectPluginStateReadFailure(store.lookup("k"), {
          operation: "lookup",
          path: databasePath,
        });
        await expectPluginStateReadFailure(store.entries(), {
          operation: "entries",
          path: databasePath,
        });
        await expectPluginStateReadFailure(store.count(), {
          operation: "count",
          path: databasePath,
        });
      },
    );
  });
});