| import { DatabaseSync } from "node:sqlite"; |
| import { describe, expect, it } from "vitest"; |
| import { OPENCLAW_STATE_SCHEMA_VERSION } from "../state/openclaw-state-db-contract.js"; |
| import { resolveOpenClawStateSqlitePath } from "../state/openclaw-state-db.paths.js"; |
| import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; |
| import { |
| createPluginStateKeyedStore, |
| resetPluginStateStoreForTests, |
| } from "./plugin-state-store.js"; |
|
|
| describe("plugin state schema compatibility", () => { |
| it("cold-opens when an existing move table lacks its first-use column", async () => { |
| await withOpenClawTestState( |
| { label: "plugin-state-placement-move-column", applyEnv: false }, |
| async (state) => { |
| try { |
| const first = createPluginStateKeyedStore<{ owner: string }>("discord", { |
| namespace: "same-version-placement-move", |
| maxEntries: 10, |
| env: state.env, |
| }); |
| await first.register("first", { owner: "discord" }); |
| resetPluginStateStoreForTests(); |
|
|
| const databasePath = resolveOpenClawStateSqlitePath(state.env); |
| const previousDatabase = new DatabaseSync(databasePath); |
| let versionBefore: unknown; |
| let metadataBefore: unknown; |
| try { |
| versionBefore = previousDatabase.prepare("PRAGMA user_version").get(); |
| metadataBefore = previousDatabase |
| .prepare("SELECT * FROM schema_meta WHERE meta_key = 'primary'") |
| .get(); |
| expect(versionBefore).toEqual({ user_version: OPENCLAW_STATE_SCHEMA_VERSION }); |
| expect( |
| previousDatabase |
| .prepare( |
| "SELECT plugin_id, namespace, entry_key, value_json FROM plugin_state_entries", |
| ) |
| .all(), |
| ).toEqual([ |
| { |
| plugin_id: "discord", |
| namespace: "same-version-placement-move", |
| entry_key: "first", |
| value_json: JSON.stringify({ owner: "discord" }), |
| }, |
| ]); |
| expect( |
| previousDatabase |
| .prepare("SELECT COUNT(*) AS count FROM worker_session_placement_moves") |
| .get(), |
| ).toEqual({ count: 0 }); |
| const columnsBefore = previousDatabase |
| .prepare("PRAGMA table_info(worker_session_placement_moves)") |
| .all() |
| .map((column) => (column as { name: string }).name); |
| expect(columnsBefore).toContain("target_machine_class"); |
| previousDatabase.exec( |
| "ALTER TABLE worker_session_placement_moves DROP COLUMN target_machine_class;", |
| ); |
| expect( |
| previousDatabase |
| .prepare("PRAGMA table_info(worker_session_placement_moves)") |
| .all() |
| .map((column) => (column as { name: string }).name), |
| ).toEqual(columnsBefore.filter((column) => column !== "target_machine_class")); |
| } finally { |
| previousDatabase.close(); |
| } |
|
|
| const second = createPluginStateKeyedStore<{ owner: string }>("telegram", { |
| namespace: "same-version-placement-move", |
| maxEntries: 10, |
| env: state.env, |
| }); |
| await second.register("second", { owner: "telegram" }); |
| resetPluginStateStoreForTests(); |
|
|
| const reopenedDatabase = new DatabaseSync(databasePath); |
| try { |
| expect( |
| reopenedDatabase |
| .prepare( |
| `SELECT plugin_id, namespace, entry_key, value_json |
| FROM plugin_state_entries |
| ORDER BY plugin_id`, |
| ) |
| .all(), |
| ).toEqual([ |
| { |
| plugin_id: "discord", |
| namespace: "same-version-placement-move", |
| entry_key: "first", |
| value_json: JSON.stringify({ owner: "discord" }), |
| }, |
| { |
| plugin_id: "telegram", |
| namespace: "same-version-placement-move", |
| entry_key: "second", |
| value_json: JSON.stringify({ owner: "telegram" }), |
| }, |
| ]); |
| expect( |
| reopenedDatabase |
| .prepare("PRAGMA table_info(worker_session_placement_moves)") |
| .all() |
| .map((column) => (column as { name: string }).name), |
| ).not.toContain("target_machine_class"); |
| expect(reopenedDatabase.prepare("PRAGMA user_version").get()).toEqual(versionBefore); |
| expect( |
| reopenedDatabase |
| .prepare("SELECT * FROM schema_meta WHERE meta_key = 'primary'") |
| .get(), |
| ).toEqual(metadataBefore); |
| } finally { |
| reopenedDatabase.close(); |
| } |
| } finally { |
| resetPluginStateStoreForTests(); |
| } |
| }, |
| ); |
| }); |
| }); |
|
|