import { EventEmitter } from 'node:events'; import type { IncomingMessage, ServerResponse } from 'http'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { handleAiProxyRoutes } from '@electron/api/routes/ai-proxy'; import { clearWorksSquareAIGatewayCredential, seedWorksSquareAIGatewayCredential, } from '@electron/services/works-square-ai-gateway'; import { clearWorksSquareSession, storeWorksSquareSession, } from '@electron/services/works-square-session'; const loggerWarnMock = vi.hoisted(() => vi.fn()); vi.mock('@electron/utils/logger', () => ({ logger: { debug: vi.fn(), error: vi.fn(), info: vi.fn(), warn: (...args: unknown[]) => loggerWarnMock(...args), }, })); function createRequest( method: string, body?: unknown, headers: Record = {}, ): IncomingMessage { const req = new EventEmitter(); const rawBody = body === undefined ? undefined : (typeof body === 'string' ? body : JSON.stringify(body)); Object.assign(req, { method, headers: { host: '127.0.0.1:13210', authorization: 'Bearer local-host-api-token', 'content-type': 'application/json', ...(rawBody ? { 'content-length': String(Buffer.byteLength(rawBody)) } : {}), ...headers, }, [Symbol.asyncIterator]: async function* () { if (rawBody !== undefined) { yield Buffer.from(rawBody); } }, }); return req as IncomingMessage; } function createResponse(options: { writeResults?: boolean[] } = {}) { const chunks: string[] = []; const headers = new Map(); const writeResults = [...(options.writeResults ?? [])]; const socket = { setNoDelay: vi.fn() }; const res = new EventEmitter(); Object.assign(res, { statusCode: 0, socket, flushHeaders: vi.fn(), setHeader: vi.fn((name: string, value: number | string | string[]) => { headers.set(name.toLowerCase(), value); }), write: vi.fn((chunk: string | Uint8Array) => { chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')); return writeResults.shift() ?? true; }), end: vi.fn((chunk?: string | Uint8Array) => { if (chunk !== undefined) { chunks.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8')); } }), }); return { res: res as unknown as ServerResponse, socket, get statusCode() { return (res as { statusCode: number }).statusCode; }, header: (name: string) => headers.get(name.toLowerCase()), body: () => chunks.join(''), }; } function streamFromText(...chunks: string[]): ReadableStream { const encoder = new TextEncoder(); return new ReadableStream({ start(controller) { for (const chunk of chunks) { controller.enqueue(encoder.encode(chunk)); } controller.close(); }, }); } async function flushMicrotasks(): Promise { for (let index = 0; index < 20; index += 1) { await Promise.resolve(); } } describe('ai proxy routes', () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date('2026-07-06T08:00:00.000Z')); loggerWarnMock.mockReset(); clearWorksSquareAIGatewayCredential(); clearWorksSquareSession(); }); afterEach(() => { vi.useRealTimers(); clearWorksSquareAIGatewayCredential(); clearWorksSquareSession(); vi.unstubAllGlobals(); }); it('forwards OpenAI-compatible JSON requests to one-api with a fresh Works Square gateway token', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ id: 'chatcmpl_1' }), { status: 200, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); const handled = await handleAiProxyRoutes( createRequest('POST', { model: 'deepseek-chat', messages: [] }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions?trace=1'), {} as never, ); expect(handled).toBe(true); expect(fetchMock).toHaveBeenCalledWith( 'https://one-api.example.com/v1/chat/completions?trace=1', expect.objectContaining({ method: 'POST', body: Buffer.from(JSON.stringify({ model: 'deepseek-chat', messages: [] })), headers: expect.objectContaining({ 'content-type': 'application/json', 'x-works-square-ai-token': 'ws-ai-token', }), }), ); const forwardedHeaders = fetchMock.mock.calls[0][1].headers as Record; expect(forwardedHeaders.authorization).toBeUndefined(); expect(forwardedHeaders.host).toBeUndefined(); expect(response.statusCode).toBe(200); expect(response.header('content-type')).toBe('application/json'); expect(response.body()).toBe(JSON.stringify({ id: 'chatcmpl_1' })); }); it('refreshes a near-expiry gateway credential before forwarding a long-running task request', async () => { storeWorksSquareSession({ accessToken: 'works-access-token', refreshToken: 'works-refresh-token', expiresAt: Date.now() + 120_000, }); seedWorksSquareAIGatewayCredential({ accessToken: 'near-expiry-ai-token', expiresIn: 30, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn() .mockResolvedValueOnce(new Response(JSON.stringify({ access_token: 'fresh-ai-token', expires_in: 1800, one_api_base_url: 'https://one-api.example.com/v1', }), { status: 200 })) .mockResolvedValueOnce(new Response(JSON.stringify({ id: 'chatcmpl_refresh' }), { status: 200, headers: { 'content-type': 'application/json' }, })); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'qwen3.7-max', messages: [] }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(fetchMock).toHaveBeenCalledTimes(2); expect(fetchMock.mock.calls[0][0]).toBe('https://square.nianxx.cn/api/ai-gateway/session'); expect(fetchMock.mock.calls[1][1].headers).toMatchObject({ 'x-works-square-ai-token': 'fresh-ai-token', }); expect(response.statusCode).toBe(200); expect(response.body()).toBe(JSON.stringify({ id: 'chatcmpl_refresh' })); }); it('refreshes the gateway credential and retries once when one-api rejects an expired gateway token', async () => { storeWorksSquareSession({ accessToken: 'works-access-token', refreshToken: 'works-refresh-token', expiresAt: Date.now() + 120_000, }); seedWorksSquareAIGatewayCredential({ accessToken: 'expired-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn() .mockResolvedValueOnce(new Response('AI access token expired', { status: 401 })) .mockResolvedValueOnce( new Response(JSON.stringify({ access_token: 'fresh-ai-token', expires_in: 1800, one_api_base_url: 'https://one-api.example.com/v1', }), { status: 200 }), ) .mockResolvedValueOnce(new Response(JSON.stringify({ id: 'chatcmpl_2' }), { status: 200 })); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'deepseek-chat' }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(fetchMock).toHaveBeenCalledTimes(3); expect(fetchMock.mock.calls[0][1].headers).toMatchObject({ 'x-works-square-ai-token': 'expired-ai-token', }); expect(fetchMock.mock.calls[1][0]).toBe('https://square.nianxx.cn/api/ai-gateway/session'); expect(fetchMock.mock.calls[2][1].headers).toMatchObject({ 'x-works-square-ai-token': 'fresh-ai-token', }); expect(response.statusCode).toBe(200); expect(response.body()).toBe(JSON.stringify({ id: 'chatcmpl_2' })); }); it('does not retry quota failures', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ error: 'user quota is not enough' }), { status: 429, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'deepseek-chat' }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(fetchMock).toHaveBeenCalledOnce(); expect(response.statusCode).toBe(402); expect(response.body()).toBe(JSON.stringify({ error: 'user quota is not enough' })); }); it('maps rolling-window quota exhaustion to a non-retryable response status', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ error: { message: 'Token quota exhausted for rolling 5-hour window', code: 'works_square_gateway_authorize_failed', type: 'one_api_error', }, }), { status: 429, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'qwen3.6-plus', messages: [] }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(fetchMock).toHaveBeenCalledOnce(); expect(response.statusCode).toBe(402); expect(response.body()).toContain('Token quota exhausted for rolling 5-hour window'); }); it('preserves the upstream status for a generic gateway authorization failure', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ error: { message: 'works_square_gateway_authorize_failed', code: 'works_square_gateway_authorize_failed', type: 'one_api_error', }, }), { status: 429, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'qwen3.6-plus', messages: [] }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(fetchMock).toHaveBeenCalledOnce(); expect(response.statusCode).toBe(429); expect(response.body()).toContain('works_square_gateway_authorize_failed'); }); it('logs a sanitized one-api error summary when the upstream group is saturated', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ error: { message: '当前分组上游负载已饱和,请稍后再试 (request id: req-saturated)', code: 'rate_limit_exceeded', type: 'one_api_error', }, }), { status: 429, headers: { 'content-type': 'application/json' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'qwen3.7-max', messages: [{ role: 'user', content: 'secret prompt' }] }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(response.statusCode).toBe(429); expect(response.body()).toContain('当前分组上游负载已饱和'); expect(loggerWarnMock).toHaveBeenCalledWith( '[ai-proxy] One-api returned non-success response', expect.objectContaining({ status: 429, url: 'https://one-api.example.com/v1/chat/completions', code: 'rate_limit_exceeded', type: 'one_api_error', message: '当前分组上游负载已饱和,请稍后再试 (request id: req-saturated)', }), ); expect(JSON.stringify(loggerWarnMock.mock.calls)).not.toContain('secret prompt'); expect(JSON.stringify(loggerWarnMock.mock.calls)).not.toContain('ws-ai-token'); }); it('strips decoded compression headers from proxied responses', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(JSON.stringify({ id: 'chatcmpl_br' }), { status: 200, headers: { 'content-encoding': 'br', 'content-length': '999', 'content-type': 'application/json', }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { model: 'deepseek-chat' }, { 'accept-encoding': 'br, gzip' }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); const forwardedHeaders = fetchMock.mock.calls[0][1].headers as Record; expect(forwardedHeaders['accept-encoding']).toBeUndefined(); expect(response.statusCode).toBe(200); expect(response.header('content-type')).toBe('application/json'); expect(response.header('content-encoding')).toBeUndefined(); expect(response.header('content-length')).toBeUndefined(); expect(response.body()).toBe(JSON.stringify({ id: 'chatcmpl_br' })); }); it('passes streaming chunks through to the local client', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(streamFromText('data: one\n\n', 'data: two\n\n'), { status: 200, headers: { 'content-type': 'text/event-stream' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse(); await handleAiProxyRoutes( createRequest('POST', { stream: true }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ); expect(response.statusCode).toBe(200); expect(response.header('content-type')).toBe('text/event-stream'); expect(response.body()).toBe('data: one\n\ndata: two\n\n'); }); it('flushes streaming headers and waits for downstream drain before reading more chunks', async () => { seedWorksSquareAIGatewayCredential({ accessToken: 'ws-ai-token', expiresIn: 3600, oneApiBaseUrl: 'https://one-api.example.com/v1', }); const fetchMock = vi.fn().mockResolvedValueOnce( new Response(streamFromText('data: one\n\n', 'data: two\n\n'), { status: 200, headers: { 'content-type': 'text/event-stream' }, }), ); vi.stubGlobal('fetch', fetchMock); const response = createResponse({ writeResults: [false, true] }); let settled = false; const handledPromise = handleAiProxyRoutes( createRequest('POST', { stream: true }), response.res, new URL('http://127.0.0.1:13210/api/ai-proxy/v1/chat/completions'), {} as never, ).finally(() => { settled = true; }); await flushMicrotasks(); expect(response.res.flushHeaders).toHaveBeenCalledOnce(); expect(response.socket.setNoDelay).toHaveBeenCalledWith(true); expect(response.body()).toBe('data: one\n\n'); expect(settled).toBe(false); response.res.emit('drain'); await handledPromise; expect(settled).toBe(true); expect(response.body()).toBe('data: one\n\ndata: two\n\n'); }); });