tlc-claude-code 1.4.2 → 1.4.5

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 (113) hide show
  1. package/dashboard/dist/App.js +28 -2
  2. package/dashboard/dist/api/health-diagnostics.d.ts +26 -0
  3. package/dashboard/dist/api/health-diagnostics.js +85 -0
  4. package/dashboard/dist/api/health-diagnostics.test.d.ts +1 -0
  5. package/dashboard/dist/api/health-diagnostics.test.js +126 -0
  6. package/dashboard/dist/api/index.d.ts +5 -0
  7. package/dashboard/dist/api/index.js +5 -0
  8. package/dashboard/dist/api/notes-api.d.ts +18 -0
  9. package/dashboard/dist/api/notes-api.js +68 -0
  10. package/dashboard/dist/api/notes-api.test.d.ts +1 -0
  11. package/dashboard/dist/api/notes-api.test.js +113 -0
  12. package/dashboard/dist/api/safeFetch.d.ts +50 -0
  13. package/dashboard/dist/api/safeFetch.js +135 -0
  14. package/dashboard/dist/api/safeFetch.test.d.ts +1 -0
  15. package/dashboard/dist/api/safeFetch.test.js +215 -0
  16. package/dashboard/dist/api/tasks-api.d.ts +32 -0
  17. package/dashboard/dist/api/tasks-api.js +98 -0
  18. package/dashboard/dist/api/tasks-api.test.d.ts +1 -0
  19. package/dashboard/dist/api/tasks-api.test.js +383 -0
  20. package/dashboard/dist/components/BugsPane.d.ts +20 -0
  21. package/dashboard/dist/components/BugsPane.js +210 -0
  22. package/dashboard/dist/components/BugsPane.test.d.ts +1 -0
  23. package/dashboard/dist/components/BugsPane.test.js +256 -0
  24. package/dashboard/dist/components/HealthPane.d.ts +3 -1
  25. package/dashboard/dist/components/HealthPane.js +44 -6
  26. package/dashboard/dist/components/HealthPane.test.js +105 -2
  27. package/dashboard/dist/components/RouterPane.d.ts +4 -3
  28. package/dashboard/dist/components/RouterPane.js +60 -57
  29. package/dashboard/dist/components/RouterPane.test.js +150 -96
  30. package/dashboard/dist/components/UpdateBanner.d.ts +26 -0
  31. package/dashboard/dist/components/UpdateBanner.js +30 -0
  32. package/dashboard/dist/components/UpdateBanner.test.d.ts +1 -0
  33. package/dashboard/dist/components/UpdateBanner.test.js +96 -0
  34. package/dashboard/dist/components/accessibility.test.d.ts +1 -0
  35. package/dashboard/dist/components/accessibility.test.js +116 -0
  36. package/dashboard/dist/components/layout/MobileNav.d.ts +16 -0
  37. package/dashboard/dist/components/layout/MobileNav.js +31 -0
  38. package/dashboard/dist/components/layout/MobileNav.test.d.ts +1 -0
  39. package/dashboard/dist/components/layout/MobileNav.test.js +111 -0
  40. package/dashboard/dist/components/performance.test.d.ts +1 -0
  41. package/dashboard/dist/components/performance.test.js +114 -0
  42. package/dashboard/dist/components/responsive.test.d.ts +1 -0
  43. package/dashboard/dist/components/responsive.test.js +114 -0
  44. package/dashboard/dist/components/ui/Dropdown.d.ts +22 -0
  45. package/dashboard/dist/components/ui/Dropdown.js +109 -0
  46. package/dashboard/dist/components/ui/Dropdown.test.d.ts +1 -0
  47. package/dashboard/dist/components/ui/Dropdown.test.js +105 -0
  48. package/dashboard/dist/components/ui/EmptyState.d.ts +14 -0
  49. package/dashboard/dist/components/ui/EmptyState.js +58 -0
  50. package/dashboard/dist/components/ui/EmptyState.test.d.ts +1 -0
  51. package/dashboard/dist/components/ui/EmptyState.test.js +97 -0
  52. package/dashboard/dist/components/ui/ErrorState.d.ts +17 -0
  53. package/dashboard/dist/components/ui/ErrorState.js +80 -0
  54. package/dashboard/dist/components/ui/ErrorState.test.d.ts +1 -0
  55. package/dashboard/dist/components/ui/ErrorState.test.js +166 -0
  56. package/dashboard/dist/components/ui/Modal.d.ts +13 -0
  57. package/dashboard/dist/components/ui/Modal.js +25 -0
  58. package/dashboard/dist/components/ui/Modal.test.d.ts +1 -0
  59. package/dashboard/dist/components/ui/Modal.test.js +91 -0
  60. package/dashboard/dist/components/ui/Skeleton.d.ts +32 -0
  61. package/dashboard/dist/components/ui/Skeleton.js +48 -0
  62. package/dashboard/dist/components/ui/Skeleton.test.d.ts +1 -0
  63. package/dashboard/dist/components/ui/Skeleton.test.js +125 -0
  64. package/dashboard/dist/components/ui/Toast.d.ts +32 -0
  65. package/dashboard/dist/components/ui/Toast.js +21 -0
  66. package/dashboard/dist/components/ui/Toast.test.d.ts +1 -0
  67. package/dashboard/dist/components/ui/Toast.test.js +118 -0
  68. package/dashboard/dist/hooks/useTheme.d.ts +37 -0
  69. package/dashboard/dist/hooks/useTheme.js +96 -0
  70. package/dashboard/dist/hooks/useTheme.test.d.ts +1 -0
  71. package/dashboard/dist/hooks/useTheme.test.js +94 -0
  72. package/dashboard/dist/hooks/useWebSocket.d.ts +17 -0
  73. package/dashboard/dist/hooks/useWebSocket.js +100 -0
  74. package/dashboard/dist/hooks/useWebSocket.test.d.ts +1 -0
  75. package/dashboard/dist/hooks/useWebSocket.test.js +115 -0
  76. package/dashboard/dist/stores/projectStore.d.ts +44 -0
  77. package/dashboard/dist/stores/projectStore.js +76 -0
  78. package/dashboard/dist/stores/projectStore.test.d.ts +1 -0
  79. package/dashboard/dist/stores/projectStore.test.js +114 -0
  80. package/dashboard/dist/stores/uiStore.d.ts +29 -0
  81. package/dashboard/dist/stores/uiStore.js +72 -0
  82. package/dashboard/dist/stores/uiStore.test.d.ts +1 -0
  83. package/dashboard/dist/stores/uiStore.test.js +93 -0
  84. package/dashboard/package.json +6 -3
  85. package/docker-compose.dev.yml +6 -1
  86. package/package.json +1 -1
  87. package/server/dashboard/index.html +1545 -791
  88. package/server/index.js +64 -0
  89. package/server/lib/api-provider.js +104 -186
  90. package/server/lib/api-provider.test.js +238 -336
  91. package/server/lib/cli-detector.js +90 -166
  92. package/server/lib/cli-detector.test.js +114 -269
  93. package/server/lib/cli-provider.js +142 -212
  94. package/server/lib/cli-provider.test.js +196 -349
  95. package/server/lib/debug.test.js +1 -1
  96. package/server/lib/devserver-router-api.js +54 -249
  97. package/server/lib/devserver-router-api.test.js +126 -426
  98. package/server/lib/introspect.js +309 -0
  99. package/server/lib/introspect.test.js +286 -0
  100. package/server/lib/model-router.js +107 -245
  101. package/server/lib/model-router.test.js +122 -313
  102. package/server/lib/output-schemas.js +146 -269
  103. package/server/lib/output-schemas.test.js +106 -307
  104. package/server/lib/provider-interface.js +99 -153
  105. package/server/lib/provider-interface.test.js +228 -394
  106. package/server/lib/provider-queue.js +164 -158
  107. package/server/lib/provider-queue.test.js +186 -315
  108. package/server/lib/router-config.js +99 -221
  109. package/server/lib/router-config.test.js +83 -237
  110. package/server/lib/router-setup-command.js +94 -419
  111. package/server/lib/router-setup-command.test.js +96 -375
  112. package/server/lib/router-status-api.js +93 -0
  113. package/server/lib/router-status-api.test.js +270 -0
