mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-03 08:24:07 +00:00
fix: preserve sqlite session compat rows
This commit is contained in:
@@ -3,12 +3,23 @@ import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
|
||||
import {
|
||||
getSessionEntry,
|
||||
loadSessionStore,
|
||||
readSessionUpdatedAt,
|
||||
saveSessionStore,
|
||||
updateSessionStore,
|
||||
upsertSessionEntry,
|
||||
} from "./session-store-runtime.js";
|
||||
|
||||
describe("session-store-runtime compatibility", () => {
|
||||
function canonicalStorePath(stateDir: string): string {
|
||||
return path.join(stateDir, "agents", "main", "sessions", "sessions.json");
|
||||
}
|
||||
|
||||
function testEnv(stateDir: string): NodeJS.ProcessEnv {
|
||||
return { ...process.env, OPENCLAW_STATE_DIR: stateDir };
|
||||
}
|
||||
|
||||
it("rejects custom store paths instead of falling back to the default agent", async () => {
|
||||
await withOpenClawTestState(
|
||||
{
|
||||
@@ -42,4 +53,97 @@ describe("session-store-runtime compatibility", () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("does not delete rows created after a compatibility snapshot was loaded", async () => {
|
||||
await withOpenClawTestState(
|
||||
{
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-session-store-compat-",
|
||||
scenario: "minimal",
|
||||
},
|
||||
async (state) => {
|
||||
const env = testEnv(state.stateDir);
|
||||
const storePath = canonicalStorePath(state.stateDir);
|
||||
const staleSnapshot = loadSessionStore(storePath);
|
||||
|
||||
upsertSessionEntry({
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionKey: "agent:main:concurrent",
|
||||
entry: {
|
||||
sessionId: "concurrent-session",
|
||||
updatedAt: 200,
|
||||
sessionStartedAt: 200,
|
||||
},
|
||||
});
|
||||
|
||||
staleSnapshot["agent:main:legacy"] = {
|
||||
sessionId: "legacy-session",
|
||||
updatedAt: 100,
|
||||
sessionStartedAt: 100,
|
||||
};
|
||||
await saveSessionStore(storePath, staleSnapshot);
|
||||
|
||||
expect(
|
||||
getSessionEntry({ agentId: "main", env, sessionKey: "agent:main:concurrent" })?.sessionId,
|
||||
).toBe("concurrent-session");
|
||||
expect(
|
||||
getSessionEntry({ agentId: "main", env, sessionKey: "agent:main:legacy" })?.sessionId,
|
||||
).toBe("legacy-session");
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps updateSessionStore deletes scoped to rows visible before mutation", async () => {
|
||||
await withOpenClawTestState(
|
||||
{
|
||||
layout: "state-only",
|
||||
prefix: "openclaw-session-store-compat-",
|
||||
scenario: "minimal",
|
||||
},
|
||||
async (state) => {
|
||||
const env = testEnv(state.stateDir);
|
||||
const storePath = canonicalStorePath(state.stateDir);
|
||||
upsertSessionEntry({
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionKey: "agent:main:old",
|
||||
entry: {
|
||||
sessionId: "old-session",
|
||||
updatedAt: 100,
|
||||
sessionStartedAt: 100,
|
||||
},
|
||||
});
|
||||
|
||||
await updateSessionStore(storePath, (store) => {
|
||||
delete store["agent:main:old"];
|
||||
upsertSessionEntry({
|
||||
agentId: "main",
|
||||
env,
|
||||
sessionKey: "agent:main:concurrent",
|
||||
entry: {
|
||||
sessionId: "concurrent-session",
|
||||
updatedAt: 200,
|
||||
sessionStartedAt: 200,
|
||||
},
|
||||
});
|
||||
store["agent:main:new"] = {
|
||||
sessionId: "new-session",
|
||||
updatedAt: 300,
|
||||
sessionStartedAt: 300,
|
||||
};
|
||||
});
|
||||
|
||||
expect(
|
||||
getSessionEntry({ agentId: "main", env, sessionKey: "agent:main:old" }),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
getSessionEntry({ agentId: "main", env, sessionKey: "agent:main:concurrent" })?.sessionId,
|
||||
).toBe("concurrent-session");
|
||||
expect(
|
||||
getSessionEntry({ agentId: "main", env, sessionKey: "agent:main:new" })?.sessionId,
|
||||
).toBe("new-session");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -202,10 +202,19 @@ export async function saveSessionStore(
|
||||
): Promise<void> {
|
||||
normalizeSessionEntries(store);
|
||||
const options = resolveSessionRowOptionsFromStorePath(storePath);
|
||||
const existing = loadSqliteSessionEntries(options);
|
||||
for (const sessionKey of Object.keys(existing)) {
|
||||
if (!Object.prototype.hasOwnProperty.call(store, sessionKey)) {
|
||||
deleteSessionEntry({ ...options, sessionKey });
|
||||
await saveSessionStoreRows(options, store);
|
||||
}
|
||||
|
||||
async function saveSessionStoreRows(
|
||||
options: SessionRowOptions,
|
||||
store: Record<string, SessionEntry>,
|
||||
deleteScope?: ReadonlySet<string>,
|
||||
): Promise<void> {
|
||||
if (deleteScope) {
|
||||
for (const sessionKey of deleteScope) {
|
||||
if (!Object.prototype.hasOwnProperty.call(store, sessionKey)) {
|
||||
deleteSessionEntry({ ...options, sessionKey });
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [sessionKey, entry] of Object.entries(store)) {
|
||||
@@ -216,11 +225,14 @@ export async function saveSessionStore(
|
||||
export async function updateSessionStore<T>(
|
||||
storePath: string,
|
||||
mutator: (store: Record<string, SessionEntry>) => Promise<T> | T,
|
||||
opts?: SaveSessionStoreOptions,
|
||||
_opts?: SaveSessionStoreOptions,
|
||||
): Promise<T> {
|
||||
const store = loadSessionStore(storePath);
|
||||
const options = resolveSessionRowOptionsFromStorePath(storePath);
|
||||
const store = loadSqliteSessionEntries(options);
|
||||
const deleteScope = new Set(Object.keys(store));
|
||||
const result = await mutator(store);
|
||||
await saveSessionStore(storePath, store, opts);
|
||||
normalizeSessionEntries(store);
|
||||
await saveSessionStoreRows(options, store, deleteScope);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user