const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const ts = require('typescript'); const root = path.resolve(__dirname, '..'); function load(relative, overrides = {}) { const compiled = ts.transpileModule(fs.readFileSync(path.join(root,relative),'utf8'),{compilerOptions:{module:ts.ModuleKind.CommonJS,target:ts.ScriptTarget.ES2020}}).outputText; const exports = {}; vm.runInNewContext(compiled,{exports,require:id=>overrides[id]||require(id),process,fetch:global.fetch,Headers,Response,AbortController,TextDecoder,TextEncoder,setTimeout,clearTimeout,URL}); return exports; } const {ChatEventDecoder,readChatStream} = load('src/lib/chat-stream.ts'); const encode = x => new TextEncoder().encode(x); const event = (kind,data) => `event: ${kind}\r\ndata: ${JSON.stringify(data)}\r\n\r\n`; async function collect(iterator) {const result=[];for await(const item of iterator) result.push(item);return result;} function responder(chunks) {return async()=>new Response(new ReadableStream({start(controller){for(const chunk of chunks)controller.enqueue(encode(chunk));controller.close();}}),{headers:{'Content-Type':'text/event-stream'}});} (async()=>{ const original=event('token',{content:'你好'})+event('done',{content:'你好'}); for(let split=0;split<=original.length;split++) { const parser=new ChatEventDecoder(); const items=[...parser.push(original.slice(0,split)),...parser.push(original.slice(split))]; assert.equal(items.length,2);assert.equal(items[0].event,'token');assert.equal(items[0].data.content,'你好'); } const parser=new ChatEventDecoder(); const items=[];for(const char of original)items.push(...parser.push(char));assert.equal(items.length,2); assert.equal(new ChatEventDecoder().push('event: token\ndata: {"content":\ndata: "multiline"}\n\n')[0].data.content,'multiline'); assert.equal(new ChatEventDecoder().push(': heartbeat\r\n\r\n').length,0); assert.throws(()=>new ChatEventDecoder().push('event: token\ndata: invalid\n\n'),/格式/); const normal=await collect(readChatStream('/test',{}, {fetcher:responder([original.slice(0,8),original.slice(8)])})); assert.equal(normal[0].event,'token');assert.equal(normal[1].event,'done'); const finalOnly=await collect(readChatStream('/test',{}, {fetcher:responder([event('done',{content:'final'})])})); assert.equal(finalOnly[0].event,'token');assert.equal(finalOnly[0].data.content,'final'); await assert.rejects(()=>collect(readChatStream('/test',{}, {fetcher:responder([event('done',{content:''})])})),/正文/); await assert.rejects(()=>collect(readChatStream('/test',{}, {fetcher:responder([event('token',{content:'partial'})])})),/中断/); const error=await collect(readChatStream('/test',{}, {fetcher:responder([event('error',{error:'model failed'})])}));assert.equal(error[0].event,'error'); let aborted=false; await assert.rejects(()=>collect(readChatStream('/test',{}, {idleMs:10,totalMs:100,fetcher:async(_url,{signal})=>new Promise((_resolve,reject)=>signal.addEventListener('abort',()=>{aborted=true;reject(new Error('aborted'));}))})),/超时/); assert.equal(aborted,true); await assert.rejects(()=>collect(readChatStream('/test',{}, {fetcher:async()=>new Response('bad',{status:502})})),/502/); await assert.rejects(()=>collect(readChatStream('/test',{}, {fetcher:async()=>new Response('{}',{headers:{'Content-Type':'application/json'}})})),/事件流/); const source=fs.readFileSync(path.join(root,'src/lib/api.ts'),'utf8');assert.ok(source.includes('const API_BASE = "/api/core"'));assert.ok(!source.includes(':8002')); const NextResponse = class extends Response {static json(data,init){return new Response(JSON.stringify(data),{...init,headers:{'Content-Type':'application/json'}});}}; const oldFetch=global.fetch, oldBase=process.env.RISK_AGENT_BACKEND_URL, oldCore=process.env.CORE_BACKEND_URL; try { delete process.env.CORE_BACKEND_URL;process.env.RISK_AGENT_BACKEND_URL='http://127.0.0.1:8003'; let target, options; const upstream=new Response(new ReadableStream({start(c){c.enqueue(encode(event('token',{content:'stream'})));}}),{headers:{'Content-Type':'text/event-stream'}}); global.fetch=async(url,opts)=>{target=url;options=opts;return upstream;}; const route=load('src/app/api/core/[...path]/route.ts',{'next/server':{NextResponse}}); const request={nextUrl:{hostname:'127.0.0.1',search:'?id=abc'},headers:new Headers(),method:'POST',arrayBuffer:async()=>encode('{}'),signal:new AbortController().signal}; const response=await route.POST(request,{params:{path:['chat']}}); assert.equal(target,'http://127.0.0.1:8003/api/chat?id=abc');assert.equal(response.body,upstream.body);assert.equal(options.signal,request.signal); const reader=response.body.getReader();assert.ok((await reader.read()).value.length);await reader.cancel(); assert.equal((await route.GET({...request,nextUrl:{hostname:'example.org'}},{params:{path:['chat']}})).status,403); assert.equal((await route.GET(request,{params:{path:['chat','..']}})).status,404); assert.equal((await route.GET(request,{params:{path:['unknown']}})).status,404); } finally {global.fetch=oldFetch;if(oldBase===undefined)delete process.env.RISK_AGENT_BACKEND_URL;else process.env.RISK_AGENT_BACKEND_URL=oldBase;if(oldCore===undefined)delete process.env.CORE_BACKEND_URL;else process.env.CORE_BACKEND_URL=oldCore;} console.log('PASS: all SSE split boundaries, CRLF, multiline, final-only, empty, timeout, premature EOF, errors, streaming proxy and local-only guard'); })().catch(error=>{console.error(error);process.exitCode=1;});