session-collab-mcp 0.5.0 → 0.5.2

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,799 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { createTestDatabase, TestDatabase } from './test-helper.js';
3
+ import {
4
+ createSession,
5
+ getSession,
6
+ listSessions,
7
+ updateSessionHeartbeat,
8
+ endSession,
9
+ createClaim,
10
+ getClaim,
11
+ listClaims,
12
+ checkConflicts,
13
+ releaseClaim,
14
+ sendMessage,
15
+ listMessages,
16
+ addDecision,
17
+ listDecisions,
18
+ storeReferences,
19
+ getReferencesForSymbol,
20
+ analyzeClaimImpact,
21
+ } from '../queries.js';
22
+
23
+ describe('Session Queries', () => {
24
+ let db: TestDatabase;
25
+
26
+ beforeEach(() => {
27
+ db = createTestDatabase();
28
+ });
29
+
30
+ afterEach(() => {
31
+ db.close();
32
+ });
33
+
34
+ describe('createSession', () => {
35
+ it('should create a new session with required fields', async () => {
36
+ const session = await createSession(db, {
37
+ project_root: '/test/project',
38
+ });
39
+
40
+ expect(session.id).toBeDefined();
41
+ expect(session.project_root).toBe('/test/project');
42
+ expect(session.status).toBe('active');
43
+ expect(session.name).toBeNull();
44
+ });
45
+
46
+ it('should create a session with all optional fields', async () => {
47
+ const session = await createSession(db, {
48
+ name: 'test-session',
49
+ project_root: '/test/project',
50
+ machine_id: 'machine-1',
51
+ });
52
+
53
+ expect(session.name).toBe('test-session');
54
+ expect(session.machine_id).toBe('machine-1');
55
+ });
56
+ });
57
+
58
+ describe('getSession', () => {
59
+ it('should return null for non-existent session', async () => {
60
+ const session = await getSession(db, 'non-existent-id');
61
+ expect(session).toBeNull();
62
+ });
63
+
64
+ it('should return existing session', async () => {
65
+ const created = await createSession(db, { project_root: '/test' });
66
+ const fetched = await getSession(db, created.id);
67
+
68
+ expect(fetched).not.toBeNull();
69
+ expect(fetched!.id).toBe(created.id);
70
+ });
71
+ });
72
+
73
+ describe('listSessions', () => {
74
+ it('should list only active sessions by default', async () => {
75
+ const session1 = await createSession(db, { project_root: '/test1' });
76
+ const session2 = await createSession(db, { project_root: '/test2' });
77
+ await endSession(db, session2.id);
78
+
79
+ const sessions = await listSessions(db);
80
+ expect(sessions).toHaveLength(1);
81
+ expect(sessions[0].id).toBe(session1.id);
82
+ });
83
+
84
+ it('should include inactive sessions when requested', async () => {
85
+ await createSession(db, { project_root: '/test1' });
86
+ const session2 = await createSession(db, { project_root: '/test2' });
87
+ await endSession(db, session2.id);
88
+
89
+ const sessions = await listSessions(db, { include_inactive: true });
90
+ expect(sessions).toHaveLength(2);
91
+ });
92
+
93
+ it('should filter by project_root', async () => {
94
+ await createSession(db, { project_root: '/project-a' });
95
+ await createSession(db, { project_root: '/project-b' });
96
+
97
+ const sessions = await listSessions(db, { project_root: '/project-a' });
98
+ expect(sessions).toHaveLength(1);
99
+ expect(sessions[0].project_root).toBe('/project-a');
100
+ });
101
+ });
102
+
103
+ describe('updateSessionHeartbeat', () => {
104
+ it('should update heartbeat timestamp', async () => {
105
+ const session = await createSession(db, { project_root: '/test' });
106
+ const originalHeartbeat = session.last_heartbeat;
107
+
108
+ // Wait a bit to ensure timestamp changes
109
+ await new Promise((resolve) => setTimeout(resolve, 10));
110
+
111
+ const updated = await updateSessionHeartbeat(db, session.id);
112
+ expect(updated).toBe(true);
113
+
114
+ const fetched = await getSession(db, session.id);
115
+ expect(fetched!.last_heartbeat).not.toBe(originalHeartbeat);
116
+ });
117
+
118
+ it('should update current task and todos', async () => {
119
+ const session = await createSession(db, { project_root: '/test' });
120
+
121
+ await updateSessionHeartbeat(db, session.id, {
122
+ current_task: 'Working on feature X',
123
+ todos: [
124
+ { content: 'Task 1', status: 'completed' },
125
+ { content: 'Task 2', status: 'in_progress' },
126
+ ],
127
+ });
128
+
129
+ const fetched = await getSession(db, session.id);
130
+ expect(fetched!.current_task).toBe('Working on feature X');
131
+ expect(fetched!.progress).not.toBeNull();
132
+
133
+ const progress = JSON.parse(fetched!.progress!);
134
+ expect(progress.completed).toBe(1);
135
+ expect(progress.total).toBe(2);
136
+ expect(progress.percentage).toBe(50);
137
+ });
138
+ });
139
+
140
+ describe('endSession', () => {
141
+ it('should mark session as terminated', async () => {
142
+ const session = await createSession(db, { project_root: '/test' });
143
+ await endSession(db, session.id);
144
+
145
+ const fetched = await getSession(db, session.id);
146
+ expect(fetched!.status).toBe('terminated');
147
+ });
148
+
149
+ it('should abandon active claims by default', async () => {
150
+ const session = await createSession(db, { project_root: '/test' });
151
+ const { claim } = await createClaim(db, {
152
+ session_id: session.id,
153
+ files: ['file.ts'],
154
+ intent: 'Test',
155
+ });
156
+
157
+ await endSession(db, session.id, 'abandon');
158
+
159
+ const fetchedClaim = await getClaim(db, claim.id);
160
+ expect(fetchedClaim!.status).toBe('abandoned');
161
+ });
162
+
163
+ it('should complete claims when requested', async () => {
164
+ const session = await createSession(db, { project_root: '/test' });
165
+ const { claim } = await createClaim(db, {
166
+ session_id: session.id,
167
+ files: ['file.ts'],
168
+ intent: 'Test',
169
+ });
170
+
171
+ await endSession(db, session.id, 'complete');
172
+
173
+ const fetchedClaim = await getClaim(db, claim.id);
174
+ expect(fetchedClaim!.status).toBe('completed');
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('Claim Queries', () => {
180
+ let db: TestDatabase;
181
+ let sessionId: string;
182
+
183
+ beforeEach(async () => {
184
+ db = createTestDatabase();
185
+ const session = await createSession(db, { project_root: '/test' });
186
+ sessionId = session.id;
187
+ });
188
+
189
+ afterEach(() => {
190
+ db.close();
191
+ });
192
+
193
+ describe('createClaim', () => {
194
+ it('should create a file-level claim', async () => {
195
+ const result = await createClaim(db, {
196
+ session_id: sessionId,
197
+ files: ['src/app.ts', 'src/utils.ts'],
198
+ intent: 'Refactoring utilities',
199
+ });
200
+
201
+ expect(result.claim.id).toBeDefined();
202
+ expect(result.claim.status).toBe('active');
203
+ expect(result.files).toEqual(['src/app.ts', 'src/utils.ts']);
204
+ });
205
+
206
+ it('should create a symbol-level claim', async () => {
207
+ const result = await createClaim(db, {
208
+ session_id: sessionId,
209
+ files: ['src/auth.ts'],
210
+ intent: 'Updating auth functions',
211
+ symbols: [
212
+ { file: 'src/auth.ts', symbols: ['validateToken', 'refreshToken'] },
213
+ ],
214
+ });
215
+
216
+ expect(result.symbols).toBeDefined();
217
+ expect(result.symbols).toHaveLength(1);
218
+ expect(result.symbols![0].symbols).toContain('validateToken');
219
+ });
220
+
221
+ it('should handle glob patterns', async () => {
222
+ const result = await createClaim(db, {
223
+ session_id: sessionId,
224
+ files: ['src/**/*.ts'],
225
+ intent: 'Refactoring all TS files',
226
+ });
227
+
228
+ const claim = await getClaim(db, result.claim.id);
229
+ expect(claim!.files).toContain('src/**/*.ts');
230
+ });
231
+ });
232
+
233
+ describe('getClaim', () => {
234
+ it('should return null for non-existent claim', async () => {
235
+ const claim = await getClaim(db, 'non-existent');
236
+ expect(claim).toBeNull();
237
+ });
238
+
239
+ it('should include files and session name', async () => {
240
+ // Create session with name
241
+ const namedSession = await createSession(db, {
242
+ name: 'feature-dev',
243
+ project_root: '/test',
244
+ });
245
+
246
+ const { claim } = await createClaim(db, {
247
+ session_id: namedSession.id,
248
+ files: ['file.ts'],
249
+ intent: 'Test',
250
+ });
251
+
252
+ const fetched = await getClaim(db, claim.id);
253
+ expect(fetched!.files).toContain('file.ts');
254
+ expect(fetched!.session_name).toBe('feature-dev');
255
+ });
256
+ });
257
+
258
+ describe('listClaims', () => {
259
+ it('should list claims for a session', async () => {
260
+ await createClaim(db, { session_id: sessionId, files: ['a.ts'], intent: 'A' });
261
+ await createClaim(db, { session_id: sessionId, files: ['b.ts'], intent: 'B' });
262
+
263
+ const claims = await listClaims(db, { session_id: sessionId });
264
+ expect(claims).toHaveLength(2);
265
+ });
266
+
267
+ it('should filter by status', async () => {
268
+ const { claim } = await createClaim(db, { session_id: sessionId, files: ['a.ts'], intent: 'A' });
269
+ await createClaim(db, { session_id: sessionId, files: ['b.ts'], intent: 'B' });
270
+ await releaseClaim(db, claim.id, { status: 'completed' });
271
+
272
+ const activeClaims = await listClaims(db, { status: 'active' });
273
+ expect(activeClaims).toHaveLength(1);
274
+
275
+ const completedClaims = await listClaims(db, { status: 'completed' });
276
+ expect(completedClaims).toHaveLength(1);
277
+ });
278
+ });
279
+
280
+ describe('releaseClaim', () => {
281
+ it('should update claim status to completed', async () => {
282
+ const { claim } = await createClaim(db, {
283
+ session_id: sessionId,
284
+ files: ['file.ts'],
285
+ intent: 'Test',
286
+ });
287
+
288
+ await releaseClaim(db, claim.id, {
289
+ status: 'completed',
290
+ summary: 'Done!',
291
+ });
292
+
293
+ const fetched = await getClaim(db, claim.id);
294
+ expect(fetched!.status).toBe('completed');
295
+ expect(fetched!.completed_summary).toBe('Done!');
296
+ });
297
+
298
+ it('should update claim status to abandoned', async () => {
299
+ const { claim } = await createClaim(db, {
300
+ session_id: sessionId,
301
+ files: ['file.ts'],
302
+ intent: 'Test',
303
+ });
304
+
305
+ await releaseClaim(db, claim.id, { status: 'abandoned' });
306
+
307
+ const fetched = await getClaim(db, claim.id);
308
+ expect(fetched!.status).toBe('abandoned');
309
+ });
310
+ });
311
+ });
312
+
313
+ describe('Conflict Detection', () => {
314
+ let db: TestDatabase;
315
+ let session1Id: string;
316
+ let session2Id: string;
317
+
318
+ beforeEach(async () => {
319
+ db = createTestDatabase();
320
+ const session1 = await createSession(db, { name: 'session-1', project_root: '/test' });
321
+ const session2 = await createSession(db, { name: 'session-2', project_root: '/test' });
322
+ session1Id = session1.id;
323
+ session2Id = session2.id;
324
+ });
325
+
326
+ afterEach(() => {
327
+ db.close();
328
+ });
329
+
330
+ describe('File-level conflicts', () => {
331
+ it('should detect conflict when same file is claimed', async () => {
332
+ await createClaim(db, {
333
+ session_id: session1Id,
334
+ files: ['src/app.ts'],
335
+ intent: 'Working on app',
336
+ });
337
+
338
+ const conflicts = await checkConflicts(db, ['src/app.ts'], session2Id);
339
+ expect(conflicts).toHaveLength(1);
340
+ expect(conflicts[0].file_path).toBe('src/app.ts');
341
+ expect(conflicts[0].session_name).toBe('session-1');
342
+ expect(conflicts[0].conflict_level).toBe('file');
343
+ });
344
+
345
+ it('should not detect conflict for own claims', async () => {
346
+ await createClaim(db, {
347
+ session_id: session1Id,
348
+ files: ['src/app.ts'],
349
+ intent: 'Working on app',
350
+ });
351
+
352
+ const conflicts = await checkConflicts(db, ['src/app.ts'], session1Id);
353
+ expect(conflicts).toHaveLength(0);
354
+ });
355
+
356
+ it('should not detect conflict for released claims', async () => {
357
+ const { claim } = await createClaim(db, {
358
+ session_id: session1Id,
359
+ files: ['src/app.ts'],
360
+ intent: 'Working on app',
361
+ });
362
+ await releaseClaim(db, claim.id, { status: 'completed' });
363
+
364
+ const conflicts = await checkConflicts(db, ['src/app.ts'], session2Id);
365
+ expect(conflicts).toHaveLength(0);
366
+ });
367
+
368
+ it('should detect conflict with glob patterns', async () => {
369
+ await createClaim(db, {
370
+ session_id: session1Id,
371
+ files: ['src/**/*.ts'],
372
+ intent: 'Refactoring',
373
+ });
374
+
375
+ const conflicts = await checkConflicts(db, ['src/utils/helper.ts'], session2Id);
376
+ expect(conflicts).toHaveLength(1);
377
+ });
378
+
379
+ it('should handle multiple files with mixed conflicts', async () => {
380
+ await createClaim(db, {
381
+ session_id: session1Id,
382
+ files: ['src/a.ts', 'src/b.ts'],
383
+ intent: 'Working',
384
+ });
385
+
386
+ const conflicts = await checkConflicts(db, ['src/a.ts', 'src/c.ts'], session2Id);
387
+ expect(conflicts).toHaveLength(1);
388
+ expect(conflicts[0].file_path).toBe('src/a.ts');
389
+ });
390
+ });
391
+
392
+ describe('Symbol-level conflicts', () => {
393
+ it('should detect symbol-level conflict', async () => {
394
+ await createClaim(db, {
395
+ session_id: session1Id,
396
+ files: ['src/auth.ts'],
397
+ intent: 'Updating validateToken',
398
+ symbols: [{ file: 'src/auth.ts', symbols: ['validateToken'] }],
399
+ });
400
+
401
+ const conflicts = await checkConflicts(
402
+ db,
403
+ ['src/auth.ts'],
404
+ session2Id,
405
+ [{ file: 'src/auth.ts', symbols: ['validateToken'] }]
406
+ );
407
+
408
+ expect(conflicts).toHaveLength(1);
409
+ expect(conflicts[0].symbol_name).toBe('validateToken');
410
+ expect(conflicts[0].conflict_level).toBe('symbol');
411
+ });
412
+
413
+ it('should NOT conflict when different symbols in same file', async () => {
414
+ await createClaim(db, {
415
+ session_id: session1Id,
416
+ files: ['src/auth.ts'],
417
+ intent: 'Updating validateToken',
418
+ symbols: [{ file: 'src/auth.ts', symbols: ['validateToken'] }],
419
+ });
420
+
421
+ const conflicts = await checkConflicts(
422
+ db,
423
+ ['src/auth.ts'],
424
+ session2Id,
425
+ [{ file: 'src/auth.ts', symbols: ['refreshToken'] }]
426
+ );
427
+
428
+ expect(conflicts).toHaveLength(0);
429
+ });
430
+
431
+ it('should conflict when file-level claim exists and checking symbols', async () => {
432
+ // Session 1 claims entire file (no symbols)
433
+ await createClaim(db, {
434
+ session_id: session1Id,
435
+ files: ['src/auth.ts'],
436
+ intent: 'Refactoring entire file',
437
+ });
438
+
439
+ // Session 2 wants to modify a specific symbol
440
+ const conflicts = await checkConflicts(
441
+ db,
442
+ ['src/auth.ts'],
443
+ session2Id,
444
+ [{ file: 'src/auth.ts', symbols: ['validateToken'] }]
445
+ );
446
+
447
+ expect(conflicts).toHaveLength(1);
448
+ expect(conflicts[0].conflict_level).toBe('file');
449
+ });
450
+
451
+ it('should detect symbol conflicts when checking file without symbols', async () => {
452
+ // Session 1 claims specific symbols
453
+ await createClaim(db, {
454
+ session_id: session1Id,
455
+ files: ['src/auth.ts'],
456
+ intent: 'Updating auth',
457
+ symbols: [{ file: 'src/auth.ts', symbols: ['validateToken', 'refreshToken'] }],
458
+ });
459
+
460
+ // Session 2 wants to modify the entire file (no symbols specified)
461
+ const conflicts = await checkConflicts(db, ['src/auth.ts'], session2Id);
462
+
463
+ // Should detect both file-level claim AND symbol-level claims
464
+ expect(conflicts.length).toBeGreaterThanOrEqual(2);
465
+ const symbolConflicts = conflicts.filter((c) => c.conflict_level === 'symbol');
466
+ expect(symbolConflicts.map((c) => c.symbol_name)).toContain('validateToken');
467
+ expect(symbolConflicts.map((c) => c.symbol_name)).toContain('refreshToken');
468
+ });
469
+ });
470
+
471
+ describe('Edge cases', () => {
472
+ it('should handle empty files array', async () => {
473
+ const conflicts = await checkConflicts(db, [], session2Id);
474
+ expect(conflicts).toHaveLength(0);
475
+ });
476
+
477
+ it('should detect conflicts from simultaneous claims (race condition scenario)', async () => {
478
+ // Simulate race condition: both sessions create claims, then check conflicts
479
+ // This tests the "create first, check after" pattern
480
+
481
+ // Session 1 creates claim
482
+ await createClaim(db, {
483
+ session_id: session1Id,
484
+ files: ['src/shared.ts'],
485
+ intent: 'Session 1 working',
486
+ });
487
+
488
+ // Session 2 creates claim (simulating simultaneous creation)
489
+ await createClaim(db, {
490
+ session_id: session2Id,
491
+ files: ['src/shared.ts'],
492
+ intent: 'Session 2 working',
493
+ });
494
+
495
+ // Both sessions should see the other's claim as a conflict
496
+ const conflicts1 = await checkConflicts(db, ['src/shared.ts'], session1Id);
497
+ const conflicts2 = await checkConflicts(db, ['src/shared.ts'], session2Id);
498
+
499
+ // Session 1 sees Session 2's claim
500
+ expect(conflicts1).toHaveLength(1);
501
+ expect(conflicts1[0].session_id).toBe(session2Id);
502
+
503
+ // Session 2 sees Session 1's claim
504
+ expect(conflicts2).toHaveLength(1);
505
+ expect(conflicts2[0].session_id).toBe(session1Id);
506
+ });
507
+
508
+ it('should handle inactive session claims', async () => {
509
+ await createClaim(db, {
510
+ session_id: session1Id,
511
+ files: ['src/app.ts'],
512
+ intent: 'Working',
513
+ });
514
+ await endSession(db, session1Id);
515
+
516
+ const conflicts = await checkConflicts(db, ['src/app.ts'], session2Id);
517
+ expect(conflicts).toHaveLength(0);
518
+ });
519
+
520
+ it('should deduplicate conflicts', async () => {
521
+ // Create two claims for the same file
522
+ await createClaim(db, {
523
+ session_id: session1Id,
524
+ files: ['src/app.ts'],
525
+ intent: 'Claim 1',
526
+ });
527
+ await createClaim(db, {
528
+ session_id: session1Id,
529
+ files: ['src/app.ts'],
530
+ intent: 'Claim 2',
531
+ });
532
+
533
+ const conflicts = await checkConflicts(db, ['src/app.ts'], session2Id);
534
+ // Should have 2 conflicts (one per claim)
535
+ expect(conflicts.length).toBeGreaterThanOrEqual(1);
536
+ });
537
+ });
538
+ });
539
+
540
+ describe('Message Queries', () => {
541
+ let db: TestDatabase;
542
+ let session1Id: string;
543
+ let session2Id: string;
544
+
545
+ beforeEach(async () => {
546
+ db = createTestDatabase();
547
+ const session1 = await createSession(db, { project_root: '/test' });
548
+ const session2 = await createSession(db, { project_root: '/test' });
549
+ session1Id = session1.id;
550
+ session2Id = session2.id;
551
+ });
552
+
553
+ afterEach(() => {
554
+ db.close();
555
+ });
556
+
557
+ it('should send and receive messages', async () => {
558
+ const message = await sendMessage(db, {
559
+ from_session_id: session1Id,
560
+ to_session_id: session2Id,
561
+ content: 'Hello from session 1',
562
+ });
563
+
564
+ expect(message.id).toBeDefined();
565
+ expect(message.content).toBe('Hello from session 1');
566
+ expect(message.read_at).toBeNull();
567
+ });
568
+
569
+ it('should list messages for a session', async () => {
570
+ await sendMessage(db, {
571
+ from_session_id: session1Id,
572
+ to_session_id: session2Id,
573
+ content: 'Message 1',
574
+ });
575
+ await sendMessage(db, {
576
+ from_session_id: session1Id,
577
+ to_session_id: session2Id,
578
+ content: 'Message 2',
579
+ });
580
+
581
+ const messages = await listMessages(db, { session_id: session2Id });
582
+ expect(messages).toHaveLength(2);
583
+ });
584
+
585
+ it('should include broadcast messages', async () => {
586
+ await sendMessage(db, {
587
+ from_session_id: session1Id,
588
+ content: 'Broadcast message',
589
+ });
590
+
591
+ const messages = await listMessages(db, { session_id: session2Id });
592
+ expect(messages).toHaveLength(1);
593
+ expect(messages[0].to_session_id).toBeNull();
594
+ });
595
+
596
+ it('should filter unread messages', async () => {
597
+ await sendMessage(db, {
598
+ from_session_id: session1Id,
599
+ to_session_id: session2Id,
600
+ content: 'Message 1',
601
+ });
602
+
603
+ // Read messages (marks them as read)
604
+ await listMessages(db, { session_id: session2Id, mark_as_read: true });
605
+
606
+ // Send another message
607
+ await sendMessage(db, {
608
+ from_session_id: session1Id,
609
+ to_session_id: session2Id,
610
+ content: 'Message 2',
611
+ });
612
+
613
+ const unread = await listMessages(db, { session_id: session2Id, unread_only: true });
614
+ expect(unread).toHaveLength(1);
615
+ expect(unread[0].content).toBe('Message 2');
616
+ });
617
+ });
618
+
619
+ describe('Decision Queries', () => {
620
+ let db: TestDatabase;
621
+ let sessionId: string;
622
+
623
+ beforeEach(async () => {
624
+ db = createTestDatabase();
625
+ const session = await createSession(db, { project_root: '/test' });
626
+ sessionId = session.id;
627
+ });
628
+
629
+ afterEach(() => {
630
+ db.close();
631
+ });
632
+
633
+ it('should add a decision', async () => {
634
+ const decision = await addDecision(db, {
635
+ session_id: sessionId,
636
+ category: 'architecture',
637
+ title: 'Use microservices',
638
+ description: 'We decided to use microservices architecture for scalability.',
639
+ });
640
+
641
+ expect(decision.id).toBeDefined();
642
+ expect(decision.category).toBe('architecture');
643
+ });
644
+
645
+ it('should list decisions by category', async () => {
646
+ await addDecision(db, {
647
+ session_id: sessionId,
648
+ category: 'architecture',
649
+ title: 'Architecture decision',
650
+ description: 'Details...',
651
+ });
652
+ await addDecision(db, {
653
+ session_id: sessionId,
654
+ category: 'api',
655
+ title: 'API decision',
656
+ description: 'Details...',
657
+ });
658
+
659
+ const archDecisions = await listDecisions(db, { category: 'architecture' });
660
+ expect(archDecisions).toHaveLength(1);
661
+ expect(archDecisions[0].title).toBe('Architecture decision');
662
+ });
663
+
664
+ it('should respect limit parameter', async () => {
665
+ for (let i = 0; i < 5; i++) {
666
+ await addDecision(db, {
667
+ session_id: sessionId,
668
+ title: `Decision ${i}`,
669
+ description: 'Details...',
670
+ });
671
+ }
672
+
673
+ const limited = await listDecisions(db, { limit: 3 });
674
+ expect(limited).toHaveLength(3);
675
+ });
676
+ });
677
+
678
+ describe('Reference Queries', () => {
679
+ let db: TestDatabase;
680
+ let sessionId: string;
681
+
682
+ beforeEach(async () => {
683
+ db = createTestDatabase();
684
+ const session = await createSession(db, { project_root: '/test' });
685
+ sessionId = session.id;
686
+ });
687
+
688
+ afterEach(() => {
689
+ db.close();
690
+ });
691
+
692
+ describe('storeReferences', () => {
693
+ it('should store symbol references', async () => {
694
+ const result = await storeReferences(db, sessionId, [
695
+ {
696
+ source_file: 'src/auth.ts',
697
+ source_symbol: 'validateToken',
698
+ references: [
699
+ { file: 'src/api/users.ts', line: 15, context: 'const valid = validateToken(token)' },
700
+ { file: 'src/api/orders.ts', line: 23 },
701
+ ],
702
+ },
703
+ ]);
704
+
705
+ expect(result.stored).toBe(2);
706
+ expect(result.skipped).toBe(0);
707
+ });
708
+
709
+ it('should skip duplicate references', async () => {
710
+ await storeReferences(db, sessionId, [
711
+ {
712
+ source_file: 'src/auth.ts',
713
+ source_symbol: 'validateToken',
714
+ references: [{ file: 'src/api/users.ts', line: 15 }],
715
+ },
716
+ ]);
717
+
718
+ await storeReferences(db, sessionId, [
719
+ {
720
+ source_file: 'src/auth.ts',
721
+ source_symbol: 'validateToken',
722
+ references: [{ file: 'src/api/users.ts', line: 15 }], // Same reference
723
+ },
724
+ ]);
725
+
726
+ // INSERT OR IGNORE counts as stored (no error), so we verify by checking total count
727
+ const refs = await getReferencesForSymbol(db, 'src/auth.ts', 'validateToken');
728
+ expect(refs).toHaveLength(1); // Still only 1 reference (duplicate was ignored)
729
+ });
730
+ });
731
+
732
+ describe('getReferencesForSymbol', () => {
733
+ it('should return references for a symbol', async () => {
734
+ await storeReferences(db, sessionId, [
735
+ {
736
+ source_file: 'src/auth.ts',
737
+ source_symbol: 'validateToken',
738
+ references: [
739
+ { file: 'src/api/users.ts', line: 15 },
740
+ { file: 'src/api/orders.ts', line: 23 },
741
+ ],
742
+ },
743
+ ]);
744
+
745
+ const refs = await getReferencesForSymbol(db, 'src/auth.ts', 'validateToken');
746
+ expect(refs).toHaveLength(2);
747
+ });
748
+ });
749
+
750
+ describe('analyzeClaimImpact', () => {
751
+ it('should identify impacted claims', async () => {
752
+ // Store references
753
+ await storeReferences(db, sessionId, [
754
+ {
755
+ source_file: 'src/auth.ts',
756
+ source_symbol: 'validateToken',
757
+ references: [{ file: 'src/api/users.ts', line: 15 }],
758
+ },
759
+ ]);
760
+
761
+ // Create a second session with a claim on the referencing file
762
+ const session2 = await createSession(db, { name: 'other-session', project_root: '/test' });
763
+ await createClaim(db, {
764
+ session_id: session2.id,
765
+ files: ['src/api/users.ts'],
766
+ intent: 'Working on users API',
767
+ });
768
+
769
+ // Analyze impact
770
+ const impact = await analyzeClaimImpact(db, 'src/auth.ts', 'validateToken', sessionId);
771
+
772
+ expect(impact.symbol).toBe('validateToken');
773
+ expect(impact.reference_count).toBe(1);
774
+ expect(impact.affected_files).toContain('src/api/users.ts');
775
+ expect(impact.affected_claims).toHaveLength(1);
776
+ expect(impact.affected_claims[0].session_name).toBe('other-session');
777
+ });
778
+
779
+ it('should exclude own session from impacted claims', async () => {
780
+ await storeReferences(db, sessionId, [
781
+ {
782
+ source_file: 'src/auth.ts',
783
+ source_symbol: 'validateToken',
784
+ references: [{ file: 'src/api/users.ts', line: 15 }],
785
+ },
786
+ ]);
787
+
788
+ // Same session claims the referencing file
789
+ await createClaim(db, {
790
+ session_id: sessionId,
791
+ files: ['src/api/users.ts'],
792
+ intent: 'Own work',
793
+ });
794
+
795
+ const impact = await analyzeClaimImpact(db, 'src/auth.ts', 'validateToken', sessionId);
796
+ expect(impact.affected_claims).toHaveLength(0);
797
+ });
798
+ });
799
+ });