import { describe, expect, it } from 'bun:test' import { RouterAuthExpiredError, RouterCreditError, RouterTransport } from './router-transport' function make(opts: Partial[0]> = {}) { return new RouterTransport({ modelId: 'minimax-m3', apiKey: 'sk-test', ...opts }) } describe('RouterTransport basics', () => { it('exposes the configured model id', () => { expect(make().modelId).toBe('minimax-m3') }) it('defaults to the public Router base URL', () => { expect(make().baseUrl).toBe('https://router-api.0g.ai/v1') }) it('strips a trailing slash from a custom base URL', () => { expect(make({ baseUrl: 'http://localhost:1234/v1/' }).baseUrl).toBe('http://localhost:1234/v1') }) it('getLedger() is null (no serving-broker ledger on the Router path)', () => { expect(make().getLedger()).toBe(null) }) it('getLedgerBalance() resolves null', async () => { expect(await make().getLedgerBalance()).toBe(null) }) }) describe('RouterTransport auth', () => { it('returns a Bearer header from the pre-minted sk-', async () => { const headers = await make({ apiKey: 'sk-abc123' }).authHeaders('the last message') expect(headers).toEqual({ Authorization: 'Bearer sk-abc123' }) }) it('ignores lastContent (Bearer auth is content-independent)', async () => { const t = make({ apiKey: 'sk-xyz' }) const a = await t.authHeaders('hello') const b = await t.authHeaders('a totally different last message') expect(a).toEqual(b) }) it('throws before init() when no key and no minter', async () => { const t = new RouterTransport({ modelId: 'minimax-m3' }) await expect(t.init()).rejects.toThrow(/no api key/i) }) it('lazily mints the sk- on init() via the injected minter', async () => { let minted = 0 const t = new RouterTransport({ modelId: 'minimax-m3', mintApiKey: async () => { minted++ return 'sk-minted' }, }) await t.init() await t.init() // idempotent, minter runs once expect(minted).toBe(1) expect(await t.authHeaders('x')).toEqual({ Authorization: 'Bearer sk-minted' }) }) it('authHeaders throws if accessed before init when lazily minted', async () => { const t = new RouterTransport({ modelId: 'minimax-m3', mintApiKey: async () => 'sk-late' }) await expect(t.authHeaders('x')).rejects.toThrow(/not initialized/i) }) }) describe('RouterTransport reprovision (auth-failure re-mint)', () => { it('stripInlineThink is true (MiniMax-M3 emits inline )', () => { expect(make().stripInlineThink).toBe(true) }) it('re-mints a fresh sk- and returns true', async () => { let n = 0 let reset = 0 const t = new RouterTransport({ modelId: 'minimax-m3', apiKey: 'sk-stale', mintApiKey: async () => `sk-fresh-${++n}`, resetMint: () => { reset++ }, }) // Starts on the injected (stale) key. expect(await t.authHeaders('x')).toEqual({ Authorization: 'Bearer sk-stale' }) const ok = await t.reprovision() expect(ok).toBe(true) expect(reset).toBe(1) // busted the memo expect(await t.authHeaders('x')).toEqual({ Authorization: 'Bearer sk-fresh-1' }) }) it('returns false when there is no minter to fall back to', async () => { const t = new RouterTransport({ modelId: 'minimax-m3', apiKey: 'sk-only' }) expect(await t.reprovision()).toBe(false) // sk- unchanged. expect(await t.authHeaders('x')).toEqual({ Authorization: 'Bearer sk-only' }) }) it('returns false when re-mint throws', async () => { const t = new RouterTransport({ modelId: 'minimax-m3', apiKey: 'sk-stale', mintApiKey: async () => { throw new Error('mint down') }, }) expect(await t.reprovision()).toBe(false) }) }) describe('RouterTransport.mapHttpError', () => { it('maps 401 to a typed auth-expired error', () => { const err = make().mapHttpError(401, '{"error":{"message":"unauthorized"}}') expect(err).toBeInstanceOf(RouterAuthExpiredError) }) it('maps 403 to a typed auth-expired error', () => { expect(make().mapHttpError(403, 'forbidden')).toBeInstanceOf(RouterAuthExpiredError) }) it('maps 402 to a typed credit error with a topup hint', () => { const err = make().mapHttpError(402, '{"error":{"message":"insufficient_balance"}}') expect(err).toBeInstanceOf(RouterCreditError) expect(err?.message).toContain('topup --router') }) it('maps an insufficient_balance body to a credit error even off-402', () => { expect(make().mapHttpError(400, 'insufficient balance for request')).toBeInstanceOf( RouterCreditError, ) }) it('returns null for unmapped statuses (caller throws generic)', () => { expect(make().mapHttpError(500, 'internal error')).toBe(null) expect(make().mapHttpError(404, 'not found')).toBe(null) }) it('does NOT misclassify a 5xx whose body merely mentions insufficient/credit', () => { // Avoids telling the operator to deposit NON-recoverable credit to "fix" a // transient server outage. The body match is scoped to 4xx billing statuses. expect(make().mapHttpError(503, 'service insufficient to issue credit token')).toBe(null) expect(make().mapHttpError(500, 'insufficient credit')).toBe(null) }) })