// 不自动运行(需要 npm install 后执行 node --test app.test.ts) import assert from "node:assert/strict"; import type { AddressInfo } from "node:net"; import { after, before, test } from "node:test"; import { createApp } from "./app.ts"; let base = ""; let close: () => void = () => {}; before(async () => { const app = createApp({ async *askLLM(q) { if (q === "boom") throw new Error("upstream down"); yield "你好"; yield q; }, findOrder: async (id) => (id === "A001" ? { id, status: "paid" } : null), }); const server = app.listen(0); await new Promise((r) => server.once("listening", r)); base = `http://127.0.0.1:${(server.address() as AddressInfo).port}`; close = () => server.close(); }); after(() => close()); test("查询订单:正常 / 格式错误 / 不存在", async () => { assert.deepEqual(await (await fetch(`${base}/orders/A001`)).json(), { id: "A001", status: "paid" }); const bad = await fetch(`${base}/orders/xx`); assert.equal(bad.status, 400); assert.equal((await bad.json()).code, "BAD_ORDER_ID"); const missing = await fetch(`${base}/orders/A999`); assert.equal(missing.status, 404); assert.ok(missing.headers.get("x-request-id")); }); test("请求体不是合法 JSON 时返回 400 而不是 500", async () => { const r = await fetch(`${base}/chat`, { method: "POST", headers: { "content-type": "application/json" }, body: "{bad" }); assert.equal(r.status, 400); }); test("流式对话:逐条 SSE 事件,最后是 done", async () => { const r = await fetch(`${base}/chat`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ question: "退货" }) }); assert.match(r.headers.get("content-type") ?? "", /^text\/event-stream/); // express 会追加 charset const text = await r.text(); assert.match(text, /data: \{"token":"你好"\}/); assert.match(text, /data: \{"token":"退货"\}/); assert.match(text, /event: done/); }); test("上游失败:用 error 事件通知,而不是挂起连接", async () => { const r = await fetch(`${base}/chat`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ question: "boom" }) }); assert.match(await r.text(), /event: error/); });