@vibescope/mcp-server 0.0.1 → 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.
Files changed (170) hide show
  1. package/README.md +113 -98
  2. package/dist/api-client.d.ts +1114 -0
  3. package/dist/api-client.js +698 -0
  4. package/dist/cli.d.ts +1 -6
  5. package/dist/cli.js +39 -240
  6. package/dist/config/tool-categories.d.ts +31 -0
  7. package/dist/config/tool-categories.js +253 -0
  8. package/dist/handlers/blockers.js +57 -58
  9. package/dist/handlers/bodies-of-work.d.ts +2 -0
  10. package/dist/handlers/bodies-of-work.js +106 -476
  11. package/dist/handlers/cost.d.ts +1 -0
  12. package/dist/handlers/cost.js +35 -113
  13. package/dist/handlers/decisions.d.ts +2 -0
  14. package/dist/handlers/decisions.js +28 -27
  15. package/dist/handlers/deployment.js +112 -828
  16. package/dist/handlers/discovery.js +31 -0
  17. package/dist/handlers/fallback.d.ts +2 -0
  18. package/dist/handlers/fallback.js +39 -134
  19. package/dist/handlers/findings.js +43 -67
  20. package/dist/handlers/git-issues.d.ts +9 -13
  21. package/dist/handlers/git-issues.js +80 -225
  22. package/dist/handlers/ideas.d.ts +3 -0
  23. package/dist/handlers/ideas.js +53 -134
  24. package/dist/handlers/index.d.ts +2 -0
  25. package/dist/handlers/index.js +6 -0
  26. package/dist/handlers/milestones.d.ts +2 -0
  27. package/dist/handlers/milestones.js +51 -98
  28. package/dist/handlers/organizations.js +79 -275
  29. package/dist/handlers/progress.d.ts +2 -0
  30. package/dist/handlers/progress.js +25 -123
  31. package/dist/handlers/project.js +42 -221
  32. package/dist/handlers/requests.d.ts +2 -0
  33. package/dist/handlers/requests.js +23 -83
  34. package/dist/handlers/session.js +99 -585
  35. package/dist/handlers/sprints.d.ts +32 -0
  36. package/dist/handlers/sprints.js +274 -0
  37. package/dist/handlers/tasks.d.ts +7 -10
  38. package/dist/handlers/tasks.js +230 -900
  39. package/dist/handlers/tool-docs.d.ts +8 -0
  40. package/dist/handlers/tool-docs.js +657 -0
  41. package/dist/handlers/types.d.ts +11 -3
  42. package/dist/handlers/validation.d.ts +1 -1
  43. package/dist/handlers/validation.js +26 -153
  44. package/dist/index.js +473 -160
  45. package/dist/knowledge.js +106 -9
  46. package/dist/tools.js +4 -0
  47. package/dist/validators.d.ts +21 -0
  48. package/dist/validators.js +91 -0
  49. package/package.json +2 -3
  50. package/src/api-client.ts +1752 -0
  51. package/src/cli.test.ts +128 -302
  52. package/src/cli.ts +41 -285
  53. package/src/handlers/__test-setup__.ts +210 -0
  54. package/src/handlers/__test-utils__.ts +4 -134
  55. package/src/handlers/blockers.test.ts +114 -124
  56. package/src/handlers/blockers.ts +68 -70
  57. package/src/handlers/bodies-of-work.test.ts +236 -831
  58. package/src/handlers/bodies-of-work.ts +194 -525
  59. package/src/handlers/cost.test.ts +149 -113
  60. package/src/handlers/cost.ts +44 -132
  61. package/src/handlers/decisions.test.ts +111 -209
  62. package/src/handlers/decisions.ts +35 -27
  63. package/src/handlers/deployment.test.ts +193 -239
  64. package/src/handlers/deployment.ts +140 -895
  65. package/src/handlers/discovery.test.ts +20 -67
  66. package/src/handlers/discovery.ts +32 -0
  67. package/src/handlers/fallback.test.ts +128 -361
  68. package/src/handlers/fallback.ts +62 -148
  69. package/src/handlers/findings.test.ts +127 -345
  70. package/src/handlers/findings.ts +49 -66
  71. package/src/handlers/git-issues.test.ts +623 -0
  72. package/src/handlers/git-issues.ts +174 -0
  73. package/src/handlers/ideas.test.ts +229 -343
  74. package/src/handlers/ideas.ts +69 -143
  75. package/src/handlers/index.ts +6 -0
  76. package/src/handlers/milestones.test.ts +167 -281
  77. package/src/handlers/milestones.ts +54 -93
  78. package/src/handlers/organizations.test.ts +275 -467
  79. package/src/handlers/organizations.ts +84 -294
  80. package/src/handlers/progress.test.ts +112 -218
  81. package/src/handlers/progress.ts +29 -142
  82. package/src/handlers/project.test.ts +203 -226
  83. package/src/handlers/project.ts +48 -238
  84. package/src/handlers/requests.test.ts +74 -342
  85. package/src/handlers/requests.ts +25 -83
  86. package/src/handlers/session.test.ts +241 -206
  87. package/src/handlers/session.ts +110 -657
  88. package/src/handlers/sprints.test.ts +711 -0
  89. package/src/handlers/sprints.ts +497 -0
  90. package/src/handlers/tasks.test.ts +608 -353
  91. package/src/handlers/tasks.ts +248 -1025
  92. package/src/handlers/types.ts +12 -4
  93. package/src/handlers/validation.test.ts +189 -572
  94. package/src/handlers/validation.ts +29 -166
  95. package/src/index.ts +473 -184
  96. package/src/knowledge.ts +107 -9
  97. package/src/tools.ts +2506 -0
  98. package/src/validators.test.ts +223 -223
  99. package/src/validators.ts +127 -0
  100. package/tsconfig.json +1 -1
  101. package/vitest.config.ts +14 -13
  102. package/dist/cli.test.d.ts +0 -1
  103. package/dist/cli.test.js +0 -367
  104. package/dist/handlers/__test-utils__.d.ts +0 -72
  105. package/dist/handlers/__test-utils__.js +0 -176
  106. package/dist/handlers/checkouts.d.ts +0 -37
  107. package/dist/handlers/checkouts.js +0 -377
  108. package/dist/handlers/knowledge-query.d.ts +0 -22
  109. package/dist/handlers/knowledge-query.js +0 -253
  110. package/dist/handlers/knowledge.d.ts +0 -12
  111. package/dist/handlers/knowledge.js +0 -108
  112. package/dist/handlers/roles.d.ts +0 -30
  113. package/dist/handlers/roles.js +0 -281
  114. package/dist/handlers/tasks.test.d.ts +0 -1
  115. package/dist/handlers/tasks.test.js +0 -431
  116. package/dist/utils.test.d.ts +0 -1
  117. package/dist/utils.test.js +0 -532
  118. package/dist/validators.test.d.ts +0 -1
  119. package/dist/validators.test.js +0 -176
  120. package/src/tmpclaude-0078-cwd +0 -1
  121. package/src/tmpclaude-0ee1-cwd +0 -1
  122. package/src/tmpclaude-2dd5-cwd +0 -1
  123. package/src/tmpclaude-344c-cwd +0 -1
  124. package/src/tmpclaude-3860-cwd +0 -1
  125. package/src/tmpclaude-4b63-cwd +0 -1
  126. package/src/tmpclaude-5c73-cwd +0 -1
  127. package/src/tmpclaude-5ee3-cwd +0 -1
  128. package/src/tmpclaude-6795-cwd +0 -1
  129. package/src/tmpclaude-709e-cwd +0 -1
  130. package/src/tmpclaude-9839-cwd +0 -1
  131. package/src/tmpclaude-d829-cwd +0 -1
  132. package/src/tmpclaude-e072-cwd +0 -1
  133. package/src/tmpclaude-f6ee-cwd +0 -1
  134. package/tmpclaude-0439-cwd +0 -1
  135. package/tmpclaude-132f-cwd +0 -1
  136. package/tmpclaude-15bb-cwd +0 -1
  137. package/tmpclaude-165a-cwd +0 -1
  138. package/tmpclaude-1ba9-cwd +0 -1
  139. package/tmpclaude-21a3-cwd +0 -1
  140. package/tmpclaude-2a38-cwd +0 -1
  141. package/tmpclaude-2adf-cwd +0 -1
  142. package/tmpclaude-2f56-cwd +0 -1
  143. package/tmpclaude-3626-cwd +0 -1
  144. package/tmpclaude-3727-cwd +0 -1
  145. package/tmpclaude-40bc-cwd +0 -1
  146. package/tmpclaude-436f-cwd +0 -1
  147. package/tmpclaude-4783-cwd +0 -1
  148. package/tmpclaude-4b6d-cwd +0 -1
  149. package/tmpclaude-4ba4-cwd +0 -1
  150. package/tmpclaude-51e6-cwd +0 -1
  151. package/tmpclaude-5ecf-cwd +0 -1
  152. package/tmpclaude-6f97-cwd +0 -1
  153. package/tmpclaude-7fb2-cwd +0 -1
  154. package/tmpclaude-825c-cwd +0 -1
  155. package/tmpclaude-8baf-cwd +0 -1
  156. package/tmpclaude-8d9f-cwd +0 -1
  157. package/tmpclaude-975c-cwd +0 -1
  158. package/tmpclaude-9983-cwd +0 -1
  159. package/tmpclaude-a045-cwd +0 -1
  160. package/tmpclaude-ac4a-cwd +0 -1
  161. package/tmpclaude-b593-cwd +0 -1
  162. package/tmpclaude-b891-cwd +0 -1
  163. package/tmpclaude-c032-cwd +0 -1
  164. package/tmpclaude-cf43-cwd +0 -1
  165. package/tmpclaude-d040-cwd +0 -1
  166. package/tmpclaude-dcdd-cwd +0 -1
  167. package/tmpclaude-dcee-cwd +0 -1
  168. package/tmpclaude-e16b-cwd +0 -1
  169. package/tmpclaude-ecd2-cwd +0 -1
  170. package/tmpclaude-f48d-cwd +0 -1
