@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.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -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 +108 -477
- 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 +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- 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 +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -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 +210 -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 +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- 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 +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -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/knowledge.ts +0 -132
- 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', { current_worktree_path: undefined });
|
|
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,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
|
|
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
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
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(
|
|
198
|
-
|
|
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
|
|
218
|
-
|
|
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
|
|
228
|
-
|
|
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
|
|
238
|
-
|
|
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
|
|
250
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
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
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
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
|
+
});
|