


























@@ -2,6 +2,7 @@
22import { describe, expect, it } from "vitest";
33import { ok, type FileSystem } from "../types.js";
44import { JsonlSessionStorage, loadJsonlSessionMetadata } from "./jsonl-storage.js";
5+import { Session } from "./session.js";
5667type JsonlStorageFs = Pick<
78FileSystem,
@@ -55,4 +56,209 @@ describe("JsonlSessionStorage timestamps", () => {
5556"line 2 has invalid timestamp",
5657);
5758});
59+60+it("uses a leaf control's opaque append parent for the next entry", async () => {
61+let content = [
62+{
63+type: "session",
64+version: 3,
65+id: "session-1",
66+timestamp: "2026-06-15T00:00:00.000Z",
67+cwd: "/repo",
68+},
69+{
70+type: "custom",
71+id: "active-root",
72+parentId: null,
73+timestamp: "2026-06-15T00:00:01.000Z",
74+customType: "root",
75+},
76+{
77+type: "metadata",
78+id: "plugin-metadata",
79+parentId: null,
80+timestamp: "2026-06-15T00:00:02.000Z",
81+},
82+{
83+type: "leaf",
84+id: "active-leaf",
85+parentId: "inactive-tail",
86+timestamp: "2026-06-15T00:00:03.000Z",
87+targetId: "active-root",
88+appendParentId: "plugin-metadata",
89+},
90+]
91+.map((entry) => JSON.stringify(entry))
92+.join("\n");
93+content += "\n";
94+const fs: JsonlStorageFs = {
95+ ...createReadOnlyFs(content),
96+readTextFile: async () => ok(content),
97+appendFile: async (_path, appended) => {
98+content += String(appended);
99+return ok(undefined);
100+},
101+};
102+const storage = await JsonlSessionStorage.open(fs, "/sessions/session.jsonl");
103+const session = new Session(storage);
104+105+expect(await session.getLeafId()).toBe("active-root");
106+const entryId = await session.appendCustomEntry("continued");
107+const entry = await session.getEntry(entryId);
108+109+expect(entry).toMatchObject({ parentId: "plugin-metadata" });
110+expect((await storage.getPathToRoot(entryId)).map((pathEntry) => pathEntry.id)).toEqual([
111+"active-root",
112+entryId,
113+]);
114+expect(content.trim().split(/\r?\n/).at(-1)).toContain('"parentId":"plugin-metadata"');
115+});
116+117+it("keeps a terminal side append off the visible branch", async () => {
118+let content = [
119+{
120+type: "session",
121+version: 3,
122+id: "session-1",
123+timestamp: "2026-06-15T00:00:00.000Z",
124+cwd: "/repo",
125+},
126+{
127+type: "custom",
128+id: "active-root",
129+parentId: null,
130+timestamp: "2026-06-15T00:00:01.000Z",
131+customType: "active",
132+},
133+{
134+type: "custom",
135+id: "side-one",
136+parentId: "active-root",
137+timestamp: "2026-06-15T00:00:02.000Z",
138+customType: "side",
139+},
140+{
141+type: "leaf",
142+id: "side-leaf",
143+parentId: "side-one",
144+timestamp: "2026-06-15T00:00:03.000Z",
145+targetId: "active-root",
146+appendParentId: "side-one",
147+appendMode: "side",
148+},
149+{
150+type: "custom",
151+id: "side-two",
152+parentId: "side-one",
153+timestamp: "2026-06-15T00:00:04.000Z",
154+customType: "side",
155+appendMode: "side",
156+},
157+]
158+.map((entry) => JSON.stringify(entry))
159+.join("\n");
160+content += "\n";
161+const fs: JsonlStorageFs = {
162+ ...createReadOnlyFs(content),
163+readTextFile: async () => ok(content),
164+appendFile: async (_path, appended) => {
165+content += String(appended);
166+return ok(undefined);
167+},
168+};
169+const storage = await JsonlSessionStorage.open(fs, "/sessions/session.jsonl");
170+const session = new Session(storage);
171+172+expect(await storage.getLeafId()).toBe("active-root");
173+expect(await storage.getAppendParentId()).toBe("side-two");
174+const entryId = await session.appendCustomEntry("continued");
175+176+expect(await storage.getEntry(entryId)).toMatchObject({ parentId: "side-two" });
177+expect((await storage.getPathToRoot(entryId)).map((entry) => entry.id)).toEqual([
178+"active-root",
179+entryId,
180+]);
181+});
182+183+it("does not let opaque rows replace the selected visible leaf", async () => {
184+const content = [
185+{
186+type: "session",
187+version: 3,
188+id: "session-1",
189+timestamp: "2026-06-15T00:00:00.000Z",
190+cwd: "/repo",
191+},
192+{
193+type: "custom",
194+id: "active-root",
195+parentId: null,
196+timestamp: "2026-06-15T00:00:01.000Z",
197+customType: "active",
198+},
199+{
200+type: "custom",
201+id: "inactive-root",
202+parentId: null,
203+timestamp: "2026-06-15T00:00:02.000Z",
204+customType: "inactive",
205+},
206+{
207+type: "leaf",
208+id: "active-leaf",
209+parentId: "inactive-root",
210+timestamp: "2026-06-15T00:00:03.000Z",
211+targetId: "active-root",
212+},
213+{
214+type: "metadata",
215+id: "plugin-metadata",
216+parentId: "inactive-root",
217+timestamp: "2026-06-15T00:00:04.000Z",
218+},
219+]
220+.map((entry) => JSON.stringify(entry))
221+.join("\n");
222+const storage = await JsonlSessionStorage.open(
223+createReadOnlyFs(`${content}\n`),
224+"/sessions/session.jsonl",
225+);
226+const session = new Session(storage);
227+228+expect(await session.getLeafId()).toBe("active-root");
229+expect((await session.getBranch()).map((entry) => entry.id)).toEqual(["active-root"]);
230+});
231+232+it("rejects a leaf control with a missing append parent", async () => {
233+const content = [
234+{
235+type: "session",
236+version: 3,
237+id: "session-1",
238+timestamp: "2026-06-15T00:00:00.000Z",
239+cwd: "/repo",
240+},
241+{
242+type: "custom",
243+id: "active-root",
244+parentId: null,
245+timestamp: "2026-06-15T00:00:01.000Z",
246+customType: "active",
247+},
248+{
249+type: "leaf",
250+id: "active-leaf",
251+parentId: "active-root",
252+timestamp: "2026-06-15T00:00:02.000Z",
253+targetId: "active-root",
254+appendParentId: "missing",
255+},
256+]
257+.map((entry) => JSON.stringify(entry))
258+.join("\n");
259+260+await expect(
261+JsonlSessionStorage.open(createReadOnlyFs(`${content}\n`), "/sessions/session.jsonl"),
262+).rejects.toThrow("Append parent missing not found");
263+});
58264});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。