import { describe, it, expect, beforeAll } from 'vitest'; import type { Express } from 'express'; import { createApp } from '../../app.js'; import { initDb } from '../../db/index.js'; import { mintDashboardToken, isGatedApiPath } from '../helpers/auth.js'; let dashToken = ''; async function request(app: Express, method: string, path: string, body?: any) { const server = app.listen(0); const addr = server.address() as any; const url = `http://127.0.0.1:${addr.port}${path}`; const res = await fetch(url, { method, headers: { ...(body ? { 'Content-Type': 'application/json' } : {}), ...(isGatedApiPath(path) ? { Authorization: `Bearer ${dashToken}` } : {}), }, body: body ? JSON.stringify(body) : undefined, }); const data = await res.json().catch(() => null); server.close(); return { status: res.status, body: data }; } describe('Fallback API', () => { let app: Express; beforeAll(() => { process.env.ENCRYPTION_KEY = '0'.repeat(64); initDb(':memory:'); app = createApp(); dashToken = mintDashboardToken(); }); it('GET /api/fallback returns fallback chain', async () => { const { status, body } = await request(app, 'GET', '/api/fallback'); expect(status).toBe(200); expect(Array.isArray(body)).toBe(true); expect(body.length).toBeGreaterThan(0); // Should be sorted by priority for (let i = 1; i < body.length; i++) { expect(body[i].priority).toBeGreaterThanOrEqual(body[i - 1].priority); } }); it('GET /api/fallback entries have expected fields', async () => { const { body } = await request(app, 'GET', '/api/fallback'); const first = body[0]; expect(first).toHaveProperty('modelDbId'); expect(first).toHaveProperty('priority'); expect(first).toHaveProperty('enabled'); expect(first).toHaveProperty('platform'); expect(first).toHaveProperty('displayName'); expect(first).toHaveProperty('intelligenceRank'); }); it('PUT /api/fallback updates order', async () => { const { body: original } = await request(app, 'GET', '/api/fallback'); // Reverse the order const reversed = original.map((e: any, i: number) => ({ modelDbId: e.modelDbId, priority: original.length - i, enabled: e.enabled, })); const { status } = await request(app, 'PUT', '/api/fallback', reversed); expect(status).toBe(200); // Verify order changed const { body: after } = await request(app, 'GET', '/api/fallback'); expect(after[0].modelDbId).toBe(original[original.length - 1].modelDbId); // Restore original order const restore = original.map((e: any, i: number) => ({ modelDbId: e.modelDbId, priority: i + 1, enabled: e.enabled, })); await request(app, 'PUT', '/api/fallback', restore); }); it('POST /api/fallback/sort/intelligence sorts by cross-provider tier, then rank', async () => { const { status } = await request(app, 'POST', '/api/fallback/sort/intelligence'); expect(status).toBe(200); const { body } = await request(app, 'GET', '/api/fallback'); // intelligence_rank is per-provider, so the sort normalizes on the // cross-provider capability tier (size_label) first (issue #135). const tier: Record = { Frontier: 1, Large: 2, Medium: 3, Small: 4 }; const tierOf = (label: string) => tier[label] ?? 5; for (let i = 1; i < body.length; i++) { const prevTier = tierOf(body[i - 1].sizeLabel); const curTier = tierOf(body[i].sizeLabel); // Capability tier never decreases... expect(curTier).toBeGreaterThanOrEqual(prevTier); // ...and within the same tier, per-provider rank breaks the tie. if (curTier === prevTier) { expect(body[i].intelligenceRank).toBeGreaterThanOrEqual(body[i - 1].intelligenceRank); } } }); it('intelligence sort never places a weaker tier above a Frontier model (#135)', async () => { await request(app, 'POST', '/api/fallback/sort/intelligence'); const { body } = await request(app, 'GET', '/api/fallback'); // The last Frontier model must come before the first non-Frontier model — // i.e. no "Intel #1 from a weaker provider" leaks above the frontier tier. const lastFrontier = body.map((m: any) => m.sizeLabel).lastIndexOf('Frontier'); const firstNonFrontier = body.findIndex((m: any) => m.sizeLabel !== 'Frontier'); if (lastFrontier !== -1 && firstNonFrontier !== -1) { expect(lastFrontier).toBeLessThan(firstNonFrontier); } }); it('POST /api/fallback/sort/speed sorts by speed', async () => { const { status } = await request(app, 'POST', '/api/fallback/sort/speed'); expect(status).toBe(200); const { body } = await request(app, 'GET', '/api/fallback'); // Should be sorted ascending by speed rank for (let i = 1; i < body.length; i++) { expect(body[i].speedRank).toBeGreaterThanOrEqual(body[i - 1].speedRank); } }); it('POST /api/fallback/sort/invalid returns 400', async () => { const { status } = await request(app, 'POST', '/api/fallback/sort/invalid'); expect(status).toBe(400); }); });