Files
PlantAI-rag-new/tests/frontend/unit/b2/parser.test.js
jerryandClaude Fable 5 0875bb036c P0/M1: 四部分重构——frontend→ui、backend→sdk 改名 + 引用修复 + 骨架
- git mv 保留历史:ui/(Vue3+Vite)、sdk/(BFF 中间层)
- 路径语义修正:db_path/figures_cache 改相对 sdk/(BFF cwd),
  历史库 sdk/backend/data 迁移至 sdk/data/(会话/图片缓存无损)
- 引用修复 ~15 处:config.py、deploy/*.sh、vitest/playwright 配置、
  单测导入路径、docs、README、gitignore、.env.example
- 骨架:agent/{qa,methodology,legacy-rag,vendor}、knowledge/manifest.yaml、
  tooling/{compile,kb,crypto,assemble,audit,tools,scripts} + README
- 门禁验证:sdk pytest 125 通过、ui vitest 158 通过、
  生产 BFF 脚本重启四上游全绿 + 静态托管 + 会话库无损

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 12:56:47 +08:00

124 lines
4.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// @vitest-environment node
// sse/parser.test.js —— SSE 解析器单测(多行 data / 注释帧 / 无 event 名 / CRLF / 分块 UTF-8)
import { describe, it, expect, vi } from 'vitest';
import { parseFrame, SseParser, consumeSseStream } from '../../../../ui/src/sse/parser.js';
describe('parseFrame', () => {
it('解析单行 data 与 event 名', () => {
const ev = parseFrame('event: progress\ndata: {"phase":"planning"}\n');
expect(ev.event).toBe('progress');
expect(ev.data).toEqual({ phase: 'planning' });
});
it('多行 data 按 \\n 合并后 JSON 解析', () => {
const ev = parseFrame('data: {"a":1,\ndata: "b":2}\n');
expect(ev.data).toEqual({ a: 1, b: 2 });
expect(ev.dataStr).toBe('{"a":1,\n"b":2}');
});
it('注释帧(keepalive)返回 null', () => {
expect(parseFrame(': ping\n')).toBeNull();
expect(parseFrame(': 任何注释\n: 再来一行\n')).toBeNull();
});
it('无 event 名时默认 message', () => {
const ev = parseFrame('data: {"x":1}\n');
expect(ev.event).toBe('message');
expect(ev.data).toEqual({ x: 1 });
});
it('CRLF 行尾与空行容忍', () => {
const ev = parseFrame('event: complete\r\ndata: {"ok":true}\r\n');
expect(ev.event).toBe('complete');
expect(ev.data).toEqual({ ok: true });
});
it('非 JSON data 保留原字符串', () => {
const ev = parseFrame('data: 纯文本\n');
expect(ev.data).toBe('纯文本');
expect(ev.dataStr).toBe('纯文本');
});
it('无 data 行返回 null;冒号后仅去一个空格', () => {
expect(parseFrame('event: progress\n')).toBeNull();
const ev = parseFrame('data: 保留多余空格\n');
expect(ev.dataStr).toBe(' 保留多余空格');
});
});
describe('SseParser(增量)', () => {
it('跨 chunk 截断的完整帧', () => {
const got = [];
const p = new SseParser({ onEvent: (ev) => got.push(ev) });
p.feed('event: progress\nda');
p.feed('ta: {"phase":"planning"}\n\n');
p.end();
expect(got).toHaveLength(1);
expect(got[0].event).toBe('progress');
expect(got[0].data).toEqual({ phase: 'planning' });
});
it('多字节 UTF-8 字符跨 chunk 不丢字', () => {
const got = [];
const p = new SseParser({ onEvent: (ev) => got.push(ev) });
const text = JSON.stringify({ label: '证据审核完成' });
const bytes = new TextEncoder().encode(`data: ${text}\n\n`);
p.feed(bytes.slice(0, 5));
p.feed(bytes.slice(5));
p.end();
expect(got[0].data.label).toBe('证据审核完成');
});
it('注释帧被忽略、CRLF 帧正常', () => {
const got = [];
const p = new SseParser({ onEvent: (ev) => got.push(ev) });
p.feed(': ping\n\n');
p.feed('event: timeout\r\ndata: {"message":"检索超时"}\r\n\r\n');
p.end();
expect(got).toHaveLength(1);
expect(got[0].event).toBe('timeout');
expect(got[0].data.message).toBe('检索超时');
});
it('一 chunk 多帧', () => {
const got = [];
const p = new SseParser({ onEvent: (ev) => got.push(ev) });
p.feed('data: {"a":1}\n\ndata: {"b":2}\n\n: ping\n\n');
p.end();
expect(got.map((e) => e.data)).toEqual([{ a: 1 }, { b: 2 }]);
});
});
describe('consumeSseStream', () => {
const chunkStream = (chunks) => {
const enc = new TextEncoder();
let i = 0;
return {
body: {
getReader() {
return {
read: vi.fn(async () => (i < chunks.length ? { done: false, value: enc.encode(chunks[i++]) } : { done: true })),
releaseLock: vi.fn(),
};
},
},
};
};
it('消费完整流并逐帧回调', async () => {
const got = [];
const resp = chunkStream([
'event: progress\ndata: {"phase":"planning"}\n\n',
': ping\n\n',
'event: complete\ndata: {"answer":"好"}\n\n',
]);
await consumeSseStream(resp, (ev) => got.push(ev));
expect(got.map((e) => e.event)).toEqual(['progress', 'complete']);
expect(got[1].data.answer).toBe('好');
});
it('非流响应抛错', async () => {
await expect(consumeSseStream({ body: null }, () => {})).rejects.toThrow(/readable stream/);
});
});