@cueloop/daemon 0.1.0-alpha.63 → 0.1.0-alpha.65
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 +6 -3
- package/src/api.test.ts +479 -1
- package/src/api.ts +453 -28
- package/src/capabilities.test.ts +28 -9
- package/src/capabilities.ts +58 -22
- package/src/client.ts +183 -43
- package/src/curate.ts +109 -0
- package/src/diff-hot-reload.test.ts +16 -0
- package/src/diff-watcher.ts +1 -4
- package/src/herdr-pane.test.ts +4 -7
- package/src/herdr-pane.ts +20 -7
- package/src/herdr-split.test.ts +1 -1
- package/src/herdr-split.ts +8 -4
- package/src/herdr-tab-store.ts +10 -13
- package/src/paths.ts +5 -0
- package/src/protocol.ts +28 -0
- package/src/retention.test.ts +3 -3
- package/src/retention.ts +1 -1
- package/src/review.ts +1 -1
- package/src/server.test.ts +258 -54
- package/src/server.ts +102 -13
- package/src/share-blob.test.ts +51 -1
- package/src/share-blob.ts +1 -1
- package/src/store.conformance.test.ts +41 -0
- package/src/store.ts +89 -12
- package/src/testing/store-conformance.ts +194 -0
- package/src/validate.test.ts +60 -3
- package/src/validate.ts +104 -3
package/src/validate.test.ts
CHANGED
|
@@ -57,8 +57,10 @@ describe("parseParams", () => {
|
|
|
57
57
|
throw new Error("should have thrown");
|
|
58
58
|
} catch (err) {
|
|
59
59
|
expect(err).toBeInstanceOf(DaemonError);
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
if (err instanceof DaemonError) {
|
|
61
|
+
expect(err.code).toBe("invalid_params");
|
|
62
|
+
expect(err.message).toContain("session.create: workspace");
|
|
63
|
+
}
|
|
62
64
|
}
|
|
63
65
|
});
|
|
64
66
|
|
|
@@ -172,7 +174,7 @@ describe("validateSessionRecord", () => {
|
|
|
172
174
|
* @cueloop/schema shows up here too.
|
|
173
175
|
*/
|
|
174
176
|
describe("wire pins", () => {
|
|
175
|
-
const keys = (subject:
|
|
177
|
+
const keys = <Subject extends object>(subject: Subject) => Object.keys(subject).sort();
|
|
176
178
|
const entryKeys = (schema: { entries: object }) => Object.keys(schema.entries).sort();
|
|
177
179
|
|
|
178
180
|
const fullMeta: Required<ArtifactMeta> = {
|
|
@@ -190,6 +192,7 @@ describe("wire pins", () => {
|
|
|
190
192
|
prefix: "p",
|
|
191
193
|
suffix: "s",
|
|
192
194
|
blockIndex: 0,
|
|
195
|
+
endBlockIndex: 0,
|
|
193
196
|
start: 0,
|
|
194
197
|
end: 1,
|
|
195
198
|
selector: "div.card",
|
|
@@ -201,6 +204,7 @@ describe("wire pins", () => {
|
|
|
201
204
|
body: "b",
|
|
202
205
|
orphan: false,
|
|
203
206
|
author: "SHA256:abc",
|
|
207
|
+
replyTo: "a0",
|
|
204
208
|
resolution: { revision: 2, source: "agent" },
|
|
205
209
|
createdAt: "now",
|
|
206
210
|
};
|
|
@@ -231,6 +235,36 @@ describe("wire pins", () => {
|
|
|
231
235
|
artifact: fullArtifact,
|
|
232
236
|
revisions: [fullRevision],
|
|
233
237
|
annotations: [fullAnnotation],
|
|
238
|
+
history: {
|
|
239
|
+
entries: [
|
|
240
|
+
{
|
|
241
|
+
id: "e1",
|
|
242
|
+
parentId: null,
|
|
243
|
+
type: "revision",
|
|
244
|
+
by: "agent",
|
|
245
|
+
content: "# P",
|
|
246
|
+
createdAt: "now",
|
|
247
|
+
},
|
|
248
|
+
{ id: "e2", parentId: "e1", type: "comment", annotationId: "a1", createdAt: "now" },
|
|
249
|
+
{ id: "e3", parentId: "e2", type: "comment-removed", annotationId: "a1", createdAt: "now" },
|
|
250
|
+
{ id: "e4", parentId: "e3", type: "verdict", verdict: fullVerdict, createdAt: "now" },
|
|
251
|
+
{
|
|
252
|
+
id: "e5",
|
|
253
|
+
parentId: "e4",
|
|
254
|
+
type: "branch-summary",
|
|
255
|
+
text: "tried a thing",
|
|
256
|
+
abandoned: ["e3"],
|
|
257
|
+
createdAt: "now",
|
|
258
|
+
},
|
|
259
|
+
],
|
|
260
|
+
tips: { main: "e5" },
|
|
261
|
+
branch: "main",
|
|
262
|
+
labels: { e1: "start" },
|
|
263
|
+
},
|
|
264
|
+
curation: [{ path: "src/a.ts", hunkIndex: 0, changeIndex: 1 }],
|
|
265
|
+
shelvedAnnotations: [fullAnnotation],
|
|
266
|
+
parentSessionId: "ses_0",
|
|
267
|
+
shareBranch: "main",
|
|
234
268
|
workingCopy: "# P edited",
|
|
235
269
|
viewedPaths: ["src/a.ts"],
|
|
236
270
|
verdict: fullVerdict,
|
|
@@ -241,6 +275,29 @@ describe("wire pins", () => {
|
|
|
241
275
|
participants: [fullIdentity],
|
|
242
276
|
};
|
|
243
277
|
|
|
278
|
+
test("a history whose tree is broken is refused even when every entry is well-formed", () => {
|
|
279
|
+
// Arrange: a parent cycle
|
|
280
|
+
const record = {
|
|
281
|
+
...fullSession,
|
|
282
|
+
history: {
|
|
283
|
+
entries: [
|
|
284
|
+
{ id: "a", parentId: "b", type: "revision", by: "agent", content: "x", createdAt: "now" },
|
|
285
|
+
{ id: "b", parentId: "a", type: "comment", annotationId: "a1", createdAt: "now" },
|
|
286
|
+
],
|
|
287
|
+
tips: { main: "b" },
|
|
288
|
+
branch: "main",
|
|
289
|
+
labels: {},
|
|
290
|
+
},
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// Act
|
|
294
|
+
const parsed = validateSessionRecord(record);
|
|
295
|
+
|
|
296
|
+
// Assert
|
|
297
|
+
expect(parsed.ok).toBe(false);
|
|
298
|
+
if (!parsed.ok) expect(parsed.error).toContain("history:");
|
|
299
|
+
});
|
|
300
|
+
|
|
244
301
|
test("schema key sets match the schema types", () => {
|
|
245
302
|
expect(entryKeys(WorkspaceSchema)).toEqual(keys(fullWorkspace));
|
|
246
303
|
expect(entryKeys(ArtifactMetaSchema)).toEqual(keys(fullMeta));
|
package/src/validate.ts
CHANGED
|
@@ -20,11 +20,15 @@ import {
|
|
|
20
20
|
type DiffFileContents,
|
|
21
21
|
type Identity,
|
|
22
22
|
type ReviewSession,
|
|
23
|
+
type HunkRejection,
|
|
23
24
|
type Revision,
|
|
25
|
+
type SessionHistory,
|
|
26
|
+
validateHistory,
|
|
24
27
|
type Verdict,
|
|
25
28
|
type WorkspaceKey,
|
|
26
29
|
} from "@cueloop/schema";
|
|
27
30
|
import { DaemonError } from "./errors";
|
|
31
|
+
import type { Request } from "./protocol";
|
|
28
32
|
|
|
29
33
|
/**
|
|
30
34
|
* Drift guard for every hand-mirrored shape below. v.object strips keys it
|
|
@@ -74,6 +78,7 @@ export const AnchorSchema = v.object({
|
|
|
74
78
|
prefix: v.optional(v.string(), ""),
|
|
75
79
|
suffix: v.optional(v.string(), ""),
|
|
76
80
|
blockIndex: v.optional(v.number()),
|
|
81
|
+
endBlockIndex: v.optional(v.number()),
|
|
77
82
|
start: v.optional(v.number()),
|
|
78
83
|
end: v.optional(v.number()),
|
|
79
84
|
selector: v.optional(v.string()),
|
|
@@ -88,6 +93,7 @@ export const AnnotationSchema = v.object({
|
|
|
88
93
|
body: v.string(),
|
|
89
94
|
orphan: v.optional(v.boolean()),
|
|
90
95
|
author: v.optional(v.string()),
|
|
96
|
+
replyTo: v.optional(NonEmpty),
|
|
91
97
|
resolution: v.optional(
|
|
92
98
|
v.object({
|
|
93
99
|
revision: v.number(),
|
|
@@ -136,8 +142,39 @@ export const Params = {
|
|
|
136
142
|
authorName: v.optional(v.string()),
|
|
137
143
|
}),
|
|
138
144
|
"session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
|
|
145
|
+
"session.setParticipantName": v.object({ id: SessionId, author: NonEmpty, name: NonEmpty }),
|
|
139
146
|
"session.setWorkingCopy": v.object({ id: SessionId, workingCopy: v.optional(v.string()) }),
|
|
147
|
+
"session.cutBlock": v.object({
|
|
148
|
+
id: SessionId,
|
|
149
|
+
blockIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
150
|
+
}),
|
|
151
|
+
"session.restoreBlock": v.object({
|
|
152
|
+
id: SessionId,
|
|
153
|
+
baseBlockIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
154
|
+
line: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
155
|
+
}),
|
|
156
|
+
"session.curate": v.object({
|
|
157
|
+
id: SessionId,
|
|
158
|
+
rejections: v.array(
|
|
159
|
+
v.object({
|
|
160
|
+
path: NonEmpty,
|
|
161
|
+
hunkIndex: v.pipe(v.number(), v.integer(), v.minValue(0)),
|
|
162
|
+
changeIndex: v.optional(v.pipe(v.number(), v.integer(), v.minValue(0))),
|
|
163
|
+
} satisfies EntriesOf<HunkRejection>),
|
|
164
|
+
),
|
|
165
|
+
}),
|
|
140
166
|
"session.setViewed": v.object({ id: SessionId, viewedPaths: v.array(v.string()) }),
|
|
167
|
+
"session.navigate": v.object({
|
|
168
|
+
id: SessionId,
|
|
169
|
+
entryId: NonEmpty,
|
|
170
|
+
summary: v.optional(v.string()),
|
|
171
|
+
// stand on this branch first, so a move on another branch is one request
|
|
172
|
+
branch: v.optional(NonEmpty),
|
|
173
|
+
}),
|
|
174
|
+
"session.branch": v.object({ id: SessionId, name: NonEmpty }),
|
|
175
|
+
"session.switch": v.object({ id: SessionId, branch: NonEmpty }),
|
|
176
|
+
"session.label": v.object({ id: SessionId, label: NonEmpty }),
|
|
177
|
+
"session.fork": v.object({ id: SessionId }),
|
|
141
178
|
"session.refreshDiff": v.object({ id: SessionId }),
|
|
142
179
|
"session.setShareId": v.object({ id: SessionId, shareId: NonEmpty }),
|
|
143
180
|
"session.delete": v.object({ id: SessionId }),
|
|
@@ -145,6 +182,10 @@ export const Params = {
|
|
|
145
182
|
id: SessionId,
|
|
146
183
|
annotations: v.array(FullAnnotationSchema),
|
|
147
184
|
participants: v.optional(v.array(IdentitySchema)),
|
|
185
|
+
// the removal entries a share recorded, carried by id so a merge applies each once
|
|
186
|
+
removals: v.optional(
|
|
187
|
+
v.array(v.object({ id: NonEmpty, annotationId: NonEmpty, createdAt: v.string() })),
|
|
188
|
+
),
|
|
148
189
|
}),
|
|
149
190
|
"session.resolve": v.object({
|
|
150
191
|
id: SessionId,
|
|
@@ -159,7 +200,13 @@ export const Params = {
|
|
|
159
200
|
}),
|
|
160
201
|
"events.subscribe": v.object({}),
|
|
161
202
|
"daemon.ping": v.object({}),
|
|
162
|
-
|
|
203
|
+
// the owner token proves ownership; without it a request for owner stays a collaborator
|
|
204
|
+
"daemon.hello": v.object({
|
|
205
|
+
role: v.picklist(["owner", "collaborator", "agent"]),
|
|
206
|
+
token: v.optional(v.string()),
|
|
207
|
+
// the author a non-owner acts as, bound once for the connection
|
|
208
|
+
author: v.optional(NonEmpty),
|
|
209
|
+
}),
|
|
163
210
|
"daemon.shutdown": v.object({}),
|
|
164
211
|
// herdr adapter scratch: the review's opened tab, kept off the session record.
|
|
165
212
|
"herdr.getTab": v.object({ id: SessionId }),
|
|
@@ -175,7 +222,7 @@ export function isKnownMethod(method: string): method is MethodName {
|
|
|
175
222
|
/** Validate params for a method, or throw a DaemonError the client can read. */
|
|
176
223
|
export function parseParams<M extends MethodName>(
|
|
177
224
|
method: M,
|
|
178
|
-
params:
|
|
225
|
+
params: Request["params"],
|
|
179
226
|
): v.InferOutput<(typeof Params)[M]> {
|
|
180
227
|
const result = v.safeParse(Params[method], params ?? {});
|
|
181
228
|
|
|
@@ -205,6 +252,47 @@ export const VerdictSchema = v.object({
|
|
|
205
252
|
resolvedAt: v.string(),
|
|
206
253
|
} satisfies EntriesOf<Verdict>);
|
|
207
254
|
|
|
255
|
+
const EntryBaseEntries = {
|
|
256
|
+
id: NonEmpty,
|
|
257
|
+
parentId: v.nullable(NonEmpty),
|
|
258
|
+
createdAt: v.string(),
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
/** One record of a session's history; the tree pointers plus the entry's own fields. */
|
|
262
|
+
export const SessionEntrySchema = v.variant("type", [
|
|
263
|
+
v.object({
|
|
264
|
+
...EntryBaseEntries,
|
|
265
|
+
type: v.literal("revision"),
|
|
266
|
+
by: v.picklist(["agent", "reviewer"]),
|
|
267
|
+
content: v.string(),
|
|
268
|
+
}),
|
|
269
|
+
v.object({ ...EntryBaseEntries, type: v.literal("comment"), annotationId: NonEmpty }),
|
|
270
|
+
v.object({ ...EntryBaseEntries, type: v.literal("comment-removed"), annotationId: NonEmpty }),
|
|
271
|
+
v.object({ ...EntryBaseEntries, type: v.literal("verdict"), verdict: VerdictSchema }),
|
|
272
|
+
v.object({
|
|
273
|
+
...EntryBaseEntries,
|
|
274
|
+
type: v.literal("branch-summary"),
|
|
275
|
+
text: v.string(),
|
|
276
|
+
abandoned: v.array(NonEmpty),
|
|
277
|
+
}),
|
|
278
|
+
]);
|
|
279
|
+
|
|
280
|
+
export const SessionHistorySchema = v.pipe(
|
|
281
|
+
v.object({
|
|
282
|
+
entries: v.array(SessionEntrySchema),
|
|
283
|
+
tips: v.record(v.string(), NonEmpty),
|
|
284
|
+
branch: NonEmpty,
|
|
285
|
+
labels: v.record(v.string(), v.string()),
|
|
286
|
+
} satisfies EntriesOf<SessionHistory>),
|
|
287
|
+
// the shape is not enough: the tree itself must hold, or a walk hangs or rewires the path
|
|
288
|
+
v.rawCheck(({ dataset, addIssue }) => {
|
|
289
|
+
if (!dataset.typed) return;
|
|
290
|
+
const problem = validateHistory(dataset.value);
|
|
291
|
+
|
|
292
|
+
if (problem !== null) addIssue({ message: `history: ${problem}` });
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
208
296
|
/** Persisted records are validated on recovery: a bad file is skipped, not fatal. */
|
|
209
297
|
export const SessionRecordSchema = v.object({
|
|
210
298
|
schemaVersion: v.literal(SCHEMA_VERSION),
|
|
@@ -213,18 +301,31 @@ export const SessionRecordSchema = v.object({
|
|
|
213
301
|
artifact: ArtifactSchema,
|
|
214
302
|
revisions: v.array(RevisionSchema),
|
|
215
303
|
annotations: v.array(FullAnnotationSchema),
|
|
304
|
+
history: v.optional(SessionHistorySchema),
|
|
305
|
+
curation: v.optional(
|
|
306
|
+
v.array(
|
|
307
|
+
v.object({
|
|
308
|
+
path: NonEmpty,
|
|
309
|
+
hunkIndex: v.number(),
|
|
310
|
+
changeIndex: v.optional(v.number()),
|
|
311
|
+
} satisfies EntriesOf<HunkRejection>),
|
|
312
|
+
),
|
|
313
|
+
),
|
|
216
314
|
workingCopy: v.optional(v.string()),
|
|
217
315
|
viewedPaths: v.optional(v.array(v.string())),
|
|
218
316
|
verdict: v.nullable(VerdictSchema),
|
|
219
317
|
status: v.picklist(["pending", "resolved"]),
|
|
220
318
|
createdAt: v.string(),
|
|
319
|
+
shelvedAnnotations: v.optional(v.array(FullAnnotationSchema)),
|
|
320
|
+
parentSessionId: v.optional(v.string()),
|
|
221
321
|
shareId: v.optional(v.string()),
|
|
322
|
+
shareBranch: v.optional(v.string()),
|
|
222
323
|
owner: v.optional(v.string()),
|
|
223
324
|
participants: v.optional(v.array(IdentitySchema)),
|
|
224
325
|
} satisfies EntriesOf<ReviewSession>);
|
|
225
326
|
|
|
226
327
|
export function validateSessionRecord(
|
|
227
|
-
raw:
|
|
328
|
+
raw: Parameters<typeof v.safeParse>[1],
|
|
228
329
|
): { ok: true; value: v.InferOutput<typeof SessionRecordSchema> } | { ok: false; error: string } {
|
|
229
330
|
const result = v.safeParse(SessionRecordSchema, raw);
|
|
230
331
|
|