@cueloop/daemon 0.1.0-alpha.6 → 0.1.0-alpha.61
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 +30 -22
- package/src/api.test.ts +292 -46
- package/src/api.ts +301 -52
- package/src/capabilities.test.ts +38 -0
- package/src/capabilities.ts +30 -0
- package/src/client.test.ts +24 -0
- package/src/client.ts +151 -38
- package/src/diff-hot-reload.test.ts +170 -0
- package/src/diff-watcher.ts +105 -0
- package/src/herdr-pane.test.ts +243 -0
- package/src/herdr-pane.ts +154 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +88 -0
- package/src/herdr-tab-store.test.ts +96 -0
- package/src/herdr-tab-store.ts +69 -0
- package/src/index.ts +8 -8
- package/src/main.ts +4 -1
- package/src/paths.ts +9 -0
- package/src/protocol.ts +51 -7
- package/src/retention.test.ts +182 -0
- package/src/retention.ts +122 -0
- package/src/review.test.ts +370 -0
- package/src/review.ts +289 -0
- package/src/server.test.ts +169 -20
- package/src/server.ts +193 -84
- package/src/share-blob.test.ts +62 -0
- package/src/share-blob.ts +55 -0
- package/src/single-instance.test.ts +38 -11
- package/src/store.ts +13 -5
- package/src/validate.test.ts +196 -14
- package/src/validate.ts +138 -38
- package/src/working-tree.ts +113 -0
package/package.json
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cueloop/daemon",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.61",
|
|
4
|
+
"description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits",
|
|
5
|
+
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
8
|
+
},
|
|
4
9
|
"license": "Apache-2.0",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
13
|
+
"directory": "packages/daemon"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
5
19
|
"type": "module",
|
|
6
20
|
"exports": {
|
|
7
21
|
".": "./src/index.ts",
|
|
8
|
-
"./client": "./src/client.ts"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
22
|
+
"./client": "./src/client.ts",
|
|
23
|
+
"./review": "./src/review.ts",
|
|
24
|
+
"./working-tree": "./src/working-tree.ts",
|
|
25
|
+
"./store": "./src/store.ts",
|
|
26
|
+
"./paths": "./src/paths.ts",
|
|
27
|
+
"./retention": "./src/retention.ts",
|
|
28
|
+
"./herdr-pane": "./src/herdr-pane.ts",
|
|
29
|
+
"./herdr-split": "./src/herdr-split.ts",
|
|
30
|
+
"./share-blob": "./src/share-blob.ts"
|
|
13
31
|
},
|
|
14
32
|
"publishConfig": {
|
|
15
33
|
"access": "public"
|
|
16
34
|
},
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@cueloop/schema": "0.1.0-alpha.61",
|
|
37
|
+
"valibot": "^1.4.2"
|
|
38
|
+
},
|
|
21
39
|
"engines": {
|
|
22
40
|
"bun": ">=1.3.0"
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
"type": "git",
|
|
26
|
-
"url": "git+https://github.com/mmurakaru/cueloop.git",
|
|
27
|
-
"directory": "packages/daemon"
|
|
28
|
-
},
|
|
29
|
-
"homepage": "https://github.com/mmurakaru/cueloop#readme",
|
|
30
|
-
"bugs": {
|
|
31
|
-
"url": "https://github.com/mmurakaru/cueloop/issues"
|
|
32
|
-
},
|
|
33
|
-
"description": "The cueloop session daemon and its client: socket protocol, atomic store, resumable waits"
|
|
34
|
-
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/api.test.ts
CHANGED
|
@@ -24,71 +24,174 @@ afterEach(() => rmSync(home, { recursive: true, force: true }));
|
|
|
24
24
|
|
|
25
25
|
describe("session lifecycle", () => {
|
|
26
26
|
test("create → get → list", () => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// Act
|
|
28
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
29
|
+
|
|
30
|
+
// Assert
|
|
31
|
+
expect(session.status).toBe("pending");
|
|
32
|
+
expect(session.revisions.length).toBe(1);
|
|
33
|
+
expect(core.sessionGet(session.id).id).toBe(session.id);
|
|
31
34
|
expect(core.sessionList({ status: "pending" }).length).toBe(1);
|
|
32
35
|
});
|
|
33
36
|
|
|
34
37
|
test("annotate upserts by id; remove deletes", () => {
|
|
35
|
-
|
|
36
|
-
core.
|
|
38
|
+
// Arrange
|
|
39
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
40
|
+
|
|
41
|
+
// Act
|
|
42
|
+
core.sessionAnnotate(session.id, {
|
|
37
43
|
id: "a1",
|
|
38
44
|
kind: "comment",
|
|
39
45
|
anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
|
|
40
46
|
body: "How carefully?",
|
|
41
47
|
});
|
|
42
|
-
core.sessionAnnotate(
|
|
48
|
+
core.sessionAnnotate(session.id, {
|
|
43
49
|
id: "a1",
|
|
44
50
|
kind: "comment",
|
|
45
51
|
anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
|
|
46
52
|
body: "Define carefully.",
|
|
47
53
|
});
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
core.
|
|
51
|
-
expect(core.sessionGet(
|
|
54
|
+
|
|
55
|
+
// Assert
|
|
56
|
+
expect(core.sessionGet(session.id).annotations.length).toBe(1);
|
|
57
|
+
expect(core.sessionGet(session.id).annotations[0]!.body).toBe("Define carefully.");
|
|
58
|
+
|
|
59
|
+
// Act
|
|
60
|
+
core.sessionRemoveAnnotation(session.id, "a1");
|
|
61
|
+
|
|
62
|
+
// Assert
|
|
63
|
+
expect(core.sessionGet(session.id).annotations.length).toBe(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("annotate with an author registers them in the participant registry", () => {
|
|
67
|
+
// Arrange
|
|
68
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
69
|
+
|
|
70
|
+
// Act
|
|
71
|
+
core.sessionAnnotate(
|
|
72
|
+
session.id,
|
|
73
|
+
{
|
|
74
|
+
id: "a1",
|
|
75
|
+
kind: "comment",
|
|
76
|
+
anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
|
|
77
|
+
body: "A collaborator's note.",
|
|
78
|
+
author: "SHA256:ana",
|
|
79
|
+
},
|
|
80
|
+
"Ana",
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// Assert
|
|
84
|
+
const stored = core.sessionGet(session.id);
|
|
85
|
+
|
|
86
|
+
expect(stored.annotations[0]!.author).toBe("SHA256:ana");
|
|
87
|
+
expect(stored.participants).toEqual([{ id: "SHA256:ana", provider: "ssh", name: "Ana" }]);
|
|
52
88
|
});
|
|
53
89
|
|
|
54
90
|
test("resolve produces feedback.md and maps to the agent contract", () => {
|
|
55
|
-
|
|
56
|
-
core.
|
|
91
|
+
// Arrange
|
|
92
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
93
|
+
|
|
94
|
+
core.sessionAnnotate(session.id, {
|
|
57
95
|
id: "a1",
|
|
58
96
|
kind: "comment",
|
|
59
97
|
anchor: { quote: "carefully", prefix: "the thing ", suffix: "." },
|
|
60
98
|
body: "Spell out the steps.",
|
|
61
99
|
});
|
|
62
|
-
|
|
100
|
+
|
|
101
|
+
// Act
|
|
102
|
+
const resolved = core.sessionResolve(session.id, "request_changes", "Needs detail.");
|
|
103
|
+
|
|
104
|
+
// Assert
|
|
63
105
|
expect(resolved.verdict!.feedback).toContain("# Review: request changes");
|
|
64
106
|
expect(resolved.verdict!.feedback).toContain("> carefully");
|
|
65
|
-
expect(verdictResponse(resolved)).toEqual({
|
|
107
|
+
expect(verdictResponse(resolved)).toEqual({
|
|
108
|
+
allow: false,
|
|
109
|
+
feedback: resolved.verdict!.feedback,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// Arrange
|
|
66
113
|
const approved = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
114
|
+
|
|
115
|
+
// Act
|
|
67
116
|
core.sessionResolve(approved.id, "approve", "LGTM");
|
|
117
|
+
|
|
118
|
+
// Assert
|
|
68
119
|
expect(verdictResponse(core.sessionGet(approved.id)).allow).toBe(true);
|
|
69
120
|
});
|
|
70
121
|
|
|
71
122
|
test("mutating a resolved session throws", () => {
|
|
72
|
-
|
|
73
|
-
core.
|
|
123
|
+
// Arrange
|
|
124
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
125
|
+
|
|
126
|
+
core.sessionResolve(session.id, "approve", "");
|
|
127
|
+
|
|
128
|
+
// Assert
|
|
74
129
|
expect(() =>
|
|
75
|
-
core.sessionAnnotate(
|
|
130
|
+
core.sessionAnnotate(session.id, {
|
|
131
|
+
id: "a1",
|
|
132
|
+
kind: "comment",
|
|
133
|
+
anchor: { quote: "x", prefix: "", suffix: "" },
|
|
134
|
+
body: "b",
|
|
135
|
+
}),
|
|
76
136
|
).toThrow("already resolved");
|
|
77
137
|
});
|
|
78
138
|
|
|
79
139
|
test("working copy stores edits and clears on revert or no-op", () => {
|
|
80
|
-
|
|
81
|
-
core.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
140
|
+
// Arrange
|
|
141
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
142
|
+
|
|
143
|
+
// Act
|
|
144
|
+
core.sessionSetWorkingCopy(session.id, PLAN.content.replace("carefully", "very carefully"));
|
|
145
|
+
|
|
146
|
+
// Assert
|
|
147
|
+
expect(core.sessionGet(session.id).workingCopy).toContain("very carefully");
|
|
148
|
+
|
|
149
|
+
// Act
|
|
150
|
+
core.sessionSetWorkingCopy(session.id, PLAN.content);
|
|
151
|
+
|
|
152
|
+
// Assert
|
|
153
|
+
expect(core.sessionGet(session.id).workingCopy).toBeUndefined();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("viewed paths merge, dedupe, clear on empty, and survive a daemon restart", () => {
|
|
157
|
+
// Arrange
|
|
158
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
159
|
+
|
|
160
|
+
// Act
|
|
161
|
+
core.sessionSetViewed(session.id, ["src/a.ts", "src/b.ts", "src/a.ts"]);
|
|
162
|
+
|
|
163
|
+
// Assert
|
|
164
|
+
expect(core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts"]);
|
|
165
|
+
|
|
166
|
+
// Act
|
|
167
|
+
// merge-additive: a stale client sending only its own new mark loses nothing
|
|
168
|
+
core.sessionSetViewed(session.id, ["src/c.ts"]);
|
|
169
|
+
|
|
170
|
+
// Assert
|
|
171
|
+
expect(core.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
|
|
172
|
+
// a resumed review reads its progress back after a daemon restart
|
|
173
|
+
const reborn = new DaemonCore(home);
|
|
174
|
+
|
|
175
|
+
expect(reborn.sessionGet(session.id).viewedPaths).toEqual(["src/a.ts", "src/b.ts", "src/c.ts"]);
|
|
176
|
+
|
|
177
|
+
// Act
|
|
178
|
+
core.sessionSetViewed(session.id, []);
|
|
179
|
+
|
|
180
|
+
// Assert
|
|
181
|
+
expect(core.sessionGet(session.id).viewedPaths).toBeUndefined();
|
|
85
182
|
});
|
|
86
183
|
|
|
87
184
|
test("revision reopens the session and resets working copy + verdict", () => {
|
|
88
|
-
|
|
89
|
-
core.
|
|
90
|
-
|
|
91
|
-
|
|
185
|
+
// Arrange
|
|
186
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
187
|
+
|
|
188
|
+
core.sessionSetWorkingCopy(session.id, PLAN.content + "\nedit");
|
|
189
|
+
core.sessionResolve(session.id, "request_changes", "redo");
|
|
190
|
+
|
|
191
|
+
// Act
|
|
192
|
+
const revised = core.sessionSubmitRevision(session.id, "# Plan v2\n");
|
|
193
|
+
|
|
194
|
+
// Assert
|
|
92
195
|
expect(revised.status).toBe("pending");
|
|
93
196
|
expect(revised.verdict).toBeNull();
|
|
94
197
|
expect(revised.workingCopy).toBeUndefined();
|
|
@@ -97,47 +200,172 @@ describe("session lifecycle", () => {
|
|
|
97
200
|
});
|
|
98
201
|
});
|
|
99
202
|
|
|
203
|
+
describe("revision marks addressed annotations", () => {
|
|
204
|
+
const annotate = (sessionId: string, id: string, quote: string, body = "note") =>
|
|
205
|
+
core.sessionAnnotate(sessionId, {
|
|
206
|
+
id,
|
|
207
|
+
kind: "comment",
|
|
208
|
+
anchor: { quote, prefix: "", suffix: "" },
|
|
209
|
+
body,
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("ids the agent reports are marked addressed by that revision", () => {
|
|
213
|
+
// Arrange
|
|
214
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
215
|
+
|
|
216
|
+
annotate(session.id, "a1", "carefully");
|
|
217
|
+
annotate(session.id, "a2", "Context");
|
|
218
|
+
|
|
219
|
+
// Act: the revision keeps both quoted texts, so only the reported id resolves
|
|
220
|
+
const revised = core.sessionSubmitRevision(session.id, PLAN.content + "\nMore.\n", ["a1"]);
|
|
221
|
+
|
|
222
|
+
// Assert
|
|
223
|
+
const [first, second] = revised.annotations;
|
|
224
|
+
|
|
225
|
+
expect(first!.resolution).toEqual({ revision: 2, source: "agent" });
|
|
226
|
+
expect(second!.resolution).toBeUndefined();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a plan annotation whose quoted text vanished is drift-addressed; a surviving quote stays open", () => {
|
|
230
|
+
// Arrange
|
|
231
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
232
|
+
|
|
233
|
+
annotate(session.id, "gone", "carefully");
|
|
234
|
+
annotate(session.id, "kept", "Context");
|
|
235
|
+
|
|
236
|
+
// Act: the revision rewrites the "carefully" sentence but keeps the Context heading
|
|
237
|
+
const revised = core.sessionSubmitRevision(
|
|
238
|
+
session.id,
|
|
239
|
+
"# Plan\n\n## Context\n\nDo the thing with tests.\n",
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
// Assert
|
|
243
|
+
expect(revised.annotations.find((a) => a.id === "gone")!.resolution).toEqual({
|
|
244
|
+
revision: 2,
|
|
245
|
+
source: "drift",
|
|
246
|
+
});
|
|
247
|
+
expect(revised.annotations.find((a) => a.id === "kept")!.resolution).toBeUndefined();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("an unknown reported id is ignored, and an already-addressed annotation keeps its first resolution", () => {
|
|
251
|
+
// Arrange
|
|
252
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
253
|
+
|
|
254
|
+
annotate(session.id, "a1", "carefully");
|
|
255
|
+
core.sessionSubmitRevision(session.id, PLAN.content, ["a1"]);
|
|
256
|
+
|
|
257
|
+
// Act: a second revision reports a stale id and re-reports the settled one
|
|
258
|
+
const revised = core.sessionSubmitRevision(session.id, PLAN.content, ["a1", "a_never_existed"]);
|
|
259
|
+
|
|
260
|
+
// Assert
|
|
261
|
+
expect(revised.annotations[0]!.resolution).toEqual({ revision: 2, source: "agent" });
|
|
262
|
+
expect(revised.revisions.length).toBe(3);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("a diff revision never drift-addresses - a new patch says nothing about the feedback", () => {
|
|
266
|
+
// Arrange
|
|
267
|
+
const diffArtifact: Artifact = { type: "diff", content: "+++ b/a.ts\n+new line\n", meta: {} };
|
|
268
|
+
const session = core.sessionCreate({ workspace: WS, artifact: diffArtifact });
|
|
269
|
+
|
|
270
|
+
annotate(session.id, "d1", "new line");
|
|
271
|
+
|
|
272
|
+
// Act: the revised patch no longer contains the quoted text
|
|
273
|
+
const revised = core.sessionSubmitRevision(session.id, "+++ b/b.ts\n+other\n");
|
|
274
|
+
|
|
275
|
+
// Assert
|
|
276
|
+
expect(revised.annotations[0]!.resolution).toBeUndefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
test("the next feedback document omits addressed annotations and teaches the addressed-ids call", () => {
|
|
280
|
+
// Arrange
|
|
281
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
282
|
+
|
|
283
|
+
annotate(session.id, "settled", "carefully");
|
|
284
|
+
annotate(session.id, "open", "Context", "needs a diagram");
|
|
285
|
+
core.sessionSubmitRevision(session.id, PLAN.content, ["settled"]);
|
|
286
|
+
|
|
287
|
+
// Act
|
|
288
|
+
const resolved = core.sessionResolve(session.id, "request_changes", "one left");
|
|
289
|
+
|
|
290
|
+
// Assert
|
|
291
|
+
expect(resolved.verdict!.feedback).not.toContain("settled");
|
|
292
|
+
expect(resolved.verdict!.feedback).toContain("annotation id: `open`");
|
|
293
|
+
expect(resolved.verdict!.feedback).toContain(`submit-revision ${session.id}`);
|
|
294
|
+
expect(resolved.verdict!.feedback).toContain("--addressed");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
100
298
|
describe("the wait contract: verdicts outlive waits", () => {
|
|
101
299
|
test("wait resolves when the verdict arrives", async () => {
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
core.
|
|
300
|
+
// Arrange
|
|
301
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
302
|
+
const wait = core.sessionWait(session.id, 5_000);
|
|
303
|
+
|
|
304
|
+
// Act
|
|
305
|
+
core.sessionResolve(session.id, "approve", "");
|
|
105
306
|
const resolved = await wait;
|
|
307
|
+
|
|
308
|
+
// Assert
|
|
106
309
|
expect(resolved!.verdict!.kind).toBe("approve");
|
|
107
310
|
});
|
|
108
311
|
|
|
109
312
|
test("wait times out to null; a later wait collects the stored verdict", async () => {
|
|
110
|
-
|
|
111
|
-
const
|
|
313
|
+
// Arrange
|
|
314
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
315
|
+
|
|
316
|
+
// Act
|
|
317
|
+
const first = await core.sessionWait(session.id, 30);
|
|
318
|
+
|
|
319
|
+
// Assert
|
|
112
320
|
expect(first).toBeNull();
|
|
113
|
-
|
|
114
|
-
|
|
321
|
+
|
|
322
|
+
// Act
|
|
323
|
+
core.sessionResolve(session.id, "request_changes", "later");
|
|
324
|
+
const second = await core.sessionWait(session.id, 30);
|
|
325
|
+
|
|
326
|
+
// Assert
|
|
115
327
|
expect(second!.verdict!.summary).toBe("later");
|
|
116
328
|
});
|
|
117
329
|
|
|
118
330
|
test("multiple waiters all resolve", async () => {
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
core.
|
|
331
|
+
// Arrange
|
|
332
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
333
|
+
const waits = [core.sessionWait(session.id, 5_000), core.sessionWait(session.id, 5_000)];
|
|
334
|
+
|
|
335
|
+
// Act
|
|
336
|
+
core.sessionResolve(session.id, "approve", "");
|
|
122
337
|
const results = await Promise.all(waits);
|
|
123
|
-
|
|
338
|
+
|
|
339
|
+
// Assert
|
|
340
|
+
expect(results.every((result) => result?.status === "resolved")).toBe(true);
|
|
124
341
|
});
|
|
125
342
|
});
|
|
126
343
|
|
|
127
344
|
describe("persistence and recovery", () => {
|
|
128
345
|
test("sessions survive a daemon restart", () => {
|
|
129
|
-
|
|
130
|
-
core.
|
|
346
|
+
// Arrange
|
|
347
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
348
|
+
|
|
349
|
+
core.sessionResolve(session.id, "approve", "done");
|
|
350
|
+
|
|
351
|
+
// Act
|
|
131
352
|
const reborn = new DaemonCore(home);
|
|
132
|
-
const recovered = reborn.sessionGet(
|
|
353
|
+
const recovered = reborn.sessionGet(session.id);
|
|
354
|
+
|
|
355
|
+
// Assert
|
|
133
356
|
expect(recovered.verdict!.summary).toBe("done");
|
|
134
357
|
});
|
|
135
358
|
|
|
136
359
|
test("corrupt records are skipped, not fatal, and never deleted", async () => {
|
|
360
|
+
// Arrange
|
|
137
361
|
core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
138
362
|
await Bun.write(join(home, "sessions", "broken.json"), "{ not json");
|
|
139
363
|
const store = new SessionStore(home);
|
|
364
|
+
|
|
365
|
+
// Act
|
|
140
366
|
const report = store.recover();
|
|
367
|
+
|
|
368
|
+
// Assert
|
|
141
369
|
expect(report.recovered.length).toBe(1);
|
|
142
370
|
expect(report.skipped.length).toBe(1);
|
|
143
371
|
expect(await Bun.file(join(home, "sessions", "broken.json")).exists()).toBe(true);
|
|
@@ -146,11 +374,29 @@ describe("persistence and recovery", () => {
|
|
|
146
374
|
|
|
147
375
|
describe("events", () => {
|
|
148
376
|
test("lifecycle events fire in order", () => {
|
|
377
|
+
// Arrange
|
|
149
378
|
const seen: string[] = [];
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
379
|
+
|
|
380
|
+
core.onEvent((event) => seen.push(event.event));
|
|
381
|
+
|
|
382
|
+
// Act
|
|
383
|
+
const session = core.sessionCreate({ workspace: WS, artifact: PLAN });
|
|
384
|
+
|
|
385
|
+
core.sessionAnnotate(session.id, {
|
|
386
|
+
id: "a1",
|
|
387
|
+
kind: "comment",
|
|
388
|
+
anchor: { quote: "thing", prefix: "", suffix: "" },
|
|
389
|
+
body: "b",
|
|
390
|
+
});
|
|
391
|
+
core.sessionResolve(session.id, "approve", "");
|
|
392
|
+
|
|
393
|
+
// Assert
|
|
394
|
+
expect(seen).toEqual([
|
|
395
|
+
"session.created",
|
|
396
|
+
"inbox.changed",
|
|
397
|
+
"session.updated",
|
|
398
|
+
"session.resolved",
|
|
399
|
+
"inbox.changed",
|
|
400
|
+
]);
|
|
155
401
|
});
|
|
156
402
|
});
|