@os-eco/overstory-cli 0.7.3 → 0.7.4
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 +20 -8
- package/agents/builder.md +6 -0
- package/agents/coordinator.md +2 -2
- package/agents/lead.md +4 -1
- package/agents/merger.md +3 -2
- package/agents/monitor.md +1 -1
- package/agents/reviewer.md +1 -0
- package/agents/scout.md +1 -0
- package/package.json +2 -2
- package/src/commands/agents.ts +18 -8
- package/src/commands/prime.test.ts +1 -0
- package/src/commands/prime.ts +1 -16
- package/src/index.ts +1 -1
- package/src/metrics/pricing.ts +80 -0
- package/src/metrics/transcript.test.ts +58 -1
- package/src/metrics/transcript.ts +9 -68
- package/src/runtimes/pi-guards.test.ts +29 -0
- package/src/runtimes/pi-guards.ts +23 -6
- package/src/tracker/beads.test.ts +454 -0
- package/src/tracker/seeds.test.ts +461 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seeds tracker adapter tests.
|
|
3
|
+
*
|
|
4
|
+
* Uses Bun.spawn mocks — legitimate exception to "never mock what you can use for real".
|
|
5
|
+
* The `sd` CLI may not be installed in all environments and would modify real tracker
|
|
6
|
+
* state (creating/closing actual issues) if invoked directly in tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
10
|
+
import { AgentError } from "../errors.ts";
|
|
11
|
+
import { createSeedsTracker } from "./seeds.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Helper to create a mock Bun.spawn return value.
|
|
15
|
+
*
|
|
16
|
+
* The actual code reads stdout/stderr via `new Response(proc.stdout).text()`
|
|
17
|
+
* and `new Response(proc.stderr).text()`, so we need ReadableStreams.
|
|
18
|
+
*/
|
|
19
|
+
function mockSpawnResult(
|
|
20
|
+
stdout: string,
|
|
21
|
+
stderr: string,
|
|
22
|
+
exitCode: number,
|
|
23
|
+
): {
|
|
24
|
+
stdout: ReadableStream<Uint8Array>;
|
|
25
|
+
stderr: ReadableStream<Uint8Array>;
|
|
26
|
+
exited: Promise<number>;
|
|
27
|
+
pid: number;
|
|
28
|
+
} {
|
|
29
|
+
return {
|
|
30
|
+
stdout: new Response(stdout).body as ReadableStream<Uint8Array>,
|
|
31
|
+
stderr: new Response(stderr).body as ReadableStream<Uint8Array>,
|
|
32
|
+
exited: Promise.resolve(exitCode),
|
|
33
|
+
pid: 12345,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const TEST_CWD = "/test/repo";
|
|
38
|
+
|
|
39
|
+
describe("createSeedsTracker — ready()", () => {
|
|
40
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
spawnSpy.mockRestore();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("parses envelope and returns normalized TrackerIssue[]", async () => {
|
|
51
|
+
const envelope = {
|
|
52
|
+
success: true,
|
|
53
|
+
command: "ready",
|
|
54
|
+
issues: [
|
|
55
|
+
{ id: "sd-1", title: "Fix bug", status: "open", priority: 1, type: "task" },
|
|
56
|
+
{
|
|
57
|
+
id: "sd-2",
|
|
58
|
+
title: "Add feature",
|
|
59
|
+
status: "open",
|
|
60
|
+
priority: 2,
|
|
61
|
+
type: "feature",
|
|
62
|
+
assignee: "alice",
|
|
63
|
+
blocks: ["sd-5"],
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
68
|
+
|
|
69
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
70
|
+
const issues = await tracker.ready();
|
|
71
|
+
|
|
72
|
+
expect(issues).toHaveLength(2);
|
|
73
|
+
expect(issues[0]).toMatchObject({ id: "sd-1", title: "Fix bug", type: "task" });
|
|
74
|
+
expect(issues[1]).toMatchObject({ id: "sd-2", assignee: "alice", blocks: ["sd-5"] });
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("verifies CLI args: [sd, ready, --json]", async () => {
|
|
78
|
+
spawnSpy.mockImplementation(() =>
|
|
79
|
+
mockSpawnResult(JSON.stringify({ success: true, command: "ready", issues: [] }), "", 0),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
83
|
+
await tracker.ready();
|
|
84
|
+
|
|
85
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
86
|
+
const cmd = callArgs[0] as string[];
|
|
87
|
+
expect(cmd).toEqual(["sd", "ready", "--json"]);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("throws AgentError on non-zero exit code", async () => {
|
|
91
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "sd: command failed", 1));
|
|
92
|
+
|
|
93
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
94
|
+
await expect(tracker.ready()).rejects.toThrow(AgentError);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("throws AgentError on envelope failure", async () => {
|
|
98
|
+
const envelope = { success: false, command: "ready", error: "no issues available" };
|
|
99
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
100
|
+
|
|
101
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
102
|
+
await expect(tracker.ready()).rejects.toThrow(AgentError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("throws AgentError on empty output", async () => {
|
|
106
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "", 0));
|
|
107
|
+
|
|
108
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
109
|
+
await expect(tracker.ready()).rejects.toThrow(AgentError);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe("createSeedsTracker — show()", () => {
|
|
114
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
afterEach(() => {
|
|
121
|
+
spawnSpy.mockRestore();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("returns normalized TrackerIssue from envelope", async () => {
|
|
125
|
+
const envelope = {
|
|
126
|
+
success: true,
|
|
127
|
+
command: "show",
|
|
128
|
+
issue: {
|
|
129
|
+
id: "sd-42",
|
|
130
|
+
title: "My issue",
|
|
131
|
+
status: "open",
|
|
132
|
+
priority: 3,
|
|
133
|
+
type: "bug",
|
|
134
|
+
description: "A detailed bug report",
|
|
135
|
+
blockedBy: ["sd-10"],
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
139
|
+
|
|
140
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
141
|
+
const issue = await tracker.show("sd-42");
|
|
142
|
+
|
|
143
|
+
expect(issue).toMatchObject({
|
|
144
|
+
id: "sd-42",
|
|
145
|
+
title: "My issue",
|
|
146
|
+
type: "bug",
|
|
147
|
+
description: "A detailed bug report",
|
|
148
|
+
blockedBy: ["sd-10"],
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("verifies CLI args: [sd, show, <id>, --json]", async () => {
|
|
153
|
+
const envelope = {
|
|
154
|
+
success: true,
|
|
155
|
+
command: "show",
|
|
156
|
+
issue: { id: "sd-1", title: "t", status: "open", priority: 1, type: "task" },
|
|
157
|
+
};
|
|
158
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
159
|
+
|
|
160
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
161
|
+
await tracker.show("sd-1");
|
|
162
|
+
|
|
163
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
164
|
+
const cmd = callArgs[0] as string[];
|
|
165
|
+
expect(cmd).toEqual(["sd", "show", "sd-1", "--json"]);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("throws AgentError on non-zero exit code", async () => {
|
|
169
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "issue not found", 1));
|
|
170
|
+
|
|
171
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
172
|
+
await expect(tracker.show("sd-999")).rejects.toThrow(AgentError);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("createSeedsTracker — create()", () => {
|
|
177
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
178
|
+
|
|
179
|
+
beforeEach(() => {
|
|
180
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
afterEach(() => {
|
|
184
|
+
spawnSpy.mockRestore();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("returns issue ID from envelope.id", async () => {
|
|
188
|
+
const envelope = { success: true, command: "create", id: "sd-100" };
|
|
189
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
190
|
+
|
|
191
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
192
|
+
const id = await tracker.create("New issue");
|
|
193
|
+
|
|
194
|
+
expect(id).toBe("sd-100");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("returns issue ID from envelope.issue.id (alternate format)", async () => {
|
|
198
|
+
const envelope = { success: true, command: "create", issue: { id: "sd-200" } };
|
|
199
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
200
|
+
|
|
201
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
202
|
+
const id = await tracker.create("Another issue");
|
|
203
|
+
|
|
204
|
+
expect(id).toBe("sd-200");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("passes optional --type, --priority, --description args", async () => {
|
|
208
|
+
const envelope = { success: true, command: "create", id: "sd-300" };
|
|
209
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
210
|
+
|
|
211
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
212
|
+
await tracker.create("My task", {
|
|
213
|
+
type: "feature",
|
|
214
|
+
priority: 2,
|
|
215
|
+
description: "A detailed description",
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
219
|
+
const cmd = callArgs[0] as string[];
|
|
220
|
+
expect(cmd).toContain("--type");
|
|
221
|
+
expect(cmd).toContain("feature");
|
|
222
|
+
expect(cmd).toContain("--priority");
|
|
223
|
+
expect(cmd).toContain("2");
|
|
224
|
+
expect(cmd).toContain("--description");
|
|
225
|
+
expect(cmd).toContain("A detailed description");
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("throws AgentError when no ID returned in envelope", async () => {
|
|
229
|
+
const envelope = { success: true, command: "create" };
|
|
230
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
231
|
+
|
|
232
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
233
|
+
await expect(tracker.create("No ID")).rejects.toThrow(AgentError);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("throws AgentError on envelope failure", async () => {
|
|
237
|
+
const envelope = { success: false, command: "create", error: "validation failed" };
|
|
238
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
239
|
+
|
|
240
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
241
|
+
await expect(tracker.create("Bad issue")).rejects.toThrow(AgentError);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("createSeedsTracker — claim()", () => {
|
|
246
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
247
|
+
|
|
248
|
+
beforeEach(() => {
|
|
249
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
afterEach(() => {
|
|
253
|
+
spawnSpy.mockRestore();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("calls [sd, update, <id>, --status, in_progress]", async () => {
|
|
257
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "", 0));
|
|
258
|
+
|
|
259
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
260
|
+
await tracker.claim("sd-5");
|
|
261
|
+
|
|
262
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
263
|
+
const cmd = callArgs[0] as string[];
|
|
264
|
+
expect(cmd).toEqual(["sd", "update", "sd-5", "--status", "in_progress"]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("throws AgentError on failure", async () => {
|
|
268
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "issue already claimed", 1));
|
|
269
|
+
|
|
270
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
271
|
+
await expect(tracker.claim("sd-5")).rejects.toThrow(AgentError);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
describe("createSeedsTracker — close()", () => {
|
|
276
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
277
|
+
|
|
278
|
+
beforeEach(() => {
|
|
279
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
spawnSpy.mockRestore();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("calls [sd, close, <id>] without reason", async () => {
|
|
287
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "", 0));
|
|
288
|
+
|
|
289
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
290
|
+
await tracker.close("sd-10");
|
|
291
|
+
|
|
292
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
293
|
+
const cmd = callArgs[0] as string[];
|
|
294
|
+
expect(cmd).toEqual(["sd", "close", "sd-10"]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("calls [sd, close, <id>, --reason, ...] with reason", async () => {
|
|
298
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "", 0));
|
|
299
|
+
|
|
300
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
301
|
+
await tracker.close("sd-10", "Done implementing");
|
|
302
|
+
|
|
303
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
304
|
+
const cmd = callArgs[0] as string[];
|
|
305
|
+
expect(cmd).toEqual(["sd", "close", "sd-10", "--reason", "Done implementing"]);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("createSeedsTracker — list()", () => {
|
|
310
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
afterEach(() => {
|
|
317
|
+
spawnSpy.mockRestore();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("returns normalized issues from envelope", async () => {
|
|
321
|
+
const envelope = {
|
|
322
|
+
success: true,
|
|
323
|
+
command: "list",
|
|
324
|
+
issues: [
|
|
325
|
+
{ id: "sd-1", title: "Issue A", status: "open", priority: 1, type: "task" },
|
|
326
|
+
{ id: "sd-2", title: "Issue B", status: "in_progress", priority: 2, type: "bug" },
|
|
327
|
+
],
|
|
328
|
+
};
|
|
329
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
330
|
+
|
|
331
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
332
|
+
const issues = await tracker.list();
|
|
333
|
+
|
|
334
|
+
expect(issues).toHaveLength(2);
|
|
335
|
+
expect(issues[0]).toMatchObject({ id: "sd-1", status: "open" });
|
|
336
|
+
expect(issues[1]).toMatchObject({ id: "sd-2", status: "in_progress" });
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("verifies CLI args: [sd, list, --json]", async () => {
|
|
340
|
+
spawnSpy.mockImplementation(() =>
|
|
341
|
+
mockSpawnResult(JSON.stringify({ success: true, command: "list", issues: [] }), "", 0),
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
345
|
+
await tracker.list();
|
|
346
|
+
|
|
347
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
348
|
+
const cmd = callArgs[0] as string[];
|
|
349
|
+
expect(cmd[0]).toBe("sd");
|
|
350
|
+
expect(cmd[1]).toBe("list");
|
|
351
|
+
expect(cmd).toContain("--json");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("passes --status and --limit options", async () => {
|
|
355
|
+
const envelope = { success: true, command: "list", issues: [] };
|
|
356
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
357
|
+
|
|
358
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
359
|
+
await tracker.list({ status: "open", limit: 10 });
|
|
360
|
+
|
|
361
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
362
|
+
const cmd = callArgs[0] as string[];
|
|
363
|
+
expect(cmd).toContain("--status");
|
|
364
|
+
expect(cmd).toContain("open");
|
|
365
|
+
expect(cmd).toContain("--limit");
|
|
366
|
+
expect(cmd).toContain("10");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
describe("createSeedsTracker — sync()", () => {
|
|
371
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
372
|
+
|
|
373
|
+
beforeEach(() => {
|
|
374
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
afterEach(() => {
|
|
378
|
+
spawnSpy.mockRestore();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("calls [sd, sync]", async () => {
|
|
382
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "", 0));
|
|
383
|
+
|
|
384
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
385
|
+
await tracker.sync();
|
|
386
|
+
|
|
387
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
388
|
+
const cmd = callArgs[0] as string[];
|
|
389
|
+
expect(cmd).toEqual(["sd", "sync"]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("throws AgentError on failure", async () => {
|
|
393
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("", "sync failed: dirty working tree", 1));
|
|
394
|
+
|
|
395
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
396
|
+
await expect(tracker.sync()).rejects.toThrow(AgentError);
|
|
397
|
+
});
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
describe("createSeedsTracker — edge cases", () => {
|
|
401
|
+
let spawnSpy: ReturnType<typeof spyOn>;
|
|
402
|
+
|
|
403
|
+
beforeEach(() => {
|
|
404
|
+
spawnSpy = spyOn(Bun, "spawn");
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
afterEach(() => {
|
|
408
|
+
spawnSpy.mockRestore();
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
test("strips non-JSON prefix lines before parsing", async () => {
|
|
412
|
+
const envelope = {
|
|
413
|
+
success: true,
|
|
414
|
+
command: "ready",
|
|
415
|
+
issues: [{ id: "sd-1", title: "Test", status: "open", priority: 1, type: "task" }],
|
|
416
|
+
};
|
|
417
|
+
const output = `Syncing with remote...\nDone.\n${JSON.stringify(envelope)}`;
|
|
418
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(output, "", 0));
|
|
419
|
+
|
|
420
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
421
|
+
const issues = await tracker.ready();
|
|
422
|
+
|
|
423
|
+
expect(issues).toHaveLength(1);
|
|
424
|
+
expect(issues[0]).toMatchObject({ id: "sd-1" });
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("throws AgentError on invalid JSON", async () => {
|
|
428
|
+
spawnSpy.mockImplementation(() => mockSpawnResult("{not: valid json}", "", 0));
|
|
429
|
+
|
|
430
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
431
|
+
await expect(tracker.ready()).rejects.toThrow(AgentError);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("handles envelope with missing error field (defaults to 'unknown error')", async () => {
|
|
435
|
+
const envelope = { success: false, command: "ready" }; // No error field
|
|
436
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
437
|
+
|
|
438
|
+
const tracker = createSeedsTracker(TEST_CWD);
|
|
439
|
+
try {
|
|
440
|
+
await tracker.ready();
|
|
441
|
+
expect(true).toBe(false); // Should have thrown
|
|
442
|
+
} catch (err: unknown) {
|
|
443
|
+
expect(err).toBeInstanceOf(AgentError);
|
|
444
|
+
const agentErr = err as AgentError;
|
|
445
|
+
expect(agentErr.message).toContain("unknown error");
|
|
446
|
+
}
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("propagates cwd to Bun.spawn", async () => {
|
|
450
|
+
const envelope = { success: true, command: "ready", issues: [] };
|
|
451
|
+
spawnSpy.mockImplementation(() => mockSpawnResult(JSON.stringify(envelope), "", 0));
|
|
452
|
+
|
|
453
|
+
const customCwd = "/my/custom/project";
|
|
454
|
+
const tracker = createSeedsTracker(customCwd);
|
|
455
|
+
await tracker.ready();
|
|
456
|
+
|
|
457
|
+
const callArgs = spawnSpy.mock.calls[0] as unknown[];
|
|
458
|
+
const opts = callArgs[1] as { cwd: string };
|
|
459
|
+
expect(opts.cwd).toBe(customCwd);
|
|
460
|
+
});
|
|
461
|
+
});
|