Skip to content

Commit 2f02bbc

Browse files
committed
fix: harden legacy session SQLite migration
1 parent 5e1fbca commit 2f02bbc

3 files changed

Lines changed: 967 additions & 57 deletions

File tree

scripts/e2e/lib/upgrade-survivor/assertions.mjs

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
// Assertions for upgrade-survivor E2E scenarios.
22
import fs from "node:fs";
3+
import { createRequire } from "node:module";
34
import path from "node:path";
45
import { readPluginInstallIndex } from "../plugin-index-sqlite.mjs";
56

7+
const require = createRequire(import.meta.url);
8+
69
const command = process.argv[2];
710
const SCENARIOS = new Set([
811
"base",
@@ -23,6 +26,10 @@ const PERSONA_FILES = new Map([
2326
["MEMORY.md", "# Existing Memory\n\nUpgrade reports came from real users.\n"],
2427
]);
2528

29+
const LEGACY_SESSION_MAIN_ID = "upgrade-main-session";
30+
const LEGACY_SESSION_DIRECT_ID = "upgrade-direct-session";
31+
const LEGACY_SESSION_GROUP_ID = "upgrade-group-session";
32+
2633
function requireEnv(name) {
2734
const value = process.env[name];
2835
if (!value) {
@@ -83,6 +90,71 @@ function assert(condition, message) {
8390
}
8491
}
8592

93+
function readSessionRowsFromAgentSqlite(stateDir, agentId = "main") {
94+
const dbPath = path.join(stateDir, "agents", agentId, "agent", "openclaw-agent.sqlite");
95+
assert(fs.existsSync(dbPath), `agent SQLite session database missing: ${dbPath}`);
96+
const { DatabaseSync } = require("node:sqlite");
97+
const db = new DatabaseSync(dbPath, { readOnly: true });
98+
try {
99+
const rows = db
100+
.prepare("SELECT key, value_json FROM cache_entries WHERE scope = ? ORDER BY key ASC")
101+
.all("session_entries");
102+
return Object.fromEntries(
103+
rows.map((row) => {
104+
assert(typeof row.key === "string", "session SQLite row key was not a string");
105+
assert(
106+
typeof row.value_json === "string",
107+
`session SQLite row ${String(row.key)} had no JSON payload`,
108+
);
109+
return [row.key, JSON.parse(row.value_json)];
110+
}),
111+
);
112+
} finally {
113+
db.close();
114+
}
115+
}
116+
117+
function seedLegacySessionMetadata(stateDir) {
118+
const legacySessionsDir = path.join(stateDir, "sessions");
119+
writeJson(path.join(legacySessionsDir, "sessions.json"), {
120+
main: {
121+
sessionId: LEGACY_SESSION_MAIN_ID,
122+
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
123+
provider: "openai",
124+
model: "gpt-5.5",
125+
updatedAt: 1710000000000,
126+
skillsSnapshot: {
127+
prompt: "legacy prompt survives as metadata",
128+
resolvedSkills: [
129+
{
130+
name: "legacy-heavy-skill-cache",
131+
filePath: "/tmp/openclaw-old-package/skills/legacy-heavy-skill-cache/SKILL.md",
132+
},
133+
],
134+
},
135+
},
136+
"+15551234567": {
137+
sessionId: LEGACY_SESSION_DIRECT_ID,
138+
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
139+
provider: "openai",
140+
model: "gpt-5.5",
141+
updatedAt: 1710000000100,
142+
},
143+
"slack:channel:CUPGRADE": {
144+
sessionId: LEGACY_SESSION_GROUP_ID,
145+
sessionFile: path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
146+
provider: "openai",
147+
model: "gpt-5.5",
148+
updatedAt: 1710000000200,
149+
lastChannel: "slack",
150+
lastTo: "CUPGRADE",
151+
},
152+
});
153+
write(path.join(legacySessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`), '{"type":"main"}\n');
154+
write(path.join(legacySessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`), '{"type":"direct"}\n');
155+
write(path.join(legacySessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`), '{"type":"group"}\n');
156+
}
157+
86158
function getScenario() {
87159
const scenario = process.env.OPENCLAW_UPGRADE_SURVIVOR_SCENARIO || "base";
88160
assert(SCENARIOS.has(scenario), `unknown upgrade survivor scenario: ${scenario}`);
@@ -139,6 +211,7 @@ function seedState() {
139211
agentId: "main",
140212
title: "Existing user session",
141213
});
214+
seedLegacySessionMetadata(stateDir);
142215

143216
const runtimeRoot = path.join(stateDir, "plugin-runtime-deps");
144217
for (const plugin of ["discord", "telegram", "whatsapp"]) {
@@ -357,12 +430,15 @@ function assertStateSurvived() {
357430
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
358431
const workspace = requireEnv("OPENCLAW_TEST_WORKSPACE_DIR");
359432
const scenario = getScenario();
433+
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
360434
assert(fs.existsSync(path.join(workspace, "IDENTITY.md")), "workspace identity file missing");
361435
assert(
362436
fs.existsSync(path.join(stateDir, "agents", "main", "sessions", "legacy-session.json")),
363437
"legacy session file missing",
364438
);
365-
const stage = process.env.OPENCLAW_UPGRADE_SURVIVOR_ASSERT_STAGE || "survival";
439+
if (stage !== "baseline") {
440+
assertSessionMetadataMigratedToSqlite(stateDir);
441+
}
366442
const legacyRuntimeRoot = path.join(stateDir, "plugin-runtime-deps");
367443
if (stage === "baseline") {
368444
if (fs.existsSync(legacyRuntimeRoot)) {
@@ -406,6 +482,59 @@ function assertStateSurvived() {
406482
}
407483
}
408484

485+
function assertSessionMetadataMigratedToSqlite(stateDir) {
486+
const legacyStorePath = path.join(stateDir, "sessions", "sessions.json");
487+
const agentSessionsDir = path.join(stateDir, "agents", "main", "sessions");
488+
assert(
489+
!fs.existsSync(legacyStorePath),
490+
`legacy sessions.json survived migration: ${legacyStorePath}`,
491+
);
492+
for (const sessionId of [
493+
LEGACY_SESSION_MAIN_ID,
494+
LEGACY_SESSION_DIRECT_ID,
495+
LEGACY_SESSION_GROUP_ID,
496+
]) {
497+
assert(
498+
fs.existsSync(path.join(agentSessionsDir, `${sessionId}.jsonl`)),
499+
`legacy session transcript was not moved for ${sessionId}`,
500+
);
501+
}
502+
503+
const store = readSessionRowsFromAgentSqlite(stateDir);
504+
const main = store["agent:main:main"];
505+
const direct = store["agent:main:+15551234567"];
506+
const group = store["agent:main:slack:channel:cupgrade"];
507+
assert(main?.sessionId === LEGACY_SESSION_MAIN_ID, "main legacy session row missing from SQLite");
508+
assert(
509+
direct?.sessionId === LEGACY_SESSION_DIRECT_ID,
510+
"direct legacy session row missing from SQLite",
511+
);
512+
assert(
513+
group?.sessionId === LEGACY_SESSION_GROUP_ID,
514+
"channel legacy session row missing from SQLite",
515+
);
516+
assert(
517+
main?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_MAIN_ID}.jsonl`),
518+
"main legacy session row still points at the old sessions directory",
519+
);
520+
assert(
521+
direct?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_DIRECT_ID}.jsonl`),
522+
"direct legacy session row still points at the old sessions directory",
523+
);
524+
assert(
525+
group?.sessionFile === path.join(agentSessionsDir, `${LEGACY_SESSION_GROUP_ID}.jsonl`),
526+
"channel legacy session row still points at the old sessions directory",
527+
);
528+
assert(
529+
main.skillsSnapshot?.prompt === "legacy prompt survives as metadata",
530+
"legacy session metadata prompt was not preserved",
531+
);
532+
assert(
533+
main.skillsSnapshot?.resolvedSkills === undefined,
534+
"heavy resolvedSkills cache was persisted into SQLite session metadata",
535+
);
536+
}
537+
409538
function readInstalledPluginIndex() {
410539
const stateDir = requireEnv("OPENCLAW_STATE_DIR");
411540
const index = readPluginInstallIndex({ stateDir });

0 commit comments

Comments
 (0)