import { describe, expect, it } from 'vitest'; import { parseSseChunk, parseSseEvent } from './task-sse'; const TASK_ID = '01JCNB2M8K4Q7T3F9V6W1X0Y5Z'; describe('SSE 帧解析', () => { it('按空行切分多帧,处理心跳注释与 retry 字段', () => { const chunk = [ ': ping', '', `id: 7\nevent: progress\ndata: {"taskId":"${TASK_ID}","status":"running","progress":25,"itemTotal":8,"itemSucceeded":2,"itemFailed":0,"itemSkipped":0}`, '', '', ].join('\n'); const parsed = parseSseChunk(chunk); expect(parsed.frames).toHaveLength(1); expect(parsed.frames[0]?.id).toBe('7'); expect(parsed.frames[0]?.event).toBe('progress'); expect(parsed.rest).toBe(''); }); it('未收完的尾帧留在 rest,下一 chunk 拼接后解析', () => { const first = parseSseChunk('id: 1\nevent: progress\ndata: {"a":'); expect(first.frames).toHaveLength(0); expect(first.rest).toContain('"a":'); const second = parseSseChunk(`${first.rest}1}\n\n`); expect(second.frames).toHaveLength(1); }); it('多行 data 用换行拼接,未知字段忽略,兼容 CRLF', () => { const chunk = 'id: 2\r\nevent: item\r\ndata: {"taskId":"a",\r\ndata: "seq":1}\r\n\r\n'; const parsed = parseSseChunk(chunk); expect(parsed.frames[0]?.data).toBe(`{"taskId":"a",\n"seq":1}`); }); }); describe('SSE 事件契约解析', () => { it('合法帧 → 契约事件;非法 JSON / 未知 event / 缺 id 返回 null', () => { const valid = parseSseEvent({ id: '3', event: 'done', data: `{"taskId":"${TASK_ID}","status":"pending_review","progress":100}`, }); expect(valid?.event).toBe('done'); expect(parseSseEvent({ id: '4', event: 'progress', data: '{oops' })).toBeNull(); expect(parseSseEvent({ id: '5', event: 'unknown', data: '{}' })).toBeNull(); expect(parseSseEvent({ event: 'done', data: '{}' })).toBeNull(); }); });