@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,209 +1,158 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
3
|
-
import type { HandlerContext } from './types.js';
|
|
4
2
|
import {
|
|
5
3
|
getTasks,
|
|
4
|
+
getNextTask,
|
|
6
5
|
addTask,
|
|
7
6
|
updateTask,
|
|
8
7
|
completeTask,
|
|
9
8
|
deleteTask,
|
|
10
9
|
addTaskReference,
|
|
11
10
|
removeTaskReference,
|
|
12
|
-
|
|
11
|
+
batchUpdateTasks,
|
|
12
|
+
batchCompleteTasks,
|
|
13
|
+
addSubtask,
|
|
14
|
+
getSubtasks,
|
|
15
|
+
getValidationApprovedGitInstructions,
|
|
13
16
|
} from './tasks.js';
|
|
14
17
|
import { ValidationError } from '../validators.js';
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
// Test Utilities
|
|
18
|
-
// ============================================================================
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Creates a mock Supabase client with chainable methods
|
|
22
|
-
*/
|
|
23
|
-
function createMockSupabase(overrides: {
|
|
24
|
-
selectResult?: { data: unknown; error: unknown };
|
|
25
|
-
insertResult?: { data: unknown; error: unknown };
|
|
26
|
-
updateResult?: { data: unknown; error: unknown };
|
|
27
|
-
deleteResult?: { data: unknown; error: unknown };
|
|
28
|
-
} = {}) {
|
|
29
|
-
const defaultResult = { data: null, error: null };
|
|
30
|
-
|
|
31
|
-
// Track both the operation type AND if insert has been followed by select
|
|
32
|
-
let currentOperation = 'select';
|
|
33
|
-
let insertThenSelect = false;
|
|
34
|
-
|
|
35
|
-
const mock = {
|
|
36
|
-
from: vi.fn().mockReturnThis(),
|
|
37
|
-
select: vi.fn(() => {
|
|
38
|
-
// If we just did an insert and now calling select, it's insert().select() chain
|
|
39
|
-
if (currentOperation === 'insert') {
|
|
40
|
-
insertThenSelect = true;
|
|
41
|
-
} else {
|
|
42
|
-
currentOperation = 'select';
|
|
43
|
-
insertThenSelect = false;
|
|
44
|
-
}
|
|
45
|
-
return mock;
|
|
46
|
-
}),
|
|
47
|
-
insert: vi.fn(() => {
|
|
48
|
-
currentOperation = 'insert';
|
|
49
|
-
insertThenSelect = false;
|
|
50
|
-
return mock;
|
|
51
|
-
}),
|
|
52
|
-
update: vi.fn(() => {
|
|
53
|
-
currentOperation = 'update';
|
|
54
|
-
insertThenSelect = false;
|
|
55
|
-
return mock;
|
|
56
|
-
}),
|
|
57
|
-
delete: vi.fn(() => {
|
|
58
|
-
currentOperation = 'delete';
|
|
59
|
-
insertThenSelect = false;
|
|
60
|
-
return mock;
|
|
61
|
-
}),
|
|
62
|
-
eq: vi.fn().mockReturnThis(),
|
|
63
|
-
neq: vi.fn().mockReturnThis(),
|
|
64
|
-
in: vi.fn().mockReturnThis(),
|
|
65
|
-
is: vi.fn().mockReturnThis(),
|
|
66
|
-
not: vi.fn().mockReturnThis(),
|
|
67
|
-
or: vi.fn().mockReturnThis(),
|
|
68
|
-
lt: vi.fn().mockReturnThis(),
|
|
69
|
-
order: vi.fn().mockReturnThis(),
|
|
70
|
-
limit: vi.fn().mockReturnThis(),
|
|
71
|
-
single: vi.fn(() => {
|
|
72
|
-
// Handle insert().select().single() pattern
|
|
73
|
-
if (currentOperation === 'insert' || insertThenSelect) {
|
|
74
|
-
return Promise.resolve(overrides.insertResult ?? defaultResult);
|
|
75
|
-
}
|
|
76
|
-
if (currentOperation === 'select') {
|
|
77
|
-
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
78
|
-
}
|
|
79
|
-
if (currentOperation === 'update') {
|
|
80
|
-
return Promise.resolve(overrides.updateResult ?? defaultResult);
|
|
81
|
-
}
|
|
82
|
-
return Promise.resolve(defaultResult);
|
|
83
|
-
}),
|
|
84
|
-
maybeSingle: vi.fn(() => {
|
|
85
|
-
return Promise.resolve(overrides.selectResult ?? defaultResult);
|
|
86
|
-
}),
|
|
87
|
-
then: vi.fn((resolve: (value: unknown) => void) => {
|
|
88
|
-
if (currentOperation === 'insert' || insertThenSelect) {
|
|
89
|
-
return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
|
|
90
|
-
}
|
|
91
|
-
if (currentOperation === 'select') {
|
|
92
|
-
return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
|
|
93
|
-
}
|
|
94
|
-
if (currentOperation === 'update') {
|
|
95
|
-
return Promise.resolve(overrides.updateResult ?? defaultResult).then(resolve);
|
|
96
|
-
}
|
|
97
|
-
if (currentOperation === 'delete') {
|
|
98
|
-
return Promise.resolve(overrides.deleteResult ?? defaultResult).then(resolve);
|
|
99
|
-
}
|
|
100
|
-
return Promise.resolve(defaultResult).then(resolve);
|
|
101
|
-
}),
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
return mock as unknown as SupabaseClient;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Creates a mock handler context
|
|
109
|
-
*/
|
|
110
|
-
function createMockContext(supabase: SupabaseClient, sessionId: string | null = 'session-123'): HandlerContext {
|
|
111
|
-
return {
|
|
112
|
-
supabase,
|
|
113
|
-
auth: {
|
|
114
|
-
userId: 'user-123',
|
|
115
|
-
apiKeyId: 'api-key-123',
|
|
116
|
-
},
|
|
117
|
-
session: {
|
|
118
|
-
currentSessionId: sessionId,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
}
|
|
18
|
+
import { createMockContext } from './__test-utils__.js';
|
|
19
|
+
import { mockApiClient } from './__test-setup__.js';
|
|
122
20
|
|
|
123
21
|
// ============================================================================
|
|
124
22
|
// getTasks Tests
|
|
125
23
|
// ============================================================================
|
|
126
24
|
|
|
127
25
|
describe('getTasks', () => {
|
|
128
|
-
beforeEach(() =>
|
|
129
|
-
|
|
26
|
+
beforeEach(() => vi.clearAllMocks());
|
|
27
|
+
|
|
28
|
+
it('should throw error for missing project_id', async () => {
|
|
29
|
+
const ctx = createMockContext();
|
|
30
|
+
await expect(getTasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
130
31
|
});
|
|
131
32
|
|
|
132
|
-
it('should
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
];
|
|
33
|
+
it('should throw error for invalid project_id UUID', async () => {
|
|
34
|
+
const ctx = createMockContext();
|
|
35
|
+
await expect(getTasks({ project_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
|
|
36
|
+
});
|
|
137
37
|
|
|
138
|
-
|
|
139
|
-
|
|
38
|
+
it('should throw error for invalid status', async () => {
|
|
39
|
+
const ctx = createMockContext();
|
|
40
|
+
await expect(
|
|
41
|
+
getTasks({ project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'invalid' }, ctx)
|
|
42
|
+
).rejects.toThrow(ValidationError);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should return tasks successfully', async () => {
|
|
46
|
+
mockApiClient.getTasks.mockResolvedValue({
|
|
47
|
+
ok: true,
|
|
48
|
+
data: {
|
|
49
|
+
tasks: [
|
|
50
|
+
{ id: 'task-1', title: 'Test task', status: 'pending', priority: 1 },
|
|
51
|
+
],
|
|
52
|
+
total_count: 1,
|
|
53
|
+
has_more: false,
|
|
54
|
+
},
|
|
140
55
|
});
|
|
141
|
-
const ctx = createMockContext(supabase);
|
|
142
56
|
|
|
57
|
+
const ctx = createMockContext();
|
|
143
58
|
const result = await getTasks(
|
|
144
59
|
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
145
60
|
ctx
|
|
146
61
|
);
|
|
147
62
|
|
|
148
|
-
expect(result.result
|
|
149
|
-
|
|
63
|
+
expect(result.result).toMatchObject({
|
|
64
|
+
tasks: expect.any(Array),
|
|
65
|
+
total_count: 1,
|
|
66
|
+
has_more: false,
|
|
67
|
+
});
|
|
68
|
+
expect(mockApiClient.getTasks).toHaveBeenCalledWith(
|
|
69
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
70
|
+
expect.any(Object)
|
|
71
|
+
);
|
|
150
72
|
});
|
|
151
73
|
|
|
152
|
-
it('should
|
|
153
|
-
|
|
154
|
-
|
|
74
|
+
it('should pass status filter to API', async () => {
|
|
75
|
+
mockApiClient.getTasks.mockResolvedValue({
|
|
76
|
+
ok: true,
|
|
77
|
+
data: { tasks: [], total_count: 0, has_more: false },
|
|
78
|
+
});
|
|
155
79
|
|
|
156
|
-
|
|
80
|
+
const ctx = createMockContext();
|
|
81
|
+
await getTasks(
|
|
82
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000', status: 'pending' },
|
|
83
|
+
ctx
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
expect(mockApiClient.getTasks).toHaveBeenCalledWith(
|
|
87
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
88
|
+
expect.objectContaining({ status: 'pending' })
|
|
89
|
+
);
|
|
157
90
|
});
|
|
91
|
+
});
|
|
158
92
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// getNextTask Tests
|
|
95
|
+
// ============================================================================
|
|
162
96
|
|
|
163
|
-
|
|
97
|
+
describe('getNextTask', () => {
|
|
98
|
+
beforeEach(() => vi.clearAllMocks());
|
|
99
|
+
|
|
100
|
+
it('should throw error for missing project_id', async () => {
|
|
101
|
+
const ctx = createMockContext();
|
|
102
|
+
await expect(getNextTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
164
103
|
});
|
|
165
104
|
|
|
166
|
-
it('should
|
|
167
|
-
const
|
|
105
|
+
it('should throw error for invalid project_id', async () => {
|
|
106
|
+
const ctx = createMockContext();
|
|
107
|
+
await expect(getNextTask({ project_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
|
|
108
|
+
});
|
|
168
109
|
|
|
169
|
-
|
|
170
|
-
|
|
110
|
+
it('should return next task when available', async () => {
|
|
111
|
+
mockApiClient.getNextTask.mockResolvedValue({
|
|
112
|
+
ok: true,
|
|
113
|
+
data: {
|
|
114
|
+
task: { id: 'task-1', title: 'Next task', priority: 1 },
|
|
115
|
+
directive: 'Start working',
|
|
116
|
+
},
|
|
171
117
|
});
|
|
172
|
-
const ctx = createMockContext(supabase);
|
|
173
118
|
|
|
174
|
-
|
|
175
|
-
|
|
119
|
+
const ctx = createMockContext();
|
|
120
|
+
const result = await getNextTask(
|
|
121
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
176
122
|
ctx
|
|
177
123
|
);
|
|
178
124
|
|
|
179
|
-
expect(
|
|
125
|
+
expect(result.result.task).toMatchObject({ id: 'task-1' });
|
|
180
126
|
});
|
|
181
127
|
|
|
182
|
-
it('should
|
|
183
|
-
|
|
184
|
-
|
|
128
|
+
it('should return null when no tasks available', async () => {
|
|
129
|
+
mockApiClient.getNextTask.mockResolvedValue({
|
|
130
|
+
ok: true,
|
|
131
|
+
data: { task: null, message: 'No tasks available' },
|
|
132
|
+
});
|
|
185
133
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
134
|
+
const ctx = createMockContext();
|
|
135
|
+
const result = await getNextTask(
|
|
136
|
+
{ project_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
137
|
+
ctx
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
expect(result.result.task).toBeNull();
|
|
189
141
|
});
|
|
190
142
|
|
|
191
|
-
it('should
|
|
192
|
-
|
|
193
|
-
|
|
143
|
+
it('should pass session_id to API', async () => {
|
|
144
|
+
mockApiClient.getNextTask.mockResolvedValue({
|
|
145
|
+
ok: true,
|
|
146
|
+
data: { task: null },
|
|
194
147
|
});
|
|
195
|
-
const ctx = createMockContext(supabase);
|
|
196
148
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
...supabase,
|
|
200
|
-
then: (resolve: (val: unknown) => void) =>
|
|
201
|
-
Promise.resolve({ data: null, error: { message: 'Database error' } }).then(resolve),
|
|
202
|
-
} as unknown as ReturnType<SupabaseClient['from']>);
|
|
149
|
+
const ctx = createMockContext({ sessionId: 'my-session' });
|
|
150
|
+
await getNextTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx);
|
|
203
151
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
152
|
+
expect(mockApiClient.getNextTask).toHaveBeenCalledWith(
|
|
153
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
154
|
+
'my-session'
|
|
155
|
+
);
|
|
207
156
|
});
|
|
208
157
|
});
|
|
209
158
|
|
|
@@ -212,224 +161,375 @@ describe('getTasks', () => {
|
|
|
212
161
|
// ============================================================================
|
|
213
162
|
|
|
214
163
|
describe('addTask', () => {
|
|
215
|
-
beforeEach(() =>
|
|
216
|
-
|
|
164
|
+
beforeEach(() => vi.clearAllMocks());
|
|
165
|
+
|
|
166
|
+
it('should throw error for missing project_id', async () => {
|
|
167
|
+
const ctx = createMockContext();
|
|
168
|
+
await expect(addTask({ title: 'Test task' }, ctx)).rejects.toThrow(ValidationError);
|
|
217
169
|
});
|
|
218
170
|
|
|
219
|
-
it('should
|
|
220
|
-
const
|
|
221
|
-
|
|
171
|
+
it('should throw error for invalid project_id', async () => {
|
|
172
|
+
const ctx = createMockContext();
|
|
173
|
+
await expect(
|
|
174
|
+
addTask({ project_id: 'invalid', title: 'Test task' }, ctx)
|
|
175
|
+
).rejects.toThrow(ValidationError);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should throw error for missing title', async () => {
|
|
179
|
+
const ctx = createMockContext();
|
|
180
|
+
await expect(
|
|
181
|
+
addTask({ project_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
182
|
+
).rejects.toThrow(ValidationError);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should throw error for invalid priority', async () => {
|
|
186
|
+
const ctx = createMockContext();
|
|
187
|
+
await expect(
|
|
188
|
+
addTask({
|
|
189
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
190
|
+
title: 'Test',
|
|
191
|
+
priority: 10,
|
|
192
|
+
}, ctx)
|
|
193
|
+
).rejects.toThrow(ValidationError);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should add task successfully', async () => {
|
|
197
|
+
mockApiClient.createTask.mockResolvedValue({
|
|
198
|
+
ok: true,
|
|
199
|
+
data: { task_id: 'new-task-id', title: 'Test task' },
|
|
222
200
|
});
|
|
223
|
-
const ctx = createMockContext(supabase);
|
|
224
201
|
|
|
202
|
+
const ctx = createMockContext();
|
|
225
203
|
const result = await addTask(
|
|
226
204
|
{
|
|
227
205
|
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
228
|
-
title: '
|
|
229
|
-
|
|
206
|
+
title: 'Test task',
|
|
207
|
+
},
|
|
208
|
+
ctx
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(result.result).toMatchObject({
|
|
212
|
+
success: true,
|
|
213
|
+
task_id: 'new-task-id',
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should include optional fields in API call', async () => {
|
|
218
|
+
mockApiClient.createTask.mockResolvedValue({
|
|
219
|
+
ok: true,
|
|
220
|
+
data: { task_id: 'new-task-id' },
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const ctx = createMockContext();
|
|
224
|
+
await addTask(
|
|
225
|
+
{
|
|
226
|
+
project_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
227
|
+
title: 'Test task',
|
|
228
|
+
description: 'A description',
|
|
230
229
|
priority: 2,
|
|
230
|
+
estimated_minutes: 30,
|
|
231
|
+
blocking: true,
|
|
231
232
|
},
|
|
232
233
|
ctx
|
|
233
234
|
);
|
|
234
235
|
|
|
235
|
-
expect(
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
expect(mockApiClient.createTask).toHaveBeenCalledWith(
|
|
237
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
238
|
+
expect.objectContaining({
|
|
239
|
+
title: 'Test task',
|
|
240
|
+
description: 'A description',
|
|
241
|
+
priority: 2,
|
|
242
|
+
estimated_minutes: 30,
|
|
243
|
+
blocking: true,
|
|
244
|
+
})
|
|
245
|
+
);
|
|
240
246
|
});
|
|
247
|
+
});
|
|
241
248
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// updateTask Tests
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
describe('updateTask', () => {
|
|
254
|
+
beforeEach(() => vi.clearAllMocks());
|
|
245
255
|
|
|
246
|
-
|
|
256
|
+
it('should throw error for missing task_id', async () => {
|
|
257
|
+
const ctx = createMockContext();
|
|
258
|
+
await expect(updateTask({ status: 'in_progress' }, ctx)).rejects.toThrow(ValidationError);
|
|
247
259
|
});
|
|
248
260
|
|
|
249
|
-
it('should throw error for
|
|
250
|
-
const
|
|
251
|
-
|
|
261
|
+
it('should throw error for invalid task_id', async () => {
|
|
262
|
+
const ctx = createMockContext();
|
|
263
|
+
await expect(
|
|
264
|
+
updateTask({ task_id: 'invalid', status: 'in_progress' }, ctx)
|
|
265
|
+
).rejects.toThrow(ValidationError);
|
|
266
|
+
});
|
|
252
267
|
|
|
268
|
+
it('should throw error for invalid status', async () => {
|
|
269
|
+
const ctx = createMockContext();
|
|
253
270
|
await expect(
|
|
254
|
-
|
|
271
|
+
updateTask({
|
|
272
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
273
|
+
status: 'invalid',
|
|
274
|
+
}, ctx)
|
|
255
275
|
).rejects.toThrow(ValidationError);
|
|
256
276
|
});
|
|
257
277
|
|
|
258
278
|
it('should throw error for invalid priority', async () => {
|
|
259
|
-
const
|
|
260
|
-
const ctx = createMockContext(supabase);
|
|
261
|
-
|
|
279
|
+
const ctx = createMockContext();
|
|
262
280
|
await expect(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
priority: 10, // Invalid: should be 1-5
|
|
281
|
+
updateTask({
|
|
282
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
283
|
+
priority: 0,
|
|
267
284
|
}, ctx)
|
|
268
285
|
).rejects.toThrow(ValidationError);
|
|
269
286
|
});
|
|
270
287
|
|
|
271
|
-
it('should throw error for invalid
|
|
272
|
-
const
|
|
273
|
-
const ctx = createMockContext(supabase);
|
|
274
|
-
|
|
288
|
+
it('should throw error for invalid progress_percentage', async () => {
|
|
289
|
+
const ctx = createMockContext();
|
|
275
290
|
await expect(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
estimated_minutes: 0, // Invalid: must be positive
|
|
291
|
+
updateTask({
|
|
292
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
293
|
+
progress_percentage: 150,
|
|
280
294
|
}, ctx)
|
|
281
295
|
).rejects.toThrow(ValidationError);
|
|
282
296
|
});
|
|
283
297
|
|
|
284
|
-
it('should
|
|
285
|
-
|
|
286
|
-
|
|
298
|
+
it('should update task successfully', async () => {
|
|
299
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
300
|
+
ok: true,
|
|
301
|
+
data: { success: true },
|
|
287
302
|
});
|
|
288
|
-
const ctx = createMockContext(supabase);
|
|
289
303
|
|
|
290
|
-
const
|
|
304
|
+
const ctx = createMockContext();
|
|
305
|
+
const result = await updateTask(
|
|
291
306
|
{
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
blocking: true,
|
|
307
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
308
|
+
status: 'in_progress',
|
|
295
309
|
},
|
|
296
310
|
ctx
|
|
297
311
|
);
|
|
298
312
|
|
|
299
|
-
expect(result.result
|
|
300
|
-
expect(result.result.message).toContain('BLOCKING TASK');
|
|
313
|
+
expect(result.result).toMatchObject({ success: true });
|
|
301
314
|
});
|
|
302
315
|
|
|
303
|
-
it('should
|
|
304
|
-
|
|
305
|
-
|
|
316
|
+
it('should handle agent_task_limit error', async () => {
|
|
317
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
318
|
+
ok: false,
|
|
319
|
+
error: 'agent_task_limit: Agent already has a task in progress',
|
|
306
320
|
});
|
|
307
|
-
const ctx = createMockContext(supabase);
|
|
308
321
|
|
|
309
|
-
|
|
322
|
+
const ctx = createMockContext();
|
|
323
|
+
const result = await updateTask(
|
|
310
324
|
{
|
|
311
|
-
|
|
312
|
-
|
|
325
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
326
|
+
status: 'in_progress',
|
|
313
327
|
},
|
|
314
328
|
ctx
|
|
315
329
|
);
|
|
316
330
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
331
|
+
expect(result.result).toMatchObject({
|
|
332
|
+
error: 'agent_task_limit',
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should handle task_claimed error', async () => {
|
|
337
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
338
|
+
ok: false,
|
|
339
|
+
error: 'task_claimed: Task is being worked on by another agent',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
const ctx = createMockContext();
|
|
343
|
+
const result = await updateTask(
|
|
344
|
+
{
|
|
345
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
346
|
+
status: 'in_progress',
|
|
347
|
+
},
|
|
348
|
+
ctx
|
|
320
349
|
);
|
|
350
|
+
|
|
351
|
+
expect(result.result).toMatchObject({
|
|
352
|
+
error: 'task_claimed',
|
|
353
|
+
});
|
|
321
354
|
});
|
|
322
355
|
});
|
|
323
356
|
|
|
324
357
|
// ============================================================================
|
|
325
|
-
//
|
|
358
|
+
// completeTask Tests
|
|
326
359
|
// ============================================================================
|
|
327
360
|
|
|
328
|
-
describe('
|
|
329
|
-
beforeEach(() =>
|
|
330
|
-
|
|
361
|
+
describe('completeTask', () => {
|
|
362
|
+
beforeEach(() => vi.clearAllMocks());
|
|
363
|
+
|
|
364
|
+
it('should throw error for missing task_id', async () => {
|
|
365
|
+
const ctx = createMockContext();
|
|
366
|
+
await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
331
367
|
});
|
|
332
368
|
|
|
333
|
-
it('should
|
|
334
|
-
const
|
|
335
|
-
|
|
369
|
+
it('should throw error for invalid task_id', async () => {
|
|
370
|
+
const ctx = createMockContext();
|
|
371
|
+
await expect(
|
|
372
|
+
completeTask({ task_id: 'invalid' }, ctx)
|
|
373
|
+
).rejects.toThrow(ValidationError);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('should complete task successfully', async () => {
|
|
377
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
378
|
+
ok: true,
|
|
379
|
+
data: {
|
|
380
|
+
success: true,
|
|
381
|
+
directive: 'Start next task',
|
|
382
|
+
auto_continue: true,
|
|
383
|
+
completed_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
384
|
+
next_task: { id: 'task-2', title: 'Next task' },
|
|
385
|
+
},
|
|
336
386
|
});
|
|
337
|
-
const ctx = createMockContext(supabase);
|
|
338
387
|
|
|
339
|
-
const
|
|
388
|
+
const ctx = createMockContext();
|
|
389
|
+
const result = await completeTask(
|
|
340
390
|
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
341
391
|
ctx
|
|
342
392
|
);
|
|
343
393
|
|
|
344
|
-
expect(result.result
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
394
|
+
expect(result.result).toMatchObject({
|
|
395
|
+
success: true,
|
|
396
|
+
auto_continue: true,
|
|
397
|
+
next_task: expect.any(Object),
|
|
398
|
+
});
|
|
348
399
|
});
|
|
349
400
|
|
|
350
|
-
it('should
|
|
351
|
-
|
|
352
|
-
|
|
401
|
+
it('should include summary in API call', async () => {
|
|
402
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
403
|
+
ok: true,
|
|
404
|
+
data: { success: true },
|
|
405
|
+
});
|
|
353
406
|
|
|
354
|
-
|
|
407
|
+
const ctx = createMockContext();
|
|
408
|
+
await completeTask(
|
|
409
|
+
{
|
|
410
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
411
|
+
summary: 'Completed the feature',
|
|
412
|
+
},
|
|
413
|
+
ctx
|
|
414
|
+
);
|
|
415
|
+
|
|
416
|
+
expect(mockApiClient.completeTask).toHaveBeenCalledWith(
|
|
417
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
418
|
+
expect.objectContaining({ summary: 'Completed the feature' })
|
|
419
|
+
);
|
|
355
420
|
});
|
|
356
421
|
|
|
357
|
-
it('should throw error
|
|
358
|
-
|
|
359
|
-
|
|
422
|
+
it('should throw error when API returns error', async () => {
|
|
423
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
424
|
+
ok: false,
|
|
425
|
+
error: 'Task not found',
|
|
426
|
+
});
|
|
360
427
|
|
|
361
|
-
|
|
428
|
+
const ctx = createMockContext();
|
|
429
|
+
await expect(
|
|
430
|
+
completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
431
|
+
).rejects.toThrow('Failed to complete task: Task not found');
|
|
362
432
|
});
|
|
363
433
|
});
|
|
364
434
|
|
|
365
435
|
// ============================================================================
|
|
366
|
-
//
|
|
436
|
+
// deleteTask Tests
|
|
367
437
|
// ============================================================================
|
|
368
438
|
|
|
369
|
-
describe('
|
|
370
|
-
beforeEach(() =>
|
|
371
|
-
|
|
439
|
+
describe('deleteTask', () => {
|
|
440
|
+
beforeEach(() => vi.clearAllMocks());
|
|
441
|
+
|
|
442
|
+
it('should throw error for missing task_id', async () => {
|
|
443
|
+
const ctx = createMockContext();
|
|
444
|
+
await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
372
445
|
});
|
|
373
446
|
|
|
374
|
-
it('should
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
447
|
+
it('should throw error for invalid task_id', async () => {
|
|
448
|
+
const ctx = createMockContext();
|
|
449
|
+
await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should delete task successfully', async () => {
|
|
453
|
+
mockApiClient.deleteTask.mockResolvedValue({
|
|
454
|
+
ok: true,
|
|
455
|
+
data: { success: true },
|
|
378
456
|
});
|
|
379
|
-
const ctx = createMockContext(supabase);
|
|
380
457
|
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
url: 'https://github.com/user/repo/pull/123',
|
|
385
|
-
label: 'PR #123',
|
|
386
|
-
},
|
|
458
|
+
const ctx = createMockContext();
|
|
459
|
+
const result = await deleteTask(
|
|
460
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
387
461
|
ctx
|
|
388
462
|
);
|
|
389
463
|
|
|
390
|
-
expect(result.result
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
label: 'PR #123',
|
|
464
|
+
expect(result.result).toMatchObject({
|
|
465
|
+
success: true,
|
|
466
|
+
deleted_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
394
467
|
});
|
|
395
|
-
expect(result.result.total_references).toBe(1);
|
|
396
468
|
});
|
|
469
|
+
});
|
|
397
470
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
471
|
+
// ============================================================================
|
|
472
|
+
// addTaskReference Tests
|
|
473
|
+
// ============================================================================
|
|
401
474
|
|
|
475
|
+
describe('addTaskReference', () => {
|
|
476
|
+
beforeEach(() => vi.clearAllMocks());
|
|
477
|
+
|
|
478
|
+
it('should throw error for missing task_id', async () => {
|
|
479
|
+
const ctx = createMockContext();
|
|
402
480
|
await expect(
|
|
403
481
|
addTaskReference({ url: 'https://example.com' }, ctx)
|
|
404
482
|
).rejects.toThrow(ValidationError);
|
|
405
483
|
});
|
|
406
484
|
|
|
407
485
|
it('should throw error for missing url', async () => {
|
|
408
|
-
const
|
|
409
|
-
const ctx = createMockContext(supabase);
|
|
410
|
-
|
|
486
|
+
const ctx = createMockContext();
|
|
411
487
|
await expect(
|
|
412
488
|
addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
413
489
|
).rejects.toThrow(ValidationError);
|
|
414
490
|
});
|
|
415
491
|
|
|
416
|
-
it('should
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
492
|
+
it('should add reference successfully', async () => {
|
|
493
|
+
mockApiClient.addTaskReference.mockResolvedValue({
|
|
494
|
+
ok: true,
|
|
495
|
+
data: { reference: { url: 'https://example.com', label: 'Test' } },
|
|
420
496
|
});
|
|
421
|
-
const ctx = createMockContext(supabase);
|
|
422
497
|
|
|
498
|
+
const ctx = createMockContext();
|
|
423
499
|
const result = await addTaskReference(
|
|
424
500
|
{
|
|
425
501
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
426
502
|
url: 'https://example.com',
|
|
503
|
+
label: 'Test',
|
|
427
504
|
},
|
|
428
505
|
ctx
|
|
429
506
|
);
|
|
430
507
|
|
|
431
|
-
expect(result.result
|
|
432
|
-
|
|
508
|
+
expect(result.result).toMatchObject({
|
|
509
|
+
success: true,
|
|
510
|
+
reference: expect.any(Object),
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('should handle duplicate reference error', async () => {
|
|
515
|
+
mockApiClient.addTaskReference.mockResolvedValue({
|
|
516
|
+
ok: false,
|
|
517
|
+
error: 'Reference already exists',
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
const ctx = createMockContext();
|
|
521
|
+
const result = await addTaskReference(
|
|
522
|
+
{
|
|
523
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
524
|
+
url: 'https://example.com',
|
|
525
|
+
},
|
|
526
|
+
ctx
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
expect(result.result).toMatchObject({
|
|
530
|
+
success: false,
|
|
531
|
+
error: expect.stringContaining('already exists'),
|
|
532
|
+
});
|
|
433
533
|
});
|
|
434
534
|
});
|
|
435
535
|
|
|
@@ -438,18 +538,29 @@ describe('addTaskReference', () => {
|
|
|
438
538
|
// ============================================================================
|
|
439
539
|
|
|
440
540
|
describe('removeTaskReference', () => {
|
|
441
|
-
beforeEach(() =>
|
|
442
|
-
|
|
541
|
+
beforeEach(() => vi.clearAllMocks());
|
|
542
|
+
|
|
543
|
+
it('should throw error for missing task_id', async () => {
|
|
544
|
+
const ctx = createMockContext();
|
|
545
|
+
await expect(
|
|
546
|
+
removeTaskReference({ url: 'https://example.com' }, ctx)
|
|
547
|
+
).rejects.toThrow(ValidationError);
|
|
443
548
|
});
|
|
444
549
|
|
|
445
|
-
it('should
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
550
|
+
it('should throw error for missing url', async () => {
|
|
551
|
+
const ctx = createMockContext();
|
|
552
|
+
await expect(
|
|
553
|
+
removeTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
554
|
+
).rejects.toThrow(ValidationError);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it('should remove reference successfully', async () => {
|
|
558
|
+
mockApiClient.removeTaskReference.mockResolvedValue({
|
|
559
|
+
ok: true,
|
|
560
|
+
data: { success: true },
|
|
450
561
|
});
|
|
451
|
-
const ctx = createMockContext(supabase);
|
|
452
562
|
|
|
563
|
+
const ctx = createMockContext();
|
|
453
564
|
const result = await removeTaskReference(
|
|
454
565
|
{
|
|
455
566
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
@@ -458,145 +569,289 @@ describe('removeTaskReference', () => {
|
|
|
458
569
|
ctx
|
|
459
570
|
);
|
|
460
571
|
|
|
461
|
-
expect(result.result
|
|
462
|
-
expect(result.result.remaining_references).toBe(0);
|
|
572
|
+
expect(result.result).toMatchObject({ success: true });
|
|
463
573
|
});
|
|
464
574
|
|
|
465
|
-
it('should
|
|
466
|
-
|
|
467
|
-
|
|
575
|
+
it('should handle not found error', async () => {
|
|
576
|
+
mockApiClient.removeTaskReference.mockResolvedValue({
|
|
577
|
+
ok: false,
|
|
578
|
+
error: 'Reference not found',
|
|
468
579
|
});
|
|
469
|
-
const ctx = createMockContext(supabase);
|
|
470
580
|
|
|
581
|
+
const ctx = createMockContext();
|
|
471
582
|
const result = await removeTaskReference(
|
|
472
583
|
{
|
|
473
584
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
474
|
-
url: 'https://
|
|
585
|
+
url: 'https://example.com',
|
|
475
586
|
},
|
|
476
587
|
ctx
|
|
477
588
|
);
|
|
478
589
|
|
|
479
|
-
expect(result.result
|
|
480
|
-
|
|
590
|
+
expect(result.result).toMatchObject({
|
|
591
|
+
success: false,
|
|
592
|
+
error: expect.stringContaining('not found'),
|
|
593
|
+
});
|
|
481
594
|
});
|
|
482
595
|
});
|
|
483
596
|
|
|
484
597
|
// ============================================================================
|
|
485
|
-
//
|
|
598
|
+
// batchUpdateTasks Tests
|
|
486
599
|
// ============================================================================
|
|
487
600
|
|
|
488
|
-
describe('
|
|
489
|
-
|
|
490
|
-
const mockConfig = {
|
|
491
|
-
git_workflow: 'github-flow',
|
|
492
|
-
git_main_branch: 'main',
|
|
493
|
-
git_develop_branch: null,
|
|
494
|
-
git_auto_branch: true,
|
|
495
|
-
};
|
|
601
|
+
describe('batchUpdateTasks', () => {
|
|
602
|
+
beforeEach(() => vi.clearAllMocks());
|
|
496
603
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
604
|
+
it('should throw error for missing updates array', async () => {
|
|
605
|
+
const ctx = createMockContext();
|
|
606
|
+
await expect(batchUpdateTasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
607
|
+
});
|
|
500
608
|
|
|
501
|
-
|
|
609
|
+
it('should throw error for empty updates array', async () => {
|
|
610
|
+
const ctx = createMockContext();
|
|
611
|
+
await expect(batchUpdateTasks({ updates: [] }, ctx)).rejects.toThrow(ValidationError);
|
|
612
|
+
});
|
|
502
613
|
|
|
503
|
-
|
|
504
|
-
|
|
614
|
+
it('should throw error for too many updates', async () => {
|
|
615
|
+
const ctx = createMockContext();
|
|
616
|
+
const updates = Array(51).fill({
|
|
617
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
618
|
+
});
|
|
619
|
+
await expect(batchUpdateTasks({ updates }, ctx)).rejects.toThrow(ValidationError);
|
|
505
620
|
});
|
|
506
621
|
|
|
507
|
-
it('should
|
|
508
|
-
|
|
509
|
-
|
|
622
|
+
it('should batch update tasks successfully', async () => {
|
|
623
|
+
mockApiClient.batchUpdateTasks.mockResolvedValue({
|
|
624
|
+
ok: true,
|
|
625
|
+
data: { success: true, updated_count: 2 },
|
|
510
626
|
});
|
|
511
627
|
|
|
512
|
-
const
|
|
628
|
+
const ctx = createMockContext();
|
|
629
|
+
const result = await batchUpdateTasks(
|
|
630
|
+
{
|
|
631
|
+
updates: [
|
|
632
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'in_progress' },
|
|
633
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174001', status: 'completed' },
|
|
634
|
+
],
|
|
635
|
+
},
|
|
636
|
+
ctx
|
|
637
|
+
);
|
|
513
638
|
|
|
514
|
-
expect(result).
|
|
639
|
+
expect(result.result).toMatchObject({
|
|
640
|
+
success: true,
|
|
641
|
+
total: 2,
|
|
642
|
+
succeeded: 2,
|
|
643
|
+
});
|
|
515
644
|
});
|
|
516
645
|
});
|
|
517
646
|
|
|
518
647
|
// ============================================================================
|
|
519
|
-
//
|
|
648
|
+
// batchCompleteTasks Tests
|
|
520
649
|
// ============================================================================
|
|
521
650
|
|
|
522
|
-
describe('
|
|
523
|
-
beforeEach(() =>
|
|
524
|
-
|
|
651
|
+
describe('batchCompleteTasks', () => {
|
|
652
|
+
beforeEach(() => vi.clearAllMocks());
|
|
653
|
+
|
|
654
|
+
it('should throw error for missing completions array', async () => {
|
|
655
|
+
const ctx = createMockContext();
|
|
656
|
+
await expect(batchCompleteTasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
525
657
|
});
|
|
526
658
|
|
|
527
|
-
it('should throw error for
|
|
528
|
-
const
|
|
529
|
-
|
|
659
|
+
it('should throw error for empty completions array', async () => {
|
|
660
|
+
const ctx = createMockContext();
|
|
661
|
+
await expect(batchCompleteTasks({ completions: [] }, ctx)).rejects.toThrow(ValidationError);
|
|
662
|
+
});
|
|
530
663
|
|
|
531
|
-
|
|
664
|
+
it('should batch complete tasks successfully', async () => {
|
|
665
|
+
mockApiClient.batchCompleteTasks.mockResolvedValue({
|
|
666
|
+
ok: true,
|
|
667
|
+
data: {
|
|
668
|
+
success: true,
|
|
669
|
+
completed_count: 2,
|
|
670
|
+
next_task: { id: 'task-3', title: 'Next' },
|
|
671
|
+
},
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
const ctx = createMockContext();
|
|
675
|
+
const result = await batchCompleteTasks(
|
|
676
|
+
{
|
|
677
|
+
completions: [
|
|
678
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
679
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174001', summary: 'Done' },
|
|
680
|
+
],
|
|
681
|
+
},
|
|
682
|
+
ctx
|
|
683
|
+
);
|
|
684
|
+
|
|
685
|
+
expect(result.result).toMatchObject({
|
|
686
|
+
success: true,
|
|
687
|
+
total: 2,
|
|
688
|
+
succeeded: 2,
|
|
689
|
+
next_task: expect.any(Object),
|
|
690
|
+
});
|
|
532
691
|
});
|
|
692
|
+
});
|
|
533
693
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
694
|
+
// ============================================================================
|
|
695
|
+
// addSubtask Tests
|
|
696
|
+
// ============================================================================
|
|
537
697
|
|
|
538
|
-
|
|
698
|
+
describe('addSubtask', () => {
|
|
699
|
+
beforeEach(() => vi.clearAllMocks());
|
|
700
|
+
|
|
701
|
+
it('should throw error for missing parent_task_id', async () => {
|
|
702
|
+
const ctx = createMockContext();
|
|
703
|
+
await expect(addSubtask({ title: 'Subtask' }, ctx)).rejects.toThrow(ValidationError);
|
|
539
704
|
});
|
|
540
705
|
|
|
541
|
-
it('should throw error
|
|
542
|
-
const
|
|
543
|
-
|
|
706
|
+
it('should throw error for missing title', async () => {
|
|
707
|
+
const ctx = createMockContext();
|
|
708
|
+
await expect(
|
|
709
|
+
addSubtask({ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
710
|
+
).rejects.toThrow(ValidationError);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
it('should add subtask successfully', async () => {
|
|
714
|
+
mockApiClient.addSubtask.mockResolvedValue({
|
|
715
|
+
ok: true,
|
|
716
|
+
data: {
|
|
717
|
+
subtask_id: 'subtask-1',
|
|
718
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
719
|
+
},
|
|
544
720
|
});
|
|
545
|
-
const ctx = createMockContext(supabase);
|
|
546
721
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
722
|
+
const ctx = createMockContext();
|
|
723
|
+
const result = await addSubtask(
|
|
724
|
+
{
|
|
725
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
726
|
+
title: 'Subtask 1',
|
|
727
|
+
},
|
|
728
|
+
ctx
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
expect(result.result).toMatchObject({
|
|
732
|
+
success: true,
|
|
733
|
+
subtask_id: 'subtask-1',
|
|
734
|
+
});
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
it('should handle nested subtask error', async () => {
|
|
738
|
+
mockApiClient.addSubtask.mockResolvedValue({
|
|
739
|
+
ok: false,
|
|
740
|
+
error: 'Cannot create subtask of a subtask',
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
const ctx = createMockContext();
|
|
744
|
+
const result = await addSubtask(
|
|
745
|
+
{
|
|
746
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
747
|
+
title: 'Nested subtask',
|
|
748
|
+
},
|
|
749
|
+
ctx
|
|
750
|
+
);
|
|
751
|
+
|
|
752
|
+
expect(result.result).toMatchObject({
|
|
753
|
+
success: false,
|
|
754
|
+
error: 'Cannot create subtask of a subtask',
|
|
755
|
+
});
|
|
550
756
|
});
|
|
551
757
|
});
|
|
552
758
|
|
|
553
759
|
// ============================================================================
|
|
554
|
-
//
|
|
760
|
+
// getSubtasks Tests
|
|
555
761
|
// ============================================================================
|
|
556
762
|
|
|
557
|
-
describe('
|
|
558
|
-
beforeEach(() =>
|
|
559
|
-
|
|
763
|
+
describe('getSubtasks', () => {
|
|
764
|
+
beforeEach(() => vi.clearAllMocks());
|
|
765
|
+
|
|
766
|
+
it('should throw error for missing parent_task_id', async () => {
|
|
767
|
+
const ctx = createMockContext();
|
|
768
|
+
await expect(getSubtasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
560
769
|
});
|
|
561
770
|
|
|
562
|
-
it('should throw error for
|
|
563
|
-
const
|
|
564
|
-
|
|
771
|
+
it('should throw error for invalid parent_task_id', async () => {
|
|
772
|
+
const ctx = createMockContext();
|
|
773
|
+
await expect(
|
|
774
|
+
getSubtasks({ parent_task_id: 'invalid' }, ctx)
|
|
775
|
+
).rejects.toThrow(ValidationError);
|
|
776
|
+
});
|
|
565
777
|
|
|
566
|
-
|
|
778
|
+
it('should return subtasks successfully', async () => {
|
|
779
|
+
mockApiClient.getSubtasks.mockResolvedValue({
|
|
780
|
+
ok: true,
|
|
781
|
+
data: {
|
|
782
|
+
subtasks: [
|
|
783
|
+
{ id: 'sub-1', title: 'Subtask 1', status: 'pending' },
|
|
784
|
+
{ id: 'sub-2', title: 'Subtask 2', status: 'completed' },
|
|
785
|
+
],
|
|
786
|
+
stats: { total: 2, completed: 1, progress_percentage: 50 },
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
const ctx = createMockContext();
|
|
791
|
+
const result = await getSubtasks(
|
|
792
|
+
{ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
793
|
+
ctx
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
expect(result.result).toMatchObject({
|
|
797
|
+
subtasks: expect.any(Array),
|
|
798
|
+
stats: expect.objectContaining({ total: 2 }),
|
|
799
|
+
});
|
|
567
800
|
});
|
|
801
|
+
});
|
|
568
802
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
803
|
+
// ============================================================================
|
|
804
|
+
// getValidationApprovedGitInstructions Tests
|
|
805
|
+
// ============================================================================
|
|
572
806
|
|
|
573
|
-
|
|
807
|
+
describe('getValidationApprovedGitInstructions', () => {
|
|
808
|
+
it('should return undefined for none workflow', () => {
|
|
809
|
+
const result = getValidationApprovedGitInstructions(
|
|
810
|
+
{ git_workflow: 'none', git_main_branch: 'main' },
|
|
811
|
+
'feature/test'
|
|
812
|
+
);
|
|
813
|
+
expect(result).toBeUndefined();
|
|
574
814
|
});
|
|
575
815
|
|
|
576
|
-
it('should
|
|
577
|
-
const
|
|
578
|
-
|
|
816
|
+
it('should return undefined for trunk-based workflow', () => {
|
|
817
|
+
const result = getValidationApprovedGitInstructions(
|
|
818
|
+
{ git_workflow: 'trunk-based', git_main_branch: 'main' },
|
|
819
|
+
'feature/test'
|
|
820
|
+
);
|
|
821
|
+
expect(result).toBeUndefined();
|
|
822
|
+
});
|
|
579
823
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
824
|
+
it('should return undefined when no task branch', () => {
|
|
825
|
+
const result = getValidationApprovedGitInstructions(
|
|
826
|
+
{ git_workflow: 'github-flow', git_main_branch: 'main' },
|
|
827
|
+
undefined
|
|
828
|
+
);
|
|
829
|
+
expect(result).toBeUndefined();
|
|
583
830
|
});
|
|
584
831
|
|
|
585
|
-
it('should
|
|
586
|
-
const
|
|
587
|
-
|
|
832
|
+
it('should return merge instructions for github-flow', () => {
|
|
833
|
+
const result = getValidationApprovedGitInstructions(
|
|
834
|
+
{ git_workflow: 'github-flow', git_main_branch: 'main' },
|
|
835
|
+
'feature/test'
|
|
836
|
+
);
|
|
588
837
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
838
|
+
expect(result).toMatchObject({
|
|
839
|
+
target_branch: 'main',
|
|
840
|
+
feature_branch: 'feature/test',
|
|
841
|
+
steps: expect.any(Array),
|
|
842
|
+
cleanup: expect.any(Array),
|
|
843
|
+
});
|
|
592
844
|
});
|
|
593
845
|
|
|
594
|
-
it('should
|
|
595
|
-
const
|
|
596
|
-
|
|
846
|
+
it('should return merge instructions for git-flow with develop', () => {
|
|
847
|
+
const result = getValidationApprovedGitInstructions(
|
|
848
|
+
{ git_workflow: 'git-flow', git_main_branch: 'main', git_develop_branch: 'develop' },
|
|
849
|
+
'feature/test'
|
|
850
|
+
);
|
|
597
851
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
852
|
+
expect(result).toMatchObject({
|
|
853
|
+
target_branch: 'develop',
|
|
854
|
+
feature_branch: 'feature/test',
|
|
855
|
+
});
|
|
601
856
|
});
|
|
602
857
|
});
|