@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,431 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for overstory run command.
|
|
3
|
+
*
|
|
4
|
+
* Uses real SQLite (temp files) and real file I/O for current-run.txt.
|
|
5
|
+
* No mocks -- tests exercise the actual RunStore and SessionStore.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import { mkdir, mkdtemp, rm, unlink } from "node:fs/promises";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import type { SessionStore } from "../sessions/store.ts";
|
|
13
|
+
import { createRunStore, createSessionStore } from "../sessions/store.ts";
|
|
14
|
+
import type { AgentSession, InsertRun, RunStore } from "../types.ts";
|
|
15
|
+
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
let overstoryDir: string;
|
|
18
|
+
let dbPath: string;
|
|
19
|
+
let runStore: RunStore;
|
|
20
|
+
let sessionStore: SessionStore;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
tempDir = await mkdtemp(join(tmpdir(), "overstory-run-test-"));
|
|
24
|
+
overstoryDir = join(tempDir, ".overstory");
|
|
25
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
26
|
+
dbPath = join(overstoryDir, "sessions.db");
|
|
27
|
+
runStore = createRunStore(dbPath);
|
|
28
|
+
sessionStore = createSessionStore(dbPath);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
afterEach(async () => {
|
|
32
|
+
runStore.close();
|
|
33
|
+
sessionStore.close();
|
|
34
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/** Write a run ID to current-run.txt. */
|
|
38
|
+
async function writeCurrentRun(runId: string): Promise<void> {
|
|
39
|
+
await Bun.write(join(overstoryDir, "current-run.txt"), runId);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Read current-run.txt contents, or null if missing/empty. */
|
|
43
|
+
async function readCurrentRunFile(): Promise<string | null> {
|
|
44
|
+
const file = Bun.file(join(overstoryDir, "current-run.txt"));
|
|
45
|
+
if (!(await file.exists())) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const trimmed = (await file.text()).trim();
|
|
49
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Helper to create an InsertRun with optional overrides. */
|
|
53
|
+
function makeRun(overrides: Partial<InsertRun> = {}): InsertRun {
|
|
54
|
+
return {
|
|
55
|
+
id: "run-2026-02-13T10:00:00.000Z",
|
|
56
|
+
startedAt: "2026-02-13T10:00:00.000Z",
|
|
57
|
+
coordinatorSessionId: "coord-session-001",
|
|
58
|
+
status: "active",
|
|
59
|
+
...overrides,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Helper to create an AgentSession with optional overrides. */
|
|
64
|
+
function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
65
|
+
return {
|
|
66
|
+
id: "session-001",
|
|
67
|
+
agentName: "test-agent",
|
|
68
|
+
capability: "builder",
|
|
69
|
+
worktreePath: "/tmp/worktrees/test-agent",
|
|
70
|
+
branchName: "overstory/test-agent/task-1",
|
|
71
|
+
beadId: "task-1",
|
|
72
|
+
tmuxSession: "overstory-test-agent",
|
|
73
|
+
state: "working",
|
|
74
|
+
pid: 12345,
|
|
75
|
+
parentAgent: null,
|
|
76
|
+
depth: 0,
|
|
77
|
+
runId: null,
|
|
78
|
+
startedAt: "2026-02-13T10:00:00.000Z",
|
|
79
|
+
lastActivity: "2026-02-13T10:30:00.000Z",
|
|
80
|
+
escalationLevel: 0,
|
|
81
|
+
stalledSince: null,
|
|
82
|
+
...overrides,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ============================================================
|
|
87
|
+
// Direct function tests (testing the store interactions)
|
|
88
|
+
// ============================================================
|
|
89
|
+
|
|
90
|
+
describe("show current run (default)", () => {
|
|
91
|
+
test("shows 'No active run' when current-run.txt does not exist", async () => {
|
|
92
|
+
// No current-run.txt written -- file does not exist
|
|
93
|
+
const file = Bun.file(join(overstoryDir, "current-run.txt"));
|
|
94
|
+
expect(await file.exists()).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("reads run ID from current-run.txt", async () => {
|
|
98
|
+
const runId = "run-2026-02-13T10:00:00.000Z";
|
|
99
|
+
await writeCurrentRun(runId);
|
|
100
|
+
const content = await readCurrentRunFile();
|
|
101
|
+
expect(content).toBe(runId);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("fetches run details from RunStore", () => {
|
|
105
|
+
const run = makeRun();
|
|
106
|
+
runStore.createRun(run);
|
|
107
|
+
|
|
108
|
+
const fetched = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
109
|
+
expect(fetched).not.toBeNull();
|
|
110
|
+
expect(fetched?.id).toBe("run-2026-02-13T10:00:00.000Z");
|
|
111
|
+
expect(fetched?.status).toBe("active");
|
|
112
|
+
expect(fetched?.agentCount).toBe(0);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns null for missing run ID", () => {
|
|
116
|
+
const fetched = runStore.getRun("nonexistent");
|
|
117
|
+
expect(fetched).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("list runs", () => {
|
|
122
|
+
test("returns empty array when no runs exist", () => {
|
|
123
|
+
const runs = runStore.listRuns({ limit: 10 });
|
|
124
|
+
expect(runs).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("returns runs ordered by started_at descending", () => {
|
|
128
|
+
runStore.createRun(makeRun({ id: "run-1", startedAt: "2026-02-13T08:00:00.000Z" }));
|
|
129
|
+
runStore.createRun(makeRun({ id: "run-2", startedAt: "2026-02-13T12:00:00.000Z" }));
|
|
130
|
+
runStore.createRun(makeRun({ id: "run-3", startedAt: "2026-02-13T10:00:00.000Z" }));
|
|
131
|
+
|
|
132
|
+
const runs = runStore.listRuns({ limit: 10 });
|
|
133
|
+
expect(runs).toHaveLength(3);
|
|
134
|
+
expect(runs[0]?.id).toBe("run-2");
|
|
135
|
+
expect(runs[1]?.id).toBe("run-3");
|
|
136
|
+
expect(runs[2]?.id).toBe("run-1");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("respects --last limit", () => {
|
|
140
|
+
for (let i = 0; i < 5; i++) {
|
|
141
|
+
runStore.createRun(
|
|
142
|
+
makeRun({
|
|
143
|
+
id: `run-${i}`,
|
|
144
|
+
startedAt: `2026-02-13T${String(10 + i).padStart(2, "0")}:00:00.000Z`,
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const runs = runStore.listRuns({ limit: 3 });
|
|
150
|
+
expect(runs).toHaveLength(3);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("includes completed and active runs", () => {
|
|
154
|
+
runStore.createRun(makeRun({ id: "run-active", status: "active" }));
|
|
155
|
+
runStore.createRun(
|
|
156
|
+
makeRun({
|
|
157
|
+
id: "run-done",
|
|
158
|
+
startedAt: "2026-02-13T11:00:00.000Z",
|
|
159
|
+
status: "active",
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
runStore.completeRun("run-done", "completed");
|
|
163
|
+
|
|
164
|
+
const runs = runStore.listRuns({ limit: 10 });
|
|
165
|
+
expect(runs).toHaveLength(2);
|
|
166
|
+
const statuses = runs.map((r) => r.status);
|
|
167
|
+
expect(statuses).toContain("active");
|
|
168
|
+
expect(statuses).toContain("completed");
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("complete run", () => {
|
|
173
|
+
test("marks active run as completed in RunStore", () => {
|
|
174
|
+
runStore.createRun(makeRun());
|
|
175
|
+
|
|
176
|
+
runStore.completeRun("run-2026-02-13T10:00:00.000Z", "completed");
|
|
177
|
+
|
|
178
|
+
const run = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
179
|
+
expect(run?.status).toBe("completed");
|
|
180
|
+
expect(run?.completedAt).not.toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("current-run.txt can be deleted after completion", async () => {
|
|
184
|
+
await writeCurrentRun("run-2026-02-13T10:00:00.000Z");
|
|
185
|
+
expect(await readCurrentRunFile()).toBe("run-2026-02-13T10:00:00.000Z");
|
|
186
|
+
|
|
187
|
+
// Simulate what completeCurrentRun does
|
|
188
|
+
await unlink(join(overstoryDir, "current-run.txt"));
|
|
189
|
+
const file = Bun.file(join(overstoryDir, "current-run.txt"));
|
|
190
|
+
expect(await file.exists()).toBe(false);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("no active run returns null from readCurrentRunFile", async () => {
|
|
194
|
+
// No current-run.txt exists
|
|
195
|
+
const content = await readCurrentRunFile();
|
|
196
|
+
expect(content).toBeNull();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("empty current-run.txt returns null", async () => {
|
|
200
|
+
await Bun.write(join(overstoryDir, "current-run.txt"), "");
|
|
201
|
+
const content = await readCurrentRunFile();
|
|
202
|
+
expect(content).toBeNull();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("whitespace-only current-run.txt returns null", async () => {
|
|
206
|
+
await Bun.write(join(overstoryDir, "current-run.txt"), " \n ");
|
|
207
|
+
const content = await readCurrentRunFile();
|
|
208
|
+
expect(content).toBeNull();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe("show run details", () => {
|
|
213
|
+
test("fetches run and its agents from stores", () => {
|
|
214
|
+
const runId = "run-2026-02-13T10:00:00.000Z";
|
|
215
|
+
runStore.createRun(makeRun({ agentCount: 2 }));
|
|
216
|
+
|
|
217
|
+
sessionStore.upsert(
|
|
218
|
+
makeSession({
|
|
219
|
+
agentName: "builder-1",
|
|
220
|
+
id: "s-1",
|
|
221
|
+
runId,
|
|
222
|
+
capability: "builder",
|
|
223
|
+
state: "working",
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
sessionStore.upsert(
|
|
227
|
+
makeSession({
|
|
228
|
+
agentName: "scout-1",
|
|
229
|
+
id: "s-2",
|
|
230
|
+
runId,
|
|
231
|
+
capability: "scout",
|
|
232
|
+
state: "completed",
|
|
233
|
+
}),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const run = runStore.getRun(runId);
|
|
237
|
+
expect(run).not.toBeNull();
|
|
238
|
+
expect(run?.agentCount).toBe(2);
|
|
239
|
+
|
|
240
|
+
const agents = sessionStore.getByRun(runId);
|
|
241
|
+
expect(agents).toHaveLength(2);
|
|
242
|
+
expect(agents.map((a) => a.agentName).sort()).toEqual(["builder-1", "scout-1"]);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("returns null for missing run", () => {
|
|
246
|
+
const run = runStore.getRun("nonexistent-run");
|
|
247
|
+
expect(run).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("returns empty agents for run with no sessions", () => {
|
|
251
|
+
runStore.createRun(makeRun());
|
|
252
|
+
const agents = sessionStore.getByRun("run-2026-02-13T10:00:00.000Z");
|
|
253
|
+
expect(agents).toEqual([]);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("agents include capability and state", () => {
|
|
257
|
+
const runId = "run-2026-02-13T10:00:00.000Z";
|
|
258
|
+
runStore.createRun(makeRun());
|
|
259
|
+
|
|
260
|
+
sessionStore.upsert(
|
|
261
|
+
makeSession({
|
|
262
|
+
agentName: "reviewer-1",
|
|
263
|
+
id: "s-1",
|
|
264
|
+
runId,
|
|
265
|
+
capability: "reviewer",
|
|
266
|
+
state: "stalled",
|
|
267
|
+
}),
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
const agents = sessionStore.getByRun(runId);
|
|
271
|
+
expect(agents).toHaveLength(1);
|
|
272
|
+
expect(agents[0]?.capability).toBe("reviewer");
|
|
273
|
+
expect(agents[0]?.state).toBe("stalled");
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("--json output mode", () => {
|
|
278
|
+
test("current run JSON includes run and duration", () => {
|
|
279
|
+
runStore.createRun(makeRun());
|
|
280
|
+
const run = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
281
|
+
expect(run).not.toBeNull();
|
|
282
|
+
|
|
283
|
+
// Simulate JSON output structure
|
|
284
|
+
const output = JSON.stringify({ run, duration: "some-duration" });
|
|
285
|
+
const parsed = JSON.parse(output) as { run: unknown; duration: string };
|
|
286
|
+
expect(parsed.run).not.toBeNull();
|
|
287
|
+
expect(parsed.duration).toBe("some-duration");
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("list JSON includes runs array", () => {
|
|
291
|
+
runStore.createRun(makeRun({ id: "run-1" }));
|
|
292
|
+
runStore.createRun(makeRun({ id: "run-2", startedAt: "2026-02-13T11:00:00.000Z" }));
|
|
293
|
+
|
|
294
|
+
const runs = runStore.listRuns({ limit: 10 });
|
|
295
|
+
const output = JSON.stringify({ runs });
|
|
296
|
+
const parsed = JSON.parse(output) as { runs: unknown[] };
|
|
297
|
+
expect(parsed.runs).toHaveLength(2);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("show JSON includes run and agents", () => {
|
|
301
|
+
const runId = "run-2026-02-13T10:00:00.000Z";
|
|
302
|
+
runStore.createRun(makeRun());
|
|
303
|
+
sessionStore.upsert(makeSession({ agentName: "a1", id: "s-1", runId }));
|
|
304
|
+
|
|
305
|
+
const run = runStore.getRun(runId);
|
|
306
|
+
const agents = sessionStore.getByRun(runId);
|
|
307
|
+
|
|
308
|
+
const output = JSON.stringify({ run, agents, duration: "1m 30s" });
|
|
309
|
+
const parsed = JSON.parse(output) as {
|
|
310
|
+
run: unknown;
|
|
311
|
+
agents: unknown[];
|
|
312
|
+
duration: string;
|
|
313
|
+
};
|
|
314
|
+
expect(parsed.run).not.toBeNull();
|
|
315
|
+
expect(parsed.agents).toHaveLength(1);
|
|
316
|
+
expect(parsed.duration).toBe("1m 30s");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("no active run JSON returns null run", () => {
|
|
320
|
+
// No current-run.txt exists
|
|
321
|
+
const output = JSON.stringify({ run: null, message: "No active run" });
|
|
322
|
+
const parsed = JSON.parse(output) as { run: unknown; message: string };
|
|
323
|
+
expect(parsed.run).toBeNull();
|
|
324
|
+
expect(parsed.message).toBe("No active run");
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("duration formatting", () => {
|
|
329
|
+
test("run with completedAt uses that for duration endpoint", () => {
|
|
330
|
+
runStore.createRun(makeRun());
|
|
331
|
+
runStore.completeRun("run-2026-02-13T10:00:00.000Z", "completed");
|
|
332
|
+
|
|
333
|
+
const run = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
334
|
+
expect(run?.completedAt).not.toBeNull();
|
|
335
|
+
|
|
336
|
+
// Verify the start and end are both set, so duration can be computed
|
|
337
|
+
const start = new Date(run?.startedAt ?? "").getTime();
|
|
338
|
+
const end = new Date(run?.completedAt ?? "").getTime();
|
|
339
|
+
expect(end).toBeGreaterThan(start);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("active run uses current time as duration endpoint", () => {
|
|
343
|
+
runStore.createRun(makeRun());
|
|
344
|
+
|
|
345
|
+
const run = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
346
|
+
expect(run?.completedAt).toBeNull();
|
|
347
|
+
expect(run?.status).toBe("active");
|
|
348
|
+
|
|
349
|
+
// Duration should be from startedAt to now
|
|
350
|
+
const start = new Date(run?.startedAt ?? "").getTime();
|
|
351
|
+
expect(Date.now() - start).toBeGreaterThan(0);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
describe("multiple runs lifecycle", () => {
|
|
356
|
+
test("create, use, complete multiple runs sequentially", async () => {
|
|
357
|
+
// Run 1
|
|
358
|
+
runStore.createRun(makeRun({ id: "run-1", startedAt: "2026-02-13T08:00:00.000Z" }));
|
|
359
|
+
await writeCurrentRun("run-1");
|
|
360
|
+
runStore.incrementAgentCount("run-1");
|
|
361
|
+
runStore.incrementAgentCount("run-1");
|
|
362
|
+
runStore.completeRun("run-1", "completed");
|
|
363
|
+
|
|
364
|
+
// Run 2
|
|
365
|
+
runStore.createRun(makeRun({ id: "run-2", startedAt: "2026-02-13T12:00:00.000Z" }));
|
|
366
|
+
await writeCurrentRun("run-2");
|
|
367
|
+
runStore.incrementAgentCount("run-2");
|
|
368
|
+
|
|
369
|
+
// Verify state
|
|
370
|
+
const currentRunId = await readCurrentRunFile();
|
|
371
|
+
expect(currentRunId).toBe("run-2");
|
|
372
|
+
|
|
373
|
+
const run1 = runStore.getRun("run-1");
|
|
374
|
+
expect(run1?.status).toBe("completed");
|
|
375
|
+
expect(run1?.agentCount).toBe(2);
|
|
376
|
+
|
|
377
|
+
const run2 = runStore.getRun("run-2");
|
|
378
|
+
expect(run2?.status).toBe("active");
|
|
379
|
+
expect(run2?.agentCount).toBe(1);
|
|
380
|
+
|
|
381
|
+
const allRuns = runStore.listRuns();
|
|
382
|
+
expect(allRuns).toHaveLength(2);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
describe("edge cases", () => {
|
|
387
|
+
test("run with zero agents", () => {
|
|
388
|
+
runStore.createRun(makeRun());
|
|
389
|
+
const run = runStore.getRun("run-2026-02-13T10:00:00.000Z");
|
|
390
|
+
expect(run?.agentCount).toBe(0);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("show run with agents from different capabilities", () => {
|
|
394
|
+
const runId = "run-2026-02-13T10:00:00.000Z";
|
|
395
|
+
runStore.createRun(makeRun({ agentCount: 3 }));
|
|
396
|
+
|
|
397
|
+
const capabilities = ["builder", "scout", "reviewer"];
|
|
398
|
+
for (let i = 0; i < capabilities.length; i++) {
|
|
399
|
+
sessionStore.upsert(
|
|
400
|
+
makeSession({
|
|
401
|
+
agentName: `agent-${i}`,
|
|
402
|
+
id: `s-${i}`,
|
|
403
|
+
runId,
|
|
404
|
+
capability: capabilities[i] ?? "builder",
|
|
405
|
+
}),
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const agents = sessionStore.getByRun(runId);
|
|
410
|
+
expect(agents).toHaveLength(3);
|
|
411
|
+
const caps = agents.map((a) => a.capability).sort();
|
|
412
|
+
expect(caps).toEqual(["builder", "reviewer", "scout"]);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("sessions.db does not exist for list returns empty", async () => {
|
|
416
|
+
// Remove the db that was created in beforeEach
|
|
417
|
+
runStore.close();
|
|
418
|
+
sessionStore.close();
|
|
419
|
+
await rm(dbPath, { force: true });
|
|
420
|
+
// Also remove WAL/SHM files
|
|
421
|
+
await rm(`${dbPath}-wal`, { force: true });
|
|
422
|
+
await rm(`${dbPath}-shm`, { force: true });
|
|
423
|
+
|
|
424
|
+
const file = Bun.file(dbPath);
|
|
425
|
+
expect(await file.exists()).toBe(false);
|
|
426
|
+
|
|
427
|
+
// Re-create stores for afterEach cleanup
|
|
428
|
+
runStore = createRunStore(join(overstoryDir, "unused-run.db"));
|
|
429
|
+
sessionStore = createSessionStore(join(overstoryDir, "unused-session.db"));
|
|
430
|
+
});
|
|
431
|
+
});
|