fix: preserve sqlite session compat rows

This commit is contained in:
Peter Steinberger
2026-05-15 23:31:37 +01:00
parent 7633c63761
commit d71cc9c360
2 changed files with 123 additions and 7 deletions

View File

@@ -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");
},
);
});
});

View File

@@ -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;
}