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