d7d98acfd4
切换画布 Agent 与通用 LLM 代理到 VectorEngine gpt-5.4-mini。 为画布 Agent 注入上一轮生成结果并默认承接上一张图编辑。 补齐规范图工具分流、规范展板 prompt 要求和 JSON 残片回复兜底。 同步前后端契约、测试脚本、环境示例和项目文档。
112 lines
3.1 KiB
TypeScript
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('代理成功');
|
|
});
|
|
});
|