@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.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1114 -0
- package/dist/api-client.js +698 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +106 -476
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +112 -828
- package/dist/handlers/discovery.js +31 -0
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +39 -134
- package/dist/handlers/findings.js +43 -67
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +99 -585
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +274 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +230 -900
- package/dist/handlers/tool-docs.d.ts +8 -0
- package/dist/handlers/tool-docs.js +657 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +26 -153
- package/dist/index.js +473 -160
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +4 -0
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1752 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +210 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +194 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +140 -895
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +32 -0
- package/src/handlers/fallback.test.ts +128 -361
- package/src/handlers/fallback.ts +62 -148
- package/src/handlers/findings.test.ts +127 -345
- package/src/handlers/findings.ts +49 -66
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +241 -206
- package/src/handlers/session.ts +110 -657
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +497 -0
- package/src/handlers/tasks.test.ts +608 -353
- package/src/handlers/tasks.ts +248 -1025
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +189 -572
- package/src/handlers/validation.ts +29 -166
- package/src/index.ts +473 -184
- package/src/knowledge.ts +107 -9
- package/src/tools.ts +2506 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- 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
|
|
146
|
-
|
|
147
|
-
|
|
20
|
+
const ctx = createMockContext();
|
|
21
|
+
mockApiClient.heartbeat.mockResolvedValue({
|
|
22
|
+
ok: true,
|
|
23
|
+
data: { timestamp: '2026-01-14T10:00:00Z' },
|
|
148
24
|
});
|
|
149
|
-
|
|
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(
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
38
|
+
const ctx = createMockContext();
|
|
39
|
+
mockApiClient.heartbeat.mockResolvedValue({
|
|
40
|
+
ok: true,
|
|
41
|
+
data: { timestamp: '2026-01-14T10:00:00Z' },
|
|
166
42
|
});
|
|
167
|
-
|
|
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
|
|
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
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
82
|
+
mockApiClient.syncSession.mockResolvedValue({ ok: true });
|
|
194
83
|
|
|
195
84
|
await heartbeat({}, ctx);
|
|
196
85
|
|
|
197
|
-
expect(
|
|
198
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
+
});
|