bingocode 1.0.29 → 1.0.31

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 (52) hide show
  1. package/adapters/common/__tests__/chat-queue.test.ts +61 -0
  2. package/adapters/common/__tests__/format.test.ts +148 -0
  3. package/adapters/common/__tests__/http-client.test.ts +105 -0
  4. package/adapters/common/__tests__/message-buffer.test.ts +84 -0
  5. package/adapters/common/__tests__/message-dedup.test.ts +57 -0
  6. package/adapters/common/__tests__/session-store.test.ts +62 -0
  7. package/adapters/common/__tests__/ws-bridge.test.ts +177 -0
  8. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +52 -0
  9. package/adapters/common/attachment/__tests__/attachment-store.test.ts +108 -0
  10. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +115 -0
  11. package/adapters/feishu/__tests__/card-errors.test.ts +194 -0
  12. package/adapters/feishu/__tests__/cardkit.test.ts +295 -0
  13. package/adapters/feishu/__tests__/extract-payload.test.ts +77 -0
  14. package/adapters/feishu/__tests__/feishu.test.ts +907 -0
  15. package/adapters/feishu/__tests__/flush-controller.test.ts +290 -0
  16. package/adapters/feishu/__tests__/markdown-style.test.ts +353 -0
  17. package/adapters/feishu/__tests__/media.test.ts +120 -0
  18. package/adapters/feishu/__tests__/streaming-card.test.ts +914 -0
  19. package/adapters/telegram/__tests__/media.test.ts +86 -0
  20. package/adapters/telegram/__tests__/telegram.test.ts +115 -0
  21. package/bin/bingo-win.cjs +26 -0
  22. package/bin/bingocode-win.cjs +55 -3
  23. package/bin/claude-win.cjs +55 -3
  24. package/package.json +1 -1
  25. package/src/entrypoints/cli.tsx +4 -2
  26. package/src/manager/CliMenuManager.tsx +48 -17
  27. package/src/server/__tests__/conversation-service.test.ts +173 -0
  28. package/src/server/__tests__/conversations.test.ts +458 -0
  29. package/src/server/__tests__/cron-scheduler.test.ts +575 -0
  30. package/src/server/__tests__/e2e/business-flow.test.ts +841 -0
  31. package/src/server/__tests__/e2e/full-flow.test.ts +357 -0
  32. package/src/server/__tests__/fixtures/mock-sdk-cli.ts +123 -0
  33. package/src/server/__tests__/haha-oauth-api.test.ts +146 -0
  34. package/src/server/__tests__/haha-oauth-service.test.ts +185 -0
  35. package/src/server/__tests__/providers-real.test.ts +244 -0
  36. package/src/server/__tests__/providers.test.ts +579 -0
  37. package/src/server/__tests__/proxy-streaming.test.ts +317 -0
  38. package/src/server/__tests__/proxy-transform.test.ts +469 -0
  39. package/src/server/__tests__/real-llm-test.ts +526 -0
  40. package/src/server/__tests__/scheduled-tasks.test.ts +371 -0
  41. package/src/server/__tests__/sessions.test.ts +786 -0
  42. package/src/server/__tests__/settings.test.ts +376 -0
  43. package/src/server/__tests__/skills.test.ts +125 -0
  44. package/src/server/__tests__/tasks.test.ts +171 -0
  45. package/src/server/__tests__/team-watcher.test.ts +400 -0
  46. package/src/server/__tests__/teams.test.ts +627 -0
  47. package/src/server/ensureSingletonLocalServer.ts +1 -1
  48. package/src/server/middleware/cors.test.ts +27 -0
  49. package/src/utils/__tests__/cronFrequency.test.ts +153 -0
  50. package/src/utils/__tests__/cronTasks.test.ts +204 -0
  51. package/src/utils/computerUse/permissions.test.ts +44 -0
  52. package/src/utils/config.ts +15 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * Unit tests for ProviderService and Providers REST API
