@polpo-ai/drizzle 0.1.0
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/dist/__tests__/stores.test.d.ts +2 -0
- package/dist/__tests__/stores.test.d.ts.map +1 -0
- package/dist/__tests__/stores.test.js +922 -0
- package/dist/__tests__/stores.test.js.map +1 -0
- package/dist/index.d.ts +4811 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +129 -0
- package/dist/index.js.map +1 -0
- package/dist/migrate.d.ts +8 -0
- package/dist/migrate.d.ts.map +1 -0
- package/dist/migrate.js +214 -0
- package/dist/migrate.js.map +1 -0
- package/dist/schema/approvals.d.ts +413 -0
- package/dist/schema/approvals.d.ts.map +1 -0
- package/dist/schema/approvals.js +37 -0
- package/dist/schema/approvals.js.map +1 -0
- package/dist/schema/index.d.ts +17 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +19 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/logs.d.ts +281 -0
- package/dist/schema/logs.d.ts.map +1 -0
- package/dist/schema/logs.js +33 -0
- package/dist/schema/logs.js.map +1 -0
- package/dist/schema/memory.d.ts +87 -0
- package/dist/schema/memory.d.ts.map +1 -0
- package/dist/schema/memory.js +13 -0
- package/dist/schema/memory.js.map +1 -0
- package/dist/schema/notifications.d.ts +521 -0
- package/dist/schema/notifications.d.ts.map +1 -0
- package/dist/schema/notifications.js +47 -0
- package/dist/schema/notifications.js.map +1 -0
- package/dist/schema/peers.d.ts +743 -0
- package/dist/schema/peers.d.ts.map +1 -0
- package/dist/schema/peers.js +71 -0
- package/dist/schema/peers.js.map +1 -0
- package/dist/schema/runs.d.ts +483 -0
- package/dist/schema/runs.d.ts.map +1 -0
- package/dist/schema/runs.js +41 -0
- package/dist/schema/runs.js.map +1 -0
- package/dist/schema/sessions.d.ts +389 -0
- package/dist/schema/sessions.d.ts.map +1 -0
- package/dist/schema/sessions.js +37 -0
- package/dist/schema/sessions.js.map +1 -0
- package/dist/schema/tasks.d.ts +1843 -0
- package/dist/schema/tasks.d.ts.map +1 -0
- package/dist/schema/tasks.js +135 -0
- package/dist/schema/tasks.js.map +1 -0
- package/dist/stores/approval-store.d.ts +19 -0
- package/dist/stores/approval-store.d.ts.map +1 -0
- package/dist/stores/approval-store.js +77 -0
- package/dist/stores/approval-store.js.map +1 -0
- package/dist/stores/checkpoint-store.d.ts +14 -0
- package/dist/stores/checkpoint-store.d.ts.map +1 -0
- package/dist/stores/checkpoint-store.js +44 -0
- package/dist/stores/checkpoint-store.js.map +1 -0
- package/dist/stores/config-store.d.ts +15 -0
- package/dist/stores/config-store.d.ts.map +1 -0
- package/dist/stores/config-store.js +31 -0
- package/dist/stores/config-store.js.map +1 -0
- package/dist/stores/delay-store.d.ts +14 -0
- package/dist/stores/delay-store.d.ts.map +1 -0
- package/dist/stores/delay-store.js +42 -0
- package/dist/stores/delay-store.js.map +1 -0
- package/dist/stores/index.d.ts +13 -0
- package/dist/stores/index.d.ts.map +1 -0
- package/dist/stores/index.js +12 -0
- package/dist/stores/index.js.map +1 -0
- package/dist/stores/log-store.d.ts +20 -0
- package/dist/stores/log-store.d.ts.map +1 -0
- package/dist/stores/log-store.js +87 -0
- package/dist/stores/log-store.js.map +1 -0
- package/dist/stores/memory-store.d.ts +14 -0
- package/dist/stores/memory-store.d.ts.map +1 -0
- package/dist/stores/memory-store.js +39 -0
- package/dist/stores/memory-store.js.map +1 -0
- package/dist/stores/notification-store.d.ts +20 -0
- package/dist/stores/notification-store.d.ts.map +1 -0
- package/dist/stores/notification-store.js +111 -0
- package/dist/stores/notification-store.js.map +1 -0
- package/dist/stores/peer-store.d.ts +40 -0
- package/dist/stores/peer-store.d.ts.map +1 -0
- package/dist/stores/peer-store.js +203 -0
- package/dist/stores/peer-store.js.map +1 -0
- package/dist/stores/run-store.d.ts +23 -0
- package/dist/stores/run-store.d.ts.map +1 -0
- package/dist/stores/run-store.js +120 -0
- package/dist/stores/run-store.js.map +1 -0
- package/dist/stores/session-store.d.ts +26 -0
- package/dist/stores/session-store.d.ts.map +1 -0
- package/dist/stores/session-store.js +166 -0
- package/dist/stores/session-store.js.map +1 -0
- package/dist/stores/task-store.d.ts +42 -0
- package/dist/stores/task-store.d.ts.map +1 -0
- package/dist/stores/task-store.js +387 -0
- package/dist/stores/task-store.js.map +1 -0
- package/dist/utils.d.ts +11 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/package.json +74 -0
|
@@ -0,0 +1,922 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @polpo-ai/drizzle — SQLite in-memory tests for all 11 Drizzle stores.
|
|
3
|
+
*
|
|
4
|
+
* Uses better-sqlite3 :memory: — no PG required.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
7
|
+
import Database from "better-sqlite3";
|
|
8
|
+
import { drizzle } from "drizzle-orm/better-sqlite3";
|
|
9
|
+
import { createSqliteStores } from "../index.js";
|
|
10
|
+
// ── Test helpers ─────────────────────────────────────────────────────
|
|
11
|
+
let sqlite;
|
|
12
|
+
let db;
|
|
13
|
+
let stores;
|
|
14
|
+
/** Create all SQLite tables via raw SQL (mirrors what ensurePgSchema does for PG). */
|
|
15
|
+
function createTables(raw) {
|
|
16
|
+
raw.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
title TEXT NOT NULL,
|
|
20
|
+
description TEXT NOT NULL,
|
|
21
|
+
assign_to TEXT NOT NULL,
|
|
22
|
+
"group" TEXT,
|
|
23
|
+
mission_id TEXT,
|
|
24
|
+
depends_on TEXT NOT NULL DEFAULT '[]',
|
|
25
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
26
|
+
retries INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
max_retries INTEGER NOT NULL DEFAULT 2,
|
|
28
|
+
max_duration INTEGER,
|
|
29
|
+
retry_policy TEXT,
|
|
30
|
+
expectations TEXT NOT NULL DEFAULT '[]',
|
|
31
|
+
metrics TEXT NOT NULL DEFAULT '[]',
|
|
32
|
+
result TEXT,
|
|
33
|
+
phase TEXT,
|
|
34
|
+
fix_attempts INTEGER NOT NULL DEFAULT 0,
|
|
35
|
+
resolution_attempts INTEGER NOT NULL DEFAULT 0,
|
|
36
|
+
original_description TEXT,
|
|
37
|
+
session_id TEXT,
|
|
38
|
+
notifications TEXT,
|
|
39
|
+
outcomes TEXT,
|
|
40
|
+
expected_outcomes TEXT,
|
|
41
|
+
deadline TEXT,
|
|
42
|
+
priority TEXT,
|
|
43
|
+
side_effects INTEGER,
|
|
44
|
+
revision_count INTEGER,
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
updated_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
CREATE TABLE IF NOT EXISTS missions (
|
|
49
|
+
id TEXT PRIMARY KEY,
|
|
50
|
+
name TEXT NOT NULL UNIQUE,
|
|
51
|
+
data TEXT NOT NULL,
|
|
52
|
+
prompt TEXT,
|
|
53
|
+
status TEXT NOT NULL DEFAULT 'draft',
|
|
54
|
+
schedule TEXT,
|
|
55
|
+
end_date TEXT,
|
|
56
|
+
quality_threshold TEXT,
|
|
57
|
+
deadline TEXT,
|
|
58
|
+
notifications TEXT,
|
|
59
|
+
execution_count INTEGER NOT NULL DEFAULT 0,
|
|
60
|
+
created_at TEXT NOT NULL,
|
|
61
|
+
updated_at TEXT NOT NULL
|
|
62
|
+
);
|
|
63
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
64
|
+
key TEXT PRIMARY KEY,
|
|
65
|
+
value TEXT NOT NULL
|
|
66
|
+
);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS processes (
|
|
68
|
+
agent_name TEXT NOT NULL,
|
|
69
|
+
pid INTEGER NOT NULL,
|
|
70
|
+
task_id TEXT NOT NULL,
|
|
71
|
+
started_at TEXT NOT NULL,
|
|
72
|
+
alive INTEGER NOT NULL DEFAULT 1,
|
|
73
|
+
activity TEXT NOT NULL DEFAULT '{}'
|
|
74
|
+
);
|
|
75
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
76
|
+
id TEXT PRIMARY KEY,
|
|
77
|
+
task_id TEXT NOT NULL,
|
|
78
|
+
pid INTEGER NOT NULL DEFAULT 0,
|
|
79
|
+
agent_name TEXT NOT NULL,
|
|
80
|
+
adapter_type TEXT NOT NULL,
|
|
81
|
+
session_id TEXT,
|
|
82
|
+
status TEXT NOT NULL DEFAULT 'running',
|
|
83
|
+
started_at TEXT NOT NULL,
|
|
84
|
+
updated_at TEXT NOT NULL,
|
|
85
|
+
activity TEXT NOT NULL DEFAULT '{}',
|
|
86
|
+
result TEXT,
|
|
87
|
+
outcomes TEXT,
|
|
88
|
+
config_path TEXT NOT NULL
|
|
89
|
+
);
|
|
90
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
91
|
+
id TEXT PRIMARY KEY,
|
|
92
|
+
title TEXT,
|
|
93
|
+
created_at TEXT NOT NULL,
|
|
94
|
+
updated_at TEXT NOT NULL
|
|
95
|
+
);
|
|
96
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
97
|
+
id TEXT PRIMARY KEY,
|
|
98
|
+
session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
|
|
99
|
+
role TEXT NOT NULL,
|
|
100
|
+
content TEXT NOT NULL,
|
|
101
|
+
ts TEXT NOT NULL,
|
|
102
|
+
tool_calls TEXT
|
|
103
|
+
);
|
|
104
|
+
CREATE TABLE IF NOT EXISTS notifications (
|
|
105
|
+
id TEXT PRIMARY KEY,
|
|
106
|
+
timestamp TEXT NOT NULL,
|
|
107
|
+
rule_id TEXT NOT NULL,
|
|
108
|
+
rule_name TEXT NOT NULL,
|
|
109
|
+
channel TEXT NOT NULL,
|
|
110
|
+
channel_type TEXT NOT NULL,
|
|
111
|
+
status TEXT NOT NULL,
|
|
112
|
+
error TEXT,
|
|
113
|
+
title TEXT NOT NULL,
|
|
114
|
+
body TEXT NOT NULL,
|
|
115
|
+
severity TEXT NOT NULL,
|
|
116
|
+
source_event TEXT NOT NULL,
|
|
117
|
+
attachment_count INTEGER NOT NULL DEFAULT 0,
|
|
118
|
+
attachment_types TEXT
|
|
119
|
+
);
|
|
120
|
+
CREATE TABLE IF NOT EXISTS log_sessions (
|
|
121
|
+
id TEXT PRIMARY KEY,
|
|
122
|
+
started_at TEXT NOT NULL
|
|
123
|
+
);
|
|
124
|
+
CREATE TABLE IF NOT EXISTS log_entries (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
session_id TEXT NOT NULL REFERENCES log_sessions(id) ON DELETE CASCADE,
|
|
127
|
+
ts TEXT NOT NULL,
|
|
128
|
+
event TEXT NOT NULL,
|
|
129
|
+
data TEXT
|
|
130
|
+
);
|
|
131
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
132
|
+
id TEXT PRIMARY KEY,
|
|
133
|
+
gate_id TEXT NOT NULL,
|
|
134
|
+
gate_name TEXT NOT NULL,
|
|
135
|
+
task_id TEXT,
|
|
136
|
+
mission_id TEXT,
|
|
137
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
138
|
+
payload TEXT,
|
|
139
|
+
requested_at TEXT NOT NULL,
|
|
140
|
+
resolved_at TEXT,
|
|
141
|
+
resolved_by TEXT,
|
|
142
|
+
note TEXT
|
|
143
|
+
);
|
|
144
|
+
CREATE TABLE IF NOT EXISTS memory (
|
|
145
|
+
key TEXT PRIMARY KEY,
|
|
146
|
+
content TEXT NOT NULL DEFAULT ''
|
|
147
|
+
);
|
|
148
|
+
CREATE TABLE IF NOT EXISTS peers (
|
|
149
|
+
id TEXT PRIMARY KEY,
|
|
150
|
+
channel TEXT NOT NULL,
|
|
151
|
+
external_id TEXT NOT NULL,
|
|
152
|
+
display_name TEXT,
|
|
153
|
+
first_seen_at TEXT NOT NULL,
|
|
154
|
+
last_seen_at TEXT NOT NULL,
|
|
155
|
+
linked_to TEXT
|
|
156
|
+
);
|
|
157
|
+
CREATE TABLE IF NOT EXISTS peer_allowlist (
|
|
158
|
+
peer_id TEXT PRIMARY KEY
|
|
159
|
+
);
|
|
160
|
+
CREATE TABLE IF NOT EXISTS pairing_requests (
|
|
161
|
+
id TEXT PRIMARY KEY,
|
|
162
|
+
peer_id TEXT NOT NULL,
|
|
163
|
+
channel TEXT NOT NULL,
|
|
164
|
+
external_id TEXT NOT NULL,
|
|
165
|
+
display_name TEXT,
|
|
166
|
+
code TEXT NOT NULL UNIQUE,
|
|
167
|
+
created_at TEXT NOT NULL,
|
|
168
|
+
expires_at TEXT NOT NULL,
|
|
169
|
+
resolved INTEGER NOT NULL DEFAULT 0
|
|
170
|
+
);
|
|
171
|
+
CREATE TABLE IF NOT EXISTS peer_sessions (
|
|
172
|
+
peer_id TEXT PRIMARY KEY,
|
|
173
|
+
session_id TEXT NOT NULL
|
|
174
|
+
);
|
|
175
|
+
PRAGMA foreign_keys = ON;
|
|
176
|
+
`);
|
|
177
|
+
}
|
|
178
|
+
beforeEach(() => {
|
|
179
|
+
sqlite = new Database(":memory:");
|
|
180
|
+
createTables(sqlite);
|
|
181
|
+
db = drizzle(sqlite);
|
|
182
|
+
stores = createSqliteStores(db);
|
|
183
|
+
});
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
sqlite.close();
|
|
186
|
+
});
|
|
187
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
188
|
+
// TaskStore
|
|
189
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
190
|
+
describe("DrizzleTaskStore", () => {
|
|
191
|
+
it("addTask + getTask round-trip", async () => {
|
|
192
|
+
const task = await stores.taskStore.addTask({
|
|
193
|
+
title: "Fix bug",
|
|
194
|
+
description: "Fix the login bug",
|
|
195
|
+
assignTo: "claude",
|
|
196
|
+
dependsOn: [],
|
|
197
|
+
maxRetries: 3,
|
|
198
|
+
expectations: [{ type: "llm_review", criteria: "Login works" }],
|
|
199
|
+
metrics: [],
|
|
200
|
+
});
|
|
201
|
+
expect(task.id).toBeDefined();
|
|
202
|
+
expect(task.status).toBe("pending");
|
|
203
|
+
expect(task.retries).toBe(0);
|
|
204
|
+
expect(task.title).toBe("Fix bug");
|
|
205
|
+
const fetched = await stores.taskStore.getTask(task.id);
|
|
206
|
+
expect(fetched).toBeDefined();
|
|
207
|
+
expect(fetched.title).toBe("Fix bug");
|
|
208
|
+
expect(fetched.expectations).toEqual([{ type: "llm_review", criteria: "Login works" }]);
|
|
209
|
+
});
|
|
210
|
+
it("getAllTasks returns ordered by createdAt", async () => {
|
|
211
|
+
await stores.taskStore.addTask({
|
|
212
|
+
title: "A", description: "first", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
213
|
+
});
|
|
214
|
+
await stores.taskStore.addTask({
|
|
215
|
+
title: "B", description: "second", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
216
|
+
});
|
|
217
|
+
const all = await stores.taskStore.getAllTasks();
|
|
218
|
+
expect(all).toHaveLength(2);
|
|
219
|
+
expect(all[0].title).toBe("A");
|
|
220
|
+
expect(all[1].title).toBe("B");
|
|
221
|
+
});
|
|
222
|
+
it("updateTask merges fields", async () => {
|
|
223
|
+
const task = await stores.taskStore.addTask({
|
|
224
|
+
title: "Original", description: "desc", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
225
|
+
});
|
|
226
|
+
const updated = await stores.taskStore.updateTask(task.id, { title: "Updated" });
|
|
227
|
+
expect(updated.title).toBe("Updated");
|
|
228
|
+
expect(updated.description).toBe("desc"); // unchanged
|
|
229
|
+
const fetched = await stores.taskStore.getTask(task.id);
|
|
230
|
+
expect(fetched.title).toBe("Updated");
|
|
231
|
+
});
|
|
232
|
+
it("removeTask deletes by ID", async () => {
|
|
233
|
+
const task = await stores.taskStore.addTask({
|
|
234
|
+
title: "Delete me", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
235
|
+
});
|
|
236
|
+
const removed = await stores.taskStore.removeTask(task.id);
|
|
237
|
+
expect(removed).toBe(true);
|
|
238
|
+
const fetched = await stores.taskStore.getTask(task.id);
|
|
239
|
+
expect(fetched).toBeUndefined();
|
|
240
|
+
});
|
|
241
|
+
it("removeTasks with filter", async () => {
|
|
242
|
+
await stores.taskStore.addTask({
|
|
243
|
+
title: "Keep", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
244
|
+
});
|
|
245
|
+
await stores.taskStore.addTask({
|
|
246
|
+
title: "Remove", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [], group: "old",
|
|
247
|
+
});
|
|
248
|
+
const count = await stores.taskStore.removeTasks((t) => t.group === "old");
|
|
249
|
+
expect(count).toBe(1);
|
|
250
|
+
const all = await stores.taskStore.getAllTasks();
|
|
251
|
+
expect(all).toHaveLength(1);
|
|
252
|
+
expect(all[0].title).toBe("Keep");
|
|
253
|
+
});
|
|
254
|
+
it("transition validates state machine", async () => {
|
|
255
|
+
const task = await stores.taskStore.addTask({
|
|
256
|
+
title: "T", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
257
|
+
});
|
|
258
|
+
// pending → assigned is valid
|
|
259
|
+
const assigned = await stores.taskStore.transition(task.id, "assigned");
|
|
260
|
+
expect(assigned.status).toBe("assigned");
|
|
261
|
+
// assigned → pending is invalid
|
|
262
|
+
await expect(stores.taskStore.transition(task.id, "pending")).rejects.toThrow();
|
|
263
|
+
});
|
|
264
|
+
it("transition increments retries on failed→pending", async () => {
|
|
265
|
+
const task = await stores.taskStore.addTask({
|
|
266
|
+
title: "T", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 3, expectations: [], metrics: [],
|
|
267
|
+
});
|
|
268
|
+
await stores.taskStore.transition(task.id, "assigned");
|
|
269
|
+
await stores.taskStore.transition(task.id, "in_progress");
|
|
270
|
+
await stores.taskStore.transition(task.id, "failed");
|
|
271
|
+
const retried = await stores.taskStore.transition(task.id, "pending");
|
|
272
|
+
expect(retried.retries).toBe(1);
|
|
273
|
+
});
|
|
274
|
+
it("unsafeSetStatus bypasses state machine", async () => {
|
|
275
|
+
const task = await stores.taskStore.addTask({
|
|
276
|
+
title: "T", description: "d", assignTo: "claude", dependsOn: [], maxRetries: 2, expectations: [], metrics: [],
|
|
277
|
+
});
|
|
278
|
+
// pending → done is not a valid transition, but unsafeSetStatus allows it
|
|
279
|
+
const result = await stores.taskStore.unsafeSetStatus(task.id, "done", "admin override");
|
|
280
|
+
expect(result.status).toBe("done");
|
|
281
|
+
});
|
|
282
|
+
// ── Missions ────────────────────────────────────────────────────────
|
|
283
|
+
it("saveMission + getMission round-trip", async () => {
|
|
284
|
+
const mission = await stores.taskStore.saveMission({
|
|
285
|
+
name: "mission-1",
|
|
286
|
+
data: '{"tasks":[]}',
|
|
287
|
+
status: "draft",
|
|
288
|
+
});
|
|
289
|
+
expect(mission.id).toBeDefined();
|
|
290
|
+
expect(mission.name).toBe("mission-1");
|
|
291
|
+
const fetched = await stores.taskStore.getMission(mission.id);
|
|
292
|
+
expect(fetched).toBeDefined();
|
|
293
|
+
expect(fetched.name).toBe("mission-1");
|
|
294
|
+
});
|
|
295
|
+
it("getMissionByName finds by name", async () => {
|
|
296
|
+
await stores.taskStore.saveMission({ name: "deploy-v2", data: "{}", status: "draft" });
|
|
297
|
+
const found = await stores.taskStore.getMissionByName("deploy-v2");
|
|
298
|
+
expect(found).toBeDefined();
|
|
299
|
+
expect(found.name).toBe("deploy-v2");
|
|
300
|
+
});
|
|
301
|
+
it("updateMission merges fields", async () => {
|
|
302
|
+
const m = await stores.taskStore.saveMission({ name: "m-1", data: "{}", status: "draft" });
|
|
303
|
+
const updated = await stores.taskStore.updateMission(m.id, { status: "active" });
|
|
304
|
+
expect(updated.status).toBe("active");
|
|
305
|
+
expect(updated.name).toBe("m-1");
|
|
306
|
+
});
|
|
307
|
+
it("deleteMission removes", async () => {
|
|
308
|
+
const m = await stores.taskStore.saveMission({ name: "m-del", data: "{}", status: "draft" });
|
|
309
|
+
const ok = await stores.taskStore.deleteMission(m.id);
|
|
310
|
+
expect(ok).toBe(true);
|
|
311
|
+
const fetched = await stores.taskStore.getMission(m.id);
|
|
312
|
+
expect(fetched).toBeUndefined();
|
|
313
|
+
});
|
|
314
|
+
it("nextMissionName increments", async () => {
|
|
315
|
+
expect(await stores.taskStore.nextMissionName()).toBe("mission-1");
|
|
316
|
+
await stores.taskStore.saveMission({ name: "mission-1", data: "{}", status: "draft" });
|
|
317
|
+
expect(await stores.taskStore.nextMissionName()).toBe("mission-2");
|
|
318
|
+
await stores.taskStore.saveMission({ name: "mission-5", data: "{}", status: "draft" });
|
|
319
|
+
expect(await stores.taskStore.nextMissionName()).toBe("mission-6");
|
|
320
|
+
});
|
|
321
|
+
// ── State ────────────────────────────────────────────────────────────
|
|
322
|
+
it("setState + getState round-trip", async () => {
|
|
323
|
+
await stores.taskStore.setState({
|
|
324
|
+
project: "test-project",
|
|
325
|
+
teams: [{ name: "alpha", agents: [{ name: "claude" }] }],
|
|
326
|
+
startedAt: "2025-01-01T00:00:00Z",
|
|
327
|
+
});
|
|
328
|
+
const state = await stores.taskStore.getState();
|
|
329
|
+
expect(state.project).toBe("test-project");
|
|
330
|
+
expect(state.teams).toHaveLength(1);
|
|
331
|
+
expect(state.teams[0].name).toBe("alpha");
|
|
332
|
+
expect(state.startedAt).toBe("2025-01-01T00:00:00Z");
|
|
333
|
+
});
|
|
334
|
+
it("setState with processes", async () => {
|
|
335
|
+
await stores.taskStore.setState({
|
|
336
|
+
project: "p",
|
|
337
|
+
processes: [{
|
|
338
|
+
agentName: "claude",
|
|
339
|
+
pid: 1234,
|
|
340
|
+
taskId: "t1",
|
|
341
|
+
startedAt: "2025-01-01T00:00:00Z",
|
|
342
|
+
alive: true,
|
|
343
|
+
activity: { filesCreated: [], filesEdited: [], toolCalls: 5, totalTokens: 100, lastUpdate: "now" },
|
|
344
|
+
}],
|
|
345
|
+
});
|
|
346
|
+
const state = await stores.taskStore.getState();
|
|
347
|
+
expect(state.processes).toHaveLength(1);
|
|
348
|
+
expect(state.processes[0].pid).toBe(1234);
|
|
349
|
+
expect(state.processes[0].alive).toBe(true);
|
|
350
|
+
expect(state.processes[0].activity.toolCalls).toBe(5);
|
|
351
|
+
});
|
|
352
|
+
});
|
|
353
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
354
|
+
// RunStore
|
|
355
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
356
|
+
describe("DrizzleRunStore", () => {
|
|
357
|
+
const now = new Date().toISOString();
|
|
358
|
+
const makeRun = (id, taskId, status = "running") => ({
|
|
359
|
+
id,
|
|
360
|
+
taskId,
|
|
361
|
+
pid: 0,
|
|
362
|
+
agentName: "claude",
|
|
363
|
+
sessionId: undefined,
|
|
364
|
+
status,
|
|
365
|
+
startedAt: now,
|
|
366
|
+
updatedAt: now,
|
|
367
|
+
activity: { filesCreated: [], filesEdited: [], toolCalls: 0, totalTokens: 0, lastUpdate: "" },
|
|
368
|
+
result: undefined,
|
|
369
|
+
outcomes: undefined,
|
|
370
|
+
configPath: "/tmp/config.json",
|
|
371
|
+
});
|
|
372
|
+
it("upsertRun + getRun round-trip", async () => {
|
|
373
|
+
const run = makeRun("r1", "t1");
|
|
374
|
+
await stores.runStore.upsertRun(run);
|
|
375
|
+
const fetched = await stores.runStore.getRun("r1");
|
|
376
|
+
expect(fetched).toBeDefined();
|
|
377
|
+
expect(fetched.taskId).toBe("t1");
|
|
378
|
+
expect(fetched.status).toBe("running");
|
|
379
|
+
});
|
|
380
|
+
it("upsertRun updates on conflict", async () => {
|
|
381
|
+
await stores.runStore.upsertRun(makeRun("r1", "t1"));
|
|
382
|
+
await stores.runStore.upsertRun({ ...makeRun("r1", "t1"), status: "completed" });
|
|
383
|
+
const fetched = await stores.runStore.getRun("r1");
|
|
384
|
+
expect(fetched.status).toBe("completed");
|
|
385
|
+
});
|
|
386
|
+
it("getRunByTaskId returns latest", async () => {
|
|
387
|
+
await stores.runStore.upsertRun({ ...makeRun("r1", "t1"), startedAt: "2025-01-01T00:00:00Z" });
|
|
388
|
+
await stores.runStore.upsertRun({ ...makeRun("r2", "t1"), startedAt: "2025-01-02T00:00:00Z" });
|
|
389
|
+
const latest = await stores.runStore.getRunByTaskId("t1");
|
|
390
|
+
expect(latest).toBeDefined();
|
|
391
|
+
expect(latest.id).toBe("r2");
|
|
392
|
+
});
|
|
393
|
+
it("getActiveRuns returns only running", async () => {
|
|
394
|
+
await stores.runStore.upsertRun(makeRun("r1", "t1"));
|
|
395
|
+
await stores.runStore.upsertRun({ ...makeRun("r2", "t2"), status: "completed" });
|
|
396
|
+
const active = await stores.runStore.getActiveRuns();
|
|
397
|
+
expect(active).toHaveLength(1);
|
|
398
|
+
expect(active[0].id).toBe("r1");
|
|
399
|
+
});
|
|
400
|
+
it("getTerminalRuns returns completed/failed/killed", async () => {
|
|
401
|
+
await stores.runStore.upsertRun(makeRun("r1", "t1"));
|
|
402
|
+
await stores.runStore.upsertRun({ ...makeRun("r2", "t2"), status: "completed" });
|
|
403
|
+
await stores.runStore.upsertRun({ ...makeRun("r3", "t3"), status: "failed" });
|
|
404
|
+
const terminal = await stores.runStore.getTerminalRuns();
|
|
405
|
+
expect(terminal).toHaveLength(2);
|
|
406
|
+
});
|
|
407
|
+
it("completeRun guards against overwriting terminal status", async () => {
|
|
408
|
+
await stores.runStore.upsertRun({ ...makeRun("r1", "t1"), status: "completed" });
|
|
409
|
+
// Try to overwrite with failed — should be silently ignored
|
|
410
|
+
await stores.runStore.completeRun("r1", "failed", { exitCode: 1, stdout: "", stderr: "nope", duration: 100 });
|
|
411
|
+
const fetched = await stores.runStore.getRun("r1");
|
|
412
|
+
expect(fetched.status).toBe("completed"); // unchanged
|
|
413
|
+
});
|
|
414
|
+
it("updateActivity updates activity and sessionId", async () => {
|
|
415
|
+
await stores.runStore.upsertRun(makeRun("r1", "t1"));
|
|
416
|
+
await stores.runStore.updateActivity("r1", {
|
|
417
|
+
filesCreated: ["a.ts"], filesEdited: [], toolCalls: 10, totalTokens: 500, lastUpdate: "now", sessionId: "s1",
|
|
418
|
+
});
|
|
419
|
+
const fetched = await stores.runStore.getRun("r1");
|
|
420
|
+
expect(fetched.activity.toolCalls).toBe(10);
|
|
421
|
+
expect(fetched.sessionId).toBe("s1");
|
|
422
|
+
});
|
|
423
|
+
it("deleteRun removes the record", async () => {
|
|
424
|
+
await stores.runStore.upsertRun(makeRun("r1", "t1"));
|
|
425
|
+
await stores.runStore.deleteRun("r1");
|
|
426
|
+
expect(await stores.runStore.getRun("r1")).toBeUndefined();
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
430
|
+
// SessionStore
|
|
431
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
432
|
+
describe("DrizzleSessionStore", () => {
|
|
433
|
+
it("create + getSession", async () => {
|
|
434
|
+
const id = await stores.sessionStore.create("My Session");
|
|
435
|
+
const session = await stores.sessionStore.getSession(id);
|
|
436
|
+
expect(session).toBeDefined();
|
|
437
|
+
expect(session.title).toBe("My Session");
|
|
438
|
+
expect(session.messageCount).toBe(0);
|
|
439
|
+
});
|
|
440
|
+
it("addMessage + getMessages", async () => {
|
|
441
|
+
const sid = await stores.sessionStore.create();
|
|
442
|
+
await stores.sessionStore.addMessage(sid, "user", "Hello");
|
|
443
|
+
await stores.sessionStore.addMessage(sid, "assistant", "Hi there");
|
|
444
|
+
const msgs = await stores.sessionStore.getMessages(sid);
|
|
445
|
+
expect(msgs).toHaveLength(2);
|
|
446
|
+
expect(msgs[0].role).toBe("user");
|
|
447
|
+
expect(msgs[1].content).toBe("Hi there");
|
|
448
|
+
});
|
|
449
|
+
it("getRecentMessages returns last N", async () => {
|
|
450
|
+
const sid = await stores.sessionStore.create();
|
|
451
|
+
await stores.sessionStore.addMessage(sid, "user", "1");
|
|
452
|
+
await stores.sessionStore.addMessage(sid, "assistant", "2");
|
|
453
|
+
await stores.sessionStore.addMessage(sid, "user", "3");
|
|
454
|
+
const recent = await stores.sessionStore.getRecentMessages(sid, 2);
|
|
455
|
+
expect(recent).toHaveLength(2);
|
|
456
|
+
// Should contain the 2 most recent messages (exact order depends on timestamp granularity)
|
|
457
|
+
const contents = recent.map((m) => m.content).sort();
|
|
458
|
+
expect(contents).toEqual(["2", "3"]);
|
|
459
|
+
});
|
|
460
|
+
it("listSessions includes messageCount", async () => {
|
|
461
|
+
const s1 = await stores.sessionStore.create("S1");
|
|
462
|
+
await stores.sessionStore.addMessage(s1, "user", "msg1");
|
|
463
|
+
await stores.sessionStore.addMessage(s1, "assistant", "msg2");
|
|
464
|
+
await stores.sessionStore.create("S2");
|
|
465
|
+
const list = await stores.sessionStore.listSessions();
|
|
466
|
+
expect(list).toHaveLength(2);
|
|
467
|
+
const withMessages = list.find((s) => s.title === "S1");
|
|
468
|
+
expect(withMessages.messageCount).toBe(2);
|
|
469
|
+
});
|
|
470
|
+
it("renameSession updates title", async () => {
|
|
471
|
+
const id = await stores.sessionStore.create("Old");
|
|
472
|
+
const ok = await stores.sessionStore.renameSession(id, "New");
|
|
473
|
+
expect(ok).toBe(true);
|
|
474
|
+
const session = await stores.sessionStore.getSession(id);
|
|
475
|
+
expect(session.title).toBe("New");
|
|
476
|
+
});
|
|
477
|
+
it("deleteSession cascade-deletes messages", async () => {
|
|
478
|
+
const id = await stores.sessionStore.create("Del");
|
|
479
|
+
await stores.sessionStore.addMessage(id, "user", "msg");
|
|
480
|
+
const ok = await stores.sessionStore.deleteSession(id);
|
|
481
|
+
expect(ok).toBe(true);
|
|
482
|
+
expect(await stores.sessionStore.getSession(id)).toBeUndefined();
|
|
483
|
+
expect(await stores.sessionStore.getMessages(id)).toEqual([]);
|
|
484
|
+
});
|
|
485
|
+
it("prune keeps the N most recent sessions", async () => {
|
|
486
|
+
await stores.sessionStore.create("Old");
|
|
487
|
+
await stores.sessionStore.create("New");
|
|
488
|
+
const pruned = await stores.sessionStore.prune(1);
|
|
489
|
+
expect(pruned).toBe(1);
|
|
490
|
+
const list = await stores.sessionStore.listSessions();
|
|
491
|
+
expect(list).toHaveLength(1);
|
|
492
|
+
});
|
|
493
|
+
it("getLatestSession returns most recently updated", async () => {
|
|
494
|
+
await stores.sessionStore.create("First");
|
|
495
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
496
|
+
const id2 = await stores.sessionStore.create("Second");
|
|
497
|
+
const latest = await stores.sessionStore.getLatestSession();
|
|
498
|
+
expect(latest).toBeDefined();
|
|
499
|
+
expect(latest.id).toBe(id2);
|
|
500
|
+
});
|
|
501
|
+
it("updateMessage changes content", async () => {
|
|
502
|
+
const sid = await stores.sessionStore.create();
|
|
503
|
+
const msg = await stores.sessionStore.addMessage(sid, "assistant", "draft");
|
|
504
|
+
const ok = await stores.sessionStore.updateMessage(sid, msg.id, "final");
|
|
505
|
+
expect(ok).toBe(true);
|
|
506
|
+
const msgs = await stores.sessionStore.getMessages(sid);
|
|
507
|
+
expect(msgs[0].content).toBe("final");
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
511
|
+
// NotificationStore
|
|
512
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
513
|
+
describe("DrizzleNotificationStore", () => {
|
|
514
|
+
const makeNotification = (id, overrides = {}) => ({
|
|
515
|
+
id,
|
|
516
|
+
timestamp: new Date().toISOString(),
|
|
517
|
+
ruleId: "rule-1",
|
|
518
|
+
ruleName: "Test Rule",
|
|
519
|
+
channel: "slack-general",
|
|
520
|
+
channelType: "slack",
|
|
521
|
+
status: "sent",
|
|
522
|
+
title: "Alert",
|
|
523
|
+
body: "Something happened",
|
|
524
|
+
severity: "info",
|
|
525
|
+
sourceEvent: "task:done",
|
|
526
|
+
attachmentCount: 0,
|
|
527
|
+
...overrides,
|
|
528
|
+
});
|
|
529
|
+
it("append + list", async () => {
|
|
530
|
+
await stores.notificationStore.append(makeNotification("n1"));
|
|
531
|
+
await stores.notificationStore.append(makeNotification("n2"));
|
|
532
|
+
const all = await stores.notificationStore.list();
|
|
533
|
+
expect(all).toHaveLength(2);
|
|
534
|
+
});
|
|
535
|
+
it("listByChannel filters", async () => {
|
|
536
|
+
await stores.notificationStore.append(makeNotification("n1", { channel: "slack" }));
|
|
537
|
+
await stores.notificationStore.append(makeNotification("n2", { channel: "email" }));
|
|
538
|
+
const slack = await stores.notificationStore.listByChannel("slack");
|
|
539
|
+
expect(slack).toHaveLength(1);
|
|
540
|
+
expect(slack[0].channel).toBe("slack");
|
|
541
|
+
});
|
|
542
|
+
it("listByStatus filters", async () => {
|
|
543
|
+
await stores.notificationStore.append(makeNotification("n1", { status: "sent" }));
|
|
544
|
+
await stores.notificationStore.append(makeNotification("n2", { status: "failed" }));
|
|
545
|
+
const failed = await stores.notificationStore.listByStatus("failed");
|
|
546
|
+
expect(failed).toHaveLength(1);
|
|
547
|
+
});
|
|
548
|
+
it("count with and without status filter", async () => {
|
|
549
|
+
await stores.notificationStore.append(makeNotification("n1", { status: "sent" }));
|
|
550
|
+
await stores.notificationStore.append(makeNotification("n2", { status: "failed" }));
|
|
551
|
+
expect(await stores.notificationStore.count()).toBe(2);
|
|
552
|
+
expect(await stores.notificationStore.count("sent")).toBe(1);
|
|
553
|
+
});
|
|
554
|
+
it("prune keeps most recent", async () => {
|
|
555
|
+
await stores.notificationStore.append(makeNotification("n1", { timestamp: "2025-01-01T00:00:00Z" }));
|
|
556
|
+
await stores.notificationStore.append(makeNotification("n2", { timestamp: "2025-01-02T00:00:00Z" }));
|
|
557
|
+
await stores.notificationStore.append(makeNotification("n3", { timestamp: "2025-01-03T00:00:00Z" }));
|
|
558
|
+
const pruned = await stores.notificationStore.prune(1);
|
|
559
|
+
expect(pruned).toBe(2);
|
|
560
|
+
expect(await stores.notificationStore.count()).toBe(1);
|
|
561
|
+
});
|
|
562
|
+
});
|
|
563
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
564
|
+
// LogStore
|
|
565
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
566
|
+
describe("DrizzleLogStore", () => {
|
|
567
|
+
it("startSession + append + getSessionEntries", async () => {
|
|
568
|
+
const sid = await stores.logStore.startSession();
|
|
569
|
+
expect(sid).toBeDefined();
|
|
570
|
+
await stores.logStore.append({ ts: "2025-01-01T00:00:00Z", event: "task:started", data: { taskId: "t1" } });
|
|
571
|
+
await stores.logStore.append({ ts: "2025-01-01T00:01:00Z", event: "task:done", data: { taskId: "t1" } });
|
|
572
|
+
const entries = await stores.logStore.getSessionEntries(sid);
|
|
573
|
+
expect(entries).toHaveLength(2);
|
|
574
|
+
expect(entries[0].event).toBe("task:started");
|
|
575
|
+
expect(entries[1].event).toBe("task:done");
|
|
576
|
+
});
|
|
577
|
+
it("getSessionId returns current", async () => {
|
|
578
|
+
expect(await stores.logStore.getSessionId()).toBeUndefined();
|
|
579
|
+
const sid = await stores.logStore.startSession();
|
|
580
|
+
expect(await stores.logStore.getSessionId()).toBe(sid);
|
|
581
|
+
});
|
|
582
|
+
it("listSessions returns sessions with entry count", async () => {
|
|
583
|
+
await stores.logStore.startSession();
|
|
584
|
+
await stores.logStore.append({ ts: "2025-01-01T00:00:00Z", event: "e1", data: null });
|
|
585
|
+
await stores.logStore.append({ ts: "2025-01-01T00:01:00Z", event: "e2", data: null });
|
|
586
|
+
const sessions = await stores.logStore.listSessions();
|
|
587
|
+
expect(sessions).toHaveLength(1);
|
|
588
|
+
expect(sessions[0].entries).toBe(2);
|
|
589
|
+
});
|
|
590
|
+
it("auto-creates session on append if none started", async () => {
|
|
591
|
+
await stores.logStore.append({ ts: "2025-01-01T00:00:00Z", event: "auto", data: null });
|
|
592
|
+
const sid = await stores.logStore.getSessionId();
|
|
593
|
+
expect(sid).toBeDefined();
|
|
594
|
+
const entries = await stores.logStore.getSessionEntries(sid);
|
|
595
|
+
expect(entries).toHaveLength(1);
|
|
596
|
+
});
|
|
597
|
+
it("prune removes old sessions", async () => {
|
|
598
|
+
await stores.logStore.startSession();
|
|
599
|
+
await stores.logStore.append({ ts: "2025-01-01T00:00:00Z", event: "old", data: null });
|
|
600
|
+
await stores.logStore.startSession();
|
|
601
|
+
await stores.logStore.append({ ts: "2025-01-02T00:00:00Z", event: "new", data: null });
|
|
602
|
+
const pruned = await stores.logStore.prune(1);
|
|
603
|
+
expect(pruned).toBe(1);
|
|
604
|
+
expect(await stores.logStore.listSessions()).toHaveLength(1);
|
|
605
|
+
});
|
|
606
|
+
});
|
|
607
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
608
|
+
// ApprovalStore
|
|
609
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
610
|
+
describe("DrizzleApprovalStore", () => {
|
|
611
|
+
const makeApproval = (id, overrides = {}) => ({
|
|
612
|
+
id,
|
|
613
|
+
gateId: "gate-1",
|
|
614
|
+
gateName: "Deploy Gate",
|
|
615
|
+
taskId: "t1",
|
|
616
|
+
status: "pending",
|
|
617
|
+
payload: null,
|
|
618
|
+
requestedAt: new Date().toISOString(),
|
|
619
|
+
...overrides,
|
|
620
|
+
});
|
|
621
|
+
it("upsert + get round-trip", async () => {
|
|
622
|
+
const req = makeApproval("a1");
|
|
623
|
+
await stores.approvalStore.upsert(req);
|
|
624
|
+
const fetched = await stores.approvalStore.get("a1");
|
|
625
|
+
expect(fetched).toBeDefined();
|
|
626
|
+
expect(fetched.gateName).toBe("Deploy Gate");
|
|
627
|
+
expect(fetched.status).toBe("pending");
|
|
628
|
+
});
|
|
629
|
+
it("upsert updates on conflict", async () => {
|
|
630
|
+
await stores.approvalStore.upsert(makeApproval("a1"));
|
|
631
|
+
await stores.approvalStore.upsert(makeApproval("a1", {
|
|
632
|
+
status: "approved",
|
|
633
|
+
resolvedBy: "admin",
|
|
634
|
+
resolvedAt: new Date().toISOString(),
|
|
635
|
+
}));
|
|
636
|
+
const fetched = await stores.approvalStore.get("a1");
|
|
637
|
+
expect(fetched.status).toBe("approved");
|
|
638
|
+
expect(fetched.resolvedBy).toBe("admin");
|
|
639
|
+
});
|
|
640
|
+
it("list filters by status", async () => {
|
|
641
|
+
await stores.approvalStore.upsert(makeApproval("a1", { status: "pending" }));
|
|
642
|
+
await stores.approvalStore.upsert(makeApproval("a2", { status: "approved" }));
|
|
643
|
+
const pending = await stores.approvalStore.list("pending");
|
|
644
|
+
expect(pending).toHaveLength(1);
|
|
645
|
+
expect(pending[0].id).toBe("a1");
|
|
646
|
+
});
|
|
647
|
+
it("listByTask filters by taskId", async () => {
|
|
648
|
+
await stores.approvalStore.upsert(makeApproval("a1", { taskId: "t1" }));
|
|
649
|
+
await stores.approvalStore.upsert(makeApproval("a2", { taskId: "t2" }));
|
|
650
|
+
const t1 = await stores.approvalStore.listByTask("t1");
|
|
651
|
+
expect(t1).toHaveLength(1);
|
|
652
|
+
expect(t1[0].id).toBe("a1");
|
|
653
|
+
});
|
|
654
|
+
it("delete removes", async () => {
|
|
655
|
+
await stores.approvalStore.upsert(makeApproval("a1"));
|
|
656
|
+
const ok = await stores.approvalStore.delete("a1");
|
|
657
|
+
expect(ok).toBe(true);
|
|
658
|
+
expect(await stores.approvalStore.get("a1")).toBeUndefined();
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
662
|
+
// MemoryStore
|
|
663
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
664
|
+
describe("DrizzleMemoryStore", () => {
|
|
665
|
+
it("starts empty", async () => {
|
|
666
|
+
expect(await stores.memoryStore.exists()).toBe(false);
|
|
667
|
+
expect(await stores.memoryStore.get()).toBe("");
|
|
668
|
+
});
|
|
669
|
+
it("save + get round-trip", async () => {
|
|
670
|
+
await stores.memoryStore.save("Hello world");
|
|
671
|
+
expect(await stores.memoryStore.exists()).toBe(true);
|
|
672
|
+
expect(await stores.memoryStore.get()).toBe("Hello world");
|
|
673
|
+
});
|
|
674
|
+
it("save overwrites", async () => {
|
|
675
|
+
await stores.memoryStore.save("first");
|
|
676
|
+
await stores.memoryStore.save("second");
|
|
677
|
+
expect(await stores.memoryStore.get()).toBe("second");
|
|
678
|
+
});
|
|
679
|
+
it("append adds lines", async () => {
|
|
680
|
+
await stores.memoryStore.append("line 1");
|
|
681
|
+
await stores.memoryStore.append("line 2");
|
|
682
|
+
expect(await stores.memoryStore.get()).toBe("line 1\nline 2");
|
|
683
|
+
});
|
|
684
|
+
it("update replaces text", async () => {
|
|
685
|
+
await stores.memoryStore.save("foo bar baz");
|
|
686
|
+
const result = await stores.memoryStore.update("bar", "qux");
|
|
687
|
+
expect(result).toBe(true);
|
|
688
|
+
expect(await stores.memoryStore.get()).toBe("foo qux baz");
|
|
689
|
+
});
|
|
690
|
+
it("update returns error string when text not found", async () => {
|
|
691
|
+
await stores.memoryStore.save("hello");
|
|
692
|
+
const result = await stores.memoryStore.update("missing", "new");
|
|
693
|
+
expect(typeof result).toBe("string");
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
697
|
+
// CheckpointStore
|
|
698
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
699
|
+
describe("DrizzleCheckpointStore", () => {
|
|
700
|
+
it("load returns empty state when nothing saved", async () => {
|
|
701
|
+
const state = await stores.checkpointStore.load();
|
|
702
|
+
expect(state).toEqual({ definitions: {}, active: {}, resumed: [] });
|
|
703
|
+
});
|
|
704
|
+
it("save + load round-trip", async () => {
|
|
705
|
+
const state = {
|
|
706
|
+
definitions: { "mission-1": [{ name: "review", afterTasks: ["Task A"], blocksTasks: ["Task B"] }] },
|
|
707
|
+
active: { "mission-1:review": { checkpoint: { name: "review", afterTasks: ["Task A"], blocksTasks: ["Task B"] }, reachedAt: "2025-01-01T00:00:00Z" } },
|
|
708
|
+
resumed: [],
|
|
709
|
+
};
|
|
710
|
+
await stores.checkpointStore.save(state);
|
|
711
|
+
const loaded = await stores.checkpointStore.load();
|
|
712
|
+
expect(loaded.definitions["mission-1"]).toHaveLength(1);
|
|
713
|
+
expect(loaded.active["mission-1:review"]).toBeDefined();
|
|
714
|
+
});
|
|
715
|
+
it("removeGroup clears group-specific data", async () => {
|
|
716
|
+
const cp1 = { name: "cp1", afterTasks: ["A"], blocksTasks: ["B"] };
|
|
717
|
+
const cp2 = { name: "cp2", afterTasks: ["C"], blocksTasks: ["D"] };
|
|
718
|
+
const state = {
|
|
719
|
+
definitions: { "g1": [cp1], "g2": [cp2] },
|
|
720
|
+
active: {
|
|
721
|
+
"g1:cp1": { checkpoint: cp1, reachedAt: "now" },
|
|
722
|
+
"g2:cp2": { checkpoint: cp2, reachedAt: "now" },
|
|
723
|
+
},
|
|
724
|
+
resumed: ["g1:cp1", "g2:cp2"],
|
|
725
|
+
};
|
|
726
|
+
await stores.checkpointStore.save(state);
|
|
727
|
+
const next = await stores.checkpointStore.removeGroup(state, "g1");
|
|
728
|
+
expect(next.definitions["g1"]).toBeUndefined();
|
|
729
|
+
expect(next.definitions["g2"]).toBeDefined();
|
|
730
|
+
expect(next.active["g1:cp1"]).toBeUndefined();
|
|
731
|
+
expect(next.active["g2:cp2"]).toBeDefined();
|
|
732
|
+
expect(next.resumed).toEqual(["g2:cp2"]);
|
|
733
|
+
// Verify persisted
|
|
734
|
+
const reloaded = await stores.checkpointStore.load();
|
|
735
|
+
expect(reloaded.definitions["g1"]).toBeUndefined();
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
739
|
+
// DelayStore
|
|
740
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
741
|
+
describe("DrizzleDelayStore", () => {
|
|
742
|
+
it("load returns empty state when nothing saved", async () => {
|
|
743
|
+
const state = await stores.delayStore.load();
|
|
744
|
+
expect(state).toEqual({ definitions: {}, active: {}, expired: [] });
|
|
745
|
+
});
|
|
746
|
+
it("save + load round-trip", async () => {
|
|
747
|
+
const delay = { name: "cooldown", duration: "PT5M", afterTasks: ["Task A"], blocksTasks: ["Task B"] };
|
|
748
|
+
const state = {
|
|
749
|
+
definitions: { "mission-1": [delay] },
|
|
750
|
+
active: { "mission-1:cooldown": { delay, startedAt: "2025-01-01T00:00:00Z", expiresAt: "2025-01-01T00:05:00Z" } },
|
|
751
|
+
expired: [],
|
|
752
|
+
};
|
|
753
|
+
await stores.delayStore.save(state);
|
|
754
|
+
const loaded = await stores.delayStore.load();
|
|
755
|
+
expect(loaded.definitions["mission-1"]).toHaveLength(1);
|
|
756
|
+
expect(loaded.active["mission-1:cooldown"]).toBeDefined();
|
|
757
|
+
});
|
|
758
|
+
it("removeGroup clears group-specific data", async () => {
|
|
759
|
+
const d1 = { name: "d1", duration: "PT5M", afterTasks: ["A"], blocksTasks: ["B"] };
|
|
760
|
+
const d2 = { name: "d2", duration: "PT10M", afterTasks: ["C"], blocksTasks: ["D"] };
|
|
761
|
+
const state = {
|
|
762
|
+
definitions: { "g1": [d1], "g2": [d2] },
|
|
763
|
+
active: {
|
|
764
|
+
"g1:d1": { delay: d1, startedAt: "now", expiresAt: "later" },
|
|
765
|
+
"g2:d2": { delay: d2, startedAt: "now", expiresAt: "later" },
|
|
766
|
+
},
|
|
767
|
+
expired: ["g1:d1", "g2:d2"],
|
|
768
|
+
};
|
|
769
|
+
await stores.delayStore.save(state);
|
|
770
|
+
const next = await stores.delayStore.removeGroup(state, "g1");
|
|
771
|
+
expect(next.definitions["g1"]).toBeUndefined();
|
|
772
|
+
expect(next.active["g1:d1"]).toBeUndefined();
|
|
773
|
+
expect(next.expired).toEqual(["g2:d2"]);
|
|
774
|
+
});
|
|
775
|
+
});
|
|
776
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
777
|
+
// ConfigStore
|
|
778
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
779
|
+
describe("DrizzleConfigStore", () => {
|
|
780
|
+
it("exists returns false initially", async () => {
|
|
781
|
+
expect(await stores.configStore.exists()).toBe(false);
|
|
782
|
+
});
|
|
783
|
+
it("save + get round-trip", async () => {
|
|
784
|
+
const config = {
|
|
785
|
+
settings: { storage: "postgres", model: "claude-sonnet-4-20250514" },
|
|
786
|
+
};
|
|
787
|
+
await stores.configStore.save(config);
|
|
788
|
+
expect(await stores.configStore.exists()).toBe(true);
|
|
789
|
+
const loaded = await stores.configStore.get();
|
|
790
|
+
expect(loaded).toBeDefined();
|
|
791
|
+
expect(loaded.settings.storage).toBe("postgres");
|
|
792
|
+
});
|
|
793
|
+
it("save overwrites previous config", async () => {
|
|
794
|
+
await stores.configStore.save({ settings: { workDir: "/old" } });
|
|
795
|
+
await stores.configStore.save({ settings: { workDir: "/new" } });
|
|
796
|
+
const loaded = await stores.configStore.get();
|
|
797
|
+
expect(loaded.settings.workDir).toBe("/new");
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
801
|
+
// PeerStore
|
|
802
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
803
|
+
describe("DrizzlePeerStore", () => {
|
|
804
|
+
it("upsertPeer + getPeer round-trip", async () => {
|
|
805
|
+
const peer = await stores.peerStore.upsertPeer({
|
|
806
|
+
channel: "slack",
|
|
807
|
+
externalId: "U123",
|
|
808
|
+
displayName: "Alice",
|
|
809
|
+
lastSeenAt: new Date().toISOString(),
|
|
810
|
+
});
|
|
811
|
+
expect(peer.id).toBeDefined();
|
|
812
|
+
expect(peer.displayName).toBe("Alice");
|
|
813
|
+
const fetched = await stores.peerStore.getPeer(peer.id);
|
|
814
|
+
expect(fetched).toBeDefined();
|
|
815
|
+
expect(fetched.externalId).toBe("U123");
|
|
816
|
+
});
|
|
817
|
+
it("upsertPeer updates on conflict", async () => {
|
|
818
|
+
const p1 = await stores.peerStore.upsertPeer({
|
|
819
|
+
channel: "slack", externalId: "U1", displayName: "Old", lastSeenAt: "2025-01-01",
|
|
820
|
+
});
|
|
821
|
+
await stores.peerStore.upsertPeer({
|
|
822
|
+
id: p1.id, channel: "slack", externalId: "U1", displayName: "New", lastSeenAt: "2025-01-02",
|
|
823
|
+
});
|
|
824
|
+
const fetched = await stores.peerStore.getPeer(p1.id);
|
|
825
|
+
expect(fetched.displayName).toBe("New");
|
|
826
|
+
expect(fetched.lastSeenAt).toBe("2025-01-02");
|
|
827
|
+
});
|
|
828
|
+
it("listPeers filters by channel", async () => {
|
|
829
|
+
await stores.peerStore.upsertPeer({ channel: "slack", externalId: "U1", lastSeenAt: "now" });
|
|
830
|
+
await stores.peerStore.upsertPeer({ channel: "discord", externalId: "D1", lastSeenAt: "now" });
|
|
831
|
+
const slack = await stores.peerStore.listPeers("slack");
|
|
832
|
+
expect(slack).toHaveLength(1);
|
|
833
|
+
expect(slack[0].channel).toBe("slack");
|
|
834
|
+
});
|
|
835
|
+
// ── Allowlist ──────────────────────────────────────────────────────
|
|
836
|
+
it("allowlist CRUD", async () => {
|
|
837
|
+
const peer = await stores.peerStore.upsertPeer({ channel: "slack", externalId: "U1", lastSeenAt: "now" });
|
|
838
|
+
expect(await stores.peerStore.isAllowed(peer.id)).toBe(false);
|
|
839
|
+
await stores.peerStore.addToAllowlist(peer.id);
|
|
840
|
+
expect(await stores.peerStore.isAllowed(peer.id)).toBe(true);
|
|
841
|
+
expect(await stores.peerStore.getAllowlist()).toEqual([peer.id]);
|
|
842
|
+
await stores.peerStore.removeFromAllowlist(peer.id);
|
|
843
|
+
expect(await stores.peerStore.isAllowed(peer.id)).toBe(false);
|
|
844
|
+
});
|
|
845
|
+
it("isAllowed returns true for open dmPolicy", async () => {
|
|
846
|
+
expect(await stores.peerStore.isAllowed("anyone", { dmPolicy: "open" })).toBe(true);
|
|
847
|
+
});
|
|
848
|
+
// ── Session mapping ───────────────────────────────────────────────
|
|
849
|
+
it("session mapping CRUD", async () => {
|
|
850
|
+
expect(await stores.peerStore.getSessionId("p1")).toBeUndefined();
|
|
851
|
+
await stores.peerStore.setSessionId("p1", "s1");
|
|
852
|
+
expect(await stores.peerStore.getSessionId("p1")).toBe("s1");
|
|
853
|
+
await stores.peerStore.setSessionId("p1", "s2"); // overwrite
|
|
854
|
+
expect(await stores.peerStore.getSessionId("p1")).toBe("s2");
|
|
855
|
+
await stores.peerStore.clearSession("p1");
|
|
856
|
+
expect(await stores.peerStore.getSessionId("p1")).toBeUndefined();
|
|
857
|
+
});
|
|
858
|
+
// ── Identity linking ─────────────────────────────────────────────
|
|
859
|
+
it("linkPeers + resolveCanonicalId", async () => {
|
|
860
|
+
const p1 = await stores.peerStore.upsertPeer({ channel: "slack", externalId: "U1", lastSeenAt: "now" });
|
|
861
|
+
const p2 = await stores.peerStore.upsertPeer({ channel: "discord", externalId: "D1", lastSeenAt: "now" });
|
|
862
|
+
await stores.peerStore.linkPeers(p2.id, p1.id);
|
|
863
|
+
expect(await stores.peerStore.resolveCanonicalId(p2.id)).toBe(p1.id);
|
|
864
|
+
expect(await stores.peerStore.resolveCanonicalId(p1.id)).toBe(p1.id); // no link = self
|
|
865
|
+
});
|
|
866
|
+
// ── Pairing ───────────────────────────────────────────────────────
|
|
867
|
+
it("createPairingRequest + resolvePairing", async () => {
|
|
868
|
+
const req = await stores.peerStore.createPairingRequest("slack", "U1", "Alice");
|
|
869
|
+
expect(req.code).toHaveLength(6);
|
|
870
|
+
expect(req.resolved).toBe(false);
|
|
871
|
+
const resolved = await stores.peerStore.resolvePairing(req.code);
|
|
872
|
+
expect(resolved).toBeDefined();
|
|
873
|
+
expect(resolved.resolved).toBe(true);
|
|
874
|
+
// Peer should now be in allowlist
|
|
875
|
+
expect(await stores.peerStore.isAllowed(req.peerId)).toBe(true);
|
|
876
|
+
});
|
|
877
|
+
it("resolvePairing returns undefined for invalid code", async () => {
|
|
878
|
+
expect(await stores.peerStore.resolvePairing("INVALID")).toBeUndefined();
|
|
879
|
+
});
|
|
880
|
+
it("getPendingPairing returns non-expired request", async () => {
|
|
881
|
+
const req = await stores.peerStore.createPairingRequest("slack", "U1");
|
|
882
|
+
const pending = await stores.peerStore.getPendingPairing(req.peerId);
|
|
883
|
+
expect(pending).toBeDefined();
|
|
884
|
+
expect(pending.code).toBe(req.code);
|
|
885
|
+
});
|
|
886
|
+
// ── Presence (in-memory) ──────────────────────────────────────────
|
|
887
|
+
it("presence is in-memory", async () => {
|
|
888
|
+
const peer = await stores.peerStore.upsertPeer({ channel: "slack", externalId: "U1", lastSeenAt: "now" });
|
|
889
|
+
await stores.peerStore.updatePresence(peer.id, "chatting");
|
|
890
|
+
const presence = await stores.peerStore.getPresence();
|
|
891
|
+
expect(presence).toHaveLength(1);
|
|
892
|
+
expect(presence[0].activity).toBe("chatting");
|
|
893
|
+
});
|
|
894
|
+
it("prunePresence removes old entries", async () => {
|
|
895
|
+
const peer = await stores.peerStore.upsertPeer({ channel: "slack", externalId: "U1", lastSeenAt: "now" });
|
|
896
|
+
await stores.peerStore.updatePresence(peer.id, "idle");
|
|
897
|
+
// Prune with 1ms TTL — the entry was just created so wait a tick
|
|
898
|
+
await new Promise((r) => setTimeout(r, 5));
|
|
899
|
+
const pruned = await stores.peerStore.prunePresence(1);
|
|
900
|
+
expect(pruned).toBe(1);
|
|
901
|
+
expect(await stores.peerStore.getPresence()).toEqual([]);
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
905
|
+
// Factory function
|
|
906
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
907
|
+
describe("createSqliteStores", () => {
|
|
908
|
+
it("returns all 11 stores", () => {
|
|
909
|
+
expect(stores.taskStore).toBeDefined();
|
|
910
|
+
expect(stores.runStore).toBeDefined();
|
|
911
|
+
expect(stores.sessionStore).toBeDefined();
|
|
912
|
+
expect(stores.notificationStore).toBeDefined();
|
|
913
|
+
expect(stores.logStore).toBeDefined();
|
|
914
|
+
expect(stores.approvalStore).toBeDefined();
|
|
915
|
+
expect(stores.memoryStore).toBeDefined();
|
|
916
|
+
expect(stores.peerStore).toBeDefined();
|
|
917
|
+
expect(stores.checkpointStore).toBeDefined();
|
|
918
|
+
expect(stores.delayStore).toBeDefined();
|
|
919
|
+
expect(stores.configStore).toBeDefined();
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
//# sourceMappingURL=stores.test.js.map
|