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 { sql } from 'drizzle-orm'; type DbModule = typeof import('../db/index.js'); type TokenRouterModule = typeof import('./tokenRouter.js'); describe('TokenRouter site status guard', () => { let db: DbModule['db']; let schema: DbModule['schema']; let TokenRouter: TokenRouterModule['TokenRouter']; let invalidateTokenRouterCache: TokenRouterModule['invalidateTokenRouterCache']; let dataDir = ''; beforeAll(async () => { dataDir = mkdtempSync(join(tmpdir(), 'metapi-token-router-site-status-')); process.env.DATA_DIR = dataDir; await import('../db/migrate.js'); const dbModule = await import('../db/index.js'); const tokenRouterModule = await import('./tokenRouter.js'); db = dbModule.db; schema = dbModule.schema; TokenRouter = tokenRouterModule.TokenRouter; invalidateTokenRouterCache = tokenRouterModule.invalidateTokenRouterCache; }); 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(); invalidateTokenRouterCache(); }); afterAll(() => { invalidateTokenRouterCache(); delete process.env.DATA_DIR; }); it('does not select channels from disabled sites', async () => { const site = await db.insert(schema.sites).values({ name: 'disabled-site', url: 'https://disabled.example.com', platform: 'new-api', }).returning().get(); await db.run(sql`update sites set status = 'disabled' where id = ${site.id}`); const account = await db.insert(schema.accounts).values({ siteId: site.id, username: 'disabled-user', accessToken: 'access-disabled', apiToken: 'sk-disabled', status: 'active', }).returning().get(); const route = await db.insert(schema.tokenRoutes).values({ modelPattern: 'gpt-4o-mini', enabled: true, }).returning().get(); const channel = await db.insert(schema.routeChannels).values({ routeId: route.id, accountId: account.id, tokenId: null, priority: 0, weight: 10, enabled: true, manualOverride: false, }).returning().get(); const router = new TokenRouter(); const selected = await router.selectChannel('gpt-4o-mini'); expect(selected).toBeNull(); const decision = await router.explainSelection('gpt-4o-mini'); expect(decision.matched).toBe(true); const candidate = decision.candidates.find((item) => item.channelId === channel.id); expect(candidate?.eligible).toBe(false); }); it('does not fallback to deprecated site apiKey when channel and account tokens are missing', async () => { const site = await db.insert(schema.sites).values({ name: 'site-api-key-only', url: 'https://site-key.example.com', platform: 'new-api', apiKey: 'sk-site-only', }).returning().get(); const account = await db.insert(schema.accounts).values({ siteId: site.id, username: 'site-key-user', accessToken: '', apiToken: null, status: 'active', extraConfig: JSON.stringify({ credentialMode: 'apikey' }), }).returning().get(); const route = await db.insert(schema.tokenRoutes).values({ modelPattern: 'gpt-4.1-mini', enabled: true, }).returning().get(); await db.insert(schema.routeChannels).values({ routeId: route.id, accountId: account.id, tokenId: null, priority: 0, weight: 10, enabled: true, manualOverride: false, }).run(); const selected = await new TokenRouter().selectChannel('gpt-4.1-mini'); expect(selected).toBeNull(); }); it('uses codex oauth access token when no api token is present', async () => { const site = await db.insert(schema.sites).values({ name: 'codex-site', url: 'https://chatgpt.com/backend-api/codex', platform: 'codex', status: 'active', }).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: 'plus', }, }), }).returning().get(); const route = await db.insert(schema.tokenRoutes).values({ modelPattern: 'gpt-5.2-codex', enabled: true, }).returning().get(); await db.insert(schema.routeChannels).values({ routeId: route.id, accountId: account.id, tokenId: null, priority: 0, weight: 10, enabled: true, manualOverride: false, }).run(); const selected = await new TokenRouter().selectChannel('gpt-5.2-codex'); expect(selected).not.toBeNull(); expect(selected?.tokenValue).toBe('oauth-access-token'); }); it('prefers codex oauth access token over a stale legacy api token on oauth-managed rows', async () => { const site = await db.insert(schema.sites).values({ name: 'codex-site', url: 'https://chatgpt.com/backend-api/codex', platform: 'codex', status: 'active', }).returning().get(); const account = await db.insert(schema.accounts).values({ siteId: site.id, username: 'codex-user@example.com', accessToken: 'oauth-access-token', apiToken: 'stale-direct-token', status: 'active', extraConfig: JSON.stringify({ credentialMode: 'session', oauth: { provider: 'codex', accountId: 'chatgpt-account-123', email: 'codex-user@example.com', planType: 'plus', }, }), }).returning().get(); const route = await db.insert(schema.tokenRoutes).values({ modelPattern: 'gpt-5.2-codex', enabled: true, }).returning().get(); await db.insert(schema.routeChannels).values({ routeId: route.id, accountId: account.id, tokenId: null, priority: 0, weight: 10, enabled: true, manualOverride: false, }).run(); const selected = await new TokenRouter().selectChannel('gpt-5.2-codex'); expect(selected).not.toBeNull(); expect(selected?.tokenValue).toBe('oauth-access-token'); }); it('does not fall back to a stale legacy api token when an oauth-managed row has no access token', async () => { const site = await db.insert(schema.sites).values({ name: 'codex-site', url: 'https://chatgpt.com/backend-api/codex', platform: 'codex', status: 'active', }).returning().get(); const account = await db.insert(schema.accounts).values({ siteId: site.id, username: 'codex-user@example.com', accessToken: '', apiToken: 'stale-direct-token', status: 'active', extraConfig: JSON.stringify({ credentialMode: 'session', oauth: { provider: 'codex', accountId: 'chatgpt-account-123', email: 'codex-user@example.com', planType: 'plus', }, }), }).returning().get(); const route = await db.insert(schema.tokenRoutes).values({ modelPattern: 'gpt-5.2-codex', enabled: true, }).returning().get(); await db.insert(schema.routeChannels).values({ routeId: route.id, accountId: account.id, tokenId: null, priority: 0, weight: 10, enabled: true, manualOverride: false, }).run(); const selected = await new TokenRouter().selectChannel('gpt-5.2-codex'); expect(selected).toBeNull(); }); });