@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,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for EventStore (SQLite-backed event observability storage).
|
|
3
|
+
*
|
|
4
|
+
* Uses real bun:sqlite with :memory: databases. No mocks.
|
|
5
|
+
* Philosophy: "never mock what you can use for real".
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
9
|
+
import type { EventStore, InsertEvent, StoredEvent, ToolStats } from "../types.ts";
|
|
10
|
+
import { createEventStore } from "./store.ts";
|
|
11
|
+
|
|
12
|
+
let store: EventStore;
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
store = createEventStore(":memory:");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
store.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
/** Helper to create an InsertEvent with sensible defaults. */
|
|
23
|
+
function makeEvent(overrides: Partial<InsertEvent> = {}): InsertEvent {
|
|
24
|
+
return {
|
|
25
|
+
runId: "run-001",
|
|
26
|
+
agentName: "builder-1",
|
|
27
|
+
sessionId: "sess-abc",
|
|
28
|
+
eventType: "tool_start",
|
|
29
|
+
toolName: "Read",
|
|
30
|
+
toolArgs: '{"file": "src/index.ts"}',
|
|
31
|
+
toolDurationMs: null,
|
|
32
|
+
level: "info",
|
|
33
|
+
data: null,
|
|
34
|
+
...overrides,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// === insert ===
|
|
39
|
+
|
|
40
|
+
describe("insert", () => {
|
|
41
|
+
test("inserts an event and returns the auto-generated id", () => {
|
|
42
|
+
const id = store.insert(makeEvent());
|
|
43
|
+
expect(id).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("sequential inserts return incrementing ids", () => {
|
|
47
|
+
const id1 = store.insert(makeEvent());
|
|
48
|
+
const id2 = store.insert(makeEvent({ agentName: "builder-2" }));
|
|
49
|
+
const id3 = store.insert(makeEvent({ agentName: "builder-3" }));
|
|
50
|
+
|
|
51
|
+
expect(id1).toBe(1);
|
|
52
|
+
expect(id2).toBe(2);
|
|
53
|
+
expect(id3).toBe(3);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("all fields roundtrip correctly", () => {
|
|
57
|
+
const event = makeEvent({
|
|
58
|
+
runId: "run-xyz",
|
|
59
|
+
agentName: "scout-1",
|
|
60
|
+
sessionId: "sess-999",
|
|
61
|
+
eventType: "session_start",
|
|
62
|
+
toolName: null,
|
|
63
|
+
toolArgs: null,
|
|
64
|
+
toolDurationMs: null,
|
|
65
|
+
level: "warn",
|
|
66
|
+
data: '{"reason": "something happened"}',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const id = store.insert(event);
|
|
70
|
+
const retrieved = store.getByAgent("scout-1");
|
|
71
|
+
|
|
72
|
+
expect(retrieved).toHaveLength(1);
|
|
73
|
+
const stored = retrieved[0] as StoredEvent;
|
|
74
|
+
expect(stored.id).toBe(id);
|
|
75
|
+
expect(stored.runId).toBe("run-xyz");
|
|
76
|
+
expect(stored.agentName).toBe("scout-1");
|
|
77
|
+
expect(stored.sessionId).toBe("sess-999");
|
|
78
|
+
expect(stored.eventType).toBe("session_start");
|
|
79
|
+
expect(stored.toolName).toBeNull();
|
|
80
|
+
expect(stored.toolArgs).toBeNull();
|
|
81
|
+
expect(stored.toolDurationMs).toBeNull();
|
|
82
|
+
expect(stored.level).toBe("warn");
|
|
83
|
+
expect(stored.data).toBe('{"reason": "something happened"}');
|
|
84
|
+
expect(stored.createdAt).toBeTruthy();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("null fields stored and retrieved as null", () => {
|
|
88
|
+
const id = store.insert(
|
|
89
|
+
makeEvent({
|
|
90
|
+
runId: null,
|
|
91
|
+
sessionId: null,
|
|
92
|
+
toolName: null,
|
|
93
|
+
toolArgs: null,
|
|
94
|
+
toolDurationMs: null,
|
|
95
|
+
data: null,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const events = store.getByAgent("builder-1");
|
|
100
|
+
expect(events).toHaveLength(1);
|
|
101
|
+
const stored = events[0] as StoredEvent;
|
|
102
|
+
expect(stored.id).toBe(id);
|
|
103
|
+
expect(stored.runId).toBeNull();
|
|
104
|
+
expect(stored.sessionId).toBeNull();
|
|
105
|
+
expect(stored.toolName).toBeNull();
|
|
106
|
+
expect(stored.toolArgs).toBeNull();
|
|
107
|
+
expect(stored.toolDurationMs).toBeNull();
|
|
108
|
+
expect(stored.data).toBeNull();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("rejects invalid level at DB level", () => {
|
|
112
|
+
expect(() => store.insert(makeEvent({ level: "critical" as InsertEvent["level"] }))).toThrow();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("createdAt is auto-populated by SQLite", () => {
|
|
116
|
+
store.insert(makeEvent());
|
|
117
|
+
const events = store.getByAgent("builder-1");
|
|
118
|
+
expect(events).toHaveLength(1);
|
|
119
|
+
const stored = events[0] as StoredEvent;
|
|
120
|
+
// Should be a valid ISO-ish timestamp
|
|
121
|
+
expect(stored.createdAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// === correlateToolEnd ===
|
|
126
|
+
|
|
127
|
+
describe("correlateToolEnd", () => {
|
|
128
|
+
test("finds matching tool_start and returns duration", () => {
|
|
129
|
+
store.insert(
|
|
130
|
+
makeEvent({
|
|
131
|
+
eventType: "tool_start",
|
|
132
|
+
toolName: "Bash",
|
|
133
|
+
toolDurationMs: null,
|
|
134
|
+
}),
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Small delay to ensure measurable duration
|
|
138
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
139
|
+
expect(result).not.toBeNull();
|
|
140
|
+
expect(result?.startId).toBe(1);
|
|
141
|
+
expect(result?.durationMs).toBeGreaterThanOrEqual(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("returns null when no matching tool_start exists", () => {
|
|
145
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
146
|
+
expect(result).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("returns null when tool_start is for a different agent", () => {
|
|
150
|
+
store.insert(
|
|
151
|
+
makeEvent({
|
|
152
|
+
agentName: "scout-1",
|
|
153
|
+
eventType: "tool_start",
|
|
154
|
+
toolName: "Bash",
|
|
155
|
+
}),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
159
|
+
expect(result).toBeNull();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("returns null when tool_start is for a different tool", () => {
|
|
163
|
+
store.insert(
|
|
164
|
+
makeEvent({
|
|
165
|
+
eventType: "tool_start",
|
|
166
|
+
toolName: "Read",
|
|
167
|
+
}),
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
171
|
+
expect(result).toBeNull();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("does not match already-correlated tool_start (has duration)", () => {
|
|
175
|
+
store.insert(
|
|
176
|
+
makeEvent({
|
|
177
|
+
eventType: "tool_start",
|
|
178
|
+
toolName: "Bash",
|
|
179
|
+
toolDurationMs: 500, // already has duration
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
184
|
+
expect(result).toBeNull();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("correlates with the most recent unmatched tool_start", () => {
|
|
188
|
+
// Insert two tool_starts for the same tool
|
|
189
|
+
store.insert(
|
|
190
|
+
makeEvent({
|
|
191
|
+
eventType: "tool_start",
|
|
192
|
+
toolName: "Bash",
|
|
193
|
+
toolDurationMs: null,
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
store.insert(
|
|
197
|
+
makeEvent({
|
|
198
|
+
eventType: "tool_start",
|
|
199
|
+
toolName: "Bash",
|
|
200
|
+
toolDurationMs: null,
|
|
201
|
+
}),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
205
|
+
expect(result).not.toBeNull();
|
|
206
|
+
// Should match the second (most recent) tool_start
|
|
207
|
+
expect(result?.startId).toBe(2);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("updates tool_duration_ms on the start event after correlation", () => {
|
|
211
|
+
store.insert(
|
|
212
|
+
makeEvent({
|
|
213
|
+
eventType: "tool_start",
|
|
214
|
+
toolName: "Bash",
|
|
215
|
+
toolDurationMs: null,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const result = store.correlateToolEnd("builder-1", "Bash");
|
|
220
|
+
expect(result).not.toBeNull();
|
|
221
|
+
|
|
222
|
+
// After correlation, the start event should have a duration
|
|
223
|
+
const events = store.getByAgent("builder-1");
|
|
224
|
+
expect(events).toHaveLength(1);
|
|
225
|
+
const updated = events[0] as StoredEvent;
|
|
226
|
+
expect(updated.toolDurationMs).toBeGreaterThanOrEqual(0);
|
|
227
|
+
|
|
228
|
+
// A second correlation should return null (already matched)
|
|
229
|
+
const secondResult = store.correlateToolEnd("builder-1", "Bash");
|
|
230
|
+
expect(secondResult).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// === getByAgent ===
|
|
235
|
+
|
|
236
|
+
describe("getByAgent", () => {
|
|
237
|
+
test("returns events for a specific agent", () => {
|
|
238
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
239
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
240
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_end" }));
|
|
241
|
+
|
|
242
|
+
const events = store.getByAgent("builder-1");
|
|
243
|
+
expect(events).toHaveLength(2);
|
|
244
|
+
for (const e of events) {
|
|
245
|
+
expect(e.agentName).toBe("builder-1");
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("returns events in chronological order (ASC)", () => {
|
|
250
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
251
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
252
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
253
|
+
|
|
254
|
+
const events = store.getByAgent("builder-1");
|
|
255
|
+
expect(events).toHaveLength(3);
|
|
256
|
+
expect(events[0]?.eventType).toBe("session_start");
|
|
257
|
+
expect(events[1]?.eventType).toBe("tool_start");
|
|
258
|
+
expect(events[2]?.eventType).toBe("session_end");
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("returns empty array for unknown agent", () => {
|
|
262
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
263
|
+
const events = store.getByAgent("unknown-agent");
|
|
264
|
+
expect(events).toEqual([]);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("respects limit option", () => {
|
|
268
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
269
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_end" }));
|
|
270
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_end" }));
|
|
271
|
+
|
|
272
|
+
const events = store.getByAgent("builder-1", { limit: 2 });
|
|
273
|
+
expect(events).toHaveLength(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("respects level filter", () => {
|
|
277
|
+
store.insert(makeEvent({ agentName: "builder-1", level: "info" }));
|
|
278
|
+
store.insert(makeEvent({ agentName: "builder-1", level: "error" }));
|
|
279
|
+
store.insert(makeEvent({ agentName: "builder-1", level: "info" }));
|
|
280
|
+
|
|
281
|
+
const events = store.getByAgent("builder-1", { level: "error" });
|
|
282
|
+
expect(events).toHaveLength(1);
|
|
283
|
+
expect(events[0]?.level).toBe("error");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// === getByRun ===
|
|
288
|
+
|
|
289
|
+
describe("getByRun", () => {
|
|
290
|
+
test("returns events for a specific run", () => {
|
|
291
|
+
store.insert(makeEvent({ runId: "run-001" }));
|
|
292
|
+
store.insert(makeEvent({ runId: "run-002" }));
|
|
293
|
+
store.insert(makeEvent({ runId: "run-001", eventType: "session_end" }));
|
|
294
|
+
|
|
295
|
+
const events = store.getByRun("run-001");
|
|
296
|
+
expect(events).toHaveLength(2);
|
|
297
|
+
for (const e of events) {
|
|
298
|
+
expect(e.runId).toBe("run-001");
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("returns events in chronological order (ASC)", () => {
|
|
303
|
+
store.insert(makeEvent({ runId: "run-001", eventType: "session_start" }));
|
|
304
|
+
store.insert(makeEvent({ runId: "run-001", eventType: "tool_start" }));
|
|
305
|
+
store.insert(makeEvent({ runId: "run-001", eventType: "session_end" }));
|
|
306
|
+
|
|
307
|
+
const events = store.getByRun("run-001");
|
|
308
|
+
expect(events).toHaveLength(3);
|
|
309
|
+
expect(events[0]?.eventType).toBe("session_start");
|
|
310
|
+
expect(events[2]?.eventType).toBe("session_end");
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("returns empty array for unknown run", () => {
|
|
314
|
+
const events = store.getByRun("nonexistent-run");
|
|
315
|
+
expect(events).toEqual([]);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("respects limit option", () => {
|
|
319
|
+
store.insert(makeEvent({ runId: "run-001" }));
|
|
320
|
+
store.insert(makeEvent({ runId: "run-001" }));
|
|
321
|
+
store.insert(makeEvent({ runId: "run-001" }));
|
|
322
|
+
|
|
323
|
+
const events = store.getByRun("run-001", { limit: 1 });
|
|
324
|
+
expect(events).toHaveLength(1);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// === getErrors ===
|
|
329
|
+
|
|
330
|
+
describe("getErrors", () => {
|
|
331
|
+
test("returns only error-level events", () => {
|
|
332
|
+
store.insert(makeEvent({ level: "info" }));
|
|
333
|
+
store.insert(makeEvent({ level: "error", eventType: "error", data: '{"msg": "fail1"}' }));
|
|
334
|
+
store.insert(makeEvent({ level: "warn" }));
|
|
335
|
+
store.insert(makeEvent({ level: "error", eventType: "error", data: '{"msg": "fail2"}' }));
|
|
336
|
+
|
|
337
|
+
const errors = store.getErrors();
|
|
338
|
+
expect(errors).toHaveLength(2);
|
|
339
|
+
for (const e of errors) {
|
|
340
|
+
expect(e.level).toBe("error");
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("returns errors in reverse chronological order (most recent first)", () => {
|
|
345
|
+
store.insert(
|
|
346
|
+
makeEvent({
|
|
347
|
+
level: "error",
|
|
348
|
+
agentName: "agent-a",
|
|
349
|
+
eventType: "error",
|
|
350
|
+
}),
|
|
351
|
+
);
|
|
352
|
+
store.insert(
|
|
353
|
+
makeEvent({
|
|
354
|
+
level: "error",
|
|
355
|
+
agentName: "agent-b",
|
|
356
|
+
eventType: "error",
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
const errors = store.getErrors();
|
|
361
|
+
expect(errors).toHaveLength(2);
|
|
362
|
+
// Both are error level; verify they are returned (order depends on
|
|
363
|
+
// sub-millisecond timestamps which may tie, so just verify content)
|
|
364
|
+
const names = errors.map((e) => e.agentName).sort();
|
|
365
|
+
expect(names).toEqual(["agent-a", "agent-b"]);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("returns empty array when no errors exist", () => {
|
|
369
|
+
store.insert(makeEvent({ level: "info" }));
|
|
370
|
+
store.insert(makeEvent({ level: "warn" }));
|
|
371
|
+
|
|
372
|
+
const errors = store.getErrors();
|
|
373
|
+
expect(errors).toEqual([]);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("respects limit option", () => {
|
|
377
|
+
for (let i = 0; i < 5; i++) {
|
|
378
|
+
store.insert(makeEvent({ level: "error", eventType: "error" }));
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const errors = store.getErrors({ limit: 3 });
|
|
382
|
+
expect(errors).toHaveLength(3);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// === getTimeline ===
|
|
387
|
+
|
|
388
|
+
describe("getTimeline", () => {
|
|
389
|
+
test("returns events since a given timestamp", () => {
|
|
390
|
+
// Insert events with default timestamps (all "now")
|
|
391
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
392
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
393
|
+
|
|
394
|
+
// Use a past timestamp to capture all events
|
|
395
|
+
const events = store.getTimeline({ since: "2020-01-01T00:00:00Z" });
|
|
396
|
+
expect(events).toHaveLength(2);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("returns events in chronological order (ASC)", () => {
|
|
400
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "session_start" }));
|
|
401
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_start" }));
|
|
402
|
+
|
|
403
|
+
const events = store.getTimeline({ since: "2020-01-01T00:00:00Z" });
|
|
404
|
+
expect(events).toHaveLength(2);
|
|
405
|
+
expect(events[0]?.eventType).toBe("session_start");
|
|
406
|
+
expect(events[1]?.eventType).toBe("tool_start");
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("respects limit option", () => {
|
|
410
|
+
for (let i = 0; i < 10; i++) {
|
|
411
|
+
store.insert(makeEvent());
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const events = store.getTimeline({ since: "2020-01-01T00:00:00Z", limit: 5 });
|
|
415
|
+
expect(events).toHaveLength(5);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("returns empty array when no events match the time range", () => {
|
|
419
|
+
store.insert(makeEvent());
|
|
420
|
+
|
|
421
|
+
// Use a future timestamp -- no events should match
|
|
422
|
+
const events = store.getTimeline({ since: "2099-01-01T00:00:00Z" });
|
|
423
|
+
expect(events).toEqual([]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("respects level filter", () => {
|
|
427
|
+
store.insert(makeEvent({ level: "info" }));
|
|
428
|
+
store.insert(makeEvent({ level: "error" }));
|
|
429
|
+
store.insert(makeEvent({ level: "info" }));
|
|
430
|
+
|
|
431
|
+
const events = store.getTimeline({
|
|
432
|
+
since: "2020-01-01T00:00:00Z",
|
|
433
|
+
level: "error",
|
|
434
|
+
});
|
|
435
|
+
expect(events).toHaveLength(1);
|
|
436
|
+
expect(events[0]?.level).toBe("error");
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// === getToolStats ===
|
|
441
|
+
|
|
442
|
+
describe("getToolStats", () => {
|
|
443
|
+
test("aggregates tool usage counts", () => {
|
|
444
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
445
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
446
|
+
store.insert(makeEvent({ toolName: "Bash", eventType: "tool_start" }));
|
|
447
|
+
|
|
448
|
+
const stats = store.getToolStats();
|
|
449
|
+
expect(stats).toHaveLength(2);
|
|
450
|
+
|
|
451
|
+
const readStats = stats.find((s) => s.toolName === "Read");
|
|
452
|
+
const bashStats = stats.find((s) => s.toolName === "Bash");
|
|
453
|
+
|
|
454
|
+
expect(readStats?.count).toBe(2);
|
|
455
|
+
expect(bashStats?.count).toBe(1);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
test("computes average and max duration", () => {
|
|
459
|
+
store.insert(
|
|
460
|
+
makeEvent({
|
|
461
|
+
toolName: "Read",
|
|
462
|
+
eventType: "tool_start",
|
|
463
|
+
toolDurationMs: 100,
|
|
464
|
+
}),
|
|
465
|
+
);
|
|
466
|
+
store.insert(
|
|
467
|
+
makeEvent({
|
|
468
|
+
toolName: "Read",
|
|
469
|
+
eventType: "tool_start",
|
|
470
|
+
toolDurationMs: 300,
|
|
471
|
+
}),
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
const stats = store.getToolStats();
|
|
475
|
+
expect(stats).toHaveLength(1);
|
|
476
|
+
|
|
477
|
+
const readStats = stats[0] as ToolStats;
|
|
478
|
+
expect(readStats.toolName).toBe("Read");
|
|
479
|
+
expect(readStats.count).toBe(2);
|
|
480
|
+
expect(readStats.avgDurationMs).toBe(200);
|
|
481
|
+
expect(readStats.maxDurationMs).toBe(300);
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
test("returns stats ordered by count DESC", () => {
|
|
485
|
+
store.insert(makeEvent({ toolName: "Bash", eventType: "tool_start" }));
|
|
486
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
487
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
488
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
489
|
+
store.insert(makeEvent({ toolName: "Bash", eventType: "tool_start" }));
|
|
490
|
+
|
|
491
|
+
const stats = store.getToolStats();
|
|
492
|
+
expect(stats).toHaveLength(2);
|
|
493
|
+
expect(stats[0]?.toolName).toBe("Read"); // 3 uses
|
|
494
|
+
expect(stats[1]?.toolName).toBe("Bash"); // 2 uses
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("filters by agent name", () => {
|
|
498
|
+
store.insert(makeEvent({ agentName: "builder-1", toolName: "Read", eventType: "tool_start" }));
|
|
499
|
+
store.insert(makeEvent({ agentName: "scout-1", toolName: "Read", eventType: "tool_start" }));
|
|
500
|
+
store.insert(makeEvent({ agentName: "builder-1", toolName: "Bash", eventType: "tool_start" }));
|
|
501
|
+
|
|
502
|
+
const stats = store.getToolStats({ agentName: "builder-1" });
|
|
503
|
+
expect(stats).toHaveLength(2);
|
|
504
|
+
// Only builder-1's tools
|
|
505
|
+
const total = stats.reduce((sum, s) => sum + s.count, 0);
|
|
506
|
+
expect(total).toBe(2);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test("returns empty array when no tool events exist", () => {
|
|
510
|
+
store.insert(makeEvent({ toolName: null, eventType: "session_start" }));
|
|
511
|
+
const stats = store.getToolStats();
|
|
512
|
+
expect(stats).toEqual([]);
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("only counts tool_start events (not tool_end)", () => {
|
|
516
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_start" }));
|
|
517
|
+
store.insert(makeEvent({ toolName: "Read", eventType: "tool_end" }));
|
|
518
|
+
|
|
519
|
+
const stats = store.getToolStats();
|
|
520
|
+
expect(stats).toHaveLength(1);
|
|
521
|
+
expect(stats[0]?.count).toBe(1);
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
test("handles null toolDurationMs in averages", () => {
|
|
525
|
+
store.insert(
|
|
526
|
+
makeEvent({
|
|
527
|
+
toolName: "Read",
|
|
528
|
+
eventType: "tool_start",
|
|
529
|
+
toolDurationMs: null,
|
|
530
|
+
}),
|
|
531
|
+
);
|
|
532
|
+
store.insert(
|
|
533
|
+
makeEvent({
|
|
534
|
+
toolName: "Read",
|
|
535
|
+
eventType: "tool_start",
|
|
536
|
+
toolDurationMs: 200,
|
|
537
|
+
}),
|
|
538
|
+
);
|
|
539
|
+
|
|
540
|
+
const stats = store.getToolStats();
|
|
541
|
+
expect(stats).toHaveLength(1);
|
|
542
|
+
// AVG of (NULL, 200) -- SQLite AVG ignores NULL, so result is 200
|
|
543
|
+
expect(stats[0]?.avgDurationMs).toBe(200);
|
|
544
|
+
});
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// === purge ===
|
|
548
|
+
|
|
549
|
+
describe("purge", () => {
|
|
550
|
+
test("purge all deletes everything and returns count", () => {
|
|
551
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
552
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
553
|
+
store.insert(makeEvent({ agentName: "builder-2" }));
|
|
554
|
+
|
|
555
|
+
const count = store.purge({ all: true });
|
|
556
|
+
expect(count).toBe(3);
|
|
557
|
+
|
|
558
|
+
const remaining = store.getByAgent("builder-1");
|
|
559
|
+
expect(remaining).toEqual([]);
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
test("purge by agent name deletes only that agent's events", () => {
|
|
563
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
564
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
565
|
+
store.insert(makeEvent({ agentName: "builder-1", eventType: "tool_end" }));
|
|
566
|
+
|
|
567
|
+
const count = store.purge({ agentName: "builder-1" });
|
|
568
|
+
expect(count).toBe(2);
|
|
569
|
+
|
|
570
|
+
const remaining = store.getByAgent("scout-1");
|
|
571
|
+
expect(remaining).toHaveLength(1);
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
test("purge on empty DB returns 0", () => {
|
|
575
|
+
const count = store.purge({ all: true });
|
|
576
|
+
expect(count).toBe(0);
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
test("purge with no options returns 0 without deleting", () => {
|
|
580
|
+
store.insert(makeEvent());
|
|
581
|
+
const count = store.purge({});
|
|
582
|
+
expect(count).toBe(0);
|
|
583
|
+
|
|
584
|
+
const events = store.getByAgent("builder-1");
|
|
585
|
+
expect(events).toHaveLength(1);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("purge by olderThanMs deletes old events", () => {
|
|
589
|
+
// Insert an event (created_at is "now")
|
|
590
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
591
|
+
|
|
592
|
+
// Purging events older than 1 hour should delete nothing (events are fresh)
|
|
593
|
+
const count = store.purge({ olderThanMs: 3_600_000 });
|
|
594
|
+
expect(count).toBe(0);
|
|
595
|
+
|
|
596
|
+
const events = store.getByAgent("builder-1");
|
|
597
|
+
expect(events).toHaveLength(1);
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
test("purge combines agentName and olderThanMs", () => {
|
|
601
|
+
store.insert(makeEvent({ agentName: "builder-1" }));
|
|
602
|
+
store.insert(makeEvent({ agentName: "scout-1" }));
|
|
603
|
+
|
|
604
|
+
// Both agents' events are fresh, so nothing should be deleted
|
|
605
|
+
const count = store.purge({ agentName: "builder-1", olderThanMs: 3_600_000 });
|
|
606
|
+
expect(count).toBe(0);
|
|
607
|
+
});
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
// === close ===
|
|
611
|
+
|
|
612
|
+
describe("close", () => {
|
|
613
|
+
test("calling close does not throw", () => {
|
|
614
|
+
expect(() => store.close()).not.toThrow();
|
|
615
|
+
});
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
// === CHECK constraints ===
|
|
619
|
+
|
|
620
|
+
describe("CHECK constraints", () => {
|
|
621
|
+
test("accepts all valid level values", () => {
|
|
622
|
+
const levels: InsertEvent["level"][] = ["debug", "info", "warn", "error"];
|
|
623
|
+
for (const level of levels) {
|
|
624
|
+
const id = store.insert(makeEvent({ level }));
|
|
625
|
+
expect(id).toBeGreaterThan(0);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
test("rejects invalid level value", () => {
|
|
630
|
+
expect(() => store.insert(makeEvent({ level: "fatal" as InsertEvent["level"] }))).toThrow();
|
|
631
|
+
});
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
// === concurrent access ===
|
|
635
|
+
|
|
636
|
+
describe("concurrent access", () => {
|
|
637
|
+
test("second store instance can read events written by first", () => {
|
|
638
|
+
// Use a temp file for this test since :memory: databases are isolated
|
|
639
|
+
const { mkdtempSync } = require("node:fs");
|
|
640
|
+
const { tmpdir } = require("node:os");
|
|
641
|
+
const { join } = require("node:path");
|
|
642
|
+
const { rmSync } = require("node:fs");
|
|
643
|
+
|
|
644
|
+
const tempDir = mkdtempSync(join(tmpdir(), "overstory-events-test-"));
|
|
645
|
+
const dbPath = join(tempDir, "events.db");
|
|
646
|
+
|
|
647
|
+
const store1 = createEventStore(dbPath);
|
|
648
|
+
const store2 = createEventStore(dbPath);
|
|
649
|
+
|
|
650
|
+
store1.insert(makeEvent({ agentName: "builder-1" }));
|
|
651
|
+
|
|
652
|
+
const events = store2.getByAgent("builder-1");
|
|
653
|
+
expect(events).toHaveLength(1);
|
|
654
|
+
expect(events[0]?.agentName).toBe("builder-1");
|
|
655
|
+
|
|
656
|
+
store1.close();
|
|
657
|
+
store2.close();
|
|
658
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
659
|
+
});
|
|
660
|
+
});
|