@@ -1,394 +1,228 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import {
3
- createProvider,
4
- validateConfig,
5
- PROVIDER_TYPES,
6
- createProviderResult,
7
- } from './provider-interface.js';
8
-
9
- describe('provider-interface', () => {
10
- describe('createProvider', () => {
11
- it('creates CLI provider with correct shape', () => {
12
- const provider = createProvider({
13
- name: 'claude',
14
- type: 'cli',
15
- command: 'claude',
16
- capabilities: ['review', 'code-gen'],
17
- });
18
-
19
- expect(provider.name).toBe('claude');
20
- expect(provider.type).toBe('cli');
21
- expect(provider.capabilities).toContain('review');
22
- expect(typeof provider.run).toBe('function');
23
- });
24
-
25
- it('creates API provider with correct shape', () => {
26
- const provider = createProvider({
27
- name: 'deepseek',
28
- type: 'api',
29
- baseUrl: 'https://api.deepseek.com',
30
- model: 'deepseek-coder',
31
- capabilities: ['review'],
32
- });
33
-
34
- expect(provider.name).toBe('deepseek');
35
- expect(provider.type).toBe('api');
36
- expect(provider.baseUrl).toBe('https://api.deepseek.com');
37
- expect(typeof provider.run).toBe('function');
38
- });
39
-
40
- it('throws on invalid type', () => {
41
- expect(() => createProvider({
42
- name: 'test',
43
- type: 'invalid',
44
- capabilities: [],
45
- })).toThrow('Invalid provider type');
46
- });
47
-
48
- it('throws on missing name', () => {
49
- expect(() => createProvider({
50
- type: 'cli',
51
- capabilities: [],
52
- })).toThrow('name is required');
53
- });
54
-
55
- it('defaults capabilities to empty array', () => {
56
- const provider = createProvider({
57
- name: 'test',
58
- type: 'cli',
59
- command: 'test',
60
- });
61
-
62
- expect(provider.capabilities).toEqual([]);
63
- });
64
-
65
- it('sets detected to false by default', () => {
66
- const provider = createProvider({
67
- name: 'test',
68
- type: 'cli',
69
- command: 'test',
70
- });
71
-
72
- expect(provider.detected).toBe(false);
73
- });
74
- });
75
-
76
- describe('provider.run', () => {
77
- it('returns ProviderResult shape', async () => {
78
- const provider = createProvider({
79
- name: 'mock',
80
- type: 'cli',
81
- command: 'echo',
82
- capabilities: [],
83
- // Mock runner for testing
84
- runner: async () => ({
85
- raw: '{"result": "ok"}',
86
- parsed: { result: 'ok' },
87
- exitCode: 0,
88
- }),
89
- });
90
-
91
- const result = await provider.run('test prompt', {});
92
-
93
- expect(result).toHaveProperty('raw');
94
- expect(result).toHaveProperty('parsed');
95
- expect(result).toHaveProperty('exitCode');
96
- });
97
-
98
- it('includes token usage when available', async () => {
99
- const provider = createProvider({
100
- name: 'mock',
101
- type: 'api',
102
- baseUrl: 'https://example.com',
103
- capabilities: [],
104
- runner: async () => ({
105
- raw: '{}',
106
- parsed: {},
107
- exitCode: 0,
108
- tokenUsage: { input: 100, output: 50 },
109
- }),
110
- });
111
-
112
- const result = await provider.run('test', {});
113
-
114
- expect(result.tokenUsage).toEqual({ input: 100, output: 50 });
115
- });
116
-
117
- it('calculates cost from token usage', async () => {
118
- const provider = createProvider({
119
- name: 'mock',
120
- type: 'api',
121
- baseUrl: 'https://example.com',
122
- capabilities: [],
123
- pricing: { input: 0.001, output: 0.002 }, // per 1K tokens
124
- runner: async () => ({
125
- raw: '{}',
126
- parsed: {},
127
- exitCode: 0,
128
- tokenUsage: { input: 1000, output: 500 },
129
- }),
130
- });
131
-
132
- const result = await provider.run('test', {});
133
-
134
- expect(result.cost).toBe(0.002); // (1000 * 0.001 + 500 * 0.002) / 1000
135
- });
136
- });
137
-
138
- describe('createProviderResult', () => {
139
- it('creates result with required fields', () => {
140
- const result = createProviderResult({
141
- raw: 'output',
142
- parsed: { data: 'test' },
143
- exitCode: 0,
144
- });
145
-
146
- expect(result.raw).toBe('output');
147
- expect(result.parsed).toEqual({ data: 'test' });
148
- expect(result.exitCode).toBe(0);
149
- });
150
-
151
- it('includes optional tokenUsage', () => {
152
- const result = createProviderResult({
153
- raw: '',
154
- parsed: null,
155
- exitCode: 0,
156
- tokenUsage: { input: 100, output: 50 },
157
- });
158
-
159
- expect(result.tokenUsage).toEqual({ input: 100, output: 50 });
160
- });
161
-
162
- it('includes optional cost', () => {
163
- const result = createProviderResult({
164
- raw: '',
165
- parsed: null,
166
- exitCode: 0,
167
- cost: 0.05,
168
- });
169
-
170
- expect(result.cost).toBe(0.05);
171
- });
172
-
173
- it('defaults tokenUsage to null', () => {
174
- const result = createProviderResult({
175
- raw: '',
176
- parsed: null,
177
- exitCode: 0,
178
- });
179
-
180
- expect(result.tokenUsage).toBeNull();
181
- });
182
-
183
- it('defaults cost to null', () => {
184
- const result = createProviderResult({
185
- raw: '',
186
- parsed: null,
187
- exitCode: 0,
188
- });
189
-
190
- expect(result.cost).toBeNull();
191
- });
192
- });
193
-
194
- describe('validateConfig', () => {
195
- it('accepts valid CLI config', () => {
196
- const config = {
197
- name: 'claude',
198
- type: 'cli',
199
- command: 'claude',
200
- capabilities: ['review'],
201
- };
202
-
203
- expect(() => validateConfig(config)).not.toThrow();
204
- });
205
-
206
- it('accepts valid API config', () => {
207
- const config = {
208
- name: 'deepseek',
209
- type: 'api',
210
- baseUrl: 'https://api.deepseek.com',
211
- model: 'deepseek-coder',
212
- capabilities: ['review'],
213
- };
214
-
215
- expect(() => validateConfig(config)).not.toThrow();
216
- });
217
-
218
- it('rejects missing name', () => {
219
- const config = {
220
- type: 'cli',
221
- command: 'test',
222
- };
223
-
224
- expect(() => validateConfig(config)).toThrow('name is required');
225
- });
226
-
227
- it('rejects missing type', () => {
228
- const config = {
229
- name: 'test',
230
- command: 'test',
231
- };
232
-
233
- expect(() => validateConfig(config)).toThrow('type is required');
234
- });
235
-
236
- it('rejects invalid type', () => {
237
- const config = {
238
- name: 'test',
239
- type: 'invalid',
240
- };
241
-
242
- expect(() => validateConfig(config)).toThrow('Invalid provider type');
243
- });
244
-
245
- it('rejects CLI config without command', () => {
246
- const config = {
247
- name: 'test',
248
- type: 'cli',
249
- };
250
-
251
- expect(() => validateConfig(config)).toThrow('command is required for CLI providers');
252
- });
253
-
254
- it('rejects API config without baseUrl', () => {
255
- const config = {
256
- name: 'test',
257
- type: 'api',
258
- };
259
-
260
- expect(() => validateConfig(config)).toThrow('baseUrl is required for API providers');
261
- });
262
- });
263
-
264
- describe('PROVIDER_TYPES', () => {
265
- it('exports CLI type', () => {
266
- expect(PROVIDER_TYPES.CLI).toBe('cli');
267
- });
268
-
269
- it('exports API type', () => {
270
- expect(PROVIDER_TYPES.API).toBe('api');
271
- });
272
- });
273
-
274
- describe('provider.type distinguishes cli from api', () => {
275
- it('CLI provider has type cli', () => {
276
- const provider = createProvider({
277
- name: 'claude',
278
- type: 'cli',
279
- command: 'claude',
280
- });
281
-
282
- expect(provider.type).toBe('cli');
283
- });
284
-
285
- it('API provider has type api', () => {
286
- const provider = createProvider({
287
- name: 'deepseek',
288
- type: 'api',
289
- baseUrl: 'https://api.deepseek.com',
290
- });
291
-
292
- expect(provider.type).toBe('api');
293
- });
294
- });
295
-
296
- describe('provider.capabilities', () => {
297
- it('returns capabilities array', () => {
298
- const provider = createProvider({
299
- name: 'claude',
300
- type: 'cli',
301
- command: 'claude',
302
- capabilities: ['review', 'code-gen', 'refactor'],
303
- });
304
-
305
- expect(provider.capabilities).toEqual(['review', 'code-gen', 'refactor']);
306
- });
307
-
308
- it('capabilities is immutable', () => {
309
- const provider = createProvider({
310
- name: 'claude',
311
- type: 'cli',
312
- command: 'claude',
313
- capabilities: ['review'],
314
- });
315
-
316
- // Try to mutate - should throw or be ignored
317
- expect(() => provider.capabilities.push('hack')).toThrow();
318
-
319
- // Original should be unchanged
320
- expect(provider.capabilities).toEqual(['review']);
321
- });
322
- });
323
-
324
- describe('RunOpts', () => {
325
- it('accepts outputFormat option', async () => {
326
- let capturedOpts;
327
- const provider = createProvider({
328
- name: 'mock',
329
- type: 'cli',
330
- command: 'test',
331
- runner: async (prompt, opts) => {
332
- capturedOpts = opts;
333
- return { raw: '', parsed: null, exitCode: 0 };
334
- },
335
- });
336
-
337
- await provider.run('test', { outputFormat: 'json' });
338
-
339
- expect(capturedOpts.outputFormat).toBe('json');
340
- });
341
-
342
- it('accepts sandbox option', async () => {
343
- let capturedOpts;
344
- const provider = createProvider({
345
- name: 'mock',
346
- type: 'cli',
347
- command: 'test',
348
- runner: async (prompt, opts) => {
349
- capturedOpts = opts;
350
- return { raw: '', parsed: null, exitCode: 0 };
351
- },
352
- });
353
-
354
- await provider.run('test', { sandbox: 'read-only' });
355
-
356
- expect(capturedOpts.sandbox).toBe('read-only');
357
- });
358
-
359
- it('accepts outputSchema option', async () => {
360
- let capturedOpts;
361
- const schema = { type: 'object', properties: { result: { type: 'string' } } };
362
- const provider = createProvider({
363
- name: 'mock',
364
- type: 'cli',
365
- command: 'test',
366
- runner: async (prompt, opts) => {
367
- capturedOpts = opts;
368
- return { raw: '', parsed: null, exitCode: 0 };
369
- },
370
- });
371
-
372
- await provider.run('test', { outputSchema: schema });
373
-
374
- expect(capturedOpts.outputSchema).toEqual(schema);
375
- });
376
-
377
- it('accepts cwd option', async () => {
378
- let capturedOpts;
379
- const provider = createProvider({
380
- name: 'mock',
381
- type: 'cli',
382
- command: 'test',
383
- runner: async (prompt, opts) => {
384
- capturedOpts = opts;
385
- return { raw: '', parsed: null, exitCode: 0 };
386
- },
387
- });
388
-
389
- await provider.run('test', { cwd: '/some/path' });
390
-
391
- expect(capturedOpts.cwd).toBe('/some/path');
392
- });
393
- });
394
- });
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import {
3
+ createProvider,
4
+ validateProviderConfig,
5
+ ProviderType,
6
+ } from './provider-interface.js';
7
+
8
+ describe('Provider Interface', () => {
9
+ describe('createProvider', () => {
10
+ it('creates CLI provider with correct type', () => {
11
+ const provider = createProvider({
12
+ name: 'claude',
13
+ type: 'cli',
14
+ command: 'claude',
15
+ capabilities: ['review', 'code-gen'],
16
+ });
17
+
18
+ expect(provider.name).toBe('claude');
19
+ expect(provider.type).toBe('cli');
20
+ });
21
+
22
+ it('creates API provider with correct type', () => {
23
+ const provider = createProvider({
24
+ name: 'deepseek',
25
+ type: 'api',
26
+ baseUrl: 'https://api.deepseek.com',
27
+ model: 'deepseek-coder',
28
+ capabilities: ['review'],
29
+ });
30
+
31
+ expect(provider.name).toBe('deepseek');
32
+ expect(provider.type).toBe('api');
33
+ });
34
+
35
+ it('throws on invalid type', () => {
36
+ expect(() =>
37
+ createProvider({
38
+ name: 'test',
39
+ type: 'invalid',
40
+ capabilities: [],
41
+ })
42
+ ).toThrow(/invalid.*type/i);
43
+ });
44
+
45
+ it('provider.run returns ProviderResult shape', async () => {
46
+ const provider = createProvider({
47
+ name: 'mock',
48
+ type: 'cli',
49
+ command: 'echo',
50
+ capabilities: ['test'],
51
+ });
52
+
53
+ provider._execute = vi.fn().mockResolvedValue({
54
+ raw: '{"result": "test"}',
55
+ parsed: { result: 'test' },
56
+ exitCode: 0,
57
+ tokenUsage: { input: 10, output: 5 },
58
+ cost: 0.0001,
59
+ });
60
+
61
+ const result = await provider.run('test prompt');
62
+
63
+ expect(result).toHaveProperty('raw');
64
+ expect(result).toHaveProperty('parsed');
65
+ expect(result).toHaveProperty('exitCode');
66
+ expect(result).toHaveProperty('tokenUsage');
67
+ expect(result).toHaveProperty('cost');
68
+ });
69
+
70
+ it('ProviderResult includes token usage', async () => {
71
+ const provider = createProvider({
72
+ name: 'mock',
73
+ type: 'api',
74
+ baseUrl: 'https://test.com',
75
+ model: 'test',
76
+ capabilities: ['test'],
77
+ });
78
+
79
+ provider._execute = vi.fn().mockResolvedValue({
80
+ raw: 'response',
81
+ parsed: null,
82
+ exitCode: 0,
83
+ tokenUsage: { input: 100, output: 50 },
84
+ cost: 0.001,
85
+ });
86
+
87
+ const result = await provider.run('test');
88
+
89
+ expect(result.tokenUsage).toEqual({ input: 100, output: 50 });
90
+ });
91
+
92
+ it('ProviderResult calculates cost', async () => {
93
+ const provider = createProvider({
94
+ name: 'mock',
95
+ type: 'api',
96
+ baseUrl: 'https://test.com',
97
+ model: 'test',
98
+ capabilities: ['test'],
99
+ pricing: { inputPer1k: 0.001, outputPer1k: 0.002 },
100
+ });
101
+
102
+ provider._execute = vi.fn().mockResolvedValue({
103
+ raw: 'response',
104
+ parsed: null,
105
+ exitCode: 0,
106
+ tokenUsage: { input: 1000, output: 500 },
107
+ cost: 0.002,
108
+ });
109
+
110
+ const result = await provider.run('test');
111
+
112
+ expect(result.cost).toBe(0.002);
113
+ });
114
+
115
+ it('provider.capabilities returns array', () => {
116
+ const provider = createProvider({
117
+ name: 'claude',
118
+ type: 'cli',
119
+ command: 'claude',
120
+ capabilities: ['review', 'code-gen', 'refactor'],
121
+ });
122
+
123
+ expect(provider.capabilities).toEqual(['review', 'code-gen', 'refactor']);
124
+ });
125
+
126
+ it('provider.type distinguishes cli from api', () => {
127
+ const cliProvider = createProvider({
128
+ name: 'claude',
129
+ type: 'cli',
130
+ command: 'claude',
131
+ capabilities: ['review'],
132
+ });
133
+
134
+ const apiProvider = createProvider({
135
+ name: 'deepseek',
136
+ type: 'api',
137
+ baseUrl: 'https://api.deepseek.com',
138
+ model: 'deepseek-coder',
139
+ capabilities: ['review'],
140
+ });
141
+
142
+ expect(cliProvider.type).toBe('cli');
143
+ expect(apiProvider.type).toBe('api');
144
+ });
145
+ });
146
+
147
+ describe('validateProviderConfig', () => {
148
+ it('rejects missing name', () => {
149
+ const result = validateProviderConfig({
150
+ type: 'cli',
151
+ command: 'claude',
152
+ capabilities: ['review'],
153
+ });
154
+
155
+ expect(result.valid).toBe(false);
156
+ expect(result.errors).toContain('name is required');
157
+ });
158
+
159
+ it('rejects missing type', () => {
160
+ const result = validateProviderConfig({
161
+ name: 'test',
162
+ command: 'claude',
163
+ capabilities: ['review'],
164
+ });
165
+
166
+ expect(result.valid).toBe(false);
167
+ expect(result.errors).toContain('type is required');
168
+ });
169
+
170
+ it('rejects CLI without command', () => {
171
+ const result = validateProviderConfig({
172
+ name: 'test',
173
+ type: 'cli',
174
+ capabilities: ['review'],
175
+ });
176
+
177
+ expect(result.valid).toBe(false);
178
+ expect(result.errors).toContain('command is required for CLI providers');
179
+ });
180
+
181
+ it('rejects API without baseUrl', () => {
182
+ const result = validateProviderConfig({
183
+ name: 'test',
184
+ type: 'api',
185
+ model: 'test',
186
+ capabilities: ['review'],
187
+ });
188
+
189
+ expect(result.valid).toBe(false);
190
+ expect(result.errors).toContain('baseUrl is required for API providers');
191
+ });
192
+
193
+ it('accepts valid CLI config', () => {
194
+ const result = validateProviderConfig({
195
+ name: 'claude',
196
+ type: 'cli',
197
+ command: 'claude',
198
+ capabilities: ['review', 'code-gen'],
199
+ });
200
+
201
+ expect(result.valid).toBe(true);
202
+ expect(result.errors).toHaveLength(0);
203
+ });
204
+
205
+ it('accepts valid API config', () => {
206
+ const result = validateProviderConfig({
207
+ name: 'deepseek',
208
+ type: 'api',
209
+ baseUrl: 'https://api.deepseek.com',
210
+ model: 'deepseek-coder',
211
+ capabilities: ['review'],
212
+ });
213
+
214
+ expect(result.valid).toBe(true);
215
+ expect(result.errors).toHaveLength(0);
216
+ });
217
+ });
218
+
219
+ describe('ProviderType', () => {
220
+ it('exports CLI constant', () => {
221
+ expect(ProviderType.CLI).toBe('cli');
222
+ });
223
+
224
+ it('exports API constant', () => {
225
+ expect(ProviderType.API).toBe('api');
226
+ });
227
+ });
228
+ });