Files

168 lines
7.8 KiB
JavaScript
Raw Permalink Normal View History

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 DATA_DIR_MODULE = path.resolve(__dirname, "../electron/lib/codex_data_dir.js");
const SESSIONS_MODULE = path.resolve(__dirname, "../electron/lib/sessions.js");
function loadModules(userData) {
const originalLoad = Module._load;
Module._load = function (request, parent, isMain) {
if (request === "electron") return { app: { getPath: () => userData } };
return originalLoad.call(this, request, parent, isMain);
};
delete require.cache[DATA_DIR_MODULE];
delete require.cache[SESSIONS_MODULE];
try {
return { dataDir: require(DATA_DIR_MODULE), sessions: require(SESSIONS_MODULE) };
} finally {
Module._load = originalLoad;
}
}
function writeRollout(root, id, title) {
const dir = path.join(root, "sessions", "2026", "08", "06");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, `rollout-${id}.jsonl`), [
JSON.stringify({ type: "session_meta", payload: { session_id: id, cwd: "/work/demo", timestamp: "2026-08-06T10:00:00.000Z" } }),
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: title } }),
].join("\n") + "\n");
}
test("会话数据目录切换后列表读取新目录,配置可持久化", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-data-dir-"));
const userData = path.join(root, "user-data");
const first = path.join(root, "first-codex");
const second = path.join(root, "second-codex");
fs.mkdirSync(userData, { recursive: true });
writeRollout(first, "11111111-1111-4111-8111-111111111111", "第一份记录");
writeRollout(second, "22222222-2222-4222-8222-222222222222", "第二份记录");
try {
let { dataDir, sessions } = loadModules(userData);
assert.throws(() => dataDir.set(path.join(root, "not-codex")), /Codex 数据/);
assert.equal(dataDir.set(first).path, first);
assert.equal(sessions.getCodexHome(), first);
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第一份记录"]);
assert.equal(dataDir.set(second).path, second);
assert.equal(sessions.getCodexHome(), second);
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第二份记录"]);
({ dataDir, sessions } = loadModules(userData));
assert.equal(dataDir.info().path, second);
assert.deepEqual(sessions.listConversations().map((row) => row.title), ["第二份记录"]);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("会话列表默认限量,避免大量历史记录一次性阻塞 IPC", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-data-dir-"));
const userData = path.join(root, "user-data");
const dataDir = path.join(root, "many-codex");
fs.mkdirSync(userData, { recursive: true });
for (let i = 0; i < 520; i++) {
const id = `${String(i).padStart(8, "0")}-1111-4111-8111-111111111111`;
writeRollout(dataDir, id, `第 ${i} 份记录`);
}
try {
const { dataDir: codexDir, sessions } = loadModules(userData);
codexDir.set(dataDir);
assert.equal(sessions.listConversations().length, 500);
assert.equal(sessions.listConversations({ limit: 12 }).length, 12);
assert.equal(sessions.listConversations({ limit: 9999 }).length, 520);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("subagent 同时含 id/session_id 时使用自身 id,不误绑定父线程", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-data-dir-"));
const userData = path.join(root, "user-data");
const dataDir = path.join(root, "subagent-codex");
const parentId = "33333333-3333-4333-8333-333333333333";
const childId = "44444444-4444-4444-8444-444444444444";
const dir = path.join(dataDir, "sessions", "2026", "08", "17");
fs.mkdirSync(dir, { recursive: true });
fs.writeFileSync(path.join(dir, `rollout-${childId}.jsonl`), [
JSON.stringify({
type: "session_meta",
payload: {
id: childId,
session_id: parentId,
cwd: "/work/subagent",
timestamp: "2026-08-17T10:00:00.000Z",
thread_source: "subagent",
},
}),
JSON.stringify({ type: "event_msg", payload: { type: "user_message", message: "子任务" } }),
].join("\n") + "\n");
try {
const { dataDir: codexDir, sessions } = loadModules(userData);
codexDir.set(dataDir);
const items = sessions.listConversations({ includeSubagent: true, limit: 10 });
assert.equal(items.length, 1);
assert.equal(items[0].id, childId);
const conversation = sessions.readConversation(items[0].path);
assert.equal(conversation.meta.id, childId);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("一键清空会话会清理正文、索引、状态库和侧栏目录并保留备份", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-clear-sessions-"));
const userData = path.join(root, "user-data");
const dataDir = path.join(root, "codex");
const id = "55555555-5555-4555-8555-555555555555";
fs.mkdirSync(userData, { recursive: true });
writeRollout(dataDir, id, "待清除会话");
fs.mkdirSync(path.join(dataDir, "archived_sessions", "2026", "08"), { recursive: true });
fs.writeFileSync(path.join(dataDir, "archived_sessions", "2026", "08", `rollout-${id}.jsonl`), "archived\n");
fs.writeFileSync(path.join(dataDir, "session_index.jsonl"), JSON.stringify({ id, thread_name: "待清除会话" }) + "\n");
const SQL = require("sql.js");
const init = await SQL({ locateFile: (file) => path.join(__dirname, "../node_modules/sql.js/dist", file) });
const state = new init.Database();
state.run("CREATE TABLE threads (id TEXT PRIMARY KEY, title TEXT)");
state.run("CREATE TABLE thread_artifacts (thread_id TEXT, payload TEXT)");
state.run("INSERT INTO threads VALUES (?, ?)", [id, "待清除会话"]);
state.run("INSERT INTO thread_artifacts VALUES (?, ?)", [id, "artifact"]);
fs.writeFileSync(path.join(dataDir, "state_5.sqlite"), Buffer.from(state.export()));
state.close();
fs.mkdirSync(path.join(dataDir, "sqlite"), { recursive: true });
const catalog = new init.Database();
catalog.run("CREATE TABLE local_thread_catalog (thread_id TEXT, display_title TEXT)");
catalog.run("CREATE TABLE automations (id TEXT PRIMARY KEY, name TEXT)");
catalog.run("INSERT INTO local_thread_catalog VALUES (?, ?)", [id, "待清除会话"]);
catalog.run("INSERT INTO automations VALUES (?, ?)", ["keep", "保留自动化"]);
fs.writeFileSync(path.join(dataDir, "sqlite", "codex-dev.db"), Buffer.from(catalog.export()));
catalog.close();
try {
const { dataDir: codexDir, sessions } = loadModules(userData);
codexDir.set(dataDir);
const result = await sessions.clearAllConversations();
assert.equal(result.filesRemoved, 2);
assert.equal(sessions.listConversations({ includeArchived: true, includeSubagent: true }).length, 0);
assert.equal(fs.readFileSync(path.join(dataDir, "session_index.jsonl"), "utf-8"), "");
assert.equal(fs.existsSync(result.backup), true);
const stateAfter = new init.Database(fs.readFileSync(path.join(dataDir, "state_5.sqlite")));
assert.equal(stateAfter.exec("SELECT COUNT(*) FROM threads")[0].values[0][0], 0);
assert.equal(stateAfter.exec("SELECT COUNT(*) FROM thread_artifacts")[0].values[0][0], 0);
stateAfter.close();
const catalogAfter = new init.Database(fs.readFileSync(path.join(dataDir, "sqlite", "codex-dev.db")));
assert.equal(catalogAfter.exec("SELECT COUNT(*) FROM local_thread_catalog")[0].values[0][0], 0);
assert.equal(catalogAfter.exec("SELECT COUNT(*) FROM automations")[0].values[0][0], 1);
catalogAfter.close();
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});