95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
|
|
import { OpenAIClient } from './openai.js';
|
|
|
|
vi.mock('../auth/openai.js', () => ({
|
|
ensureValidOpenAIAuth: vi.fn(async () => ({
|
|
access_token: 'at',
|
|
refresh_token: 'rt',
|
|
expires_at: Date.now() + 60_000,
|
|
created_at: new Date().toISOString(),
|
|
account_id: 'acct',
|
|
})),
|
|
}));
|
|
|
|
function makeSse(events: Array<{ event: string; data: unknown }>): string {
|
|
return events
|
|
.map((e) => `event: ${e.event}\ndata: ${JSON.stringify(e.data)}\n\n`)
|
|
.join('');
|
|
}
|
|
|
|
describe('OpenAIClient OAuth (Codex)', () => {
|
|
const originalFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('streams SSE and accumulates output_text.delta', async () => {
|
|
const sse = makeSse([
|
|
{ event: 'response.created', data: { type: 'response.created', response: { id: 'r1' } } },
|
|
{ event: 'response.output_text.delta', data: { type: 'response.output_text.delta', delta: 'hel' } },
|
|
{ event: 'response.output_text.delta', data: { type: 'response.output_text.delta', delta: 'lo' } },
|
|
{ event: 'response.completed', data: { type: 'response.completed', response: { usage: { input_tokens: 2, output_tokens: 2 } } } },
|
|
]);
|
|
|
|
globalThis.fetch = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
|
|
const body = typeof init?.body === 'string' ? init.body : '';
|
|
if (!body) {
|
|
throw new Error('Expected JSON body');
|
|
}
|
|
const parsed = JSON.parse(body) as Record<string, unknown>;
|
|
expect(parsed.store).toBe(false);
|
|
expect(parsed.stream).toBe(true);
|
|
expect(typeof parsed.instructions).toBe('string');
|
|
expect(Array.isArray(parsed.input)).toBe(true);
|
|
|
|
const stream = new ReadableStream({
|
|
start(controller) {
|
|
controller.enqueue(new TextEncoder().encode(sse));
|
|
controller.close();
|
|
},
|
|
});
|
|
|
|
return new Response(stream, { status: 200 });
|
|
}) as typeof fetch;
|
|
|
|
const client = new OpenAIClient({ model: 'gpt-5.3-codex', useOAuth: true });
|
|
const resp = await client.chat({
|
|
system: 'You are helpful.',
|
|
messages: [{ role: 'user', content: 'hi' }],
|
|
});
|
|
|
|
expect(resp.content).toBe('hello');
|
|
expect(resp.usage).toEqual({ inputTokens: 2, outputTokens: 2 });
|
|
});
|
|
|
|
it('throws when tools are requested in OAuth mode', async () => {
|
|
const fetchSpy = vi.fn();
|
|
globalThis.fetch = fetchSpy as unknown as typeof fetch;
|
|
const client = new OpenAIClient({ model: 'gpt-5.3-codex', useOAuth: true });
|
|
|
|
await expect(client.chat({
|
|
system: 'You are helpful.',
|
|
messages: [{ role: 'user', content: 'use tools' }],
|
|
tools: [{
|
|
name: 'gmail_read',
|
|
description: 'Read Gmail message',
|
|
input_schema: {
|
|
type: 'object',
|
|
properties: { id: { type: 'string' } },
|
|
required: ['id'],
|
|
},
|
|
}],
|
|
})).rejects.toThrow('does not support tool execution');
|
|
|
|
expect(fetchSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|