@vibescope/mcp-server 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,176 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ValidationError, validateRequired, validateUUID, validateTaskStatus, validateProjectStatus, validatePriority, validateProgressPercentage, validateEstimatedMinutes, validateEnvironment, VALID_TASK_STATUSES, VALID_PROJECT_STATUSES, VALID_ENVIRONMENTS, } from './validators.js';
3
+ describe('ValidationError', () => {
4
+ it('should create error with message', () => {
5
+ const error = new ValidationError('Test error');
6
+ expect(error.message).toBe('Test error');
7
+ expect(error.name).toBe('ValidationError');
8
+ });
9
+ it('should include optional fields', () => {
10
+ const error = new ValidationError('Test error', {
11
+ field: 'test_field',
12
+ hint: 'Test hint',
13
+ validValues: ['a', 'b'],
14
+ });
15
+ expect(error.field).toBe('test_field');
16
+ expect(error.hint).toBe('Test hint');
17
+ expect(error.validValues).toEqual(['a', 'b']);
18
+ });
19
+ it('should serialize to JSON correctly', () => {
20
+ const error = new ValidationError('Test error', {
21
+ field: 'test_field',
22
+ hint: 'Test hint',
23
+ validValues: ['a', 'b'],
24
+ });
25
+ const json = error.toJSON();
26
+ expect(json.error).toBe('validation_error');
27
+ expect(json.message).toBe('Test error');
28
+ expect(json.field).toBe('test_field');
29
+ expect(json.hint).toBe('Test hint');
30
+ expect(json.valid_values).toEqual(['a', 'b']);
31
+ });
32
+ });
33
+ describe('validateRequired', () => {
34
+ it('should pass for valid values', () => {
35
+ expect(() => validateRequired('value', 'field')).not.toThrow();
36
+ expect(() => validateRequired(0, 'field')).not.toThrow();
37
+ expect(() => validateRequired(false, 'field')).not.toThrow();
38
+ expect(() => validateRequired({}, 'field')).not.toThrow();
39
+ });
40
+ it('should throw for undefined', () => {
41
+ expect(() => validateRequired(undefined, 'test_field')).toThrow(ValidationError);
42
+ expect(() => validateRequired(undefined, 'test_field')).toThrow('Missing required field: test_field');
43
+ });
44
+ it('should throw for null', () => {
45
+ expect(() => validateRequired(null, 'test_field')).toThrow(ValidationError);
46
+ });
47
+ it('should throw for empty string', () => {
48
+ expect(() => validateRequired('', 'test_field')).toThrow(ValidationError);
49
+ });
50
+ });
51
+ describe('validateUUID', () => {
52
+ it('should pass for valid UUIDs', () => {
53
+ expect(() => validateUUID('123e4567-e89b-12d3-a456-426614174000', 'id')).not.toThrow();
54
+ expect(() => validateUUID('00000000-0000-0000-0000-000000000000', 'id')).not.toThrow();
55
+ expect(() => validateUUID('FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF', 'id')).not.toThrow();
56
+ });
57
+ it('should pass for undefined (optional)', () => {
58
+ expect(() => validateUUID(undefined, 'id')).not.toThrow();
59
+ });
60
+ it('should pass for empty string (treated as optional)', () => {
61
+ expect(() => validateUUID('', 'id')).not.toThrow();
62
+ });
63
+ it('should throw for invalid UUIDs', () => {
64
+ expect(() => validateUUID('not-a-uuid', 'id')).toThrow(ValidationError);
65
+ expect(() => validateUUID('123', 'id')).toThrow(ValidationError);
66
+ expect(() => validateUUID('123e4567-e89b-12d3-a456', 'id')).toThrow(ValidationError);
67
+ expect(() => validateUUID('123e4567-e89b-12d3-a456-426614174000-extra', 'id')).toThrow(ValidationError);
68
+ });
69
+ });
70
+ describe('validateTaskStatus', () => {
71
+ it('should pass for valid task statuses', () => {
72
+ for (const status of VALID_TASK_STATUSES) {
73
+ expect(() => validateTaskStatus(status)).not.toThrow();
74
+ }
75
+ });
76
+ it('should pass for undefined (optional)', () => {
77
+ expect(() => validateTaskStatus(undefined)).not.toThrow();
78
+ });
79
+ it('should throw for invalid status', () => {
80
+ expect(() => validateTaskStatus('invalid')).toThrow(ValidationError);
81
+ expect(() => validateTaskStatus('PENDING')).toThrow(ValidationError);
82
+ expect(() => validateTaskStatus('done')).toThrow(ValidationError);
83
+ });
84
+ });
85
+ describe('validateProjectStatus', () => {
86
+ it('should pass for valid project statuses', () => {
87
+ for (const status of VALID_PROJECT_STATUSES) {
88
+ expect(() => validateProjectStatus(status)).not.toThrow();
89
+ }
90
+ });
91
+ it('should pass for undefined (optional)', () => {
92
+ expect(() => validateProjectStatus(undefined)).not.toThrow();
93
+ });
94
+ it('should throw for invalid status', () => {
95
+ expect(() => validateProjectStatus('invalid')).toThrow(ValidationError);
96
+ expect(() => validateProjectStatus('ACTIVE')).toThrow(ValidationError);
97
+ });
98
+ });
99
+ describe('validatePriority', () => {
100
+ it('should pass for valid priorities 1-5', () => {
101
+ for (let i = 1; i <= 5; i++) {
102
+ expect(() => validatePriority(i)).not.toThrow();
103
+ }
104
+ });
105
+ it('should pass for undefined (optional)', () => {
106
+ expect(() => validatePriority(undefined)).not.toThrow();
107
+ });
108
+ it('should throw for out of range values', () => {
109
+ expect(() => validatePriority(0)).toThrow(ValidationError);
110
+ expect(() => validatePriority(6)).toThrow(ValidationError);
111
+ expect(() => validatePriority(-1)).toThrow(ValidationError);
112
+ });
113
+ it('should throw for non-integers', () => {
114
+ expect(() => validatePriority(1.5)).toThrow(ValidationError);
115
+ expect(() => validatePriority(2.7)).toThrow(ValidationError);
116
+ });
117
+ });
118
+ describe('validateProgressPercentage', () => {
119
+ it('should pass for valid percentages 0-100', () => {
120
+ expect(() => validateProgressPercentage(0)).not.toThrow();
121
+ expect(() => validateProgressPercentage(50)).not.toThrow();
122
+ expect(() => validateProgressPercentage(100)).not.toThrow();
123
+ expect(() => validateProgressPercentage(33.33)).not.toThrow();
124
+ });
125
+ it('should pass for undefined (optional)', () => {
126
+ expect(() => validateProgressPercentage(undefined)).not.toThrow();
127
+ });
128
+ it('should throw for out of range values', () => {
129
+ expect(() => validateProgressPercentage(-1)).toThrow(ValidationError);
130
+ expect(() => validateProgressPercentage(101)).toThrow(ValidationError);
131
+ expect(() => validateProgressPercentage(150)).toThrow(ValidationError);
132
+ });
133
+ it('should throw for non-finite values', () => {
134
+ expect(() => validateProgressPercentage(Infinity)).toThrow(ValidationError);
135
+ expect(() => validateProgressPercentage(NaN)).toThrow(ValidationError);
136
+ });
137
+ });
138
+ describe('validateEstimatedMinutes', () => {
139
+ it('should pass for valid positive integers', () => {
140
+ expect(() => validateEstimatedMinutes(1)).not.toThrow();
141
+ expect(() => validateEstimatedMinutes(30)).not.toThrow();
142
+ expect(() => validateEstimatedMinutes(120)).not.toThrow();
143
+ });
144
+ it('should pass for undefined (optional)', () => {
145
+ expect(() => validateEstimatedMinutes(undefined)).not.toThrow();
146
+ });
147
+ it('should throw for zero', () => {
148
+ expect(() => validateEstimatedMinutes(0)).toThrow(ValidationError);
149
+ });
150
+ it('should throw for negative values', () => {
151
+ expect(() => validateEstimatedMinutes(-1)).toThrow(ValidationError);
152
+ expect(() => validateEstimatedMinutes(-30)).toThrow(ValidationError);
153
+ });
154
+ it('should throw for non-integers', () => {
155
+ expect(() => validateEstimatedMinutes(1.5)).toThrow(ValidationError);
156
+ expect(() => validateEstimatedMinutes(30.5)).toThrow(ValidationError);
157
+ });
158
+ });
159
+ describe('validateEnvironment', () => {
160
+ it('should pass for valid environments', () => {
161
+ for (const env of VALID_ENVIRONMENTS) {
162
+ expect(() => validateEnvironment(env)).not.toThrow();
163
+ }
164
+ });
165
+ it('should pass for undefined (optional)', () => {
166
+ expect(() => validateEnvironment(undefined)).not.toThrow();
167
+ });
168
+ it('should pass for empty string (treated as optional)', () => {
169
+ expect(() => validateEnvironment('')).not.toThrow();
170
+ });
171
+ it('should throw for invalid environment', () => {
172
+ expect(() => validateEnvironment('invalid')).toThrow(ValidationError);
173
+ expect(() => validateEnvironment('PRODUCTION')).toThrow(ValidationError);
174
+ expect(() => validateEnvironment('prod')).toThrow(ValidationError);
175
+ });
176
+ });
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@vibescope/mcp-server",
3
+ "version": "0.0.1",
4
+ "description": "MCP server for Vibescope - AI project tracking tools",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "vibescope-mcp": "dist/index.js",
10
+ "vibescope-cli": "dist/cli.js"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/Nonatomic/Vibescope.git",
15
+ "directory": "packages/mcp-server"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "dev": "tsc --watch",
23
+ "test": "vitest run",
24
+ "test:watch": "vitest",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "keywords": [
28
+ "mcp",
29
+ "model-context-protocol",
30
+ "vibescope",
31
+ "ai",
32
+ "project-management"
33
+ ],
34
+ "license": "MIT",
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.0",
37
+ "typescript": "^5.7.0",
38
+ "vitest": "^4.0.17"
39
+ },
40
+ "dependencies": {
41
+ "@modelcontextprotocol/sdk": "^1.25.2",
42
+ "@supabase/supabase-js": "^2.90.1"
43
+ }
44
+ }
@@ -0,0 +1,442 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { createHash } from 'crypto';
3
+
4
+ // Mock child_process before importing cli module
5
+ vi.mock('child_process', () => ({
6
+ execSync: vi.fn(),
7
+ }));
8
+
9
+ // Mock @supabase/supabase-js
10
+ vi.mock('@supabase/supabase-js', () => ({
11
+ createClient: vi.fn(),
12
+ }));
13
+
14
+ import { execSync } from 'child_process';
15
+ import { createClient } from '@supabase/supabase-js';
16
+ import {
17
+ hashApiKey,
18
+ detectGitUrl,
19
+ validateApiKey,
20
+ type AuthContext,
21
+ type VerificationResult,
22
+ } from './cli.js';
23
+
24
+ describe('CLI verification logic', () => {
25
+ beforeEach(() => {
26
+ vi.clearAllMocks();
27
+ });
28
+
29
+ describe('hashApiKey', () => {
30
+ it('should return SHA256 hash of the key', () => {
31
+ const key = 'test-api-key';
32
+ const expected = createHash('sha256').update(key).digest('hex');
33
+ expect(hashApiKey(key)).toBe(expected);
34
+ });
35
+
36
+ it('should return different hashes for different keys', () => {
37
+ const hash1 = hashApiKey('key1');
38
+ const hash2 = hashApiKey('key2');
39
+ expect(hash1).not.toBe(hash2);
40
+ });
41
+
42
+ it('should return same hash for same key', () => {
43
+ const hash1 = hashApiKey('same-key');
44
+ const hash2 = hashApiKey('same-key');
45
+ expect(hash1).toBe(hash2);
46
+ });
47
+
48
+ it('should return 64-character hex string', () => {
49
+ const hash = hashApiKey('any-key');
50
+ expect(hash).toMatch(/^[a-f0-9]{64}$/);
51
+ });
52
+ });
53
+
54
+ describe('detectGitUrl', () => {
55
+ it('should return normalized git URL when git command succeeds', () => {
56
+ vi.mocked(execSync).mockReturnValue('git@github.com:user/repo.git\n');
57
+ const result = detectGitUrl();
58
+ expect(result).toBe('https://github.com/user/repo');
59
+ });
60
+
61
+ it('should return null when git command fails', () => {
62
+ vi.mocked(execSync).mockImplementation(() => {
63
+ throw new Error('Not a git repository');
64
+ });
65
+ const result = detectGitUrl();
66
+ expect(result).toBeNull();
67
+ });
68
+
69
+ it('should handle HTTPS URLs', () => {
70
+ vi.mocked(execSync).mockReturnValue('https://github.com/user/repo.git\n');
71
+ const result = detectGitUrl();
72
+ expect(result).toBe('https://github.com/user/repo');
73
+ });
74
+
75
+ it('should call execSync with correct options', () => {
76
+ vi.mocked(execSync).mockReturnValue('https://github.com/user/repo.git');
77
+ detectGitUrl();
78
+ expect(execSync).toHaveBeenCalledWith('git config --get remote.origin.url', {
79
+ encoding: 'utf8',
80
+ timeout: 5000,
81
+ stdio: ['pipe', 'pipe', 'pipe'],
82
+ });
83
+ });
84
+ });
85
+
86
+ describe('validateApiKey', () => {
87
+ it('should return auth context for valid API key', async () => {
88
+ const mockSupabase = {
89
+ from: vi.fn().mockReturnThis(),
90
+ select: vi.fn().mockReturnThis(),
91
+ eq: vi.fn().mockReturnThis(),
92
+ single: vi.fn().mockResolvedValue({
93
+ data: { id: 'key-id-123', user_id: 'user-id-456' },
94
+ error: null,
95
+ }),
96
+ };
97
+
98
+ const result = await validateApiKey(mockSupabase, 'test-api-key');
99
+
100
+ expect(result).toEqual({
101
+ userId: 'user-id-456',
102
+ apiKeyId: 'key-id-123',
103
+ });
104
+ });
105
+
106
+ it('should return null for invalid API key', async () => {
107
+ const mockSupabase = {
108
+ from: vi.fn().mockReturnThis(),
109
+ select: vi.fn().mockReturnThis(),
110
+ eq: vi.fn().mockReturnThis(),
111
+ single: vi.fn().mockResolvedValue({
112
+ data: null,
113
+ error: { message: 'Not found' },
114
+ }),
115
+ };
116
+
117
+ const result = await validateApiKey(mockSupabase, 'invalid-key');
118
+ expect(result).toBeNull();
119
+ });
120
+
121
+ it('should query api_keys table with hashed key', async () => {
122
+ const mockFrom = vi.fn().mockReturnThis();
123
+ const mockSelect = vi.fn().mockReturnThis();
124
+ const mockEq = vi.fn().mockReturnThis();
125
+ const mockSingle = vi.fn().mockResolvedValue({ data: null, error: { message: 'Not found' } });
126
+
127
+ const mockSupabase = {
128
+ from: mockFrom,
129
+ select: mockSelect,
130
+ eq: mockEq,
131
+ single: mockSingle,
132
+ };
133
+
134
+ await validateApiKey(mockSupabase, 'test-key');
135
+
136
+ expect(mockFrom).toHaveBeenCalledWith('api_keys');
137
+ expect(mockSelect).toHaveBeenCalledWith('id, user_id');
138
+ expect(mockEq).toHaveBeenCalledWith('key_hash', hashApiKey('test-key'));
139
+ });
140
+
141
+ it('should return null when data is missing', async () => {
142
+ const mockSupabase = {
143
+ from: vi.fn().mockReturnThis(),
144
+ select: vi.fn().mockReturnThis(),
145
+ eq: vi.fn().mockReturnThis(),
146
+ single: vi.fn().mockResolvedValue({
147
+ data: undefined,
148
+ error: null,
149
+ }),
150
+ };
151
+
152
+ const result = await validateApiKey(mockSupabase, 'test-key');
153
+ expect(result).toBeNull();
154
+ });
155
+ });
156
+ });
157
+
158
+ describe('verify function integration', () => {
159
+ const originalEnv = process.env;
160
+
161
+ beforeEach(() => {
162
+ vi.clearAllMocks();
163
+ // Reset process.env for each test
164
+ process.env = { ...originalEnv };
165
+ });
166
+
167
+ afterEach(() => {
168
+ process.env = originalEnv;
169
+ });
170
+
171
+ // Note: The verify function reads environment variables at module load time,
172
+ // so we need to test through dynamic imports or by mocking at a higher level.
173
+ // For now, we test the exported functions individually.
174
+
175
+ describe('environment variable validation', () => {
176
+ it('hashApiKey should work without env vars', () => {
177
+ // hashApiKey is a pure function that doesn't depend on env vars
178
+ expect(hashApiKey('test')).toBeTruthy();
179
+ });
180
+
181
+ it('detectGitUrl should work without env vars', () => {
182
+ vi.mocked(execSync).mockReturnValue('https://github.com/user/repo.git');
183
+ expect(detectGitUrl()).toBe('https://github.com/user/repo');
184
+ });
185
+ });
186
+
187
+ describe('validateApiKey error handling', () => {
188
+ it('should handle database errors gracefully', async () => {
189
+ const mockSupabase = {
190
+ from: vi.fn().mockReturnThis(),
191
+ select: vi.fn().mockReturnThis(),
192
+ eq: vi.fn().mockReturnThis(),
193
+ single: vi.fn().mockRejectedValue(new Error('Database connection failed')),
194
+ };
195
+
196
+ await expect(validateApiKey(mockSupabase, 'test-key')).rejects.toThrow('Database connection failed');
197
+ });
198
+
199
+ it('should handle empty string API key', async () => {
200
+ const mockSupabase = {
201
+ from: vi.fn().mockReturnThis(),
202
+ select: vi.fn().mockReturnThis(),
203
+ eq: vi.fn().mockReturnThis(),
204
+ single: vi.fn().mockResolvedValue({ data: null, error: null }),
205
+ };
206
+
207
+ const result = await validateApiKey(mockSupabase, '');
208
+ expect(result).toBeNull();
209
+ });
210
+ });
211
+ });
212
+
213
+ describe('VerificationResult types', () => {
214
+ it('should have correct status types', () => {
215
+ const compliant: VerificationResult = {
216
+ status: 'compliant',
217
+ reason: 'All good',
218
+ };
219
+ expect(compliant.status).toBe('compliant');
220
+
221
+ const nonCompliant: VerificationResult = {
222
+ status: 'non_compliant',
223
+ reason: 'Tasks in progress',
224
+ continuation_prompt: 'Complete your tasks',
225
+ };
226
+ expect(nonCompliant.status).toBe('non_compliant');
227
+
228
+ const error: VerificationResult = {
229
+ status: 'error',
230
+ reason: 'Missing env vars',
231
+ };
232
+ expect(error.status).toBe('error');
233
+
234
+ const noSession: VerificationResult = {
235
+ status: 'no_session',
236
+ reason: 'No git URL detected',
237
+ };
238
+ expect(noSession.status).toBe('no_session');
239
+ });
240
+
241
+ it('should support optional details', () => {
242
+ const result: VerificationResult = {
243
+ status: 'compliant',
244
+ reason: 'All good',
245
+ details: {
246
+ session_started: true,
247
+ project_id: 'proj-123',
248
+ project_name: 'Test Project',
249
+ git_url: 'https://github.com/user/repo',
250
+ in_progress_tasks: 0,
251
+ tasks_completed_this_session: 5,
252
+ progress_logs_this_session: 3,
253
+ blockers_logged_this_session: 1,
254
+ session_duration_minutes: 45,
255
+ },
256
+ };
257
+
258
+ expect(result.details?.session_started).toBe(true);
259
+ expect(result.details?.tasks_completed_this_session).toBe(5);
260
+ });
261
+ });
262
+
263
+ describe('AuthContext type', () => {
264
+ it('should have required fields', () => {
265
+ const auth: AuthContext = {
266
+ userId: 'user-123',
267
+ apiKeyId: 'key-456',
268
+ };
269
+ expect(auth.userId).toBe('user-123');
270
+ expect(auth.apiKeyId).toBe('key-456');
271
+ });
272
+ });
273
+
274
+ describe('verify function scenarios', () => {
275
+ const originalEnv = { ...process.env };
276
+
277
+ beforeEach(() => {
278
+ vi.clearAllMocks();
279
+ // Set up required env vars
280
+ process.env.SUPABASE_URL = 'https://test.supabase.co';
281
+ process.env.SUPABASE_SERVICE_KEY = 'test-service-key';
282
+ process.env.VIBESCOPE_API_KEY = 'test-api-key';
283
+ });
284
+
285
+ afterEach(() => {
286
+ process.env = { ...originalEnv };
287
+ });
288
+
289
+ // Helper to create a mock Supabase client with chainable methods
290
+ function createMockSupabase(overrides: {
291
+ apiKeyResult?: { data: unknown; error: unknown };
292
+ projectResult?: { data: unknown; error: unknown };
293
+ sessionResult?: { data: unknown; error: unknown };
294
+ tasksResult?: { data: unknown[] | null; error: unknown };
295
+ completedResult?: { count: number | null; error: unknown };
296
+ progressResult?: { count: number | null; error: unknown };
297
+ blockerResult?: { count: number | null; error: unknown };
298
+ } = {}) {
299
+ const defaultApiKey = { data: { id: 'key-id', user_id: 'user-id' }, error: null };
300
+ const defaultProject = {
301
+ data: { id: 'proj-id', name: 'Test Project', git_url: 'https://github.com/test/repo' },
302
+ error: null,
303
+ };
304
+ const defaultSession = {
305
+ data: {
306
+ id: 'session-id',
307
+ created_at: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 mins ago
308
+ last_synced_at: new Date().toISOString(),
309
+ },
310
+ error: null,
311
+ };
312
+ const defaultTasks = { data: [], error: null };
313
+ const defaultCompleted = { count: 1, error: null };
314
+ const defaultProgress = { count: 0, error: null };
315
+ const defaultBlocker = { count: 0, error: null };
316
+
317
+ let currentTable = '';
318
+ let selectCount = false;
319
+
320
+ const mock = {
321
+ from: vi.fn((table: string) => {
322
+ currentTable = table;
323
+ return mock;
324
+ }),
325
+ select: vi.fn((cols: string, opts?: { count: string }) => {
326
+ selectCount = !!opts?.count;
327
+ return mock;
328
+ }),
329
+ eq: vi.fn().mockReturnThis(),
330
+ gte: vi.fn().mockReturnThis(),
331
+ single: vi.fn(() => {
332
+ if (currentTable === 'api_keys') {
333
+ return Promise.resolve(overrides.apiKeyResult ?? defaultApiKey);
334
+ }
335
+ if (currentTable === 'projects') {
336
+ return Promise.resolve(overrides.projectResult ?? defaultProject);
337
+ }
338
+ if (currentTable === 'agent_sessions') {
339
+ return Promise.resolve(overrides.sessionResult ?? defaultSession);
340
+ }
341
+ return Promise.resolve({ data: null, error: null });
342
+ }),
343
+ then: vi.fn((resolve: (value: unknown) => void) => {
344
+ if (currentTable === 'tasks') {
345
+ if (selectCount) {
346
+ return Promise.resolve(overrides.completedResult ?? defaultCompleted).then(resolve);
347
+ }
348
+ return Promise.resolve(overrides.tasksResult ?? defaultTasks).then(resolve);
349
+ }
350
+ if (currentTable === 'progress_logs') {
351
+ return Promise.resolve(overrides.progressResult ?? defaultProgress).then(resolve);
352
+ }
353
+ if (currentTable === 'blockers') {
354
+ return Promise.resolve(overrides.blockerResult ?? defaultBlocker).then(resolve);
355
+ }
356
+ return Promise.resolve({ data: null, count: 0 }).then(resolve);
357
+ }),
358
+ };
359
+
360
+ return mock;
361
+ }
362
+
363
+ it('should validate API key and return auth context', async () => {
364
+ const mockSupabase = createMockSupabase();
365
+ const result = await validateApiKey(mockSupabase, 'test-key');
366
+
367
+ expect(result).toEqual({
368
+ userId: 'user-id',
369
+ apiKeyId: 'key-id',
370
+ });
371
+ });
372
+
373
+ it('should return null for invalid API key', async () => {
374
+ const mockSupabase = createMockSupabase({
375
+ apiKeyResult: { data: null, error: { message: 'Not found' } },
376
+ });
377
+ const result = await validateApiKey(mockSupabase, 'invalid-key');
378
+
379
+ expect(result).toBeNull();
380
+ });
381
+
382
+ it('should handle database query errors', async () => {
383
+ const mockSupabase = {
384
+ from: vi.fn().mockReturnThis(),
385
+ select: vi.fn().mockReturnThis(),
386
+ eq: vi.fn().mockReturnThis(),
387
+ single: vi.fn().mockRejectedValue(new Error('Connection timeout')),
388
+ };
389
+
390
+ await expect(validateApiKey(mockSupabase, 'key')).rejects.toThrow('Connection timeout');
391
+ });
392
+ });
393
+
394
+ describe('detectGitUrl edge cases', () => {
395
+ beforeEach(() => {
396
+ vi.clearAllMocks();
397
+ });
398
+
399
+ it('should trim whitespace from git URL', () => {
400
+ vi.mocked(execSync).mockReturnValue(' https://github.com/user/repo.git \n');
401
+ const result = detectGitUrl();
402
+ expect(result).toBe('https://github.com/user/repo');
403
+ });
404
+
405
+ it('should handle GitLab SSH URLs', () => {
406
+ vi.mocked(execSync).mockReturnValue('git@gitlab.com:user/repo.git');
407
+ const result = detectGitUrl();
408
+ expect(result).toBe('https://gitlab.com/user/repo');
409
+ });
410
+
411
+ it('should handle Azure DevOps SSH URLs (not normalized)', () => {
412
+ // Azure DevOps URLs are not normalized by the current implementation
413
+ // The URL is trimmed but SSH format is preserved
414
+ vi.mocked(execSync).mockReturnValue('git@ssh.dev.azure.com:v3/org/project/repo');
415
+ const result = detectGitUrl();
416
+ expect(result).toBe('git@ssh.dev.azure.com:v3/org/project/repo');
417
+ });
418
+
419
+ it('should handle Bitbucket SSH URLs', () => {
420
+ vi.mocked(execSync).mockReturnValue('git@bitbucket.org:user/repo.git');
421
+ const result = detectGitUrl();
422
+ expect(result).toBe('https://bitbucket.org/user/repo');
423
+ });
424
+
425
+ it('should return null on timeout', () => {
426
+ vi.mocked(execSync).mockImplementation(() => {
427
+ const error = new Error('Command timed out');
428
+ (error as NodeJS.ErrnoException).code = 'ETIMEDOUT';
429
+ throw error;
430
+ });
431
+ const result = detectGitUrl();
432
+ expect(result).toBeNull();
433
+ });
434
+
435
+ it('should return null when not in a git repo', () => {
436
+ vi.mocked(execSync).mockImplementation(() => {
437
+ throw new Error('fatal: not a git repository');
438
+ });
439
+ const result = detectGitUrl();
440
+ expect(result).toBeNull();
441
+ });
442
+ });