@lightupai/polaris 0.0.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.
@@ -0,0 +1,415 @@
1
+ import { describe, expect, test, beforeAll, afterAll, beforeEach } from "bun:test";
2
+ import { startServer } from "../src/service/server";
3
+ import { startDaemon } from "../src/daemon/daemon";
4
+ import type { Sql } from "../src/service/db";
5
+
6
+ const DATABASE_URL = process.env.DATABASE_URL ?? "postgres://polaris:polaris@localhost:5432/polaris";
7
+
8
+ let serviceUrl: string;
9
+ let daemonUrl: string;
10
+ let sql: Sql;
11
+ let stopService: () => Promise<void>;
12
+ let stopDaemon: () => void;
13
+
14
+ beforeAll(async () => {
15
+ const s = await startServer({ port: 0, databaseUrl: DATABASE_URL });
16
+ sql = s.sql;
17
+ stopService = s.stop;
18
+ serviceUrl = `http://localhost:${s.server.port}`;
19
+
20
+ process.env.POLARIS_SERVICE_URL = serviceUrl;
21
+ const d = startDaemon(0);
22
+ stopDaemon = d.stop;
23
+ daemonUrl = `http://127.0.0.1:${d.server.port}`;
24
+ });
25
+
26
+ afterAll(async () => {
27
+ stopDaemon();
28
+ await stopService();
29
+ });
30
+
31
+ beforeEach(async () => {
32
+ await sql`DROP TABLE IF EXISTS events`;
33
+ await sql`DROP TABLE IF EXISTS sessions`;
34
+ await sql`DROP TABLE IF EXISTS projects`;
35
+ await sql`DROP TABLE IF EXISTS users`;
36
+ await sql`DROP TABLE IF EXISTS orgs`;
37
+ await sql`CREATE TABLE IF NOT EXISTS orgs (id TEXT PRIMARY KEY, name TEXT NOT NULL, domain TEXT, slack_team_id TEXT, slack_bot_token TEXT, slack_system_channel_id TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`;
38
+ await sql`CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, org_id TEXT NOT NULL REFERENCES orgs(id), participant_id TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now())`;
39
+ await sql`CREATE TABLE IF NOT EXISTS projects (name TEXT NOT NULL, org_id TEXT NOT NULL REFERENCES orgs(id), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, name))`;
40
+ await sql`CREATE TABLE IF NOT EXISTS sessions (name TEXT NOT NULL, project TEXT NOT NULL, org_id TEXT NOT NULL, driver TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (org_id, project, name), FOREIGN KEY (org_id, project) REFERENCES projects(org_id, name))`;
41
+ await sql`CREATE TABLE IF NOT EXISTS events (id UUID PRIMARY KEY, org_id TEXT NOT NULL, project TEXT NOT NULL, session TEXT NOT NULL, timestamp TIMESTAMPTZ NOT NULL, source TEXT NOT NULL, sender TEXT NOT NULL, payload JSONB NOT NULL)`;
42
+ await sql`CREATE INDEX IF NOT EXISTS idx_events_project ON events(org_id, project, timestamp)`;
43
+ await sql`CREATE INDEX IF NOT EXISTS idx_events_session ON events(org_id, project, session, timestamp)`;
44
+ await sql`INSERT INTO orgs (id, name) VALUES ('default', 'Default') ON CONFLICT DO NOTHING`;
45
+ });
46
+
47
+ async function post(base: string, path: string, body: unknown) {
48
+ return fetch(`${base}${path}`, {
49
+ method: "POST",
50
+ headers: { "Content-Type": "application/json" },
51
+ body: JSON.stringify(body),
52
+ });
53
+ }
54
+
55
+ async function get(base: string, path: string) {
56
+ return fetch(`${base}${path}`);
57
+ }
58
+
59
+ // --- Full stack: daemon + cloud service + hooks + WebSocket ---
60
+
61
+ describe("e2e: two drivers on same project", () => {
62
+ test("connect two sessions, events route independently", async () => {
63
+ // Manu connects to pj/fxm
64
+ const manuRes = await post(daemonUrl, "/connect", {
65
+ ccSessionId: "cc-manu",
66
+ project: "pj",
67
+ session: "fxm",
68
+ user: "user:manu",
69
+ });
70
+ expect(manuRes.status).toBe(200);
71
+
72
+ // Krishna connects to pj/fxk
73
+ const krishnaRes = await post(daemonUrl, "/connect", {
74
+ ccSessionId: "cc-krishna",
75
+ project: "pj",
76
+ session: "fxk",
77
+ user: "user:krishna",
78
+ });
79
+ expect(krishnaRes.status).toBe(200);
80
+
81
+ // Both send hook events
82
+ await post(daemonUrl, "/events", {
83
+ session_id: "cc-manu",
84
+ hook_event_name: "UserPromptSubmit",
85
+ prompt: "build the auth middleware",
86
+ });
87
+ await post(daemonUrl, "/events", {
88
+ session_id: "cc-krishna",
89
+ hook_event_name: "UserPromptSubmit",
90
+ prompt: "set up the database schema",
91
+ });
92
+
93
+ // Verify isolation: fxm only has Manu's event
94
+ const fxmRes = await get(serviceUrl, "/projects/pj/sessions/fxm/messages");
95
+ const fxmBody = await fxmRes.json();
96
+ expect(fxmBody).toHaveLength(1);
97
+ expect(fxmBody[0].sender).toBe("user:manu");
98
+ expect(fxmBody[0].payload.prompt).toBe("build the auth middleware");
99
+
100
+ // Verify isolation: fxk only has Krishna's event
101
+ const fxkRes = await get(serviceUrl, "/projects/pj/sessions/fxk/messages");
102
+ const fxkBody = await fxkRes.json();
103
+ expect(fxkBody).toHaveLength(1);
104
+ expect(fxkBody[0].sender).toBe("user:krishna");
105
+ expect(fxkBody[0].payload.prompt).toBe("set up the database schema");
106
+
107
+ // Project-level view has both
108
+ const projRes = await get(serviceUrl, "/projects/pj/messages");
109
+ const projBody = await projRes.json();
110
+ expect(projBody).toHaveLength(2);
111
+ });
112
+ });
113
+
114
+ describe("e2e: advisor injection", () => {
115
+ test("advisor message reaches the target session only", async () => {
116
+ await post(daemonUrl, "/connect", {
117
+ ccSessionId: "cc-manu",
118
+ project: "pj",
119
+ session: "fxm",
120
+ user: "user:manu",
121
+ });
122
+ await post(daemonUrl, "/connect", {
123
+ ccSessionId: "cc-krishna",
124
+ project: "pj",
125
+ session: "fxk",
126
+ user: "user:krishna",
127
+ });
128
+
129
+ // Priya advises fxk only
130
+ await post(serviceUrl, "/projects/pj/sessions/fxk/inject", {
131
+ content: "Remember GDPR compliance on the users table",
132
+ sender: "user:priya",
133
+ });
134
+
135
+ // fxk has the advisory message
136
+ const fxkRes = await get(serviceUrl, "/projects/pj/sessions/fxk/messages");
137
+ const fxkBody = await fxkRes.json();
138
+ expect(fxkBody).toHaveLength(1);
139
+ expect(fxkBody[0].source).toBe("inject");
140
+ expect(fxkBody[0].payload.content).toBe("Remember GDPR compliance on the users table");
141
+
142
+ // fxm does not
143
+ const fxmRes = await get(serviceUrl, "/projects/pj/sessions/fxm/messages");
144
+ const fxmBody = await fxmRes.json();
145
+ expect(fxmBody).toHaveLength(0);
146
+ });
147
+
148
+ test("advisor injection broadcasts via WebSocket", async () => {
149
+ await post(daemonUrl, "/connect", {
150
+ ccSessionId: "cc-ws",
151
+ project: "pj",
152
+ session: "fxm",
153
+ user: "user:manu",
154
+ });
155
+
156
+ // Subscribe to session-level WS
157
+ const wsUrl = serviceUrl.replace("http", "ws");
158
+ const ws = new WebSocket(`${wsUrl}/projects/pj/sessions/fxm/ws`);
159
+ const received: unknown[] = [];
160
+ await new Promise<void>((resolve) => { ws.onopen = () => resolve(); });
161
+ ws.onmessage = (e) => received.push(JSON.parse(e.data as string));
162
+
163
+ // Inject
164
+ await post(serviceUrl, "/projects/pj/sessions/fxm/inject", {
165
+ content: "Use RS256 for JWT",
166
+ sender: "user:krishna",
167
+ });
168
+
169
+ await new Promise((r) => setTimeout(r, 100));
170
+ expect(received).toHaveLength(1);
171
+ expect((received[0] as { source: string }).source).toBe("inject");
172
+ expect((received[0] as { payload: { content: string } }).payload.content).toBe("Use RS256 for JWT");
173
+ ws.close();
174
+ });
175
+ });
176
+
177
+ describe("e2e: project-level WebSocket", () => {
178
+ test("receives events from all sessions", async () => {
179
+ await post(daemonUrl, "/connect", {
180
+ ccSessionId: "cc-proj-a",
181
+ project: "pj",
182
+ session: "fxm",
183
+ user: "user:manu",
184
+ });
185
+ await post(daemonUrl, "/connect", {
186
+ ccSessionId: "cc-proj-b",
187
+ project: "pj",
188
+ session: "fxk",
189
+ user: "user:krishna",
190
+ });
191
+
192
+ // Subscribe to project-level WS
193
+ const wsUrl = serviceUrl.replace("http", "ws");
194
+ const ws = new WebSocket(`${wsUrl}/projects/pj/ws`);
195
+ const received: unknown[] = [];
196
+ await new Promise<void>((resolve) => { ws.onopen = () => resolve(); });
197
+ ws.onmessage = (e) => received.push(JSON.parse(e.data as string));
198
+
199
+ // Events from both sessions
200
+ await post(daemonUrl, "/events", {
201
+ session_id: "cc-proj-a",
202
+ hook_event_name: "UserPromptSubmit",
203
+ prompt: "from fxm",
204
+ });
205
+ await post(daemonUrl, "/events", {
206
+ session_id: "cc-proj-b",
207
+ hook_event_name: "UserPromptSubmit",
208
+ prompt: "from fxk",
209
+ });
210
+
211
+ await new Promise((r) => setTimeout(r, 100));
212
+ expect(received).toHaveLength(2);
213
+ const sessions = (received as Array<{ session: string }>).map((e) => e.session).sort();
214
+ expect(sessions).toEqual(["fxk", "fxm"]);
215
+ ws.close();
216
+ });
217
+ });
218
+
219
+ describe("e2e: handoff", () => {
220
+ test("driver releases, new driver claims, log is continuous", async () => {
221
+ await post(daemonUrl, "/connect", {
222
+ ccSessionId: "cc-handoff-manu",
223
+ project: "pj",
224
+ session: "fxm",
225
+ user: "user:manu",
226
+ });
227
+
228
+ // Manu works
229
+ await post(daemonUrl, "/events", {
230
+ session_id: "cc-handoff-manu",
231
+ hook_event_name: "UserPromptSubmit",
232
+ prompt: "build auth middleware",
233
+ });
234
+
235
+ // Manu hands off
236
+ const handoffRes = await post(serviceUrl, "/projects/pj/sessions/fxm/handoff", {});
237
+ expect(handoffRes.status).toBe(200);
238
+
239
+ // Verify session is open
240
+ const sessRes = await get(serviceUrl, "/projects/pj/sessions/fxm");
241
+ const sessBody = await sessRes.json();
242
+ expect(sessBody.driver).toBeNull();
243
+
244
+ // Krishna claims
245
+ const claimRes = await post(serviceUrl, "/projects/pj/sessions/fxm/driver", {
246
+ driver: "user:krishna",
247
+ });
248
+ expect(claimRes.status).toBe(200);
249
+
250
+ // Krishna connects via daemon
251
+ await post(daemonUrl, "/connect", {
252
+ ccSessionId: "cc-handoff-krishna",
253
+ project: "pj",
254
+ session: "fxm",
255
+ user: "user:krishna",
256
+ });
257
+
258
+ // Krishna works
259
+ await post(daemonUrl, "/events", {
260
+ session_id: "cc-handoff-krishna",
261
+ hook_event_name: "UserPromptSubmit",
262
+ prompt: "add token refresh endpoint",
263
+ });
264
+
265
+ // Full log shows continuous history
266
+ const messages = await get(serviceUrl, "/projects/pj/sessions/fxm/messages");
267
+ const body = await messages.json();
268
+ expect(body).toHaveLength(2);
269
+ expect(body[0].sender).toBe("user:manu");
270
+ expect(body[0].payload.prompt).toBe("build auth middleware");
271
+ expect(body[1].sender).toBe("user:krishna");
272
+ expect(body[1].payload.prompt).toBe("add token refresh endpoint");
273
+ });
274
+ });
275
+
276
+ describe("e2e: status line", () => {
277
+ test("returns connected state per CC session", async () => {
278
+ await post(daemonUrl, "/connect", {
279
+ ccSessionId: "cc-status-1",
280
+ project: "pj",
281
+ session: "fxm",
282
+ user: "user:manu",
283
+ });
284
+
285
+ const connRes = await get(daemonUrl, "/status/cc-status-1");
286
+ const connBody = await connRes.json();
287
+ expect(connBody.connected).toBe(true);
288
+ expect(connBody.project).toBe("pj");
289
+ expect(connBody.session).toBe("fxm");
290
+ expect(connBody.user).toBe("user:manu");
291
+
292
+ const unknownRes = await get(daemonUrl, "/status/cc-nobody");
293
+ const unknownBody = await unknownRes.json();
294
+ expect(unknownBody.connected).toBe(false);
295
+ });
296
+
297
+ test("reflects disconnect", async () => {
298
+ await post(daemonUrl, "/connect", {
299
+ ccSessionId: "cc-status-2",
300
+ project: "pj",
301
+ session: "fxm",
302
+ user: "user:manu",
303
+ });
304
+
305
+ await post(daemonUrl, "/disconnect", { ccSessionId: "cc-status-2" });
306
+
307
+ const res = await get(daemonUrl, "/status/cc-status-2");
308
+ const body = await res.json();
309
+ expect(body.connected).toBe(false);
310
+ });
311
+ });
312
+
313
+ describe("e2e: capture.sh through daemon", () => {
314
+ test("hook script relays to correct session via daemon", async () => {
315
+ await post(daemonUrl, "/connect", {
316
+ ccSessionId: "cc-capture",
317
+ project: "pj",
318
+ session: "fxm",
319
+ user: "user:manu",
320
+ });
321
+
322
+ const hookPayload = JSON.stringify({
323
+ session_id: "cc-capture",
324
+ hook_event_name: "Stop",
325
+ stop_response: "Auth middleware is ready",
326
+ });
327
+
328
+ const proc = Bun.spawn(["sh", "hooks/capture.sh"], {
329
+ stdin: "pipe",
330
+ env: { ...process.env, POLARIS_PORT: String(new URL(daemonUrl).port) },
331
+ });
332
+ proc.stdin.write(hookPayload);
333
+ proc.stdin.end();
334
+ const exitCode = await proc.exited;
335
+ expect(exitCode).toBe(0);
336
+
337
+ await new Promise((r) => setTimeout(r, 200));
338
+
339
+ const res = await get(serviceUrl, "/projects/pj/sessions/fxm/messages");
340
+ const body = await res.json();
341
+ expect(body).toHaveLength(1);
342
+ expect(body[0].payload.stop_response).toBe("Auth middleware is ready");
343
+ expect(body[0].sender).toBe("user:manu");
344
+ });
345
+
346
+ test("hook script exits 0 when daemon is down", async () => {
347
+ const proc = Bun.spawn(["sh", "hooks/capture.sh"], {
348
+ stdin: "pipe",
349
+ env: { ...process.env, POLARIS_PORT: "59999" },
350
+ });
351
+ proc.stdin.write('{"session_id":"x","hook_event_name":"Stop","stop_response":"test"}');
352
+ proc.stdin.end();
353
+ const exitCode = await proc.exited;
354
+ expect(exitCode).toBe(0);
355
+ });
356
+
357
+ test("hook events for unconnected sessions are silently discarded", async () => {
358
+ const res = await post(daemonUrl, "/events", {
359
+ session_id: "cc-nobody",
360
+ hook_event_name: "UserPromptSubmit",
361
+ prompt: "this should go nowhere",
362
+ });
363
+ expect(res.status).toBe(200);
364
+ const body = await res.json();
365
+ expect(body.status).toBe("not_connected");
366
+ });
367
+ });
368
+
369
+ describe("e2e: session switching", () => {
370
+ test("same CC session can switch polaris sessions", async () => {
371
+ // Connect to fxm first
372
+ await post(daemonUrl, "/connect", {
373
+ ccSessionId: "cc-switch",
374
+ project: "pj",
375
+ session: "fxm",
376
+ user: "user:manu",
377
+ });
378
+
379
+ await post(daemonUrl, "/events", {
380
+ session_id: "cc-switch",
381
+ hook_event_name: "UserPromptSubmit",
382
+ prompt: "working on fxm",
383
+ });
384
+
385
+ // Switch to fxk
386
+ await post(daemonUrl, "/connect", {
387
+ ccSessionId: "cc-switch",
388
+ project: "pj",
389
+ session: "fxk",
390
+ user: "user:manu",
391
+ });
392
+
393
+ await post(daemonUrl, "/events", {
394
+ session_id: "cc-switch",
395
+ hook_event_name: "UserPromptSubmit",
396
+ prompt: "now working on fxk",
397
+ });
398
+
399
+ // Verify events went to correct sessions
400
+ const fxmRes = await get(serviceUrl, "/projects/pj/sessions/fxm/messages");
401
+ const fxmBody = await fxmRes.json();
402
+ expect(fxmBody).toHaveLength(1);
403
+ expect(fxmBody[0].payload.prompt).toBe("working on fxm");
404
+
405
+ const fxkRes = await get(serviceUrl, "/projects/pj/sessions/fxk/messages");
406
+ const fxkBody = await fxkRes.json();
407
+ expect(fxkBody).toHaveLength(1);
408
+ expect(fxkBody[0].payload.prompt).toBe("now working on fxk");
409
+
410
+ // Status reflects the new session
411
+ const statusRes = await get(daemonUrl, "/status/cc-switch");
412
+ const statusBody = await statusRes.json();
413
+ expect(statusBody.session).toBe("fxk");
414
+ });
415
+ });