Files
Genarrative/src/services/llmClient.test.ts
T
kdletters d7d98acfd4 接入VectorEngine画布Agent规划
切换画布 Agent 与通用 LLM 代理到 VectorEngine gpt-5.4-mini。

为画布 Agent 注入上一轮生成结果并默认承接上一张图编辑。

补齐规范图工具分流、规范展板 prompt 要求和 JSON 残片回复兜底。

同步前后端契约、测试脚本、环境示例和项目文档。
2026-07-05 17:42:27 +08:00

112 lines
3.1 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import {
requestPlainTextCompletion,
streamPlainTextCompletion,
} from './llmClient';
function createSseResponse(body: string) {
const encoder = new TextEncoder();
const stream = new ReadableStream<Uint8Array>({
start(controller) {
controller.enqueue(encoder.encode(body));
controller.close();
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream; charset=utf-8',
},
});
}
describe('llmClient streamPlainTextCompletion', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('reads OpenAI compatible SSE through the shared stream reader', async () => {
const onUpdate = vi.fn();
const fetchMock = vi.fn().mockResolvedValue(
createSseResponse(
[
'data: {"choices":[{"delta":{"content":"溪上"}}]}\r\n\r\n',
'data: not-json\r\n\r\n',
'data: {"choices":[{"delta":{"content":"春风"}}]}\r\n\r\n',
'data: [DONE]\r\n\r\n',
'data: {"choices":[{"delta":{"content":"不应读取"}}]}\r\n\r\n',
].join(''),
),
);
vi.stubGlobal('fetch', fetchMock);
const result = await streamPlainTextCompletion('system', 'user', {
onUpdate,
});
expect(result).toBe('溪上春风');
expect(onUpdate).toHaveBeenNthCalledWith(1, '溪上');
expect(onUpdate).toHaveBeenNthCalledWith(2, '溪上春风');
expect(onUpdate).toHaveBeenCalledTimes(2);
});
it('reads api-server SSE delta events', async () => {
const onUpdate = vi.fn();
const fetchMock = vi.fn().mockResolvedValue(
createSseResponse(
[
'event: delta\r\n',
'data: {"delta":"你","content":"你","finishReason":null}\r\n\r\n',
'event: delta\r\n',
'data: {"delta":"好","content":"你好","finishReason":null}\r\n\r\n',
'event: complete\r\n',
'data: {"id":"resp_01","model":"gpt-5.4-mini","content":"你好","finishReason":"stop"}\r\n\r\n',
'data: [DONE]\r\n\r\n',
].join(''),
),
);
vi.stubGlobal('fetch', fetchMock);
const result = await streamPlainTextCompletion('system', 'user', {
onUpdate,
});
expect(result).toBe('你好');
expect(onUpdate).toHaveBeenNthCalledWith(1, '你');
expect(onUpdate).toHaveBeenNthCalledWith(2, '你好');
expect(onUpdate).toHaveBeenCalledTimes(2);
});
});
describe('llmClient requestPlainTextCompletion', () => {
afterEach(() => {
vi.unstubAllGlobals();
vi.restoreAllMocks();
});
it('reads api-server response envelope content', async () => {
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
ok: true,
data: {
id: 'resp_01',
model: 'gpt-5.4-mini',
content: '代理成功',
finishReason: 'stop',
},
error: null,
meta: {requestId: 'req_01'},
}),
),
);
vi.stubGlobal('fetch', fetchMock);
const result = await requestPlainTextCompletion('system', 'user');
expect(result).toBe('代理成功');
});
});