@vibescope/mcp-server 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +108 -477
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +210 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/knowledge.ts +0 -132
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
|
@@ -1,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,436 @@ 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',
|
|
327
|
+
},
|
|
328
|
+
ctx
|
|
329
|
+
);
|
|
330
|
+
|
|
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',
|
|
313
347
|
},
|
|
314
348
|
ctx
|
|
315
349
|
);
|
|
316
350
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
351
|
+
expect(result.result).toMatchObject({
|
|
352
|
+
error: 'task_claimed',
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('should warn when setting in_progress without git_branch', async () => {
|
|
357
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
358
|
+
ok: true,
|
|
359
|
+
data: { success: true },
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const ctx = createMockContext();
|
|
363
|
+
const result = await updateTask(
|
|
364
|
+
{
|
|
365
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
366
|
+
status: 'in_progress',
|
|
367
|
+
},
|
|
368
|
+
ctx
|
|
320
369
|
);
|
|
370
|
+
|
|
371
|
+
expect(result.result).toMatchObject({
|
|
372
|
+
success: true,
|
|
373
|
+
warning: expect.stringContaining('git_branch not set'),
|
|
374
|
+
hint: expect.stringContaining('update_task again'),
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should not warn when setting in_progress with git_branch', async () => {
|
|
379
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
380
|
+
ok: true,
|
|
381
|
+
data: { success: true },
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const ctx = createMockContext();
|
|
385
|
+
const result = await updateTask(
|
|
386
|
+
{
|
|
387
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
388
|
+
status: 'in_progress',
|
|
389
|
+
git_branch: 'feature/my-task',
|
|
390
|
+
},
|
|
391
|
+
ctx
|
|
392
|
+
);
|
|
393
|
+
|
|
394
|
+
expect(result.result).toMatchObject({ success: true });
|
|
395
|
+
expect(result.result).not.toHaveProperty('warning');
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('should not warn when updating status other than in_progress', async () => {
|
|
399
|
+
mockApiClient.updateTask.mockResolvedValue({
|
|
400
|
+
ok: true,
|
|
401
|
+
data: { success: true },
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const ctx = createMockContext();
|
|
405
|
+
const result = await updateTask(
|
|
406
|
+
{
|
|
407
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
408
|
+
status: 'completed',
|
|
409
|
+
},
|
|
410
|
+
ctx
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
expect(result.result).toMatchObject({ success: true });
|
|
414
|
+
expect(result.result).not.toHaveProperty('warning');
|
|
321
415
|
});
|
|
322
416
|
});
|
|
323
417
|
|
|
324
418
|
// ============================================================================
|
|
325
|
-
//
|
|
419
|
+
// completeTask Tests
|
|
326
420
|
// ============================================================================
|
|
327
421
|
|
|
328
|
-
describe('
|
|
329
|
-
beforeEach(() =>
|
|
330
|
-
|
|
422
|
+
describe('completeTask', () => {
|
|
423
|
+
beforeEach(() => vi.clearAllMocks());
|
|
424
|
+
|
|
425
|
+
it('should throw error for missing task_id', async () => {
|
|
426
|
+
const ctx = createMockContext();
|
|
427
|
+
await expect(completeTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
331
428
|
});
|
|
332
429
|
|
|
333
|
-
it('should
|
|
334
|
-
const
|
|
335
|
-
|
|
430
|
+
it('should throw error for invalid task_id', async () => {
|
|
431
|
+
const ctx = createMockContext();
|
|
432
|
+
await expect(
|
|
433
|
+
completeTask({ task_id: 'invalid' }, ctx)
|
|
434
|
+
).rejects.toThrow(ValidationError);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
it('should complete task successfully', async () => {
|
|
438
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
439
|
+
ok: true,
|
|
440
|
+
data: {
|
|
441
|
+
success: true,
|
|
442
|
+
directive: 'Start next task',
|
|
443
|
+
auto_continue: true,
|
|
444
|
+
completed_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
445
|
+
next_task: { id: 'task-2', title: 'Next task' },
|
|
446
|
+
},
|
|
336
447
|
});
|
|
337
|
-
const ctx = createMockContext(supabase);
|
|
338
448
|
|
|
339
|
-
const
|
|
449
|
+
const ctx = createMockContext();
|
|
450
|
+
const result = await completeTask(
|
|
340
451
|
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
341
452
|
ctx
|
|
342
453
|
);
|
|
343
454
|
|
|
344
|
-
expect(result.result
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
455
|
+
expect(result.result).toMatchObject({
|
|
456
|
+
success: true,
|
|
457
|
+
auto_continue: true,
|
|
458
|
+
next_task: expect.any(Object),
|
|
459
|
+
});
|
|
348
460
|
});
|
|
349
461
|
|
|
350
|
-
it('should
|
|
351
|
-
|
|
352
|
-
|
|
462
|
+
it('should include summary in API call', async () => {
|
|
463
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
464
|
+
ok: true,
|
|
465
|
+
data: { success: true },
|
|
466
|
+
});
|
|
353
467
|
|
|
354
|
-
|
|
468
|
+
const ctx = createMockContext();
|
|
469
|
+
await completeTask(
|
|
470
|
+
{
|
|
471
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
472
|
+
summary: 'Completed the feature',
|
|
473
|
+
},
|
|
474
|
+
ctx
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
expect(mockApiClient.completeTask).toHaveBeenCalledWith(
|
|
478
|
+
'123e4567-e89b-12d3-a456-426614174000',
|
|
479
|
+
expect.objectContaining({ summary: 'Completed the feature' })
|
|
480
|
+
);
|
|
355
481
|
});
|
|
356
482
|
|
|
357
|
-
it('should throw error
|
|
358
|
-
|
|
359
|
-
|
|
483
|
+
it('should throw error when API returns error', async () => {
|
|
484
|
+
mockApiClient.completeTask.mockResolvedValue({
|
|
485
|
+
ok: false,
|
|
486
|
+
error: 'Task not found',
|
|
487
|
+
});
|
|
360
488
|
|
|
361
|
-
|
|
489
|
+
const ctx = createMockContext();
|
|
490
|
+
await expect(
|
|
491
|
+
completeTask({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
492
|
+
).rejects.toThrow('Failed to complete task: Task not found');
|
|
362
493
|
});
|
|
363
494
|
});
|
|
364
495
|
|
|
365
496
|
// ============================================================================
|
|
366
|
-
//
|
|
497
|
+
// deleteTask Tests
|
|
367
498
|
// ============================================================================
|
|
368
499
|
|
|
369
|
-
describe('
|
|
370
|
-
beforeEach(() =>
|
|
371
|
-
|
|
500
|
+
describe('deleteTask', () => {
|
|
501
|
+
beforeEach(() => vi.clearAllMocks());
|
|
502
|
+
|
|
503
|
+
it('should throw error for missing task_id', async () => {
|
|
504
|
+
const ctx = createMockContext();
|
|
505
|
+
await expect(deleteTask({}, ctx)).rejects.toThrow(ValidationError);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
it('should throw error for invalid task_id', async () => {
|
|
509
|
+
const ctx = createMockContext();
|
|
510
|
+
await expect(deleteTask({ task_id: 'invalid' }, ctx)).rejects.toThrow(ValidationError);
|
|
372
511
|
});
|
|
373
512
|
|
|
374
|
-
it('should
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
513
|
+
it('should delete task successfully', async () => {
|
|
514
|
+
mockApiClient.deleteTask.mockResolvedValue({
|
|
515
|
+
ok: true,
|
|
516
|
+
data: { success: true },
|
|
378
517
|
});
|
|
379
|
-
const ctx = createMockContext(supabase);
|
|
380
518
|
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
url: 'https://github.com/user/repo/pull/123',
|
|
385
|
-
label: 'PR #123',
|
|
386
|
-
},
|
|
519
|
+
const ctx = createMockContext();
|
|
520
|
+
const result = await deleteTask(
|
|
521
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
387
522
|
ctx
|
|
388
523
|
);
|
|
389
524
|
|
|
390
|
-
expect(result.result
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
label: 'PR #123',
|
|
525
|
+
expect(result.result).toMatchObject({
|
|
526
|
+
success: true,
|
|
527
|
+
deleted_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
394
528
|
});
|
|
395
|
-
expect(result.result.total_references).toBe(1);
|
|
396
529
|
});
|
|
530
|
+
});
|
|
397
531
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
532
|
+
// ============================================================================
|
|
533
|
+
// addTaskReference Tests
|
|
534
|
+
// ============================================================================
|
|
535
|
+
|
|
536
|
+
describe('addTaskReference', () => {
|
|
537
|
+
beforeEach(() => vi.clearAllMocks());
|
|
401
538
|
|
|
539
|
+
it('should throw error for missing task_id', async () => {
|
|
540
|
+
const ctx = createMockContext();
|
|
402
541
|
await expect(
|
|
403
542
|
addTaskReference({ url: 'https://example.com' }, ctx)
|
|
404
543
|
).rejects.toThrow(ValidationError);
|
|
405
544
|
});
|
|
406
545
|
|
|
407
546
|
it('should throw error for missing url', async () => {
|
|
408
|
-
const
|
|
409
|
-
const ctx = createMockContext(supabase);
|
|
410
|
-
|
|
547
|
+
const ctx = createMockContext();
|
|
411
548
|
await expect(
|
|
412
549
|
addTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
413
550
|
).rejects.toThrow(ValidationError);
|
|
414
551
|
});
|
|
415
552
|
|
|
416
|
-
it('should
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
553
|
+
it('should add reference successfully', async () => {
|
|
554
|
+
mockApiClient.addTaskReference.mockResolvedValue({
|
|
555
|
+
ok: true,
|
|
556
|
+
data: { reference: { url: 'https://example.com', label: 'Test' } },
|
|
420
557
|
});
|
|
421
|
-
const ctx = createMockContext(supabase);
|
|
422
558
|
|
|
559
|
+
const ctx = createMockContext();
|
|
423
560
|
const result = await addTaskReference(
|
|
424
561
|
{
|
|
425
562
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
426
563
|
url: 'https://example.com',
|
|
564
|
+
label: 'Test',
|
|
427
565
|
},
|
|
428
566
|
ctx
|
|
429
567
|
);
|
|
430
568
|
|
|
431
|
-
expect(result.result
|
|
432
|
-
|
|
569
|
+
expect(result.result).toMatchObject({
|
|
570
|
+
success: true,
|
|
571
|
+
reference: expect.any(Object),
|
|
572
|
+
});
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
it('should handle duplicate reference error', async () => {
|
|
576
|
+
mockApiClient.addTaskReference.mockResolvedValue({
|
|
577
|
+
ok: false,
|
|
578
|
+
error: 'Reference already exists',
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
const ctx = createMockContext();
|
|
582
|
+
const result = await addTaskReference(
|
|
583
|
+
{
|
|
584
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
585
|
+
url: 'https://example.com',
|
|
586
|
+
},
|
|
587
|
+
ctx
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
expect(result.result).toMatchObject({
|
|
591
|
+
success: false,
|
|
592
|
+
error: expect.stringContaining('already exists'),
|
|
593
|
+
});
|
|
433
594
|
});
|
|
434
595
|
});
|
|
435
596
|
|
|
@@ -438,18 +599,29 @@ describe('addTaskReference', () => {
|
|
|
438
599
|
// ============================================================================
|
|
439
600
|
|
|
440
601
|
describe('removeTaskReference', () => {
|
|
441
|
-
beforeEach(() =>
|
|
442
|
-
|
|
602
|
+
beforeEach(() => vi.clearAllMocks());
|
|
603
|
+
|
|
604
|
+
it('should throw error for missing task_id', async () => {
|
|
605
|
+
const ctx = createMockContext();
|
|
606
|
+
await expect(
|
|
607
|
+
removeTaskReference({ url: 'https://example.com' }, ctx)
|
|
608
|
+
).rejects.toThrow(ValidationError);
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
it('should throw error for missing url', async () => {
|
|
612
|
+
const ctx = createMockContext();
|
|
613
|
+
await expect(
|
|
614
|
+
removeTaskReference({ task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
615
|
+
).rejects.toThrow(ValidationError);
|
|
443
616
|
});
|
|
444
617
|
|
|
445
|
-
it('should remove
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
updateResult: { data: null, error: null },
|
|
618
|
+
it('should remove reference successfully', async () => {
|
|
619
|
+
mockApiClient.removeTaskReference.mockResolvedValue({
|
|
620
|
+
ok: true,
|
|
621
|
+
data: { success: true },
|
|
450
622
|
});
|
|
451
|
-
const ctx = createMockContext(supabase);
|
|
452
623
|
|
|
624
|
+
const ctx = createMockContext();
|
|
453
625
|
const result = await removeTaskReference(
|
|
454
626
|
{
|
|
455
627
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
@@ -458,145 +630,289 @@ describe('removeTaskReference', () => {
|
|
|
458
630
|
ctx
|
|
459
631
|
);
|
|
460
632
|
|
|
461
|
-
expect(result.result
|
|
462
|
-
expect(result.result.remaining_references).toBe(0);
|
|
633
|
+
expect(result.result).toMatchObject({ success: true });
|
|
463
634
|
});
|
|
464
635
|
|
|
465
|
-
it('should
|
|
466
|
-
|
|
467
|
-
|
|
636
|
+
it('should handle not found error', async () => {
|
|
637
|
+
mockApiClient.removeTaskReference.mockResolvedValue({
|
|
638
|
+
ok: false,
|
|
639
|
+
error: 'Reference not found',
|
|
468
640
|
});
|
|
469
|
-
const ctx = createMockContext(supabase);
|
|
470
641
|
|
|
642
|
+
const ctx = createMockContext();
|
|
471
643
|
const result = await removeTaskReference(
|
|
472
644
|
{
|
|
473
645
|
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
474
|
-
url: 'https://
|
|
646
|
+
url: 'https://example.com',
|
|
475
647
|
},
|
|
476
648
|
ctx
|
|
477
649
|
);
|
|
478
650
|
|
|
479
|
-
expect(result.result
|
|
480
|
-
|
|
651
|
+
expect(result.result).toMatchObject({
|
|
652
|
+
success: false,
|
|
653
|
+
error: expect.stringContaining('not found'),
|
|
654
|
+
});
|
|
481
655
|
});
|
|
482
656
|
});
|
|
483
657
|
|
|
484
658
|
// ============================================================================
|
|
485
|
-
//
|
|
659
|
+
// batchUpdateTasks Tests
|
|
486
660
|
// ============================================================================
|
|
487
661
|
|
|
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
|
-
};
|
|
662
|
+
describe('batchUpdateTasks', () => {
|
|
663
|
+
beforeEach(() => vi.clearAllMocks());
|
|
496
664
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
});
|
|
665
|
+
it('should throw error for missing updates array', async () => {
|
|
666
|
+
const ctx = createMockContext();
|
|
667
|
+
await expect(batchUpdateTasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
668
|
+
});
|
|
500
669
|
|
|
501
|
-
|
|
670
|
+
it('should throw error for empty updates array', async () => {
|
|
671
|
+
const ctx = createMockContext();
|
|
672
|
+
await expect(batchUpdateTasks({ updates: [] }, ctx)).rejects.toThrow(ValidationError);
|
|
673
|
+
});
|
|
502
674
|
|
|
503
|
-
|
|
504
|
-
|
|
675
|
+
it('should throw error for too many updates', async () => {
|
|
676
|
+
const ctx = createMockContext();
|
|
677
|
+
const updates = Array(51).fill({
|
|
678
|
+
task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
679
|
+
});
|
|
680
|
+
await expect(batchUpdateTasks({ updates }, ctx)).rejects.toThrow(ValidationError);
|
|
505
681
|
});
|
|
506
682
|
|
|
507
|
-
it('should
|
|
508
|
-
|
|
509
|
-
|
|
683
|
+
it('should batch update tasks successfully', async () => {
|
|
684
|
+
mockApiClient.batchUpdateTasks.mockResolvedValue({
|
|
685
|
+
ok: true,
|
|
686
|
+
data: { success: true, updated_count: 2 },
|
|
510
687
|
});
|
|
511
688
|
|
|
512
|
-
const
|
|
689
|
+
const ctx = createMockContext();
|
|
690
|
+
const result = await batchUpdateTasks(
|
|
691
|
+
{
|
|
692
|
+
updates: [
|
|
693
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000', status: 'in_progress' },
|
|
694
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174001', status: 'completed' },
|
|
695
|
+
],
|
|
696
|
+
},
|
|
697
|
+
ctx
|
|
698
|
+
);
|
|
513
699
|
|
|
514
|
-
expect(result).
|
|
700
|
+
expect(result.result).toMatchObject({
|
|
701
|
+
success: true,
|
|
702
|
+
total: 2,
|
|
703
|
+
succeeded: 2,
|
|
704
|
+
});
|
|
515
705
|
});
|
|
516
706
|
});
|
|
517
707
|
|
|
518
708
|
// ============================================================================
|
|
519
|
-
//
|
|
709
|
+
// batchCompleteTasks Tests
|
|
520
710
|
// ============================================================================
|
|
521
711
|
|
|
522
|
-
describe('
|
|
523
|
-
beforeEach(() =>
|
|
524
|
-
|
|
712
|
+
describe('batchCompleteTasks', () => {
|
|
713
|
+
beforeEach(() => vi.clearAllMocks());
|
|
714
|
+
|
|
715
|
+
it('should throw error for missing completions array', async () => {
|
|
716
|
+
const ctx = createMockContext();
|
|
717
|
+
await expect(batchCompleteTasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
525
718
|
});
|
|
526
719
|
|
|
527
|
-
it('should throw error for
|
|
528
|
-
const
|
|
529
|
-
|
|
720
|
+
it('should throw error for empty completions array', async () => {
|
|
721
|
+
const ctx = createMockContext();
|
|
722
|
+
await expect(batchCompleteTasks({ completions: [] }, ctx)).rejects.toThrow(ValidationError);
|
|
723
|
+
});
|
|
530
724
|
|
|
531
|
-
|
|
725
|
+
it('should batch complete tasks successfully', async () => {
|
|
726
|
+
mockApiClient.batchCompleteTasks.mockResolvedValue({
|
|
727
|
+
ok: true,
|
|
728
|
+
data: {
|
|
729
|
+
success: true,
|
|
730
|
+
completed_count: 2,
|
|
731
|
+
next_task: { id: 'task-3', title: 'Next' },
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
const ctx = createMockContext();
|
|
736
|
+
const result = await batchCompleteTasks(
|
|
737
|
+
{
|
|
738
|
+
completions: [
|
|
739
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
740
|
+
{ task_id: '123e4567-e89b-12d3-a456-426614174001', summary: 'Done' },
|
|
741
|
+
],
|
|
742
|
+
},
|
|
743
|
+
ctx
|
|
744
|
+
);
|
|
745
|
+
|
|
746
|
+
expect(result.result).toMatchObject({
|
|
747
|
+
success: true,
|
|
748
|
+
total: 2,
|
|
749
|
+
succeeded: 2,
|
|
750
|
+
next_task: expect.any(Object),
|
|
751
|
+
});
|
|
532
752
|
});
|
|
753
|
+
});
|
|
533
754
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
755
|
+
// ============================================================================
|
|
756
|
+
// addSubtask Tests
|
|
757
|
+
// ============================================================================
|
|
758
|
+
|
|
759
|
+
describe('addSubtask', () => {
|
|
760
|
+
beforeEach(() => vi.clearAllMocks());
|
|
537
761
|
|
|
538
|
-
|
|
762
|
+
it('should throw error for missing parent_task_id', async () => {
|
|
763
|
+
const ctx = createMockContext();
|
|
764
|
+
await expect(addSubtask({ title: 'Subtask' }, ctx)).rejects.toThrow(ValidationError);
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
it('should throw error for missing title', async () => {
|
|
768
|
+
const ctx = createMockContext();
|
|
769
|
+
await expect(
|
|
770
|
+
addSubtask({ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' }, ctx)
|
|
771
|
+
).rejects.toThrow(ValidationError);
|
|
539
772
|
});
|
|
540
773
|
|
|
541
|
-
it('should
|
|
542
|
-
|
|
543
|
-
|
|
774
|
+
it('should add subtask successfully', async () => {
|
|
775
|
+
mockApiClient.addSubtask.mockResolvedValue({
|
|
776
|
+
ok: true,
|
|
777
|
+
data: {
|
|
778
|
+
subtask_id: 'subtask-1',
|
|
779
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
780
|
+
},
|
|
544
781
|
});
|
|
545
|
-
const ctx = createMockContext(supabase);
|
|
546
782
|
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
783
|
+
const ctx = createMockContext();
|
|
784
|
+
const result = await addSubtask(
|
|
785
|
+
{
|
|
786
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
787
|
+
title: 'Subtask 1',
|
|
788
|
+
},
|
|
789
|
+
ctx
|
|
790
|
+
);
|
|
791
|
+
|
|
792
|
+
expect(result.result).toMatchObject({
|
|
793
|
+
success: true,
|
|
794
|
+
subtask_id: 'subtask-1',
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
it('should handle nested subtask error', async () => {
|
|
799
|
+
mockApiClient.addSubtask.mockResolvedValue({
|
|
800
|
+
ok: false,
|
|
801
|
+
error: 'Cannot create subtask of a subtask',
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
const ctx = createMockContext();
|
|
805
|
+
const result = await addSubtask(
|
|
806
|
+
{
|
|
807
|
+
parent_task_id: '123e4567-e89b-12d3-a456-426614174000',
|
|
808
|
+
title: 'Nested subtask',
|
|
809
|
+
},
|
|
810
|
+
ctx
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
expect(result.result).toMatchObject({
|
|
814
|
+
success: false,
|
|
815
|
+
error: 'Cannot create subtask of a subtask',
|
|
816
|
+
});
|
|
550
817
|
});
|
|
551
818
|
});
|
|
552
819
|
|
|
553
820
|
// ============================================================================
|
|
554
|
-
//
|
|
821
|
+
// getSubtasks Tests
|
|
555
822
|
// ============================================================================
|
|
556
823
|
|
|
557
|
-
describe('
|
|
558
|
-
beforeEach(() =>
|
|
559
|
-
|
|
824
|
+
describe('getSubtasks', () => {
|
|
825
|
+
beforeEach(() => vi.clearAllMocks());
|
|
826
|
+
|
|
827
|
+
it('should throw error for missing parent_task_id', async () => {
|
|
828
|
+
const ctx = createMockContext();
|
|
829
|
+
await expect(getSubtasks({}, ctx)).rejects.toThrow(ValidationError);
|
|
560
830
|
});
|
|
561
831
|
|
|
562
|
-
it('should throw error for
|
|
563
|
-
const
|
|
564
|
-
|
|
832
|
+
it('should throw error for invalid parent_task_id', async () => {
|
|
833
|
+
const ctx = createMockContext();
|
|
834
|
+
await expect(
|
|
835
|
+
getSubtasks({ parent_task_id: 'invalid' }, ctx)
|
|
836
|
+
).rejects.toThrow(ValidationError);
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('should return subtasks successfully', async () => {
|
|
840
|
+
mockApiClient.getSubtasks.mockResolvedValue({
|
|
841
|
+
ok: true,
|
|
842
|
+
data: {
|
|
843
|
+
subtasks: [
|
|
844
|
+
{ id: 'sub-1', title: 'Subtask 1', status: 'pending' },
|
|
845
|
+
{ id: 'sub-2', title: 'Subtask 2', status: 'completed' },
|
|
846
|
+
],
|
|
847
|
+
stats: { total: 2, completed: 1, progress_percentage: 50 },
|
|
848
|
+
},
|
|
849
|
+
});
|
|
565
850
|
|
|
566
|
-
|
|
851
|
+
const ctx = createMockContext();
|
|
852
|
+
const result = await getSubtasks(
|
|
853
|
+
{ parent_task_id: '123e4567-e89b-12d3-a456-426614174000' },
|
|
854
|
+
ctx
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
expect(result.result).toMatchObject({
|
|
858
|
+
subtasks: expect.any(Array),
|
|
859
|
+
stats: expect.objectContaining({ total: 2 }),
|
|
860
|
+
});
|
|
567
861
|
});
|
|
862
|
+
});
|
|
568
863
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
864
|
+
// ============================================================================
|
|
865
|
+
// getValidationApprovedGitInstructions Tests
|
|
866
|
+
// ============================================================================
|
|
572
867
|
|
|
573
|
-
|
|
868
|
+
describe('getValidationApprovedGitInstructions', () => {
|
|
869
|
+
it('should return undefined for none workflow', () => {
|
|
870
|
+
const result = getValidationApprovedGitInstructions(
|
|
871
|
+
{ git_workflow: 'none', git_main_branch: 'main' },
|
|
872
|
+
'feature/test'
|
|
873
|
+
);
|
|
874
|
+
expect(result).toBeUndefined();
|
|
574
875
|
});
|
|
575
876
|
|
|
576
|
-
it('should
|
|
577
|
-
const
|
|
578
|
-
|
|
877
|
+
it('should return undefined for trunk-based workflow', () => {
|
|
878
|
+
const result = getValidationApprovedGitInstructions(
|
|
879
|
+
{ git_workflow: 'trunk-based', git_main_branch: 'main' },
|
|
880
|
+
'feature/test'
|
|
881
|
+
);
|
|
882
|
+
expect(result).toBeUndefined();
|
|
883
|
+
});
|
|
579
884
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
885
|
+
it('should return undefined when no task branch', () => {
|
|
886
|
+
const result = getValidationApprovedGitInstructions(
|
|
887
|
+
{ git_workflow: 'github-flow', git_main_branch: 'main' },
|
|
888
|
+
undefined
|
|
889
|
+
);
|
|
890
|
+
expect(result).toBeUndefined();
|
|
583
891
|
});
|
|
584
892
|
|
|
585
|
-
it('should
|
|
586
|
-
const
|
|
587
|
-
|
|
893
|
+
it('should return merge instructions for github-flow', () => {
|
|
894
|
+
const result = getValidationApprovedGitInstructions(
|
|
895
|
+
{ git_workflow: 'github-flow', git_main_branch: 'main' },
|
|
896
|
+
'feature/test'
|
|
897
|
+
);
|
|
588
898
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
899
|
+
expect(result).toMatchObject({
|
|
900
|
+
target_branch: 'main',
|
|
901
|
+
feature_branch: 'feature/test',
|
|
902
|
+
steps: expect.any(Array),
|
|
903
|
+
cleanup: expect.any(Array),
|
|
904
|
+
});
|
|
592
905
|
});
|
|
593
906
|
|
|
594
|
-
it('should
|
|
595
|
-
const
|
|
596
|
-
|
|
907
|
+
it('should return merge instructions for git-flow with develop', () => {
|
|
908
|
+
const result = getValidationApprovedGitInstructions(
|
|
909
|
+
{ git_workflow: 'git-flow', git_main_branch: 'main', git_develop_branch: 'develop' },
|
|
910
|
+
'feature/test'
|
|
911
|
+
);
|
|
597
912
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
913
|
+
expect(result).toMatchObject({
|
|
914
|
+
target_branch: 'develop',
|
|
915
|
+
feature_branch: 'feature/test',
|
|
916
|
+
});
|
|
601
917
|
});
|
|
602
918
|
});
|