import { existsSync, mkdirSync, readFileSync } from "node:fs"; import path from "node:path"; import { DatabaseSync } from "node:sqlite"; import { afterEach, describe, expect, it } from "vitest"; import { clearOpenClawDatabaseQuarantine, recordOpenClawDatabaseQuarantine, } from "../state/openclaw-quarantine-store.js"; import { closeOpenClawStateDatabaseByPath } from "../state/openclaw-state-db-cache.js"; import { OPENCLAW_STATE_SCHEMA_VERSION } from "../state/openclaw-state-db-contract.js"; import { clearOpenClawStateDatabaseOpenFailure, isOpenClawStateDatabaseOpen, openOpenClawStateDatabase, recordOpenClawStateDatabaseOpenFailure, } 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 { createPluginBlobStoreForTests, resetPluginBlobStoreForTests, } from "./plugin-blob-store.js"; afterEach(() => resetPluginBlobStoreForTests()); function createStore(env: NodeJS.ProcessEnv) { return createPluginBlobStoreForTests<{ version: number }>( "diffs", { namespace: "readonly", maxEntries: 3, maxBytesPerEntry: 16, maxBytesPerNamespace: 32 }, env, ); } describe("plugin blob read-only access", () => { it("returns empty reads without creating an absent database", async () => { await withOpenClawTestState({ label: "blob-read-absent", applyEnv: false }, async (state) => { const store = createStore(state.env); const databasePath = resolveOpenClawStateSqlitePath(state.env); await expect(store.lookup("missing")).resolves.toBeUndefined(); await expect(store.entries()).resolves.toEqual([]); expect(existsSync(path.dirname(databasePath))).toBe(false); expect(isOpenClawStateDatabaseOpen(databasePath)).toBe(false); }); }); it("reads committed blobs after close without reopening a writable owner", async () => { await withOpenClawTestState({ label: "blob-read-reopen", applyEnv: false }, async (state) => { const store = createStore(state.env); await store.register("saved", new Uint8Array([1, 2]), { version: 1 }); const databasePath = resolveOpenClawStateSqlitePath(state.env); expect(closeOpenClawStateDatabaseByPath(databasePath)).toBe(true); const entry = await store.lookup("saved"); expect(entry).toMatchObject({ metadata: { version: 1 }, bytes: new Uint8Array([1, 2]) }); entry!.bytes[0] = 9; await expect(store.lookup("saved")).resolves.toMatchObject({ bytes: new Uint8Array([1, 2]) }); await expect(store.entries()).resolves.toMatchObject([{ key: "saved", sizeBytes: 2 }]); expect(isOpenClawStateDatabaseOpen(databasePath)).toBe(false); }); }); it("keeps an active writer's uncommitted changes out of blob reads", async () => { await withOpenClawTestState( { label: "blob-read-transaction", applyEnv: false }, async (state) => { const store = createStore(state.env); await store.register("saved", new Uint8Array([1]), { version: 1 }); const { db } = openOpenClawStateDatabase({ env: state.env }); db.exec("BEGIN IMMEDIATE; DELETE FROM plugin_blob_entries;"); try { await expect(store.lookup("saved")).resolves.toMatchObject({ metadata: { version: 1 } }); await expect(store.entries()).resolves.toMatchObject([{ key: "saved" }]); } finally { db.exec("ROLLBACK"); } await expect(store.lookup("saved")).resolves.toMatchObject({ metadata: { version: 1 } }); }, ); }); it("leaves a checkpoint-only database unchanged when its blob table is absent", async () => { await withOpenClawTestState( { label: "blob-read-bootstrap", applyEnv: false }, async (state) => { const databasePath = resolveOpenClawStateSqlitePath(state.env); mkdirSync(path.dirname(databasePath), { recursive: true }); const db = new DatabaseSync(databasePath); db.exec(` CREATE TABLE schema_meta ( meta_key TEXT NOT NULL PRIMARY KEY, role TEXT NOT NULL, schema_version INTEGER NOT NULL, agent_id TEXT, app_version TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL ); CREATE TABLE state_leases ( scope TEXT NOT NULL, lease_key TEXT NOT NULL, owner TEXT NOT NULL, expires_at INTEGER, heartbeat_at INTEGER, payload_json TEXT, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, PRIMARY KEY (scope, lease_key) ); `); db.close(); const before = readFileSync(databasePath); const store = createStore(state.env); await expect(store.lookup("missing")).resolves.toBeUndefined(); await expect(store.entries()).resolves.toEqual([]); expect(readFileSync(databasePath)).toEqual(before); expect(isOpenClawStateDatabaseOpen(databasePath)).toBe(false); }, ); }); it("reports a missing initialized blob table as a read error without repairing it", async () => { await withOpenClawTestState({ label: "blob-read-damaged", applyEnv: false }, async (state) => { const store = createStore(state.env); await store.register("saved", new Uint8Array([1]), { version: 1 }); const databasePath = resolveOpenClawStateSqlitePath(state.env); openOpenClawStateDatabase({ env: state.env }).db.exec("DROP TABLE plugin_blob_entries"); closeOpenClawStateDatabaseByPath(databasePath); const before = readFileSync(databasePath); for (const operation of ["lookup", "entries"] as const) { await expect( operation === "lookup" ? store.lookup("saved") : store.entries(), ).rejects.toMatchObject({ code: "PLUGIN_BLOB_READ_FAILED", operation, path: databasePath, }); } expect(readFileSync(databasePath)).toEqual(before); expect(isOpenClawStateDatabaseOpen(databasePath)).toBe(false); }); }); it.each(["warm", "cold"])( "rejects a newer schema through %s acquisition", async (temperature) => { await withOpenClawTestState({ label: "blob-read-newer", applyEnv: false }, async (state) => { const store = createStore(state.env); await store.register("saved", new Uint8Array([1]), { version: 1 }); const { db, path: databasePath } = openOpenClawStateDatabase({ env: state.env }); db.exec(`PRAGMA user_version = ${OPENCLAW_STATE_SCHEMA_VERSION + 1};`); if (temperature === "cold") { closeOpenClawStateDatabaseByPath(databasePath); } for (const operation of ["lookup", "entries"] as const) { await expect( operation === "lookup" ? store.lookup("saved") : store.entries(), ).rejects.toMatchObject({ code: "PLUGIN_BLOB_OPEN_FAILED", operation, path: databasePath, }); } }); }, ); it("preserves process-local and persisted quarantine failures on cold reads", async () => { await withOpenClawTestState( { label: "blob-read-quarantine", applyEnv: false }, async (state) => { const store = createStore(state.env); await store.register("saved", new Uint8Array([1]), { version: 1 }); const databasePath = resolveOpenClawStateSqlitePath(state.env); closeOpenClawStateDatabaseByPath(databasePath); recordOpenClawStateDatabaseOpenFailure(databasePath, new Error("latched failure")); try { await expect(store.lookup("saved")).rejects.toMatchObject({ code: "PLUGIN_BLOB_OPEN_FAILED", }); await expect(store.entries()).rejects.toMatchObject({ code: "PLUGIN_BLOB_OPEN_FAILED" }); } finally { clearOpenClawStateDatabaseOpenFailure(databasePath); } expect( recordOpenClawDatabaseQuarantine({ env: state.env, kind: "state", path: databasePath, reason: "persisted failure", }), ).toBe(true); try { await expect(store.lookup("saved")).rejects.toMatchObject({ code: "PLUGIN_BLOB_OPEN_FAILED", }); await expect(store.entries()).rejects.toMatchObject({ code: "PLUGIN_BLOB_OPEN_FAILED" }); } finally { clearOpenClawStateDatabaseOpenFailure(databasePath); expect(clearOpenClawDatabaseQuarantine(databasePath, { env: state.env })).toBe(true); } }, ); }); });