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