Files
codex-market-app/test/session-restore.test.js
T

167 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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");
}
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"));
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);
assert.equal(catalog.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 2);
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 });
}
});