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