Files
metapi/src/server/services/modelService.test.ts
T
ciat-777 575a15232b feat: implement Codex OAuth flow with loopback callback server and management UI
- Add localCallbackServer for handling OAuth callback requests.
- Implement service functions for managing OAuth sessions and accounts.
- Create CodexAdapter for platform-specific interactions.
- Develop OAuthManagement page for managing OAuth connections and providers.
- Add tests for localCallbackServer and OAuthManagement components.
2026-03-17 11:07:25 +08:00

222 lines
7.1 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { mkdtempSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { and, eq } from 'drizzle-orm';
type DbModule = typeof import('../db/index.js');
type ModelServiceModule = typeof import('./modelService.js');
describe('rebuildTokenRoutesFromAvailability', () => {
let db: DbModule['db'];
let schema: DbModule['schema'];
let rebuildTokenRoutesFromAvailability: ModelServiceModule['rebuildTokenRoutesFromAvailability'];
let dataDir = '';
beforeAll(async () => {
dataDir = mkdtempSync(join(tmpdir(), 'metapi-model-service-'));
process.env.DATA_DIR = dataDir;
await import('../db/migrate.js');
const dbModule = await import('../db/index.js');
const modelService = await import('./modelService.js');
db = dbModule.db;
schema = dbModule.schema;
rebuildTokenRoutesFromAvailability = modelService.rebuildTokenRoutesFromAvailability;
});
beforeEach(async () => {
await db.delete(schema.routeChannels).run();
await db.delete(schema.tokenRoutes).run();
await db.delete(schema.tokenModelAvailability).run();
await db.delete(schema.modelAvailability).run();
await db.delete(schema.accountTokens).run();
await db.delete(schema.accounts).run();
await db.delete(schema.sites).run();
});
afterAll(() => {
delete process.env.DATA_DIR;
});
it('creates an exact route with an account-direct channel for apikey model availability', async () => {
const site = await db.insert(schema.sites).values({
name: 'apikey-site',
url: 'https://apikey-site.example.com',
platform: 'new-api',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'apikey-user',
accessToken: '',
apiToken: 'sk-apikey-route',
status: 'active',
extraConfig: JSON.stringify({ credentialMode: 'apikey' }),
}).returning().get();
await db.insert(schema.modelAvailability).values({
accountId: account.id,
modelName: 'gpt-5.2-codex',
available: true,
latencyMs: 1200,
checkedAt: '2026-03-08T08:00:00.000Z',
}).run();
const rebuild = await rebuildTokenRoutesFromAvailability();
expect(rebuild.models).toBe(1);
const route = await db.select().from(schema.tokenRoutes)
.where(eq(schema.tokenRoutes.modelPattern, 'gpt-5.2-codex'))
.get();
expect(route).toBeDefined();
const channels = await db.select().from(schema.routeChannels)
.where(and(
eq(schema.routeChannels.routeId, route!.id),
eq(schema.routeChannels.accountId, account.id),
))
.all();
expect(channels).toHaveLength(1);
expect(channels[0]?.tokenId ?? null).toBeNull();
expect(channels[0]?.manualOverride).toBe(false);
});
it('creates an exact route with an account-direct channel for oauth model availability', async () => {
const site = await db.insert(schema.sites).values({
name: 'codex-site',
url: 'https://chatgpt.com/backend-api/codex',
platform: 'codex',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'codex-user@example.com',
accessToken: 'oauth-access-token',
apiToken: null,
status: 'active',
extraConfig: JSON.stringify({
credentialMode: 'session',
oauth: {
provider: 'codex',
accountId: 'chatgpt-account-123',
email: 'codex-user@example.com',
planType: 'team',
},
}),
}).returning().get();
await db.insert(schema.modelAvailability).values({
accountId: account.id,
modelName: 'gpt-5.2-codex',
available: true,
latencyMs: 320,
checkedAt: '2026-03-17T00:00:00.000Z',
}).run();
const rebuild = await rebuildTokenRoutesFromAvailability();
expect(rebuild.models).toBe(1);
const route = await db.select().from(schema.tokenRoutes)
.where(eq(schema.tokenRoutes.modelPattern, 'gpt-5.2-codex'))
.get();
expect(route).toBeDefined();
const channels = await db.select().from(schema.routeChannels)
.where(and(
eq(schema.routeChannels.routeId, route!.id),
eq(schema.routeChannels.accountId, account.id),
))
.all();
expect(channels).toHaveLength(1);
expect(channels[0]?.tokenId ?? null).toBeNull();
expect(channels[0]?.manualOverride).toBe(false);
});
it('removes stale exact routes and keeps wildcard routes on rebuild', async () => {
const site = await db.insert(schema.sites).values({
name: 'site-1',
url: 'https://site-1.example.com',
platform: 'new-api',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'user-1',
accessToken: 'access-1',
status: 'active',
}).returning().get();
const token = await db.insert(schema.accountTokens).values({
accountId: account.id,
name: 'default',
token: 'sk-test',
source: 'manual',
enabled: true,
isDefault: true,
}).returning().get();
await db.insert(schema.tokenModelAvailability).values({
tokenId: token.id,
modelName: 'latest-model',
available: true,
}).run();
const staleRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 'old-model',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: staleRoute.id,
accountId: account.id,
tokenId: token.id,
priority: 0,
weight: 10,
enabled: true,
manualOverride: false,
}).run();
const wildcardRoute = await db.insert(schema.tokenRoutes).values({
modelPattern: 'gpt-*',
enabled: true,
}).returning().get();
await db.insert(schema.routeChannels).values({
routeId: wildcardRoute.id,
accountId: account.id,
tokenId: token.id,
priority: 0,
weight: 10,
enabled: true,
manualOverride: false,
}).run();
const rebuild = await rebuildTokenRoutesFromAvailability();
expect(rebuild.models).toBe(1);
expect(rebuild.removedRoutes).toBe(1);
const oldRoute = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, staleRoute.id)).get();
expect(oldRoute).toBeUndefined();
const oldChannels = await db.select().from(schema.routeChannels).where(eq(schema.routeChannels.routeId, staleRoute.id)).all();
expect(oldChannels).toHaveLength(0);
const latestRoute = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.modelPattern, 'latest-model')).get();
expect(latestRoute).toBeDefined();
const latestChannels = await db.select().from(schema.routeChannels)
.where(and(eq(schema.routeChannels.routeId, latestRoute!.id), eq(schema.routeChannels.tokenId, token.id)))
.all();
expect(latestChannels.length).toBeGreaterThan(0);
const wildcardRouteAfter = await db.select().from(schema.tokenRoutes).where(eq(schema.tokenRoutes.id, wildcardRoute.id)).get();
expect(wildcardRouteAfter).toBeDefined();
});
});