3
+ */
4
+
5
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
6
+ import * as fs from 'fs/promises'
7
+ import * as path from 'path'
8
+ import * as os from 'os'
9
+ import { ProviderService } from '../services/providerService.ts'
10
+ import { handleProvidersApi } from '../api/providers.js'
11
+ import type { CreateProviderInput } from '../types/provider.ts'
12
+
13
+ // ─── Test helpers ─────────────────────────────────────────────────────────────
14
+
15
+ let tmpDir: string
16
+ let originalConfigDir: string | undefined
17
+
18
+ async function setup() {
19
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'provider-test-'))
20
+ originalConfigDir = process.env.CLAUDE_CONFIG_DIR
21
+ process.env.CLAUDE_CONFIG_DIR = tmpDir
22
+ }
23
+
24
+ async function teardown() {
25
+ if (originalConfigDir !== undefined) {
26
+ process.env.CLAUDE_CONFIG_DIR = originalConfigDir
27
+ } else {
28
+ delete process.env.CLAUDE_CONFIG_DIR
29
+ }
30
+ await fs.rm(tmpDir, { recursive: true, force: true })
31
+ }
32
+
33
+ /** Create a mock Request */
34
+ function makeRequest(
35
+ method: string,
36
+ urlStr: string,
37
+ body?: Record<string, unknown>,
38
+ ): { req: Request; url: URL; segments: string[] } {
39
+ const url = new URL(urlStr, 'http://localhost:3456')
40
+ const init: RequestInit = { method }
41
+ if (body) {
42
+ init.headers = { 'Content-Type': 'application/json' }
43
+ init.body = JSON.stringify(body)
44
+ }
45
+ const req = new Request(url.toString(), init)
46
+ const segments = url.pathname.split('/').filter(Boolean)
47
+ return { req, url, segments }
48
+ }
49
+
50
+ /** A sample provider input for reuse across tests */
51
+ function sampleInput(overrides?: Partial<CreateProviderInput>): CreateProviderInput {
52
+ return {
53
+ name: 'Test Provider',
54
+ baseUrl: 'https://api.example.com',
55
+ apiKey: 'sk-test-key-123',
56
+ models: [
57
+ { id: 'model-a', name: 'Model A' },
58
+ { id: 'model-b', name: 'Model B' },
59
+ ],
60
+ ...overrides,
61
+ }
62
+ }
63
+
64
+ /** Read the settings.json written to the temp config dir */
65
+ async function readSettings(): Promise<Record<string, unknown>> {
66
+ const raw = await fs.readFile(path.join(tmpDir, 'settings.json'), 'utf-8')
67
+ return JSON.parse(raw) as Record<string, unknown>
68
+ }
69
+
70
+ /** Read the providers.json written to the temp config dir */
71
+ async function readProvidersConfig(): Promise<Record<string, unknown>> {
72
+ const raw = await fs.readFile(path.join(tmpDir, 'providers.json'), 'utf-8')
73
+ return JSON.parse(raw) as Record<string, unknown>
74
+ }
75
+
76
+ // =============================================================================
77
+ // ProviderService
78
+ // =============================================================================
79
+
80
+ describe('ProviderService', () => {
81
+ beforeEach(setup)
82
+ afterEach(teardown)
83
+
84
+ // ─── listProviders ───────────────────────────────────────────────────────
85
+
86
+ describe('listProviders', () => {
87
+ test('should return empty array when no providers exist', async () => {
88
+ const svc = new ProviderService()
89
+ const providers = await svc.listProviders()
90
+ expect(providers).toEqual([])
91
+ })
92
+
93
+ test('should return all added providers', async () => {
94
+ const svc = new ProviderService()
95
+ await svc.addProvider(sampleInput({ name: 'Provider A' }))
96
+ await svc.addProvider(sampleInput({ name: 'Provider B' }))
97
+
98
+ const providers = await svc.listProviders()
99
+ expect(providers).toHaveLength(2)
100
+ expect(providers[0].name).toBe('Provider A')
101
+ expect(providers[1].name).toBe('Provider B')
102
+ })
103
+ })
104
+
105
+ // ─── addProvider ─────────────────────────────────────────────────────────
106
+
107
+ describe('addProvider', () => {
108
+ test('should add a provider and return it with generated fields', async () => {
109
+ const svc = new ProviderService()
110
+ const provider = await svc.addProvider(sampleInput())
111
+
112
+ expect(provider.id).toBeDefined()
113
+ expect(provider.name).toBe('Test Provider')
114
+ expect(provider.baseUrl).toBe('https://api.example.com')
115
+ expect(provider.apiKey).toBe('sk-test-key-123')
116
+ expect(provider.models).toHaveLength(2)
117
+ expect(provider.createdAt).toBeGreaterThan(0)
118
+ expect(provider.updatedAt).toBeGreaterThan(0)
119
+ })
120
+
121
+ test('first provider should be auto-activated', async () => {
122
+ const svc = new ProviderService()
123
+ const provider = await svc.addProvider(sampleInput())
124
+
125
+ expect(provider.isActive).toBe(true)
126
+ })
127
+
128
+ test('first provider auto-activation should sync to settings.json', async () => {
129
+ const svc = new ProviderService()
130
+ await svc.addProvider(sampleInput())
131
+
132
+ const settings = await readSettings()
133
+ const env = settings.env as Record<string, string>
134
+ expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
135
+ expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-test-key-123')
136
+ expect(settings.model).toBe('model-a')
137
+ })
138
+
139
+ test('second provider should not be auto-activated', async () => {
140
+ const svc = new ProviderService()
141
+ await svc.addProvider(sampleInput({ name: 'First' }))
142
+ const second = await svc.addProvider(sampleInput({ name: 'Second' }))
143
+
144
+ expect(second.isActive).toBe(false)
145
+ })
146
+
147
+ test('should preserve optional notes field', async () => {
148
+ const svc = new ProviderService()
149
+ const provider = await svc.addProvider(sampleInput({ notes: 'dev environment' }))
150
+
151
+ expect(provider.notes).toBe('dev environment')
152
+ })
153
+ })
154
+
155
+ // ─── getProvider ─────────────────────────────────────────────────────────
156
+
157
+ describe('getProvider', () => {
158
+ test('should return the provider by id', async () => {
159
+ const svc = new ProviderService()
160
+ const added = await svc.addProvider(sampleInput())
161
+
162
+ const fetched = await svc.getProvider(added.id)
163
+ expect(fetched.id).toBe(added.id)
164
+ expect(fetched.name).toBe(added.name)
165
+ })
166
+
167
+ test('should throw 404 for non-existent id', async () => {
168
+ const svc = new ProviderService()
169
+
170
+ try {
171
+ await svc.getProvider('non-existent-id')
172
+ expect(true).toBe(false) // should not reach here
173
+ } catch (err: unknown) {
174
+ const apiErr = err as { statusCode: number }
175
+ expect(apiErr.statusCode).toBe(404)
176
+ }
177
+ })
178
+ })
179
+
180
+ // ─── updateProvider ──────────────────────────────────────────────────────
181
+
182
+ describe('updateProvider', () => {
183
+ test('should update provider fields', async () => {
184
+ const svc = new ProviderService()
185
+ const added = await svc.addProvider(sampleInput())
186
+
187
+ const updated = await svc.updateProvider(added.id, {
188
+ name: 'Updated Name',
189
+ baseUrl: 'https://new-api.example.com',
190
+ })
191
+
192
+ expect(updated.name).toBe('Updated Name')
193
+ expect(updated.baseUrl).toBe('https://new-api.example.com')
194
+ // unchanged fields preserved
195
+ expect(updated.apiKey).toBe('sk-test-key-123')
196
+ expect(updated.updatedAt).toBeGreaterThanOrEqual(added.updatedAt)
197
+ })
198
+
199
+ test('should throw 404 for non-existent provider', async () => {
200
+ const svc = new ProviderService()
201
+
202
+ try {
203
+ await svc.updateProvider('non-existent-id', { name: 'X' })
204
+ expect(true).toBe(false)
205
+ } catch (err: unknown) {
206
+ const apiErr = err as { statusCode: number }
207
+ expect(apiErr.statusCode).toBe(404)
208
+ }
209
+ })
210
+
211
+ test('updating active provider should re-sync settings.json', async () => {
212
+ const svc = new ProviderService()
213
+ const added = await svc.addProvider(sampleInput())
214
+
215
+ // First provider is auto-activated, so updating it should re-sync
216
+ await svc.updateProvider(added.id, {
217
+ baseUrl: 'https://new-api.example.com',
218
+ apiKey: 'sk-new-key',
219
+ })
220
+
221
+ const settings = await readSettings()
222
+ const env = settings.env as Record<string, string>
223
+ expect(env.ANTHROPIC_BASE_URL).toBe('https://new-api.example.com')
224
+ expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-new-key')
225
+ })
226
+ })
227
+
228
+ // ─── deleteProvider ──────────────────────────────────────────────────────
229
+
230
+ describe('deleteProvider', () => {
231
+ test('should delete an inactive provider', async () => {
232
+ const svc = new ProviderService()
233
+ await svc.addProvider(sampleInput({ name: 'First' }))
234
+ const second = await svc.addProvider(sampleInput({ name: 'Second' }))
235
+
236
+ // Second is inactive, so deletion should succeed
237
+ await svc.deleteProvider(second.id)
238
+
239
+ const providers = await svc.listProviders()
240
+ expect(providers).toHaveLength(1)
241
+ expect(providers[0].name).toBe('First')
242
+ })
243
+
244
+ test('should throw 409 when deleting an active provider', async () => {
245
+ const svc = new ProviderService()
246
+ const active = await svc.addProvider(sampleInput())
247
+
248
+ try {
249
+ await svc.deleteProvider(active.id)
250
+ expect(true).toBe(false)
251
+ } catch (err: unknown) {
252
+ const apiErr = err as { statusCode: number }
253
+ expect(apiErr.statusCode).toBe(409)
254
+ }
255
+ })
256
+
257
+ test('should throw 404 when deleting non-existent provider', async () => {
258
+ const svc = new ProviderService()
259
+
260
+ try {
261
+ await svc.deleteProvider('non-existent-id')
262
+ expect(true).toBe(false)
263
+ } catch (err: unknown) {
264
+ const apiErr = err as { statusCode: number }
265
+ expect(apiErr.statusCode).toBe(404)
266
+ }
267
+ })
268
+ })
269
+
270
+ // ─── activateProvider ────────────────────────────────────────────────────
271
+
272
+ describe('activateProvider', () => {
273
+ test('should activate a provider with a valid model', async () => {
274
+ const svc = new ProviderService()
275
+ const first = await svc.addProvider(sampleInput({ name: 'First' }))
276
+ const second = await svc.addProvider(
277
+ sampleInput({
278
+ name: 'Second',
279
+ baseUrl: 'https://second-api.example.com',
280
+ apiKey: 'sk-second-key',
281
+ }),
282
+ )
283
+
284
+ await svc.activateProvider(second.id, 'model-a')
285
+
286
+ // Second should now be active
287
+ const providers = await svc.listProviders()
288
+ const activeFirst = providers.find((p) => p.id === first.id)
289
+ const activeSecond = providers.find((p) => p.id === second.id)
290
+ expect(activeFirst!.isActive).toBe(false)
291
+ expect(activeSecond!.isActive).toBe(true)
292
+ })
293
+
294
+ test('should write correct settings.json on activation', async () => {
295
+ const svc = new ProviderService()
296
+ await svc.addProvider(sampleInput({ name: 'First' }))
297
+ const second = await svc.addProvider(
298
+ sampleInput({
299
+ name: 'Second',
300
+ baseUrl: 'https://second-api.example.com',
301
+ apiKey: 'sk-second-key',
302
+ }),
303
+ )
304
+
305
+ await svc.activateProvider(second.id, 'model-b')
306
+
307
+ const settings = await readSettings()
308
+ const env = settings.env as Record<string, string>
309
+ expect(env.ANTHROPIC_BASE_URL).toBe('https://second-api.example.com')
310
+ expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-second-key')
311
+ expect(settings.model).toBe('model-b')
312
+ })
313
+
314
+ test('should preserve existing settings.json fields on activation', async () => {
315
+ // Pre-seed settings with an extra field
316
+ await fs.writeFile(
317
+ path.join(tmpDir, 'settings.json'),
318
+ JSON.stringify({ theme: 'dark', env: { CUSTOM_VAR: 'keep-me' } }),
319
+ )
320
+
321
+ const svc = new ProviderService()
322
+ const provider = await svc.addProvider(sampleInput())
323
+
324
+ // Re-activate to verify merge behavior
325
+ await svc.activateProvider(provider.id, 'model-a')
326
+
327
+ const settings = await readSettings()
328
+ expect(settings.theme).toBe('dark')
329
+ const env = settings.env as Record<string, string>
330
+ expect(env.CUSTOM_VAR).toBe('keep-me')
331
+ expect(env.ANTHROPIC_BASE_URL).toBe('https://api.example.com')
332
+ })
333
+
334
+ test('should throw 400 for non-existent model id', async () => {
335
+ const svc = new ProviderService()
336
+ const provider = await svc.addProvider(sampleInput())
337
+
338
+ try {
339
+ await svc.activateProvider(provider.id, 'non-existent-model')
340
+ expect(true).toBe(false)
341
+ } catch (err: unknown) {
342
+ const apiErr = err as { statusCode: number }
343
+ expect(apiErr.statusCode).toBe(400)
344
+ }
345
+ })
346
+
347
+ test('should throw 404 for non-existent provider id', async () => {
348
+ const svc = new ProviderService()
349
+
350
+ try {
351
+ await svc.activateProvider('non-existent-id', 'model-a')
352
+ expect(true).toBe(false)
353
+ } catch (err: unknown) {
354
+ const apiErr = err as { statusCode: number }
355
+ expect(apiErr.statusCode).toBe(404)
356
+ }
357
+ })
358
+
359
+ test('activeModel should be persisted in providers.json', async () => {
360
+ const svc = new ProviderService()
361
+ const provider = await svc.addProvider(sampleInput())
362
+
363
+ await svc.activateProvider(provider.id, 'model-b')
364
+
365
+ const config = await readProvidersConfig()
366
+ expect(config.activeModel).toBe('model-b')
367
+ })
368
+ })
369
+
370
+ // ─── getActiveProvider ───────────────────────────────────────────────────
371
+
372
+ describe('getActiveProvider', () => {
373
+ test('should return null when no providers exist', async () => {
374
+ const svc = new ProviderService()
375
+ const active = await svc.getActiveProvider()
376
+ expect(active).toBeNull()
377
+ })
378
+
379
+ test('should return the active provider', async () => {
380
+ const svc = new ProviderService()
381
+ const provider = await svc.addProvider(sampleInput())
382
+
383
+ const active = await svc.getActiveProvider()
384
+ expect(active).not.toBeNull()
385
+ expect(active!.id).toBe(provider.id)
386
+ })
387
+ })
388
+ })
389
+
390
+ // =============================================================================
391
+ // Providers REST API
392
+ // =============================================================================
393
+
394
+ describe('Providers API', () => {
395
+ beforeEach(setup)
396
+ afterEach(teardown)
397
+
398
+ // ─── GET /api/providers ──────────────────────────────────────────────────
399
+
400
+ test('GET /api/providers should return empty list initially', async () => {
401
+ const { req, url, segments } = makeRequest('GET', '/api/providers')
402
+ const res = await handleProvidersApi(req, url, segments)
403
+
404
+ expect(res.status).toBe(200)
405
+ const body = (await res.json()) as { providers: unknown[] }
406
+ expect(body.providers).toEqual([])
407
+ })
408
+
409
+ test('GET /api/providers should list added providers', async () => {
410
+ // Seed a provider via service
411
+ const svc = new ProviderService()
412
+ await svc.addProvider(sampleInput())
413
+
414
+ const { req, url, segments } = makeRequest('GET', '/api/providers')
415
+ const res = await handleProvidersApi(req, url, segments)
416
+
417
+ expect(res.status).toBe(200)
418
+ const body = (await res.json()) as { providers: { name: string }[] }
419
+ expect(body.providers).toHaveLength(1)
420
+ expect(body.providers[0].name).toBe('Test Provider')
421
+ })
422
+
423
+ // ─── POST /api/providers ─────────────────────────────────────────────────
424
+
425
+ test('POST /api/providers should create a provider', async () => {
426
+ const { req, url, segments } = makeRequest('POST', '/api/providers', {
427
+ name: 'New Provider',
428
+ baseUrl: 'https://api.example.com',
429
+ apiKey: 'sk-test',
430
+ models: [{ id: 'gpt-4', name: 'GPT-4' }],
431
+ })
432
+ const res = await handleProvidersApi(req, url, segments)
433
+
434
+ expect(res.status).toBe(201)
435
+ const body = (await res.json()) as { provider: { name: string; isActive: boolean } }
436
+ expect(body.provider.name).toBe('New Provider')
437
+ expect(body.provider.isActive).toBe(true) // first provider auto-activated
438
+ })
439
+
440
+ test('POST /api/providers should return 400 for invalid input', async () => {
441
+ const { req, url, segments } = makeRequest('POST', '/api/providers', {
442
+ name: '', // invalid: empty name
443
+ })
444
+ const res = await handleProvidersApi(req, url, segments)
445
+
446
+ expect(res.status).toBe(400)
447
+ })
448
+
449
+ // ─── GET /api/providers/:id ──────────────────────────────────────────────
450
+
451
+ test('GET /api/providers/:id should return a provider', async () => {
452
+ const svc = new ProviderService()
453
+ const added = await svc.addProvider(sampleInput())
454
+
455
+ const { req, url, segments } = makeRequest('GET', `/api/providers/${added.id}`)
456
+ const res = await handleProvidersApi(req, url, segments)
457
+
458
+ expect(res.status).toBe(200)
459
+ const body = (await res.json()) as { provider: { id: string; name: string } }
460
+ expect(body.provider.id).toBe(added.id)
461
+ })
462
+
463
+ test('GET /api/providers/:id should return 404 for unknown id', async () => {
464
+ const { req, url, segments } = makeRequest('GET', '/api/providers/unknown-id')
465
+ const res = await handleProvidersApi(req, url, segments)
466
+
467
+ expect(res.status).toBe(404)
468
+ })
469
+
470
+ // ─── PUT /api/providers/:id ──────────────────────────────────────────────
471
+
472
+ test('PUT /api/providers/:id should update a provider', async () => {
473
+ const svc = new ProviderService()
474
+ const added = await svc.addProvider(sampleInput())
475
+
476
+ const { req, url, segments } = makeRequest('PUT', `/api/providers/${added.id}`, {
477
+ name: 'Renamed Provider',
478
+ })
479
+ const res = await handleProvidersApi(req, url, segments)
480
+
481
+ expect(res.status).toBe(200)
482
+ const body = (await res.json()) as { provider: { name: string } }
483
+ expect(body.provider.name).toBe('Renamed Provider')
484
+ })
485
+
486
+ // ─── DELETE /api/providers/:id ───────────────────────────────────────────
487
+
488
+ test('DELETE /api/providers/:id should delete an inactive provider', async () => {
489
+ const svc = new ProviderService()
490
+ await svc.addProvider(sampleInput({ name: 'First' }))
491
+ const second = await svc.addProvider(sampleInput({ name: 'Second' }))
492
+
493
+ const { req, url, segments } = makeRequest('DELETE', `/api/providers/${second.id}`)
494
+ const res = await handleProvidersApi(req, url, segments)
495
+
496
+ expect(res.status).toBe(200)
497
+ const body = (await res.json()) as { ok: boolean }
498
+ expect(body.ok).toBe(true)
499
+ })
500
+
501
+ test('DELETE /api/providers/:id should return 409 for active provider', async () => {
502
+ const svc = new ProviderService()
503
+ const active = await svc.addProvider(sampleInput())
504
+
505
+ const { req, url, segments } = makeRequest('DELETE', `/api/providers/${active.id}`)
506
+ const res = await handleProvidersApi(req, url, segments)
507
+
508
+ expect(res.status).toBe(409)
509
+ })
510
+
511
+ // ─── POST /api/providers/:id/activate ────────────────────────────────────
512
+
513
+ test('POST /api/providers/:id/activate should activate a provider', async () => {
514
+ const svc = new ProviderService()
515
+ await svc.addProvider(sampleInput({ name: 'First' }))
516
+ const second = await svc.addProvider(
517
+ sampleInput({
518
+ name: 'Second',
519
+ baseUrl: 'https://second.example.com',
520
+ apiKey: 'sk-second',
521
+ }),
522
+ )
523
+
524
+ const { req, url, segments } = makeRequest(
525
+ 'POST',
526
+ `/api/providers/${second.id}/activate`,
527
+ { modelId: 'model-a' },
528
+ )
529
+ const res = await handleProvidersApi(req, url, segments)
530
+
531
+ expect(res.status).toBe(200)
532
+ const body = (await res.json()) as { ok: boolean }
533
+ expect(body.ok).toBe(true)
534
+
535
+ // Verify settings were synced
536
+ const settings = await readSettings()
537
+ const env = settings.env as Record<string, string>
538
+ expect(env.ANTHROPIC_BASE_URL).toBe('https://second.example.com')
539
+ expect(env.ANTHROPIC_AUTH_TOKEN).toBe('sk-second')
540
+ expect(settings.model).toBe('model-a')
541
+ })
542
+
543
+ test('POST /api/providers/:id/activate should return 400 for missing modelId', async () => {
544
+ const svc = new ProviderService()
545
+ const provider = await svc.addProvider(sampleInput())
546
+
547
+ const { req, url, segments } = makeRequest(
548
+ 'POST',
549
+ `/api/providers/${provider.id}/activate`,
550
+ {},
551
+ )
552
+ const res = await handleProvidersApi(req, url, segments)
553
+
554
+ expect(res.status).toBe(400)
555
+ })
556
+
557
+ test('POST /api/providers/:id/activate should return 400 for invalid model', async () => {
558
+ const svc = new ProviderService()
559
+ const provider = await svc.addProvider(sampleInput())
560
+
561
+ const { req, url, segments } = makeRequest(
562
+ 'POST',
563
+ `/api/providers/${provider.id}/activate`,
564
+ { modelId: 'non-existent-model' },
565
+ )
566
+ const res = await handleProvidersApi(req, url, segments)
567
+
568
+ expect(res.status).toBe(400)
569
+ })
570
+
571
+ // ─── Method not allowed ──────────────────────────────────────────────────
572
+
573
+ test('should return 405 for unsupported methods', async () => {
574
+ const { req, url, segments } = makeRequest('PATCH', '/api/providers')
575
+ const res = await handleProvidersApi(req, url, segments)
576
+
577
+ expect(res.status).toBe(405)
578
+ })
579
+ })