@cueloop/daemon 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.
Files changed (50) hide show
  1. package/package.json +34 -22
  2. package/src/api.test.ts +925 -65
  3. package/src/api.ts +1060 -88
  4. package/src/capabilities.test.ts +57 -0
  5. package/src/capabilities.ts +84 -0
  6. package/src/client.test.ts +24 -0
  7. package/src/client.ts +526 -70
  8. package/src/curate.ts +109 -0
  9. package/src/daemon-version.test.ts +93 -0
  10. package/src/diff-hot-reload.test.ts +272 -0
  11. package/src/diff-watcher.ts +274 -0
  12. package/src/gh.ts +37 -0
  13. package/src/harness-delivery.test.ts +193 -0
  14. package/src/harness-state-store.test.ts +65 -0
  15. package/src/harness-state-store.ts +182 -0
  16. package/src/herdr-pane.test.ts +240 -0
  17. package/src/herdr-pane.ts +167 -0
  18. package/src/herdr-split.test.ts +106 -0
  19. package/src/herdr-split.ts +92 -0
  20. package/src/herdr-tab-store.test.ts +96 -0
  21. package/src/herdr-tab-store.ts +66 -0
  22. package/src/index.ts +8 -9
  23. package/src/interruptible-wait.test.ts +127 -0
  24. package/src/interruptible-wait.ts +66 -0
  25. package/src/main.ts +4 -1
  26. package/src/paths.ts +44 -0
  27. package/src/pr-poller.test.ts +76 -0
  28. package/src/pr-poller.ts +74 -0
  29. package/src/project-files.test.ts +124 -0
  30. package/src/project-files.ts +56 -0
  31. package/src/protocol.ts +79 -7
  32. package/src/retention.test.ts +191 -0
  33. package/src/retention.ts +123 -0
  34. package/src/server.test.ts +430 -42
  35. package/src/server.ts +362 -84
  36. package/src/share-blob.test.ts +112 -0
  37. package/src/share-blob.ts +55 -0
  38. package/src/single-instance.test.ts +72 -11
  39. package/src/store.conformance.test.ts +41 -0
  40. package/src/store.jsonl.test.ts +159 -0
  41. package/src/store.ts +264 -34
  42. package/src/testing/store-conformance.ts +194 -0
  43. package/src/thread-review.test.ts +394 -0
  44. package/src/thread-review.ts +324 -0
  45. package/src/validate.test.ts +214 -35
  46. package/src/validate.ts +296 -25
  47. package/src/version.ts +6 -0
  48. package/src/working-tree.ts +149 -0
  49. package/src/review.test.ts +0 -167
  50. package/src/review.ts +0 -174
@@ -1,4 +1,5 @@
1
1
  import { describe, expect, test } from "bun:test";
2
+ import * as v from "valibot";
2
3
  import { mkdtempSync, rmSync } from "node:fs";
3
4
  import { tmpdir } from "node:os";
4
5
  import { join } from "node:path";
@@ -9,23 +10,26 @@ import {
9
10
  AnnotationSchema,
10
11
  ArtifactMetaSchema,
11
12
  ArtifactSchema,
13
+ IdentitySchema,
12
14
  RevisionSchema,
13
- SessionRecordSchema,
14
- VerdictSchema,
15
+ ThreadRecordSchema,
16
+ MessageSchema,
15
17
  WorkspaceSchema,
16
18
  isKnownMethod,
17
19
  parseParams,
18
- validateSessionRecord,
20
+ validateThreadRecord,
19
21
  } from "./validate";
20
22
  import {
23
+ ARTIFACT_TYPES,
21
24
  SCHEMA_VERSION,
22
25
  type Anchor,
23
26
  type Annotation,
24
27
  type Artifact,
25
28
  type ArtifactMeta,
26
- type ReviewSession,
29
+ type Identity,
30
+ type Thread,
27
31
  type Revision,
28
- type Verdict,
32
+ type Message,
29
33
  type WorkspaceKey,
30
34
  } from "@cueloop/schema";
31
35
 
