2026-08-16 12:45:32 +08:00
const test = require ( "node:test" );
const assert = require ( "node:assert/strict" );
const fs = require ( "node:fs" );
const os = require ( "node:os" );
const path = require ( "node:path" );
const Module = require ( "node:module" );
const childProcess = require ( "node:child_process" );
const initSqlJs = require ( "sql.js" );
const PROVIDER_MODULE = path . resolve ( __dirname , "../electron/lib/provider_sync.js" );
const DESKTOP_MODULE = path . resolve ( __dirname , "../electron/lib/codex_desktop.js" );
function writeRollout ( root , id ) {
const dir = path . join ( root , "sessions" , "2026" , "08" , "09" );
fs . mkdirSync ( dir , { recursive : true });
fs . writeFileSync ( path . join ( dir , `rollout- ${ id } .jsonl` ), [
JSON . stringify ({ type : "session_meta" , payload : { session_id : id , cwd : "C:/work/demo" , timestamp : "2026-08-09T10:00:00.000Z" , model_provider : "old" } }),
JSON . stringify ({ type : "event_msg" , payload : { type : "user_message" , message : "Windows restore test" } }),
]. join ( "\n" ) + "\n" );
}
// subagent 会话的 session_meta.payload.source 是对象({subagent:{thread_spawn:{...}}}),
// 不是字符串。曾导致 Windows(无 sqlite3 CLI,走 sql.js 绑定)整批 upsert 抛错回滚,
// state 库 0 线程而侧栏 catalog 正常,Codex 启动 full reconcile 后会话"闪现后全部消失"。
function writeSubagentRollout ( root , id , parentId ) {
const dir = path . join ( root , "sessions" , "2026" , "08" , "09" );
fs . mkdirSync ( dir , { recursive : true });
fs . writeFileSync ( path . join ( dir , `rollout- ${ id } .jsonl` ), [
JSON . stringify ({
type : "session_meta" ,
payload : {
session_id : id ,
cwd : "C:/work/demo" ,
timestamp : "2026-08-09T11:00:00.000Z" ,
model_provider : "old" ,
source : { subagent : { thread_spawn : { parent_thread_id : parentId , depth : 1 , agent_path : null , agent_nickname : "Dirac" , agent_role : null } } },
thread_source : "subagent" ,
},
}),
JSON . stringify ({ type : "event_msg" , payload : { type : "user_message" , message : "subagent task" } }),
]. join ( "\n" ) + "\n" );
}
2026-09-11 09:06:41 +08:00
async function writeLegacyPollutedState ( root , id , subId ) {
const SQL = await initSqlJs ({ locateFile : ( file ) => path . join ( __dirname , "../node_modules/sql.js/dist" , file ) });
const state = new SQL . Database ();
state . run ( "CREATE TABLE threads (id TEXT PRIMARY KEY, source TEXT, thread_source TEXT, has_user_event INTEGER)" );
state . run ( "INSERT INTO threads VALUES (?,?,?,?)" , [ id , "vscode" , "user" , 1 ]);
state . run ( "INSERT INTO threads VALUES (?,?,?,?)" , [ subId , "[object Object]" , "subagent" , 1 ]);
fs . writeFileSync ( path . join ( root , "state_5.sqlite" ), Buffer . from ( state . export ()));
state . close ();
fs . mkdirSync ( path . join ( root , "sqlite" ), { recursive : true });
const catalog = new SQL . Database ();
catalog . run ( "CREATE TABLE local_thread_catalog (thread_id TEXT PRIMARY KEY, thread_source TEXT)" );
catalog . run ( "CREATE TABLE local_thread_catalog_metadata (id INTEGER PRIMARY KEY, catalog_revision INTEGER NOT NULL DEFAULT 0)" );
catalog . run ( "INSERT INTO local_thread_catalog VALUES (?,?)" , [ id , "user" ]);
catalog . run ( "INSERT INTO local_thread_catalog VALUES (?,?)" , [ subId , "subagent" ]);
catalog . run ( "INSERT INTO local_thread_catalog_metadata VALUES (1,0)" );
fs . writeFileSync ( path . join ( root , "sqlite" , "codex-dev.db" ), Buffer . from ( catalog . export ()));
catalog . close ();
}
2026-08-16 12:45:32 +08:00
test ( "会话恢复在没有 sqlite3 时插入线程并创建侧栏目录" , { concurrency : false }, async () => {
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "codex-session-restore-" ));
const dataDir = path . join ( root , "codex" );
const userData = path . join ( root , "app-data" );
const id = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa" ;
const originalLoad = Module . _load ;
const originalSpawnSync = childProcess . spawnSync ;
const originalBundleRoot = global . __BUNDLE_ROOT__ ;
try {
fs . mkdirSync ( userData , { recursive : true });
fs . mkdirSync ( path . join ( dataDir , "sessions" ), { recursive : true });
fs . writeFileSync ( path . join ( dataDir , "config.toml" ), 'model_provider = "openai"\n' );
writeRollout ( dataDir , id );
Module . _load = function ( request , parent , isMain ) {
if ( request === "electron" ) return { app : { getPath : () => userData } };
return originalLoad . call ( this , request , parent , isMain );
};
childProcess . spawnSync = function ( command , args , options ) {
if ( command === "sqlite3" ) return { error : new Error ( "sqlite3 not installed" ), status : null , stdout : "" , stderr : "" };
return originalSpawnSync . call ( this , command , args , options );
};
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
global . __BUNDLE_ROOT__ = path . resolve ( __dirname , ".." );
const provider = require ( PROVIDER_MODULE );
const desktop = require ( DESKTOP_MODULE );
desktop . codexRunning = () => false ;
provider . setCodexDataDir ( dataDir );
const result = await provider . syncProviderToCurrent (() => {});
assert . equal ( result . insertedRows , 1 );
assert . equal ( result . catalogRebuilt , 1 );
assert . equal ( result . stateDbCreated , 1 );
assert . equal ( result . stateDbCreateError , "" );
assert . equal ( fs . existsSync ( path . join ( dataDir , "sqlite" , "codex-dev.db" )), true );
assert . equal ( fs . existsSync ( path . join ( dataDir , "session_index.jsonl" )), true );
assert . equal ( fs . existsSync ( path . join ( dataDir , "state_5.sqlite" )), true );
const SQL = await initSqlJs ({ locateFile : ( file ) => path . join ( __dirname , "../node_modules/sql.js/dist" , file ) });
const restoredState = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "state_5.sqlite" )));
assert . equal ( restoredState . exec ( "SELECT COUNT(*) FROM threads" )[ 0 ]. values [ 0 ][ 0 ], 1 );
assert . equal ( restoredState . exec ( "SELECT has_user_event FROM threads WHERE id='" + id + "'" )[ 0 ]. values [ 0 ][ 0 ], 1 );
assert . equal ( restoredState . exec ( "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name IN ('thread_sections','thread_dynamic_tools','thread_spawn_edges','backfill_state','external_agent_config_imports','remote_control_enrollments')" )[ 0 ]. values [ 0 ][ 0 ], 6 );
assert . equal ( restoredState . exec ( "SELECT COUNT(*) FROM _sqlx_migrations" )[ 0 ]. values [ 0 ][ 0 ], 45 );
restoredState . close ();
const catalog = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "sqlite" , "codex-dev.db" )));
assert . equal ( catalog . exec ( "SELECT COUNT(*) FROM local_thread_catalog" )[ 0 ]. values [ 0 ][ 0 ], 1 );
catalog . close ();
} finally {
Module . _load = originalLoad ;
childProcess . spawnSync = originalSpawnSync ;
global . __BUNDLE_ROOT__ = originalBundleRoot ;
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
fs . rmSync ( root , { recursive : true , force : true });
}
});
test ( "subagent source 对象不再让 Windows(sql.js) 路径整批失败,且自建 catalog 库带 user_version" , { concurrency : false }, async () => {
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "codex-session-restore-" ));
const dataDir = path . join ( root , "codex" );
const userData = path . join ( root , "app-data" );
const id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb" ;
const subId = "cccccccc-cccc-4ccc-8ccc-cccccccccccc" ;
const originalLoad = Module . _load ;
const originalSpawnSync = childProcess . spawnSync ;
const originalBundleRoot = global . __BUNDLE_ROOT__ ;
try {
fs . mkdirSync ( userData , { recursive : true });
fs . mkdirSync ( path . join ( dataDir , "sessions" ), { recursive : true });
fs . writeFileSync ( path . join ( dataDir , "config.toml" ), 'model_provider = "openai"\n' );
writeRollout ( dataDir , id );
writeSubagentRollout ( dataDir , subId , id );
Module . _load = function ( request , parent , isMain ) {
if ( request === "electron" ) return { app : { getPath : () => userData } };
return originalLoad . call ( this , request , parent , isMain );
};
childProcess . spawnSync = function ( command , args , options ) {
if ( command === "sqlite3" ) return { error : new Error ( "sqlite3 not installed" ), status : null , stdout : "" , stderr : "" };
return originalSpawnSync . call ( this , command , args , options );
};
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
global . __BUNDLE_ROOT__ = path . resolve ( __dirname , ".." );
const provider = require ( PROVIDER_MODULE );
const desktop = require ( DESKTOP_MODULE );
desktop . codexRunning = () => false ;
provider . setCodexDataDir ( dataDir );
const result = await provider . syncProviderToCurrent (() => {}, () => {});
// 两条线程都必须进 state 库(修复前 subagent 的 source 对象会让整批回滚,insertedRows=0)
assert . equal ( result . insertedRows , 2 );
const SQL = await initSqlJs ({ locateFile : ( file ) => path . join ( __dirname , "../node_modules/sql.js/dist" , file ) });
const state = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "state_5.sqlite" )));
assert . equal ( state . exec ( "SELECT COUNT(*) FROM threads" )[ 0 ]. values [ 0 ][ 0 ], 2 );
// subagent 的 source 以 JSON 字符串入库(与 Codex 自身存储一致),而不是 '[object Object]'
const subSource = state . exec ( `SELECT source FROM threads WHERE id=' ${ subId } '` )[ 0 ]. values [ 0 ][ 0 ];
assert . ok ( subSource . startsWith ( '{"subagent"' ), `source 应为 JSON 字符串,实际: ${ subSource } ` );
assert . ok ( subSource . includes ( "Dirac" ));
2026-09-11 09:06:41 +08:00
assert . equal ( state . exec ( `SELECT has_user_event FROM threads WHERE id=' ${ subId } '` )[ 0 ]. values [ 0 ][ 0 ], 0 );
2026-08-16 12:45:32 +08:00
state . close ();
// 自建 codex-dev.db 必须带 user_version(对齐 Codex 迁移链 v30),
// 否则 Codex 重跑迁移 v27/28/29 会置空 last_full_reconciled_at 触发 full reconcile
const catalog = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "sqlite" , "codex-dev.db" )));
const uv = catalog . exec ( "PRAGMA user_version" )[ 0 ]. values [ 0 ][ 0 ];
assert . equal ( uv , 30 );
2026-09-11 09:06:41 +08:00
// 子任务仍进 state 库,但不再作为 Codex 顶层侧栏任务展示;
// 否则大量 subagent 会话会污染侧栏,点击派生线程容易一直转圈。
assert . equal ( catalog . exec ( "SELECT COUNT(*) FROM local_thread_catalog" )[ 0 ]. values [ 0 ][ 0 ], 1 );
catalog . close ();
} finally {
Module . _load = originalLoad ;
childProcess . spawnSync = originalSpawnSync ;
global . __BUNDLE_ROOT__ = originalBundleRoot ;
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
fs . rmSync ( root , { recursive : true , force : true });
}
});
test ( "启动轻量迁移清理旧 subagent 顶层目录污染" , { concurrency : false }, async () => {
const root = fs . mkdtempSync ( path . join ( os . tmpdir (), "codex-session-repair-" ));
const dataDir = path . join ( root , "codex" );
const userData = path . join ( root , "app-data" );
const id = "dddddddd-dddd-4ddd-8ddd-dddddddddddd" ;
const subId = "eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee" ;
const originalLoad = Module . _load ;
const originalSpawnSync = childProcess . spawnSync ;
const originalBundleRoot = global . __BUNDLE_ROOT__ ;
try {
fs . mkdirSync ( userData , { recursive : true });
fs . mkdirSync ( dataDir , { recursive : true });
await writeLegacyPollutedState ( dataDir , id , subId );
Module . _load = function ( request , parent , isMain ) {
if ( request === "electron" ) return { app : { getPath : () => userData } };
return originalLoad . call ( this , request , parent , isMain );
};
childProcess . spawnSync = function ( command , args , options ) {
if ( command === "sqlite3" ) return { error : new Error ( "sqlite3 not installed" ), status : null , stdout : "" , stderr : "" };
return originalSpawnSync . call ( this , command , args , options );
};
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
global . __BUNDLE_ROOT__ = path . resolve ( __dirname , ".." );
const provider = require ( PROVIDER_MODULE );
const desktop = require ( DESKTOP_MODULE );
desktop . codexRunning = () => false ;
provider . setCodexDataDir ( dataDir );
const result = await provider . repairLegacySessionVisibility (() => {});
assert . equal ( result . stateRows , 1 );
assert . equal ( result . catalogRows , 1 );
const SQL = await initSqlJs ({ locateFile : ( file ) => path . join ( __dirname , "../node_modules/sql.js/dist" , file ) });
const state = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "state_5.sqlite" )));
assert . equal ( state . exec ( `SELECT has_user_event FROM threads WHERE id=' ${ subId } '` )[ 0 ]. values [ 0 ][ 0 ], 0 );
state . close ();
const catalog = new SQL . Database ( fs . readFileSync ( path . join ( dataDir , "sqlite" , "codex-dev.db" )));
assert . equal ( catalog . exec ( "SELECT COUNT(*) FROM local_thread_catalog" )[ 0 ]. values [ 0 ][ 0 ], 1 );
assert . equal ( catalog . exec ( `SELECT COUNT(*) FROM local_thread_catalog WHERE thread_id=' ${ subId } '` )[ 0 ]. values [ 0 ][ 0 ], 0 );
2026-08-16 12:45:32 +08:00
catalog . close ();
} finally {
Module . _load = originalLoad ;
childProcess . spawnSync = originalSpawnSync ;
global . __BUNDLE_ROOT__ = originalBundleRoot ;
delete require . cache [ PROVIDER_MODULE ];
delete require . cache [ DESKTOP_MODULE ];
fs . rmSync ( root , { recursive : true , force : true });
}
});