@cueloop/schema 0.1.0-alpha.8 → 0.1.0-alpha.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,401 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ appendEntry,
4
+ createBranch,
5
+ derivePath,
6
+ followBranch,
7
+ forkHistory,
8
+ historyFromLinear,
9
+ HistoryError,
10
+ labelTip,
11
+ MAIN_BRANCH,
12
+ navigateTo,
13
+ pathOf,
14
+ recaptureMainHead,
15
+ removalEntries,
16
+ switchBranch,
17
+ tipOf,
18
+ validateHistory,
19
+ type SessionHistory,
20
+ } from "./history";
21
+ import type { Annotation, Message } from "./types";
22
+
23
+ const AT = "2026-09-01T10:00:00.000Z";
24
+ const MESSAGE: Message = {
25
+ id: "msg_1",
26
+ outcome: "approved",
27
+ summary: "",
28
+ body: "",
29
+ sentAt: AT,
30
+ };
31
+
32
+ function annotation(id: string, createdAt: string): Annotation {
33
+ return {
34
+ id,
35
+ kind: "comment",
36
+ anchor: { quote: "q", prefix: "", suffix: "" },
37
+ body: id,
38
+ createdAt,
39
+ };
40
+ }
41
+
42
+ /** A fresh history: one agent revision on main. */
43
+ function root(): SessionHistory {
44
+ return historyFromLinear({
45
+ id: "ses_1",
46
+ revisions: [{ revision: 1, content: "Plan v1", submittedAt: AT }],
47
+ annotations: [],
48
+ message: null,
49
+ createdAt: AT,
50
+ });
51
+ }
52
+
53
+ describe("appendEntry and derivePath", () => {
54
+ test("entries chain on the current tip and the path derives the head, comments, and messages", () => {
55
+ // Arrange
56
+ let history = root();
57
+
58
+ // Act
59
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
60
+ history = appendEntry(history, { type: "comment", annotationId: "a2", createdAt: AT }).history;
61
+ history = appendEntry(history, {
62
+ type: "comment-removed",
63
+ annotationId: "a1",
64
+ createdAt: AT,
65
+ }).history;
66
+ history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
67
+ history = appendEntry(history, {
68
+ type: "revision",
69
+ by: "agent",
70
+ content: "Plan v2",
71
+ createdAt: AT,
72
+ }).history;
73
+
74
+ // Assert
75
+ const derived = derivePath(history);
76
+
77
+ expect(derived.head.content).toBe("Plan v2");
78
+ expect(derived.annotationIds).toEqual(["a2"]);
79
+ expect(derived.messages).toEqual([MESSAGE]);
80
+ expect(derived.rounds).toBe(2);
81
+ expect(pathOf(history).map((entry) => entry.type)).toEqual([
82
+ "revision",
83
+ "comment",
84
+ "comment",
85
+ "comment-removed",
86
+ "message",
87
+ "revision",
88
+ ]);
89
+ });
90
+ });
91
+
92
+ describe("branches and navigation", () => {
93
+ test("a branch starts at the tip, ramble stays off main, navigating back leaves a summary", () => {
94
+ // Arrange
95
+ let history = labelTip(root(), "before ramble");
96
+ const checkpoint = tipOf(history);
97
+
98
+ history = createBranch(history, "ramble");
99
+ history = appendEntry(history, { type: "comment", annotationId: "r1", createdAt: AT }).history;
100
+ history = appendEntry(history, {
101
+ type: "revision",
102
+ by: "reviewer",
103
+ content: "Plan v1 (edited)",
104
+ createdAt: AT,
105
+ }).history;
106
+ expect(derivePath(history).head.content).toBe("Plan v1 (edited)");
107
+ expect(tipOf(history, MAIN_BRANCH)).toBe(checkpoint);
108
+
109
+ // Act
110
+ history = navigateTo(history, checkpoint, {
111
+ summary: "explored dropping the daemon",
112
+ createdAt: AT,
113
+ });
114
+
115
+ // Assert: the ramble's entries stay, the tip sits on the summary at the checkpoint
116
+ expect(history.entries).toHaveLength(4);
117
+ const derived = derivePath(history);
118
+
119
+ expect(derived.head.content).toBe("Plan v1");
120
+ expect(derived.annotationIds).toEqual([]);
121
+ expect(derived.summaries[0]!.abandoned).toHaveLength(2);
122
+ expect(history.labels[checkpoint]).toBe("before ramble");
123
+
124
+ // Act
125
+ history = switchBranch(history, MAIN_BRANCH);
126
+
127
+ // Assert
128
+ expect(derivePath(history).summaries).toEqual([]);
129
+ });
130
+
131
+ test("navigating without a summary just moves the tip", () => {
132
+ // Arrange
133
+ let history = root();
134
+ const checkpoint = tipOf(history);
135
+
136
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
137
+
138
+ // Act
139
+ history = navigateTo(history, checkpoint);
140
+
141
+ // Assert
142
+ expect(tipOf(history)).toBe(checkpoint);
143
+ expect(history.entries).toHaveLength(2);
144
+ });
145
+
146
+ test("refusals: unknown entry, the current tip, an entry off the branch, a duplicate branch", () => {
147
+ // Arrange
148
+ let history = root();
149
+ const checkpoint = tipOf(history);
150
+
151
+ history = createBranch(history, "ramble");
152
+ history = appendEntry(history, { type: "comment", annotationId: "r1", createdAt: AT }).history;
153
+ const rambleTip = tipOf(history);
154
+
155
+ history = switchBranch(history, MAIN_BRANCH);
156
+
157
+ // Assert
158
+ expect(() => navigateTo(history, "nope")).toThrow(HistoryError);
159
+ expect(() => navigateTo(history, checkpoint)).toThrow(/already/);
160
+ expect(() => navigateTo(history, rambleTip)).toThrow(/not on branch/);
161
+ expect(() => createBranch(history, "ramble")).toThrow(/exists/);
162
+ expect(() => switchBranch(history, "ghost")).toThrow(/no branch/);
163
+ });
164
+ });
165
+
166
+ describe("validateHistory and cycles", () => {
167
+ test("a healthy tree validates; each broken invariant names itself", () => {
168
+ // Arrange
169
+ const healthy = root();
170
+ const entry = healthy.entries[0]!;
171
+
172
+ // Assert
173
+ expect(validateHistory(healthy)).toBeNull();
174
+ expect(validateHistory({ ...healthy, entries: [entry, { ...entry }] })).toMatch(
175
+ /duplicate entry id/,
176
+ );
177
+ expect(validateHistory({ ...healthy, entries: [{ ...entry, parentId: "ghost" }] })).toMatch(
178
+ /missing parent/,
179
+ );
180
+ expect(validateHistory({ ...healthy, tips: {} })).toMatch(/no "main" branch/);
181
+ expect(validateHistory({ ...healthy, tips: { main: "ghost" } })).toMatch(/missing entry/);
182
+ expect(validateHistory({ ...healthy, branch: "ramble" })).toMatch(/has no tip/);
183
+ expect(
184
+ validateHistory({
185
+ ...healthy,
186
+ entries: [{ id: "c", parentId: null, type: "comment", annotationId: "a", createdAt: AT }],
187
+ tips: { main: "c" },
188
+ }),
189
+ ).toMatch(/no revision/);
190
+ });
191
+
192
+ test("a parent cycle is refused instead of walked forever", () => {
193
+ // Arrange: two entries that point at each other, off the main path
194
+ const healthy = root();
195
+ const loopA = {
196
+ id: "la",
197
+ parentId: "lb",
198
+ type: "comment",
199
+ annotationId: "x",
200
+ createdAt: AT,
201
+ } as const;
202
+ const loopB = {
203
+ id: "lb",
204
+ parentId: "la",
205
+ type: "comment",
206
+ annotationId: "y",
207
+ createdAt: AT,
208
+ } as const;
209
+ const cyclic = { ...healthy, entries: [...healthy.entries, loopA, loopB] };
210
+
211
+ // Assert
212
+ expect(validateHistory(cyclic)).toMatch(/own ancestor/);
213
+ expect(() => pathOf(cyclic, "la")).toThrow(HistoryError);
214
+ });
215
+ });
216
+
217
+ describe("forkHistory", () => {
218
+ test("copies the path with its comments and labels, drops messages, starts at the head", () => {
219
+ // Arrange
220
+ let history = root();
221
+
222
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
223
+ history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
224
+ history = labelTip(history, "approved v1");
225
+ const approvedTip = tipOf(history);
226
+
227
+ history = createBranch(history, "other");
228
+ history = appendEntry(history, { type: "comment", annotationId: "off", createdAt: AT }).history;
229
+ history = switchBranch(history, MAIN_BRANCH);
230
+
231
+ // Act
232
+ const fork = forkHistory(history);
233
+
234
+ // Assert
235
+ expect(fork.entries.map((entry) => entry.type)).toEqual(["revision", "comment"]);
236
+ expect(fork.tips).toEqual({ [MAIN_BRANCH]: fork.entries[1]!.id });
237
+ expect(derivePath(fork).annotationIds).toEqual(["a1"]);
238
+ // the label sat on the dropped message entry, so it does not travel
239
+ expect(fork.labels).toEqual({});
240
+ expect(history.labels[approvedTip]).toBe("approved v1");
241
+ });
242
+
243
+ test("entries after a dropped one are chained to the kept entry before it", () => {
244
+ // Arrange: rev1, message, rev2 by the agent, a reviewer edit, a comment on top
245
+ let history = root();
246
+
247
+ history = appendEntry(history, { type: "message", message: MESSAGE, createdAt: AT }).history;
248
+ history = appendEntry(history, {
249
+ type: "revision",
250
+ by: "agent",
251
+ content: "Plan v2",
252
+ createdAt: AT,
253
+ }).history;
254
+ history = appendEntry(history, {
255
+ type: "revision",
256
+ by: "reviewer",
257
+ content: "Plan v2 edited",
258
+ createdAt: AT,
259
+ }).history;
260
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
261
+
262
+ // Act
263
+ const fork = forkHistory(history);
264
+
265
+ // Assert: a valid one-branch tree with the agent's head and the comment, edits reset
266
+ expect(validateHistory(fork)).toBeNull();
267
+ expect(fork.entries.map((entry) => entry.type)).toEqual(["revision", "revision", "comment"]);
268
+ expect(derivePath(fork).head.content).toBe("Plan v2");
269
+ expect(derivePath(fork).annotationIds).toEqual(["a1"]);
270
+ });
271
+ });
272
+
273
+ describe("recaptureMainHead", () => {
274
+ test("replaces the agent's current revision on main in place, wherever the reviewer is", () => {
275
+ // Arrange: main at rev1, a reviewer edit on a branch
276
+ let history = createBranch(root(), "alt");
277
+
278
+ history = appendEntry(history, {
279
+ type: "revision",
280
+ by: "reviewer",
281
+ content: "Plan v1 edited",
282
+ createdAt: AT,
283
+ }).history;
284
+
285
+ // Act
286
+ const recaptured = recaptureMainHead(history, "Plan v1 recaptured");
287
+
288
+ // Assert: same entry count, main's head text moved, the branch keeps its edit
289
+ expect(recaptured.entries).toHaveLength(history.entries.length);
290
+ expect(derivePath(recaptured, tipOf(recaptured, MAIN_BRANCH)).head.content).toBe(
291
+ "Plan v1 recaptured",
292
+ );
293
+ expect(derivePath(recaptured).head.content).toBe("Plan v1 edited");
294
+ });
295
+ });
296
+
297
+ describe("historyFromLinear", () => {
298
+ test("chains revisions on main, files comments after the revision they were made on, ends with the message", () => {
299
+ // Act
300
+ const history = historyFromLinear({
301
+ id: "ses_9",
302
+ revisions: [
303
+ { revision: 1, content: "v1", submittedAt: "2026-09-01T10:00:00.000Z" },
304
+ { revision: 2, content: "v2", submittedAt: "2026-09-01T12:00:00.000Z" },
305
+ ],
306
+ annotations: [
307
+ annotation("late", "2026-09-01T13:00:00.000Z"),
308
+ annotation("early", "2026-09-01T11:00:00.000Z"),
309
+ ],
310
+ message: MESSAGE,
311
+ createdAt: "2026-09-01T09:00:00.000Z",
312
+ });
313
+
314
+ // Assert
315
+ expect(
316
+ history.entries.map(
317
+ (entry) => `${entry.type}:${"annotationId" in entry ? entry.annotationId : ""}`,
318
+ ),
319
+ ).toEqual(["revision:", "comment:early", "revision:", "comment:late", "message:"]);
320
+ expect(history.entries[0]!.id).toBe("ses_9_rev1");
321
+ expect(history.branch).toBe(MAIN_BRANCH);
322
+ const derived = derivePath(history);
323
+
324
+ expect(derived.head.content).toBe("v2");
325
+ expect(derived.annotationIds).toEqual(["early", "late"]);
326
+ expect(derived.messages).toEqual([MESSAGE]);
327
+ expect(derived.rounds).toBe(2);
328
+ });
329
+
330
+ test("is deterministic: the same record migrates to the same tree", () => {
331
+ // Arrange
332
+ const record = {
333
+ id: "ses_2",
334
+ revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
335
+ annotations: [annotation("a", AT)],
336
+ message: null,
337
+ createdAt: AT,
338
+ };
339
+
340
+ // Assert
341
+ expect(historyFromLinear(record)).toEqual(historyFromLinear(record));
342
+ });
343
+ });
344
+
345
+ describe("followBranch", () => {
346
+ test("carries one branch's path and its labels, dropping other branches", () => {
347
+ // Arrange: main has rev1 -> a1 (labelled); a branch off a1 adds a comment
348
+ let history = historyFromLinear({
349
+ id: "ses_1",
350
+ revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
351
+ annotations: [],
352
+ message: null,
353
+ createdAt: AT,
354
+ });
355
+
356
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
357
+ history = labelTip(history, "checkpoint");
358
+ const labelled = tipOf(history);
359
+
360
+ history = createBranch(history, "alt");
361
+ history = appendEntry(history, { type: "comment", annotationId: "b1", createdAt: AT }).history;
362
+
363
+ // Act: a share following main sees only main's path
364
+ const followed = followBranch(history, MAIN_BRANCH);
365
+
366
+ // Assert
367
+ expect(followed.branch).toBe(MAIN_BRANCH);
368
+ expect(followed.tips).toEqual({ [MAIN_BRANCH]: labelled });
369
+ expect(followed.entries.map((entry) => entry.type)).toEqual(["revision", "comment"]);
370
+ expect(followed.labels).toEqual({ [labelled]: "checkpoint" });
371
+ expect(validateHistory(followed)).toBeNull();
372
+ });
373
+ });
374
+
375
+ describe("removalEntries", () => {
376
+ test("returns the comment-removed entries in order, and nothing else", () => {
377
+ // Arrange
378
+ let history = historyFromLinear({
379
+ id: "ses_1",
380
+ revisions: [{ revision: 1, content: "v1", submittedAt: AT }],
381
+ annotations: [],
382
+ message: null,
383
+ createdAt: AT,
384
+ });
385
+
386
+ history = appendEntry(history, { type: "comment", annotationId: "a1", createdAt: AT }).history;
387
+ history = appendEntry(history, { type: "comment", annotationId: "a2", createdAt: AT }).history;
388
+ history = appendEntry(history, {
389
+ type: "comment-removed",
390
+ annotationId: "a1",
391
+ createdAt: AT,
392
+ }).history;
393
+
394
+ // Act
395
+ const removals = removalEntries(history);
396
+
397
+ // Assert
398
+ expect(removals.map((entry) => entry.annotationId)).toEqual(["a1"]);
399
+ expect(removals.every((entry) => entry.type === "comment-removed")).toBe(true);
400
+ });
401
+ });