@@ -38,11 +42,14 @@ describe("method allowlist", () => {
38
42
 
39
43
  describe("parseParams", () => {
40
44
  test("accepts a well-formed create and defaults meta", () => {
41
- const p = parseParams("session.create", {
45
+ // Act
46
+ const params = parseParams("session.create", {
42
47
  workspace: { repoRoot: "/repo", branch: "main" },
43
48
  artifact: { type: "plan", content: "# P" },
44
49
  });
45
- expect(p.artifact.meta).toEqual({});
50
+
51
+ // Assert
52
+ expect(params.artifact.meta).toEqual({});
46
53
  });
47
54
 
48
55
  test("rejects a missing workspace with a pathed message", () => {
@@ -51,8 +58,21 @@ describe("parseParams", () => {
51
58
  throw new Error("should have thrown");
52
59
  } catch (err) {
53
60
  expect(err).toBeInstanceOf(DaemonError);
54
- expect((err as DaemonError).code).toBe("invalid_params");
55
- expect((err as DaemonError).message).toContain("session.create: workspace");
61
+ if (err instanceof DaemonError) {
62
+ expect(err.code).toBe("invalid_params");
63
+ expect(err.message).toContain("session.create: workspace");
64
+ }
65
+ }
66
+ });
67
+
68
+ test("accepts every artifact type in the schema union", () => {
69
+ for (const type of ARTIFACT_TYPES) {
70
+ const params = parseParams("session.create", {
71
+ workspace: { repoRoot: "/repo", branch: "main" },
72
+ artifact: { type, content: "body" },
73
+ });
74
+
75
+ expect(params.artifact.type).toBe(type);
56
76
  }
57
77
  });
58
78
 
@@ -72,21 +92,40 @@ describe("parseParams", () => {
72
92
  test("defaults and clamps the wait timeout", () => {
73
93
  expect(parseParams("session.wait", { id: "ses_1" }).timeoutMs).toBe(60_000);
74
94
  expect(() => parseParams("session.wait", { id: "ses_1", timeoutMs: -5 })).toThrow();
75
- expect(() => parseParams("session.wait", { id: "ses_1", timeoutMs: 99 * 60 * 60 * 1000 })).toThrow();
95
+ expect(() =>
96
+ parseParams("session.wait", { id: "ses_1", timeoutMs: 99 * 60 * 60 * 1000 }),
97
+ ).toThrow();
76
98
  });
77
99
 
78
- test("verdict kinds are closed", () => {
79
- expect(parseParams("session.resolve", { id: "s", verdictKind: "approve" }).summary).toBe("");
80
- expect(() => parseParams("session.resolve", { id: "s", verdictKind: "lgtm" })).toThrow(/verdictKind/);
100
+ test("message kinds are closed", () => {
101
+ expect(parseParams("session.sendMessage", { id: "s", outcome: "approved" }).summary).toBe("");
102
+ expect(() => parseParams("session.sendMessage", { id: "s", outcome: "lgtm" })).toThrow(
103
+ /outcome/,
104
+ );
81
105
  });
82
106
 
83
107
  test("annotation kinds stay open (extension kinds are allowed)", () => {
84
- const p = parseParams("session.annotate", {
108
+ // Act
109
+ const params = parseParams("session.annotate", {
85
110
  id: "s",
86
111
  annotation: { id: "a1", kind: "praise", anchor: { quote: "x" }, body: "nice" },
87
112
  });
88
- expect(p.annotation.kind).toBe("praise");
89
- expect(p.annotation.anchor.prefix).toBe("");
113
+
114
+ // Assert
115
+ expect(params.annotation.kind).toBe("praise");
116
+ expect(params.annotation.anchor.prefix).toBe("");
117
+ });
118
+
119
+ test("setViewed takes a full path list and rejects non-string entries", () => {
120
+ expect(
121
+ parseParams("session.setViewed", { id: "s", viewedPaths: ["src/a.ts", "src/b.ts"] })
122
+ .viewedPaths,
123
+ ).toEqual(["src/a.ts", "src/b.ts"]);
124
+ expect(parseParams("session.setViewed", { id: "s", viewedPaths: [] }).viewedPaths).toEqual([]);
125
+ expect(() => parseParams("session.setViewed", { id: "s", viewedPaths: [1] })).toThrow(
126
+ /viewedPaths/,
127
+ );
128
+ expect(() => parseParams("session.setViewed", { id: "s" })).toThrow(/viewedPaths/);
90
129
  });
91
130
 
92
131
  test("null params are treated as empty", () => {
@@ -94,7 +133,7 @@ describe("parseParams", () => {
94
133
  });
95
134
  });
96
135
 
97
- describe("validateSessionRecord", () => {
136
+ describe("validateThreadRecord", () => {
98
137
  const record = {
99
138
  schemaVersion: SCHEMA_VERSION,
100
139
  id: "ses_1",
@@ -102,72 +141,185 @@ describe("validateSessionRecord", () => {
102
141
  artifact: { type: "plan", content: "# P", meta: {} },
103
142
  revisions: [{ revision: 1, content: "# P", submittedAt: "now" }],
104
143
  annotations: [],
105
- verdict: null,
144
+ message: null,
106
145
  status: "pending",
107
146
  createdAt: "now",
108
147
  };
109
148
 
110
149
  test("accepts a valid record", () => {
111
- expect(validateSessionRecord(record).ok).toBe(true);
150
+ expect(validateThreadRecord(record).ok).toBe(true);
112
151
  });
113
152
 
114
153
  test("rejects a foreign schema version with a readable reason", () => {
115
- const r = validateSessionRecord({ ...record, schemaVersion: "99" });
116
- expect(r.ok).toBe(false);
117
- if (!r.ok) expect(r.error).toContain("schemaVersion");
154
+ // Act
155
+ const result = validateThreadRecord({ ...record, schemaVersion: "99" });
156
+
157
+ // Assert
158
+ expect(result.ok).toBe(false);
159
+ if (!result.ok) expect(result.error).toContain("schemaVersion");
118
160
  });
119
161
 
120
162
  test("rejects a structurally broken record", () => {
121
- const r = validateSessionRecord({ ...record, revisions: "nope" });
122
- expect(r.ok).toBe(false);
163
+ // Act
164
+ const result = validateThreadRecord({ ...record, revisions: "nope" });
165
+
166
+ // Assert
167
+ expect(result.ok).toBe(false);
123
168
  });
124
169
  });
125
170
 
126
171
  /**
127
- * Wire pins (#70): every schema's entries must cover exactly the keys of the
172
+ * Wire pins: every schema's entries must cover exactly the keys of the
128
173
  * schema type it mirrors. Runtime complement to the compile-time EntriesOf
129
174
  * check - the samples are fully populated and typed, so a new field in
130
175
  * @cueloop/schema shows up here too.
131
176
  */
132
177
  describe("wire pins", () => {
133
- const keys = (o: object) => Object.keys(o).sort();
178
+ const keys = <Subject extends object>(subject: Subject) => Object.keys(subject).sort();
134
179
  const entryKeys = (schema: { entries: object }) => Object.keys(schema.entries).sort();
135
180
 
136
181
  const fullMeta: Required<ArtifactMeta> = {
182
+ workflow: "plan",
137
183
  cwd: "/repo",
138
184
  agent: "claude-code",
139
185
  agentSessionId: "sess-1",
140
186
  planPath: "/repo/plan.md",
187
+ prototypePath: "/repo/proto.html",
141
188
  pr: "org/repo#1",
142
189
  herdrPane: "%7",
143
190
  title: "Plan",
191
+ workbench: false,
192
+ snapshot: false,
193
+ };
194
+ const fullAnchor: Required<Anchor> = {
195
+ quote: "q",
196
+ prefix: "p",
197
+ suffix: "s",
198
+ blockIndex: 0,
199
+ endBlockIndex: 0,
200
+ start: 0,
201
+ end: 1,
202
+ selector: "div.card",
144
203
  };
145
- const fullAnchor: Required<Anchor> = { quote: "q", prefix: "p", suffix: "s", blockIndex: 0, start: 0, end: 1 };
146
204
  const fullAnnotation: Required<Annotation> = {
147
205
  id: "a1",
148
206
  kind: "comment",
149
207
  anchor: fullAnchor,
208
+ target: { kind: "file", path: "src/x.ts", rev: "worktree" },
150
209
  body: "b",
151
210
  orphan: false,
211
+ author: "SHA256:abc",
212
+ replyTo: "a0",
213
+ resolution: { revision: 2, source: "agent" },
152
214
  createdAt: "now",
153
215
  };
154
- const fullArtifact: Required<Artifact> = { type: "plan", content: "# P", meta: fullMeta };
155
- const fullVerdict: Required<Verdict> = { kind: "approve", summary: "", feedback: "", resolvedAt: "now" };
216
+ const fullArtifact: Required<Artifact> = {
217
+ type: "plan",
218
+ content: "# P",
219
+ meta: fullMeta,
220
+ files: [{ path: "src/a.ts", oldContents: "old\n", newContents: "new\n", status: "modified" }],
221
+ };
222
+ const fullMessage: Required<Message> = {
223
+ id: "msg_1",
224
+ outcome: "approved",
225
+ summary: "",
226
+ body: "",
227
+ sentAt: "now",
228
+ };
156
229
  const fullRevision: Required<Revision> = { revision: 1, content: "# P", submittedAt: "now" };
157
- const fullWorkspace: Required<WorkspaceKey> = { repoRoot: "/repo", branch: "main" };
158
- const fullSession: Required<ReviewSession> = {
230
+ const fullWorkspace: Required<WorkspaceKey> = {
231
+ repoRoot: "/repo",
232
+ branch: "main",
233
+ rootCommit: "11e7abbcf507a13ae5e4e7559c4b42dbb52237fe",
234
+ remote: "git@github.com:acme/repo.git",
235
+ };
236
+ const fullIdentity: Required<Identity> = {
237
+ id: "SHA256:abc",
238
+ provider: "ssh",
239
+ name: "Al",
240
+ handle: "abc",
241
+ };
242
+ const fullSession: Required<Thread> = {
159
243
  schemaVersion: SCHEMA_VERSION,
160
244
  id: "ses_1",
161
245
  workspace: fullWorkspace,
162
246
  artifact: fullArtifact,
163
247
  revisions: [fullRevision],
164
248
  annotations: [fullAnnotation],
249
+ history: {
250
+ entries: [
251
+ {
252
+ id: "e1",
253
+ parentId: null,
254
+ type: "revision",
255
+ by: "agent",
256
+ content: "# P",
257
+ createdAt: "now",
258
+ },
259
+ { id: "e2", parentId: "e1", type: "comment", annotationId: "a1", createdAt: "now" },
260
+ { id: "e3", parentId: "e2", type: "comment-removed", annotationId: "a1", createdAt: "now" },
261
+ { id: "e4", parentId: "e3", type: "message", message: fullMessage, createdAt: "now" },
262
+ {
263
+ id: "e5",
264
+ parentId: "e4",
265
+ type: "branch-summary",
266
+ text: "tried a thing",
267
+ abandoned: ["e3"],
268
+ createdAt: "now",
269
+ },
270
+ ],
271
+ tips: { main: "e5" },
272
+ branch: "main",
273
+ labels: { e1: "start" },
274
+ },
275
+ curation: [{ path: "src/a.ts", hunkIndex: 0, changeIndex: 1 }],
276
+ shelvedAnnotations: [fullAnnotation],
277
+ parentSessionId: "ses_0",
278
+ shareBranch: "main",
165
279
  workingCopy: "# P edited",
166
- verdict: fullVerdict,
280
+ viewedPaths: ["src/a.ts"],
281
+ message: fullMessage,
167
282
  status: "pending",
168
283
  createdAt: "now",
284
+ shares: [
285
+ {
286
+ id: "p_abc123xy",
287
+ name: "review link",
288
+ requireAuth: true,
289
+ allowlist: ["octocat"],
290
+ owner: "SHA256:owner",
291
+ shareBranch: "main",
292
+ },
293
+ ],
294
+ shareId: "p_abc123xy",
295
+ owner: "SHA256:owner",
296
+ access: { githubLogins: ["octocat"] },
297
+ participants: [fullIdentity],
169
298
  };
170
299
 
300
+ test("a history whose tree is broken is refused even when every entry is well-formed", () => {
301
+ // Arrange: a parent cycle
302
+ const record = {
303
+ ...fullSession,
304
+ history: {
305
+ entries: [
306
+ { id: "a", parentId: "b", type: "revision", by: "agent", content: "x", createdAt: "now" },
307
+ { id: "b", parentId: "a", type: "comment", annotationId: "a1", createdAt: "now" },
308
+ ],
309
+ tips: { main: "b" },
310
+ branch: "main",
311
+ labels: {},
312
+ },
313
+ };
314
+
315
+ // Act
316
+ const parsed = validateThreadRecord(record);
317
+
318
+ // Assert
319
+ expect(parsed.ok).toBe(false);
320
+ if (!parsed.ok) expect(parsed.error).toContain("history:");
321
+ });
322
+
171
323
  test("schema key sets match the schema types", () => {
172
324
  expect(entryKeys(WorkspaceSchema)).toEqual(keys(fullWorkspace));
173
325
  expect(entryKeys(ArtifactMetaSchema)).toEqual(keys(fullMeta));
@@ -175,27 +327,54 @@ describe("wire pins", () => {
175
327
  expect(entryKeys(AnchorSchema)).toEqual(keys(fullAnchor));
176
328
  // wire annotations arrive without createdAt; the daemon stamps it
177
329
  const { createdAt: _stamped, ...wireAnnotation } = fullAnnotation;
330
+
178
331
  expect(entryKeys(AnnotationSchema)).toEqual(keys(wireAnnotation));
179
332
  expect(entryKeys(RevisionSchema)).toEqual(keys(fullRevision));
180
- expect(entryKeys(VerdictSchema)).toEqual(keys(fullVerdict));
181
- expect(entryKeys(SessionRecordSchema)).toEqual(keys(fullSession));
333
+ expect(entryKeys(MessageSchema)).toEqual(keys(fullMessage));
334
+ expect(entryKeys(IdentitySchema)).toEqual(keys(fullIdentity));
335
+ expect(entryKeys(ThreadRecordSchema)).toEqual(keys(fullSession));
182
336
  // persisted annotations carry the stamped createdAt
183
- const stored = SessionRecordSchema.entries.annotations.item;
337
+ const stored = ThreadRecordSchema.entries.annotations.item;
338
+
184
339
  expect(entryKeys(stored)).toEqual(keys(fullAnnotation));
185
340
  });
186
341
 
187
342
  test("a fully-populated meta survives validation and DaemonCore unchanged", () => {
188
343
  const home = mkdtempSync(join(tmpdir(), "cueloop-val-"));
344
+
189
345
  try {
346
+ // Arrange
190
347
  const params = parseParams("session.create", {
191
348
  workspace: fullWorkspace,
192
349
  artifact: { type: "plan", content: "# P", meta: fullMeta },
193
350
  });
194
351
  const core = new DaemonCore(home);
352
+
353
+ // Act
195
354
  const created = core.sessionCreate(params);
355
+
356
+ // Assert
196
357
  expect(core.sessionGet(created.id).artifact.meta).toEqual(fullMeta);
197
358
  } finally {
198
359
  rmSync(home, { recursive: true, force: true });
199
360
  }
200
361
  });
201
362
  });
363
+
364
+ describe("IdentitySchema provider", () => {
365
+ test("accepts a verified github identity", () => {
366
+ const parsed = v.safeParse(IdentitySchema, {
367
+ id: "SHA256:abc",
368
+ provider: "github",
369
+ name: "markus",
370
+ });
371
+
372
+ expect(parsed.success).toBe(true);
373
+ });
374
+
375
+ test("rejects an unknown provider", () => {
376
+ const parsed = v.safeParse(IdentitySchema, { id: "SHA256:abc", provider: "email" });
377
+
378
+ expect(parsed.success).toBe(false);
379
+ });
380
+ });