@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,745 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `overstory trace` command.
|
|
3
|
+
*
|
|
4
|
+
* Uses real bun:sqlite (temp files) to test the trace 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
|
+
* SessionStore). 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 { createSessionStore } from "../sessions/store.ts";
|
|
18
|
+
import type { InsertEvent } from "../types.ts";
|
|
19
|
+
import { traceCommand } from "./trace.ts";
|
|
20
|
+
|
|
21
|
+
/** Helper to create an InsertEvent with sensible defaults. */
|
|
22
|
+
function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
|
|
23
|
+
return {
|
|
24
|
+
runId: "run-001",
|
|
25
|
+
agentName: "builder-1",
|
|
26
|
+
sessionId: "sess-abc",
|
|
27
|
+
eventType: "tool_start",
|
|
28
|
+
toolName: "Read",
|
|
29
|
+
toolArgs: '{"file": "src/index.ts"}',
|
|
30
|
+
toolDurationMs: null,
|
|
31
|
+
level: "info",
|
|
32
|
+
data: null,
|
|
33
|
+
...overrides,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("traceCommand", () => {
|
|
38
|
+
let chunks: string[];
|
|
39
|
+
let originalWrite: typeof process.stdout.write;
|
|
40
|
+
let tempDir: string;
|
|
41
|
+
let originalCwd: string;
|
|
42
|
+
|
|
43
|
+
beforeEach(async () => {
|
|
44
|
+
// Spy on stdout
|
|
45
|
+
chunks = [];
|
|
46
|
+
originalWrite = process.stdout.write;
|
|
47
|
+
process.stdout.write = ((chunk: string) => {
|
|
48
|
+
chunks.push(chunk);
|
|
49
|
+
return true;
|
|
50
|
+
}) as typeof process.stdout.write;
|
|
51
|
+
|
|
52
|
+
// Create temp dir with .overstory/config.yaml structure
|
|
53
|
+
tempDir = await mkdtemp(join(tmpdir(), "trace-test-"));
|
|
54
|
+
const overstoryDir = join(tempDir, ".overstory");
|
|
55
|
+
await Bun.write(
|
|
56
|
+
join(overstoryDir, "config.yaml"),
|
|
57
|
+
`project:\n name: test\n root: ${tempDir}\n canonicalBranch: main\n`,
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Change to temp dir so loadConfig() works
|
|
61
|
+
originalCwd = process.cwd();
|
|
62
|
+
process.chdir(tempDir);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
afterEach(async () => {
|
|
66
|
+
process.stdout.write = originalWrite;
|
|
67
|
+
process.chdir(originalCwd);
|
|
68
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function output(): string {
|
|
72
|
+
return chunks.join("");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// === Help flag ===
|
|
76
|
+
|
|
77
|
+
describe("help flag", () => {
|
|
78
|
+
test("--help shows help text", async () => {
|
|
79
|
+
await traceCommand(["--help"]);
|
|
80
|
+
const out = output();
|
|
81
|
+
|
|
82
|
+
expect(out).toContain("overstory trace");
|
|
83
|
+
expect(out).toContain("<target>");
|
|
84
|
+
expect(out).toContain("--json");
|
|
85
|
+
expect(out).toContain("--since");
|
|
86
|
+
expect(out).toContain("--until");
|
|
87
|
+
expect(out).toContain("--limit");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("-h shows help text", async () => {
|
|
91
|
+
await traceCommand(["-h"]);
|
|
92
|
+
const out = output();
|
|
93
|
+
|
|
94
|
+
expect(out).toContain("overstory trace");
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// === Argument parsing ===
|
|
99
|
+
|
|
100
|
+
describe("argument parsing", () => {
|
|
101
|
+
test("missing target throws ValidationError", async () => {
|
|
102
|
+
await expect(traceCommand([])).rejects.toThrow(ValidationError);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("missing target error mentions usage", async () => {
|
|
106
|
+
try {
|
|
107
|
+
await traceCommand([]);
|
|
108
|
+
expect.unreachable("should have thrown");
|
|
109
|
+
} catch (err) {
|
|
110
|
+
expect(err).toBeInstanceOf(ValidationError);
|
|
111
|
+
expect((err as ValidationError).message).toContain("Missing target");
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("only flags with no target throws ValidationError", async () => {
|
|
116
|
+
await expect(traceCommand(["--json"])).rejects.toThrow(ValidationError);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("--limit with non-numeric value throws ValidationError", async () => {
|
|
120
|
+
await expect(traceCommand(["builder-1", "--limit", "abc"])).rejects.toThrow(ValidationError);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("--limit with zero throws ValidationError", async () => {
|
|
124
|
+
await expect(traceCommand(["builder-1", "--limit", "0"])).rejects.toThrow(ValidationError);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("--limit with negative value throws ValidationError", async () => {
|
|
128
|
+
await expect(traceCommand(["builder-1", "--limit", "-5"])).rejects.toThrow(ValidationError);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("--since with invalid timestamp throws ValidationError", async () => {
|
|
132
|
+
await expect(traceCommand(["builder-1", "--since", "not-a-date"])).rejects.toThrow(
|
|
133
|
+
ValidationError,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("--until with invalid timestamp throws ValidationError", async () => {
|
|
138
|
+
await expect(traceCommand(["builder-1", "--until", "not-a-date"])).rejects.toThrow(
|
|
139
|
+
ValidationError,
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("target is extracted correctly when flags come first", async () => {
|
|
144
|
+
// Create events.db with an event so the command runs to completion
|
|
145
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
146
|
+
const store = createEventStore(dbPath);
|
|
147
|
+
store.insert(makeEvent({ agentName: "my-agent" }));
|
|
148
|
+
store.close();
|
|
149
|
+
|
|
150
|
+
await traceCommand(["--json", "--limit", "50", "my-agent"]);
|
|
151
|
+
const out = output();
|
|
152
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
153
|
+
expect(parsed).toHaveLength(1);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("target is extracted correctly when flags come after", async () => {
|
|
157
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
158
|
+
const store = createEventStore(dbPath);
|
|
159
|
+
store.insert(makeEvent({ agentName: "my-agent" }));
|
|
160
|
+
store.close();
|
|
161
|
+
|
|
162
|
+
await traceCommand(["my-agent", "--json", "--limit", "50"]);
|
|
163
|
+
const out = output();
|
|
164
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
165
|
+
expect(parsed).toHaveLength(1);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// === Missing events.db (graceful handling) ===
|
|
170
|
+
|
|
171
|
+
describe("missing events.db", () => {
|
|
172
|
+
test("text mode outputs friendly message when no events.db exists", async () => {
|
|
173
|
+
await traceCommand(["builder-1"]);
|
|
174
|
+
const out = output();
|
|
175
|
+
|
|
176
|
+
expect(out).toBe("No events data yet.\n");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("JSON mode outputs empty array when no events.db exists", async () => {
|
|
180
|
+
await traceCommand(["builder-1", "--json"]);
|
|
181
|
+
const out = output();
|
|
182
|
+
|
|
183
|
+
expect(out).toBe("[]\n");
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// === JSON output mode ===
|
|
188
|
+
|
|
189
|
+
describe("JSON output mode", () => {
|
|
190
|
+
test("outputs valid JSON array with events", async () => {
|
|
191
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
192
|
+
const store = createEventStore(dbPath);
|
|
193
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
194
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
195
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
196
|
+
store.close();
|
|
197
|
+
|
|
198
|
+
await traceCommand(["builder-1", "--json"]);
|
|
199
|
+
const out = output();
|
|
200
|
+
|
|
201
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
202
|
+
expect(parsed).toHaveLength(3);
|
|
203
|
+
expect(Array.isArray(parsed)).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("JSON output includes expected fields", async () => {
|
|
207
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
208
|
+
const store = createEventStore(dbPath);
|
|
209
|
+
store.insert(
|
|
210
|
+
makeEvent({
|
|
211
|
+
agentName: "builder-1",
|
|
212
|
+
eventType: "tool_start",
|
|
213
|
+
toolName: "Bash",
|
|
214
|
+
level: "info",
|
|
215
|
+
}),
|
|
216
|
+
);
|
|
217
|
+
store.close();
|
|
218
|
+
|
|
219
|
+
await traceCommand(["builder-1", "--json"]);
|
|
220
|
+
const out = output();
|
|
221
|
+
|
|
222
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
223
|
+
expect(parsed).toHaveLength(1);
|
|
224
|
+
const event = parsed[0];
|
|
225
|
+
expect(event).toBeDefined();
|
|
226
|
+
expect(event?.agentName).toBe("builder-1");
|
|
227
|
+
expect(event?.eventType).toBe("tool_start");
|
|
228
|
+
expect(event?.toolName).toBe("Bash");
|
|
229
|
+
expect(event?.level).toBe("info");
|
|
230
|
+
expect(event?.createdAt).toBeTruthy();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("JSON output returns empty array when no events match agent", async () => {
|
|
234
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
235
|
+
const store = createEventStore(dbPath);
|
|
236
|
+
store.insert(makeEvent({ agentName: "other-agent" }));
|
|
237
|
+
store.close();
|
|
238
|
+
|
|
239
|
+
await traceCommand(["builder-1", "--json"]);
|
|
240
|
+
const out = output();
|
|
241
|
+
|
|
242
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
243
|
+
expect(parsed).toEqual([]);
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// === Timeline output format ===
|
|
248
|
+
|
|
249
|
+
describe("timeline output", () => {
|
|
250
|
+
test("shows header with agent name", async () => {
|
|
251
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
252
|
+
const store = createEventStore(dbPath);
|
|
253
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
254
|
+
store.close();
|
|
255
|
+
|
|
256
|
+
await traceCommand(["builder-1"]);
|
|
257
|
+
const out = output();
|
|
258
|
+
|
|
259
|
+
expect(out).toContain("Timeline for builder-1");
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("shows event count", async () => {
|
|
263
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
264
|
+
const store = createEventStore(dbPath);
|
|
265
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
266
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
267
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
268
|
+
store.close();
|
|
269
|
+
|
|
270
|
+
await traceCommand(["builder-1"]);
|
|
271
|
+
const out = output();
|
|
272
|
+
|
|
273
|
+
expect(out).toContain("3 events");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("shows singular event count", async () => {
|
|
277
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
278
|
+
const store = createEventStore(dbPath);
|
|
279
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
280
|
+
store.close();
|
|
281
|
+
|
|
282
|
+
await traceCommand(["builder-1"]);
|
|
283
|
+
const out = output();
|
|
284
|
+
|
|
285
|
+
expect(out).toContain("1 event");
|
|
286
|
+
// Should NOT say "1 events"
|
|
287
|
+
expect(out).not.toMatch(/1 events/);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("no events shows 'No events found' message", async () => {
|
|
291
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
292
|
+
const store = createEventStore(dbPath);
|
|
293
|
+
// Create the DB but don't insert anything for builder-1
|
|
294
|
+
store.insert(makeEvent({ agentName: "other-agent" }));
|
|
295
|
+
store.close();
|
|
296
|
+
|
|
297
|
+
await traceCommand(["builder-1"]);
|
|
298
|
+
const out = output();
|
|
299
|
+
|
|
300
|
+
expect(out).toContain("No events found");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("shows separator line", async () => {
|
|
304
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
305
|
+
const store = createEventStore(dbPath);
|
|
306
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
307
|
+
store.close();
|
|
308
|
+
|
|
309
|
+
await traceCommand(["builder-1"]);
|
|
310
|
+
const out = output();
|
|
311
|
+
|
|
312
|
+
expect(out).toContain("=".repeat(70));
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("event type labels are shown", async () => {
|
|
316
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
317
|
+
const store = createEventStore(dbPath);
|
|
318
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
319
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
320
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "error", level: "error" }));
|
|
321
|
+
store.close();
|
|
322
|
+
|
|
323
|
+
await traceCommand(["builder-1"]);
|
|
324
|
+
const out = output();
|
|
325
|
+
|
|
326
|
+
expect(out).toContain("SESSION +");
|
|
327
|
+
expect(out).toContain("TOOL START");
|
|
328
|
+
expect(out).toContain("ERROR");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("tool name is shown in detail", async () => {
|
|
332
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
333
|
+
const store = createEventStore(dbPath);
|
|
334
|
+
store.insert(
|
|
335
|
+
makeEvent({
|
|
336
|
+
agentName: "builder-1",
|
|
337
|
+
eventType: "tool_start",
|
|
338
|
+
toolName: "Bash",
|
|
339
|
+
}),
|
|
340
|
+
);
|
|
341
|
+
store.close();
|
|
342
|
+
|
|
343
|
+
await traceCommand(["builder-1"]);
|
|
344
|
+
const out = output();
|
|
345
|
+
|
|
346
|
+
expect(out).toContain("tool=Bash");
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("tool duration is shown in detail", async () => {
|
|
350
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
351
|
+
const store = createEventStore(dbPath);
|
|
352
|
+
store.insert(
|
|
353
|
+
makeEvent({
|
|
354
|
+
agentName: "builder-1",
|
|
355
|
+
eventType: "tool_start",
|
|
356
|
+
toolName: "Read",
|
|
357
|
+
toolDurationMs: 42,
|
|
358
|
+
}),
|
|
359
|
+
);
|
|
360
|
+
store.close();
|
|
361
|
+
|
|
362
|
+
await traceCommand(["builder-1"]);
|
|
363
|
+
const out = output();
|
|
364
|
+
|
|
365
|
+
expect(out).toContain("duration=42ms");
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("custom data fields are shown in detail", async () => {
|
|
369
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
370
|
+
const store = createEventStore(dbPath);
|
|
371
|
+
store.insert(
|
|
372
|
+
makeEvent({
|
|
373
|
+
agentName: "builder-1",
|
|
374
|
+
eventType: "custom",
|
|
375
|
+
toolName: null,
|
|
376
|
+
data: '{"reason":"testing","count":5}',
|
|
377
|
+
}),
|
|
378
|
+
);
|
|
379
|
+
store.close();
|
|
380
|
+
|
|
381
|
+
await traceCommand(["builder-1"]);
|
|
382
|
+
const out = output();
|
|
383
|
+
|
|
384
|
+
expect(out).toContain("reason=testing");
|
|
385
|
+
expect(out).toContain("count=5");
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("date separator appears in timeline", async () => {
|
|
389
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
390
|
+
const store = createEventStore(dbPath);
|
|
391
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
392
|
+
store.close();
|
|
393
|
+
|
|
394
|
+
await traceCommand(["builder-1"]);
|
|
395
|
+
const out = output();
|
|
396
|
+
|
|
397
|
+
// Should contain a date separator with --- prefix
|
|
398
|
+
expect(out).toMatch(/---\s+\d{4}-\d{2}-\d{2}\s+---/);
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// === --limit flag ===
|
|
403
|
+
|
|
404
|
+
describe("--limit flag", () => {
|
|
405
|
+
test("limits the number of events returned", async () => {
|
|
406
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
407
|
+
const store = createEventStore(dbPath);
|
|
408
|
+
for (let i = 0; i < 10; i++) {
|
|
409
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
410
|
+
}
|
|
411
|
+
store.close();
|
|
412
|
+
|
|
413
|
+
await traceCommand(["builder-1", "--json", "--limit", "3"]);
|
|
414
|
+
const out = output();
|
|
415
|
+
|
|
416
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
417
|
+
expect(parsed).toHaveLength(3);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
test("default limit is 100", async () => {
|
|
421
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
422
|
+
const store = createEventStore(dbPath);
|
|
423
|
+
for (let i = 0; i < 120; i++) {
|
|
424
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
425
|
+
}
|
|
426
|
+
store.close();
|
|
427
|
+
|
|
428
|
+
await traceCommand(["builder-1", "--json"]);
|
|
429
|
+
const out = output();
|
|
430
|
+
|
|
431
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
432
|
+
expect(parsed).toHaveLength(100);
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
// === --since and --until flags ===
|
|
437
|
+
|
|
438
|
+
describe("--since and --until flags", () => {
|
|
439
|
+
test("--since filters events after a timestamp", async () => {
|
|
440
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
441
|
+
const store = createEventStore(dbPath);
|
|
442
|
+
|
|
443
|
+
// Insert events -- all get "now" timestamps from SQLite
|
|
444
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
445
|
+
store.close();
|
|
446
|
+
|
|
447
|
+
// A future timestamp should return no events
|
|
448
|
+
await traceCommand(["builder-1", "--json", "--since", "2099-01-01T00:00:00Z"]);
|
|
449
|
+
const out = output();
|
|
450
|
+
|
|
451
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
452
|
+
expect(parsed).toEqual([]);
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
test("--since with past timestamp returns all events", async () => {
|
|
456
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
457
|
+
const store = createEventStore(dbPath);
|
|
458
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
459
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
460
|
+
store.close();
|
|
461
|
+
|
|
462
|
+
await traceCommand(["builder-1", "--json", "--since", "2020-01-01T00:00:00Z"]);
|
|
463
|
+
const out = output();
|
|
464
|
+
|
|
465
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
466
|
+
expect(parsed).toHaveLength(2);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("--until with past timestamp returns no events", async () => {
|
|
470
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
471
|
+
const store = createEventStore(dbPath);
|
|
472
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
473
|
+
store.close();
|
|
474
|
+
|
|
475
|
+
await traceCommand(["builder-1", "--json", "--until", "2000-01-01T00:00:00Z"]);
|
|
476
|
+
const out = output();
|
|
477
|
+
|
|
478
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
479
|
+
expect(parsed).toEqual([]);
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
test("--since causes absolute timestamps in text mode", async () => {
|
|
483
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
484
|
+
const store = createEventStore(dbPath);
|
|
485
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
486
|
+
store.close();
|
|
487
|
+
|
|
488
|
+
await traceCommand(["builder-1", "--since", "2020-01-01T00:00:00Z"]);
|
|
489
|
+
const out = output();
|
|
490
|
+
|
|
491
|
+
// Absolute timestamps show HH:MM:SS format
|
|
492
|
+
expect(out).toMatch(/\d{2}:\d{2}:\d{2}/);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
test("valid --since timestamp is accepted", async () => {
|
|
496
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
497
|
+
const store = createEventStore(dbPath);
|
|
498
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
499
|
+
store.close();
|
|
500
|
+
|
|
501
|
+
// Should not throw
|
|
502
|
+
await traceCommand(["builder-1", "--json", "--since", "2024-06-15T12:00:00Z"]);
|
|
503
|
+
const out = output();
|
|
504
|
+
// Should be valid JSON
|
|
505
|
+
JSON.parse(out.trim());
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// === Target resolution ===
|
|
510
|
+
|
|
511
|
+
describe("target resolution", () => {
|
|
512
|
+
test("agent name is used as-is when not a bead ID pattern", async () => {
|
|
513
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
514
|
+
const store = createEventStore(dbPath);
|
|
515
|
+
store.insert(makeEvent({ agentName: "my-custom-agent" }));
|
|
516
|
+
store.close();
|
|
517
|
+
|
|
518
|
+
await traceCommand(["my-custom-agent", "--json"]);
|
|
519
|
+
const out = output();
|
|
520
|
+
|
|
521
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
522
|
+
expect(parsed).toHaveLength(1);
|
|
523
|
+
expect(parsed[0]?.agentName).toBe("my-custom-agent");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
test("bead ID pattern is detected and resolved to agent name via SessionStore", async () => {
|
|
527
|
+
// Create a session that maps bead ID to agent name
|
|
528
|
+
const sessDbPath = join(tempDir, ".overstory", "sessions.db");
|
|
529
|
+
const sessionStore = createSessionStore(sessDbPath);
|
|
530
|
+
sessionStore.upsert({
|
|
531
|
+
id: "sess-001",
|
|
532
|
+
agentName: "builder-for-task",
|
|
533
|
+
capability: "builder",
|
|
534
|
+
worktreePath: "/tmp/wt",
|
|
535
|
+
branchName: "feat/task",
|
|
536
|
+
beadId: "overstory-rj1k",
|
|
537
|
+
tmuxSession: "tmux-001",
|
|
538
|
+
state: "completed",
|
|
539
|
+
pid: null,
|
|
540
|
+
parentAgent: null,
|
|
541
|
+
depth: 0,
|
|
542
|
+
runId: null,
|
|
543
|
+
startedAt: new Date().toISOString(),
|
|
544
|
+
lastActivity: new Date().toISOString(),
|
|
545
|
+
escalationLevel: 0,
|
|
546
|
+
stalledSince: null,
|
|
547
|
+
});
|
|
548
|
+
sessionStore.close();
|
|
549
|
+
|
|
550
|
+
// Create events for the agent name that the bead resolves to
|
|
551
|
+
const eventsDbPath = join(tempDir, ".overstory", "events.db");
|
|
552
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
553
|
+
eventStore.insert(makeEvent({ agentName: "builder-for-task" }));
|
|
554
|
+
eventStore.close();
|
|
555
|
+
|
|
556
|
+
await traceCommand(["overstory-rj1k", "--json"]);
|
|
557
|
+
const out = output();
|
|
558
|
+
|
|
559
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
560
|
+
expect(parsed).toHaveLength(1);
|
|
561
|
+
expect(parsed[0]?.agentName).toBe("builder-for-task");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
test("unresolved bead ID falls back to using bead ID as agent name", async () => {
|
|
565
|
+
// Create sessions.db but with no matching bead
|
|
566
|
+
const sessDbPath = join(tempDir, ".overstory", "sessions.db");
|
|
567
|
+
const sessionStore = createSessionStore(sessDbPath);
|
|
568
|
+
sessionStore.close();
|
|
569
|
+
|
|
570
|
+
// Create events.db (empty for this bead)
|
|
571
|
+
const eventsDbPath = join(tempDir, ".overstory", "events.db");
|
|
572
|
+
const eventStore = createEventStore(eventsDbPath);
|
|
573
|
+
eventStore.close();
|
|
574
|
+
|
|
575
|
+
await traceCommand(["myproj-abc1", "--json"]);
|
|
576
|
+
const out = output();
|
|
577
|
+
|
|
578
|
+
const parsed = JSON.parse(out.trim()) as unknown[];
|
|
579
|
+
expect(parsed).toEqual([]);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
test("short agent names without bead pattern are not resolved as bead IDs", async () => {
|
|
583
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
584
|
+
const store = createEventStore(dbPath);
|
|
585
|
+
store.insert(makeEvent({ agentName: "scout" }));
|
|
586
|
+
store.close();
|
|
587
|
+
|
|
588
|
+
// "scout" does not match bead pattern word-alphanumeric
|
|
589
|
+
await traceCommand(["scout", "--json"]);
|
|
590
|
+
const out = output();
|
|
591
|
+
|
|
592
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
593
|
+
expect(parsed).toHaveLength(1);
|
|
594
|
+
expect(parsed[0]?.agentName).toBe("scout");
|
|
595
|
+
});
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// === Event filtering edge cases ===
|
|
599
|
+
|
|
600
|
+
describe("edge cases", () => {
|
|
601
|
+
test("only returns events for the specified agent", async () => {
|
|
602
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
603
|
+
const store = createEventStore(dbPath);
|
|
604
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
605
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
606
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
607
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
608
|
+
store.close();
|
|
609
|
+
|
|
610
|
+
await traceCommand(["builder-1", "--json"]);
|
|
611
|
+
const out = output();
|
|
612
|
+
|
|
613
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
614
|
+
expect(parsed).toHaveLength(2);
|
|
615
|
+
for (const event of parsed) {
|
|
616
|
+
expect(event.agentName).toBe("builder-1");
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
test("all event types have labeled output", async () => {
|
|
621
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
622
|
+
const store = createEventStore(dbPath);
|
|
623
|
+
const eventTypes = [
|
|
624
|
+
"tool_start",
|
|
625
|
+
"tool_end",
|
|
626
|
+
"session_start",
|
|
627
|
+
"session_end",
|
|
628
|
+
"mail_sent",
|
|
629
|
+
"mail_received",
|
|
630
|
+
"spawn",
|
|
631
|
+
"error",
|
|
632
|
+
"custom",
|
|
633
|
+
] as const;
|
|
634
|
+
for (const eventType of eventTypes) {
|
|
635
|
+
store.insert(
|
|
636
|
+
makeEvent({
|
|
637
|
+
agentName: "builder-1",
|
|
638
|
+
eventType,
|
|
639
|
+
level: eventType === "error" ? "error" : "info",
|
|
640
|
+
}),
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
store.close();
|
|
644
|
+
|
|
645
|
+
await traceCommand(["builder-1"]);
|
|
646
|
+
const out = output();
|
|
647
|
+
|
|
648
|
+
// Verify all expected labels appear
|
|
649
|
+
expect(out).toContain("TOOL START");
|
|
650
|
+
expect(out).toContain("TOOL END");
|
|
651
|
+
expect(out).toContain("SESSION +");
|
|
652
|
+
expect(out).toContain("SESSION -");
|
|
653
|
+
expect(out).toContain("MAIL SENT");
|
|
654
|
+
expect(out).toContain("MAIL RECV");
|
|
655
|
+
expect(out).toContain("SPAWN");
|
|
656
|
+
expect(out).toContain("ERROR");
|
|
657
|
+
expect(out).toContain("CUSTOM");
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
test("long data values are truncated", async () => {
|
|
661
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
662
|
+
const store = createEventStore(dbPath);
|
|
663
|
+
const longValue = "x".repeat(200);
|
|
664
|
+
store.insert(
|
|
665
|
+
makeEvent({
|
|
666
|
+
agentName: "builder-1",
|
|
667
|
+
eventType: "custom",
|
|
668
|
+
toolName: null,
|
|
669
|
+
data: JSON.stringify({ message: longValue }),
|
|
670
|
+
}),
|
|
671
|
+
);
|
|
672
|
+
store.close();
|
|
673
|
+
|
|
674
|
+
await traceCommand(["builder-1"]);
|
|
675
|
+
const out = output();
|
|
676
|
+
|
|
677
|
+
// The full 200-char value should not appear
|
|
678
|
+
expect(out).not.toContain(longValue);
|
|
679
|
+
// But a truncated version with "..." should
|
|
680
|
+
expect(out).toContain("...");
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
test("non-JSON data is shown raw if short", async () => {
|
|
684
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
685
|
+
const store = createEventStore(dbPath);
|
|
686
|
+
store.insert(
|
|
687
|
+
makeEvent({
|
|
688
|
+
agentName: "builder-1",
|
|
689
|
+
eventType: "custom",
|
|
690
|
+
toolName: null,
|
|
691
|
+
data: "simple text data",
|
|
692
|
+
}),
|
|
693
|
+
);
|
|
694
|
+
store.close();
|
|
695
|
+
|
|
696
|
+
await traceCommand(["builder-1"]);
|
|
697
|
+
const out = output();
|
|
698
|
+
|
|
699
|
+
expect(out).toContain("simple text data");
|
|
700
|
+
});
|
|
701
|
+
|
|
702
|
+
test("events are ordered chronologically", async () => {
|
|
703
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
704
|
+
const store = createEventStore(dbPath);
|
|
705
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
706
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
707
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
708
|
+
store.close();
|
|
709
|
+
|
|
710
|
+
await traceCommand(["builder-1", "--json"]);
|
|
711
|
+
const out = output();
|
|
712
|
+
|
|
713
|
+
const parsed = JSON.parse(out.trim()) as Record<string, unknown>[];
|
|
714
|
+
expect(parsed).toHaveLength(3);
|
|
715
|
+
expect(parsed[0]?.eventType).toBe("session_start");
|
|
716
|
+
expect(parsed[1]?.eventType).toBe("tool_start");
|
|
717
|
+
expect(parsed[2]?.eventType).toBe("session_end");
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
test("handles event with all null optional fields", async () => {
|
|
721
|
+
const dbPath = join(tempDir, ".overstory", "events.db");
|
|
722
|
+
const store = createEventStore(dbPath);
|
|
723
|
+
store.insert(
|
|
724
|
+
makeEvent({
|
|
725
|
+
agentName: "builder-1",
|
|
726
|
+
eventType: "session_start",
|
|
727
|
+
runId: null,
|
|
728
|
+
sessionId: null,
|
|
729
|
+
toolName: null,
|
|
730
|
+
toolArgs: null,
|
|
731
|
+
toolDurationMs: null,
|
|
732
|
+
data: null,
|
|
733
|
+
}),
|
|
734
|
+
);
|
|
735
|
+
store.close();
|
|
736
|
+
|
|
737
|
+
// Should not throw
|
|
738
|
+
await traceCommand(["builder-1"]);
|
|
739
|
+
const out = output();
|
|
740
|
+
|
|
741
|
+
expect(out).toContain("Timeline for builder-1");
|
|
742
|
+
expect(out).toContain("1 event");
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
});
|