nexu-io--open-design
142 行
3.9 KiB
TypeScript
142 行
3.9 KiB
TypeScript
// Surface checks for the daemon /api/test/connection helper. Verifies the request
|
|
// shape (URL, method, JSON body), happy-path decoding, and how the helper
|
|
// degrades when the daemon answers with a 5xx envelope.
|
|
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { testAgent, testApiProvider } from '../../src/providers/connection-test';
|
|
import type { ConnectionTestResponse } from '../../src/types';
|
|
|
|
const realFetch = globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
// Each test installs its own stub.
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = realFetch;
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
function jsonResponse(body: unknown, status = 200): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { 'content-type': 'application/json' },
|
|
});
|
|
}
|
|
|
|
describe('testApiProvider', () => {
|
|
it('POSTs the protocol-shaped body and returns the daemon envelope', async () => {
|
|
const expected: ConnectionTestResponse = {
|
|
ok: true,
|
|
kind: 'success',
|
|
latencyMs: 42,
|
|
model: 'claude-sonnet-4-5',
|
|
sample: 'ok',
|
|
};
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(jsonResponse(expected));
|
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
|
|
const result = await testApiProvider({
|
|
protocol: 'anthropic',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
apiKey: 'sk-ant-test',
|
|
model: 'claude-sonnet-4-5',
|
|
});
|
|
|
|
expect(result).toEqual(expected);
|
|
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
const [url, init] = fetchMock.mock.calls[0]!;
|
|
expect(url).toBe('/api/test/connection');
|
|
expect(init.method).toBe('POST');
|
|
expect(init.headers['content-type']).toBe('application/json');
|
|
expect(JSON.parse(init.body)).toMatchObject({
|
|
mode: 'provider',
|
|
protocol: 'anthropic',
|
|
apiKey: 'sk-ant-test',
|
|
model: 'claude-sonnet-4-5',
|
|
});
|
|
});
|
|
|
|
it('synthesizes a kind=unknown envelope when the daemon returns 5xx', async () => {
|
|
globalThis.fetch = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({ error: { message: 'daemon down' } }, 500),
|
|
) as typeof fetch;
|
|
|
|
const result = await testApiProvider({
|
|
protocol: 'openai',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKey: 'sk',
|
|
model: 'gpt-4o',
|
|
});
|
|
|
|
expect(result.ok).toBe(false);
|
|
expect(result.kind).toBe('unknown');
|
|
expect(result.model).toBe('gpt-4o');
|
|
expect(result.detail).toContain('daemon down');
|
|
});
|
|
|
|
it('rethrows AbortError so the UI can suppress the resulting state update', async () => {
|
|
const controller = new AbortController();
|
|
globalThis.fetch = vi.fn(() => {
|
|
controller.abort();
|
|
return Promise.reject(
|
|
new DOMException('aborted', 'AbortError'),
|
|
);
|
|
}) as typeof fetch;
|
|
|
|
await expect(
|
|
testApiProvider(
|
|
{
|
|
protocol: 'openai',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKey: 'sk',
|
|
model: 'gpt-4o',
|
|
},
|
|
controller.signal,
|
|
),
|
|
).rejects.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('testAgent', () => {
|
|
it('POSTs to /api/test/connection with model and reasoning passthrough', async () => {
|
|
const fetchMock = vi
|
|
.fn()
|
|
.mockResolvedValueOnce(
|
|
jsonResponse({
|
|
ok: false,
|
|
kind: 'agent_not_installed',
|
|
latencyMs: 0,
|
|
agentName: 'Devin',
|
|
}),
|
|
);
|
|
globalThis.fetch = fetchMock as typeof fetch;
|
|
|
|
const result = await testAgent({
|
|
agentId: 'devin',
|
|
model: 'sonnet',
|
|
reasoning: 'high',
|
|
agentCliEnv: {
|
|
codex: { CODEX_HOME: '~/.codex-alt' },
|
|
},
|
|
});
|
|
|
|
expect(result.kind).toBe('agent_not_installed');
|
|
const [url, init] = fetchMock.mock.calls[0]!;
|
|
expect(url).toBe('/api/test/connection');
|
|
expect(JSON.parse(init.body)).toEqual({
|
|
mode: 'agent',
|
|
agentId: 'devin',
|
|
model: 'sonnet',
|
|
reasoning: 'high',
|
|
agentCliEnv: {
|
|
codex: { CODEX_HOME: '~/.codex-alt' },
|
|
},
|
|
});
|
|
});
|
|
});
|