@vibescope/mcp-server 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/README.md +60 -7
  2. package/dist/api-client.d.ts +251 -1
  3. package/dist/api-client.js +82 -3
  4. package/dist/handlers/blockers.js +9 -8
  5. package/dist/handlers/bodies-of-work.js +96 -63
  6. package/dist/handlers/connectors.d.ts +45 -0
  7. package/dist/handlers/connectors.js +183 -0
  8. package/dist/handlers/cost.d.ts +10 -0
  9. package/dist/handlers/cost.js +112 -50
  10. package/dist/handlers/decisions.js +32 -19
  11. package/dist/handlers/deployment.js +144 -122
  12. package/dist/handlers/discovery.d.ts +7 -0
  13. package/dist/handlers/discovery.js +96 -7
  14. package/dist/handlers/fallback.js +29 -23
  15. package/dist/handlers/file-checkouts.d.ts +20 -0
  16. package/dist/handlers/file-checkouts.js +133 -0
  17. package/dist/handlers/findings.d.ts +6 -0
  18. package/dist/handlers/findings.js +96 -40
  19. package/dist/handlers/git-issues.js +40 -36
  20. package/dist/handlers/ideas.js +49 -31
  21. package/dist/handlers/index.d.ts +3 -0
  22. package/dist/handlers/index.js +9 -0
  23. package/dist/handlers/milestones.js +39 -32
  24. package/dist/handlers/organizations.js +99 -91
  25. package/dist/handlers/progress.js +24 -13
  26. package/dist/handlers/project.js +68 -28
  27. package/dist/handlers/requests.js +18 -14
  28. package/dist/handlers/roles.d.ts +18 -0
  29. package/dist/handlers/roles.js +130 -0
  30. package/dist/handlers/session.js +58 -17
  31. package/dist/handlers/sprints.js +93 -81
  32. package/dist/handlers/tasks.d.ts +2 -0
  33. package/dist/handlers/tasks.js +189 -91
  34. package/dist/handlers/types.d.ts +64 -2
  35. package/dist/handlers/types.js +48 -1
  36. package/dist/handlers/validation.js +21 -17
  37. package/dist/index.js +7 -2716
  38. package/dist/token-tracking.d.ts +74 -0
  39. package/dist/token-tracking.js +122 -0
  40. package/dist/tools.js +685 -9
  41. package/dist/utils.d.ts +5 -0
  42. package/dist/utils.js +17 -0
  43. package/docs/TOOLS.md +2053 -0
  44. package/package.json +4 -1
  45. package/scripts/generate-docs.ts +212 -0
  46. package/src/api-client.test.ts +718 -0
  47. package/src/api-client.ts +320 -6
  48. package/src/handlers/__test-setup__.ts +16 -0
  49. package/src/handlers/blockers.test.ts +31 -19
  50. package/src/handlers/blockers.ts +9 -8
  51. package/src/handlers/bodies-of-work.test.ts +55 -32
  52. package/src/handlers/bodies-of-work.ts +115 -115
  53. package/src/handlers/connectors.test.ts +834 -0
  54. package/src/handlers/connectors.ts +229 -0
  55. package/src/handlers/cost.test.ts +34 -44
  56. package/src/handlers/cost.ts +136 -85
  57. package/src/handlers/decisions.test.ts +37 -27
  58. package/src/handlers/decisions.ts +35 -30
  59. package/src/handlers/deployment.ts +180 -208
  60. package/src/handlers/discovery.test.ts +4 -5
  61. package/src/handlers/discovery.ts +98 -8
  62. package/src/handlers/fallback.test.ts +26 -22
  63. package/src/handlers/fallback.ts +36 -33
  64. package/src/handlers/file-checkouts.test.ts +670 -0
  65. package/src/handlers/file-checkouts.ts +165 -0
  66. package/src/handlers/findings.test.ts +178 -19
  67. package/src/handlers/findings.ts +112 -74
  68. package/src/handlers/git-issues.test.ts +51 -43
  69. package/src/handlers/git-issues.ts +44 -84
  70. package/src/handlers/ideas.test.ts +28 -23
  71. package/src/handlers/ideas.ts +61 -59
  72. package/src/handlers/index.ts +9 -0
  73. package/src/handlers/milestones.test.ts +33 -28
  74. package/src/handlers/milestones.ts +52 -50
  75. package/src/handlers/organizations.test.ts +104 -83
  76. package/src/handlers/organizations.ts +117 -142
  77. package/src/handlers/progress.test.ts +20 -14
  78. package/src/handlers/progress.ts +26 -24
  79. package/src/handlers/project.test.ts +34 -27
  80. package/src/handlers/project.ts +95 -63
  81. package/src/handlers/requests.test.ts +27 -18
  82. package/src/handlers/requests.ts +21 -17
  83. package/src/handlers/roles.test.ts +303 -0
  84. package/src/handlers/roles.ts +208 -0
  85. package/src/handlers/session.test.ts +47 -0
  86. package/src/handlers/session.ts +71 -26
  87. package/src/handlers/sprints.test.ts +71 -50
  88. package/src/handlers/sprints.ts +113 -146
  89. package/src/handlers/tasks.test.ts +77 -15
  90. package/src/handlers/tasks.ts +231 -156
  91. package/src/handlers/tool-categories.test.ts +66 -0
  92. package/src/handlers/types.ts +81 -2
  93. package/src/handlers/validation.test.ts +78 -45
  94. package/src/handlers/validation.ts +23 -25
  95. package/src/index.ts +12 -2732
  96. package/src/token-tracking.test.ts +453 -0
  97. package/src/token-tracking.ts +164 -0
  98. package/src/tools.ts +685 -9
  99. package/src/utils.test.ts +2 -2
  100. package/src/utils.ts +17 -0
  101. package/dist/config/tool-categories.d.ts +0 -31
  102. package/dist/config/tool-categories.js +0 -253
  103. package/dist/knowledge.d.ts +0 -6
  104. package/dist/knowledge.js +0 -218
