Files
ttstd-infinite-canvas/web/test/text-reasoning-stream.test.ts

78 lines
3.8 KiB
TypeScript

import { afterEach, describe, expect, test } from "bun:test";
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { GenerationToolCard } from "../src/components/ai/generation-tool-card";
import type { ModelProtocol } from "../src/lib/model-protocols";
import { requestImageQuestion } from "../src/services/api/image";
import { createModelChannel, defaultConfig, type AiConfig } from "../src/stores/use-config-store";
const originalFetch = globalThis.fetch;
afterEach(() => {
globalThis.fetch = originalFetch;
});
function reasoningConfig(protocol: ModelProtocol | undefined, apiFormat: "openai" | "gemini" = "openai"): AiConfig {
const channel = createModelChannel({
id: "reasoning",
name: "Reasoning",
baseUrl: "https://reasoning.example/v1",
apiKey: "synthetic-test-key",
apiFormat,
interfaceType: apiFormat === "gemini" ? undefined : protocol,
models: ["reasoner"],
modelCosts: [{ model: "reasoner", capability: "text", ...(protocol ? { protocol } : {}), billingMode: "fixed_request", unitPriceMicrocredits: 0 }],
});
return { ...defaultConfig, channels: [channel], model: "reasoning::reasoner", textModel: "reasoning::reasoner" };
}
function streamResponse(events: unknown[]) {
const body = `${events.map((event) => `data: ${JSON.stringify(event)}`).join("\n\n")}\n\n`;
return new Response(body, { headers: { "Content-Type": "text/event-stream" } });
}
async function requestWithReasoning(config: AiConfig, events: unknown[]) {
globalThis.fetch = (async () => streamResponse(events)) as typeof fetch;
let content = "";
let reasoning = "";
const result = await requestImageQuestion(config, [{ role: "user", content: "请回答" }], (value) => { content = value; }, { onReasoning: (value) => { reasoning = value; } });
return { result, content, reasoning };
}
describe("text reasoning streams", () => {
test("separates Responses reasoning summaries from answer text", async () => {
const output = await requestWithReasoning(reasoningConfig("openai-response"), [
{ type: "response.reasoning_summary_text.delta", delta: "先分析" },
{ type: "response.output_text.delta", delta: "最终答案" },
]);
expect(output).toEqual({ result: "最终答案", content: "最终答案", reasoning: "先分析" });
});
test("separates Chat Completions reasoning_content from answer text", async () => {
const output = await requestWithReasoning(reasoningConfig("chat-completion"), [
{ choices: [{ delta: { reasoning_content: "推理过程" } }] },
{ choices: [{ delta: { content: "回答内容" } }] },
]);
expect(output).toEqual({ result: "回答内容", content: "回答内容", reasoning: "推理过程" });
});
test("does not duplicate Gemini thought parts into visible content", async () => {
const output = await requestWithReasoning(reasoningConfig(undefined, "gemini"), [
{ candidates: [{ content: { parts: [{ text: "内部思考", thought: true }, { text: "可见回答" }] } }] },
]);
expect(output).toEqual({ result: "可见回答", content: "可见回答", reasoning: "内部思考" });
});
test("keeps single results open and collapses completed bulk results on restore", () => {
const single = renderToStaticMarkup(React.createElement(GenerationToolCard, { status: "completed", heading: "图像生成" }, React.createElement("span", null, "单图结果")));
const bulk = renderToStaticMarkup(React.createElement(GenerationToolCard, { status: "completed", isBulk: true, heading: "图像生成" }, React.createElement("span", null, "批量结果")));
expect(single).toContain("单图结果");
expect(bulk).not.toContain("批量结果");
});
});