@os-eco/overstory-cli 0.6.1
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/LICENSE +21 -0
- package/README.md +381 -0
- package/agents/builder.md +137 -0
- package/agents/coordinator.md +263 -0
- package/agents/lead.md +301 -0
- package/agents/merger.md +160 -0
- package/agents/monitor.md +214 -0
- package/agents/reviewer.md +140 -0
- package/agents/scout.md +119 -0
- package/agents/supervisor.md +423 -0
- package/package.json +47 -0
- package/src/agents/checkpoint.test.ts +88 -0
- package/src/agents/checkpoint.ts +101 -0
- package/src/agents/hooks-deployer.test.ts +2040 -0
- package/src/agents/hooks-deployer.ts +607 -0
- package/src/agents/identity.test.ts +603 -0
- package/src/agents/identity.ts +384 -0
- package/src/agents/lifecycle.test.ts +196 -0
- package/src/agents/lifecycle.ts +183 -0
- package/src/agents/manifest.test.ts +746 -0
- package/src/agents/manifest.ts +354 -0
- package/src/agents/overlay.test.ts +676 -0
- package/src/agents/overlay.ts +308 -0
- package/src/beads/client.test.ts +217 -0
- package/src/beads/client.ts +202 -0
- package/src/beads/molecules.test.ts +338 -0
- package/src/beads/molecules.ts +198 -0
- package/src/commands/agents.test.ts +322 -0
- package/src/commands/agents.ts +287 -0
- package/src/commands/clean.test.ts +670 -0
- package/src/commands/clean.ts +618 -0
- package/src/commands/completions.test.ts +342 -0
- package/src/commands/completions.ts +887 -0
- package/src/commands/coordinator.test.ts +1530 -0
- package/src/commands/coordinator.ts +733 -0
- package/src/commands/costs.test.ts +1119 -0
- package/src/commands/costs.ts +564 -0
- package/src/commands/dashboard.test.ts +308 -0
- package/src/commands/dashboard.ts +838 -0
- package/src/commands/doctor.test.ts +294 -0
- package/src/commands/doctor.ts +213 -0
- package/src/commands/errors.test.ts +647 -0
- package/src/commands/errors.ts +248 -0
- package/src/commands/feed.test.ts +578 -0
- package/src/commands/feed.ts +361 -0
- package/src/commands/group.test.ts +262 -0
- package/src/commands/group.ts +511 -0
- package/src/commands/hooks.test.ts +458 -0
- package/src/commands/hooks.ts +253 -0
- package/src/commands/init.test.ts +347 -0
- package/src/commands/init.ts +650 -0
- package/src/commands/inspect.test.ts +670 -0
- package/src/commands/inspect.ts +431 -0
- package/src/commands/log.test.ts +1454 -0
- package/src/commands/log.ts +724 -0
- package/src/commands/logs.test.ts +379 -0
- package/src/commands/logs.ts +546 -0
- package/src/commands/mail.test.ts +1270 -0
- package/src/commands/mail.ts +771 -0
- package/src/commands/merge.test.ts +670 -0
- package/src/commands/merge.ts +355 -0
- package/src/commands/metrics.test.ts +444 -0
- package/src/commands/metrics.ts +143 -0
- package/src/commands/monitor.test.ts +191 -0
- package/src/commands/monitor.ts +390 -0
- package/src/commands/nudge.test.ts +230 -0
- package/src/commands/nudge.ts +372 -0
- package/src/commands/prime.test.ts +470 -0
- package/src/commands/prime.ts +381 -0
- package/src/commands/replay.test.ts +741 -0
- package/src/commands/replay.ts +360 -0
- package/src/commands/run.test.ts +431 -0
- package/src/commands/run.ts +351 -0
- package/src/commands/sling.test.ts +657 -0
- package/src/commands/sling.ts +661 -0
- package/src/commands/spec.test.ts +203 -0
- package/src/commands/spec.ts +168 -0
- package/src/commands/status.test.ts +430 -0
- package/src/commands/status.ts +398 -0
- package/src/commands/stop.test.ts +420 -0
- package/src/commands/stop.ts +151 -0
- package/src/commands/supervisor.test.ts +187 -0
- package/src/commands/supervisor.ts +535 -0
- package/src/commands/trace.test.ts +745 -0
- package/src/commands/trace.ts +325 -0
- package/src/commands/watch.test.ts +145 -0
- package/src/commands/watch.ts +247 -0
- package/src/commands/worktree.test.ts +786 -0
- package/src/commands/worktree.ts +311 -0
- package/src/config.test.ts +822 -0
- package/src/config.ts +829 -0
- package/src/doctor/agents.test.ts +454 -0
- package/src/doctor/agents.ts +396 -0
- package/src/doctor/config-check.test.ts +190 -0
- package/src/doctor/config-check.ts +183 -0
- package/src/doctor/consistency.test.ts +651 -0
- package/src/doctor/consistency.ts +294 -0
- package/src/doctor/databases.test.ts +290 -0
- package/src/doctor/databases.ts +218 -0
- package/src/doctor/dependencies.test.ts +184 -0
- package/src/doctor/dependencies.ts +175 -0
- package/src/doctor/logs.test.ts +251 -0
- package/src/doctor/logs.ts +295 -0
- package/src/doctor/merge-queue.test.ts +216 -0
- package/src/doctor/merge-queue.ts +144 -0
- package/src/doctor/structure.test.ts +291 -0
- package/src/doctor/structure.ts +198 -0
- package/src/doctor/types.ts +37 -0
- package/src/doctor/version.test.ts +136 -0
- package/src/doctor/version.ts +129 -0
- package/src/e2e/init-sling-lifecycle.test.ts +277 -0
- package/src/errors.ts +217 -0
- package/src/events/store.test.ts +660 -0
- package/src/events/store.ts +369 -0
- package/src/events/tool-filter.test.ts +330 -0
- package/src/events/tool-filter.ts +126 -0
- package/src/index.ts +316 -0
- package/src/insights/analyzer.test.ts +466 -0
- package/src/insights/analyzer.ts +203 -0
- package/src/logging/color.test.ts +142 -0
- package/src/logging/color.ts +71 -0
- package/src/logging/logger.test.ts +813 -0
- package/src/logging/logger.ts +266 -0
- package/src/logging/reporter.test.ts +259 -0
- package/src/logging/reporter.ts +109 -0
- package/src/logging/sanitizer.test.ts +190 -0
- package/src/logging/sanitizer.ts +57 -0
- package/src/mail/broadcast.test.ts +203 -0
- package/src/mail/broadcast.ts +92 -0
- package/src/mail/client.test.ts +773 -0
- package/src/mail/client.ts +223 -0
- package/src/mail/store.test.ts +705 -0
- package/src/mail/store.ts +387 -0
- package/src/merge/queue.test.ts +359 -0
- package/src/merge/queue.ts +231 -0
- package/src/merge/resolver.test.ts +1345 -0
- package/src/merge/resolver.ts +645 -0
- package/src/metrics/store.test.ts +667 -0
- package/src/metrics/store.ts +445 -0
- package/src/metrics/summary.test.ts +398 -0
- package/src/metrics/summary.ts +178 -0
- package/src/metrics/transcript.test.ts +356 -0
- package/src/metrics/transcript.ts +175 -0
- package/src/mulch/client.test.ts +671 -0
- package/src/mulch/client.ts +332 -0
- package/src/sessions/compat.test.ts +280 -0
- package/src/sessions/compat.ts +104 -0
- package/src/sessions/store.test.ts +873 -0
- package/src/sessions/store.ts +494 -0
- package/src/test-helpers.test.ts +124 -0
- package/src/test-helpers.ts +126 -0
- package/src/tracker/beads.ts +56 -0
- package/src/tracker/factory.test.ts +80 -0
- package/src/tracker/factory.ts +64 -0
- package/src/tracker/seeds.ts +182 -0
- package/src/tracker/types.ts +52 -0
- package/src/types.ts +724 -0
- package/src/watchdog/daemon.test.ts +1975 -0
- package/src/watchdog/daemon.ts +671 -0
- package/src/watchdog/health.test.ts +431 -0
- package/src/watchdog/health.ts +264 -0
- package/src/watchdog/triage.test.ts +164 -0
- package/src/watchdog/triage.ts +179 -0
- package/src/worktree/manager.test.ts +439 -0
- package/src/worktree/manager.ts +198 -0
- package/src/worktree/tmux.test.ts +1009 -0
- package/src/worktree/tmux.ts +509 -0
- package/templates/CLAUDE.md.tmpl +89 -0
- package/templates/hooks.json.tmpl +105 -0
- package/templates/overlay.md.tmpl +81 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Beads (bd) CLI client.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `bd` command-line tool for issue tracking operations.
|
|
5
|
+
* All commands use `--json` for parseable output where supported.
|
|
6
|
+
* Uses Bun.spawn — zero runtime dependencies.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { AgentError } from "../errors.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* A beads issue as returned by the bd CLI.
|
|
13
|
+
* Defined locally since it comes from an external CLI tool.
|
|
14
|
+
*/
|
|
15
|
+
export interface BeadIssue {
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
status: string;
|
|
19
|
+
priority: number;
|
|
20
|
+
type: string;
|
|
21
|
+
assignee?: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
blocks?: string[];
|
|
24
|
+
blockedBy?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface BeadsClient {
|
|
28
|
+
/** List issues that are ready for work (open, unblocked). */
|
|
29
|
+
ready(options?: { mol?: string }): Promise<BeadIssue[]>;
|
|
30
|
+
|
|
31
|
+
/** Show details for a specific issue. */
|
|
32
|
+
show(id: string): Promise<BeadIssue>;
|
|
33
|
+
|
|
34
|
+
/** Create a new issue. Returns the new issue ID. */
|
|
35
|
+
create(
|
|
36
|
+
title: string,
|
|
37
|
+
options?: { type?: string; priority?: number; description?: string },
|
|
38
|
+
): Promise<string>;
|
|
39
|
+
|
|
40
|
+
/** Claim an issue (mark as in_progress). */
|
|
41
|
+
claim(id: string): Promise<void>;
|
|
42
|
+
|
|
43
|
+
/** Close an issue with an optional reason. */
|
|
44
|
+
close(id: string, reason?: string): Promise<void>;
|
|
45
|
+
|
|
46
|
+
/** List issues with optional filters. */
|
|
47
|
+
list(options?: { status?: string; limit?: number }): Promise<BeadIssue[]>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Run a shell command and capture its output.
|
|
52
|
+
*/
|
|
53
|
+
async function runCommand(
|
|
54
|
+
cmd: string[],
|
|
55
|
+
cwd: string,
|
|
56
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
57
|
+
const proc = Bun.spawn(cmd, {
|
|
58
|
+
cwd,
|
|
59
|
+
stdout: "pipe",
|
|
60
|
+
stderr: "pipe",
|
|
61
|
+
});
|
|
62
|
+
const stdout = await new Response(proc.stdout).text();
|
|
63
|
+
const stderr = await new Response(proc.stderr).text();
|
|
64
|
+
const exitCode = await proc.exited;
|
|
65
|
+
return { stdout, stderr, exitCode };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Parse JSON output from a bd command.
|
|
70
|
+
* Handles the case where output may be empty or malformed.
|
|
71
|
+
*/
|
|
72
|
+
function parseJsonOutput<T>(stdout: string, context: string): T {
|
|
73
|
+
const trimmed = stdout.trim();
|
|
74
|
+
if (trimmed === "") {
|
|
75
|
+
throw new AgentError(`Empty output from bd ${context}`);
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(trimmed) as T;
|
|
79
|
+
} catch {
|
|
80
|
+
throw new AgentError(
|
|
81
|
+
`Failed to parse JSON output from bd ${context}: ${trimmed.slice(0, 200)}`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Raw issue shape from the bd CLI.
|
|
88
|
+
* bd uses `issue_type` instead of `type`.
|
|
89
|
+
*/
|
|
90
|
+
interface RawBeadIssue {
|
|
91
|
+
id: string;
|
|
92
|
+
title: string;
|
|
93
|
+
status: string;
|
|
94
|
+
priority: number;
|
|
95
|
+
issue_type?: string;
|
|
96
|
+
type?: string;
|
|
97
|
+
assignee?: string;
|
|
98
|
+
description?: string;
|
|
99
|
+
blocks?: string[];
|
|
100
|
+
blockedBy?: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Normalize a raw bd issue into a BeadIssue.
|
|
105
|
+
* Maps `issue_type` -> `type` to match the BeadIssue interface.
|
|
106
|
+
*/
|
|
107
|
+
function normalizeIssue(raw: RawBeadIssue): BeadIssue {
|
|
108
|
+
return {
|
|
109
|
+
id: raw.id,
|
|
110
|
+
title: raw.title,
|
|
111
|
+
status: raw.status,
|
|
112
|
+
priority: raw.priority,
|
|
113
|
+
type: raw.issue_type ?? raw.type ?? "unknown",
|
|
114
|
+
assignee: raw.assignee,
|
|
115
|
+
description: raw.description,
|
|
116
|
+
blocks: raw.blocks,
|
|
117
|
+
blockedBy: raw.blockedBy,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Create a BeadsClient bound to the given working directory.
|
|
123
|
+
*
|
|
124
|
+
* @param cwd - Working directory where bd commands should run
|
|
125
|
+
* @returns A BeadsClient instance wrapping the bd CLI
|
|
126
|
+
*/
|
|
127
|
+
export function createBeadsClient(cwd: string): BeadsClient {
|
|
128
|
+
async function runBd(
|
|
129
|
+
args: string[],
|
|
130
|
+
context: string,
|
|
131
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
132
|
+
const { stdout, stderr, exitCode } = await runCommand(["bd", ...args], cwd);
|
|
133
|
+
if (exitCode !== 0) {
|
|
134
|
+
throw new AgentError(`bd ${context} failed (exit ${exitCode}): ${stderr.trim()}`);
|
|
135
|
+
}
|
|
136
|
+
return { stdout, stderr };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
async ready(options) {
|
|
141
|
+
const args = ["ready", "--json"];
|
|
142
|
+
if (options?.mol) {
|
|
143
|
+
args.push("--mol", options.mol);
|
|
144
|
+
}
|
|
145
|
+
const { stdout } = await runBd(args, "ready");
|
|
146
|
+
const raw = parseJsonOutput<RawBeadIssue[]>(stdout, "ready");
|
|
147
|
+
return raw.map(normalizeIssue);
|
|
148
|
+
},
|
|
149
|
+
|
|
150
|
+
async show(id) {
|
|
151
|
+
const { stdout } = await runBd(["show", id, "--json"], `show ${id}`);
|
|
152
|
+
// bd show --json returns an array with a single element
|
|
153
|
+
const raw = parseJsonOutput<RawBeadIssue[]>(stdout, `show ${id}`);
|
|
154
|
+
const first = raw[0];
|
|
155
|
+
if (!first) {
|
|
156
|
+
throw new AgentError(`bd show ${id} returned empty array`);
|
|
157
|
+
}
|
|
158
|
+
return normalizeIssue(first);
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async create(title, options) {
|
|
162
|
+
const args = ["create", title, "--json"];
|
|
163
|
+
if (options?.type) {
|
|
164
|
+
args.push("--type", options.type);
|
|
165
|
+
}
|
|
166
|
+
if (options?.priority !== undefined) {
|
|
167
|
+
args.push("--priority", String(options.priority));
|
|
168
|
+
}
|
|
169
|
+
if (options?.description) {
|
|
170
|
+
args.push("--description", options.description);
|
|
171
|
+
}
|
|
172
|
+
const { stdout } = await runBd(args, "create");
|
|
173
|
+
const result = parseJsonOutput<{ id: string }>(stdout, "create");
|
|
174
|
+
return result.id;
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
async claim(id) {
|
|
178
|
+
await runBd(["update", id, "--status", "in_progress"], `claim ${id}`);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
async close(id, reason) {
|
|
182
|
+
const args = ["close", id];
|
|
183
|
+
if (reason) {
|
|
184
|
+
args.push("--reason", reason);
|
|
185
|
+
}
|
|
186
|
+
await runBd(args, `close ${id}`);
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
async list(options) {
|
|
190
|
+
const args = ["list", "--json"];
|
|
191
|
+
if (options?.status) {
|
|
192
|
+
args.push("--status", options.status);
|
|
193
|
+
}
|
|
194
|
+
if (options?.limit !== undefined) {
|
|
195
|
+
args.push("--limit", String(options.limit));
|
|
196
|
+
}
|
|
197
|
+
const { stdout } = await runBd(args, "list");
|
|
198
|
+
const raw = parseJsonOutput<RawBeadIssue[]>(stdout, "list");
|
|
199
|
+
return raw.map(normalizeIssue);
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for beads molecules module.
|
|
3
|
+
*
|
|
4
|
+
* WHY MOCKING IS USED HERE:
|
|
5
|
+
* The molecules.ts module expects a bd mol API that doesn't exist yet in beads.
|
|
6
|
+
* Expected API: bd mol create --name, bd mol step add, bd mol list, bd mol status
|
|
7
|
+
* Actual API: bd formula, bd cook, bd mol pour, bd mol wisp
|
|
8
|
+
*
|
|
9
|
+
* These tests mock Bun.spawn to verify the module's logic is correct.
|
|
10
|
+
* When the bd API is implemented to match the module's expectations,
|
|
11
|
+
* these can be converted to integration tests using the real bd CLI.
|
|
12
|
+
*
|
|
13
|
+
* See mulch record mx-56558b for why mocking is normally avoided.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
17
|
+
import { AgentError } from "../errors.ts";
|
|
18
|
+
import {
|
|
19
|
+
createMoleculePrototype,
|
|
20
|
+
getConvoyStatus,
|
|
21
|
+
listPrototypes,
|
|
22
|
+
pourMolecule,
|
|
23
|
+
} from "./molecules.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Mock Bun.spawn to simulate bd mol CLI responses.
|
|
27
|
+
* Returns a mock process with configurable stdout/stderr/exitCode.
|
|
28
|
+
*/
|
|
29
|
+
function mockSpawn(
|
|
30
|
+
stdout: string,
|
|
31
|
+
stderr = "",
|
|
32
|
+
exitCode = 0,
|
|
33
|
+
): {
|
|
34
|
+
stdout: ReadableStream<Uint8Array>;
|
|
35
|
+
stderr: ReadableStream<Uint8Array>;
|
|
36
|
+
exited: Promise<number>;
|
|
37
|
+
} {
|
|
38
|
+
const stdoutBody = new Response(stdout).body;
|
|
39
|
+
const stderrBody = new Response(stderr).body;
|
|
40
|
+
if (!stdoutBody || !stderrBody) {
|
|
41
|
+
throw new Error("Response body is null");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
stdout: stdoutBody,
|
|
45
|
+
stderr: stderrBody,
|
|
46
|
+
exited: Promise.resolve(exitCode),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let originalSpawn: typeof Bun.spawn;
|
|
51
|
+
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
// Save original spawn
|
|
54
|
+
originalSpawn = Bun.spawn;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("molecules", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
// Restore original spawn before each test
|
|
60
|
+
Bun.spawn = originalSpawn;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
afterEach(() => {
|
|
64
|
+
// Ensure cleanup after each test to prevent mock leaks
|
|
65
|
+
Bun.spawn = originalSpawn;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("createMoleculePrototype", () => {
|
|
69
|
+
test("creates a prototype with ordered steps", async () => {
|
|
70
|
+
let callCount = 0;
|
|
71
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
72
|
+
Bun.spawn = mock((cmd: string[], _opts?: unknown) => {
|
|
73
|
+
callCount++;
|
|
74
|
+
// First call: bd mol create
|
|
75
|
+
if (callCount === 1) {
|
|
76
|
+
expect(cmd).toEqual(["bd", "mol", "create", "--name", "Test Workflow", "--json"]);
|
|
77
|
+
return mockSpawn(JSON.stringify({ id: "mol-123" }));
|
|
78
|
+
}
|
|
79
|
+
// Subsequent calls: bd mol step add
|
|
80
|
+
expect(cmd[0]).toBe("bd");
|
|
81
|
+
expect(cmd[1]).toBe("mol");
|
|
82
|
+
expect(cmd[2]).toBe("step");
|
|
83
|
+
expect(cmd[3]).toBe("add");
|
|
84
|
+
expect(cmd[4]).toBe("mol-123");
|
|
85
|
+
return mockSpawn(JSON.stringify({ success: true }));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const molId = await createMoleculePrototype("/test/dir", {
|
|
89
|
+
name: "Test Workflow",
|
|
90
|
+
steps: [
|
|
91
|
+
{ title: "Step 1: Setup", type: "task" },
|
|
92
|
+
{ title: "Step 2: Implementation", type: "task" },
|
|
93
|
+
{ title: "Step 3: Testing", type: "task" },
|
|
94
|
+
],
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
expect(molId).toBe("mol-123");
|
|
98
|
+
expect(callCount).toBe(4); // 1 create + 3 step adds
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("creates a prototype with default type (task)", async () => {
|
|
102
|
+
let callCount = 0;
|
|
103
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
104
|
+
Bun.spawn = mock((cmd: string[], _opts?: unknown) => {
|
|
105
|
+
callCount++;
|
|
106
|
+
if (callCount === 1) {
|
|
107
|
+
return mockSpawn(JSON.stringify({ id: "mol-456" }));
|
|
108
|
+
}
|
|
109
|
+
// Check that default type is "task"
|
|
110
|
+
expect(cmd).toContain("--type");
|
|
111
|
+
const typeIndex = cmd.indexOf("--type");
|
|
112
|
+
expect(cmd[typeIndex + 1]).toBe("task");
|
|
113
|
+
return mockSpawn(JSON.stringify({ success: true }));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const molId = await createMoleculePrototype("/test/dir", {
|
|
117
|
+
name: "Default Type Workflow",
|
|
118
|
+
steps: [{ title: "Step without explicit type" }],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(molId).toBe("mol-456");
|
|
122
|
+
expect(callCount).toBe(2); // 1 create + 1 step add
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("creates a prototype with empty steps array", async () => {
|
|
126
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
127
|
+
Bun.spawn = mock(() => {
|
|
128
|
+
return mockSpawn(JSON.stringify({ id: "mol-empty" }));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const molId = await createMoleculePrototype("/test/dir", {
|
|
132
|
+
name: "Empty Workflow",
|
|
133
|
+
steps: [],
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(molId).toBe("mol-empty");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("throws AgentError on create failure", async () => {
|
|
140
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
141
|
+
Bun.spawn = mock(() => {
|
|
142
|
+
return mockSpawn("", "bd mol create failed: invalid name", 1);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await expect(
|
|
146
|
+
createMoleculePrototype("/test/dir", {
|
|
147
|
+
name: "Bad",
|
|
148
|
+
steps: [],
|
|
149
|
+
}),
|
|
150
|
+
).rejects.toThrow(AgentError);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("throws AgentError on step add failure", async () => {
|
|
154
|
+
let callCount = 0;
|
|
155
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
156
|
+
Bun.spawn = mock(() => {
|
|
157
|
+
callCount++;
|
|
158
|
+
if (callCount === 1) {
|
|
159
|
+
return mockSpawn(JSON.stringify({ id: "mol-789" }));
|
|
160
|
+
}
|
|
161
|
+
// Step add fails
|
|
162
|
+
return mockSpawn("", "step add failed", 1);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
await expect(
|
|
166
|
+
createMoleculePrototype("/test/dir", {
|
|
167
|
+
name: "Test",
|
|
168
|
+
steps: [{ title: "Step 1" }],
|
|
169
|
+
}),
|
|
170
|
+
).rejects.toThrow(AgentError);
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("listPrototypes", () => {
|
|
175
|
+
test("returns all created prototypes", async () => {
|
|
176
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
177
|
+
Bun.spawn = mock(() => {
|
|
178
|
+
return mockSpawn(
|
|
179
|
+
JSON.stringify([
|
|
180
|
+
{ id: "mol-1", name: "List Test 1", stepCount: 1 },
|
|
181
|
+
{ id: "mol-2", name: "List Test 2", stepCount: 2 },
|
|
182
|
+
]),
|
|
183
|
+
);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const prototypes = await listPrototypes("/test/dir");
|
|
187
|
+
|
|
188
|
+
expect(prototypes).toHaveLength(2);
|
|
189
|
+
expect(prototypes[0]).toEqual({ id: "mol-1", name: "List Test 1", stepCount: 1 });
|
|
190
|
+
expect(prototypes[1]).toEqual({ id: "mol-2", name: "List Test 2", stepCount: 2 });
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("returns empty array when no prototypes exist", async () => {
|
|
194
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
195
|
+
Bun.spawn = mock(() => {
|
|
196
|
+
return mockSpawn(JSON.stringify([]));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const prototypes = await listPrototypes("/test/dir");
|
|
200
|
+
|
|
201
|
+
expect(Array.isArray(prototypes)).toBe(true);
|
|
202
|
+
expect(prototypes).toHaveLength(0);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("throws AgentError on failure", async () => {
|
|
206
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
207
|
+
Bun.spawn = mock(() => {
|
|
208
|
+
return mockSpawn("", "bd mol list failed", 1);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
await expect(listPrototypes("/test/dir")).rejects.toThrow(AgentError);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("throws AgentError on empty output", async () => {
|
|
215
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
216
|
+
Bun.spawn = mock(() => {
|
|
217
|
+
return mockSpawn("");
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await expect(listPrototypes("/test/dir")).rejects.toThrow(AgentError);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("pourMolecule", () => {
|
|
225
|
+
test("pours a prototype into actual issues", async () => {
|
|
226
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
227
|
+
Bun.spawn = mock((cmd: string[]) => {
|
|
228
|
+
expect(cmd).toEqual(["bd", "mol", "pour", "mol-123", "--json"]);
|
|
229
|
+
return mockSpawn(JSON.stringify({ ids: ["issue-1", "issue-2", "issue-3"] }));
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const issueIds = await pourMolecule("/test/dir", {
|
|
233
|
+
prototypeId: "mol-123",
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(Array.isArray(issueIds)).toBe(true);
|
|
237
|
+
expect(issueIds).toHaveLength(3);
|
|
238
|
+
expect(issueIds).toEqual(["issue-1", "issue-2", "issue-3"]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("applies prefix when provided", async () => {
|
|
242
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
243
|
+
Bun.spawn = mock((cmd: string[]) => {
|
|
244
|
+
expect(cmd).toEqual(["bd", "mol", "pour", "mol-456", "--json", "--prefix", "v2.0"]);
|
|
245
|
+
return mockSpawn(JSON.stringify({ ids: ["issue-4", "issue-5"] }));
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const issueIds = await pourMolecule("/test/dir", {
|
|
249
|
+
prototypeId: "mol-456",
|
|
250
|
+
prefix: "v2.0",
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(issueIds).toEqual(["issue-4", "issue-5"]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("handles empty prototype (0 steps)", async () => {
|
|
257
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
258
|
+
Bun.spawn = mock(() => {
|
|
259
|
+
return mockSpawn(JSON.stringify({ ids: [] }));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const issueIds = await pourMolecule("/test/dir", {
|
|
263
|
+
prototypeId: "mol-empty",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(Array.isArray(issueIds)).toBe(true);
|
|
267
|
+
expect(issueIds).toHaveLength(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("throws AgentError for nonexistent prototype", async () => {
|
|
271
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
272
|
+
Bun.spawn = mock(() => {
|
|
273
|
+
return mockSpawn("", "prototype not found", 1);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
await expect(
|
|
277
|
+
pourMolecule("/test/dir", {
|
|
278
|
+
prototypeId: "nonexistent-mol-id",
|
|
279
|
+
}),
|
|
280
|
+
).rejects.toThrow(AgentError);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("getConvoyStatus", () => {
|
|
285
|
+
test("returns status for poured prototype", async () => {
|
|
286
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
287
|
+
Bun.spawn = mock((cmd: string[]) => {
|
|
288
|
+
expect(cmd).toEqual(["bd", "mol", "status", "mol-123", "--json"]);
|
|
289
|
+
return mockSpawn(
|
|
290
|
+
JSON.stringify({
|
|
291
|
+
total: 3,
|
|
292
|
+
completed: 1,
|
|
293
|
+
inProgress: 1,
|
|
294
|
+
blocked: 0,
|
|
295
|
+
}),
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
const status = await getConvoyStatus("/test/dir", "mol-123");
|
|
300
|
+
|
|
301
|
+
expect(status).toBeDefined();
|
|
302
|
+
expect(status.total).toBe(3);
|
|
303
|
+
expect(status.completed).toBe(1);
|
|
304
|
+
expect(status.inProgress).toBe(1);
|
|
305
|
+
expect(status.blocked).toBe(0);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("handles empty poured prototype", async () => {
|
|
309
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
310
|
+
Bun.spawn = mock(() => {
|
|
311
|
+
return mockSpawn(
|
|
312
|
+
JSON.stringify({
|
|
313
|
+
total: 0,
|
|
314
|
+
completed: 0,
|
|
315
|
+
inProgress: 0,
|
|
316
|
+
blocked: 0,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const status = await getConvoyStatus("/test/dir", "mol-empty");
|
|
322
|
+
|
|
323
|
+
expect(status.total).toBe(0);
|
|
324
|
+
expect(status.completed).toBe(0);
|
|
325
|
+
expect(status.inProgress).toBe(0);
|
|
326
|
+
expect(status.blocked).toBe(0);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("throws AgentError for nonexistent prototype", async () => {
|
|
330
|
+
// @ts-expect-error - Mocking Bun.spawn for testing
|
|
331
|
+
Bun.spawn = mock(() => {
|
|
332
|
+
return mockSpawn("", "prototype not found", 1);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await expect(getConvoyStatus("/test/dir", "nonexistent-mol-id")).rejects.toThrow(AgentError);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
});
|