@@ -1,152 +1,28 @@
1
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
2
  import {
3
+ startWorkSession,
5
4
  heartbeat,
6
5
  endWorkSession,
7
6
  getHelp,
8
7
  getTokenUsage,
9
8
  } from './session.js';
10
-
11
- // ============================================================================
12
- // Test Utilities
13
- // ============================================================================
14
-
15
- /**
16
- * Creates a mock Supabase client with chainable methods
17
- */
18
- function createMockSupabase(overrides: {
19
- selectResult?: { data: unknown; error: unknown };
20
- insertResult?: { data: unknown; error: unknown };
21
- updateResult?: { data: unknown; error: unknown };
22
- } = {}) {
23
- const defaultResult = { data: null, error: null };
24
-
25
- let currentOperation = 'select';
26
- let insertThenSelect = false;
27
-
28
- const mock = {
29
- from: vi.fn().mockReturnThis(),
30
- select: vi.fn(() => {
31
- if (currentOperation === 'insert') {
32
- insertThenSelect = true;
33
- } else {
34
- currentOperation = 'select';
35
- insertThenSelect = false;
36
- }
37
- return mock;
38
- }),
39
- insert: vi.fn(() => {
40
- currentOperation = 'insert';
41
- insertThenSelect = false;
42
- return mock;
43
- }),
44
- update: vi.fn(() => {
45
- currentOperation = 'update';
46
- insertThenSelect = false;
47
- return mock;
48
- }),
49
- delete: vi.fn(() => {
50
- currentOperation = 'delete';
51
- insertThenSelect = false;
52
- return mock;
53
- }),
54
- eq: vi.fn().mockReturnThis(),
55
- neq: vi.fn().mockReturnThis(),
56
- in: vi.fn().mockReturnThis(),
57
- is: vi.fn().mockReturnThis(),
58
- not: vi.fn().mockReturnThis(),
59
- or: vi.fn().mockReturnThis(),
60
- gte: 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
- return Promise.resolve(defaultResult).then(resolve);
90
- }),
91
- };
92
-
93
- return mock as unknown as SupabaseClient;
94
- }
95
-
96
- /**
97
- * Creates a mock handler context with full session state
98
- */
99
- function createMockContext(
100
- supabase: SupabaseClient,
101
- options: {
102
- sessionId?: string | null;
103
- tokenUsage?: TokenUsage;
104
- } = {}
105
- ): HandlerContext {
106
- const defaultTokenUsage: TokenUsage = {
107
- callCount: 10,
108
- totalTokens: 5000,
109
- byTool: {
110
- get_tasks: { calls: 3, tokens: 1500 },
111
- update_task: { calls: 4, tokens: 2000 },
112
- complete_task: { calls: 3, tokens: 1500 },
113
- },
114
- };
115
-
116
- // Use 'in' check to allow explicit null values for sessionId
117
- const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
118
-
119
- return {
120
- supabase,
121
- auth: {
122
- userId: 'user-123',
123
- apiKeyId: 'api-key-123',
124
- },
125
- session: {
126
- instanceId: 'instance-abc123',
127
- currentSessionId: sessionId,
128
- currentPersona: 'Wave',
129
- tokenUsage: options.tokenUsage ?? defaultTokenUsage,
130
- },
131
- updateSession: vi.fn(),
132
- };
133
- }
9
+ import { createMockContext } from './__test-utils__.js';
10
+ import { mockApiClient } from './__test-setup__.js';
134
11
 
