@cueloop/daemon 0.1.0-alpha.8 → 0.1.0-alpha.81
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 +37 -22
- package/src/api.test.ts +940 -65
- package/src/api.ts +1095 -88
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +89 -0
- package/src/client.test.ts +24 -0
- package/src/client.ts +669 -86
- package/src/curate.ts +109 -0
- package/src/daemon-version.test.ts +93 -0
- package/src/diff-hot-reload.test.ts +272 -0
- package/src/diff-watcher.ts +274 -0
- package/src/gh.ts +37 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +101 -0
- package/src/ghostty-thread-surface.test.ts +398 -0
- package/src/ghostty-thread-surface.ts +267 -0
- package/src/harness-delivery.test.ts +208 -0
- package/src/harness-state-store.test.ts +65 -0
- package/src/harness-state-store.ts +181 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +92 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +64 -0
- package/src/herdr-thread-surface.test.ts +411 -0
- package/src/herdr-thread-surface.ts +355 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/main.ts +4 -1
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +76 -0
- package/src/pr-poller.ts +74 -0
- package/src/project-files.test.ts +124 -0
- package/src/project-files.ts +56 -0
- package/src/protocol.ts +79 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/server.test.ts +432 -42
- package/src/server.ts +423 -89
- package/src/share-blob.test.ts +112 -0
- package/src/share-blob.ts +55 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +159 -0
- package/src/store.ts +264 -34
- package/src/testing/store-conformance.ts +194 -0
- package/src/thread-review.test.ts +394 -0
- package/src/thread-review.ts +314 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +214 -35
- package/src/validate.ts +306 -25
- package/src/version.ts +6 -0
- package/src/working-tree.ts +149 -0
- package/src/review.test.ts +0 -167
- package/src/review.ts +0 -174
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { loadGhosttyThreadSurface, loadHerdrThreadSurface } from "./thread-surface-config";
|
|
6
|
+
|
|
7
|
+
let dir: string;
|
|
8
|
+
let userPath: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "cueloop-herdr-config-"));
|
|
12
|
+
userPath = join(dir, "user.toml");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("personal Ghostty Thread surface config", () => {
|
|
16
|
+
test("defaults to tab and accepts pane, window, or none", () => {
|
|
17
|
+
expect(loadGhosttyThreadSurface(userPath)).toBe("tab");
|
|
18
|
+
|
|
19
|
+
for (const mode of ["pane", "window", "none"] as const) {
|
|
20
|
+
writeFileSync(userPath, `[integrations.ghostty]\nthread_surface = "${mode}"\n`);
|
|
21
|
+
expect(loadGhosttyThreadSurface(userPath)).toBe(mode);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
writeFileSync(userPath, '[integrations.ghostty]\nthread_surface = "invalid"\n');
|
|
25
|
+
expect(loadGhosttyThreadSurface(userPath)).toBe("tab");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("ignores repository config", () => {
|
|
29
|
+
const repoConfig = join(dir, "repo", ".cueloop", "config.toml");
|
|
30
|
+
|
|
31
|
+
mkdirSync(join(dir, "repo", ".cueloop"), { recursive: true });
|
|
32
|
+
writeFileSync(userPath, '[integrations.ghostty]\nthread_surface = "window"\n');
|
|
33
|
+
writeFileSync(repoConfig, '[integrations.ghostty]\nthread_surface = "none"\n');
|
|
34
|
+
|
|
35
|
+
expect(loadGhosttyThreadSurface(userPath)).toBe("window");
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
rmSync(dir, { recursive: true, force: true });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("personal Herdr Thread surface config", () => {
|
|
44
|
+
test("defaults to tab and accepts pane or none", () => {
|
|
45
|
+
expect(loadHerdrThreadSurface(userPath)).toBe("tab");
|
|
46
|
+
writeFileSync(userPath, '[integrations.herdr]\nthread_surface = "pane"\n');
|
|
47
|
+
expect(loadHerdrThreadSurface(userPath)).toBe("pane");
|
|
48
|
+
writeFileSync(userPath, '[integrations.herdr]\nthread_surface = "none"\n');
|
|
49
|
+
expect(loadHerdrThreadSurface(userPath)).toBe("none");
|
|
50
|
+
writeFileSync(userPath, '[integrations.herdr]\nthread_surface = "invalid"\n');
|
|
51
|
+
expect(loadHerdrThreadSurface(userPath)).toBe("tab");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("never reads a repository's terminal automation setting", () => {
|
|
55
|
+
const repoConfig = join(dir, "repo", ".cueloop", "config.toml");
|
|
56
|
+
|
|
57
|
+
mkdirSync(join(dir, "repo", ".cueloop"), { recursive: true });
|
|
58
|
+
writeFileSync(userPath, '[integrations.herdr]\nthread_surface = "pane"\n');
|
|
59
|
+
writeFileSync(repoConfig, '[integrations.herdr]\nthread_surface = "none"\n');
|
|
60
|
+
|
|
61
|
+
expect(loadHerdrThreadSurface(userPath)).toBe("pane");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { parse as parseToml } from "smol-toml";
|
|
5
|
+
import * as v from "valibot";
|
|
6
|
+
|
|
7
|
+
/** Placement of a cueloop Thread inside Herdr. */
|
|
8
|
+
export type HerdrThreadSurface = "tab" | "pane" | "none";
|
|
9
|
+
/** Placement of a cueloop Thread inside Ghostty. */
|
|
10
|
+
export type GhosttyThreadSurface = "tab" | "pane" | "window" | "none";
|
|
11
|
+
|
|
12
|
+
const PersonalThreadSurfaceSchema = v.object({
|
|
13
|
+
integrations: v.optional(
|
|
14
|
+
v.object({
|
|
15
|
+
herdr: v.optional(
|
|
16
|
+
v.object({
|
|
17
|
+
thread_surface: v.fallback(v.optional(v.picklist(["tab", "pane", "none"])), undefined),
|
|
18
|
+
}),
|
|
19
|
+
),
|
|
20
|
+
ghostty: v.optional(
|
|
21
|
+
v.object({
|
|
22
|
+
thread_surface: v.fallback(
|
|
23
|
+
v.optional(v.picklist(["tab", "pane", "window", "none"])),
|
|
24
|
+
undefined,
|
|
25
|
+
),
|
|
26
|
+
}),
|
|
27
|
+
),
|
|
28
|
+
}),
|
|
29
|
+
),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** Read Herdr placement from the personal TOML file only; invalid values use the tab default. */
|
|
33
|
+
export function loadHerdrThreadSurface(userConfigPath?: string): HerdrThreadSurface {
|
|
34
|
+
return (
|
|
35
|
+
loadPersonalThreadSurfaceConfig(userConfigPath)?.integrations?.herdr?.thread_surface ?? "tab"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Read Ghostty placement from the personal TOML file only. */
|
|
40
|
+
export function loadGhosttyThreadSurface(userConfigPath?: string): GhosttyThreadSurface {
|
|
41
|
+
return (
|
|
42
|
+
loadPersonalThreadSurfaceConfig(userConfigPath)?.integrations?.ghostty?.thread_surface ?? "tab"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadPersonalThreadSurfaceConfig(userConfigPath?: string) {
|
|
47
|
+
const path =
|
|
48
|
+
userConfigPath ??
|
|
49
|
+
process.env.CUELOOP_CONFIG ??
|
|
50
|
+
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "cueloop", "config.toml");
|
|
51
|
+
|
|
52
|
+
if (!existsSync(path)) return null;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const parsed = v.safeParse(PersonalThreadSurfaceSchema, parseToml(readFileSync(path, "utf8")));
|
|
56
|
+
|
|
57
|
+
return parsed.success ? parsed.output : null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/validate.test.ts
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
+
ThreadRecordSchema,
|
|
16
|
+
MessageSchema,
|
|
15
17
|
WorkspaceSchema,
|
|
16
18
|
isKnownMethod,
|
|
17
19
|
parseParams,
|
|
18
|
-
|
|
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
|
|
29
|
+
type Identity,
|
|
30
|
+
type Thread,
|
|
27
31
|
type Revision,
|
|
28
|
-
type
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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(() =>
|
|
95
|
+
expect(() =>
|
|
96
|
+
parseParams("session.wait", { id: "ses_1", timeoutMs: 99 * 60 * 60 * 1000 }),
|
|
97
|
+
).toThrow();
|
|
76
98
|
});
|
|
77
99
|
|
|
78
|
-
test("
|
|
79
|
-
expect(parseParams("session.
|
|
80
|
-
expect(() => parseParams("session.
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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("
|
|
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
|
-
|
|
144
|
+
message: null,
|
|
106
145
|
status: "pending",
|
|
107
146
|
createdAt: "now",
|
|
108
147
|
};
|
|
109
148
|
|
|
110
149
|
test("accepts a valid record", () => {
|
|
111
|
-
expect(
|
|
150
|
+
expect(validateThreadRecord(record).ok).toBe(true);
|
|
112
151
|
});
|
|
113
152
|
|
|
114
153
|
test("rejects a foreign schema version with a readable reason", () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
|
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 = (
|
|
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> = {
|
|
155
|
-
|
|
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> = {
|
|
158
|
-
|
|
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
|
-
|
|
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(
|
|
181
|
-
expect(entryKeys(
|
|
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 =
|
|
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
|
+
});
|