@vibescope/mcp-server 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.
Files changed (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1,687 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import type { HandlerContext, TokenUsage } from './types.js';
4
+ import {
5
+ startFallbackActivity,
6
+ stopFallbackActivity,
7
+ getActivityHistory,
8
+ getActivitySchedules,
9
+ } from './fallback.js';
10
+ import { ValidationError } from '../validators.js';
11
+
12
+ // ============================================================================
13
+ // Test Utilities
14
+ // ============================================================================
15
+
16
+ function createMockSupabase(overrides: {
17
+ selectResult?: { data: unknown; error: unknown };
18
+ insertResult?: { data: unknown; error: unknown };
19
+ updateResult?: { data: unknown; error: unknown };
20
+ deleteResult?: { data: unknown; error: unknown };
21
+ } = {}) {
22
+ const defaultResult = { data: null, error: null };
23
+ let currentOperation = 'select';
24
+ let insertThenSelect = false;
25
+
26
+ const mock = {
27
+ from: vi.fn().mockReturnThis(),
28
+ select: vi.fn(() => {
29
+ if (currentOperation === 'insert') {
30
+ insertThenSelect = true;
31
+ } else {
32
+ currentOperation = 'select';
33
+ insertThenSelect = false;
34
+ }
35
+ return mock;
36
+ }),
37
+ insert: vi.fn(() => {
38
+ currentOperation = 'insert';
39
+ insertThenSelect = false;
40
+ return mock;
41
+ }),
42
+ update: vi.fn(() => {
43
+ currentOperation = 'update';
44
+ insertThenSelect = false;
45
+ return mock;
46
+ }),
47
+ delete: vi.fn(() => {
48
+ currentOperation = 'delete';
49
+ insertThenSelect = false;
50
+ return mock;
51
+ }),
52
+ eq: vi.fn().mockReturnThis(),
53
+ neq: vi.fn().mockReturnThis(),
54
+ in: vi.fn().mockReturnThis(),
55
+ is: vi.fn().mockReturnThis(),
56
+ not: vi.fn().mockReturnThis(),
57
+ or: vi.fn().mockReturnThis(),
58
+ gt: vi.fn().mockReturnThis(),
59
+ gte: vi.fn().mockReturnThis(),
60
+ lte: vi.fn().mockReturnThis(),
61
+ lt: vi.fn().mockReturnThis(),
62
+ order: vi.fn().mockReturnThis(),
63
+ limit: vi.fn().mockReturnThis(),
64
+ single: vi.fn(() => {
65
+ if (currentOperation === 'insert' || insertThenSelect) {
66
+ return Promise.resolve(overrides.insertResult ?? defaultResult);
67
+ }
68
+ if (currentOperation === 'select') {
69
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
70
+ }
71
+ if (currentOperation === 'update') {
72
+ return Promise.resolve(overrides.updateResult ?? defaultResult);
73
+ }
74
+ return Promise.resolve(defaultResult);
75
+ }),
76
+ maybeSingle: vi.fn(() => {
77
+ return Promise.resolve(overrides.selectResult ?? defaultResult);
78
+ }),
79
+ then: vi.fn((resolve: (value: unknown) => void) => {
80
+ if (currentOperation === 'insert' || insertThenSelect) {
81
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
82
+ }
83
+ if (currentOperation === 'select') {
84
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
85
+ }
86
+ if (currentOperation === 'update') {
87
+ return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
88
+ }
89
+ if (currentOperation === 'delete') {
90
+ return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
91
+ }
92
+ return Promise.resolve(defaultResult).then(resolve);
93
+ }),
94
+ };
95
+
96
+ return mock as unknown as SupabaseClient;
97
+ }
98
+
99
+ function createMockContext(
100
+ supabase: SupabaseClient,
101
+ options: { sessionId?: string | null } = {}
102
+ ): HandlerContext {
103
+ const defaultTokenUsage: TokenUsage = {
104
+ callCount: 5,
105
+ totalTokens: 2500,
106
+ byTool: {},
107
+ };
108
+
109
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
110
+
111
+ return {
112
+ supabase,
113
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
114
+ session: {
115
+ instanceId: 'instance-abc',
116
+ currentSessionId: sessionId,
117
+ currentPersona: 'Wave',
118
+ tokenUsage: defaultTokenUsage,
119
+ },
120
+ updateSession: vi.fn(),
121
+ };
122
+ }
123
+
124
+ // ============================================================================
125
+ // startFallbackActivity Tests
126
+ // ============================================================================
127
+
128
+ describe('startFallbackActivity', () => {
129
+ beforeEach(() => vi.clearAllMocks());
130
+
131
+ it('should throw error for missing project_id', async () => {
132
+ const supabase = createMockSupabase();
133
+ const ctx = createMockContext(supabase);
134
+
135
+ await expect(
136
+ startFallbackActivity({ activity: 'code_review' }, ctx)
137
+ ).rejects.toThrow(ValidationError);
138
+ });
139
+
140
+ it('should throw error for invalid project_id UUID', async () => {
141
+ const supabase = createMockSupabase();
142
+ const ctx = createMockContext(supabase);
143
+
144
+ await expect(
145
+ startFallbackActivity({ project_id: 'invalid', activity: 'code_review' }, ctx)
146
+ ).rejects.toThrow(ValidationError);
147
+ });
148
+
149
+ it('should throw error for missing activity', async () => {
150
+ const supabase = createMockSupabase();
151
+ const ctx = createMockContext(supabase);
152
+
153
+ await expect(
154
+ startFallbackActivity({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
155
+ ).rejects.toThrow(ValidationError);
156
+ });
157
+
158
+ it('should throw error for invalid activity type', async () => {
159
+ const supabase = createMockSupabase();
160
+ const ctx = createMockContext(supabase);
161
+
162
+ await expect(
163
+ startFallbackActivity({
164
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
165
+ activity: 'invalid_activity',
166
+ }, ctx)
167
+ ).rejects.toThrow('Invalid activity');
168
+ });
169
+
170
+ it('should start fallback activity successfully', async () => {
171
+ const supabase = createMockSupabase({
172
+ updateResult: { data: null, error: null },
173
+ });
174
+ const ctx = createMockContext(supabase);
175
+
176
+ const result = await startFallbackActivity(
177
+ {
178
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
179
+ activity: 'code_review',
180
+ },
181
+ ctx
182
+ );
183
+
184
+ expect(result.result).toMatchObject({
185
+ success: true,
186
+ activity: 'code_review',
187
+ });
188
+ expect((result.result as { title: string }).title).toBeDefined();
189
+ });
190
+
191
+ it('should update agent_sessions with fallback activity', async () => {
192
+ const supabase = createMockSupabase({
193
+ updateResult: { data: null, error: null },
194
+ });
195
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
196
+
197
+ await startFallbackActivity(
198
+ {
199
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
200
+ activity: 'security_review',
201
+ },
202
+ ctx
203
+ );
204
+
205
+ expect(supabase.from).toHaveBeenCalledWith('agent_sessions');
206
+ expect(supabase.update).toHaveBeenCalledWith(
207
+ expect.objectContaining({
208
+ current_fallback_activity: 'security_review',
209
+ current_task_id: null,
210
+ status: 'active',
211
+ })
212
+ );
213
+ expect(supabase.eq).toHaveBeenCalledWith('id', 'my-session');
214
+ });
215
+
216
+ it('should accept all valid activity types', async () => {
217
+ const validActivities = [
218
+ 'feature_ideation',
219
+ 'code_review',
220
+ 'performance_audit',
221
+ 'ux_review',
222
+ 'cost_analysis',
223
+ 'security_review',
224
+ 'test_coverage',
225
+ 'documentation_review',
226
+ 'dependency_audit',
227
+ 'validate_completed_tasks',
228
+ ];
229
+
230
+ for (const activity of validActivities) {
231
+ const supabase = createMockSupabase({
232
+ updateResult: { data: null, error: null },
233
+ });
234
+ const ctx = createMockContext(supabase);
235
+
236
+ const result = await startFallbackActivity(
237
+ {
238
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
239
+ activity,
240
+ },
241
+ ctx
242
+ );
243
+
244
+ expect(result.result).toMatchObject({
245
+ success: true,
246
+ activity,
247
+ });
248
+ }
249
+ });
250
+
251
+ it('should throw error when update fails', async () => {
252
+ const supabase = createMockSupabase();
253
+ const ctx = createMockContext(supabase);
254
+
255
+ // Override update to return error
256
+ vi.mocked(supabase.from('').update).mockReturnValue({
257
+ ...supabase,
258
+ eq: vi.fn().mockReturnValue({
259
+ then: (resolve: (val: unknown) => void) =>
260
+ Promise.resolve({ data: null, error: { message: 'Update failed' } }).then(resolve),
261
+ }),
262
+ } as unknown as ReturnType<SupabaseClient['from']>);
263
+
264
+ await expect(
265
+ startFallbackActivity({
266
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
267
+ activity: 'code_review',
268
+ }, ctx)
269
+ ).rejects.toThrow();
270
+ });
271
+ });
272
+
273
+ // ============================================================================
274
+ // stopFallbackActivity Tests
275
+ // ============================================================================
276
+
277
+ describe('stopFallbackActivity', () => {
278
+ beforeEach(() => vi.clearAllMocks());
279
+
280
+ it('should throw error for missing project_id', async () => {
281
+ const supabase = createMockSupabase();
282
+ const ctx = createMockContext(supabase);
283
+
284
+ await expect(stopFallbackActivity({}, ctx)).rejects.toThrow(ValidationError);
285
+ });
286
+
287
+ it('should throw error for invalid project_id UUID', async () => {
288
+ const supabase = createMockSupabase();
289
+ const ctx = createMockContext(supabase);
290
+
291
+ await expect(
292
+ stopFallbackActivity({ project_id: 'invalid' }, ctx)
293
+ ).rejects.toThrow(ValidationError);
294
+ });
295
+
296
+ it('should stop fallback activity successfully when activity exists', async () => {
297
+ const supabase = createMockSupabase({
298
+ selectResult: { data: { current_fallback_activity: 'code_review' }, error: null },
299
+ insertResult: { data: null, error: null },
300
+ updateResult: { data: null, error: null },
301
+ });
302
+ const ctx = createMockContext(supabase);
303
+
304
+ const result = await stopFallbackActivity(
305
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
306
+ ctx
307
+ );
308
+
309
+ expect(result.result).toMatchObject({
310
+ success: true,
311
+ });
312
+ expect((result.result as { message: string }).message).toContain('code_review');
313
+ });
314
+
315
+ it('should stop fallback activity when no activity was active', async () => {
316
+ const supabase = createMockSupabase({
317
+ selectResult: { data: { current_fallback_activity: null }, error: null },
318
+ updateResult: { data: null, error: null },
319
+ });
320
+ const ctx = createMockContext(supabase);
321
+
322
+ const result = await stopFallbackActivity(
323
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
324
+ ctx
325
+ );
326
+
327
+ expect(result.result).toMatchObject({
328
+ success: true,
329
+ message: 'Fallback activity stopped',
330
+ });
331
+ });
332
+
333
+ it('should include summary in history when provided', async () => {
334
+ const supabase = createMockSupabase({
335
+ selectResult: { data: { current_fallback_activity: 'code_review' }, error: null },
336
+ insertResult: { data: null, error: null },
337
+ updateResult: { data: null, error: null },
338
+ });
339
+ const ctx = createMockContext(supabase);
340
+
341
+ await stopFallbackActivity(
342
+ {
343
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
344
+ summary: 'Reviewed 5 files, found 3 issues',
345
+ },
346
+ ctx
347
+ );
348
+
349
+ expect(supabase.from).toHaveBeenCalledWith('background_activity_history');
350
+ expect(supabase.insert).toHaveBeenCalledWith(
351
+ expect.objectContaining({
352
+ summary: 'Reviewed 5 files, found 3 issues',
353
+ })
354
+ );
355
+ });
356
+
357
+ it('should clear session fallback activity', async () => {
358
+ const supabase = createMockSupabase({
359
+ selectResult: { data: { current_fallback_activity: null }, error: null },
360
+ updateResult: { data: null, error: null },
361
+ });
362
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
363
+
364
+ await stopFallbackActivity(
365
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
366
+ ctx
367
+ );
368
+
369
+ expect(supabase.update).toHaveBeenCalledWith(
370
+ expect.objectContaining({
371
+ current_fallback_activity: null,
372
+ status: 'idle',
373
+ })
374
+ );
375
+ });
376
+ });
377
+
378
+ // ============================================================================
379
+ // getActivityHistory Tests
380
+ // ============================================================================
381
+
382
+ describe('getActivityHistory', () => {
383
+ beforeEach(() => vi.clearAllMocks());
384
+
385
+ it('should throw error for missing project_id', async () => {
386
+ const supabase = createMockSupabase();
387
+ const ctx = createMockContext(supabase);
388
+
389
+ await expect(getActivityHistory({}, ctx)).rejects.toThrow(ValidationError);
390
+ });
391
+
392
+ it('should throw error for invalid project_id UUID', async () => {
393
+ const supabase = createMockSupabase();
394
+ const ctx = createMockContext(supabase);
395
+
396
+ await expect(
397
+ getActivityHistory({ project_id: 'invalid' }, ctx)
398
+ ).rejects.toThrow(ValidationError);
399
+ });
400
+
401
+ it('should return empty history when no activities', async () => {
402
+ const supabase = createMockSupabase({
403
+ selectResult: { data: [], error: null },
404
+ });
405
+ const ctx = createMockContext(supabase);
406
+
407
+ const result = await getActivityHistory(
408
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
409
+ ctx
410
+ );
411
+
412
+ expect(result.result).toMatchObject({
413
+ history: [],
414
+ count: 0,
415
+ });
416
+ });
417
+
418
+ it('should return activity history', async () => {
419
+ const mockHistory = [
420
+ {
421
+ id: 'h1',
422
+ activity_type: 'code_review',
423
+ completed_at: '2025-01-14T10:00:00Z',
424
+ summary: 'Reviewed auth module',
425
+ agent_sessions: { agent_name: 'Wave' },
426
+ },
427
+ {
428
+ id: 'h2',
429
+ activity_type: 'security_review',
430
+ completed_at: '2025-01-14T09:00:00Z',
431
+ summary: null,
432
+ agent_sessions: { agent_name: 'Glitch' },
433
+ },
434
+ ];
435
+
436
+ const supabase = createMockSupabase({
437
+ selectResult: { data: mockHistory, error: null },
438
+ });
439
+ const ctx = createMockContext(supabase);
440
+
441
+ const result = await getActivityHistory(
442
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
443
+ ctx
444
+ );
445
+
446
+ expect((result.result as { history: unknown[] }).history).toHaveLength(2);
447
+ expect((result.result as { count: number }).count).toBe(2);
448
+ });
449
+
450
+ it('should include latest_by_type in response', async () => {
451
+ const mockHistory = [
452
+ {
453
+ id: 'h1',
454
+ activity_type: 'code_review',
455
+ completed_at: '2025-01-14T10:00:00Z',
456
+ agent_sessions: null,
457
+ },
458
+ {
459
+ id: 'h2',
460
+ activity_type: 'code_review',
461
+ completed_at: '2025-01-14T09:00:00Z',
462
+ agent_sessions: null,
463
+ },
464
+ ];
465
+
466
+ const supabase = createMockSupabase({
467
+ selectResult: { data: mockHistory, error: null },
468
+ });
469
+ const ctx = createMockContext(supabase);
470
+
471
+ const result = await getActivityHistory(
472
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
473
+ ctx
474
+ );
475
+
476
+ const latestByType = (result.result as { latest_by_type: Record<string, unknown> }).latest_by_type;
477
+ expect(latestByType['code_review']).toBeDefined();
478
+ expect((latestByType['code_review'] as { id: string }).id).toBe('h1');
479
+ });
480
+
481
+ it('should filter by activity_type when provided', async () => {
482
+ const supabase = createMockSupabase({
483
+ selectResult: { data: [], error: null },
484
+ });
485
+ const ctx = createMockContext(supabase);
486
+
487
+ await getActivityHistory(
488
+ {
489
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
490
+ activity_type: 'security_review',
491
+ },
492
+ ctx
493
+ );
494
+
495
+ expect(supabase.eq).toHaveBeenCalledWith('activity_type', 'security_review');
496
+ });
497
+
498
+ it('should respect limit parameter', async () => {
499
+ const supabase = createMockSupabase({
500
+ selectResult: { data: [], error: null },
501
+ });
502
+ const ctx = createMockContext(supabase);
503
+
504
+ await getActivityHistory(
505
+ {
506
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
507
+ limit: 10,
508
+ },
509
+ ctx
510
+ );
511
+
512
+ expect(supabase.limit).toHaveBeenCalledWith(10);
513
+ });
514
+
515
+ it('should query background_activity_history table', async () => {
516
+ const supabase = createMockSupabase({
517
+ selectResult: { data: [], error: null },
518
+ });
519
+ const ctx = createMockContext(supabase);
520
+
521
+ await getActivityHistory(
522
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
523
+ ctx
524
+ );
525
+
526
+ expect(supabase.from).toHaveBeenCalledWith('background_activity_history');
527
+ });
528
+ });
529
+
530
+ // ============================================================================
531
+ // getActivitySchedules Tests
532
+ // ============================================================================
533
+
534
+ describe('getActivitySchedules', () => {
535
+ beforeEach(() => vi.clearAllMocks());
536
+
537
+ it('should throw error for missing project_id', async () => {
538
+ const supabase = createMockSupabase();
539
+ const ctx = createMockContext(supabase);
540
+
541
+ await expect(getActivitySchedules({}, ctx)).rejects.toThrow(ValidationError);
542
+ });
543
+
544
+ it('should throw error for invalid project_id UUID', async () => {
545
+ const supabase = createMockSupabase();
546
+ const ctx = createMockContext(supabase);
547
+
548
+ await expect(
549
+ getActivitySchedules({ project_id: 'invalid' }, ctx)
550
+ ).rejects.toThrow(ValidationError);
551
+ });
552
+
553
+ it('should return empty schedules when none exist', async () => {
554
+ const supabase = createMockSupabase({
555
+ selectResult: { data: [], error: null },
556
+ });
557
+ const ctx = createMockContext(supabase);
558
+
559
+ const result = await getActivitySchedules(
560
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
561
+ ctx
562
+ );
563
+
564
+ expect(result.result).toMatchObject({
565
+ schedules: [],
566
+ due_activities: [],
567
+ count: 0,
568
+ });
569
+ });
570
+
571
+ it('should return activity schedules', async () => {
572
+ const mockSchedules = [
573
+ {
574
+ id: 's1',
575
+ activity_type: 'code_review',
576
+ schedule_type: 'weekly',
577
+ enabled: true,
578
+ next_run_at: '2025-01-20T10:00:00Z',
579
+ },
580
+ {
581
+ id: 's2',
582
+ activity_type: 'security_review',
583
+ schedule_type: 'monthly',
584
+ enabled: true,
585
+ next_run_at: '2025-02-14T10:00:00Z',
586
+ },
587
+ ];
588
+
589
+ const supabase = createMockSupabase({
590
+ selectResult: { data: mockSchedules, error: null },
591
+ });
592
+ const ctx = createMockContext(supabase);
593
+
594
+ const result = await getActivitySchedules(
595
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
596
+ ctx
597
+ );
598
+
599
+ expect((result.result as { schedules: unknown[] }).schedules).toHaveLength(2);
600
+ expect((result.result as { count: number }).count).toBe(2);
601
+ });
602
+
603
+ it('should identify due activities', async () => {
604
+ const pastDate = new Date(Date.now() - 3600000).toISOString(); // 1 hour ago
605
+ const futureDate = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
606
+
607
+ const mockSchedules = [
608
+ {
609
+ id: 's1',
610
+ activity_type: 'code_review',
611
+ enabled: true,
612
+ next_run_at: pastDate, // Due
613
+ },
614
+ {
615
+ id: 's2',
616
+ activity_type: 'security_review',
617
+ enabled: true,
618
+ next_run_at: futureDate, // Not due
619
+ },
620
+ {
621
+ id: 's3',
622
+ activity_type: 'test_coverage',
623
+ enabled: false, // Disabled
624
+ next_run_at: pastDate,
625
+ },
626
+ ];
627
+
628
+ const supabase = createMockSupabase({
629
+ selectResult: { data: mockSchedules, error: null },
630
+ });
631
+ const ctx = createMockContext(supabase);
632
+
633
+ const result = await getActivitySchedules(
634
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
635
+ ctx
636
+ );
637
+
638
+ const dueActivities = (result.result as { due_activities: string[] }).due_activities;
639
+ expect(dueActivities).toContain('code_review');
640
+ expect(dueActivities).not.toContain('security_review');
641
+ expect(dueActivities).not.toContain('test_coverage');
642
+ });
643
+
644
+ it('should query background_activity_schedules table', async () => {
645
+ const supabase = createMockSupabase({
646
+ selectResult: { data: [], error: null },
647
+ });
648
+ const ctx = createMockContext(supabase);
649
+
650
+ await getActivitySchedules(
651
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
652
+ ctx
653
+ );
654
+
655
+ expect(supabase.from).toHaveBeenCalledWith('background_activity_schedules');
656
+ });
657
+
658
+ it('should order schedules by activity_type', async () => {
659
+ const supabase = createMockSupabase({
660
+ selectResult: { data: [], error: null },
661
+ });
662
+ const ctx = createMockContext(supabase);
663
+
664
+ await getActivitySchedules(
665
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
666
+ ctx
667
+ );
668
+
669
+ expect(supabase.order).toHaveBeenCalledWith('activity_type');
670
+ });
671
+
672
+ it('should throw error when query fails', async () => {
673
+ const supabase = createMockSupabase();
674
+ const ctx = createMockContext(supabase);
675
+
676
+ // Override to return error
677
+ vi.mocked(supabase.from('').select).mockReturnValue({
678
+ ...supabase,
679
+ then: (resolve: (val: unknown) => void) =>
680
+ Promise.resolve({ data: null, error: { message: 'Query failed' } }).then(resolve),
681
+ } as unknown as ReturnType<SupabaseClient['from']>);
682
+
683
+ await expect(
684
+ getActivitySchedules({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
685
+ ).rejects.toThrow();
686
+ });
687
+ });