bingocode 1.0.27 → 1.0.28
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/package.json +1 -2
- package/.github/FUNDING.yml +0 -1
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -44
- package/.github/ISSUE_TEMPLATE/config.yml +0 -1
- package/.github/ISSUE_TEMPLATE/question.md +0 -40
- package/.github/workflows/build-desktop-dev.yml +0 -210
- package/.github/workflows/deploy-docs.yml +0 -59
- package/.github/workflows/release-desktop.yml +0 -162
- package/.spine/user.yaml +0 -5
- package/.spine/workspace.yaml +0 -1
- package/adapters/common/__tests__/chat-queue.test.ts +0 -61
- package/adapters/common/__tests__/format.test.ts +0 -148
- package/adapters/common/__tests__/http-client.test.ts +0 -105
- package/adapters/common/__tests__/message-buffer.test.ts +0 -84
- package/adapters/common/__tests__/message-dedup.test.ts +0 -57
- package/adapters/common/__tests__/session-store.test.ts +0 -62
- package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
- package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
- package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
- package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
- package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
- package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
- package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
- package/adapters/feishu/__tests__/feishu.test.ts +0 -907
- package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
- package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
- package/adapters/feishu/__tests__/media.test.ts +0 -120
- package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
- package/adapters/telegram/__tests__/media.test.ts +0 -86
- package/adapters/telegram/__tests__/telegram.test.ts +0 -115
- package/adapters/tsconfig.json +0 -18
- package/bunfig.toml +0 -1
- package/preload.ts +0 -30
- package/scripts/count-app-loc.ts +0 -256
- package/scripts/release.ts +0 -130
- package/src/server/__tests__/conversation-service.test.ts +0 -173
- package/src/server/__tests__/conversations.test.ts +0 -458
- package/src/server/__tests__/cron-scheduler.test.ts +0 -575
- package/src/server/__tests__/e2e/business-flow.test.ts +0 -841
- package/src/server/__tests__/e2e/full-flow.test.ts +0 -357
- package/src/server/__tests__/fixtures/mock-sdk-cli.ts +0 -123
- package/src/server/__tests__/haha-oauth-api.test.ts +0 -146
- package/src/server/__tests__/haha-oauth-service.test.ts +0 -185
- package/src/server/__tests__/providers-real.test.ts +0 -244
- package/src/server/__tests__/providers.test.ts +0 -579
- package/src/server/__tests__/proxy-streaming.test.ts +0 -317
- package/src/server/__tests__/proxy-transform.test.ts +0 -469
- package/src/server/__tests__/real-llm-test.ts +0 -526
- package/src/server/__tests__/scheduled-tasks.test.ts +0 -371
- package/src/server/__tests__/sessions.test.ts +0 -786
- package/src/server/__tests__/settings.test.ts +0 -376
- package/src/server/__tests__/skills.test.ts +0 -125
- package/src/server/__tests__/tasks.test.ts +0 -171
- package/src/server/__tests__/team-watcher.test.ts +0 -400
- package/src/server/__tests__/teams.test.ts +0 -627
- package/src/server/middleware/cors.test.ts +0 -27
- package/src/utils/__tests__/cronFrequency.test.ts +0 -153
- package/src/utils/__tests__/cronTasks.test.ts +0 -204
- package/src/utils/computerUse/permissions.test.ts +0 -44
- package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
- package/stubs/color-diff-napi.ts +0 -45
- package/tsconfig.json +0 -24
|
@@ -1,579 +0,0 @@
|
|
|
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
|
-
})
|