YAOTU INSIGHTS

给流式解析器做属性测试:一份字节输入应只有一种事件序列

给流式解析器做属性测试:一份字节输入应只有一种事件序列
中文字符被拆在两个网络分片里解析器不能因此改变文字一个 SSE 事件分三次到达也不能被当成三个事件。对流式解析器最值得先守住的性质是相同字节输入无论怎样分片完整事件序列都一致。这和 UI 是否保持焦点、业务卡片是否允许用户编辑是不同层的测试。这里只检查字节、UTF-8、SSE 子集与 JSON 事件不接模型不画“流畅度”成绩。先限制协议再写测试下面实现的是严格教学子集UTF-8、LF 或 CRLF 行结束仅支持 id、data 与注释行每个有数据的完整帧要求非空 IDdata 合并后是 JSON。它不实现 SSE 的 retry、event、单独 CR 行结束等完整语义不能直接替代通用 EventSource 解析器。半个完整 UTF-8 文本帧只标记 incomplete不作为事件发出非法 UTF-8 报错。重复帧仍保留在解析输出去重属于业务应用层。单帧与输入总量都有上限。import assert from node:assert/strict; const encoder new TextEncoder(); function parse(chunks, maxFrameBytes 1024, maxTotalBytes 8192) { const decoder new TextDecoder(utf-8, { fatal: true }); let pending , id , data [], frameBytes 0, total 0; const events []; function feed(text) { pending text; let nl; while ((nl pending.indexOf(\n)) 0) { const raw pending.slice(0, nl); pending pending.slice(nl 1); frameBytes encoder.encode(raw \n).length; if (frameBytes maxFrameBytes) throw new Error(frame-limit); const line raw.endsWith(\r) ? raw.slice(0, -1) : raw; if (!line) { if (data.length) { if (!/^[A-Za-z0-9_-]{1,64}$/.test(id)) throw new Error(invalid-id); events.push({ id, data: JSON.parse(data.join(\n)) }); } id ; data []; frameBytes 0; } else if (!line.startsWith(:)) { const colon line.indexOf(:); if (colon 0) throw new Error(unsupported-field); const field line.slice(0, colon); let value line.slice(colon 1); if (value.startsWith( )) value value.slice(1); if (field id) id value; else if (field data) data.push(value); else throw new Error(unsupported-field); } } if (frameBytes encoder.encode(pending).length maxFrameBytes) throw new Error(frame-limit); } for (const chunk of chunks) { total chunk.byteLength; if (total maxTotalBytes) throw new Error(total-limit); feed(decoder.decode(chunk, { stream: true })); } feed(decoder.decode()); return { events, incomplete: pending.length 0 || frameBytes 0 }; } const wire encoder.encode( id: 1\r\ndata: {type:text,value:你好}\r\n\r\n id: 2\ndata: {type:finish}\n\n); const expected parse([wire]); assert.equal(expected.events.length, 2); assert.equal(expected.incomplete, false); for (let cut 0; cut wire.length; cut) assert.deepEqual(parse([wire.slice(0, cut), wire.slice(cut)]), expected); assert.deepEqual(parse([...wire].map(x Uint8Array.of(x))), expected); for (let seed 1; seed 32; seed) { let state seed, offset 0; const chunks []; while (offset wire.length) { state (Math.imul(state, 1664525) 1013904223) 0; const end Math.min(wire.length, offset 1 state % 13); chunks.push(wire.slice(offset, end)); offset end; } assert.deepEqual(parse(chunks), expected, seed seed); } assert.equal(parse([wire.slice(0, -1)]).incomplete, true); assert.equal(parse([wire.slice(0, -1)]).events.length, 1); assert.throws(() parse([Uint8Array.of(255)])); assert.throws(() parse([wire], 10), /frame-limit/); assert.throws(() parse([wire], 1024, 10), /total-limit/); assert.equal(parse([wire, wire]).events.length, 4); console.log(all cuts, byte chunks, 32 seeds, truncation and limits passed);保存为parser-properties.mjs运行node parser-properties.mjs。这是一组确定性性质检查枚举单切点、逐字节输入和 32 个固定随机种子。它不是对所有可能字节串的形式化证明。图示的是通用分层关系代码仅实现前述严格子集图中 event 等通用字段不在本例支持范围。失败后要保留最小字节输入记录种子、原始字节、分片边界、参数和期望输出。缩小反例时先删无关事件再缩小数据与切片不要只保留一张控制台截图。UTF-8 截断、JSON 无效和缺业务终态应分开报告避免把所有问题都归成“网络失败”。本例返回完整帧不等于整条业务回复完成。应用仍要检查 finish 或明确错误终态半帧断线不得被当成成功。全量输出也受总字节限制约束生产实现需要按事件消费并限制历史存储避免无限累积。本轮在 Node v26.3.0 执行了所示测试没有使用真实模型流、浏览器网络故障或性能压测。内存和时间只受示例输入上限约束尚未建立生产级资源基准不能从这次测试宣称“零丢字”适用于所有协议。参考资料AI SDK 流式协议滚动文档本例并非其完整实现Streams 概念滚动文档。采用前应按实际协议扩展语法并重新跑保存的失败集。