@@ -10,14 +10,60 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry } from './types.js';
13
- import { validateRequired, validateUUID, validateProjectStatus } from '../validators.js';
13
+ import {
14
+ parseArgs,
15
+ uuidValidator,
16
+ projectStatusValidator,
17
+ createEnumValidator,
18
+ VALID_PROJECT_STATUSES,
19
+ } from '../validators.js';
14
20
  import { getApiClient } from '../api-client.js';
15
21
 
16
- export const getProjectContext: Handler = async (args, ctx) => {
17
- const { project_id, git_url } = args as {
18
- project_id?: string;
19
- git_url?: string;
20
- };
22
+ const VALID_GIT_WORKFLOWS = ['none', 'trunk-based', 'github-flow', 'git-flow'] as const;
23
+ type GitWorkflow = typeof VALID_GIT_WORKFLOWS[number];
24
+
25
+ // Argument schemas for type-safe parsing
26
+ const getProjectContextSchema = {
27
+ project_id: { type: 'string' as const, validate: uuidValidator },
28
+ git_url: { type: 'string' as const },
29
+ };
30
+
31
+ const getGitWorkflowSchema = {
32
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
33
+ task_id: { type: 'string' as const, validate: uuidValidator },
34
+ };
35
+
36
+ const createProjectSchema = {
37
+ name: { type: 'string' as const, required: true as const },
38
+ description: { type: 'string' as const },
39
+ goal: { type: 'string' as const },
40
+ git_url: { type: 'string' as const },
41
+ tech_stack: { type: 'array' as const },
42
+ };
43
+
44
+ const updateProjectSchema = {
45
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
46
+ name: { type: 'string' as const },
47
+ description: { type: 'string' as const },
48
+ goal: { type: 'string' as const },
49
+ git_url: { type: 'string' as const },
50
+ tech_stack: { type: 'array' as const },
51
+ status: { type: 'string' as const, validate: projectStatusValidator },
52
+ git_workflow: { type: 'string' as const, validate: createEnumValidator(VALID_GIT_WORKFLOWS) },
53
+ git_main_branch: { type: 'string' as const },
54
+ git_develop_branch: { type: 'string' as const },
55
+ git_auto_branch: { type: 'boolean' as const },
56
+ git_auto_tag: { type: 'boolean' as const },
57
+ deployment_instructions: { type: 'string' as const },
58
+ };
59
+
60
+ const updateProjectReadmeSchema = {
61
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
62
+ readme_content: { type: 'string' as const, required: true as const },
63
+ };
64
+
65
+ export const getProjectContext: Handler = async (args, _ctx) => {
66
+ const { project_id, git_url } = parseArgs(args, getProjectContextSchema);
21
67
 
22
68
  const apiClient = getApiClient();
23
69
 
@@ -26,7 +72,7 @@ export const getProjectContext: Handler = async (args, ctx) => {
26
72
  const response = await apiClient.listProjects();
27
73
 
28
74
  if (!response.ok) {
29
- throw new Error(response.error || 'Failed to fetch projects');
75
+ return { result: { error: response.error || 'Failed to fetch projects' }, isError: true };
30
76
  }
31
77
 
32
78
  return { result: { projects: response.data?.projects || [] } };
@@ -36,7 +82,7 @@ export const getProjectContext: Handler = async (args, ctx) => {
36
82
  const response = await apiClient.getProject(project_id || 'by-git-url', git_url);
37
83
 
38
84
  if (!response.ok) {
39
- throw new Error(response.error || 'Failed to fetch project');
85
+ return { result: { error: response.error || 'Failed to fetch project' }, isError: true };
40
86
  }
41
87
 
42
88
  if (!response.data?.found) {
@@ -51,35 +97,21 @@ export const getProjectContext: Handler = async (args, ctx) => {
51
97
  return { result: response.data };
52
98
  };
53
99
 
54
- export const getGitWorkflow: Handler = async (args, ctx) => {
55
- const { project_id, task_id } = args as {
56
- project_id: string;
57
- task_id?: string;
58
- };
59
-
60
- validateRequired(project_id, 'project_id');
61
- validateUUID(project_id, 'project_id');
100
+ export const getGitWorkflow: Handler = async (args, _ctx) => {
101
+ const { project_id, task_id } = parseArgs(args, getGitWorkflowSchema);
62
102
 
63
103
  const apiClient = getApiClient();
64
104
  const response = await apiClient.getGitWorkflow(project_id, task_id);
65
105
 
66
106
  if (!response.ok) {
67
- throw new Error(response.error || 'Failed to get git workflow');
107
+ return { result: { error: response.error || 'Failed to get git workflow' }, isError: true };
68
108
  }
69
109
 
70
110
  return { result: response.data };
71
111
  };
72
112
 
73
- export const createProject: Handler = async (args, ctx) => {
74
- const { name, description, goal, git_url, tech_stack } = args as {
75
- name: string;
76
- description?: string;
77
- goal?: string;
78
- git_url?: string;
79
- tech_stack?: string[];
80
- };
81
-
82
- validateRequired(name, 'name');
113
+ export const createProject: Handler = async (args, _ctx) => {
114
+ const { name, description, goal, git_url, tech_stack } = parseArgs(args, createProjectSchema);
83
115
 
84
116
  const apiClient = getApiClient();
85
117
  const response = await apiClient.createProject({
@@ -87,64 +119,64 @@ export const createProject: Handler = async (args, ctx) => {
87
119
  description,
88
120
  goal,
89
121
  git_url,
90
- tech_stack
122
+ tech_stack: tech_stack as string[] | undefined
91
123
  });
92
124
 
93
125
  if (!response.ok) {
94
- throw new Error(response.error || 'Failed to create project');
126
+ return { result: { error: response.error || 'Failed to create project' }, isError: true };
95
127
  }
96
128
 
97
129
  return { result: response.data };
98
130
  };
99
131
 
100
- export const updateProject: Handler = async (args, ctx) => {
101
- const { project_id, ...updates } = args as {
102
- project_id: string;
103
- name?: string;
104
- description?: string;
105
- goal?: string;
106
- git_url?: string;
107
- tech_stack?: string[];
108
- status?: string;
109
- git_workflow?: 'none' | 'trunk-based' | 'github-flow' | 'git-flow';
110
- git_main_branch?: string;
111
- git_develop_branch?: string;
112
- git_auto_branch?: boolean;
113
- git_auto_tag?: boolean;
114
- git_auto_commit_on_complete?: boolean;
115
- git_auto_pr_on_complete?: boolean;
116
- deployment_instructions?: string;
117
- };
118
-
119
- validateRequired(project_id, 'project_id');
120
- validateUUID(project_id, 'project_id');
121
- validateProjectStatus(updates.status);
132
+ export const updateProject: Handler = async (args, _ctx) => {
133
+ const {
134
+ project_id,
135
+ name,
136
+ description,
137
+ goal,
138
+ git_url,
139
+ tech_stack,
140
+ status,
141
+ git_workflow,
142
+ git_main_branch,
143
+ git_develop_branch,
144
+ git_auto_branch,
145
+ git_auto_tag,
146
+ deployment_instructions
147
+ } = parseArgs(args, updateProjectSchema);
122
148
 
123
149
  const apiClient = getApiClient();
124
- const response = await apiClient.updateProject(project_id, updates);
150
+ const response = await apiClient.updateProject(project_id, {
151
+ name,
152
+ description,
153
+ goal,
154
+ git_url,
155
+ tech_stack: tech_stack as string[] | undefined,
156
+ status: status as typeof VALID_PROJECT_STATUSES[number] | undefined,
157
+ git_workflow: git_workflow as GitWorkflow | undefined,
158
+ git_main_branch,
159
+ git_develop_branch,
160
+ git_auto_branch,
161
+ git_auto_tag,
162
+ deployment_instructions
163
+ });
125
164
 
126
165
  if (!response.ok) {
127
- throw new Error(response.error || 'Failed to update project');
166
+ return { result: { error: response.error || 'Failed to update project' }, isError: true };
128
167
  }
129
168
 
130
169
  return { result: response.data };
131
170
  };
132
171
 
133
- export const updateProjectReadme: Handler = async (args, ctx) => {
134
- const { project_id, readme_content } = args as {
135
- project_id: string;
136
- readme_content: string;
137
- };
138
-
139
- validateRequired(project_id, 'project_id');
140
- validateUUID(project_id, 'project_id');
141
- validateRequired(readme_content, 'readme_content');
172
+ export const updateProjectReadme: Handler = async (args, _ctx) => {
173
+ const { project_id, readme_content } = parseArgs(args, updateProjectReadmeSchema);
142
174
 
143
175
  const apiClient = getApiClient();
144
176
  const response = await apiClient.updateProjectReadme(project_id, readme_content);
145
177
 
146
178
  if (!response.ok) {
147
- throw new Error(response.error || 'Failed to update README');
179
+ return { result: { error: response.error || 'Failed to update README' }, isError: true };
148
180
  }
149
181
 
150
182
  return { result: response.data };
@@ -87,19 +87,22 @@ describe('getPendingRequests', () => {
87
87
  );
88
88
  });
89
89
 
90
- it('should throw error when API call fails', async () => {
90
+ it('should return error when API call fails', async () => {
91
91
  mockApiClient.getPendingRequests.mockResolvedValue({
92
92
  ok: false,
93
93
  error: 'Query failed',
94
94
  });
95
95
  const ctx = createMockContext();
96
96
 
97
- await expect(
98
- getPendingRequests(
99
- { project_id: '123e4567-e89b-12d3-a456-426614174000' },
100
- ctx
101
- )
102
- ).rejects.toThrow('Failed to get pending requests: Query failed');
97
+ const result = await getPendingRequests(
98
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
99
+ ctx
100
+ );
101
+
102
+ expect(result.isError).toBe(true);
103
+ expect(result.result).toMatchObject({
104
+ error: 'Query failed',
105
+ });
103
106
  });
104
107
  });
105
108
 
@@ -159,16 +162,19 @@ describe('acknowledgeRequest', () => {
159
162
  );
160
163
  });
161
164
 
162
- it('should throw error when API call fails', async () => {
165
+ it('should return error when API call fails', async () => {
163
166
  mockApiClient.acknowledgeRequest.mockResolvedValue({
164
167
  ok: false,
165
168
  error: 'Update failed',
166
169
  });
167
170
  const ctx = createMockContext();
168
171
 
169
- await expect(
170
- acknowledgeRequest({ request_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
171
- ).rejects.toThrow('Failed to acknowledge request: Update failed');
172
+ const result = await acknowledgeRequest({ request_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
173
+
174
+ expect(result.isError).toBe(true);
175
+ expect(result.result).toMatchObject({
176
+ error: 'Update failed',
177
+ });
172
178
  });
173
179
  });
174
180
 
@@ -246,18 +252,21 @@ describe('answerQuestion', () => {
246
252
  );
247
253
  });
248
254
 
249
- it('should throw error when API call fails', async () => {
255
+ it('should return error when API call fails', async () => {
250
256
  mockApiClient.answerQuestion.mockResolvedValue({
251
257
  ok: false,
252
258
  error: 'Update failed',
253
259
  });
254
260
  const ctx = createMockContext();
255
261
 
256
- await expect(
257
- answerQuestion({
258
- request_id: '123e4567-e89b-12d3-a456-426614174000',
259
- answer: 'Answer',
260
- }, ctx)
261
- ).rejects.toThrow('Failed to answer question: Update failed');
262
+ const result = await answerQuestion({
263
+ request_id: '123e4567-e89b-12d3-a456-426614174000',
264
+ answer: 'Answer',
265
+ }, ctx);
266
+
267
+ expect(result.isError).toBe(true);
268
+ expect(result.result).toMatchObject({
269
+ error: 'Update failed',
270
+ });
262
271
  });
263
272
  });
@@ -10,14 +10,25 @@
10
10
  */
11
11
 
12
12
  import type { Handler, HandlerRegistry } from './types.js';
13
- import { validateRequired, validateUUID } from '../validators.js';
13
+ import { parseArgs, uuidValidator } from '../validators.js';
14
14
  import { getApiClient } from '../api-client.js';
15
15
 
16
- export const getPendingRequests: Handler = async (args, ctx) => {
17
- const { project_id } = args as { project_id: string };
16
+ // Argument schemas for type-safe parsing
17
+ const getPendingRequestsSchema = {
18
+ project_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
19
+ };
18
20
 
19
- validateRequired(project_id, 'project_id');
20
- validateUUID(project_id, 'project_id');
21
+ const acknowledgeRequestSchema = {
22
+ request_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
23
+ };
24
+
25
+ const answerQuestionSchema = {
26
+ request_id: { type: 'string' as const, required: true as const, validate: uuidValidator },
27
+ answer: { type: 'string' as const, required: true as const },
28
+ };
29
+
30
+ export const getPendingRequests: Handler = async (args, ctx) => {
31
+ const { project_id } = parseArgs(args, getPendingRequestsSchema);
21
32
 
22
33
  const { session } = ctx;
23
34
  const apiClient = getApiClient();
@@ -25,7 +36,7 @@ export const getPendingRequests: Handler = async (args, ctx) => {
25
36
  const response = await apiClient.getPendingRequests(project_id, session.currentSessionId || undefined);
26
37
 
27
38
  if (!response.ok) {
28
- throw new Error(`Failed to get pending requests: ${response.error}`);
39
+ return { result: { error: response.error || 'Failed to get pending requests' }, isError: true };
29
40
  }
30
41
 
31
42
  return {
@@ -37,10 +48,7 @@ export const getPendingRequests: Handler = async (args, ctx) => {
37
48
  };
38
49
 
39
50
  export const acknowledgeRequest: Handler = async (args, ctx) => {
40
- const { request_id } = args as { request_id: string };
41
-
42
- validateRequired(request_id, 'request_id');
43
- validateUUID(request_id, 'request_id');
51
+ const { request_id } = parseArgs(args, acknowledgeRequestSchema);
44
52
 
45
53
  const { session } = ctx;
46
54
  const apiClient = getApiClient();
@@ -48,7 +56,7 @@ export const acknowledgeRequest: Handler = async (args, ctx) => {
48
56
  const response = await apiClient.acknowledgeRequest(request_id, session.currentSessionId || undefined);
49
57
 
50
58
  if (!response.ok) {
51
- throw new Error(`Failed to acknowledge request: ${response.error}`);
59
+ return { result: { error: response.error || 'Failed to acknowledge request' }, isError: true };
52
60
  }
53
61
 
54
62
  return {
@@ -59,11 +67,7 @@ export const acknowledgeRequest: Handler = async (args, ctx) => {
59
67
  };
60
68
 
61
69
  export const answerQuestion: Handler = async (args, ctx) => {
62
- const { request_id, answer } = args as { request_id: string; answer: string };
63
-
64
- validateRequired(request_id, 'request_id');
65
- validateRequired(answer, 'answer');
66
- validateUUID(request_id, 'request_id');
70
+ const { request_id, answer } = parseArgs(args, answerQuestionSchema);
67
71
 
68
72
  const { session } = ctx;
69
73
  const apiClient = getApiClient();
@@ -71,7 +75,7 @@ export const answerQuestion: Handler = async (args, ctx) => {
71
75
  const response = await apiClient.answerQuestion(request_id, answer, session.currentSessionId || undefined);
72
76
 
73
77
  if (!response.ok) {
74
- throw new Error(`Failed to answer question: ${response.error}`);
78
+ return { result: { error: response.error || 'Failed to answer question' }, isError: true };
75
79
  }
76
80
 
77
81
  return {
@@ -0,0 +1,303 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ getRoleSettings,
4
+ updateRoleSettings,
5
+ setSessionRole,
6
+ getAgentsByRole,
7
+ } from './roles.js';
8
+ import { createMockContext } from './__test-utils__.js';
9
+ import { mockApiClient } from './__test-setup__.js';
10
+
11
+ // ============================================================================
12
+ // getRoleSettings Tests
13
+ // ============================================================================
14
+
15
+ describe('getRoleSettings', () => {
16
+ beforeEach(() => vi.clearAllMocks());
17
+
18
+ it('should return error for missing project_id', async () => {
19
+ const ctx = createMockContext();
20
+
21
+ const result = await getRoleSettings({}, ctx);
22
+
23
+ expect(result.result).toMatchObject({
24
+ error: 'project_id is required',
25
+ });
26
+ });
27
+
28
+ it('should get role settings successfully', async () => {
29
+ const mockRoles = [
30
+ { role: 'developer', enabled: true, display_name: 'Developer' },
31
+ { role: 'validator', enabled: true, display_name: 'Validator' },
32
+ ];
33
+ mockApiClient.proxy.mockResolvedValue({
34
+ ok: true,
35
+ data: { roles: mockRoles },
36
+ });
37
+ const ctx = createMockContext();
38
+
39
+ const result = await getRoleSettings(
40
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
41
+ ctx
42
+ );
43
+
44
+ expect(result.result).toMatchObject({ roles: mockRoles });
45
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
46
+ 'get_role_settings',
47
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' }
48
+ );
49
+ });
50
+
51
+ it('should return error from API', async () => {
52
+ mockApiClient.proxy.mockResolvedValue({
53
+ ok: false,
54
+ error: 'Project not found',
55
+ });
56
+ const ctx = createMockContext();
57
+
58
+ const result = await getRoleSettings(
59
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
60
+ ctx
61
+ );
62
+
63
+ expect(result.result).toMatchObject({
64
+ error: 'Project not found',
65
+ });
66
+ });
67
+ });
68
+
69
+ // ============================================================================
70
+ // updateRoleSettings Tests
71
+ // ============================================================================
72
+
73
+ describe('updateRoleSettings', () => {
74
+ beforeEach(() => vi.clearAllMocks());
75
+
76
+ it('should return error for missing project_id', async () => {
77
+ const ctx = createMockContext();
78
+
79
+ const result = await updateRoleSettings({ role: 'developer' }, ctx);
80
+
81
+ expect(result.result).toMatchObject({
82
+ error: 'project_id is required',
83
+ });
84
+ });
85
+
86
+ it('should return error for missing role', async () => {
87
+ const ctx = createMockContext();
88
+
89
+ const result = await updateRoleSettings(
90
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
91
+ ctx
92
+ );
93
+
94
+ expect(result.result).toMatchObject({
95
+ error: 'role is required',
96
+ });
97
+ });
98
+
99
+ it('should update role settings successfully', async () => {
100
+ mockApiClient.proxy.mockResolvedValue({
101
+ ok: true,
102
+ data: { success: true, role: 'validator' },
103
+ });
104
+ const ctx = createMockContext();
105
+
106
+ const result = await updateRoleSettings(
107
+ {
108
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
109
+ role: 'validator',
110
+ enabled: true,
111
+ auto_assign_validation: true,
112
+ },
113
+ ctx
114
+ );
115
+
116
+ expect(result.result).toMatchObject({
117
+ success: true,
118
+ role: 'validator',
119
+ });
120
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
121
+ 'update_role_settings',
122
+ expect.objectContaining({
123
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
124
+ role: 'validator',
125
+ enabled: true,
126
+ auto_assign_validation: true,
127
+ })
128
+ );
129
+ });
130
+
131
+ it('should return error from API', async () => {
132
+ mockApiClient.proxy.mockResolvedValue({
133
+ ok: false,
134
+ error: 'Access denied',
135
+ });
136
+ const ctx = createMockContext();
137
+
138
+ const result = await updateRoleSettings(
139
+ {
140
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
141
+ role: 'validator',
142
+ },
143
+ ctx
144
+ );
145
+
146
+ expect(result.result).toMatchObject({
147
+ error: 'Access denied',
148
+ });
149
+ });
150
+ });
151
+
152
+ // ============================================================================
153
+ // setSessionRole Tests
154
+ // ============================================================================
155
+
156
+ describe('setSessionRole', () => {
157
+ beforeEach(() => vi.clearAllMocks());
158
+
159
+ it('should return error for missing role', async () => {
160
+ const ctx = createMockContext();
161
+
162
+ const result = await setSessionRole({}, ctx);
163
+
164
+ expect(result.result).toMatchObject({
165
+ error: 'role is required',
166
+ });
167
+ });
168
+
169
+ it('should return error for invalid role', async () => {
170
+ const ctx = createMockContext();
171
+
172
+ const result = await setSessionRole({ role: 'invalid_role' }, ctx);
173
+
174
+ expect(result.result).toMatchObject({
175
+ error: expect.stringContaining('Invalid role: invalid_role'),
176
+ });
177
+ });
178
+
179
+ it('should set local role when no active session', async () => {
180
+ const ctx = createMockContext({ sessionId: null });
181
+
182
+ const result = await setSessionRole({ role: 'validator' }, ctx);
183
+
184
+ expect(result.result).toMatchObject({
185
+ success: true,
186
+ role: 'validator',
187
+ message: expect.stringContaining('Local role set to validator'),
188
+ });
189
+ });
190
+
191
+ it('should update session role on server when session active', async () => {
192
+ mockApiClient.proxy.mockResolvedValue({
193
+ ok: true,
194
+ data: { success: true, session_id: 'session-123', role: 'deployer' },
195
+ });
196
+ const ctx = createMockContext({ sessionId: 'session-123' });
197
+
198
+ const result = await setSessionRole({ role: 'deployer' }, ctx);
199
+
200
+ expect(result.result).toMatchObject({
201
+ success: true,
202
+ session_id: 'session-123',
203
+ role: 'deployer',
204
+ });
205
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
206
+ 'set_session_role',
207
+ expect.objectContaining({
208
+ session_id: 'session-123',
209
+ role: 'deployer',
210
+ })
211
+ );
212
+ });
213
+
214
+ it('should update local state when setting role', async () => {
215
+ const updateSession = vi.fn();
216
+ const ctx = createMockContext({ sessionId: null });
217
+ ctx.updateSession = updateSession;
218
+
219
+ await setSessionRole({ role: 'reviewer' }, ctx);
220
+
221
+ expect(updateSession).toHaveBeenCalledWith({ currentRole: 'reviewer' });
222
+ });
223
+
224
+ it('should return error from API when updating session', async () => {
225
+ mockApiClient.proxy.mockResolvedValue({
226
+ ok: false,
227
+ error: 'Session not found',
228
+ });
229
+ const ctx = createMockContext({ sessionId: 'session-123' });
230
+
231
+ const result = await setSessionRole({ role: 'maintainer' }, ctx);
232
+
233
+ expect(result.result).toMatchObject({
234
+ error: 'Session not found',
235
+ });
236
+ });
237
+ });
238
+
239
+ // ============================================================================
240
+ // getAgentsByRole Tests
241
+ // ============================================================================
242
+
243
+ describe('getAgentsByRole', () => {
244
+ beforeEach(() => vi.clearAllMocks());
245
+
246
+ it('should return error for missing project_id', async () => {
247
+ const ctx = createMockContext();
248
+
249
+ const result = await getAgentsByRole({}, ctx);
250
+
251
+ expect(result.result).toMatchObject({
252
+ error: 'project_id is required',
253
+ });
254
+ });
255
+
256
+ it('should get agents by role successfully', async () => {
257
+ const mockAgentsByRole = {
258
+ developer: [
259
+ { session_id: 'session-1', agent_name: 'Edge', status: 'active' },
260
+ ],
261
+ validator: [],
262
+ deployer: [],
263
+ reviewer: [],
264
+ maintainer: [],
265
+ };
266
+ mockApiClient.proxy.mockResolvedValue({
267
+ ok: true,
268
+ data: { agents_by_role: mockAgentsByRole, total_active: 1 },
269
+ });
270
+ const ctx = createMockContext();
271
+
272
+ const result = await getAgentsByRole(
273
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
274
+ ctx
275
+ );
276
+
277
+ expect(result.result).toMatchObject({
278
+ agents_by_role: mockAgentsByRole,
279
+ total_active: 1,
280
+ });
281
+ expect(mockApiClient.proxy).toHaveBeenCalledWith(
282
+ 'get_agents_by_role',
283
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' }
284
+ );
285
+ });
286
+
287
+ it('should return error from API', async () => {
288
+ mockApiClient.proxy.mockResolvedValue({
289
+ ok: false,
290
+ error: 'Project not found',
291
+ });
292
+ const ctx = createMockContext();
293
+
294
+ const result = await getAgentsByRole(
295
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
296
+ ctx
297
+ );
298
+
299
+ expect(result.result).toMatchObject({
300
+ error: 'Project not found',
301
+ });
302
+ });
303
+ });