






















@@ -0,0 +1,115 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it } from "vitest";
5+import {
6+DEFAULT_REPLAY_MAX_MESSAGES,
7+replayRecentUserAssistantMessages,
8+} from "./session-transcript-replay.js";
9+10+const j = (obj: unknown): string => `${JSON.stringify(obj)}\n`;
11+12+describe("replayRecentUserAssistantMessages", () => {
13+let root = "";
14+beforeEach(async () => {
15+root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-replay-"));
16+});
17+afterEach(async () => {
18+await fs.rm(root, { recursive: true, force: true });
19+});
20+const call = (source: string, target: string): Promise<number> =>
21+replayRecentUserAssistantMessages({
22+sourceTranscript: source,
23+targetTranscript: target,
24+newSessionId: "new-session",
25+});
26+27+it("replays only the user/assistant tail and skips tool/system/malformed records", async () => {
28+const source = path.join(root, "prev.jsonl");
29+const target = path.join(root, "next.jsonl");
30+const lines: string[] = [j({ type: "session", id: "old" })];
31+for (let i = 0; i < DEFAULT_REPLAY_MAX_MESSAGES + 4; i += 1) {
32+lines.push(j({ message: { role: i % 2 === 0 ? "user" : "assistant", content: `m${i}` } }));
33+}
34+lines.push(j({ message: { role: "tool" } }));
35+lines.push(j({ type: "compaction", timestamp: new Date().toISOString() }));
36+lines.push("not-json-line\n");
37+await fs.writeFile(source, lines.join(""), "utf8");
38+39+expect(await call(source, target)).toBe(DEFAULT_REPLAY_MAX_MESSAGES);
40+const records = (await fs.readFile(target, "utf8"))
41+.split(/\r?\n/)
42+.filter((line) => line.trim().length > 0)
43+.map((line) => JSON.parse(line));
44+expect(records[0]).toMatchObject({ type: "session", id: "new-session" });
45+expect(records).toHaveLength(1 + DEFAULT_REPLAY_MAX_MESSAGES);
46+for (const r of records.slice(1)) {
47+expect(["user", "assistant"]).toContain(r.message.role);
48+}
49+expect(await call(path.join(root, "missing.jsonl"), path.join(root, "out.jsonl"))).toBe(0);
50+51+const assistantSource = path.join(root, "all-assistant.jsonl");
52+const assistantTarget = path.join(root, "all-assistant-out.jsonl");
53+const onlyAssistants = Array.from({ length: 3 }, () =>
54+j({ message: { role: "assistant", content: "x" } }),
55+).join("");
56+await fs.writeFile(assistantSource, onlyAssistants, "utf8");
57+expect(await call(assistantSource, assistantTarget)).toBe(0);
58+await expect(fs.stat(assistantTarget)).rejects.toThrow();
59+});
60+61+it("skips header for pre-existing targets and aligns the tail to a user turn", async () => {
62+const source = path.join(root, "prev.jsonl");
63+const target = path.join(root, "next.jsonl");
64+await fs.writeFile(target, j({ type: "session", id: "existing" }), "utf8");
65+const lines: string[] = [];
66+for (let i = 0; i < DEFAULT_REPLAY_MAX_MESSAGES + 1; i += 1) {
67+lines.push(j({ message: { role: i % 2 === 0 ? "user" : "assistant", content: `m${i}` } }));
68+}
69+await fs.writeFile(source, lines.join(""), "utf8");
70+71+expect(await call(source, target)).toBe(DEFAULT_REPLAY_MAX_MESSAGES - 1);
72+const records = (await fs.readFile(target, "utf8"))
73+.split(/\r?\n/)
74+.filter((line) => line.trim().length > 0)
75+.map((line) => JSON.parse(line));
76+expect(records.filter((r) => r.type === "session")).toHaveLength(1);
77+expect(records[0]).toMatchObject({ id: "existing" });
78+expect(records[1].message.role).toBe("user");
79+});
80+81+it("coalesces same-role runs so replayed records strictly alternate", async () => {
82+const source = path.join(root, "prev.jsonl");
83+const target = path.join(root, "next.jsonl");
84+await fs.writeFile(
85+source,
86+[
87+j({ message: { role: "user", content: "older user" } }),
88+j({ message: { role: "user", content: "latest user" } }),
89+j({ message: { role: "assistant", content: "older assistant" } }),
90+j({ message: { role: "assistant", content: "latest assistant" } }),
91+j({ message: { role: "user", content: "follow-up" } }),
92+j({ message: { role: "assistant", content: "answer" } }),
93+].join(""),
94+"utf8",
95+);
96+97+expect(await call(source, target)).toBe(4);
98+const records = (await fs.readFile(target, "utf8"))
99+.split(/\r?\n/)
100+.filter((line) => line.trim().length > 0)
101+.map((line) => JSON.parse(line));
102+expect(records.slice(1).map((r) => r.message.role)).toEqual([
103+"user",
104+"assistant",
105+"user",
106+"assistant",
107+]);
108+expect(records.slice(1).map((r) => r.message.content)).toEqual([
109+"latest user",
110+"latest assistant",
111+"follow-up",
112+"answer",
113+]);
114+});
115+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。