@vibescope/mcp-server 0.2.2 → 0.2.4
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/CHANGELOG.md +84 -0
- package/README.md +35 -20
- package/dist/api-client.d.ts +276 -8
- package/dist/api-client.js +128 -9
- package/dist/handlers/blockers.d.ts +11 -0
- package/dist/handlers/blockers.js +37 -2
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +30 -1
- package/dist/handlers/connectors.js +2 -2
- package/dist/handlers/decisions.d.ts +11 -0
- package/dist/handlers/decisions.js +37 -2
- package/dist/handlers/deployment.d.ts +6 -0
- package/dist/handlers/deployment.js +33 -5
- package/dist/handlers/discovery.js +27 -11
- package/dist/handlers/fallback.js +12 -6
- package/dist/handlers/file-checkouts.d.ts +1 -0
- package/dist/handlers/file-checkouts.js +17 -2
- package/dist/handlers/findings.d.ts +5 -0
- package/dist/handlers/findings.js +19 -2
- package/dist/handlers/git-issues.js +4 -2
- package/dist/handlers/ideas.d.ts +5 -0
- package/dist/handlers/ideas.js +19 -2
- package/dist/handlers/progress.js +2 -2
- package/dist/handlers/project.d.ts +1 -0
- package/dist/handlers/project.js +35 -2
- package/dist/handlers/requests.js +6 -3
- package/dist/handlers/roles.js +13 -2
- package/dist/handlers/session.d.ts +12 -0
- package/dist/handlers/session.js +288 -25
- package/dist/handlers/sprints.d.ts +2 -0
- package/dist/handlers/sprints.js +30 -1
- package/dist/handlers/tasks.d.ts +25 -2
- package/dist/handlers/tasks.js +228 -35
- package/dist/handlers/tool-docs.js +72 -5
- package/dist/templates/agent-guidelines.d.ts +18 -0
- package/dist/templates/agent-guidelines.js +207 -0
- package/dist/tools.js +478 -125
- package/dist/utils.d.ts +5 -2
- package/dist/utils.js +90 -51
- package/package.json +51 -46
- package/scripts/version-bump.ts +203 -0
- package/src/api-client.test.ts +8 -3
- package/src/api-client.ts +376 -13
- package/src/handlers/__test-setup__.ts +5 -0
- package/src/handlers/blockers.test.ts +76 -0
- package/src/handlers/blockers.ts +56 -2
- package/src/handlers/bodies-of-work.ts +59 -1
- package/src/handlers/connectors.ts +2 -2
- package/src/handlers/decisions.test.ts +71 -2
- package/src/handlers/decisions.ts +56 -2
- package/src/handlers/deployment.test.ts +81 -0
- package/src/handlers/deployment.ts +38 -5
- package/src/handlers/discovery.ts +27 -11
- package/src/handlers/fallback.test.ts +11 -10
- package/src/handlers/fallback.ts +14 -8
- package/src/handlers/file-checkouts.test.ts +83 -3
- package/src/handlers/file-checkouts.ts +22 -2
- package/src/handlers/findings.test.ts +2 -2
- package/src/handlers/findings.ts +38 -2
- package/src/handlers/git-issues.test.ts +2 -2
- package/src/handlers/git-issues.ts +4 -2
- package/src/handlers/ideas.test.ts +1 -1
- package/src/handlers/ideas.ts +34 -2
- package/src/handlers/progress.ts +2 -2
- package/src/handlers/project.ts +47 -2
- package/src/handlers/requests.test.ts +38 -7
- package/src/handlers/requests.ts +6 -3
- package/src/handlers/roles.test.ts +1 -1
- package/src/handlers/roles.ts +20 -2
- package/src/handlers/session.test.ts +303 -4
- package/src/handlers/session.ts +335 -28
- package/src/handlers/sprints.ts +61 -1
- package/src/handlers/tasks.test.ts +0 -73
- package/src/handlers/tasks.ts +269 -40
- package/src/handlers/tool-docs.ts +77 -5
- package/src/handlers/types.test.ts +259 -0
- package/src/templates/agent-guidelines.ts +210 -0
- package/src/tools.ts +479 -125
- package/src/utils.test.ts +7 -5
- package/src/utils.ts +95 -51
package/src/handlers/progress.ts
CHANGED
|
@@ -22,7 +22,7 @@ const logProgressSchema = {
|
|
|
22
22
|
|
|
23
23
|
const getActivityFeedSchema = {
|
|
24
24
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
25
|
-
limit: { type: 'number' as const, default:
|
|
25
|
+
limit: { type: 'number' as const, default: 10 },
|
|
26
26
|
since: { type: 'string' as const },
|
|
27
27
|
types: { type: 'array' as const },
|
|
28
28
|
created_by: { type: 'string' as const },
|
|
@@ -52,7 +52,7 @@ export const getActivityFeed: Handler = async (args, _ctx) => {
|
|
|
52
52
|
const { project_id, limit, since, types, created_by } = parseArgs(args, getActivityFeedSchema);
|
|
53
53
|
|
|
54
54
|
const apiClient = getApiClient();
|
|
55
|
-
const effectiveLimit = Math.min(limit ??
|
|
55
|
+
const effectiveLimit = Math.min(limit ?? 10, 200);
|
|
56
56
|
|
|
57
57
|
const response = await apiClient.getActivityFeed(project_id, {
|
|
58
58
|
limit: effectiveLimit,
|
package/src/handlers/project.ts
CHANGED
|
@@ -55,6 +55,15 @@ const updateProjectSchema = {
|
|
|
55
55
|
git_auto_branch: { type: 'boolean' as const },
|
|
56
56
|
git_auto_tag: { type: 'boolean' as const },
|
|
57
57
|
deployment_instructions: { type: 'string' as const },
|
|
58
|
+
// New project settings
|
|
59
|
+
git_delete_branch_on_merge: { type: 'boolean' as const },
|
|
60
|
+
require_pr_for_validation: { type: 'boolean' as const },
|
|
61
|
+
auto_merge_on_approval: { type: 'boolean' as const },
|
|
62
|
+
validation_required: { type: 'boolean' as const },
|
|
63
|
+
default_task_priority: { type: 'number' as const },
|
|
64
|
+
require_time_estimates: { type: 'boolean' as const },
|
|
65
|
+
fallback_activities_enabled: { type: 'boolean' as const },
|
|
66
|
+
preferred_fallback_activities: { type: 'array' as const },
|
|
58
67
|
};
|
|
59
68
|
|
|
60
69
|
const updateProjectReadmeSchema = {
|
|
@@ -62,6 +71,10 @@ const updateProjectReadmeSchema = {
|
|
|
62
71
|
readme_content: { type: 'string' as const, required: true as const },
|
|
63
72
|
};
|
|
64
73
|
|
|
74
|
+
const getProjectSummarySchema = {
|
|
75
|
+
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
export const getProjectContext: Handler = async (args, _ctx) => {
|
|
66
79
|
const { project_id, git_url } = parseArgs(args, getProjectContextSchema);
|
|
67
80
|
|
|
@@ -143,7 +156,16 @@ export const updateProject: Handler = async (args, _ctx) => {
|
|
|
143
156
|
git_develop_branch,
|
|
144
157
|
git_auto_branch,
|
|
145
158
|
git_auto_tag,
|
|
146
|
-
deployment_instructions
|
|
159
|
+
deployment_instructions,
|
|
160
|
+
// New project settings
|
|
161
|
+
git_delete_branch_on_merge,
|
|
162
|
+
require_pr_for_validation,
|
|
163
|
+
auto_merge_on_approval,
|
|
164
|
+
validation_required,
|
|
165
|
+
default_task_priority,
|
|
166
|
+
require_time_estimates,
|
|
167
|
+
fallback_activities_enabled,
|
|
168
|
+
preferred_fallback_activities
|
|
147
169
|
} = parseArgs(args, updateProjectSchema);
|
|
148
170
|
|
|
149
171
|
const apiClient = getApiClient();
|
|
@@ -159,7 +181,16 @@ export const updateProject: Handler = async (args, _ctx) => {
|
|
|
159
181
|
git_develop_branch,
|
|
160
182
|
git_auto_branch,
|
|
161
183
|
git_auto_tag,
|
|
162
|
-
deployment_instructions
|
|
184
|
+
deployment_instructions,
|
|
185
|
+
// New project settings
|
|
186
|
+
git_delete_branch_on_merge,
|
|
187
|
+
require_pr_for_validation,
|
|
188
|
+
auto_merge_on_approval,
|
|
189
|
+
validation_required,
|
|
190
|
+
default_task_priority,
|
|
191
|
+
require_time_estimates,
|
|
192
|
+
fallback_activities_enabled,
|
|
193
|
+
preferred_fallback_activities: preferred_fallback_activities as string[] | undefined
|
|
163
194
|
});
|
|
164
195
|
|
|
165
196
|
if (!response.ok) {
|
|
@@ -182,6 +213,19 @@ export const updateProjectReadme: Handler = async (args, _ctx) => {
|
|
|
182
213
|
return { result: response.data };
|
|
183
214
|
};
|
|
184
215
|
|
|
216
|
+
export const getProjectSummary: Handler = async (args, _ctx) => {
|
|
217
|
+
const { project_id } = parseArgs(args, getProjectSummarySchema);
|
|
218
|
+
|
|
219
|
+
const apiClient = getApiClient();
|
|
220
|
+
const response = await apiClient.getProjectSummary(project_id);
|
|
221
|
+
|
|
222
|
+
if (!response.ok) {
|
|
223
|
+
return { result: { error: response.error || 'Failed to get project summary' }, isError: true };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { result: response.data };
|
|
227
|
+
};
|
|
228
|
+
|
|
185
229
|
/**
|
|
186
230
|
* Project handlers registry
|
|
187
231
|
*/
|
|
@@ -191,4 +235,5 @@ export const projectHandlers: HandlerRegistry = {
|
|
|
191
235
|
create_project: createProject,
|
|
192
236
|
update_project: updateProject,
|
|
193
237
|
update_project_readme: updateProjectReadme,
|
|
238
|
+
get_project_summary: getProjectSummary,
|
|
194
239
|
};
|
|
@@ -28,7 +28,7 @@ describe('getPendingRequests', () => {
|
|
|
28
28
|
it('should return empty list when no requests', async () => {
|
|
29
29
|
mockApiClient.getPendingRequests.mockResolvedValue({
|
|
30
30
|
ok: true,
|
|
31
|
-
data: { requests: [] },
|
|
31
|
+
data: { requests: [], total_count: 0, has_more: false },
|
|
32
32
|
});
|
|
33
33
|
const ctx = createMockContext();
|
|
34
34
|
|
|
@@ -39,11 +39,12 @@ describe('getPendingRequests', () => {
|
|
|
39
39
|
|
|
40
40
|
expect(result.result).toMatchObject({
|
|
41
41
|
requests: [],
|
|
42
|
-
|
|
42
|
+
total_count: 0,
|
|
43
|
+
has_more: false,
|
|
43
44
|
});
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
it('should return pending requests', async () => {
|
|
47
|
+
it('should return pending requests with pagination info', async () => {
|
|
47
48
|
const mockRequests = [
|
|
48
49
|
{
|
|
49
50
|
id: 'r1',
|
|
@@ -57,7 +58,7 @@ describe('getPendingRequests', () => {
|
|
|
57
58
|
|
|
58
59
|
mockApiClient.getPendingRequests.mockResolvedValue({
|
|
59
60
|
ok: true,
|
|
60
|
-
data: { requests: mockRequests },
|
|
61
|
+
data: { requests: mockRequests, total_count: 5, has_more: true },
|
|
61
62
|
});
|
|
62
63
|
const ctx = createMockContext();
|
|
63
64
|
|
|
@@ -66,13 +67,17 @@ describe('getPendingRequests', () => {
|
|
|
66
67
|
ctx
|
|
67
68
|
);
|
|
68
69
|
|
|
69
|
-
expect(
|
|
70
|
+
expect(result.result).toMatchObject({
|
|
71
|
+
requests: mockRequests,
|
|
72
|
+
total_count: 5,
|
|
73
|
+
has_more: true,
|
|
74
|
+
});
|
|
70
75
|
});
|
|
71
76
|
|
|
72
77
|
it('should call API client with project_id and session_id', async () => {
|
|
73
78
|
mockApiClient.getPendingRequests.mockResolvedValue({
|
|
74
79
|
ok: true,
|
|
75
|
-
data: { requests: [] },
|
|
80
|
+
data: { requests: [], total_count: 0, has_more: false },
|
|
76
81
|
});
|
|
77
82
|
const ctx = createMockContext({ sessionId: 'my-session' });
|
|
78
83
|
|
|
@@ -83,7 +88,33 @@ describe('getPendingRequests', () => {
|
|
|
83
88
|
|
|
84
89
|
expect(mockApiClient.getPendingRequests).toHaveBeenCalledWith(
|
|
85
90
|
'123e4567-e89b-12d3-a456-426614174000',
|
|
86
|
-
'my-session'
|
|
91
|
+
'my-session',
|
|
92
|
+
50,
|
|
93
|
+
0
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('should pass limit and offset to API client', async () => {
|
|
98
|
+
mockApiClient.getPendingRequests.mockResolvedValue({
|
|
99
|
+
ok: true,
|
|
100
|
+
data: { requests: [], total_count: 100, has_more: true },
|
|
101
|
+
});
|
|
102
|
+
const ctx = createMockContext({ sessionId: 'my-session' });
|
|
103
|
+
|
|
104
|
+
await getPendingRequests(
|
|
105
|
+
{
|
|
106
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
107
|
+
limit: 10,
|
|
108
|
+
offset: 20,
|
|
109
|
+
},
|
|
110
|
+
ctx
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(mockApiClient.getPendingRequests).toHaveBeenCalledWith(
|
|
114
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
115
|
+
'my-session',
|
|
116
|
+
10,
|
|
117
|
+
20
|
|
87
118
|
);
|
|
88
119
|
});
|
|
89
120
|
|
package/src/handlers/requests.ts
CHANGED
|
@@ -16,6 +16,8 @@ import { getApiClient } from '../api-client.js';
|
|
|
16
16
|
// Argument schemas for type-safe parsing
|
|
17
17
|
const getPendingRequestsSchema = {
|
|
18
18
|
project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
|
|
19
|
+
limit: { type: 'number' as const, default: 50 },
|
|
20
|
+
offset: { type: 'number' as const, default: 0 },
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
const acknowledgeRequestSchema = {
|
|
@@ -28,12 +30,12 @@ const answerQuestionSchema = {
|
|
|
28
30
|
};
|
|
29
31
|
|
|
30
32
|
export const getPendingRequests: Handler = async (args, ctx) => {
|
|
31
|
-
const { project_id } = parseArgs(args, getPendingRequestsSchema);
|
|
33
|
+
const { project_id, limit, offset } = parseArgs(args, getPendingRequestsSchema);
|
|
32
34
|
|
|
33
35
|
const { session } = ctx;
|
|
34
36
|
const apiClient = getApiClient();
|
|
35
37
|
|
|
36
|
-
const response = await apiClient.getPendingRequests(project_id, session.currentSessionId || undefined);
|
|
38
|
+
const response = await apiClient.getPendingRequests(project_id, session.currentSessionId || undefined, Math.min(limit ?? 50, 50), offset);
|
|
37
39
|
|
|
38
40
|
if (!response.ok) {
|
|
39
41
|
return { result: { error: response.error || 'Failed to get pending requests' }, isError: true };
|
|
@@ -42,7 +44,8 @@ export const getPendingRequests: Handler = async (args, ctx) => {
|
|
|
42
44
|
return {
|
|
43
45
|
result: {
|
|
44
46
|
requests: response.data?.requests || [],
|
|
45
|
-
|
|
47
|
+
total_count: response.data?.total_count || 0,
|
|
48
|
+
has_more: response.data?.has_more || false,
|
|
46
49
|
},
|
|
47
50
|
};
|
|
48
51
|
};
|
|
@@ -280,7 +280,7 @@ describe('getAgentsByRole', () => {
|
|
|
280
280
|
});
|
|
281
281
|
expect(mockApiClient.proxy).toHaveBeenCalledWith(
|
|
282
282
|
'get_agents_by_role',
|
|
283
|
-
{ project_id: '123e4567-e89b-12d3-a456-426614174000' }
|
|
283
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000', counts_only: true }
|
|
284
284
|
);
|
|
285
285
|
});
|
|
286
286
|
|
package/src/handlers/roles.ts
CHANGED
|
@@ -167,7 +167,7 @@ export const setSessionRole: Handler = async (args, ctx) => {
|
|
|
167
167
|
};
|
|
168
168
|
|
|
169
169
|
export const getAgentsByRole: Handler = async (args, _ctx) => {
|
|
170
|
-
const { project_id } = args as { project_id: string };
|
|
170
|
+
const { project_id, counts_only = true } = args as { project_id: string; counts_only?: boolean };
|
|
171
171
|
|
|
172
172
|
if (!project_id) {
|
|
173
173
|
return {
|
|
@@ -176,6 +176,24 @@ export const getAgentsByRole: Handler = async (args, _ctx) => {
|
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
const apiClient = getApiClient();
|
|
179
|
+
|
|
180
|
+
// Type varies based on counts_only
|
|
181
|
+
if (counts_only) {
|
|
182
|
+
const response = await apiClient.proxy<{
|
|
183
|
+
agents_by_role: Record<AgentRole, number>;
|
|
184
|
+
total_active: number;
|
|
185
|
+
}>('get_agents_by_role', { project_id, counts_only: true });
|
|
186
|
+
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
return {
|
|
189
|
+
result: { error: response.error || 'Failed to get agents by role' },
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { result: response.data };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Full details mode
|
|
179
197
|
const response = await apiClient.proxy<{
|
|
180
198
|
agents_by_role: Record<AgentRole, Array<{
|
|
181
199
|
session_id: string;
|
|
@@ -186,7 +204,7 @@ export const getAgentsByRole: Handler = async (args, _ctx) => {
|
|
|
186
204
|
last_synced_at: string;
|
|
187
205
|
}>>;
|
|
188
206
|
total_active: number;
|
|
189
|
-
}>('get_agents_by_role', { project_id });
|
|
207
|
+
}>('get_agents_by_role', { project_id, counts_only: false });
|
|
190
208
|
|
|
191
209
|
if (!response.ok) {
|
|
192
210
|
return {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
endWorkSession,
|
|
6
6
|
getHelp,
|
|
7
7
|
getTokenUsage,
|
|
8
|
+
reportTokenUsage,
|
|
8
9
|
} from './session.js';
|
|
9
10
|
import { createMockContext } from './__test-utils__.js';
|
|
10
11
|
import { mockApiClient } from './__test-setup__.js';
|
|
@@ -31,7 +32,7 @@ describe('heartbeat', () => {
|
|
|
31
32
|
session_id: 'session-123',
|
|
32
33
|
});
|
|
33
34
|
expect(result.result).toHaveProperty('timestamp');
|
|
34
|
-
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', { current_worktree_path: undefined });
|
|
35
|
+
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', expect.objectContaining({ current_worktree_path: undefined }));
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it('should use provided session_id over current session', async () => {
|
|
@@ -48,7 +49,7 @@ describe('heartbeat', () => {
|
|
|
48
49
|
success: true,
|
|
49
50
|
session_id: 'other-session-456',
|
|
50
51
|
});
|
|
51
|
-
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('other-session-456', { current_worktree_path: undefined });
|
|
52
|
+
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('other-session-456', expect.objectContaining({ current_worktree_path: undefined }));
|
|
52
53
|
});
|
|
53
54
|
|
|
54
55
|
it('should pass worktree_path to API', async () => {
|
|
@@ -61,7 +62,7 @@ describe('heartbeat', () => {
|
|
|
61
62
|
|
|
62
63
|
await heartbeat({ current_worktree_path: '../project-task-abc123' }, ctx);
|
|
63
64
|
|
|
64
|
-
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', { current_worktree_path: '../project-task-abc123' });
|
|
65
|
+
expect(mockApiClient.heartbeat).toHaveBeenCalledWith('session-123', expect.objectContaining({ current_worktree_path: '../project-task-abc123' }));
|
|
65
66
|
});
|
|
66
67
|
|
|
67
68
|
it('should return error when no active session', async () => {
|
|
@@ -80,7 +81,7 @@ describe('heartbeat', () => {
|
|
|
80
81
|
callCount: 10,
|
|
81
82
|
totalTokens: 5000,
|
|
82
83
|
byTool: {
|
|
83
|
-
|
|
84
|
+
get_task: { calls: 3, tokens: 1500 },
|
|
84
85
|
update_task: { calls: 4, tokens: 2000 },
|
|
85
86
|
complete_task: { calls: 3, tokens: 1500 },
|
|
86
87
|
},
|
|
@@ -573,4 +574,302 @@ describe('startWorkSession', () => {
|
|
|
573
574
|
expect(result.result).not.toHaveProperty('pending_requests');
|
|
574
575
|
expect(result.result).not.toHaveProperty('pending_requests_count');
|
|
575
576
|
});
|
|
577
|
+
|
|
578
|
+
it('should surface awaiting_validation tasks when present', async () => {
|
|
579
|
+
const ctx = createMockContext({ sessionId: null });
|
|
580
|
+
const mockValidationTasks = [
|
|
581
|
+
{ id: 'task-1', title: 'Implement login' },
|
|
582
|
+
{ id: 'task-2', title: 'Add tests' },
|
|
583
|
+
];
|
|
584
|
+
mockApiClient.startSession.mockResolvedValue({
|
|
585
|
+
ok: true,
|
|
586
|
+
data: {
|
|
587
|
+
session_started: true,
|
|
588
|
+
session_id: 'new-session-123',
|
|
589
|
+
persona: 'Wave',
|
|
590
|
+
role: 'developer',
|
|
591
|
+
project: { id: 'project-123', name: 'Test Project' },
|
|
592
|
+
awaiting_validation: mockValidationTasks,
|
|
593
|
+
validation_count: 2,
|
|
594
|
+
validation_priority: 'VALIDATE FIRST: 2 task(s) need review before starting new work.',
|
|
595
|
+
directive: 'VALIDATE FIRST: 2 task(s) need review before starting new work. Call claim_validation(task_id) to start reviewing.',
|
|
596
|
+
},
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
const result = await startWorkSession({ project_id: 'project-123' }, ctx);
|
|
600
|
+
|
|
601
|
+
expect(result.result).toHaveProperty('awaiting_validation');
|
|
602
|
+
expect(result.result).toHaveProperty('validation_count', 2);
|
|
603
|
+
expect(result.result).toHaveProperty('validation_priority');
|
|
604
|
+
const awaitingValidation = (result.result as { awaiting_validation: typeof mockValidationTasks }).awaiting_validation;
|
|
605
|
+
expect(awaitingValidation.length).toBe(2);
|
|
606
|
+
expect(awaitingValidation[0].id).toBe('task-1');
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should include next_action when validation tasks are present', async () => {
|
|
610
|
+
const ctx = createMockContext({ sessionId: null });
|
|
611
|
+
mockApiClient.startSession.mockResolvedValue({
|
|
612
|
+
ok: true,
|
|
613
|
+
data: {
|
|
614
|
+
session_started: true,
|
|
615
|
+
session_id: 'new-session-123',
|
|
616
|
+
persona: 'Wave',
|
|
617
|
+
role: 'developer',
|
|
618
|
+
project: { id: 'project-123', name: 'Test Project' },
|
|
619
|
+
awaiting_validation: [{ id: 'task-abc', title: 'Fix bug' }],
|
|
620
|
+
validation_count: 1,
|
|
621
|
+
next_action: 'claim_validation(task_id: "task-abc")',
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
const result = await startWorkSession({ project_id: 'project-123' }, ctx);
|
|
626
|
+
|
|
627
|
+
expect(result.result).toHaveProperty('next_action');
|
|
628
|
+
expect((result.result as { next_action: string }).next_action).toContain('task-abc');
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
// ============================================================================
|
|
633
|
+
// reportTokenUsage Tests
|
|
634
|
+
// ============================================================================
|
|
635
|
+
|
|
636
|
+
describe('reportTokenUsage', () => {
|
|
637
|
+
beforeEach(() => vi.clearAllMocks());
|
|
638
|
+
|
|
639
|
+
it('should report token usage successfully with session', async () => {
|
|
640
|
+
const ctx = createMockContext();
|
|
641
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
642
|
+
ok: true,
|
|
643
|
+
data: {
|
|
644
|
+
success: true,
|
|
645
|
+
reported: {
|
|
646
|
+
session_id: 'session-123',
|
|
647
|
+
model: 'sonnet',
|
|
648
|
+
input_tokens: 1000,
|
|
649
|
+
output_tokens: 500,
|
|
650
|
+
total_tokens: 1500,
|
|
651
|
+
estimated_cost_usd: 0.0105,
|
|
652
|
+
},
|
|
653
|
+
task_attributed: true,
|
|
654
|
+
task_id: 'task-123',
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
const result = await reportTokenUsage(
|
|
659
|
+
{ input_tokens: 1000, output_tokens: 500, model: 'sonnet' },
|
|
660
|
+
ctx
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
expect(result.result).toMatchObject({
|
|
664
|
+
success: true,
|
|
665
|
+
task_attributed: true,
|
|
666
|
+
task_id: 'task-123',
|
|
667
|
+
});
|
|
668
|
+
expect(result.result).toHaveProperty('reported');
|
|
669
|
+
expect(mockApiClient.reportTokenUsage).toHaveBeenCalledWith('session-123', {
|
|
670
|
+
input_tokens: 1000,
|
|
671
|
+
output_tokens: 500,
|
|
672
|
+
model: 'sonnet',
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
it('should default to sonnet model when not specified', async () => {
|
|
677
|
+
const ctx = createMockContext();
|
|
678
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
679
|
+
ok: true,
|
|
680
|
+
data: {
|
|
681
|
+
success: true,
|
|
682
|
+
reported: {
|
|
683
|
+
session_id: 'session-123',
|
|
684
|
+
model: 'sonnet',
|
|
685
|
+
input_tokens: 1000,
|
|
686
|
+
output_tokens: 500,
|
|
687
|
+
total_tokens: 1500,
|
|
688
|
+
estimated_cost_usd: 0.0105,
|
|
689
|
+
},
|
|
690
|
+
task_attributed: false,
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
695
|
+
|
|
696
|
+
expect(mockApiClient.reportTokenUsage).toHaveBeenCalledWith('session-123', {
|
|
697
|
+
input_tokens: 1000,
|
|
698
|
+
output_tokens: 500,
|
|
699
|
+
model: 'sonnet',
|
|
700
|
+
});
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('should use session currentModel if available', async () => {
|
|
704
|
+
const ctx = createMockContext({
|
|
705
|
+
tokenUsage: {
|
|
706
|
+
callCount: 0,
|
|
707
|
+
totalTokens: 0,
|
|
708
|
+
byTool: {},
|
|
709
|
+
byModel: {},
|
|
710
|
+
currentModel: 'opus',
|
|
711
|
+
},
|
|
712
|
+
});
|
|
713
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
714
|
+
ok: true,
|
|
715
|
+
data: {
|
|
716
|
+
success: true,
|
|
717
|
+
reported: {
|
|
718
|
+
session_id: 'session-123',
|
|
719
|
+
model: 'opus',
|
|
720
|
+
input_tokens: 1000,
|
|
721
|
+
output_tokens: 500,
|
|
722
|
+
total_tokens: 1500,
|
|
723
|
+
estimated_cost_usd: 0.0525,
|
|
724
|
+
},
|
|
725
|
+
task_attributed: false,
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
|
|
729
|
+
await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
730
|
+
|
|
731
|
+
expect(mockApiClient.reportTokenUsage).toHaveBeenCalledWith('session-123', {
|
|
732
|
+
input_tokens: 1000,
|
|
733
|
+
output_tokens: 500,
|
|
734
|
+
model: 'opus',
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
it('should return error for negative token counts', async () => {
|
|
739
|
+
const ctx = createMockContext();
|
|
740
|
+
|
|
741
|
+
const result = await reportTokenUsage({ input_tokens: -100, output_tokens: 500 }, ctx);
|
|
742
|
+
|
|
743
|
+
expect(result.result).toMatchObject({
|
|
744
|
+
error: 'Token counts must be non-negative',
|
|
745
|
+
});
|
|
746
|
+
expect(mockApiClient.reportTokenUsage).not.toHaveBeenCalled();
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
it('should handle local-only reporting when no session', async () => {
|
|
750
|
+
const ctx = createMockContext({ sessionId: null });
|
|
751
|
+
|
|
752
|
+
const result = await reportTokenUsage(
|
|
753
|
+
{ input_tokens: 1000, output_tokens: 500, model: 'haiku' },
|
|
754
|
+
ctx
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
expect(result.result).toMatchObject({
|
|
758
|
+
success: true,
|
|
759
|
+
});
|
|
760
|
+
expect(result.result).toHaveProperty('reported');
|
|
761
|
+
const reported = (result.result as { reported: { model: string } }).reported;
|
|
762
|
+
expect(reported.model).toBe('haiku');
|
|
763
|
+
expect(result.result).toHaveProperty('note');
|
|
764
|
+
expect(mockApiClient.reportTokenUsage).not.toHaveBeenCalled();
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should update local token tracking', async () => {
|
|
768
|
+
const ctx = createMockContext({
|
|
769
|
+
tokenUsage: {
|
|
770
|
+
callCount: 5,
|
|
771
|
+
totalTokens: 2500,
|
|
772
|
+
byTool: {},
|
|
773
|
+
byModel: { sonnet: { input: 1000, output: 500 } },
|
|
774
|
+
currentModel: null,
|
|
775
|
+
},
|
|
776
|
+
});
|
|
777
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
778
|
+
ok: true,
|
|
779
|
+
data: {
|
|
780
|
+
success: true,
|
|
781
|
+
reported: {
|
|
782
|
+
session_id: 'session-123',
|
|
783
|
+
model: 'sonnet',
|
|
784
|
+
input_tokens: 1000,
|
|
785
|
+
output_tokens: 500,
|
|
786
|
+
total_tokens: 1500,
|
|
787
|
+
estimated_cost_usd: 0.0105,
|
|
788
|
+
},
|
|
789
|
+
task_attributed: false,
|
|
790
|
+
},
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
794
|
+
|
|
795
|
+
expect(ctx.updateSession).toHaveBeenCalledWith(
|
|
796
|
+
expect.objectContaining({
|
|
797
|
+
tokenUsage: expect.objectContaining({
|
|
798
|
+
callCount: 6,
|
|
799
|
+
totalTokens: 4000,
|
|
800
|
+
byModel: { sonnet: { input: 2000, output: 1000 } },
|
|
801
|
+
}),
|
|
802
|
+
})
|
|
803
|
+
);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
it('should handle backend failure gracefully', async () => {
|
|
807
|
+
const ctx = createMockContext();
|
|
808
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
809
|
+
ok: false,
|
|
810
|
+
error: 'Server error',
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
const result = await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
814
|
+
|
|
815
|
+
// Should still succeed with local calculation
|
|
816
|
+
expect(result.result).toMatchObject({
|
|
817
|
+
success: true,
|
|
818
|
+
});
|
|
819
|
+
expect(result.result).toHaveProperty('warning');
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('should indicate when task attribution succeeds', async () => {
|
|
823
|
+
const ctx = createMockContext();
|
|
824
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
825
|
+
ok: true,
|
|
826
|
+
data: {
|
|
827
|
+
success: true,
|
|
828
|
+
reported: {
|
|
829
|
+
session_id: 'session-123',
|
|
830
|
+
model: 'sonnet',
|
|
831
|
+
input_tokens: 1000,
|
|
832
|
+
output_tokens: 500,
|
|
833
|
+
total_tokens: 1500,
|
|
834
|
+
estimated_cost_usd: 0.0105,
|
|
835
|
+
},
|
|
836
|
+
task_attributed: true,
|
|
837
|
+
task_id: 'task-abc123',
|
|
838
|
+
},
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
const result = await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
842
|
+
|
|
843
|
+
expect(result.result).toMatchObject({
|
|
844
|
+
task_attributed: true,
|
|
845
|
+
task_id: 'task-abc123',
|
|
846
|
+
});
|
|
847
|
+
expect((result.result as { note: string }).note).toContain('attributed to current task');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('should indicate when no task to attribute to', async () => {
|
|
851
|
+
const ctx = createMockContext();
|
|
852
|
+
mockApiClient.reportTokenUsage.mockResolvedValue({
|
|
853
|
+
ok: true,
|
|
854
|
+
data: {
|
|
855
|
+
success: true,
|
|
856
|
+
reported: {
|
|
857
|
+
session_id: 'session-123',
|
|
858
|
+
model: 'sonnet',
|
|
859
|
+
input_tokens: 1000,
|
|
860
|
+
output_tokens: 500,
|
|
861
|
+
total_tokens: 1500,
|
|
862
|
+
estimated_cost_usd: 0.0105,
|
|
863
|
+
},
|
|
864
|
+
task_attributed: false,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
const result = await reportTokenUsage({ input_tokens: 1000, output_tokens: 500 }, ctx);
|
|
869
|
+
|
|
870
|
+
expect(result.result).toMatchObject({
|
|
871
|
+
task_attributed: false,
|
|
872
|
+
});
|
|
873
|
+
expect((result.result as { note: string }).note).toContain('No active task');
|
|
874
|
+
});
|
|
576
875
|
});
|