@cueloop/schema 0.1.0-alpha.0 → 0.1.0-alpha.100
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.
- package/package.json +16 -11
- package/src/agent-comment.test.ts +26 -0
- package/src/agent-comment.ts +17 -0
- package/src/agent-harness.ts +71 -0
- package/src/agent-input.test.ts +9 -0
- package/src/agent-input.ts +9 -0
- package/src/agent-submission.test.ts +15 -0
- package/src/agent-submission.ts +38 -0
- package/src/anchor.test.ts +340 -40
- package/src/anchor.ts +374 -43
- package/src/diff.test.ts +42 -25
- package/src/diff.ts +112 -79
- package/src/feedback.test.ts +318 -41
- package/src/feedback.ts +218 -55
- package/src/fuzzy.test.ts +110 -0
- package/src/fuzzy.ts +129 -0
- package/src/harness-output-routing.test.ts +18 -0
- package/src/harness-output-routing.ts +12 -0
- package/src/herdr.test.ts +50 -0
- package/src/herdr.ts +54 -0
- package/src/history.test.ts +401 -0
- package/src/history.ts +413 -0
- package/src/index.ts +19 -0
- package/src/inline.test.ts +195 -0
- package/src/inline.ts +234 -0
- package/src/markdown.test.ts +138 -22
- package/src/markdown.ts +332 -86
- package/src/participants.test.ts +105 -0
- package/src/participants.ts +51 -0
- package/src/path-view.test.ts +233 -0
- package/src/path-view.ts +105 -0
- package/src/share-access.test.ts +29 -0
- package/src/share-access.ts +14 -0
- package/src/thread-agent.ts +92 -0
- package/src/thread-surface.test.ts +6 -0
- package/src/thread-surface.ts +6 -0
- package/src/types.test.ts +79 -0
- package/src/types.ts +271 -32
- package/src/working-copy.test.ts +294 -0
- package/src/working-copy.ts +197 -0
package/src/herdr.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The herdr ambient env contract, single-sourced. A process running inside a
|
|
3
|
+
* herdr pane sees HERDR_ENV=1 plus the pane id; the herdr CLI itself reaches
|
|
4
|
+
* the app over HERDR_SOCKET_PATH (which the spawned `herdr` reads from the
|
|
5
|
+
* inherited env), so the binary is just `herdr` on PATH. Both the agent-side
|
|
6
|
+
* reporter (adapters) and the reviewer-side return-focus (client) read this
|
|
7
|
+
* contract; decoding it in one place stops the two sides drifting on which
|
|
8
|
+
* vars are required. Pure: no IO, env is only the default argument so callers
|
|
9
|
+
* can pass a fixture.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface HerdrContext {
|
|
13
|
+
paneId: string;
|
|
14
|
+
binPath: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type HerdrEnv = Record<string, string | undefined>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The pane's full contract - pane id and binary path - or null outside a
|
|
21
|
+
* functioning herdr. Required by anything that spawns the herdr CLI.
|
|
22
|
+
*/
|
|
23
|
+
export function detectHerdr(env: HerdrEnv = process.env): HerdrContext | null {
|
|
24
|
+
if (env.HERDR_ENV !== "1") return null;
|
|
25
|
+
const paneId = env.HERDR_PANE_ID;
|
|
26
|
+
|
|
27
|
+
if (!paneId) return null;
|
|
28
|
+
// herdr 0.8.0 exposes no HERDR_BIN_PATH; the CLI is `herdr` on PATH and finds
|
|
29
|
+
// the app over HERDR_SOCKET_PATH from the inherited env. Tests still override
|
|
30
|
+
// HERDR_BIN_PATH to point the spawn at a stub script.
|
|
31
|
+
const binPath = env.HERDR_BIN_PATH ?? "herdr";
|
|
32
|
+
|
|
33
|
+
return { paneId, binPath };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** True whenever this process runs inside a herdr pane. */
|
|
37
|
+
export function insideHerdr(env: HerdrEnv = process.env): boolean {
|
|
38
|
+
return env.HERDR_ENV === "1";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The pane a closing review should hand focus back to: the opener's
|
|
43
|
+
* CUELOOP_RETURN_PANE, or the session's recorded pane, but never this pane
|
|
44
|
+
* itself. Undefined outside herdr or when the only candidate is our own pane.
|
|
45
|
+
*/
|
|
46
|
+
export function returnPaneFor(
|
|
47
|
+
sessionHerdrPane?: string,
|
|
48
|
+
env: HerdrEnv = process.env,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
if (!insideHerdr(env)) return undefined;
|
|
51
|
+
const pane = env.CUELOOP_RETURN_PANE ?? sessionHerdrPane;
|
|
52
|
+
|
|
53
|
+
return pane && pane !== env.HERDR_PANE_ID ? pane : undefined;
|
|
54
|
+
}
|
|
@@ -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
|
+
});
|