openclaw / src /plugin-state /plugin-state-store.prepared.test.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
76289e7 verified
Raw
History Blame Contribute Delete
7.94 kB
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { getNodeSqliteKysely } from "../infra/kysely-sync.js";
import { closeOpenClawStateDatabaseByPath } from "../state/openclaw-state-db-cache.js";
import {
openOpenClawStateDatabase,
runOpenClawStateWriteTransaction,
} from "../state/openclaw-state-db.js";
import {
createOpenClawTestState,
type OpenClawTestState,
} from "../test-utils/openclaw-test-state.js";
import {
createPluginStateSyncKeyedStore,
resetPluginStateStoreForTests,
} from "./plugin-state-store.js";
import { lookupPluginStateEntry, registerPluginStateEntry } from "./plugin-state-store.kernel.js";
import { closePluginStateDatabase } from "./plugin-state-store.sqlite.js";
import {
clearPluginStateStoreForTests,
seedPluginStateEntriesForTests,
} from "./plugin-state-store.test-helpers.js";
let testState: OpenClawTestState;
beforeAll(async () => {
testState = await createOpenClawTestState({ label: "plugin-state-prepared" });
});
beforeEach(() => {
testState.applyEnv();
clearPluginStateStoreForTests();
});
afterEach(() => {
resetPluginStateStoreForTests();
});
afterAll(async () => {
await testState.cleanup();
});
describe("plugin state prepared queries", () => {
it("uses the supplied connection and keeps registration eviction in its owner's transaction", () => {
const scope = { pluginId: "discord", namespace: "owned-kernel" };
const defaultStore = createPluginStateSyncKeyedStore<string>(scope.pluginId, {
namespace: scope.namespace,
maxEntries: 1,
});
defaultStore.register("original", "default database");
const pathname = testState.statePath("kernel-owned.sqlite");
const database = openOpenClawStateDatabase({ path: pathname, env: testState.env });
const options = { database, env: testState.env };
const entry = { ...scope, maxEntries: 1, overflowPolicy: "evict-oldest" as const };
runOpenClawStateWriteTransaction(() => {
registerPluginStateEntry(database, { ...entry, key: "original", valueJson: '"owned"' }, 10);
}, options);
const aborted = new Error("abort the caller's transaction");
expect(() =>
runOpenClawStateWriteTransaction(() => {
registerPluginStateEntry(
database,
{ ...entry, key: "pending", valueJson: '"pending"' },
10,
);
expect(lookupPluginStateEntry(database, { ...scope, key: "pending" })).toBe("pending");
expect(lookupPluginStateEntry(database, { ...scope, key: "original" })).toBeUndefined();
throw aborted;
}, options),
).toThrow(aborted);
expect(lookupPluginStateEntry(database, { ...scope, key: "pending" })).toBeUndefined();
expect(lookupPluginStateEntry(database, { ...scope, key: "original" })).toBe("owned");
expect(defaultStore.lookup("original")).toBe("default database");
closeOpenClawStateDatabaseByPath(pathname);
const reopened = openOpenClawStateDatabase({ path: pathname, env: testState.env });
expect(lookupPluginStateEntry(reopened, { ...scope, key: "original" })).toBe("owned");
expect(lookupPluginStateEntry(reopened, { ...scope, key: "pending" })).toBeUndefined();
});
it("compiles exact reads once per connection with fresh scope and expiry bindings", () => {
const now = Date.now();
seedPluginStateEntriesForTests([
{ pluginId: "discord", namespace: "prepared", key: "first", value: 1, expiresAt: now + 100 },
{ pluginId: "discord", namespace: "prepared", key: "second", value: 2 },
{ pluginId: "telegram", namespace: "prepared", key: "first", value: 3 },
{ pluginId: "discord", namespace: "sibling", key: "first", value: 4 },
]);
const store = createPluginStateSyncKeyedStore<number>("discord", {
namespace: "prepared",
maxEntries: 10,
});
const pluginSibling = createPluginStateSyncKeyedStore<number>("telegram", {
namespace: "prepared",
maxEntries: 10,
});
const namespaceSibling = createPluginStateSyncKeyedStore<number>("discord", {
namespace: "sibling",
maxEntries: 10,
});
const clock = vi.spyOn(Date, "now").mockReturnValue(now);
try {
for (let connection = 0; connection < 2; connection++) {
closePluginStateDatabase();
const { db } = openOpenClawStateDatabase();
const compile = vi.spyOn(getNodeSqliteKysely(db).getExecutor(), "compileQuery");
try {
clock.mockReturnValue(now);
expect(store.lookup("first")).toBe(1);
expect(store.lookup("second")).toBe(2);
expect(pluginSibling.lookup("first")).toBe(3);
expect(namespaceSibling.lookup("first")).toBe(4);
expect(store.lookup("missing")).toBeUndefined();
clock.mockReturnValue(now + 100);
expect(store.lookup("first")).toBeUndefined();
expect(store.lookup("second")).toBe(2);
expect(compile).toHaveBeenCalledOnce();
} finally {
compile.mockRestore();
}
}
} finally {
clock.mockRestore();
}
});
it.each([
["register", "evict-oldest"],
["register", "reject-new"],
["registerIfAbsent", "evict-oldest"],
] as const)(
"reuses %s %s write and quota compilation with fresh bindings after reopening",
(operation, overflowPolicy) => {
const options = { namespace: "prepared-writes", maxEntries: 20, overflowPolicy };
const stores = [
createPluginStateSyncKeyedStore<string>("discord", options),
createPluginStateSyncKeyedStore<string>("telegram", options),
createPluginStateSyncKeyedStore<string>("discord", {
...options,
namespace: "prepared-sibling",
}),
];
const clock = vi.spyOn(Date, "now").mockReturnValue(10_000);
try {
for (let connection = 0; connection < 2; connection++) {
closePluginStateDatabase();
const { db } = openOpenClawStateDatabase();
const compile = vi.spyOn(getNodeSqliteKysely(db).getExecutor(), "compileQuery");
try {
const key = `round-${connection}`;
for (const [index, store] of stores.entries()) {
clock.mockReturnValue(10_000 + index);
store[operation](key, `value-${index}`, { ttlMs: 100 });
clock.mockReturnValue(10_010 + index);
store[operation](`${key}-durable`, `durable-${index}`);
const result = store[operation](key, `replacement-${index}`);
if (operation === "registerIfAbsent") {
expect(result).toBe(false);
}
const expected =
operation === "registerIfAbsent"
? {
key,
value: `value-${index}`,
createdAt: 10_000 + index,
expiresAt: 10_100 + index,
}
: { key, value: `replacement-${index}`, createdAt: 10_010 + index };
expect(store.entries().filter((entry) => entry.key.startsWith(key))).toEqual([
expected,
{ key: `${key}-durable`, value: `durable-${index}`, createdAt: 10_010 + index },
]);
}
const writes = compile.mock.results.filter(
(result) => result.type === "return" && result.value.sql.startsWith("insert"),
);
expect(writes).toHaveLength(1);
const counts = compile.mock.results.filter(
(result) =>
result.type === "return" &&
result.value.sql.startsWith(
'select count(*) as "count" from "plugin_state_entries"',
),
);
expect(counts).toHaveLength(2);
} finally {
compile.mockRestore();
}
}
} finally {
clock.mockRestore();
}
},
);
});