135
12
  // ============================================================================
136
13
  // heartbeat Tests
137
14
  // ============================================================================
138
15
 
139
16
  describe('heartbeat', () => {
140
- beforeEach(() => {
141
- vi.clearAllMocks();
142
- });
17
+ beforeEach(() => vi.clearAllMocks());
143
18
 
144
19
  it('should record heartbeat successfully', async () => {
145
- const supabase = createMockSupabase({
146
- insertResult: { data: null, error: null },
147
- updateResult: { data: null, error: null },
20
+ const ctx = createMockContext();
21
+ mockApiClient.heartbeat.mockResolvedValue({
22
+ ok: true,
23
+ data: { timestamp: '2026-01-14T10:00:00Z' },
148
24
  });
149
- const ctx = createMockContext(supabase);
25
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
150
26
 
151
27
  const result = await heartbeat({}, ctx);
152
28
 
@@ -155,16 +31,16 @@ describe('heartbeat', () => {
155
31
  session_id: 'session-123',
156
32
  });
157
33
  expect(result.result).toHaveProperty('timestamp');
158
- expect(supabase.from).toHaveBeenCalledWith('agent_heartbeats');
159
- expect(supabase.insert).toHaveBeenCalled();
34
+ expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123');
160
35
  });
161
36
 
162
37
  it('should use provided session_id over current session', async () => {
163
- const supabase = createMockSupabase({
164
- insertResult: { data: null, error: null },
165
- updateResult: { data: null, error: null },
38
+ const ctx = createMockContext();
39
+ mockApiClient.heartbeat.mockResolvedValue({
40
+ ok: true,
41
+ data: { timestamp: '2026-01-14T10:00:00Z' },
166
42
  });
167
- const ctx = createMockContext(supabase);
43
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
168
44
 
169
45
  const result = await heartbeat({ session_id: 'other-session-456' }, ctx);
170
46
 
@@ -172,11 +48,11 @@ describe('heartbeat', () => {
172
48
  success: true,
173
49
  session_id: 'other-session-456',
174
50
  });
51
+ expect(mockApiClient.heartbeat).toHaveBeenCalledWith('other-session-456');
175
52
  });
176
53
 
177
54
  it('should return error when no active session', async () => {
178
- const supabase = createMockSupabase();
179
- const ctx = createMockContext(supabase, { sessionId: null });
55
+ const ctx = createMockContext({ sessionId: null });
180
56
 
181
57
  const result = await heartbeat({}, ctx);
182
58
 
@@ -185,19 +61,31 @@ describe('heartbeat', () => {
185
61
  });
186
62
  });
187
63
 
188
- it('should update session with token usage', async () => {
189
- const supabase = createMockSupabase({
190
- insertResult: { data: null, error: null },
191
- updateResult: { data: null, error: null },
64
+ it('should sync session with token usage', async () => {
65
+ const ctx = createMockContext({
66
+ tokenUsage: {
67
+ callCount: 10,
68
+ totalTokens: 5000,
69
+ byTool: {
70
+ get_tasks: { calls: 3, tokens: 1500 },
71
+ update_task: { calls: 4, tokens: 2000 },
72
+ complete_task: { calls: 3, tokens: 1500 },
73
+ },
74
+ byModel: {},
75
+ currentModel: null,
76
+ },
77
+ });
78
+ mockApiClient.heartbeat.mockResolvedValue({
79
+ ok: true,
80
+ data: { timestamp: '2026-01-14T10:00:00Z' },
192
81
  });
193
- const ctx = createMockContext(supabase);
82
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
194
83
 
195
84
  await heartbeat({}, ctx);
196
85
 
197
- expect(supabase.from).toHaveBeenCalledWith('agent_sessions');
198
- expect(supabase.update).toHaveBeenCalledWith(
86
+ expect(mockApiClient.syncSession).toHaveBeenCalledWith(
87
+ 'session-123',
199
88
  expect.objectContaining({
200
- status: 'active',
201
89
  total_tokens: 5000,
202
90
  })
203
91
  );
@@ -209,13 +97,10 @@ describe('heartbeat', () => {
209
97
  // ============================================================================
210
98
 
211
99
  describe('getHelp', () => {
212
- beforeEach(() => {
213
- vi.clearAllMocks();
214
- });
100
+ beforeEach(() => vi.clearAllMocks());
215
101
 
216
102
  it('should return help content for valid topic', async () => {
217
- const supabase = createMockSupabase();
218
- const ctx = createMockContext(supabase);
103
+ const ctx = createMockContext();
219
104
 
220
105
  const result = await getHelp({ topic: 'tasks' }, ctx);
221
106
 
@@ -224,8 +109,7 @@ describe('getHelp', () => {
224
109
  });
225
110
 
226
111
  it('should return getting_started help', async () => {
227
- const supabase = createMockSupabase();
228
- const ctx = createMockContext(supabase);
112
+ const ctx = createMockContext();
229
113
 
230
114
  const result = await getHelp({ topic: 'getting_started' }, ctx);
231
115
 
@@ -234,8 +118,7 @@ describe('getHelp', () => {
234
118
  });
235
119
 
236
120
  it('should return error for unknown topic', async () => {
237
- const supabase = createMockSupabase();
238
- const ctx = createMockContext(supabase);
121
+ const ctx = createMockContext();
239
122
 
240
123
  const result = await getHelp({ topic: 'unknown_topic' }, ctx);
241
124
 
@@ -246,8 +129,7 @@ describe('getHelp', () => {
246
129
  });
247
130
 
248
131
  it('should list available topics for unknown topic', async () => {
249
- const supabase = createMockSupabase();
250
- const ctx = createMockContext(supabase);
132
+ const ctx = createMockContext();
251
133
 
252
134
  const result = await getHelp({ topic: 'nonexistent' }, ctx);
253
135
 
@@ -262,13 +144,10 @@ describe('getHelp', () => {
262
144
  // ============================================================================
263
145
 
264
146
  describe('getTokenUsage', () => {
265
- beforeEach(() => {
266
- vi.clearAllMocks();
267
- });
147
+ beforeEach(() => vi.clearAllMocks());
268
148
 
269
149
  it('should return token usage stats', async () => {
270
- const supabase = createMockSupabase();
271
- const ctx = createMockContext(supabase);
150
+ const ctx = createMockContext();
272
151
 
273
152
  const result = await getTokenUsage({}, ctx);
274
153
 
@@ -278,12 +157,13 @@ describe('getTokenUsage', () => {
278
157
  });
279
158
 
280
159
  it('should return correct session stats', async () => {
281
- const supabase = createMockSupabase();
282
- const ctx = createMockContext(supabase, {
160
+ const ctx = createMockContext({
283
161
  tokenUsage: {
284
162
  callCount: 10,
285
163
  totalTokens: 5000,
286
164
  byTool: {},
165
+ byModel: {},
166
+ currentModel: null,
287
167
  },
288
168
  });
289
169
 
@@ -296,8 +176,7 @@ describe('getTokenUsage', () => {
296
176
  });
297
177
 
298
178
  it('should return top tools sorted by tokens', async () => {
299
- const supabase = createMockSupabase();
300
- const ctx = createMockContext(supabase, {
179
+ const ctx = createMockContext({
301
180
  tokenUsage: {
302
181
  callCount: 10,
303
182
  totalTokens: 5000,
@@ -306,6 +185,8 @@ describe('getTokenUsage', () => {
306
185
  tool_b: { calls: 5, tokens: 3000 },
307
186
  tool_c: { calls: 3, tokens: 1000 },
308
187
  },
188
+ byModel: {},
189
+ currentModel: null,
309
190
  },
310
191
  });
311
192
 
@@ -318,12 +199,13 @@ describe('getTokenUsage', () => {
318
199
  });
319
200
 
320
201
  it('should handle zero calls gracefully', async () => {
321
- const supabase = createMockSupabase();
322
- const ctx = createMockContext(supabase, {
202
+ const ctx = createMockContext({
323
203
  tokenUsage: {
324
204
  callCount: 0,
325
205
  totalTokens: 0,
326
206
  byTool: {},
207
+ byModel: {},
208
+ currentModel: null,
327
209
  },
328
210
  });
329
211
 
@@ -336,8 +218,7 @@ describe('getTokenUsage', () => {
336
218
  });
337
219
 
338
220
  it('should limit top_tools to 5', async () => {
339
- const supabase = createMockSupabase();
340
- const ctx = createMockContext(supabase, {
221
+ const ctx = createMockContext({
341
222
  tokenUsage: {
342
223
  callCount: 20,
343
224
  totalTokens: 10000,
@@ -350,6 +231,8 @@ describe('getTokenUsage', () => {
350
231
  tool_6: { calls: 6, tokens: 600 },
351
232
  tool_7: { calls: 7, tokens: 700 },
352
233
  },
234
+ byModel: {},
235
+ currentModel: null,
353
236
  },
354
237
  });
355
238
 
@@ -365,13 +248,10 @@ describe('getTokenUsage', () => {
365
248
  // ============================================================================
366
249
 
367
250
  describe('endWorkSession', () => {
368
- beforeEach(() => {
369
- vi.clearAllMocks();
370
- });
251
+ beforeEach(() => vi.clearAllMocks());
371
252
 
372
253
  it('should handle no active session gracefully', async () => {
373
- const supabase = createMockSupabase();
374
- const ctx = createMockContext(supabase, { sessionId: null });
254
+ const ctx = createMockContext({ sessionId: null });
375
255
 
376
256
  const result = await endWorkSession({}, ctx);
377
257
 
@@ -382,35 +262,40 @@ describe('endWorkSession', () => {
382
262
  });
383
263
 
384
264
  it('should use provided session_id over current session', async () => {
385
- const supabase = createMockSupabase({
386
- selectResult: {
387
- data: {
388
- project_id: 'project-123',
265
+ const ctx = createMockContext();
266
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
267
+ mockApiClient.endSession.mockResolvedValue({
268
+ ok: true,
269
+ data: {
270
+ session_summary: {
389
271
  agent_name: 'Wave',
390
- started_at: new Date().toISOString()
272
+ tasks_completed_this_session: 3,
273
+ tasks_awaiting_validation: 1,
274
+ tasks_released: 0,
391
275
  },
392
- error: null
393
276
  },
394
277
  });
395
- const ctx = createMockContext(supabase);
396
278
 
397
279
  const result = await endWorkSession({ session_id: 'other-session-456' }, ctx);
398
280
 
399
281
  expect(result.result).toHaveProperty('ended_session_id', 'other-session-456');
282
+ expect(mockApiClient.endSession).toHaveBeenCalledWith('other-session-456');
400
283
  });
401
284
 
402
285
  it('should call updateSession to clear current session', async () => {
403
- const supabase = createMockSupabase({
404
- selectResult: {
405
- data: {
406
- project_id: 'project-123',
286
+ const ctx = createMockContext();
287
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
288
+ mockApiClient.endSession.mockResolvedValue({
289
+ ok: true,
290
+ data: {
291
+ session_summary: {
407
292
  agent_name: 'Wave',
408
- started_at: new Date().toISOString()
293
+ tasks_completed_this_session: 0,
294
+ tasks_awaiting_validation: 0,
295
+ tasks_released: 0,
409
296
  },
410
- error: null
411
297
  },
412
298
  });
413
- const ctx = createMockContext(supabase);
414
299
 
415
300
  await endWorkSession({}, ctx);
416
301
 
@@ -418,17 +303,19 @@ describe('endWorkSession', () => {
418
303
  });
419
304
 
420
305
  it('should return session summary', async () => {
421
- const supabase = createMockSupabase({
422
- selectResult: {
423
- data: {
424
- project_id: 'project-123',
306
+ const ctx = createMockContext();
307
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
308
+ mockApiClient.endSession.mockResolvedValue({
309
+ ok: true,
310
+ data: {
311
+ session_summary: {
425
312
  agent_name: 'Wave',
426
- started_at: new Date().toISOString()
313
+ tasks_completed_this_session: 2,
314
+ tasks_awaiting_validation: 1,
315
+ tasks_released: 0,
427
316
  },
428
- error: null
429
317
  },
430
318
  });
431
- const ctx = createMockContext(supabase);
432
319
 
433
320
  const result = await endWorkSession({}, ctx);
434
321
 
@@ -439,17 +326,19 @@ describe('endWorkSession', () => {
439
326
  });
440
327
 
441
328
  it('should not call updateSession when ending a different session', async () => {
442
- const supabase = createMockSupabase({
443
- selectResult: {
444
- data: {
445
- project_id: 'project-123',
329
+ const ctx = createMockContext({ sessionId: 'session-123' });
330
+ mockApiClient.syncSession.mockResolvedValue({ ok: true });
331
+ mockApiClient.endSession.mockResolvedValue({
332
+ ok: true,
333
+ data: {
334
+ session_summary: {
446
335
  agent_name: 'Wave',
447
- started_at: new Date().toISOString()
336
+ tasks_completed_this_session: 0,
337
+ tasks_awaiting_validation: 0,
338
+ tasks_released: 0,
448
339
  },
449
- error: null
450
340
  },
451
341
  });
452
- const ctx = createMockContext(supabase, { sessionId: 'session-123' });
453
342
 
454
343
  await endWorkSession({ session_id: 'different-session' }, ctx);
455
344
 
@@ -457,3 +346,149 @@ describe('endWorkSession', () => {
457
346
  expect(ctx.updateSession).not.toHaveBeenCalledWith({ currentSessionId: null });
458
347
  });
459
348
  });
349
+
350
+ // ============================================================================
351
+ // startWorkSession Tests
352
+ // ============================================================================
353
+
354
+ describe('startWorkSession', () => {
355
+ beforeEach(() => {
356
+ vi.clearAllMocks();
357
+ });
358
+
359
+ it('should return error when project_id and git_url are both missing', async () => {
360
+ const ctx = createMockContext({ sessionId: null });
361
+
362
+ const result = await startWorkSession({}, ctx);
363
+
364
+ expect(result.result).toMatchObject({
365
+ error: 'Please provide project_id or git_url to start a session',
366
+ });
367
+ });
368
+
369
+ it('should return project_not_found when no project matches', async () => {
370
+ const ctx = createMockContext({ sessionId: null });
371
+ mockApiClient.startSession.mockResolvedValue({
372
+ ok: true,
373
+ data: {
374
+ session_started: false,
375
+ project_not_found: true,
376
+ message: 'No project found',
377
+ suggestion: {
378
+ action: 'create_project',
379
+ example: 'create_project(name: "repo")',
380
+ note: 'After creating the project, call start_work_session again.',
381
+ },
382
+ },
383
+ });
384
+
385
+ const result = await startWorkSession({ git_url: 'https://github.com/test/repo' }, ctx);
386
+
387
+ expect(result.result).toMatchObject({
388
+ session_started: false,
389
+ project_not_found: true,
390
+ });
391
+ expect(result.result).toHaveProperty('suggestion');
392
+ });
393
+
394
+ it('should reuse existing persona when session already has one', async () => {
395
+ const ctx = createMockContext({ sessionId: null });
396
+ mockApiClient.startSession.mockResolvedValue({
397
+ ok: true,
398
+ data: {
399
+ session_started: true,
400
+ session_id: 'existing-session-123',
401
+ persona: 'Pixel',
402
+ role: 'developer',
403
+ project: { id: 'project-123', name: 'Test Project' },
404
+ },
405
+ });
406
+
407
+ const result = await startWorkSession({ project_id: 'project-123' }, ctx);
408
+
409
+ expect(result.result).toMatchObject({
410
+ session_started: true,
411
+ persona: 'Pixel',
412
+ });
413
+ });
414
+
415
+ it('should call updateSession with session ID and persona', async () => {
416
+ const ctx = createMockContext({ sessionId: null });
417
+ mockApiClient.startSession.mockResolvedValue({
418
+ ok: true,
419
+ data: {
420
+ session_started: true,
421
+ session_id: 'new-session-123',
422
+ persona: 'Wave',
423
+ role: 'developer',
424
+ project: { id: 'project-123', name: 'Test Project' },
425
+ },
426
+ });
427
+
428
+ await startWorkSession({ project_id: 'project-123' }, ctx);
429
+
430
+ expect(ctx.updateSession).toHaveBeenCalledWith(
431
+ expect.objectContaining({
432
+ currentSessionId: 'new-session-123',
433
+ currentPersona: 'Wave',
434
+ })
435
+ );
436
+ });
437
+
438
+ it('should return lite mode response by default', async () => {
439
+ const ctx = createMockContext({ sessionId: null });
440
+ mockApiClient.startSession.mockResolvedValue({
441
+ ok: true,
442
+ data: {
443
+ session_started: true,
444
+ session_id: 'new-session-123',
445
+ persona: 'Wave',
446
+ role: 'developer',
447
+ project: { id: 'project-123', name: 'Test Project' },
448
+ directive: 'ACTION_REQUIRED: Start working immediately.',
449
+ },
450
+ });
451
+
452
+ const result = await startWorkSession({ project_id: 'project-123' }, ctx);
453
+
454
+ expect(result.result).toMatchObject({
455
+ session_started: true,
456
+ });
457
+ expect(result.result).toHaveProperty('project');
458
+ expect(result.result).toHaveProperty('directive');
459
+ });
460
+
461
+ it('should return error when API call fails', async () => {
462
+ const ctx = createMockContext({ sessionId: null });
463
+ mockApiClient.startSession.mockResolvedValue({
464
+ ok: false,
465
+ error: 'Internal server error',
466
+ });
467
+
468
+ const result = await startWorkSession({ project_id: 'project-123' }, ctx);
469
+
470
+ expect(result.result).toMatchObject({
471
+ error: 'Internal server error',
472
+ });
473
+ });
474
+
475
+ it('should include next_task when available', async () => {
476
+ const ctx = createMockContext({ sessionId: null });
477
+ mockApiClient.startSession.mockResolvedValue({
478
+ ok: true,
479
+ data: {
480
+ session_started: true,
481
+ session_id: 'new-session-123',
482
+ persona: 'Wave',
483
+ role: 'developer',
484
+ project: { id: 'project-123', name: 'Test Project' },
485
+ next_task: { id: 'task-1', title: 'Fix bug', priority: 1 },
486
+ },
487
+ });
488
+
489
+ const result = await startWorkSession({ project_id: 'project-123' }, ctx);
490
+
491
+ expect(result.result).toHaveProperty('next_task');
492
+ expect((result.result as { next_task: { id: string } }).next_task.id).toBe('task-1');
493
+ });
494
+ });