@cueloop/daemon 0.1.0-alpha.10 → 0.1.0-alpha.101
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/README.md +7 -0
- package/SDK.md +102 -0
- package/package.json +44 -22
- package/src/agent-tools.ts +98 -0
- package/src/agent-turn.test.ts +24 -0
- package/src/agent-turn.ts +34 -0
- package/src/api.test.ts +1081 -65
- package/src/api.ts +1425 -89
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +95 -0
- package/src/client-errors.ts +24 -0
- package/src/client-reliability.test.ts +189 -0
- package/src/client.test.ts +37 -0
- package/src/client.ts +820 -94
- package/src/curate.ts +109 -0
- package/src/daemon-sdk-effect.ts +113 -0
- package/src/daemon-sdk.test.ts +306 -0
- package/src/daemon-sdk.ts +397 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +419 -0
- package/src/diff-watcher.test.ts +117 -0
- package/src/diff-watcher.ts +326 -0
- package/src/examples/daemon-sdk.ts +55 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +74 -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 +269 -0
- package/src/harness-diagnostics.test.ts +26 -0
- package/src/harness-diagnostics.ts +30 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +213 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +85 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +66 -0
- package/src/herdr-thread-surface.test.ts +465 -0
- package/src/herdr-thread-surface.ts +378 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/jj-working-tree.ts +131 -0
- package/src/main.ts +4 -1
- package/src/operation-receipts.test.ts +111 -0
- package/src/operation-receipts.ts +98 -0
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +73 -0
- package/src/project-files.test.ts +134 -0
- package/src/project-files.ts +68 -0
- package/src/protocol.ts +78 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/sdk-identities.ts +35 -0
- package/src/server.test.ts +502 -42
- package/src/server.ts +631 -88
- package/src/share-blob.test.ts +119 -0
- package/src/share-blob.ts +57 -0
- package/src/shared-agent-protocol.ts +30 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +173 -0
- package/src/store.ts +267 -34
- package/src/testing/store-conformance.ts +192 -0
- package/src/thread-agent-validation.ts +121 -0
- package/src/thread-agent.test.ts +719 -0
- package/src/thread-agent.ts +835 -0
- package/src/thread-review.test.ts +483 -0
- package/src/thread-review.ts +382 -0
- package/src/thread-subscription.test.ts +174 -0
- package/src/thread-subscription.ts +129 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +279 -37
- package/src/validate.ts +422 -33
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +229 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +150 -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,11 +58,48 @@ 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
|
|
|
79
|
+
test.each([
|
|
80
|
+
[
|
|
81
|
+
[
|
|
82
|
+
{ start: 2, end: 4, quote: "ab" },
|
|
83
|
+
{ start: 1, end: 2, quote: "c" },
|
|
84
|
+
],
|
|
85
|
+
],
|
|
86
|
+
[
|
|
87
|
+
[
|
|
88
|
+
{ start: 1, end: 4, quote: "abc" },
|
|
89
|
+
{ start: 3, end: 5, quote: "de" },
|
|
90
|
+
],
|
|
91
|
+
],
|
|
92
|
+
[[{ start: 1, end: 4, quote: "ab" }]],
|
|
93
|
+
])("rejects malformed text Cut provenance", (textCuts) => {
|
|
94
|
+
expect(() =>
|
|
95
|
+
parseParams("session.setWorkingCopy", {
|
|
96
|
+
id: "ses_1",
|
|
97
|
+
workingCopy: "remaining",
|
|
98
|
+
textCuts,
|
|
99
|
+
}),
|
|
100
|
+
).toThrow("text Cuts must be ordered, non-overlapping, and match their quote lengths");
|
|
101
|
+
});
|
|
102
|
+
|
|
59
103
|
test("rejects an unknown artifact type", () => {
|
|
60
104
|
expect(() =>
|
|
61
105
|
parseParams("session.create", {
|
|
@@ -72,21 +116,40 @@ describe("parseParams", () => {
|
|
|
72
116
|
test("defaults and clamps the wait timeout", () => {
|
|
73
117
|
expect(parseParams("session.wait", { id: "ses_1" }).timeoutMs).toBe(60_000);
|
|
74
118
|
expect(() => parseParams("session.wait", { id: "ses_1", timeoutMs: -5 })).toThrow();
|
|
75
|
-
expect(() =>
|
|
119
|
+
expect(() =>
|
|
120
|
+
parseParams("session.wait", { id: "ses_1", timeoutMs: 99 * 60 * 60 * 1000 }),
|
|
121
|
+
).toThrow();
|
|
76
122
|
});
|
|
77
123
|
|
|
78
|
-
test("
|
|
79
|
-
expect(parseParams("session.
|
|
80
|
-
expect(() => parseParams("session.
|
|
124
|
+
test("message kinds are closed", () => {
|
|
125
|
+
expect(parseParams("session.sendMessage", { id: "s", outcome: "approved" }).summary).toBe("");
|
|
126
|
+
expect(() => parseParams("session.sendMessage", { id: "s", outcome: "lgtm" })).toThrow(
|
|
127
|
+
/outcome/,
|
|
128
|
+
);
|
|
81
129
|
});
|
|
82
130
|
|
|
83
131
|
test("annotation kinds stay open (extension kinds are allowed)", () => {
|
|
84
|
-
|
|
132
|
+
// Act
|
|
133
|
+
const params = parseParams("session.annotate", {
|
|
85
134
|
id: "s",
|
|
86
135
|
annotation: { id: "a1", kind: "praise", anchor: { quote: "x" }, body: "nice" },
|
|
87
136
|
});
|
|
88
|
-
|
|
89
|
-
|
|
137
|
+
|
|
138
|
+
// Assert
|
|
139
|
+
expect(params.annotation.kind).toBe("praise");
|
|
140
|
+
expect(params.annotation.anchor.prefix).toBe("");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("setViewed takes a full path list and rejects non-string entries", () => {
|
|
144
|
+
expect(
|
|
145
|
+
parseParams("session.setViewed", { id: "s", viewedPaths: ["src/a.ts", "src/b.ts"] })
|
|
146
|
+
.viewedPaths,
|
|
147
|
+
).toEqual(["src/a.ts", "src/b.ts"]);
|
|
148
|
+
expect(parseParams("session.setViewed", { id: "s", viewedPaths: [] }).viewedPaths).toEqual([]);
|
|
149
|
+
expect(() => parseParams("session.setViewed", { id: "s", viewedPaths: [1] })).toThrow(
|
|
150
|
+
/viewedPaths/,
|
|
151
|
+
);
|
|
152
|
+
expect(() => parseParams("session.setViewed", { id: "s" })).toThrow(/viewedPaths/);
|
|
90
153
|
});
|
|
91
154
|
|
|
92
155
|
test("null params are treated as empty", () => {
|
|
@@ -94,7 +157,7 @@ describe("parseParams", () => {
|
|
|
94
157
|
});
|
|
95
158
|
});
|
|
96
159
|
|
|
97
|
-
describe("
|
|
160
|
+
describe("validateThreadRecord", () => {
|
|
98
161
|
const record = {
|
|
99
162
|
schemaVersion: SCHEMA_VERSION,
|
|
100
163
|
id: "ses_1",
|
|
@@ -102,72 +165,224 @@ describe("validateSessionRecord", () => {
|
|
|
102
165
|
artifact: { type: "plan", content: "# P", meta: {} },
|
|
103
166
|
revisions: [{ revision: 1, content: "# P", submittedAt: "now" }],
|
|
104
167
|
annotations: [],
|
|
105
|
-
|
|
168
|
+
message: null,
|
|
106
169
|
status: "pending",
|
|
107
170
|
createdAt: "now",
|
|
108
171
|
};
|
|
109
172
|
|
|
110
173
|
test("accepts a valid record", () => {
|
|
111
|
-
expect(
|
|
174
|
+
expect(validateThreadRecord(record).ok).toBe(true);
|
|
112
175
|
});
|
|
113
176
|
|
|
114
177
|
test("rejects a foreign schema version with a readable reason", () => {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
178
|
+
// Act
|
|
179
|
+
const result = validateThreadRecord({ ...record, schemaVersion: "99" });
|
|
180
|
+
|
|
181
|
+
// Assert
|
|
182
|
+
expect(result.ok).toBe(false);
|
|
183
|
+
if (!result.ok) expect(result.error).toContain("schemaVersion");
|
|
118
184
|
});
|
|
119
185
|
|
|
120
186
|
test("rejects a structurally broken record", () => {
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
// Act
|
|
188
|
+
const result = validateThreadRecord({ ...record, revisions: "nope" });
|
|
189
|
+
|
|
190
|
+
// Assert
|
|
191
|
+
expect(result.ok).toBe(false);
|
|
123
192
|
});
|
|
124
193
|
});
|
|
125
194
|
|
|
126
195
|
/**
|
|
127
|
-
* Wire pins
|
|
196
|
+
* Wire pins: every schema's entries must cover exactly the keys of the
|
|
128
197
|
* schema type it mirrors. Runtime complement to the compile-time EntriesOf
|
|
129
198
|
* check - the samples are fully populated and typed, so a new field in
|
|
130
199
|
* @cueloop/schema shows up here too.
|
|
131
200
|
*/
|
|
132
201
|
describe("wire pins", () => {
|
|
133
|
-
const keys = (
|
|
202
|
+
const keys = <Subject extends object>(subject: Subject) => Object.keys(subject).sort();
|
|
134
203
|
const entryKeys = (schema: { entries: object }) => Object.keys(schema.entries).sort();
|
|
135
204
|
|
|
136
205
|
const fullMeta: Required<ArtifactMeta> = {
|
|
206
|
+
workflow: "plan",
|
|
137
207
|
cwd: "/repo",
|
|
208
|
+
vcs: "jj",
|
|
209
|
+
vcsChangeId: "change",
|
|
210
|
+
vcsRevisionId: "revision",
|
|
138
211
|
agent: "claude-code",
|
|
139
212
|
agentSessionId: "sess-1",
|
|
140
213
|
planPath: "/repo/plan.md",
|
|
214
|
+
prototypePath: "/repo/proto.html",
|
|
141
215
|
pr: "org/repo#1",
|
|
216
|
+
prBrief: "# PR",
|
|
217
|
+
prBaseSha: "base",
|
|
218
|
+
prHeadSha: "head",
|
|
219
|
+
prRefreshBaseSha: "next-base",
|
|
220
|
+
prRefreshHeadSha: "next",
|
|
221
|
+
prUrl: "https://github.com/org/repo/pull/1",
|
|
142
222
|
herdrPane: "%7",
|
|
143
223
|
title: "Plan",
|
|
224
|
+
workbench: false,
|
|
225
|
+
snapshot: false,
|
|
226
|
+
};
|
|
227
|
+
const fullAnchor: Required<Anchor> = {
|
|
228
|
+
quote: "q",
|
|
229
|
+
prefix: "p",
|
|
230
|
+
suffix: "s",
|
|
231
|
+
blockIndex: 0,
|
|
232
|
+
endBlockIndex: 0,
|
|
233
|
+
start: 0,
|
|
234
|
+
end: 1,
|
|
235
|
+
selector: "div.card",
|
|
144
236
|
};
|
|
145
|
-
const fullAnchor: Required<Anchor> = { quote: "q", prefix: "p", suffix: "s", blockIndex: 0, start: 0, end: 1 };
|
|
146
237
|
const fullAnnotation: Required<Annotation> = {
|
|
147
238
|
id: "a1",
|
|
148
239
|
kind: "comment",
|
|
149
240
|
anchor: fullAnchor,
|
|
241
|
+
target: { kind: "file", path: "src/x.ts", rev: "worktree" },
|
|
150
242
|
body: "b",
|
|
151
243
|
orphan: false,
|
|
244
|
+
author: "SHA256:abc",
|
|
245
|
+
reviewComment: {
|
|
246
|
+
severity: "p1",
|
|
247
|
+
title: "Finding",
|
|
248
|
+
path: "src/x.ts",
|
|
249
|
+
line: 1,
|
|
250
|
+
startLine: 1,
|
|
251
|
+
startAnchor: fullAnchor,
|
|
252
|
+
side: "RIGHT",
|
|
253
|
+
suggestion: "replacement",
|
|
254
|
+
prompt: "fix it",
|
|
255
|
+
},
|
|
256
|
+
replyTo: "a0",
|
|
257
|
+
resolution: { revision: 2, source: "agent" },
|
|
152
258
|
createdAt: "now",
|
|
153
259
|
};
|
|
154
|
-
const fullArtifact: Required<Artifact> = {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
260
|
+
const fullArtifact: Required<Artifact> = {
|
|
261
|
+
type: "plan",
|
|
262
|
+
content: "# P",
|
|
263
|
+
meta: fullMeta,
|
|
264
|
+
files: [{ path: "src/a.ts", oldContents: "old\n", newContents: "new\n", status: "modified" }],
|
|
265
|
+
};
|
|
266
|
+
const fullMessage: Required<Message> = {
|
|
267
|
+
id: "msg_1",
|
|
268
|
+
outcome: "approved",
|
|
269
|
+
summary: "",
|
|
270
|
+
body: "",
|
|
271
|
+
annotations: [fullAnnotation],
|
|
272
|
+
sentAt: "now",
|
|
273
|
+
};
|
|
274
|
+
const fullRevision: Required<Revision> = {
|
|
275
|
+
revision: 1,
|
|
276
|
+
content: "# P",
|
|
277
|
+
submittedAt: "now",
|
|
278
|
+
source: { vcs: "jj", changeId: "change", revisionId: "revision" },
|
|
279
|
+
files: [{ path: "a.ts", oldContents: "a", newContents: "b", status: "modified" }],
|
|
280
|
+
};
|
|
281
|
+
const fullWorkspace: Required<WorkspaceKey> = {
|
|
282
|
+
repoRoot: "/repo",
|
|
283
|
+
branch: "main",
|
|
284
|
+
rootCommit: "11e7abbcf507a13ae5e4e7559c4b42dbb52237fe",
|
|
285
|
+
remote: "git@github.com:acme/repo.git",
|
|
286
|
+
};
|
|
287
|
+
const fullIdentity: Required<Identity> = {
|
|
288
|
+
id: "SHA256:abc",
|
|
289
|
+
provider: "ssh",
|
|
290
|
+
name: "Al",
|
|
291
|
+
handle: "abc",
|
|
292
|
+
};
|
|
293
|
+
const fullSession: Required<Thread> = {
|
|
159
294
|
schemaVersion: SCHEMA_VERSION,
|
|
160
295
|
id: "ses_1",
|
|
161
296
|
workspace: fullWorkspace,
|
|
162
297
|
artifact: fullArtifact,
|
|
163
298
|
revisions: [fullRevision],
|
|
164
299
|
annotations: [fullAnnotation],
|
|
165
|
-
|
|
166
|
-
|
|
300
|
+
history: {
|
|
301
|
+
entries: [
|
|
302
|
+
{
|
|
303
|
+
id: "e1",
|
|
304
|
+
parentId: null,
|
|
305
|
+
type: "revision",
|
|
306
|
+
by: "agent",
|
|
307
|
+
content: "# P",
|
|
308
|
+
createdAt: "now",
|
|
309
|
+
},
|
|
310
|
+
{ id: "e2", parentId: "e1", type: "comment", annotationId: "a1", createdAt: "now" },
|
|
311
|
+
{ id: "e3", parentId: "e2", type: "comment-removed", annotationId: "a1", createdAt: "now" },
|
|
312
|
+
{ id: "e4", parentId: "e3", type: "message", message: fullMessage, createdAt: "now" },
|
|
313
|
+
{
|
|
314
|
+
id: "e5",
|
|
315
|
+
parentId: "e4",
|
|
316
|
+
type: "branch-summary",
|
|
317
|
+
text: "tried a thing",
|
|
318
|
+
abandoned: ["e3"],
|
|
319
|
+
createdAt: "now",
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
tips: { main: "e5" },
|
|
323
|
+
branch: "main",
|
|
324
|
+
labels: { e1: "start" },
|
|
325
|
+
},
|
|
326
|
+
curation: [{ path: "src/a.ts", hunkIndex: 0, changeIndex: 1 }],
|
|
327
|
+
shelvedAnnotations: [fullAnnotation],
|
|
328
|
+
parentSessionId: "ses_0",
|
|
329
|
+
workingCopy: "# ",
|
|
330
|
+
textCuts: [{ start: 2, end: 3, quote: "P" }],
|
|
331
|
+
viewedPaths: ["src/a.ts"],
|
|
332
|
+
messageOperations: [{ operationId: "operation", fingerprint: "hash", result: fullMessage }],
|
|
333
|
+
message: fullMessage,
|
|
167
334
|
status: "pending",
|
|
168
335
|
createdAt: "now",
|
|
336
|
+
shares: [
|
|
337
|
+
{
|
|
338
|
+
id: "p_abc123xy",
|
|
339
|
+
name: "review link",
|
|
340
|
+
requireAuth: true,
|
|
341
|
+
allowlist: ["octocat"],
|
|
342
|
+
owner: "SHA256:owner",
|
|
343
|
+
shareBranch: "main",
|
|
344
|
+
},
|
|
345
|
+
],
|
|
346
|
+
participants: [fullIdentity],
|
|
169
347
|
};
|
|
170
348
|
|
|
349
|
+
test("a history whose tree is broken is refused even when every entry is well-formed", () => {
|
|
350
|
+
// Arrange: a parent cycle
|
|
351
|
+
const record = {
|
|
352
|
+
...fullSession,
|
|
353
|
+
history: {
|
|
354
|
+
entries: [
|
|
355
|
+
{ id: "a", parentId: "b", type: "revision", by: "agent", content: "x", createdAt: "now" },
|
|
356
|
+
{ id: "b", parentId: "a", type: "comment", annotationId: "a1", createdAt: "now" },
|
|
357
|
+
],
|
|
358
|
+
tips: { main: "b" },
|
|
359
|
+
branch: "main",
|
|
360
|
+
labels: {},
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
// Act
|
|
365
|
+
const parsed = validateThreadRecord(record);
|
|
366
|
+
|
|
367
|
+
// Assert
|
|
368
|
+
expect(parsed.ok).toBe(false);
|
|
369
|
+
if (!parsed.ok) expect(parsed.error).toContain("history:");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
test("a persisted Thread with stale text Cut provenance is refused", () => {
|
|
373
|
+
const result = validateThreadRecord({
|
|
374
|
+
...fullSession,
|
|
375
|
+
workingCopy: "# P",
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
expect(result.ok).toBe(false);
|
|
379
|
+
if (!result.ok) {
|
|
380
|
+
expect(result.error).toContain(
|
|
381
|
+
"text Cuts do not match the submitted artifact and working copy",
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
171
386
|
test("schema key sets match the schema types", () => {
|
|
172
387
|
expect(entryKeys(WorkspaceSchema)).toEqual(keys(fullWorkspace));
|
|
173
388
|
expect(entryKeys(ArtifactMetaSchema)).toEqual(keys(fullMeta));
|
|
@@ -175,27 +390,54 @@ describe("wire pins", () => {
|
|
|
175
390
|
expect(entryKeys(AnchorSchema)).toEqual(keys(fullAnchor));
|
|
176
391
|
// wire annotations arrive without createdAt; the daemon stamps it
|
|
177
392
|
const { createdAt: _stamped, ...wireAnnotation } = fullAnnotation;
|
|
393
|
+
|
|
178
394
|
expect(entryKeys(AnnotationSchema)).toEqual(keys(wireAnnotation));
|
|
179
395
|
expect(entryKeys(RevisionSchema)).toEqual(keys(fullRevision));
|
|
180
|
-
expect(entryKeys(
|
|
181
|
-
expect(entryKeys(
|
|
396
|
+
expect(entryKeys(MessageSchema)).toEqual(keys(fullMessage));
|
|
397
|
+
expect(entryKeys(IdentitySchema)).toEqual(keys(fullIdentity));
|
|
398
|
+
expect(entryKeys(ThreadRecordSchema)).toEqual(keys(fullSession));
|
|
182
399
|
// persisted annotations carry the stamped createdAt
|
|
183
|
-
const stored =
|
|
400
|
+
const stored = ThreadRecordSchema.entries.annotations.item;
|
|
401
|
+
|
|
184
402
|
expect(entryKeys(stored)).toEqual(keys(fullAnnotation));
|
|
185
403
|
});
|
|
186
404
|
|
|
187
405
|
test("a fully-populated meta survives validation and DaemonCore unchanged", () => {
|
|
188
406
|
const home = mkdtempSync(join(tmpdir(), "cueloop-val-"));
|
|
407
|
+
|
|
189
408
|
try {
|
|
409
|
+
// Arrange
|
|
190
410
|
const params = parseParams("session.create", {
|
|
191
411
|
workspace: fullWorkspace,
|
|
192
412
|
artifact: { type: "plan", content: "# P", meta: fullMeta },
|
|
193
413
|
});
|
|
194
414
|
const core = new DaemonCore(home);
|
|
415
|
+
|
|
416
|
+
// Act
|
|
195
417
|
const created = core.sessionCreate(params);
|
|
418
|
+
|
|
419
|
+
// Assert
|
|
196
420
|
expect(core.sessionGet(created.id).artifact.meta).toEqual(fullMeta);
|
|
197
421
|
} finally {
|
|
198
422
|
rmSync(home, { recursive: true, force: true });
|
|
199
423
|
}
|
|
200
424
|
});
|
|
201
425
|
});
|
|
426
|
+
|
|
427
|
+
describe("IdentitySchema provider", () => {
|
|
428
|
+
test("accepts a verified github identity", () => {
|
|
429
|
+
const parsed = v.safeParse(IdentitySchema, {
|
|
430
|
+
id: "SHA256:abc",
|
|
431
|
+
provider: "github",
|
|
432
|
+
name: "markus",
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
expect(parsed.success).toBe(true);
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("rejects an unknown provider", () => {
|
|
439
|
+
const parsed = v.safeParse(IdentitySchema, { id: "SHA256:abc", provider: "email" });
|
|
440
|
+
|
|
441
|
+
expect(parsed.success).toBe(false);
|
|
442
|
+
});
|
|
443
|
+
});
|