import assert from 'node:assert/strict'; import type { AddressInfo } from 'node:net'; import test from 'node:test'; import express from 'express'; import { FmodeAiClient, type AiGatewayFetch } from '../src/modules/ai-gateway/client.js'; import { createAiGatewayRouter } from '../src/modules/ai-gateway/routes.js'; import type { AiPromptConfigRecord, AiPromptConfigStore } from '../src/modules/ai-gateway/prompt-config.repository.js'; const config = { baseUrl: 'https://api.example.test/', token: 'server-only-token', defaultModel: 'deepseek-v4-pro', timeoutMs: 5_000, }; async function listen(fetchImpl: AiGatewayFetch, token = config.token, promptConfigs?: AiPromptConfigStore) { const app = express(); app.use(express.json()); app.use('/api/ai', createAiGatewayRouter(new FmodeAiClient({ ...config, token }, fetchImpl), promptConfigs)); const server = await new Promise>((resolve) => { const listening = app.listen(0, '127.0.0.1', () => resolve(listening)); }); const address = server.address() as AddressInfo; return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())), }; } test('AI gateway reports only public configuration and keeps the token server-side', async () => { const server = await listen(async () => new Response('{}')); try { const response = await fetch(`${server.baseUrl}/api/ai/status`); assert.equal(response.status, 200); const status = await response.json() as Record; assert.deepEqual(status, { service: 'Fmode AI', configured: true, baseUrl: 'https://api.example.test', defaultModel: 'deepseek-v4-pro', proxyEndpoint: 'ai.chat', }); assert.equal(JSON.stringify(status).includes(config.token), false); } finally { await server.close(); } }); test('AI prompt configuration is read and saved through the backend store', async () => { const records = new Map([ ['shared.analysisPanel.defaultSystem', { promptKey: 'shared.analysisPanel.defaultSystem', name: '默认分析提示词', scope: 'analysis', template: '仅使用真实数据', }], ]); const promptConfigs: AiPromptConfigStore = { async list() { return [...records.values()]; }, async upsert(promptKey, value) { const saved = { ...value, promptKey }; records.set(promptKey, saved); return saved; }, }; const server = await listen(async () => new Response('{}'), config.token, promptConfigs); try { const listResponse = await fetch(`${server.baseUrl}/api/ai/prompts`); assert.equal(listResponse.status, 200); const list = await listResponse.json() as { items: AiPromptConfigRecord[] }; assert.equal(list.items[0]?.template, '仅使用真实数据'); const promptKey = 'shared.analysisPanel.defaultSystem'; const saveResponse = await fetch(`${server.baseUrl}/api/ai/prompts/${promptKey}`, { method: 'PUT', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ promptKey, name: '默认分析提示词', scope: 'analysis', template: '只输出可追溯结论', }), }); assert.equal(saveResponse.status, 200); assert.equal(records.get(promptKey)?.template, '只输出可追溯结论'); } finally { await server.close(); } }); test('AI gateway validates requests and injects upstream authorization', async () => { let upstreamUrl = ''; let upstreamAuthorization = ''; let upstreamBody: Record = {}; const fetchImpl: AiGatewayFetch = async (input, init) => { upstreamUrl = String(input); upstreamAuthorization = new Headers(init?.headers).get('authorization') || ''; upstreamBody = JSON.parse(String(init?.body || '{}')) as Record; return new Response(JSON.stringify({ model: upstreamBody['model'], choices: [{ message: { role: 'assistant', content: '分析完成' }, finish_reason: 'stop' }], }), { status: 200, headers: { 'content-type': 'application/json' } }); }; const server = await listen(fetchImpl); try { const invalid = await fetch(`${server.baseUrl}/api/ai/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'test' }], token: 'browser-token' }), }); assert.equal(invalid.status, 400); const response = await fetch(`${server.baseUrl}/api/ai/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: '分析真实评论' }], stream: false }), }); assert.equal(response.status, 200); assert.equal(upstreamUrl, 'https://api.example.test/v1/chat/completions'); assert.equal(upstreamAuthorization, 'Bearer server-only-token'); assert.equal(upstreamBody['model'], 'deepseek-v4-pro'); assert.equal(Object.prototype.hasOwnProperty.call(upstreamBody, 'token'), false); } finally { await server.close(); } }); test('AI gateway streams SSE responses without buffering', async () => { const fetchImpl: AiGatewayFetch = async () => new Response( 'data: {"choices":[{"delta":{"content":"洞察"}}]}\n\ndata: [DONE]\n\n', { status: 200, headers: { 'content-type': 'text/event-stream' } }, ); const server = await listen(fetchImpl); try { const response = await fetch(`${server.baseUrl}/api/ai/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'test' }], stream: true }), }); assert.equal(response.status, 200); assert.match(response.headers.get('content-type') || '', /text\/event-stream/); assert.match(await response.text(), /洞察/); } finally { await server.close(); } }); test('AI gateway returns a controlled error when the server token is missing', async () => { const server = await listen(async () => new Response('{}'), ''); try { const response = await fetch(`${server.baseUrl}/api/ai/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'test' }], stream: false }), }); assert.equal(response.status, 503); assert.equal((await response.json() as any).error.message, 'AI 服务尚未配置,请联系管理员'); } finally { await server.close(); } }); test('AI gateway maps upstream transport failures without exposing internals', async () => { const server = await listen(async () => { throw new TypeError('socket closed while connecting to upstream'); }); try { const response = await fetch(`${server.baseUrl}/api/ai/chat/completions`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ messages: [{ role: 'user', content: 'test' }], stream: false }), }); assert.equal(response.status, 502); const payload = await response.json() as { error: { message: string } }; assert.equal(payload.error.message, 'AI 上游连接失败,请稍后重试'); assert.equal(JSON.stringify(payload).includes(config.token), false); assert.equal(JSON.stringify(payload).includes('socket closed'), false); } finally { await server.close(); } });