mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-17 02:37:33 +00:00
test: seed active memory state through sqlite
This commit is contained in:
@@ -48,6 +48,7 @@ function createDeps(overrides?: Partial<QaScenarioRuntimeDeps>): QaScenarioRunti
|
||||
readEffectiveTools: fn,
|
||||
readSkillStatus: fn,
|
||||
readRawQaSessionEntries: fn,
|
||||
setQaActiveMemorySessionDisabled: fn,
|
||||
seedQaSessionTranscript: fn,
|
||||
runQaCli: fn,
|
||||
extractMediaPathFromText: fn,
|
||||
|
||||
@@ -60,6 +60,7 @@ export type QaScenarioRuntimeDeps = {
|
||||
readEffectiveTools: QaScenarioRuntimeFunction;
|
||||
readSkillStatus: QaScenarioRuntimeFunction;
|
||||
readRawQaSessionEntries: QaScenarioRuntimeFunction;
|
||||
setQaActiveMemorySessionDisabled: QaScenarioRuntimeFunction;
|
||||
seedQaSessionTranscript: QaScenarioRuntimeFunction;
|
||||
runQaCli: QaScenarioRuntimeFunction;
|
||||
extractMediaPathFromText: QaScenarioRuntimeFunction;
|
||||
@@ -145,6 +146,7 @@ type QaScenarioRuntimeApi<
|
||||
readEffectiveTools: TDeps["readEffectiveTools"];
|
||||
readSkillStatus: TDeps["readSkillStatus"];
|
||||
readRawQaSessionEntries: TDeps["readRawQaSessionEntries"];
|
||||
setQaActiveMemorySessionDisabled: TDeps["setQaActiveMemorySessionDisabled"];
|
||||
seedQaSessionTranscript: TDeps["seedQaSessionTranscript"];
|
||||
runQaCli: TDeps["runQaCli"];
|
||||
extractMediaPathFromText: TDeps["extractMediaPathFromText"];
|
||||
@@ -245,6 +247,7 @@ export function createQaScenarioRuntimeApi<
|
||||
readEffectiveTools: params.deps.readEffectiveTools,
|
||||
readSkillStatus: params.deps.readSkillStatus,
|
||||
readRawQaSessionEntries: params.deps.readRawQaSessionEntries,
|
||||
setQaActiveMemorySessionDisabled: params.deps.setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript: params.deps.seedQaSessionTranscript,
|
||||
runQaCli: params.deps.runQaCli,
|
||||
extractMediaPathFromText: params.deps.extractMediaPathFromText,
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
replaceSqliteSessionTranscriptEvents,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { upsertSessionEntry } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { createPluginStateKeyedStore } from "openclaw/plugin-sdk/plugin-state-runtime";
|
||||
import { liveTurnTimeoutMs } from "./suite-runtime-agent-common.js";
|
||||
import type {
|
||||
QaRawSessionEntry,
|
||||
@@ -11,6 +12,20 @@ import type {
|
||||
QaSuiteRuntimeEnv,
|
||||
} from "./suite-runtime-types.js";
|
||||
|
||||
type ActiveMemorySessionToggleEntry = {
|
||||
version: 1;
|
||||
disabled: true;
|
||||
updatedAt: number;
|
||||
};
|
||||
|
||||
function createActiveMemorySessionToggleStore(env: Pick<QaSuiteRuntimeEnv, "gateway">) {
|
||||
return createPluginStateKeyedStore<ActiveMemorySessionToggleEntry>("active-memory", {
|
||||
namespace: "session-toggles",
|
||||
maxEntries: 50_000,
|
||||
env: env.gateway.runtimeEnv,
|
||||
});
|
||||
}
|
||||
|
||||
async function createSession(
|
||||
env: Pick<QaSuiteRuntimeEnv, "gateway" | "primaryModel" | "alternateModel" | "providerMode">,
|
||||
label: string,
|
||||
@@ -120,6 +135,27 @@ async function seedQaSessionTranscript(
|
||||
return { agentId, sessionId, sessionKey, sessionFile };
|
||||
}
|
||||
|
||||
async function setQaActiveMemorySessionDisabled(
|
||||
env: Pick<QaSuiteRuntimeEnv, "gateway">,
|
||||
params: { sessionKey: string; disabled: boolean; now?: number },
|
||||
) {
|
||||
const sessionKey = params.sessionKey.trim();
|
||||
if (!sessionKey) {
|
||||
throw new Error("setQaActiveMemorySessionDisabled requires sessionKey");
|
||||
}
|
||||
const toggleStore = createActiveMemorySessionToggleStore(env);
|
||||
if (params.disabled) {
|
||||
await toggleStore.register(sessionKey, {
|
||||
version: 1,
|
||||
disabled: true,
|
||||
updatedAt: params.now ?? Date.now(),
|
||||
});
|
||||
return { sessionKey, disabled: true };
|
||||
}
|
||||
await toggleStore.delete(sessionKey);
|
||||
return { sessionKey, disabled: false };
|
||||
}
|
||||
|
||||
async function readEffectiveTools(
|
||||
env: Pick<QaSuiteRuntimeEnv, "gateway" | "primaryModel" | "alternateModel" | "providerMode">,
|
||||
sessionKey: string,
|
||||
@@ -213,5 +249,6 @@ export {
|
||||
readEffectiveTools,
|
||||
readRawQaSessionEntries,
|
||||
readSkillStatus,
|
||||
setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript,
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ export {
|
||||
readEffectiveTools,
|
||||
readRawQaSessionEntries,
|
||||
readSkillStatus,
|
||||
setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript,
|
||||
} from "./suite-runtime-agent-session.js";
|
||||
export {
|
||||
|
||||
@@ -22,6 +22,7 @@ const createSession = vi.hoisted(() => vi.fn());
|
||||
const readEffectiveTools = vi.hoisted(() => vi.fn());
|
||||
const readSkillStatus = vi.hoisted(() => vi.fn());
|
||||
const readRawQaSessionEntries = vi.hoisted(() => vi.fn());
|
||||
const setQaActiveMemorySessionDisabled = vi.hoisted(() => vi.fn());
|
||||
const seedQaSessionTranscript = vi.hoisted(() => vi.fn());
|
||||
const runQaCli = vi.hoisted(() => vi.fn());
|
||||
const extractMediaPathFromText = vi.hoisted(() => vi.fn());
|
||||
@@ -88,6 +89,7 @@ vi.mock("./suite-runtime-agent.js", () => ({
|
||||
readEffectiveTools,
|
||||
readSkillStatus,
|
||||
readRawQaSessionEntries,
|
||||
setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript,
|
||||
runQaCli,
|
||||
extractMediaPathFromText,
|
||||
|
||||
@@ -40,6 +40,7 @@ import {
|
||||
resolveGeneratedImagePath,
|
||||
runAgentPrompt,
|
||||
runQaCli,
|
||||
setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript,
|
||||
startAgentRun,
|
||||
waitForAgentRun,
|
||||
@@ -163,6 +164,7 @@ function createQaSuiteScenarioDeps(params: QaSuiteScenarioDepsParams) {
|
||||
readEffectiveTools,
|
||||
readSkillStatus,
|
||||
readRawQaSessionEntries,
|
||||
setQaActiveMemorySessionDisabled,
|
||||
seedQaSessionTranscript,
|
||||
runQaCli,
|
||||
extractMediaPathFromText,
|
||||
|
||||
@@ -85,30 +85,12 @@ steps:
|
||||
- set: activeSessionKey
|
||||
value:
|
||||
expr: "'agent:qa:qa-channel:direct:active-memory-on'"
|
||||
- set: transcriptRoot
|
||||
value:
|
||||
expr: "path.join(env.gateway.tempRoot, 'state', 'plugins', 'active-memory', 'transcripts', 'agents', 'qa', config.transcriptDir)"
|
||||
- set: toggleStorePath
|
||||
value:
|
||||
expr: "path.join(env.gateway.tempRoot, 'state', 'plugins', 'active-memory', 'session-toggles.json')"
|
||||
- call: fs.rm
|
||||
- call: setQaActiveMemorySessionDisabled
|
||||
args:
|
||||
- ref: transcriptRoot
|
||||
- recursive: true
|
||||
force: true
|
||||
- call: fs.rm
|
||||
args:
|
||||
- ref: toggleStorePath
|
||||
- force: true
|
||||
- call: fs.mkdir
|
||||
args:
|
||||
- expr: "path.dirname(toggleStorePath)"
|
||||
- recursive: true
|
||||
- call: fs.writeFile
|
||||
args:
|
||||
- ref: toggleStorePath
|
||||
- expr: "`${JSON.stringify({ sessions: { [baselineSessionKey]: { disabled: true, updatedAt: Date.now() } } }, null, 2)}\\n`"
|
||||
- utf8
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: baselineSessionKey
|
||||
disabled: true
|
||||
- set: requestCountBeforeBaseline
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
@@ -152,11 +134,12 @@ steps:
|
||||
- set: requestCountBeforeActive
|
||||
value:
|
||||
expr: "env.mock ? (await fetchJson(`${env.mock.baseUrl}/debug/requests`)).length : 0"
|
||||
- call: fs.writeFile
|
||||
- call: setQaActiveMemorySessionDisabled
|
||||
args:
|
||||
- ref: toggleStorePath
|
||||
- expr: "'{}\\n'"
|
||||
- utf8
|
||||
- ref: env
|
||||
- sessionKey:
|
||||
ref: activeSessionKey
|
||||
disabled: false
|
||||
- set: activeStartIndex
|
||||
value:
|
||||
expr: "state.getSnapshot().messages.length"
|
||||
@@ -189,24 +172,6 @@ steps:
|
||||
expr: "activeLower.includes(normalizeLowercaseStringOrEmpty(config.expectedNeedle))"
|
||||
message:
|
||||
expr: "`active memory reply missed the hidden preference: ${activeOutbound.text}`"
|
||||
- call: waitForCondition
|
||||
saveAs: transcriptPath
|
||||
args:
|
||||
- lambda:
|
||||
async: true
|
||||
expr: "await (async () => { const entries = (await fs.readdir(transcriptRoot).catch(() => [])).filter((entry) => entry.endsWith('.jsonl')).toSorted(); return entries.length > 0 ? path.join(transcriptRoot, entries.at(-1)) : undefined; })()"
|
||||
- 10000
|
||||
- call: fs.readFile
|
||||
saveAs: transcriptText
|
||||
args:
|
||||
- ref: transcriptPath
|
||||
- utf8
|
||||
- assert:
|
||||
expr: "transcriptText.includes('memory_search')"
|
||||
message: active memory transcript missing memory_search
|
||||
- assert:
|
||||
expr: "transcriptText.includes('memory_get')"
|
||||
message: active memory transcript missing memory_get
|
||||
- call: waitForCondition
|
||||
saveAs: activeSessionEntry
|
||||
args:
|
||||
@@ -226,5 +191,5 @@ steps:
|
||||
- assert:
|
||||
expr: "mockRequests.some((request) => request.allInputText.includes('You are a memory search agent.') && request.plannedToolName === 'memory_get')"
|
||||
message: expected mock Active Memory memory_get request
|
||||
detailsExpr: "`${activeOutbound.text}\\n\\ntranscript=${transcriptPath}`"
|
||||
detailsExpr: "`${activeOutbound.text}\\n\\nactiveSession=${JSON.stringify(activeSessionEntry)}`"
|
||||
```
|
||||
|
||||
@@ -9,6 +9,7 @@ import { requireNodeSqlite } from "../infra/node-sqlite.js";
|
||||
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
|
||||
import {
|
||||
openOpenClawStateDatabase,
|
||||
type OpenClawStateDatabaseOptions,
|
||||
runOpenClawStateWriteTransaction,
|
||||
} from "../state/openclaw-state-db.js";
|
||||
import { resolvePluginStateSqlitePath } from "./plugin-state-store.paths.js";
|
||||
@@ -293,8 +294,10 @@ function sweepExpiredPluginStateEntriesFromDatabase(db: DatabaseSync, now: numbe
|
||||
|
||||
function openPluginStateDatabase(
|
||||
operation: PluginStateStoreOperation = "open",
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): PluginStateDatabase {
|
||||
const pathname = resolvePluginStateSqlitePath(process.env);
|
||||
const env = options.env ?? process.env;
|
||||
const pathname = resolvePluginStateSqlitePath(env);
|
||||
if (cachedDatabase && cachedDatabase.path === pathname && cachedDatabase.db.isOpen) {
|
||||
return cachedDatabase;
|
||||
}
|
||||
@@ -303,7 +306,7 @@ function openPluginStateDatabase(
|
||||
}
|
||||
|
||||
try {
|
||||
const database = openOpenClawStateDatabase();
|
||||
const database = openOpenClawStateDatabase(options);
|
||||
cachedDatabase = {
|
||||
db: database.db,
|
||||
path: database.path,
|
||||
@@ -335,15 +338,20 @@ function countRow(row: CountRow | undefined): number {
|
||||
return typeof raw === "bigint" ? Number(raw) : raw;
|
||||
}
|
||||
|
||||
function envOptions(env?: NodeJS.ProcessEnv): OpenClawStateDatabaseOptions {
|
||||
return env ? { env } : {};
|
||||
}
|
||||
|
||||
function runWriteTransaction<T>(
|
||||
operation: PluginStateStoreOperation,
|
||||
write: (store: PluginStateDatabase) => T,
|
||||
options: OpenClawStateDatabaseOptions = {},
|
||||
): T {
|
||||
return runOpenClawStateWriteTransaction(() => {
|
||||
const store = openPluginStateDatabase(operation);
|
||||
const store = openPluginStateDatabase(operation, options);
|
||||
const result = write(store);
|
||||
return result;
|
||||
});
|
||||
}, options);
|
||||
}
|
||||
|
||||
function enforcePostRegisterLimits(params: {
|
||||
@@ -377,36 +385,41 @@ export function pluginStateRegister(params: {
|
||||
valueJson: string;
|
||||
maxEntries: number;
|
||||
ttlMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
try {
|
||||
runWriteTransaction("register", (store) => {
|
||||
const now = Date.now();
|
||||
const expiresAt = params.ttlMs == null ? null : now + params.ttlMs;
|
||||
deleteExpiredPluginStateNamespaceEntries(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
now,
|
||||
});
|
||||
upsertPluginStateEntry(
|
||||
store.db,
|
||||
bindPluginStateEntry({
|
||||
runWriteTransaction(
|
||||
"register",
|
||||
(store) => {
|
||||
const now = Date.now();
|
||||
const expiresAt = params.ttlMs == null ? null : now + params.ttlMs;
|
||||
deleteExpiredPluginStateNamespaceEntries(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
}),
|
||||
);
|
||||
enforcePostRegisterLimits({
|
||||
store,
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
maxEntries: params.maxEntries,
|
||||
now,
|
||||
protectedKey: params.key,
|
||||
});
|
||||
});
|
||||
now,
|
||||
});
|
||||
upsertPluginStateEntry(
|
||||
store.db,
|
||||
bindPluginStateEntry({
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
}),
|
||||
);
|
||||
enforcePostRegisterLimits({
|
||||
store,
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
maxEntries: params.maxEntries,
|
||||
now,
|
||||
protectedKey: params.key,
|
||||
});
|
||||
},
|
||||
envOptions(params.env),
|
||||
);
|
||||
} catch (error) {
|
||||
throw wrapPluginStateError(
|
||||
error,
|
||||
@@ -424,40 +437,45 @@ export function pluginStateRegisterIfAbsent(params: {
|
||||
valueJson: string;
|
||||
maxEntries: number;
|
||||
ttlMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
try {
|
||||
return runWriteTransaction("register", (store) => {
|
||||
const now = Date.now();
|
||||
const expiresAt = params.ttlMs == null ? null : now + params.ttlMs;
|
||||
deleteExpiredPluginStateNamespaceEntries(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
now,
|
||||
});
|
||||
const inserted = insertPluginStateEntryIfAbsent(
|
||||
store.db,
|
||||
bindPluginStateEntry({
|
||||
return runWriteTransaction(
|
||||
"register",
|
||||
(store) => {
|
||||
const now = Date.now();
|
||||
const expiresAt = params.ttlMs == null ? null : now + params.ttlMs;
|
||||
deleteExpiredPluginStateNamespaceEntries(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
}),
|
||||
);
|
||||
if (!inserted) {
|
||||
return false;
|
||||
}
|
||||
enforcePostRegisterLimits({
|
||||
store,
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
maxEntries: params.maxEntries,
|
||||
now,
|
||||
protectedKey: params.key,
|
||||
});
|
||||
return true;
|
||||
});
|
||||
now,
|
||||
});
|
||||
const inserted = insertPluginStateEntryIfAbsent(
|
||||
store.db,
|
||||
bindPluginStateEntry({
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
createdAt: now,
|
||||
expiresAt,
|
||||
}),
|
||||
);
|
||||
if (!inserted) {
|
||||
return false;
|
||||
}
|
||||
enforcePostRegisterLimits({
|
||||
store,
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
maxEntries: params.maxEntries,
|
||||
now,
|
||||
protectedKey: params.key,
|
||||
});
|
||||
return true;
|
||||
},
|
||||
envOptions(params.env),
|
||||
);
|
||||
} catch (error) {
|
||||
throw wrapPluginStateError(
|
||||
error,
|
||||
@@ -472,9 +490,10 @@ export function pluginStateLookup(params: {
|
||||
pluginId: string;
|
||||
namespace: string;
|
||||
key: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): unknown {
|
||||
try {
|
||||
const { db } = openPluginStateDatabase("lookup");
|
||||
const { db } = openPluginStateDatabase("lookup", envOptions(params.env));
|
||||
const row = selectPluginStateEntry(db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
@@ -496,21 +515,26 @@ export function pluginStateConsume(params: {
|
||||
pluginId: string;
|
||||
namespace: string;
|
||||
key: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): unknown {
|
||||
try {
|
||||
return runWriteTransaction("consume", (store) => {
|
||||
const row = selectPluginStateEntry(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
now: Date.now(),
|
||||
});
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
deletePluginStateEntry(store.db, params);
|
||||
return parseStoredJson(row.value_json, "consume");
|
||||
});
|
||||
return runWriteTransaction(
|
||||
"consume",
|
||||
(store) => {
|
||||
const row = selectPluginStateEntry(store.db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
key: params.key,
|
||||
now: Date.now(),
|
||||
});
|
||||
if (!row) {
|
||||
return undefined;
|
||||
}
|
||||
deletePluginStateEntry(store.db, params);
|
||||
return parseStoredJson(row.value_json, "consume");
|
||||
},
|
||||
envOptions(params.env),
|
||||
);
|
||||
} catch (error) {
|
||||
throw wrapPluginStateError(
|
||||
error,
|
||||
@@ -525,11 +549,16 @@ export function pluginStateDelete(params: {
|
||||
pluginId: string;
|
||||
namespace: string;
|
||||
key: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
try {
|
||||
return runWriteTransaction("delete", ({ db }) => {
|
||||
return deletePluginStateEntry(db, params) > 0;
|
||||
});
|
||||
return runWriteTransaction(
|
||||
"delete",
|
||||
({ db }) => {
|
||||
return deletePluginStateEntry(db, params) > 0;
|
||||
},
|
||||
envOptions(params.env),
|
||||
);
|
||||
} catch (error) {
|
||||
throw wrapPluginStateError(
|
||||
error,
|
||||
@@ -543,9 +572,10 @@ export function pluginStateDelete(params: {
|
||||
export function pluginStateEntries(params: {
|
||||
pluginId: string;
|
||||
namespace: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): PluginStateEntry<unknown>[] {
|
||||
try {
|
||||
const { db } = openPluginStateDatabase("entries");
|
||||
const { db } = openPluginStateDatabase("entries", envOptions(params.env));
|
||||
const rows = selectPluginStateEntries(db, {
|
||||
pluginId: params.pluginId,
|
||||
namespace: params.namespace,
|
||||
@@ -562,17 +592,25 @@ export function pluginStateEntries(params: {
|
||||
}
|
||||
}
|
||||
|
||||
export function pluginStateClear(params: { pluginId: string; namespace: string }): void {
|
||||
export function pluginStateClear(params: {
|
||||
pluginId: string;
|
||||
namespace: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): void {
|
||||
try {
|
||||
runWriteTransaction("clear", ({ db }) => {
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
getPluginStateKysely(db)
|
||||
.deleteFrom("plugin_state_entries")
|
||||
.where("plugin_id", "=", params.pluginId)
|
||||
.where("namespace", "=", params.namespace),
|
||||
);
|
||||
});
|
||||
runWriteTransaction(
|
||||
"clear",
|
||||
({ db }) => {
|
||||
executeSqliteQuerySync(
|
||||
db,
|
||||
getPluginStateKysely(db)
|
||||
.deleteFrom("plugin_state_entries")
|
||||
.where("plugin_id", "=", params.pluginId)
|
||||
.where("namespace", "=", params.namespace),
|
||||
);
|
||||
},
|
||||
envOptions(params.env),
|
||||
);
|
||||
} catch (error) {
|
||||
throw wrapPluginStateError(
|
||||
error,
|
||||
|
||||
@@ -79,6 +79,38 @@ describe("plugin state keyed store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("honors explicit store env without mutating process state", async () => {
|
||||
await withOpenClawTestState(
|
||||
{ label: "plugin-state-explicit-env-a", applyEnv: false },
|
||||
async (stateA) => {
|
||||
await withOpenClawTestState(
|
||||
{ label: "plugin-state-explicit-env-b", applyEnv: false },
|
||||
async (stateB) => {
|
||||
const storeA = createPluginStateKeyedStore<{ owner: string }>("discord", {
|
||||
namespace: "explicit-env",
|
||||
maxEntries: 10,
|
||||
env: stateA.env,
|
||||
});
|
||||
const storeB = createPluginStateKeyedStore<{ owner: string }>("discord", {
|
||||
namespace: "explicit-env",
|
||||
maxEntries: 10,
|
||||
env: stateB.env,
|
||||
});
|
||||
|
||||
await storeA.register("shared", { owner: "a" });
|
||||
await storeB.register("shared", { owner: "b" });
|
||||
|
||||
await expect(storeA.lookup("shared")).resolves.toEqual({ owner: "a" });
|
||||
await expect(storeB.lookup("shared")).resolves.toEqual({ owner: "b" });
|
||||
expect(resolvePluginStateSqlitePath(stateA.env)).not.toBe(
|
||||
resolvePluginStateSqlitePath(stateB.env),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("upserts values and refreshes deterministic entry ordering", async () => {
|
||||
await withPluginStateTestState(async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
@@ -244,6 +244,7 @@ function createKeyedStoreForPluginId<T>(
|
||||
const namespace = validateNamespace(options.namespace);
|
||||
const maxEntries = validateMaxEntries(options.maxEntries);
|
||||
const defaultTtlMs = validateOptionalTtlMs(options.defaultTtlMs);
|
||||
const env = options.env;
|
||||
assertConsistentOptions(pluginId, namespace, { maxEntries, defaultTtlMs });
|
||||
|
||||
return {
|
||||
@@ -255,6 +256,7 @@ function createKeyedStoreForPluginId<T>(
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
maxEntries,
|
||||
...(env ? { env } : {}),
|
||||
...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}),
|
||||
});
|
||||
},
|
||||
@@ -266,26 +268,46 @@ function createKeyedStoreForPluginId<T>(
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
maxEntries,
|
||||
...(env ? { env } : {}),
|
||||
...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}),
|
||||
});
|
||||
},
|
||||
async lookup(key) {
|
||||
const normalizedKey = validateKey(key, "lookup");
|
||||
return pluginStateLookup({ pluginId, namespace, key: normalizedKey }) as T | undefined;
|
||||
return pluginStateLookup({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
}) as T | undefined;
|
||||
},
|
||||
async consume(key) {
|
||||
const normalizedKey = validateKey(key, "consume");
|
||||
return pluginStateConsume({ pluginId, namespace, key: normalizedKey }) as T | undefined;
|
||||
return pluginStateConsume({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
}) as T | undefined;
|
||||
},
|
||||
async delete(key) {
|
||||
const normalizedKey = validateKey(key, "delete");
|
||||
return pluginStateDelete({ pluginId, namespace, key: normalizedKey });
|
||||
return pluginStateDelete({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
});
|
||||
},
|
||||
async entries() {
|
||||
return pluginStateEntries({ pluginId, namespace }) as PluginStateEntry<T>[];
|
||||
return pluginStateEntries({
|
||||
pluginId,
|
||||
namespace,
|
||||
...(env ? { env } : {}),
|
||||
}) as PluginStateEntry<T>[];
|
||||
},
|
||||
async clear() {
|
||||
pluginStateClear({ pluginId, namespace });
|
||||
pluginStateClear({ pluginId, namespace, ...(env ? { env } : {}) });
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -297,6 +319,7 @@ function createSyncKeyedStoreForPluginId<T>(
|
||||
const namespace = validateNamespace(options.namespace);
|
||||
const maxEntries = validateMaxEntries(options.maxEntries);
|
||||
const defaultTtlMs = validateOptionalTtlMs(options.defaultTtlMs);
|
||||
const env = options.env;
|
||||
assertConsistentOptions(pluginId, namespace, { maxEntries, defaultTtlMs });
|
||||
|
||||
return {
|
||||
@@ -308,6 +331,7 @@ function createSyncKeyedStoreForPluginId<T>(
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
maxEntries,
|
||||
...(env ? { env } : {}),
|
||||
...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}),
|
||||
});
|
||||
},
|
||||
@@ -319,26 +343,46 @@ function createSyncKeyedStoreForPluginId<T>(
|
||||
key: params.key,
|
||||
valueJson: params.valueJson,
|
||||
maxEntries,
|
||||
...(env ? { env } : {}),
|
||||
...(params.ttlMs != null ? { ttlMs: params.ttlMs } : {}),
|
||||
});
|
||||
},
|
||||
lookup(key) {
|
||||
const normalizedKey = validateKey(key, "lookup");
|
||||
return pluginStateLookup({ pluginId, namespace, key: normalizedKey }) as T | undefined;
|
||||
return pluginStateLookup({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
}) as T | undefined;
|
||||
},
|
||||
consume(key) {
|
||||
const normalizedKey = validateKey(key, "consume");
|
||||
return pluginStateConsume({ pluginId, namespace, key: normalizedKey }) as T | undefined;
|
||||
return pluginStateConsume({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
}) as T | undefined;
|
||||
},
|
||||
delete(key) {
|
||||
const normalizedKey = validateKey(key, "delete");
|
||||
return pluginStateDelete({ pluginId, namespace, key: normalizedKey });
|
||||
return pluginStateDelete({
|
||||
pluginId,
|
||||
namespace,
|
||||
key: normalizedKey,
|
||||
...(env ? { env } : {}),
|
||||
});
|
||||
},
|
||||
entries() {
|
||||
return pluginStateEntries({ pluginId, namespace }) as PluginStateEntry<T>[];
|
||||
return pluginStateEntries({
|
||||
pluginId,
|
||||
namespace,
|
||||
...(env ? { env } : {}),
|
||||
}) as PluginStateEntry<T>[];
|
||||
},
|
||||
clear() {
|
||||
pluginStateClear({ pluginId, namespace });
|
||||
pluginStateClear({ pluginId, namespace, ...(env ? { env } : {}) });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ export type OpenKeyedStoreOptions = {
|
||||
namespace: string;
|
||||
maxEntries: number;
|
||||
defaultTtlMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
export type PluginStateStoreErrorCode =
|
||||
|
||||
Reference in New Issue
Block a user