P1-6 useStyleLearn/useKimiOauth 修 stale-closure 依赖。 P1-7 CommandPalette/Drawer 加 focus trap(新 lib/a11y/focusTrap)。 P1-8 SSE 解析与 server.ts 去 as 强转改类型守卫/运行时校验。 P2 删冗余强转、开 noUncheckedIndexedAccess、Toast 清理+稳定 key、列表 key 稳定化、 rel=noopener、useMemo 大文本、ConflictCard role=group、useInjection 加锁、 client.test 真断言。 codegen 重生成 schema.d.ts + 消费 DimensionEntry/ReviewConflictView 强类型。
115 lines
3.7 KiB
TypeScript
115 lines
3.7 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
|
||
import {
|
||
SseFrameBuffer,
|
||
initialStreamState,
|
||
parseSseBlock,
|
||
reduceStream,
|
||
type SseEvent,
|
||
} from "./sse";
|
||
|
||
describe("parseSseBlock", () => {
|
||
it("parses a token frame", () => {
|
||
const evt = parseSseBlock('event:token\ndata:{"text":"夜"}');
|
||
expect(evt).toEqual({ event: "token", data: { text: "夜" } });
|
||
});
|
||
|
||
it("parses a done frame", () => {
|
||
const evt = parseSseBlock('event:done\ndata:{"length":12}');
|
||
expect(evt).toEqual({ event: "done", data: { length: 12 } });
|
||
});
|
||
|
||
it("parses an error frame with request_id", () => {
|
||
const evt = parseSseBlock(
|
||
'event:error\ndata:{"code":"INTERNAL","message":"boom","request_id":"r1"}',
|
||
);
|
||
expect(evt).toEqual({
|
||
event: "error",
|
||
data: { code: "INTERNAL", message: "boom", request_id: "r1" },
|
||
});
|
||
});
|
||
|
||
it("ignores comment/heartbeat lines and tolerates leading space after colon", () => {
|
||
const evt = parseSseBlock(': keep-alive\nevent: token\ndata: {"text":"x"}');
|
||
expect(evt).toEqual({ event: "token", data: { text: "x" } });
|
||
});
|
||
|
||
it("returns null for unknown event types", () => {
|
||
expect(parseSseBlock('event:section\ndata:{"x":1}')).toBeNull();
|
||
});
|
||
|
||
it("returns null for malformed json", () => {
|
||
expect(parseSseBlock("event:token\ndata:{not json")).toBeNull();
|
||
});
|
||
|
||
// P1-8:逐事件类型守卫——已知事件但 data 形状不符也拒绝(不再强转放行)。
|
||
it("returns null when token frame lacks string text", () => {
|
||
expect(parseSseBlock('event:token\ndata:{"text":42}')).toBeNull();
|
||
});
|
||
|
||
it("returns null when error frame lacks code/message", () => {
|
||
expect(parseSseBlock('event:error\ndata:{"code":"X"}')).toBeNull();
|
||
});
|
||
|
||
it("coerces missing request_id to null on error frame", () => {
|
||
expect(
|
||
parseSseBlock('event:error\ndata:{"code":"X","message":"m"}'),
|
||
).toEqual({
|
||
event: "error",
|
||
data: { code: "X", message: "m", request_id: null },
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("SseFrameBuffer", () => {
|
||
it("emits complete blocks split by blank line and buffers the tail", () => {
|
||
const buf = new SseFrameBuffer();
|
||
const first = buf.push('event:token\ndata:{"text":"a"}\n\nevent:tok');
|
||
expect(first).toEqual([{ event: "token", data: { text: "a" } }]);
|
||
const second = buf.push('en\ndata:{"text":"b"}\n\n');
|
||
expect(second).toEqual([{ event: "token", data: { text: "b" } }]);
|
||
});
|
||
|
||
it("handles CRLF boundaries", () => {
|
||
const buf = new SseFrameBuffer();
|
||
const out = buf.push('event:token\r\ndata:{"text":"z"}\r\n\r\n');
|
||
expect(out).toEqual([{ event: "token", data: { text: "z" } }]);
|
||
});
|
||
});
|
||
|
||
describe("reduceStream", () => {
|
||
it("accumulates token text and marks streaming", () => {
|
||
const tokens: SseEvent[] = [
|
||
{ event: "token", data: { text: "夜色" } },
|
||
{ event: "token", data: { text: "如墨" } },
|
||
];
|
||
const state = tokens.reduce(reduceStream, initialStreamState);
|
||
expect(state.text).toBe("夜色如墨");
|
||
expect(state.phase).toBe("streaming");
|
||
});
|
||
|
||
it("marks done on done event preserving text", () => {
|
||
let s = reduceStream(initialStreamState, {
|
||
event: "token",
|
||
data: { text: "abc" },
|
||
});
|
||
s = reduceStream(s, { event: "done", data: { length: 3 } });
|
||
expect(s.phase).toBe("done");
|
||
expect(s.text).toBe("abc");
|
||
});
|
||
|
||
it("captures error and retains partial text", () => {
|
||
let s = reduceStream(initialStreamState, {
|
||
event: "token",
|
||
data: { text: "partial" },
|
||
});
|
||
s = reduceStream(s, {
|
||
event: "error",
|
||
data: { code: "LLM_UNAVAILABLE", message: "no key" },
|
||
});
|
||
expect(s.phase).toBe("error");
|
||
expect(s.text).toBe("partial");
|
||
expect(s.error?.code).toBe("LLM_UNAVAILABLE");
|
||
});
|
||
});
|