11// Assertions for upgrade-survivor E2E scenarios.
22import fs from "node:fs" ;
3+ import { createRequire } from "node:module" ;
34import path from "node:path" ;
45import { readPluginInstallIndex } from "../plugin-index-sqlite.mjs" ;
56
7+ const require = createRequire ( import . meta. url ) ;
8+
69const command = process . argv [ 2 ] ;
710const 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+
2633function 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+
86158function 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+
409538function readInstalledPluginIndex ( ) {
410539 const stateDir = requireEnv ( "OPENCLAW_STATE_DIR" ) ;
411540 const index = readPluginInstallIndex ( { stateDir } ) ;
0 commit comments