@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,578 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `overstory feed` command.
|
|
3
|
+
*
|
|
4
|
+
* Uses real bun:sqlite (temp files) to test the feed command end-to-end.
|
|
5
|
+
* Captures process.stdout.write to verify output formatting.
|
|
6
|
+
*
|
|
7
|
+
* Real implementations used for: filesystem (temp dirs), SQLite (EventStore).
|
|
8
|
+
* No mocks needed -- all dependencies are cheap and local.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { ValidationError } from "../errors.ts";
|
|
16
|
+
import { createEventStore } from "../events/store.ts";
|
|
17
|
+
import type { InsertEvent } from "../types.ts";
|
|
18
|
+
import { feedCommand } from "./feed.ts";
|
|
19
|
+
|
|
20
|
+
/** Helper to create an InsertEvent with sensible defaults. */
|
|
21
|
+
function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
|
|
22
|
+
return {
|
|
23
|
+
runId: "run-001",
|
|
24
|
+
agentName: "builder-1",
|
|
25
|
+
sessionId: "sess-abc",
|
|
26
|
+
eventType: "tool_start",
|
|
27
|
+
toolName: "Read",
|
|
28
|
+
toolArgs: '{"file": "src/index.ts"}',
|
|
29
|
+
toolDurationMs: null,
|
|
30
|
+
level: "info",
|
|
31
|
+
data: null,
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe("feedCommand", () => {
|
|
37
|
+
let chunks: string[];
|
|
38
|
+
let originalWrite: typeof process.stdout.write;
|
|
39
|
+
let tempDir: string;
|
|
40
|
+
let originalCwd: string;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
// Spy on stdout
|
|
44
|
+
chunks = [];
|
|
45
|
+
originalWrite = process.stdout.write;
|
|
46
|
+
process.stdout.write = ((chunk: string) => {
|
|
47
|
+
chunks.push(chunk);
|
|
48
|
+
return true;
|
|
49
|
+
}) as typeof process.stdout.write;
|
|
50
|
+
|
|
51
|
+
// Create temp dir with .overstory/config.yaml structure
|
|
52
|
+
tempDir = await mkdtemp(join(tmpdir(), "feed-test-"));
|
|
53
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
54
|
+
await Bun.write(
|
|
55
|
+
join(overstoryDir, "config.yaml"),
|
|
56
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Change to temp dir so loadConfig() works
|
|
60
|
+
originalCwd = process.cwd();
|
|
61
|
+
process.chdir(tempDir);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
process.stdout.write = originalWrite;
|
|
66
|
+
process.chdir(originalCwd);
|
|
67
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function output(): string {
|
|
71
|
+
return chunks.join("");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// === Help flag ===
|
|
75
|
+
|
|
76
|
+
describe("help flag", () => {
|
|
77
|
+
test("--help shows help text", async () => {
|
|
78
|
+
await feedCommand(["--help"]);
|
|
79
|
+
const out = output();
|
|
80
|
+
|
|
81
|
+
expect(out).toContain("overstory feed");
|
|
82
|
+
expect(out).toContain("--follow");
|
|
83
|
+
expect(out).toContain("--agent");
|
|
84
|
+
expect(out).toContain("--run");
|
|
85
|
+
expect(out).toContain("--since");
|
|
86
|
+
expect(out).toContain("--limit");
|
|
87
|
+
expect(out).toContain("--interval");
|
|
88
|
+
expect(out).toContain("--json");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("-h shows help text", async () => {
|
|
92
|
+
await feedCommand(["-h"]);
|
|
93
|
+
const out = output();
|
|
94
|
+
|
|
95
|
+
expect(out).toContain("overstory feed");
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// === Argument parsing ===
|
|
100
|
+
|
|
101
|
+
describe("argument parsing", () => {
|
|
102
|
+
test("--limit with non-numeric value throws ValidationError", async () => {
|
|
103
|
+
await expect(feedCommand(["--limit", "abc"])).rejects.toThrow(ValidationError);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("--limit with zero throws ValidationError", async () => {
|
|
107
|
+
await expect(feedCommand(["--limit", "0"])).rejects.toThrow(ValidationError);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("--limit with negative value throws ValidationError", async () => {
|
|
111
|
+
await expect(feedCommand(["--limit", "-5"])).rejects.toThrow(ValidationError);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("--interval with non-numeric value throws ValidationError", async () => {
|
|
115
|
+
await expect(feedCommand(["--interval", "abc"])).rejects.toThrow(ValidationError);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("--interval below 200 throws ValidationError", async () => {
|
|
119
|
+
await expect(feedCommand(["--interval", "100"])).rejects.toThrow(ValidationError);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("--since with invalid timestamp throws ValidationError", async () => {
|
|
123
|
+
await expect(feedCommand(["--since", "not-a-date"])).rejects.toThrow(ValidationError);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// === Missing events.db (graceful handling) ===
|
|
128
|
+
|
|
129
|
+
describe("missing events.db", () => {
|
|
130
|
+
test("text mode outputs friendly message when no events.db exists", async () => {
|
|
131
|
+
await feedCommand([]);
|
|
132
|
+
const out = output();
|
|
133
|
+
|
|
134
|
+
expect(out).toBe("No events data yet.\n");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("JSON mode outputs empty array when no events.db exists", async () => {
|
|
138
|
+
await feedCommand(["--json"]);
|
|
139
|
+
const out = output();
|
|
140
|
+
|
|
141
|
+
expect(out).toBe("[]\n");
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// === JSON output mode ===
|
|
146
|
+
|
|
147
|
+
describe("JSON output mode", () => {
|
|
148
|
+
test("outputs valid JSON array with events", async () => {
|
|
149
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
150
|
+
const store = createEventStore(dbPath);
|
|
151
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
152
|
+
store.insert(makeEvent({ agentName: "builder-2", eventType: "tool_start" }));
|
|
153
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
154
|
+
store.close();
|
|
155
|
+
|
|
156
|
+
await feedCommand(["--json"]);
|
|
157
|
+
const out = output();
|
|
158
|
+
|
|
159
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
160
|
+
expect(parsed).toHaveLength(3);
|
|
161
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("JSON output includes expected fields", async () => {
|
|
165
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
166
|
+
const store = createEventStore(dbPath);
|
|
167
|
+
store.insert(
|
|
168
|
+
makeEvent({
|
|
169
|
+
agentName: "builder-1",
|
|
170
|
+
eventType: "tool_start",
|
|
171
|
+
toolName: "Bash",
|
|
172
|
+
level: "info",
|
|
173
|
+
}),
|
|
174
|
+
);
|
|
175
|
+
store.close();
|
|
176
|
+
|
|
177
|
+
await feedCommand(["--json"]);
|
|
178
|
+
const out = output();
|
|
179
|
+
|
|
180
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
181
|
+
expect(parsed).toHaveLength(1);
|
|
182
|
+
const event = parsed[0];
|
|
183
|
+
expect(event).toBeDefined();
|
|
184
|
+
expect(event?.agentName).toBe("builder-1");
|
|
185
|
+
expect(event?.eventType).toBe("tool_start");
|
|
186
|
+
expect(event?.toolName).toBe("Bash");
|
|
187
|
+
expect(event?.level).toBe("info");
|
|
188
|
+
expect(event?.createdAt).toBeTruthy();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("JSON output returns empty array when no events match since filter", async () => {
|
|
192
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
193
|
+
const store = createEventStore(dbPath);
|
|
194
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
195
|
+
store.close();
|
|
196
|
+
|
|
197
|
+
// Query from future date
|
|
198
|
+
await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
199
|
+
const out = output();
|
|
200
|
+
|
|
201
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
202
|
+
expect(parsed).toEqual([]);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// === Feed output format ===
|
|
207
|
+
|
|
208
|
+
describe("feed output", () => {
|
|
209
|
+
test("shows events from multiple agents", async () => {
|
|
210
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
211
|
+
const store = createEventStore(dbPath);
|
|
212
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
213
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
214
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
215
|
+
store.close();
|
|
216
|
+
|
|
217
|
+
await feedCommand([]);
|
|
218
|
+
const out = output();
|
|
219
|
+
|
|
220
|
+
expect(out).toContain("builder-1");
|
|
221
|
+
expect(out).toContain("scout-1");
|
|
222
|
+
expect(out).toContain("builder-2");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("compact event labels are shown", async () => {
|
|
226
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
227
|
+
const store = createEventStore(dbPath);
|
|
228
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
229
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
230
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "mail_sent" }));
|
|
231
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "error", level: "error" }));
|
|
232
|
+
store.close();
|
|
233
|
+
|
|
234
|
+
await feedCommand([]);
|
|
235
|
+
const out = output();
|
|
236
|
+
|
|
237
|
+
expect(out).toContain("SESS+");
|
|
238
|
+
expect(out).toContain("TOOL+");
|
|
239
|
+
expect(out).toContain("MAIL>");
|
|
240
|
+
expect(out).toContain("ERROR");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("tool name is shown in detail", async () => {
|
|
244
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
245
|
+
const store = createEventStore(dbPath);
|
|
246
|
+
store.insert(
|
|
247
|
+
makeEvent({
|
|
248
|
+
agentName: "builder-1",
|
|
249
|
+
eventType: "tool_start",
|
|
250
|
+
toolName: "Bash",
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
store.close();
|
|
254
|
+
|
|
255
|
+
await feedCommand([]);
|
|
256
|
+
const out = output();
|
|
257
|
+
|
|
258
|
+
expect(out).toContain("tool=Bash");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("tool duration is shown in detail", async () => {
|
|
262
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
263
|
+
const store = createEventStore(dbPath);
|
|
264
|
+
store.insert(
|
|
265
|
+
makeEvent({
|
|
266
|
+
agentName: "builder-1",
|
|
267
|
+
eventType: "tool_start",
|
|
268
|
+
toolName: "Read",
|
|
269
|
+
toolDurationMs: 42,
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
store.close();
|
|
273
|
+
|
|
274
|
+
await feedCommand([]);
|
|
275
|
+
const out = output();
|
|
276
|
+
|
|
277
|
+
expect(out).toContain("42ms");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("absolute time format is shown (HH:MM:SS)", async () => {
|
|
281
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
282
|
+
const store = createEventStore(dbPath);
|
|
283
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
284
|
+
store.close();
|
|
285
|
+
|
|
286
|
+
await feedCommand([]);
|
|
287
|
+
const out = output();
|
|
288
|
+
|
|
289
|
+
// Should show HH:MM:SS format
|
|
290
|
+
expect(out).toMatch(/\d{2}:\d{2}:\d{2}/);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("no events shows 'No events found' message", async () => {
|
|
294
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
295
|
+
const store = createEventStore(dbPath);
|
|
296
|
+
// Create DB but no events
|
|
297
|
+
store.close();
|
|
298
|
+
|
|
299
|
+
await feedCommand([]);
|
|
300
|
+
const out = output();
|
|
301
|
+
|
|
302
|
+
expect(out).toContain("No events found");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// === --agent filter ===
|
|
307
|
+
|
|
308
|
+
describe("--agent filter", () => {
|
|
309
|
+
test("filters to single agent", async () => {
|
|
310
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
311
|
+
const store = createEventStore(dbPath);
|
|
312
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
313
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
314
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
315
|
+
store.close();
|
|
316
|
+
|
|
317
|
+
await feedCommand(["--agent", "builder-1", "--json"]);
|
|
318
|
+
const out = output();
|
|
319
|
+
|
|
320
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
321
|
+
expect(parsed).toHaveLength(1);
|
|
322
|
+
expect(parsed[0]?.agentName).toBe("builder-1");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("filters to multiple agents", async () => {
|
|
326
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
327
|
+
const store = createEventStore(dbPath);
|
|
328
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
329
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
330
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
331
|
+
store.close();
|
|
332
|
+
|
|
333
|
+
await feedCommand(["--agent", "builder-1", "--agent", "scout-1", "--json"]);
|
|
334
|
+
const out = output();
|
|
335
|
+
|
|
336
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
337
|
+
expect(parsed).toHaveLength(2);
|
|
338
|
+
const agents = parsed.map((e) => e.agentName);
|
|
339
|
+
expect(agents).toContain("builder-1");
|
|
340
|
+
expect(agents).toContain("scout-1");
|
|
341
|
+
expect(agents).not.toContain("builder-2");
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
// === --run filter ===
|
|
346
|
+
|
|
347
|
+
describe("--run filter", () => {
|
|
348
|
+
test("filters events by run ID", async () => {
|
|
349
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
350
|
+
const store = createEventStore(dbPath);
|
|
351
|
+
store.insert(makeEvent({ runId: "run-001", agentName: "builder-1" }));
|
|
352
|
+
store.insert(makeEvent({ runId: "run-002", agentName: "builder-2" }));
|
|
353
|
+
store.insert(makeEvent({ runId: "run-001", agentName: "scout-1" }));
|
|
354
|
+
store.close();
|
|
355
|
+
|
|
356
|
+
await feedCommand(["--run", "run-001", "--json"]);
|
|
357
|
+
const out = output();
|
|
358
|
+
|
|
359
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
360
|
+
expect(parsed).toHaveLength(2);
|
|
361
|
+
for (const event of parsed) {
|
|
362
|
+
expect(event.runId).toBe("run-001");
|
|
363
|
+
}
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
// === --limit flag ===
|
|
368
|
+
|
|
369
|
+
describe("--limit flag", () => {
|
|
370
|
+
test("limits the number of events returned", async () => {
|
|
371
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
372
|
+
const store = createEventStore(dbPath);
|
|
373
|
+
for (let i = 0; i < 100; i++) {
|
|
374
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
375
|
+
}
|
|
376
|
+
store.close();
|
|
377
|
+
|
|
378
|
+
await feedCommand(["--json", "--limit", "10"]);
|
|
379
|
+
const out = output();
|
|
380
|
+
|
|
381
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
382
|
+
expect(parsed).toHaveLength(10);
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("default limit is 50", async () => {
|
|
386
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
387
|
+
const store = createEventStore(dbPath);
|
|
388
|
+
for (let i = 0; i < 100; i++) {
|
|
389
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
390
|
+
}
|
|
391
|
+
store.close();
|
|
392
|
+
|
|
393
|
+
await feedCommand(["--json"]);
|
|
394
|
+
const out = output();
|
|
395
|
+
|
|
396
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
397
|
+
expect(parsed).toHaveLength(50);
|
|
398
|
+
});
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// === --since flag ===
|
|
402
|
+
|
|
403
|
+
describe("--since flag", () => {
|
|
404
|
+
test("--since filters events after a timestamp", async () => {
|
|
405
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
406
|
+
const store = createEventStore(dbPath);
|
|
407
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
408
|
+
store.close();
|
|
409
|
+
|
|
410
|
+
// A future timestamp should return no events
|
|
411
|
+
await feedCommand(["--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
412
|
+
const out = output();
|
|
413
|
+
|
|
414
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
415
|
+
expect(parsed).toEqual([]);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("--since with past timestamp returns all events", async () => {
|
|
419
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
420
|
+
const store = createEventStore(dbPath);
|
|
421
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
422
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
423
|
+
store.close();
|
|
424
|
+
|
|
425
|
+
await feedCommand(["--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
426
|
+
const out = output();
|
|
427
|
+
|
|
428
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
429
|
+
expect(parsed).toHaveLength(2);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test("default since is 5 minutes ago", async () => {
|
|
433
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
434
|
+
const store = createEventStore(dbPath);
|
|
435
|
+
// Insert event with current timestamp
|
|
436
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
437
|
+
store.close();
|
|
438
|
+
|
|
439
|
+
// Without --since, should get recent events
|
|
440
|
+
await feedCommand(["--json"]);
|
|
441
|
+
const out = output();
|
|
442
|
+
|
|
443
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
444
|
+
expect(parsed).toHaveLength(1);
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// === Event types coverage ===
|
|
449
|
+
|
|
450
|
+
describe("event types coverage", () => {
|
|
451
|
+
test("all event types have compact labels", async () => {
|
|
452
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
453
|
+
const store = createEventStore(dbPath);
|
|
454
|
+
const eventTypes = [
|
|
455
|
+
"tool_start",
|
|
456
|
+
"tool_end",
|
|
457
|
+
"session_start",
|
|
458
|
+
"session_end",
|
|
459
|
+
"mail_sent",
|
|
460
|
+
"mail_received",
|
|
461
|
+
"spawn",
|
|
462
|
+
"error",
|
|
463
|
+
"custom",
|
|
464
|
+
] as const;
|
|
465
|
+
for (const eventType of eventTypes) {
|
|
466
|
+
store.insert(
|
|
467
|
+
makeEvent({
|
|
468
|
+
agentName: "builder-1",
|
|
469
|
+
eventType,
|
|
470
|
+
level: eventType === "error" ? "error" : "info",
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
store.close();
|
|
475
|
+
|
|
476
|
+
await feedCommand([]);
|
|
477
|
+
const out = output();
|
|
478
|
+
|
|
479
|
+
// Verify all compact labels appear
|
|
480
|
+
expect(out).toContain("TOOL+");
|
|
481
|
+
expect(out).toContain("TOOL-");
|
|
482
|
+
expect(out).toContain("SESS+");
|
|
483
|
+
expect(out).toContain("SESS-");
|
|
484
|
+
expect(out).toContain("MAIL>");
|
|
485
|
+
expect(out).toContain("MAIL<");
|
|
486
|
+
expect(out).toContain("SPAWN");
|
|
487
|
+
expect(out).toContain("ERROR");
|
|
488
|
+
expect(out).toContain("CUSTM");
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// === Edge cases ===
|
|
493
|
+
|
|
494
|
+
describe("edge cases", () => {
|
|
495
|
+
test("events are ordered chronologically", async () => {
|
|
496
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
497
|
+
const store = createEventStore(dbPath);
|
|
498
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
499
|
+
store.insert(makeEvent({ agentName: "scout-1", eventType: "tool_start" }));
|
|
500
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
501
|
+
store.close();
|
|
502
|
+
|
|
503
|
+
await feedCommand(["--json"]);
|
|
504
|
+
const out = output();
|
|
505
|
+
|
|
506
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
507
|
+
expect(parsed).toHaveLength(3);
|
|
508
|
+
expect(parsed[0]?.eventType).toBe("session_start");
|
|
509
|
+
expect(parsed[1]?.eventType).toBe("tool_start");
|
|
510
|
+
expect(parsed[2]?.eventType).toBe("session_end");
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("handles event with all null optional fields", async () => {
|
|
514
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
515
|
+
const store = createEventStore(dbPath);
|
|
516
|
+
store.insert(
|
|
517
|
+
makeEvent({
|
|
518
|
+
agentName: "builder-1",
|
|
519
|
+
eventType: "session_start",
|
|
520
|
+
runId: null,
|
|
521
|
+
sessionId: null,
|
|
522
|
+
toolName: null,
|
|
523
|
+
toolArgs: null,
|
|
524
|
+
toolDurationMs: null,
|
|
525
|
+
data: null,
|
|
526
|
+
}),
|
|
527
|
+
);
|
|
528
|
+
store.close();
|
|
529
|
+
|
|
530
|
+
// Should not throw
|
|
531
|
+
await feedCommand([]);
|
|
532
|
+
const out = output();
|
|
533
|
+
|
|
534
|
+
expect(out).toContain("SESS+");
|
|
535
|
+
expect(out).toContain("builder-1");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
test("long data values are truncated in output", async () => {
|
|
539
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
540
|
+
const store = createEventStore(dbPath);
|
|
541
|
+
const longValue = "x".repeat(200);
|
|
542
|
+
store.insert(
|
|
543
|
+
makeEvent({
|
|
544
|
+
agentName: "builder-1",
|
|
545
|
+
eventType: "custom",
|
|
546
|
+
toolName: null,
|
|
547
|
+
data: JSON.stringify({ message: longValue }),
|
|
548
|
+
}),
|
|
549
|
+
);
|
|
550
|
+
store.close();
|
|
551
|
+
|
|
552
|
+
await feedCommand([]);
|
|
553
|
+
const out = output();
|
|
554
|
+
|
|
555
|
+
// The full 200-char value should not appear
|
|
556
|
+
expect(out).not.toContain(longValue);
|
|
557
|
+
// But a truncated version with "..." should
|
|
558
|
+
expect(out).toContain("...");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
test("agent color assignment is stable", async () => {
|
|
562
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
563
|
+
const store = createEventStore(dbPath);
|
|
564
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
565
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
566
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
567
|
+
store.close();
|
|
568
|
+
|
|
569
|
+
await feedCommand([]);
|
|
570
|
+
const out = output();
|
|
571
|
+
|
|
572
|
+
// Both builder-1 events should appear
|
|
573
|
+
expect(out).toContain("builder-1");
|
|
574
|
+
// scout-1 should appear
|
|
575
|
+
expect(out).toContain("scout-1");
|
|
576
|
+
});
|
|
577
|
+
});
|
|
578
|
+
});
|