| import { describe, it, expect } from 'vitest'; |
| import Database from 'better-sqlite3'; |
| import { initDb } from '../../db/index.js'; |
|
|
| |
| |
| |
| |
| describe('Migration idempotency', () => { |
| it('initDb on a fresh in-memory DB then re-run produces identical row counts', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| |
| const tmpPath = `/tmp/freeapi-idempotency-${Date.now()}.db`; |
|
|
| const db1 = initDb(tmpPath); |
| const before = { |
| models: (db1.prepare('SELECT COUNT(*) AS c FROM models').get() as { c: number }).c, |
| fallback: (db1.prepare('SELECT COUNT(*) AS c FROM fallback_config').get() as { c: number }).c, |
| enabledModels: (db1.prepare('SELECT COUNT(*) AS c FROM models WHERE enabled = 1').get() as { c: number }).c, |
| disabledModels: (db1.prepare('SELECT COUNT(*) AS c FROM models WHERE enabled = 0').get() as { c: number }).c, |
| orphanFallbacks: (db1.prepare(` |
| SELECT COUNT(*) AS c FROM fallback_config f |
| LEFT JOIN models m ON f.model_db_id = m.id |
| WHERE m.id IS NULL |
| `).get() as { c: number }).c, |
| }; |
| db1.close(); |
|
|
| |
| const db2 = initDb(tmpPath); |
| const after = { |
| models: (db2.prepare('SELECT COUNT(*) AS c FROM models').get() as { c: number }).c, |
| fallback: (db2.prepare('SELECT COUNT(*) AS c FROM fallback_config').get() as { c: number }).c, |
| enabledModels: (db2.prepare('SELECT COUNT(*) AS c FROM models WHERE enabled = 1').get() as { c: number }).c, |
| disabledModels: (db2.prepare('SELECT COUNT(*) AS c FROM models WHERE enabled = 0').get() as { c: number }).c, |
| orphanFallbacks: (db2.prepare(` |
| SELECT COUNT(*) AS c FROM fallback_config f |
| LEFT JOIN models m ON f.model_db_id = m.id |
| WHERE m.id IS NULL |
| `).get() as { c: number }).c, |
| }; |
| db2.close(); |
|
|
| expect(after).toEqual(before); |
| expect(after.orphanFallbacks).toBe(0); |
| }); |
|
|
| it('every catalog row has exactly one fallback_config entry', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
|
|
| const rows = db.prepare(` |
| SELECT m.id, COUNT(f.id) AS fb_count |
| FROM models m |
| LEFT JOIN fallback_config f ON m.id = f.model_db_id |
| GROUP BY m.id |
| HAVING COUNT(f.id) <> 1 |
| `).all() as { id: number; fb_count: number }[]; |
|
|
| expect(rows).toEqual([]); |
| }); |
|
|
| it('UNIQUE(platform, model_id) constraint holds — no duplicate catalog rows', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
|
|
| const dups = db.prepare(` |
| SELECT platform, model_id, COUNT(*) AS c FROM models |
| GROUP BY platform, model_id |
| HAVING COUNT(*) > 1 |
| `).all(); |
|
|
| expect(dups).toEqual([]); |
| }); |
|
|
| it('V12: dead OR :free rows are absent and the four new rows are present', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
|
|
| const dead = db.prepare(` |
| SELECT model_id FROM models |
| WHERE platform = 'openrouter' |
| AND model_id IN ('inclusionai/ling-2.6-1t:free', 'tencent/hy3-preview:free') |
| `).all(); |
| expect(dead).toEqual([]); |
|
|
| const live = db.prepare(` |
| SELECT model_id FROM models |
| WHERE platform = 'openrouter' |
| AND model_id IN ( |
| 'arcee-ai/trinity-large-thinking:free', |
| 'baidu/cobuddy:free', |
| 'openrouter/owl-alpha', |
| 'nousresearch/hermes-3-llama-3.1-405b:free' |
| ) |
| ORDER BY model_id |
| `).all() as { model_id: string }[]; |
| expect(live.map(r => r.model_id)).toEqual([ |
| 'arcee-ai/trinity-large-thinking:free', |
| 'baidu/cobuddy:free', |
| 'nousresearch/hermes-3-llama-3.1-405b:free', |
| 'openrouter/owl-alpha', |
| ]); |
|
|
| const widened = db.prepare(` |
| SELECT model_id, context_window FROM models |
| WHERE platform = 'openrouter' |
| AND model_id IN ('nvidia/nemotron-3-super-120b-a12b:free', 'qwen/qwen3-coder:free') |
| ORDER BY model_id |
| `).all() as { model_id: string; context_window: number }[]; |
| expect(widened).toEqual([ |
| { model_id: 'nvidia/nemotron-3-super-120b-a12b:free', context_window: 1000000 }, |
| { model_id: 'qwen/qwen3-coder:free', context_window: 1048576 }, |
| ]); |
| }); |
|
|
| it('V13: cross-provider catalog refresh applies cleanly', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
|
|
| |
| const disabled = db.prepare(` |
| SELECT platform, model_id, enabled FROM models |
| WHERE (platform = 'google' AND model_id = 'gemini-3.1-pro-preview') |
| OR (platform = 'ollama' AND model_id IN ('kimi-k2-thinking', 'mistral-large-3:675b', 'deepseek-v3.2')) |
| ORDER BY platform, model_id |
| `).all() as { platform: string; model_id: string; enabled: number }[]; |
| expect(disabled).toHaveLength(4); |
| for (const row of disabled) expect(row.enabled).toBe(0); |
|
|
| |
| const removed = db.prepare(` |
| SELECT model_id FROM models |
| WHERE (platform = 'sambanova' AND model_id = 'DeepSeek-V3.1-cb') |
| OR (platform = 'cloudflare' AND model_id = '@cf/moonshotai/kimi-k2.5') |
| `).all(); |
| expect(removed).toEqual([]); |
|
|
| |
| const additions = db.prepare(` |
| SELECT platform, model_id FROM models |
| WHERE (platform, model_id) IN (VALUES |
| ('groq', 'openai/gpt-oss-safeguard-20b'), |
| ('cloudflare', '@cf/nvidia/nemotron-3-120b-a12b'), |
| ('cloudflare', '@cf/google/gemma-4-26b-a4b-it'), |
| ('google', 'gemini-3.5-flash'), |
| ('nvidia', 'deepseek-ai/deepseek-v4-flash'), |
| ('nvidia', 'z-ai/glm-5.1'), |
| ('nvidia', 'qwen/qwen3-coder-480b-a35b-instruct'), |
| ('mistral', 'mistral-small-latest'), |
| ('mistral', 'ministral-8b-latest'), |
| ('cohere', 'command-a-reasoning-08-2025'), |
| ('cohere', 'command-r-08-2024'), |
| ('ollama', 'qwen3-coder-next'), |
| ('huggingface', 'deepseek-ai/DeepSeek-V4-Flash'), |
| ('huggingface', 'moonshotai/Kimi-K2.6'), |
| ('huggingface', 'Qwen/Qwen3-Coder-Next') |
| ) |
| `).all(); |
| expect(additions).toHaveLength(15); |
|
|
| |
| const cerebrasLimits = db.prepare(` |
| SELECT rpm_limit, rpd_limit, tpm_limit, tpd_limit FROM models |
| WHERE platform = 'cerebras' AND model_id = 'qwen-3-235b-a22b-instruct-2507' |
| `).get() as { rpm_limit: number; rpd_limit: number; tpm_limit: number; tpd_limit: number }; |
| expect(cerebrasLimits).toEqual({ rpm_limit: 5, rpd_limit: 2400, tpm_limit: 30000, tpd_limit: 1000000 }); |
|
|
| const sambanovaCtx = (db.prepare(` |
| SELECT context_window FROM models WHERE platform = 'sambanova' AND model_id = 'DeepSeek-V3.2' |
| `).get() as { context_window: number }).context_window; |
| expect(sambanovaCtx).toBe(32768); |
|
|
| const cfFp8Ctx = (db.prepare(` |
| SELECT context_window FROM models WHERE platform = 'cloudflare' AND model_id = '@cf/meta/llama-3.3-70b-instruct-fp8-fast' |
| `).get() as { context_window: number }).context_window; |
| expect(cfFp8Ctx).toBe(24000); |
|
|
| const mistralCtx = db.prepare(` |
| SELECT model_id, context_window FROM models |
| WHERE platform = 'mistral' |
| AND model_id IN ('codestral-latest', 'devstral-latest', 'magistral-medium-latest', 'mistral-large-latest') |
| ORDER BY model_id |
| `).all() as { model_id: string; context_window: number }[]; |
| expect(mistralCtx).toEqual([ |
| { model_id: 'codestral-latest', context_window: 256000 }, |
| { model_id: 'devstral-latest', context_window: 262144 }, |
| { model_id: 'magistral-medium-latest', context_window: 131072 }, |
| { model_id: 'mistral-large-latest', context_window: 262144 }, |
| ]); |
| }); |
|
|
| it('V14: cerebras deprecation disables qwen-3-235b and llama3.1-8b but keeps gpt-oss-120b enabled', () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
|
|
| const rows = db.prepare(` |
| SELECT model_id, enabled FROM models |
| WHERE platform = 'cerebras' |
| AND model_id IN ('qwen-3-235b-a22b-instruct-2507', 'llama3.1-8b', 'gpt-oss-120b') |
| ORDER BY model_id |
| `).all() as { model_id: string; enabled: number }[]; |
|
|
| expect(rows).toEqual([ |
| { model_id: 'gpt-oss-120b', enabled: 1 }, |
| { model_id: 'llama3.1-8b', enabled: 0 }, |
| { model_id: 'qwen-3-235b-a22b-instruct-2507', enabled: 0 }, |
| ]); |
| }); |
|
|
| it('all enabled catalog platforms have a registered provider', async () => { |
| process.env.ENCRYPTION_KEY = '0'.repeat(64); |
| const db = initDb(':memory:'); |
| const { hasProvider } = await import('../../providers/index.js'); |
|
|
| const platforms = (db.prepare( |
| `SELECT DISTINCT platform FROM models WHERE enabled = 1` |
| ).all() as { platform: any }[]).map(r => r.platform); |
|
|
| const missing = platforms.filter(p => !hasProvider(p)); |
| expect(missing).toEqual([]); |
| }); |
| }); |
|
|