@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,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the `overstory clean` command.
|
|
3
|
+
*
|
|
4
|
+
* Uses real filesystem (temp dirs), real git repos, real SQLite.
|
|
5
|
+
* No mocks. tmux operations are tested indirectly — when no tmux
|
|
6
|
+
* server is running, the command handles it gracefully.
|
|
7
|
+
*
|
|
8
|
+
* Philosophy: "never mock what you can use for real" (mx-252b16).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createEventStore } from "../events/store.ts";
|
|
16
|
+
import { createMailStore } from "../mail/store.ts";
|
|
17
|
+
import { createMergeQueue } from "../merge/queue.ts";
|
|
18
|
+
import { createMetricsStore } from "../metrics/store.ts";
|
|
19
|
+
import { openSessionStore } from "../sessions/compat.ts";
|
|
20
|
+
import { cleanupTempDir, createTempGitRepo } from "../test-helpers.ts";
|
|
21
|
+
import type { AgentSession } from "../types.ts";
|
|
22
|
+
import { cleanCommand } from "./clean.ts";
|
|
23
|
+
|
|
24
|
+
let tempDir: string;
|
|
25
|
+
let overstoryDir: string;
|
|
26
|
+
let originalCwd: string;
|
|
27
|
+
let stdoutOutput: string;
|
|
28
|
+
let _stderrOutput: string;
|
|
29
|
+
let originalStdoutWrite: typeof process.stdout.write;
|
|
30
|
+
let originalStderrWrite: typeof process.stderr.write;
|
|
31
|
+
|
|
32
|
+
beforeEach(async () => {
|
|
33
|
+
tempDir = await createTempGitRepo();
|
|
34
|
+
overstoryDir = join(tempDir, ".overstory");
|
|
35
|
+
await mkdir(overstoryDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
// Write minimal config.yaml so loadConfig succeeds
|
|
38
|
+
await Bun.write(
|
|
39
|
+
join(overstoryDir, "config.yaml"),
|
|
40
|
+
`project:\n name: test-project\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
// Create the standard directories
|
|
44
|
+
await mkdir(join(overstoryDir, "logs"), { recursive: true });
|
|
45
|
+
await mkdir(join(overstoryDir, "agents"), { recursive: true });
|
|
46
|
+
await mkdir(join(overstoryDir, "specs"), { recursive: true });
|
|
47
|
+
await mkdir(join(overstoryDir, "worktrees"), { recursive: true });
|
|
48
|
+
|
|
49
|
+
originalCwd = process.cwd();
|
|
50
|
+
process.chdir(tempDir);
|
|
51
|
+
|
|
52
|
+
// Capture stdout/stderr
|
|
53
|
+
stdoutOutput = "";
|
|
54
|
+
_stderrOutput = "";
|
|
55
|
+
originalStdoutWrite = process.stdout.write;
|
|
56
|
+
originalStderrWrite = process.stderr.write;
|
|
57
|
+
process.stdout.write = ((chunk: string) => {
|
|
58
|
+
stdoutOutput += chunk;
|
|
59
|
+
return true;
|
|
60
|
+
}) as typeof process.stdout.write;
|
|
61
|
+
process.stderr.write = ((chunk: string) => {
|
|
62
|
+
_stderrOutput += chunk;
|
|
63
|
+
return true;
|
|
64
|
+
}) as typeof process.stderr.write;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(async () => {
|
|
68
|
+
process.chdir(originalCwd);
|
|
69
|
+
process.stdout.write = originalStdoutWrite;
|
|
70
|
+
process.stderr.write = originalStderrWrite;
|
|
71
|
+
await cleanupTempDir(tempDir);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// === help ===
|
|
75
|
+
|
|
76
|
+
describe("help", () => {
|
|
77
|
+
test("--help shows usage", async () => {
|
|
78
|
+
await cleanCommand(["--help"]);
|
|
79
|
+
expect(stdoutOutput).toContain("overstory clean");
|
|
80
|
+
expect(stdoutOutput).toContain("--all");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("-h shows usage", async () => {
|
|
84
|
+
await cleanCommand(["-h"]);
|
|
85
|
+
expect(stdoutOutput).toContain("overstory clean");
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// === validation ===
|
|
90
|
+
|
|
91
|
+
describe("validation", () => {
|
|
92
|
+
test("no flags throws ValidationError", async () => {
|
|
93
|
+
await expect(cleanCommand([])).rejects.toThrow("No cleanup targets specified");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// === --all ===
|
|
98
|
+
|
|
99
|
+
describe("--all", () => {
|
|
100
|
+
test("wipes mail.db and WAL files", async () => {
|
|
101
|
+
// Create a mail DB with messages
|
|
102
|
+
const mailDbPath = join(overstoryDir, "mail.db");
|
|
103
|
+
const store = createMailStore(mailDbPath);
|
|
104
|
+
store.insert({
|
|
105
|
+
id: "msg-1",
|
|
106
|
+
from: "agent-a",
|
|
107
|
+
to: "agent-b",
|
|
108
|
+
subject: "test",
|
|
109
|
+
body: "hello",
|
|
110
|
+
type: "status",
|
|
111
|
+
priority: "normal",
|
|
112
|
+
threadId: null,
|
|
113
|
+
});
|
|
114
|
+
store.close();
|
|
115
|
+
|
|
116
|
+
// Verify DB exists
|
|
117
|
+
expect(await Bun.file(mailDbPath).exists()).toBe(true);
|
|
118
|
+
|
|
119
|
+
await cleanCommand(["--all"]);
|
|
120
|
+
|
|
121
|
+
// DB should be gone
|
|
122
|
+
expect(await Bun.file(mailDbPath).exists()).toBe(false);
|
|
123
|
+
expect(stdoutOutput).toContain("Wiped mail.db");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("wipes metrics.db", async () => {
|
|
127
|
+
const metricsDbPath = join(overstoryDir, "metrics.db");
|
|
128
|
+
const store = createMetricsStore(metricsDbPath);
|
|
129
|
+
store.recordSession({
|
|
130
|
+
agentName: "test-agent",
|
|
131
|
+
beadId: "task-1",
|
|
132
|
+
capability: "builder",
|
|
133
|
+
startedAt: new Date().toISOString(),
|
|
134
|
+
completedAt: null,
|
|
135
|
+
durationMs: 0,
|
|
136
|
+
exitCode: null,
|
|
137
|
+
mergeResult: null,
|
|
138
|
+
parentAgent: null,
|
|
139
|
+
inputTokens: 0,
|
|
140
|
+
outputTokens: 0,
|
|
141
|
+
cacheReadTokens: 0,
|
|
142
|
+
cacheCreationTokens: 0,
|
|
143
|
+
estimatedCostUsd: null,
|
|
144
|
+
modelUsed: null,
|
|
145
|
+
runId: null,
|
|
146
|
+
});
|
|
147
|
+
store.close();
|
|
148
|
+
|
|
149
|
+
expect(await Bun.file(metricsDbPath).exists()).toBe(true);
|
|
150
|
+
|
|
151
|
+
await cleanCommand(["--all"]);
|
|
152
|
+
|
|
153
|
+
expect(await Bun.file(metricsDbPath).exists()).toBe(false);
|
|
154
|
+
expect(stdoutOutput).toContain("Wiped metrics.db");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("wipes sessions.db", async () => {
|
|
158
|
+
// Use the SessionStore to create sessions.db with data
|
|
159
|
+
const { store } = openSessionStore(overstoryDir);
|
|
160
|
+
store.upsert({
|
|
161
|
+
id: "s1",
|
|
162
|
+
agentName: "test-agent",
|
|
163
|
+
capability: "builder",
|
|
164
|
+
worktreePath: "/tmp/wt",
|
|
165
|
+
branchName: "overstory/test/task",
|
|
166
|
+
beadId: "task-1",
|
|
167
|
+
tmuxSession: "overstory-test-agent",
|
|
168
|
+
state: "completed",
|
|
169
|
+
pid: 12345,
|
|
170
|
+
parentAgent: null,
|
|
171
|
+
depth: 1,
|
|
172
|
+
runId: null,
|
|
173
|
+
startedAt: new Date().toISOString(),
|
|
174
|
+
lastActivity: new Date().toISOString(),
|
|
175
|
+
escalationLevel: 0,
|
|
176
|
+
stalledSince: null,
|
|
177
|
+
});
|
|
178
|
+
store.close();
|
|
179
|
+
|
|
180
|
+
const sessionsDbPath = join(overstoryDir, "sessions.db");
|
|
181
|
+
expect(await Bun.file(sessionsDbPath).exists()).toBe(true);
|
|
182
|
+
|
|
183
|
+
await cleanCommand(["--all"]);
|
|
184
|
+
|
|
185
|
+
expect(await Bun.file(sessionsDbPath).exists()).toBe(false);
|
|
186
|
+
expect(stdoutOutput).toContain("Wiped sessions.db");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("wipes merge-queue.db", async () => {
|
|
190
|
+
const queuePath = join(overstoryDir, "merge-queue.db");
|
|
191
|
+
// Create a queue with an entry so we can verify it gets wiped
|
|
192
|
+
const queue = createMergeQueue(queuePath);
|
|
193
|
+
queue.enqueue({
|
|
194
|
+
branchName: "test-branch",
|
|
195
|
+
beadId: "beads-test",
|
|
196
|
+
agentName: "test",
|
|
197
|
+
filesModified: ["src/test.ts"],
|
|
198
|
+
});
|
|
199
|
+
queue.close();
|
|
200
|
+
|
|
201
|
+
await cleanCommand(["--all"]);
|
|
202
|
+
|
|
203
|
+
expect(await Bun.file(queuePath).exists()).toBe(false);
|
|
204
|
+
expect(stdoutOutput).toContain("Wiped merge-queue.db");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("clears logs directory contents", async () => {
|
|
208
|
+
const logsDir = join(overstoryDir, "logs");
|
|
209
|
+
await mkdir(join(logsDir, "agent-a", "2026-01-01"), { recursive: true });
|
|
210
|
+
await writeFile(join(logsDir, "agent-a", "2026-01-01", "session.log"), "log data");
|
|
211
|
+
|
|
212
|
+
await cleanCommand(["--all"]);
|
|
213
|
+
|
|
214
|
+
const entries = await readdir(logsDir);
|
|
215
|
+
expect(entries).toHaveLength(0);
|
|
216
|
+
expect(stdoutOutput).toContain("Cleared logs/");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("clears agents directory contents", async () => {
|
|
220
|
+
const agentsDir = join(overstoryDir, "agents");
|
|
221
|
+
await mkdir(join(agentsDir, "test-agent"), { recursive: true });
|
|
222
|
+
await writeFile(join(agentsDir, "test-agent", "identity.yaml"), "name: test-agent");
|
|
223
|
+
|
|
224
|
+
await cleanCommand(["--all"]);
|
|
225
|
+
|
|
226
|
+
const entries = await readdir(agentsDir);
|
|
227
|
+
expect(entries).toHaveLength(0);
|
|
228
|
+
expect(stdoutOutput).toContain("Cleared agents/");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("clears specs directory contents", async () => {
|
|
232
|
+
const specsDir = join(overstoryDir, "specs");
|
|
233
|
+
await writeFile(join(specsDir, "task-123.md"), "# Spec");
|
|
234
|
+
|
|
235
|
+
await cleanCommand(["--all"]);
|
|
236
|
+
|
|
237
|
+
const entries = await readdir(specsDir);
|
|
238
|
+
expect(entries).toHaveLength(0);
|
|
239
|
+
expect(stdoutOutput).toContain("Cleared specs/");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("deletes nudge-state.json", async () => {
|
|
243
|
+
const nudgePath = join(overstoryDir, "nudge-state.json");
|
|
244
|
+
await Bun.write(nudgePath, "{}");
|
|
245
|
+
|
|
246
|
+
await cleanCommand(["--all"]);
|
|
247
|
+
|
|
248
|
+
expect(await Bun.file(nudgePath).exists()).toBe(false);
|
|
249
|
+
expect(stdoutOutput).toContain("Cleared nudge-state.json");
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("deletes current-run.txt", async () => {
|
|
253
|
+
const currentRunPath = join(overstoryDir, "current-run.txt");
|
|
254
|
+
await Bun.write(currentRunPath, "run-2026-02-13T10-00-00-000Z");
|
|
255
|
+
|
|
256
|
+
await cleanCommand(["--all"]);
|
|
257
|
+
|
|
258
|
+
expect(await Bun.file(currentRunPath).exists()).toBe(false);
|
|
259
|
+
expect(stdoutOutput).toContain("Cleared current-run.txt");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("handles missing current-run.txt gracefully", async () => {
|
|
263
|
+
// current-run.txt does not exist — should not error
|
|
264
|
+
await cleanCommand(["--all"]);
|
|
265
|
+
expect(stdoutOutput).not.toContain("Cleared current-run.txt");
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// === individual flags ===
|
|
270
|
+
|
|
271
|
+
describe("individual flags", () => {
|
|
272
|
+
test("--mail only wipes mail.db, leaves other state intact", async () => {
|
|
273
|
+
// Create mail and sessions
|
|
274
|
+
const mailDbPath = join(overstoryDir, "mail.db");
|
|
275
|
+
const store = createMailStore(mailDbPath);
|
|
276
|
+
store.insert({
|
|
277
|
+
id: "msg-1",
|
|
278
|
+
from: "a",
|
|
279
|
+
to: "b",
|
|
280
|
+
subject: "test",
|
|
281
|
+
body: "hi",
|
|
282
|
+
type: "status",
|
|
283
|
+
priority: "normal",
|
|
284
|
+
threadId: null,
|
|
285
|
+
});
|
|
286
|
+
store.close();
|
|
287
|
+
|
|
288
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
289
|
+
await Bun.write(sessionsPath, '[{"id":"s1"}]\n');
|
|
290
|
+
|
|
291
|
+
await cleanCommand(["--mail"]);
|
|
292
|
+
|
|
293
|
+
// Mail gone
|
|
294
|
+
expect(await Bun.file(mailDbPath).exists()).toBe(false);
|
|
295
|
+
// Sessions untouched
|
|
296
|
+
const sessionsContent = await Bun.file(sessionsPath).text();
|
|
297
|
+
expect(JSON.parse(sessionsContent)).toEqual([{ id: "s1" }]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("--sessions only wipes sessions.db", async () => {
|
|
301
|
+
// Create sessions.db with data
|
|
302
|
+
const sessionsDbPath = join(overstoryDir, "sessions.db");
|
|
303
|
+
const { store } = openSessionStore(overstoryDir);
|
|
304
|
+
store.upsert({
|
|
305
|
+
id: "s1",
|
|
306
|
+
agentName: "test-agent",
|
|
307
|
+
capability: "builder",
|
|
308
|
+
worktreePath: "/tmp/wt",
|
|
309
|
+
branchName: "overstory/test/task",
|
|
310
|
+
beadId: "task-1",
|
|
311
|
+
tmuxSession: "overstory-test-agent",
|
|
312
|
+
state: "completed",
|
|
313
|
+
pid: 12345,
|
|
314
|
+
parentAgent: null,
|
|
315
|
+
depth: 1,
|
|
316
|
+
runId: null,
|
|
317
|
+
startedAt: new Date().toISOString(),
|
|
318
|
+
lastActivity: new Date().toISOString(),
|
|
319
|
+
escalationLevel: 0,
|
|
320
|
+
stalledSince: null,
|
|
321
|
+
});
|
|
322
|
+
store.close();
|
|
323
|
+
|
|
324
|
+
// Create a spec file that should survive
|
|
325
|
+
await writeFile(join(overstoryDir, "specs", "task.md"), "spec");
|
|
326
|
+
|
|
327
|
+
await cleanCommand(["--sessions"]);
|
|
328
|
+
|
|
329
|
+
// sessions.db should be gone
|
|
330
|
+
expect(await Bun.file(sessionsDbPath).exists()).toBe(false);
|
|
331
|
+
|
|
332
|
+
// Specs untouched
|
|
333
|
+
const specEntries = await readdir(join(overstoryDir, "specs"));
|
|
334
|
+
expect(specEntries).toHaveLength(1);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("--logs clears logs but nothing else", async () => {
|
|
338
|
+
const logsDir = join(overstoryDir, "logs");
|
|
339
|
+
await mkdir(join(logsDir, "agent-x"), { recursive: true });
|
|
340
|
+
await writeFile(join(logsDir, "agent-x", "session.log"), "data");
|
|
341
|
+
|
|
342
|
+
await writeFile(join(overstoryDir, "specs", "task.md"), "spec");
|
|
343
|
+
|
|
344
|
+
await cleanCommand(["--logs"]);
|
|
345
|
+
|
|
346
|
+
const logEntries = await readdir(logsDir);
|
|
347
|
+
expect(logEntries).toHaveLength(0);
|
|
348
|
+
|
|
349
|
+
// Specs untouched
|
|
350
|
+
const specEntries = await readdir(join(overstoryDir, "specs"));
|
|
351
|
+
expect(specEntries).toHaveLength(1);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// === idempotent ===
|
|
356
|
+
|
|
357
|
+
describe("idempotent", () => {
|
|
358
|
+
test("running --all when nothing exists does not error", async () => {
|
|
359
|
+
await cleanCommand(["--all"]);
|
|
360
|
+
expect(stdoutOutput).toContain("Nothing to clean");
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("running --all twice does not error", async () => {
|
|
364
|
+
// Create some state
|
|
365
|
+
const mailDbPath = join(overstoryDir, "mail.db");
|
|
366
|
+
const store = createMailStore(mailDbPath);
|
|
367
|
+
store.close();
|
|
368
|
+
|
|
369
|
+
await cleanCommand(["--all"]);
|
|
370
|
+
stdoutOutput = "";
|
|
371
|
+
await cleanCommand(["--all"]);
|
|
372
|
+
expect(stdoutOutput).toContain("Nothing to clean");
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// === JSON output ===
|
|
377
|
+
|
|
378
|
+
describe("JSON output", () => {
|
|
379
|
+
test("--json flag produces valid JSON", async () => {
|
|
380
|
+
const mailDbPath = join(overstoryDir, "mail.db");
|
|
381
|
+
const store = createMailStore(mailDbPath);
|
|
382
|
+
store.insert({
|
|
383
|
+
id: "msg-1",
|
|
384
|
+
from: "a",
|
|
385
|
+
to: "b",
|
|
386
|
+
subject: "test",
|
|
387
|
+
body: "hi",
|
|
388
|
+
type: "status",
|
|
389
|
+
priority: "normal",
|
|
390
|
+
threadId: null,
|
|
391
|
+
});
|
|
392
|
+
store.close();
|
|
393
|
+
|
|
394
|
+
await cleanCommand(["--all", "--json"]);
|
|
395
|
+
|
|
396
|
+
const result = JSON.parse(stdoutOutput);
|
|
397
|
+
expect(result).toHaveProperty("tmuxKilled");
|
|
398
|
+
expect(result).toHaveProperty("mailWiped");
|
|
399
|
+
expect(result).toHaveProperty("sessionsCleared");
|
|
400
|
+
expect(result).toHaveProperty("metricsWiped");
|
|
401
|
+
expect(result.mailWiped).toBe(true);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("--json includes sessionEndEventsLogged field", async () => {
|
|
405
|
+
await cleanCommand(["--all", "--json"]);
|
|
406
|
+
const result = JSON.parse(stdoutOutput);
|
|
407
|
+
expect(result).toHaveProperty("sessionEndEventsLogged");
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("--json includes currentRunCleared field", async () => {
|
|
411
|
+
const currentRunPath = join(overstoryDir, "current-run.txt");
|
|
412
|
+
await Bun.write(currentRunPath, "run-2026-02-13T10-00-00-000Z");
|
|
413
|
+
|
|
414
|
+
await cleanCommand(["--all", "--json"]);
|
|
415
|
+
const result = JSON.parse(stdoutOutput);
|
|
416
|
+
expect(result).toHaveProperty("currentRunCleared");
|
|
417
|
+
expect(result.currentRunCleared).toBe(true);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
// === synthetic session-end events ===
|
|
422
|
+
|
|
423
|
+
describe("synthetic session-end events", () => {
|
|
424
|
+
function makeSession(overrides: Partial<AgentSession> = {}): AgentSession {
|
|
425
|
+
return {
|
|
426
|
+
id: "s1",
|
|
427
|
+
agentName: "test-builder",
|
|
428
|
+
capability: "builder",
|
|
429
|
+
worktreePath: "/tmp/wt",
|
|
430
|
+
branchName: "overstory/test-builder/task-1",
|
|
431
|
+
beadId: "task-1",
|
|
432
|
+
tmuxSession: "overstory-test-builder",
|
|
433
|
+
state: "working",
|
|
434
|
+
pid: 12345,
|
|
435
|
+
parentAgent: null,
|
|
436
|
+
depth: 1,
|
|
437
|
+
runId: null,
|
|
438
|
+
startedAt: new Date().toISOString(),
|
|
439
|
+
lastActivity: new Date().toISOString(),
|
|
440
|
+
escalationLevel: 0,
|
|
441
|
+
stalledSince: null,
|
|
442
|
+
...overrides,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
test("logs session-end events for active agents before killing tmux", async () => {
|
|
447
|
+
// Write sessions.json with an active agent
|
|
448
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
449
|
+
const sessions = [makeSession({ agentName: "builder-a", state: "working" })];
|
|
450
|
+
await Bun.write(sessionsPath, JSON.stringify(sessions));
|
|
451
|
+
|
|
452
|
+
await cleanCommand(["--all"]);
|
|
453
|
+
|
|
454
|
+
// Verify event was written to events.db
|
|
455
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
456
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
457
|
+
const events = eventStore.getByAgent("builder-a");
|
|
458
|
+
eventStore.close();
|
|
459
|
+
|
|
460
|
+
const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
|
|
461
|
+
expect(sessionEndEvents).toHaveLength(1);
|
|
462
|
+
expect(sessionEndEvents[0]?.agentName).toBe("builder-a");
|
|
463
|
+
expect(sessionEndEvents[0]?.level).toBe("info");
|
|
464
|
+
|
|
465
|
+
const data = JSON.parse(sessionEndEvents[0]?.data ?? "{}");
|
|
466
|
+
expect(data.reason).toBe("clean");
|
|
467
|
+
expect(data.capability).toBe("builder");
|
|
468
|
+
|
|
469
|
+
expect(stdoutOutput).toContain("Logged 1 synthetic session-end event");
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("logs events for multiple active agents", async () => {
|
|
473
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
474
|
+
const sessions = [
|
|
475
|
+
makeSession({ id: "s1", agentName: "builder-a", state: "working" }),
|
|
476
|
+
makeSession({ id: "s2", agentName: "scout-b", capability: "scout", state: "booting" }),
|
|
477
|
+
makeSession({ id: "s3", agentName: "builder-c", state: "stalled" }),
|
|
478
|
+
];
|
|
479
|
+
await Bun.write(sessionsPath, JSON.stringify(sessions));
|
|
480
|
+
|
|
481
|
+
await cleanCommand(["--all"]);
|
|
482
|
+
|
|
483
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
484
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
485
|
+
|
|
486
|
+
for (const name of ["builder-a", "scout-b", "builder-c"]) {
|
|
487
|
+
const events = eventStore.getByAgent(name);
|
|
488
|
+
const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
|
|
489
|
+
expect(sessionEndEvents).toHaveLength(1);
|
|
490
|
+
}
|
|
491
|
+
eventStore.close();
|
|
492
|
+
|
|
493
|
+
expect(stdoutOutput).toContain("Logged 3 synthetic session-end events");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("skips completed and zombie sessions", async () => {
|
|
497
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
498
|
+
const sessions = [
|
|
499
|
+
makeSession({ id: "s1", agentName: "completed-agent", state: "completed" }),
|
|
500
|
+
makeSession({ id: "s2", agentName: "zombie-agent", state: "zombie" }),
|
|
501
|
+
];
|
|
502
|
+
await Bun.write(sessionsPath, JSON.stringify(sessions));
|
|
503
|
+
|
|
504
|
+
await cleanCommand(["--all"]);
|
|
505
|
+
|
|
506
|
+
// events.db may not even be created if there are no events to log
|
|
507
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
508
|
+
if (await Bun.file(eventsDbPath).exists()) {
|
|
509
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
510
|
+
const events1 = eventStore.getByAgent("completed-agent");
|
|
511
|
+
const events2 = eventStore.getByAgent("zombie-agent");
|
|
512
|
+
eventStore.close();
|
|
513
|
+
expect(events1).toHaveLength(0);
|
|
514
|
+
expect(events2).toHaveLength(0);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("--worktrees also logs session-end events (not just --all)", async () => {
|
|
519
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
520
|
+
const sessions = [makeSession({ agentName: "wt-agent", state: "working" })];
|
|
521
|
+
await Bun.write(sessionsPath, JSON.stringify(sessions));
|
|
522
|
+
|
|
523
|
+
await cleanCommand(["--worktrees"]);
|
|
524
|
+
|
|
525
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
526
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
527
|
+
const events = eventStore.getByAgent("wt-agent");
|
|
528
|
+
eventStore.close();
|
|
529
|
+
|
|
530
|
+
const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
|
|
531
|
+
expect(sessionEndEvents).toHaveLength(1);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
test("includes runId and sessionId from agent session", async () => {
|
|
535
|
+
const sessionsPath = join(overstoryDir, "sessions.json");
|
|
536
|
+
const sessions = [
|
|
537
|
+
makeSession({
|
|
538
|
+
agentName: "tracked-agent",
|
|
539
|
+
id: "session-123",
|
|
540
|
+
runId: "run-456",
|
|
541
|
+
state: "working",
|
|
542
|
+
}),
|
|
543
|
+
];
|
|
544
|
+
await Bun.write(sessionsPath, JSON.stringify(sessions));
|
|
545
|
+
|
|
546
|
+
await cleanCommand(["--all"]);
|
|
547
|
+
|
|
548
|
+
const eventsDbPath = join(overstoryDir, "events.db");
|
|
549
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
550
|
+
const events = eventStore.getByAgent("tracked-agent");
|
|
551
|
+
eventStore.close();
|
|
552
|
+
|
|
553
|
+
const sessionEndEvents = events.filter((e) => e.eventType === "session_end");
|
|
554
|
+
expect(sessionEndEvents).toHaveLength(1);
|
|
555
|
+
expect(sessionEndEvents[0]?.sessionId).toBe("session-123");
|
|
556
|
+
expect(sessionEndEvents[0]?.runId).toBe("run-456");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("handles missing sessions.json gracefully", async () => {
|
|
560
|
+
// No sessions.json file — should not error
|
|
561
|
+
await cleanCommand(["--all"]);
|
|
562
|
+
// Just verify it didn't crash
|
|
563
|
+
expect(stdoutOutput).toBeDefined();
|
|
564
|
+
});
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
// === mulch health checks ===
|
|
568
|
+
|
|
569
|
+
describe("mulch health checks", () => {
|
|
570
|
+
test("runs mulch health checks when --all is passed", async () => {
|
|
571
|
+
// Create a real .mulch directory with some data
|
|
572
|
+
const mulchDir = join(tempDir, ".mulch");
|
|
573
|
+
await mkdir(mulchDir, { recursive: true });
|
|
574
|
+
await mkdir(join(mulchDir, "domains"), { recursive: true });
|
|
575
|
+
|
|
576
|
+
// Create a domain file with some records
|
|
577
|
+
const domainPath = join(mulchDir, "domains", "test-domain.jsonl");
|
|
578
|
+
await writeFile(
|
|
579
|
+
domainPath,
|
|
580
|
+
`{"id":"mx-1","type":"convention","description":"Test record 1","recorded_at":"2026-01-01T00:00:00Z"}\n`,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
await cleanCommand(["--all"]);
|
|
584
|
+
|
|
585
|
+
// Mulch health checks should have run (might show warnings or might be clean)
|
|
586
|
+
// The output should not error, and if there are no issues, it's fine
|
|
587
|
+
expect(stdoutOutput).toBeDefined();
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
test("handles missing .mulch directory gracefully", async () => {
|
|
591
|
+
// No .mulch directory — should not error
|
|
592
|
+
await cleanCommand(["--all"]);
|
|
593
|
+
expect(stdoutOutput).toBeDefined();
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("JSON output includes mulchHealth field when mulch checks run", async () => {
|
|
597
|
+
// Create a .mulch directory
|
|
598
|
+
const mulchDir = join(tempDir, ".mulch");
|
|
599
|
+
await mkdir(mulchDir, { recursive: true });
|
|
600
|
+
await mkdir(join(mulchDir, "domains"), { recursive: true });
|
|
601
|
+
|
|
602
|
+
// Create a domain file
|
|
603
|
+
const domainPath = join(mulchDir, "domains", "test-domain.jsonl");
|
|
604
|
+
await writeFile(
|
|
605
|
+
domainPath,
|
|
606
|
+
`{"id":"mx-1","type":"convention","description":"Test","recorded_at":"2026-01-01T00:00:00Z"}\n`,
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
await cleanCommand(["--all", "--json"]);
|
|
610
|
+
|
|
611
|
+
const result = JSON.parse(stdoutOutput);
|
|
612
|
+
expect(result).toHaveProperty("mulchHealth");
|
|
613
|
+
|
|
614
|
+
// If mulch checks ran, mulchHealth should be an object (not null)
|
|
615
|
+
// If mulch was unavailable, it will be null
|
|
616
|
+
if (result.mulchHealth !== null) {
|
|
617
|
+
expect(result.mulchHealth).toHaveProperty("checked");
|
|
618
|
+
expect(result.mulchHealth).toHaveProperty("domainsNearLimit");
|
|
619
|
+
expect(result.mulchHealth).toHaveProperty("stalePruneCandidates");
|
|
620
|
+
expect(result.mulchHealth).toHaveProperty("doctorIssues");
|
|
621
|
+
expect(result.mulchHealth).toHaveProperty("doctorWarnings");
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
test("does not run mulch checks when only individual flags are used", async () => {
|
|
626
|
+
// Create a .mulch directory
|
|
627
|
+
const mulchDir = join(tempDir, ".mulch");
|
|
628
|
+
await mkdir(mulchDir, { recursive: true });
|
|
629
|
+
|
|
630
|
+
// Run clean with only --mail (not --all)
|
|
631
|
+
const mailDbPath = join(overstoryDir, "mail.db");
|
|
632
|
+
const store = createMailStore(mailDbPath);
|
|
633
|
+
store.close();
|
|
634
|
+
|
|
635
|
+
await cleanCommand(["--mail", "--json"]);
|
|
636
|
+
|
|
637
|
+
const result = JSON.parse(stdoutOutput);
|
|
638
|
+
// mulchHealth should be null because we didn't use --all
|
|
639
|
+
expect(result.mulchHealth).toBeNull();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("warns about domains approaching governance limits", async () => {
|
|
643
|
+
// Create a .mulch directory with a domain that has many records
|
|
644
|
+
const mulchDir = join(tempDir, ".mulch");
|
|
645
|
+
await mkdir(mulchDir, { recursive: true });
|
|
646
|
+
await mkdir(join(mulchDir, "domains"), { recursive: true });
|
|
647
|
+
|
|
648
|
+
// Create a domain with 410 records (above the 400 warn threshold)
|
|
649
|
+
const domainPath = join(mulchDir, "domains", "large-domain.jsonl");
|
|
650
|
+
const records = [];
|
|
651
|
+
for (let i = 1; i <= 410; i++) {
|
|
652
|
+
records.push(
|
|
653
|
+
`{"id":"mx-${i}","type":"convention","description":"Record ${i}","recorded_at":"2026-01-01T00:00:00Z"}`,
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
await writeFile(domainPath, `${records.join("\n")}\n`);
|
|
657
|
+
|
|
658
|
+
// Only run if mulch CLI is actually available
|
|
659
|
+
const mulchAvailable = existsSync(join(mulchDir, "domains", "large-domain.jsonl"));
|
|
660
|
+
if (!mulchAvailable) {
|
|
661
|
+
return; // Skip this test if mulch setup failed
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
await cleanCommand(["--all"]);
|
|
665
|
+
|
|
666
|
+
// Should show warning about domain near limit (if mulch status worked)
|
|
667
|
+
// The exact output depends on whether mulch CLI is available in the test environment
|
|
668
|
+
expect(stdoutOutput).toBeDefined();
|
|
669
|
+
});
|
|
670
|
+
});
|