





















@@ -0,0 +1,108 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, describe, expect, it } from "vitest";
5+import { SessionManager } from "./session-manager.js";
6+7+const tempPaths: string[] = [];
8+9+async function makeTempDir(): Promise<string> {
10+const dir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-manager-"));
11+tempPaths.push(dir);
12+return dir;
13+}
14+15+describe("SessionManager.open", () => {
16+afterEach(async () => {
17+await Promise.all(
18+tempPaths.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
19+);
20+});
21+22+it("recovers a corrupted first-line header without truncating later messages", async () => {
23+const dir = await makeTempDir();
24+const sessionFile = path.join(dir, "session.jsonl");
25+const originalHeader = {
26+type: "session",
27+version: 3,
28+id: "original-session",
29+timestamp: "2026-05-27T00:00:00.000Z",
30+cwd: "/srv/openclaw/main",
31+};
32+const userEntry = {
33+type: "message",
34+id: "user-1",
35+parentId: null,
36+timestamp: "2026-05-27T00:00:01.000Z",
37+message: { role: "user", content: "important question" },
38+};
39+const assistantEntry = {
40+type: "message",
41+id: "assistant-1",
42+parentId: "user-1",
43+timestamp: "2026-05-27T00:00:02.000Z",
44+message: { role: "assistant", content: "important answer" },
45+};
46+const originalTranscript =
47+[
48+JSON.stringify(originalHeader).slice(0, 30),
49+JSON.stringify(userEntry),
50+JSON.stringify(assistantEntry),
51+].join("\n") + "\n";
52+await fs.writeFile(sessionFile, originalTranscript, "utf8");
53+if (process.platform !== "win32") {
54+await fs.chmod(sessionFile, 0o600);
55+}
56+57+const sessionManager = SessionManager.open(sessionFile, dir, "/tmp/task-repo");
58+59+expect(sessionManager.getEntries()).toEqual([userEntry, assistantEntry]);
60+expect(await fs.readFile(sessionFile, "utf8")).toContain("important question");
61+expect(await fs.readFile(sessionFile, "utf8")).toContain("important answer");
62+await expect(fs.readFile(sessionFile, "utf8")).resolves.not.toBe(originalTranscript);
63+64+const backupFiles = (await fs.readdir(dir)).filter((file) => file.includes(".corrupt-"));
65+expect(backupFiles).toHaveLength(1);
66+await expect(fs.readFile(path.join(dir, backupFiles[0] ?? ""), "utf8")).resolves.toBe(
67+originalTranscript,
68+);
69+if (process.platform !== "win32") {
70+const backupStat = await fs.stat(path.join(dir, backupFiles[0] ?? ""));
71+expect(backupStat.mode & 0o777).toBe(0o600);
72+}
73+});
74+75+it("does not duplicate the header after recovering a header-only corrupt file", async () => {
76+const dir = await makeTempDir();
77+const sessionFile = path.join(dir, "session.jsonl");
78+await fs.writeFile(sessionFile, '{"type":"session","version":3,"id":"sess', "utf8");
79+80+const sessionManager = SessionManager.open(sessionFile, dir, "/tmp/task-repo");
81+sessionManager.appendMessage({ role: "user", content: "hello", timestamp: Date.now() });
82+sessionManager.appendMessage({
83+role: "assistant",
84+content: [{ type: "text", text: "hi" }],
85+api: "messages",
86+provider: "anthropic",
87+model: "sonnet-4.6",
88+usage: {
89+input: 0,
90+output: 0,
91+cacheRead: 0,
92+cacheWrite: 0,
93+totalTokens: 0,
94+cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
95+},
96+stopReason: "stop",
97+timestamp: Date.now(),
98+});
99+100+const entries = (await fs.readFile(sessionFile, "utf8"))
101+.trim()
102+.split("\n")
103+.map((line) => JSON.parse(line) as { type: string });
104+105+expect(entries.map((entry) => entry.type)).toEqual(["session", "message", "message"]);
106+expect(entries.filter((entry) => entry.type === "session")).toHaveLength(1);
107+});
108+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。