


























@@ -9,6 +9,25 @@ import {
991010const j = (obj: unknown): string => `${JSON.stringify(obj)}\n`;
111112+function messageEntry(params: {
13+id: string;
14+role: "user" | "assistant";
15+content: string;
16+parentId?: string | null;
17+timestamp?: string | number;
18+}): string {
19+return j({
20+type: "message",
21+id: params.id,
22+parentId: params.parentId ?? null,
23+timestamp: params.timestamp ?? "2026-05-16T00:00:00.000Z",
24+message: {
25+role: params.role,
26+content: params.content,
27+},
28+});
29+}
30+1231type ReplayRecord = {
1332type?: string;
1433id?: string;
@@ -66,9 +85,16 @@ describe("replayRecentUserAssistantMessages", () => {
6685const target = path.join(root, "next.jsonl");
6786const lines: string[] = [j({ type: "session", id: "old" })];
6887for (let i = 0; i < DEFAULT_REPLAY_MAX_MESSAGES + 4; i += 1) {
69-lines.push(j({ message: { role: i % 2 === 0 ? "user" : "assistant", content: `m${i}` } }));
88+lines.push(
89+messageEntry({
90+id: `entry-${i}`,
91+role: i % 2 === 0 ? "user" : "assistant",
92+content: `m${i}`,
93+parentId: i > 0 ? `entry-${i - 1}` : null,
94+}),
95+);
7096}
71-lines.push(j({ message: { role: "tool" } }));
97+lines.push(j({ type: "message", id: "tool", message: { role: "tool" } }));
7298lines.push(j({ type: "compaction", timestamp: new Date().toISOString() }));
7399lines.push("not-json-line\n");
74100await fs.writeFile(source, lines.join(""), "utf8");
@@ -98,8 +124,13 @@ describe("replayRecentUserAssistantMessages", () => {
9812499125const assistantSource = path.join(root, "all-assistant.jsonl");
100126const assistantTarget = path.join(root, "all-assistant-out.jsonl");
101-const onlyAssistants = Array.from({ length: 3 }, () =>
102-j({ message: { role: "assistant", content: "x" } }),
127+const onlyAssistants = Array.from({ length: 3 }, (_, index) =>
128+messageEntry({
129+id: `assistant-${index}`,
130+role: "assistant",
131+content: "x",
132+parentId: index > 0 ? `assistant-${index - 1}` : null,
133+}),
103134).join("");
104135await fs.writeFile(assistantSource, onlyAssistants, "utf8");
105136expect(await call(assistantSource, assistantTarget)).toBe(0);
@@ -112,7 +143,14 @@ describe("replayRecentUserAssistantMessages", () => {
112143await fs.writeFile(target, j({ type: "session", id: "existing" }), "utf8");
113144const lines: string[] = [];
114145for (let i = 0; i < DEFAULT_REPLAY_MAX_MESSAGES + 1; i += 1) {
115-lines.push(j({ message: { role: i % 2 === 0 ? "user" : "assistant", content: `m${i}` } }));
146+lines.push(
147+messageEntry({
148+id: `entry-${i}`,
149+role: i % 2 === 0 ? "user" : "assistant",
150+content: `m${i}`,
151+parentId: i > 0 ? `entry-${i - 1}` : null,
152+}),
153+);
116154}
117155await fs.writeFile(source, lines.join(""), "utf8");
118156@@ -129,12 +167,22 @@ describe("replayRecentUserAssistantMessages", () => {
129167await fs.writeFile(
130168source,
131169[
132-j({ message: { role: "user", content: "older user" } }),
133-j({ message: { role: "user", content: "latest user" } }),
134-j({ message: { role: "assistant", content: "older assistant" } }),
135-j({ message: { role: "assistant", content: "latest assistant" } }),
136-j({ message: { role: "user", content: "follow-up" } }),
137-j({ message: { role: "assistant", content: "answer" } }),
170+messageEntry({ id: "u1", role: "user", content: "older user" }),
171+messageEntry({ id: "u2", role: "user", content: "latest user", parentId: "u1" }),
172+messageEntry({
173+id: "a1",
174+role: "assistant",
175+content: "older assistant",
176+parentId: "u2",
177+}),
178+messageEntry({
179+id: "a2",
180+role: "assistant",
181+content: "latest assistant",
182+parentId: "a1",
183+}),
184+messageEntry({ id: "u3", role: "user", content: "follow-up", parentId: "a2" }),
185+messageEntry({ id: "a3", role: "assistant", content: "answer", parentId: "u3" }),
138186].join(""),
139187"utf8",
140188);
@@ -154,4 +202,44 @@ describe("replayRecentUserAssistantMessages", () => {
154202"answer",
155203]);
156204});
205+206+it("skips malformed user and assistant-shaped rows without poisoning the target", async () => {
207+const source = path.join(root, "prev.jsonl");
208+const target = path.join(root, "next.jsonl");
209+await fs.writeFile(
210+source,
211+[
212+messageEntry({ id: "valid-user", role: "user", content: "keep user" }),
213+j({ message: { role: "assistant", content: "missing type and id" } }),
214+j({
215+type: "message",
216+id: "missing-timestamp",
217+message: { role: "user", content: "missing timestamp" },
218+}),
219+j({
220+type: "message",
221+id: "bad-parent",
222+parentId: 123,
223+timestamp: "2026-05-16T00:00:01.000Z",
224+message: { role: "assistant", content: "bad parent" },
225+}),
226+messageEntry({
227+id: "valid-assistant",
228+role: "assistant",
229+content: "keep assistant",
230+parentId: "valid-user",
231+timestamp: "2026-05-16T00:00:02.000Z",
232+}),
233+].join(""),
234+"utf8",
235+);
236+237+expect(await call(source, target)).toBe(2);
238+const records = await readJsonlRecords(target);
239+expect(records.slice(1).map((record) => record.message?.content)).toEqual([
240+"keep user",
241+"keep assistant",
242+]);
243+expect(records.slice(1).every((record) => record.type === "message")).toBe(true);
244+});
157245});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。