@polderlabs/bizar 4.4.13 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +96 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/lightrag/SKILL.md +75 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +55 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +12 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
- package/bizar-dash/src/server/memory-store.mjs +38 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +241 -1
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +4 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +118 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +0 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Settings.tsx +6 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +82 -1
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -147,11 +147,11 @@ describe('providers-store — backup keys (v3.20.10)', () => {
|
|
|
147
147
|
provider: {
|
|
148
148
|
minimax: {
|
|
149
149
|
apiKey: '',
|
|
150
|
-
backupApiKey: '
|
|
150
|
+
backupApiKey: 'sk-cp-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
151
151
|
},
|
|
152
152
|
},
|
|
153
153
|
});
|
|
154
|
-
process.env.MINIMAX_API_KEY = '
|
|
154
|
+
process.env.MINIMAX_API_KEY = 'sk-cp-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
|
|
155
155
|
|
|
156
156
|
const store = await loadStore();
|
|
157
157
|
const results = await store.autoDetect({ probe: false });
|
|
@@ -198,7 +198,7 @@ describe('providers-store — backup keys (v3.20.10)', () => {
|
|
|
198
198
|
|
|
199
199
|
it('allows backup without primary (rotation in progress)', async () => {
|
|
200
200
|
// No primary key — only backup env var.
|
|
201
|
-
process.env.MINIMAX_API_KEY_BACKUP = '
|
|
201
|
+
process.env.MINIMAX_API_KEY_BACKUP = 'sk-cp-iiiiiiiiiiiiiiiiiiiiiiiiiiiiiii';
|
|
202
202
|
|
|
203
203
|
const store = await loadStore();
|
|
204
204
|
const results = await store.autoDetect({ probe: false });
|
|
@@ -330,4 +330,480 @@ describe('providers-store — backup keys (v3.20.10)', () => {
|
|
|
330
330
|
assert.match(minimax.backupApiKey, /^mm\.\.\.mm$/);
|
|
331
331
|
});
|
|
332
332
|
});
|
|
333
|
+
|
|
334
|
+
// ── v4.6.0 keys[] array rotation / cooldown tests ────────────────────
|
|
335
|
+
//
|
|
336
|
+
// These cover the new top-level exports from providers-store.mjs:
|
|
337
|
+
// - getActiveKey(providerId)
|
|
338
|
+
// - markKeyError(providerId, envVar, error)
|
|
339
|
+
// - markKeySuccess(providerId, envVar)
|
|
340
|
+
// - rotateKey(providerId)
|
|
341
|
+
// - addBackupKey(providerId, envVar, label)
|
|
342
|
+
// - removeBackupKey(providerId, envVar)
|
|
343
|
+
// - setKeyStatus(providerId, envVar, status)
|
|
344
|
+
// - withKeyRotation(providerId, fn, opts)
|
|
345
|
+
// - isRetryableError(err)
|
|
346
|
+
//
|
|
347
|
+
// All of these work against the v4.6.0 keys[] array shape and the
|
|
348
|
+
// legacy apiKey/backupApiKey fields are kept in sync via
|
|
349
|
+
// syncLegacyKeys() on writes.
|
|
350
|
+
|
|
351
|
+
describe('v4.6.0 — keys[] array migration from legacy', () => {
|
|
352
|
+
beforeEach(async () => {
|
|
353
|
+
delete process.env.MINIMAX_API_KEY;
|
|
354
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
355
|
+
await resetOpencodeJson();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('legacy apiKey is exposed as a keys[] entry on list()', async () => {
|
|
359
|
+
const store = await loadStore();
|
|
360
|
+
await store.add({
|
|
361
|
+
id: 'minimax',
|
|
362
|
+
name: 'MiniMax',
|
|
363
|
+
apiKey: 'sk-cp-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
364
|
+
});
|
|
365
|
+
const list = store.list();
|
|
366
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
367
|
+
assert.ok(Array.isArray(minimax.keys));
|
|
368
|
+
assert.equal(minimax.keys.length, 1);
|
|
369
|
+
assert.equal(minimax.keys[0].label, 'Primary');
|
|
370
|
+
assert.equal(minimax.keys[0].status, 'active');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
it('legacy apiKey + backupApiKey becomes a 2-entry keys[]', async () => {
|
|
374
|
+
const store = await loadStore();
|
|
375
|
+
await store.add({
|
|
376
|
+
id: 'minimax',
|
|
377
|
+
apiKey: 'sk-cp-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
378
|
+
backupApiKey: 'sk-cp-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',
|
|
379
|
+
});
|
|
380
|
+
const list = store.list();
|
|
381
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
382
|
+
assert.equal(minimax.keys.length, 2);
|
|
383
|
+
assert.equal(minimax.keys[0].label, 'Primary');
|
|
384
|
+
assert.equal(minimax.keys[0].status, 'active');
|
|
385
|
+
assert.equal(minimax.keys[1].label, 'Backup');
|
|
386
|
+
assert.equal(minimax.keys[1].status, 'standby');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('caller-provided keys[] is preserved verbatim', async () => {
|
|
390
|
+
const store = await loadStore();
|
|
391
|
+
await store.add({
|
|
392
|
+
id: 'minimax',
|
|
393
|
+
name: 'MiniMax',
|
|
394
|
+
keys: [
|
|
395
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Work laptop', status: 'active' },
|
|
396
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Home', status: 'standby' },
|
|
397
|
+
],
|
|
398
|
+
});
|
|
399
|
+
const list = store.list();
|
|
400
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
401
|
+
assert.equal(minimax.keys.length, 2);
|
|
402
|
+
assert.equal(minimax.keys[0].envVar, 'BIZAR_MINIMAX_KEY');
|
|
403
|
+
assert.equal(minimax.keys[0].status, 'active');
|
|
404
|
+
assert.equal(minimax.keys[1].envVar, 'BIZAR_MINIMAX_BACKUP');
|
|
405
|
+
assert.equal(minimax.keys[1].status, 'standby');
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe('v4.6.0 — getActiveKey', () => {
|
|
410
|
+
beforeEach(async () => {
|
|
411
|
+
delete process.env.MINIMAX_API_KEY;
|
|
412
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
413
|
+
await resetOpencodeJson();
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it('returns null when the provider does not exist', async () => {
|
|
417
|
+
const store = await loadStore();
|
|
418
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=getActive-1');
|
|
419
|
+
assert.equal(mod.getActiveKey('nonexistent'), null);
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('returns null when the provider has no keys', async () => {
|
|
423
|
+
const store = await loadStore();
|
|
424
|
+
await store.add({ id: 'minimax', name: 'MiniMax' });
|
|
425
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=getActive-2');
|
|
426
|
+
assert.equal(mod.getActiveKey('minimax'), null);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('returns the active key with the env value resolved', async () => {
|
|
430
|
+
const store = await loadStore();
|
|
431
|
+
await store.add({
|
|
432
|
+
id: 'minimax',
|
|
433
|
+
name: 'MiniMax',
|
|
434
|
+
keys: [
|
|
435
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
436
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
437
|
+
],
|
|
438
|
+
});
|
|
439
|
+
process.env.BIZAR_MINIMAX_KEY = 'sk-cp-active-value';
|
|
440
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup-value';
|
|
441
|
+
|
|
442
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=getActive-3');
|
|
443
|
+
const active = mod.getActiveKey('minimax');
|
|
444
|
+
assert.ok(active);
|
|
445
|
+
assert.equal(active.envVar, 'BIZAR_MINIMAX_KEY');
|
|
446
|
+
assert.equal(active.label, 'Primary');
|
|
447
|
+
assert.equal(active.status, 'active');
|
|
448
|
+
assert.equal(active.key, 'sk-cp-active-value');
|
|
449
|
+
delete process.env.BIZAR_MINIMAX_KEY;
|
|
450
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
it('falls back to standby when active is missing', async () => {
|
|
454
|
+
const store = await loadStore();
|
|
455
|
+
await store.add({
|
|
456
|
+
id: 'minimax',
|
|
457
|
+
name: 'MiniMax',
|
|
458
|
+
keys: [
|
|
459
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
460
|
+
],
|
|
461
|
+
});
|
|
462
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup-only';
|
|
463
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=getActive-4');
|
|
464
|
+
const active = mod.getActiveKey('minimax');
|
|
465
|
+
assert.equal(active.envVar, 'BIZAR_MINIMAX_BACKUP');
|
|
466
|
+
assert.equal(active.status, 'standby');
|
|
467
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
describe('v4.6.0 — markKeyError', () => {
|
|
472
|
+
beforeEach(async () => {
|
|
473
|
+
delete process.env.MINIMAX_API_KEY;
|
|
474
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
475
|
+
await resetOpencodeJson();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('records errorCount + lastError on the named envVar', async () => {
|
|
479
|
+
const store = await loadStore();
|
|
480
|
+
await store.add({
|
|
481
|
+
id: 'minimax',
|
|
482
|
+
name: 'MiniMax',
|
|
483
|
+
keys: [
|
|
484
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
485
|
+
],
|
|
486
|
+
});
|
|
487
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=markErr-1');
|
|
488
|
+
mod.markKeyError('minimax', 'BIZAR_MINIMAX_KEY', new Error('http_429 too many requests'));
|
|
489
|
+
const list = store.list();
|
|
490
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
491
|
+
assert.equal(minimax.keys[0].errorCount, 1);
|
|
492
|
+
assert.match(minimax.keys[0].lastError, /http_429/);
|
|
493
|
+
assert.equal(minimax.keys[0].status, 'active'); // below threshold
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('demotes to cooldown after ERROR_COOLDOWN_THRESHOLD errors', async () => {
|
|
497
|
+
const store = await loadStore();
|
|
498
|
+
await store.add({
|
|
499
|
+
id: 'minimax',
|
|
500
|
+
name: 'MiniMax',
|
|
501
|
+
keys: [
|
|
502
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
503
|
+
],
|
|
504
|
+
});
|
|
505
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=markErr-2');
|
|
506
|
+
// Threshold is 3
|
|
507
|
+
mod.markKeyError('minimax', 'BIZAR_MINIMAX_KEY', 'err1');
|
|
508
|
+
mod.markKeyError('minimax', 'BIZAR_MINIMAX_KEY', 'err2');
|
|
509
|
+
const afterTwo = store.list().find((p) => p.id === 'minimax').keys[0];
|
|
510
|
+
assert.equal(afterTwo.errorCount, 2);
|
|
511
|
+
assert.equal(afterTwo.status, 'active'); // still below
|
|
512
|
+
mod.markKeyError('minimax', 'BIZAR_MINIMAX_KEY', 'err3');
|
|
513
|
+
const afterThree = store.list().find((p) => p.id === 'minimax').keys[0];
|
|
514
|
+
assert.equal(afterThree.errorCount, 3);
|
|
515
|
+
assert.equal(afterThree.status, 'cooldown');
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('is a no-op for unknown envVars', async () => {
|
|
519
|
+
const store = await loadStore();
|
|
520
|
+
await store.add({
|
|
521
|
+
id: 'minimax',
|
|
522
|
+
name: 'MiniMax',
|
|
523
|
+
keys: [{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' }],
|
|
524
|
+
});
|
|
525
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=markErr-3');
|
|
526
|
+
const result = mod.markKeyError('minimax', 'NONEXISTENT_ENV', 'err');
|
|
527
|
+
assert.equal(result, null);
|
|
528
|
+
const list = store.list();
|
|
529
|
+
assert.equal(list.find((p) => p.id === 'minimax').keys[0].errorCount, 0);
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
describe('v4.6.0 — rotateKey', () => {
|
|
534
|
+
beforeEach(async () => {
|
|
535
|
+
delete process.env.MINIMAX_API_KEY;
|
|
536
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
537
|
+
await resetOpencodeJson();
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('promotes the next standby key', async () => {
|
|
541
|
+
const store = await loadStore();
|
|
542
|
+
await store.add({
|
|
543
|
+
id: 'minimax',
|
|
544
|
+
name: 'MiniMax',
|
|
545
|
+
keys: [
|
|
546
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
547
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
548
|
+
],
|
|
549
|
+
});
|
|
550
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rotate-1');
|
|
551
|
+
const rotated = mod.rotateKey('minimax');
|
|
552
|
+
assert.ok(rotated);
|
|
553
|
+
assert.equal(rotated.envVar, 'BIZAR_MINIMAX_BACKUP');
|
|
554
|
+
const list = store.list();
|
|
555
|
+
const minimax = list.find((p) => p.id === 'minimax');
|
|
556
|
+
assert.equal(minimax.keys[0].status, 'disabled');
|
|
557
|
+
assert.equal(minimax.keys[1].status, 'active');
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
it('returns null when no rotation target exists', async () => {
|
|
561
|
+
const store = await loadStore();
|
|
562
|
+
await store.add({
|
|
563
|
+
id: 'minimax',
|
|
564
|
+
name: 'MiniMax',
|
|
565
|
+
keys: [{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' }],
|
|
566
|
+
});
|
|
567
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rotate-2');
|
|
568
|
+
const rotated = mod.rotateKey('minimax');
|
|
569
|
+
assert.equal(rotated, null);
|
|
570
|
+
});
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
describe('v4.6.0 — addBackupKey / removeBackupKey', () => {
|
|
574
|
+
beforeEach(async () => {
|
|
575
|
+
delete process.env.MINIMAX_API_KEY;
|
|
576
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
577
|
+
await resetOpencodeJson();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
it('addBackupKey appends a new standby key', async () => {
|
|
581
|
+
const store = await loadStore();
|
|
582
|
+
await store.add({
|
|
583
|
+
id: 'minimax',
|
|
584
|
+
name: 'MiniMax',
|
|
585
|
+
keys: [{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' }],
|
|
586
|
+
});
|
|
587
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=addRm-1');
|
|
588
|
+
const inserted = mod.addBackupKey('minimax', 'BIZAR_MINIMAX_BACKUP', 'Personal');
|
|
589
|
+
assert.equal(inserted.envVar, 'BIZAR_MINIMAX_BACKUP');
|
|
590
|
+
assert.equal(inserted.status, 'standby');
|
|
591
|
+
assert.equal(inserted.label, 'Personal');
|
|
592
|
+
const list = store.list().find((p) => p.id === 'minimax');
|
|
593
|
+
assert.equal(list.keys.length, 2);
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('addBackupKey rejects duplicate envVar', async () => {
|
|
597
|
+
const store = await loadStore();
|
|
598
|
+
await store.add({
|
|
599
|
+
id: 'minimax',
|
|
600
|
+
name: 'MiniMax',
|
|
601
|
+
keys: [{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' }],
|
|
602
|
+
});
|
|
603
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=addRm-2');
|
|
604
|
+
assert.throws(() => mod.addBackupKey('minimax', 'BIZAR_MINIMAX_KEY', 'dup'), /already exists/);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it('removeBackupKey refuses to remove the last key', async () => {
|
|
608
|
+
const store = await loadStore();
|
|
609
|
+
await store.add({
|
|
610
|
+
id: 'minimax',
|
|
611
|
+
name: 'MiniMax',
|
|
612
|
+
keys: [{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' }],
|
|
613
|
+
});
|
|
614
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=addRm-3');
|
|
615
|
+
assert.throws(() => mod.removeBackupKey('minimax', 'BIZAR_MINIMAX_KEY'), /last key/);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
it('removeBackupKey removes a non-last key', async () => {
|
|
619
|
+
const store = await loadStore();
|
|
620
|
+
await store.add({
|
|
621
|
+
id: 'minimax',
|
|
622
|
+
name: 'MiniMax',
|
|
623
|
+
keys: [
|
|
624
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
625
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
626
|
+
],
|
|
627
|
+
});
|
|
628
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=addRm-4');
|
|
629
|
+
const ok = mod.removeBackupKey('minimax', 'BIZAR_MINIMAX_BACKUP');
|
|
630
|
+
assert.equal(ok, true);
|
|
631
|
+
const list = store.list().find((p) => p.id === 'minimax');
|
|
632
|
+
assert.equal(list.keys.length, 1);
|
|
633
|
+
assert.equal(list.keys[0].envVar, 'BIZAR_MINIMAX_KEY');
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
describe('v4.6.0 — withKeyRotation', () => {
|
|
638
|
+
beforeEach(async () => {
|
|
639
|
+
delete process.env.MINIMAX_API_KEY;
|
|
640
|
+
delete process.env.MINIMAX_API_KEY_BACKUP;
|
|
641
|
+
await resetOpencodeJson();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('returns the fn result on first success', async () => {
|
|
645
|
+
const store = await loadStore();
|
|
646
|
+
await store.add({
|
|
647
|
+
id: 'minimax',
|
|
648
|
+
name: 'MiniMax',
|
|
649
|
+
keys: [
|
|
650
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
651
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
652
|
+
],
|
|
653
|
+
});
|
|
654
|
+
process.env.BIZAR_MINIMAX_KEY = 'sk-cp-primary';
|
|
655
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup';
|
|
656
|
+
|
|
657
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rot-fn-1');
|
|
658
|
+
const result = await mod.withKeyRotation('minimax', async (key, envVar) => {
|
|
659
|
+
assert.equal(key, 'sk-cp-primary');
|
|
660
|
+
assert.equal(envVar, 'BIZAR_MINIMAX_KEY');
|
|
661
|
+
return 'ok';
|
|
662
|
+
});
|
|
663
|
+
assert.equal(result, 'ok');
|
|
664
|
+
|
|
665
|
+
delete process.env.BIZAR_MINIMAX_KEY;
|
|
666
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('retries on retryable errors (429) and rotates to backup', async () => {
|
|
670
|
+
const store = await loadStore();
|
|
671
|
+
await store.add({
|
|
672
|
+
id: 'minimax',
|
|
673
|
+
name: 'MiniMax',
|
|
674
|
+
keys: [
|
|
675
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
676
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
677
|
+
],
|
|
678
|
+
});
|
|
679
|
+
process.env.BIZAR_MINIMAX_KEY = 'sk-cp-primary';
|
|
680
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup';
|
|
681
|
+
|
|
682
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rot-fn-2');
|
|
683
|
+
let callCount = 0;
|
|
684
|
+
const result = await mod.withKeyRotation('minimax', async (key, envVar) => {
|
|
685
|
+
callCount++;
|
|
686
|
+
if (callCount === 1) {
|
|
687
|
+
// Primary key gets 429
|
|
688
|
+
const e = new Error('rate limit');
|
|
689
|
+
e.status = 429;
|
|
690
|
+
throw e;
|
|
691
|
+
}
|
|
692
|
+
assert.equal(key, 'sk-cp-backup');
|
|
693
|
+
assert.equal(envVar, 'BIZAR_MINIMAX_BACKUP');
|
|
694
|
+
return 'rotated';
|
|
695
|
+
});
|
|
696
|
+
assert.equal(result, 'rotated');
|
|
697
|
+
assert.equal(callCount, 2);
|
|
698
|
+
|
|
699
|
+
// Verify the primary was errored and rotated
|
|
700
|
+
const list = store.list().find((p) => p.id === 'minimax');
|
|
701
|
+
const primary = list.keys.find((k) => k.envVar === 'BIZAR_MINIMAX_KEY');
|
|
702
|
+
assert.ok(primary);
|
|
703
|
+
assert.ok(primary.errorCount >= 1);
|
|
704
|
+
assert.equal(primary.status, 'disabled'); // rotated away
|
|
705
|
+
|
|
706
|
+
delete process.env.BIZAR_MINIMAX_KEY;
|
|
707
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('does NOT rotate on non-retryable errors', async () => {
|
|
711
|
+
const store = await loadStore();
|
|
712
|
+
await store.add({
|
|
713
|
+
id: 'minimax',
|
|
714
|
+
name: 'MiniMax',
|
|
715
|
+
keys: [
|
|
716
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
717
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
718
|
+
],
|
|
719
|
+
});
|
|
720
|
+
process.env.BIZAR_MINIMAX_KEY = 'sk-cp-primary';
|
|
721
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup';
|
|
722
|
+
|
|
723
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rot-fn-3');
|
|
724
|
+
await assert.rejects(async () => {
|
|
725
|
+
await mod.withKeyRotation('minimax', async () => {
|
|
726
|
+
const e = new Error('ECONNRESET fetch failed');
|
|
727
|
+
throw e;
|
|
728
|
+
});
|
|
729
|
+
}, /ECONNRESET/);
|
|
730
|
+
|
|
731
|
+
// No rotation should have happened
|
|
732
|
+
const list = store.list().find((p) => p.id === 'minimax');
|
|
733
|
+
const primary = list.keys.find((k) => k.envVar === 'BIZAR_MINIMAX_KEY');
|
|
734
|
+
assert.equal(primary.status, 'active');
|
|
735
|
+
|
|
736
|
+
delete process.env.BIZAR_MINIMAX_KEY;
|
|
737
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('throws rotation_exhausted when all keys fail', async () => {
|
|
741
|
+
const store = await loadStore();
|
|
742
|
+
await store.add({
|
|
743
|
+
id: 'minimax',
|
|
744
|
+
name: 'MiniMax',
|
|
745
|
+
keys: [
|
|
746
|
+
{ envVar: 'BIZAR_MINIMAX_KEY', label: 'Primary', status: 'active' },
|
|
747
|
+
{ envVar: 'BIZAR_MINIMAX_BACKUP', label: 'Backup', status: 'standby' },
|
|
748
|
+
],
|
|
749
|
+
});
|
|
750
|
+
process.env.BIZAR_MINIMAX_KEY = 'sk-cp-primary';
|
|
751
|
+
process.env.BIZAR_MINIMAX_BACKUP = 'sk-cp-backup';
|
|
752
|
+
|
|
753
|
+
const mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=rot-fn-4');
|
|
754
|
+
await assert.rejects(async () => {
|
|
755
|
+
await mod.withKeyRotation('minimax', async () => {
|
|
756
|
+
const e = new Error('http_429 too many');
|
|
757
|
+
e.status = 429;
|
|
758
|
+
throw e;
|
|
759
|
+
});
|
|
760
|
+
}, (err) => {
|
|
761
|
+
assert.equal(err.code, 'rotation_exhausted');
|
|
762
|
+
assert.equal(err.attempts, 2);
|
|
763
|
+
return true;
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
delete process.env.BIZAR_MINIMAX_KEY;
|
|
767
|
+
delete process.env.BIZAR_MINIMAX_BACKUP;
|
|
768
|
+
});
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
describe('v4.6.0 — isRetryableError', () => {
|
|
772
|
+
let mod;
|
|
773
|
+
before(async () => {
|
|
774
|
+
mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + '?cb=retry-1');
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
it('treats HTTP 401/403 as retryable', () => {
|
|
778
|
+
assert.equal(mod.isRetryableError({ status: 401 }), true);
|
|
779
|
+
assert.equal(mod.isRetryableError({ status: 403 }), true);
|
|
780
|
+
});
|
|
781
|
+
it('treats HTTP 429 as retryable', () => {
|
|
782
|
+
assert.equal(mod.isRetryableError({ status: 429 }), true);
|
|
783
|
+
});
|
|
784
|
+
it('treats 5xx as retryable', () => {
|
|
785
|
+
assert.equal(mod.isRetryableError({ status: 500 }), true);
|
|
786
|
+
assert.equal(mod.isRetryableError({ status: 502 }), true);
|
|
787
|
+
assert.equal(mod.isRetryableError({ status: 503 }), true);
|
|
788
|
+
assert.equal(mod.isRetryableError({ status: 599 }), true);
|
|
789
|
+
});
|
|
790
|
+
it('treats minimax-style http_NNN error code as retryable', () => {
|
|
791
|
+
assert.equal(mod.isRetryableError({ error: 'http_429', status: 429 }), true);
|
|
792
|
+
assert.equal(mod.isRetryableError({ error: 'http_503' }), true);
|
|
793
|
+
});
|
|
794
|
+
it('does NOT treat network errors as retryable', () => {
|
|
795
|
+
assert.equal(mod.isRetryableError(new Error('ECONNRESET')), false);
|
|
796
|
+
assert.equal(mod.isRetryableError(new Error('fetch failed')), false);
|
|
797
|
+
assert.equal(mod.isRetryableError(new Error('AbortError')), false);
|
|
798
|
+
});
|
|
799
|
+
it('does NOT treat 4xx-other (400/404/422) as retryable', () => {
|
|
800
|
+
assert.equal(mod.isRetryableError({ status: 400 }), false);
|
|
801
|
+
assert.equal(mod.isRetryableError({ status: 404 }), false);
|
|
802
|
+
assert.equal(mod.isRetryableError({ status: 422 }), false);
|
|
803
|
+
});
|
|
804
|
+
it('handles quota/rate-limit messages', () => {
|
|
805
|
+
assert.equal(mod.isRetryableError(new Error('rate limit exceeded')), true);
|
|
806
|
+
assert.equal(mod.isRetryableError(new Error('insufficient credits')), true);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
333
809
|
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* providers-store-search.node.test.mjs — fuzzy search across the v4.6.0
|
|
3
|
+
* provider catalog.
|
|
4
|
+
*
|
|
5
|
+
* The catalog (`PROVIDER_CATALOG` in providers-store.mjs) is the curated
|
|
6
|
+
* list surfaced by the dashboard's "Add provider" wizard. The wizard
|
|
7
|
+
* shows a search box; typing a query should narrow the list down.
|
|
8
|
+
*
|
|
9
|
+
* Run with: node --test tests/providers-store-search.node.test.mjs
|
|
10
|
+
*/
|
|
11
|
+
import { describe, it, before } from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { resolve } from 'node:path';
|
|
14
|
+
|
|
15
|
+
const REPO = resolve(import.meta.dirname, '..', '..');
|
|
16
|
+
|
|
17
|
+
let mod;
|
|
18
|
+
before(async () => {
|
|
19
|
+
const cacheBust = '?cb=' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
20
|
+
mod = await import(join(REPO, 'bizar-dash/src/server/providers-store.mjs') + cacheBust);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
import { join } from 'node:path';
|
|
24
|
+
|
|
25
|
+
describe('PROVIDER_CATALOG (v4.6.0)', () => {
|
|
26
|
+
it('exports a frozen catalog with at least 11 entries', () => {
|
|
27
|
+
assert.ok(Array.isArray(mod.PROVIDER_CATALOG));
|
|
28
|
+
assert.ok(Object.isFrozen(mod.PROVIDER_CATALOG));
|
|
29
|
+
assert.ok(mod.PROVIDER_CATALOG.length >= 11,
|
|
30
|
+
`expected at least 11 catalog entries, got ${mod.PROVIDER_CATALOG.length}`);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('each entry has the wizard-facing fields', () => {
|
|
34
|
+
for (const e of mod.PROVIDER_CATALOG) {
|
|
35
|
+
assert.equal(typeof e.id, 'string', `${e.id} id not string`);
|
|
36
|
+
assert.ok(e.id.length > 0);
|
|
37
|
+
assert.equal(typeof e.name, 'string');
|
|
38
|
+
assert.ok(e.name.length > 0);
|
|
39
|
+
assert.equal(typeof e.baseURL, 'string');
|
|
40
|
+
assert.equal(e.keyPattern instanceof RegExp, true, `${e.id} keyPattern not a RegExp`);
|
|
41
|
+
assert.equal(typeof e.keyHint, 'string', `${e.id} keyHint not string`);
|
|
42
|
+
assert.equal(typeof e.docs, 'string', `${e.id} docs not string`);
|
|
43
|
+
assert.ok(Array.isArray(e.models), `${e.id} models not array`);
|
|
44
|
+
assert.equal(typeof e.requiresKey, 'boolean', `${e.id} requiresKey not boolean`);
|
|
45
|
+
assert.ok(Object.isFrozen(e), `${e.id} entry not frozen`);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('contains the headline providers we expect', () => {
|
|
50
|
+
const ids = mod.PROVIDER_CATALOG.map((p) => p.id);
|
|
51
|
+
for (const expected of ['opencode', 'anthropic', 'openai', 'google', 'minimax', 'groq', 'mistral', 'cohere', 'ollama', 'lmstudio', 'custom']) {
|
|
52
|
+
assert.ok(ids.includes(expected), `catalog missing ${expected}`);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('local providers (ollama, lmstudio) have requiresKey=false', () => {
|
|
57
|
+
const ollama = mod.findCatalogEntry('ollama');
|
|
58
|
+
const lmstudio = mod.findCatalogEntry('lmstudio');
|
|
59
|
+
assert.equal(ollama.requiresKey, false);
|
|
60
|
+
assert.equal(lmstudio.requiresKey, false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('cloud providers have requiresKey=true', () => {
|
|
64
|
+
const anthropic = mod.findCatalogEntry('anthropic');
|
|
65
|
+
const minimax = mod.findCatalogEntry('minimax');
|
|
66
|
+
assert.equal(anthropic.requiresKey, true);
|
|
67
|
+
assert.equal(minimax.requiresKey, true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('MiniMax keyPattern accepts sk-cp-, sk-ant-, sk-or-', () => {
|
|
71
|
+
const minimax = mod.findCatalogEntry('minimax');
|
|
72
|
+
assert.ok(minimax.keyPattern.test('sk-cp-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
|
73
|
+
assert.ok(minimax.keyPattern.test('sk-ant-aaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
|
74
|
+
assert.ok(minimax.keyPattern.test('sk-or-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'));
|
|
75
|
+
assert.equal(minimax.keyPattern.test('sk-bogus-aaaaaaaaaaaaaaaaaaaaaaaaaa'), false);
|
|
76
|
+
assert.equal(minimax.keyPattern.test('not-a-key'), false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('listCatalog (v4.6.0)', () => {
|
|
81
|
+
it('returns the public shape (no keyPattern)', () => {
|
|
82
|
+
const result = mod.listCatalog();
|
|
83
|
+
assert.ok(Array.isArray(result));
|
|
84
|
+
assert.equal(result.length, mod.PROVIDER_CATALOG.length);
|
|
85
|
+
for (const e of result) {
|
|
86
|
+
assert.equal('keyPattern' in e, false, 'listCatalog leaked keyPattern');
|
|
87
|
+
assert.equal(typeof e.id, 'string');
|
|
88
|
+
assert.equal(typeof e.name, 'string');
|
|
89
|
+
assert.equal(typeof e.baseURL, 'string');
|
|
90
|
+
assert.equal(typeof e.keyHint, 'string');
|
|
91
|
+
assert.equal(typeof e.docs, 'string');
|
|
92
|
+
assert.ok(Array.isArray(e.models));
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('searchProviders (v4.6.0)', () => {
|
|
98
|
+
it('empty query returns full catalog', () => {
|
|
99
|
+
const result = mod.searchProviders('');
|
|
100
|
+
assert.equal(result.length, mod.PROVIDER_CATALOG.length);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('exact id match ranks first', () => {
|
|
104
|
+
const result = mod.searchProviders('minimax');
|
|
105
|
+
assert.ok(result.length > 0);
|
|
106
|
+
assert.equal(result[0].id, 'minimax');
|
|
107
|
+
assert.ok(result[0].score >= 100);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('fuzzy id prefix match finds minimax when typing "mini"', () => {
|
|
111
|
+
const result = mod.searchProviders('mini');
|
|
112
|
+
const ids = result.map((r) => r.id);
|
|
113
|
+
assert.ok(ids.includes('minimax'));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('name match: "GPT" finds openai/opencode (curated names)', () => {
|
|
117
|
+
const result = mod.searchProviders('claude');
|
|
118
|
+
const ids = result.map((r) => r.id);
|
|
119
|
+
assert.ok(ids.includes('anthropic'));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('docs URL match: "openrouter" finds openrouter', () => {
|
|
123
|
+
const result = mod.searchProviders('openrouter');
|
|
124
|
+
const ids = result.map((r) => r.id);
|
|
125
|
+
assert.ok(ids.includes('openrouter'));
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('non-matching query returns empty array', () => {
|
|
129
|
+
const result = mod.searchProviders('xyzzy-no-such-provider-xyzzy');
|
|
130
|
+
assert.equal(result.length, 0);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('respects limit option', () => {
|
|
134
|
+
const result = mod.searchProviders('', { limit: 3 });
|
|
135
|
+
assert.ok(result.length <= 3);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('does not leak keyPattern in results', () => {
|
|
139
|
+
const result = mod.searchProviders('openai');
|
|
140
|
+
for (const r of result) {
|
|
141
|
+
assert.equal('keyPattern' in r, false);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('finds ollama and lmstudio via "local" docs URL match', () => {
|
|
146
|
+
const result = mod.searchProviders('local');
|
|
147
|
+
const ids = result.map((r) => r.id);
|
|
148
|
+
assert.ok(ids.includes('ollama'));
|
|
149
|
+
assert.ok(ids.includes('lmstudio'));
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('findCatalogEntry (v4.6.0)', () => {
|
|
154
|
+
it('returns the entry for known ids', () => {
|
|
155
|
+
const e = mod.findCatalogEntry('anthropic');
|
|
156
|
+
assert.ok(e);
|
|
157
|
+
assert.equal(e.id, 'anthropic');
|
|
158
|
+
});
|
|
159
|
+
it('returns null for unknown ids', () => {
|
|
160
|
+
assert.equal(mod.findCatalogEntry('not-a-real-provider'), null);
|
|
161
|
+
});
|
|
162
|
+
it('returns null for falsy input', () => {
|
|
163
|
+
assert.equal(mod.findCatalogEntry(''), null);
|
|
164
|
+
assert.equal(mod.findCatalogEntry(null), null);
|
|
165
|
+
});
|
|
166
|
+
});
|