Files
next-tjwater-frontend/tests/browser/agent-streaming-stability.e2e.ts
T
jiang d2d705ed4c fix(workbench): refine agent dock and smooth map resizing
Unify dock surfaces, headers and dividers, and remove the visible context strip.
Limit pointer resizing to 400-640px and reveal the edge grip on hover.
Resize and redraw the map in the same layout cycle to prevent blanking and lag.
Preserve horizontal centering during conversation control press feedback.

Validation: isolated lint, typecheck, 21 focused unit tests, and 10 relevant
browser checks passed. Snapshot baselines regenerated from the isolated change.
The existing mobile alert summary test also fails on unchanged HEAD because
the expected demo alert source is unavailable in this environment.
2026-10-08 17:34:42 +08:00

770 lines
28 KiB
TypeScript

import { createServer, type Server } from "node:http";
import type { AddressInfo } from "node:net";
import { expect, test } from "@playwright/test";
import { mockScadaApi } from "./support/mock-scada-api";
const STREAM_CHUNK_SIZE = 18;
const STREAM_CHUNK_DELAY_MS = 20;
const PROGRESS_GEOMETRY_TOLERANCE_PX = 0.1;
let streamServer: Server;
let streamServerUrl: string;
let appendProgress: (() => void) | null = null;
let failProgress: (() => void) | null = null;
let releaseStream: (() => void) | null = null;
test.beforeAll(async () => {
streamServer = createServer((_request, response) => {
response.writeHead(200, {
"Access-Control-Allow-Origin": "*",
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
"X-Vercel-AI-UI-Message-Stream": "v1"
});
const content = createLongMarkdownResponse();
const chunks = splitIntoCodePointChunks(content, STREAM_CHUNK_SIZE);
const send = (chunk: Record<string, unknown>) => {
response.write(`data: ${JSON.stringify(chunk)}\n\n`);
};
send({ type: "start", messageId: "assistant-stream-stability" });
send({
type: "data-progress",
id: "inspect-context",
data: {
id: "inspect-context",
phase: "planning",
status: "running",
title: "正在分析运行上下文",
detail: "核对泵站、液位与用水过程。",
started_at: Date.now()
}
});
send({
type: "data-progress",
id: "inspect-rainfall",
data: {
id: "inspect-rainfall",
phase: "evidence",
status: "completed",
title: "正在核对用水过程与供水分区响应关系",
detail: "读取最近十二小时分钟级流量序列,并检查每个供水分区的响应延迟是否处于合理范围。",
started_at: Date.now()
}
});
send({
type: "data-progress",
id: "inspect-level",
data: {
id: "inspect-level",
phase: "evidence",
status: "completed",
title: "正在核对液位连续性",
detail: "排除缺测与异常峰值。",
started_at: Date.now()
}
});
send({
type: "data-todo_update",
id: "stream-stability-plan",
data: {
session_id: "stream-stability-session",
message_id: "assistant-stream-stability",
created_at: Date.now(),
todos: [
{ id: "inspect-pump", content: "检查泵站运行状态", status: "in_progress" },
{ id: "inspect-level", content: "核对液位计连续性", status: "pending" },
{ id: "assess-supply-risk", content: "判断供水风险", status: "pending" }
]
}
});
send({ type: "text-start", id: "answer" });
let index = 0;
let timer: ReturnType<typeof setInterval> | null = null;
const sendNextTextChunk = () => {
const delta = chunks[index];
if (delta !== undefined) {
index += 1;
send({ type: "text-delta", id: "answer", delta });
send({
type: "data-stream_token",
data: {
session_id: "stream-stability-session",
message_id: "assistant-stream-stability",
content: delta
},
transient: true
});
return;
}
if (timer) {
clearInterval(timer);
timer = null;
}
send({
type: "data-progress",
id: "assess-risk",
data: {
id: "assess-risk",
phase: "analysis",
status: "completed",
title: "供水风险判断完成",
detail: "已完成风险点位排序。",
ended_at: Date.now()
}
});
send({ type: "text-end", id: "answer" });
send({ type: "finish", finishReason: "stop" });
response.end();
};
while (index < 3 && index < chunks.length) {
sendNextTextChunk();
}
appendProgress = () => {
send({
type: "data-progress",
id: "assess-risk",
data: {
id: "assess-risk",
phase: "analysis",
status: "running",
title: "正在判断供水风险",
detail: "结合流量突变与液位趋势形成判断。",
started_at: Date.now()
}
});
};
failProgress = () => {
send({
type: "data-progress",
id: "inspect-level",
data: {
id: "inspect-level",
phase: "evidence",
status: "error",
title: "液位连续性核对失败",
detail: "监测数据读取超时。",
ended_at: Date.now()
}
});
};
releaseStream = () => {
if (!timer) {
timer = setInterval(sendNextTextChunk, STREAM_CHUNK_DELAY_MS);
}
};
response.on("close", () => {
if (timer) {
clearInterval(timer);
}
});
});
await new Promise<void>((resolve) => streamServer.listen(0, "127.0.0.1", resolve));
const address = streamServer.address() as AddressInfo;
streamServerUrl = `http://127.0.0.1:${address.port}/stream`;
});
test.afterAll(async () => {
await new Promise<void>((resolve, reject) => {
streamServer.close((error) => (error ? reject(error) : resolve()));
});
});
test("stream completion and the plan card remain positionally stable", async ({ page }) => {
await mockScadaApi(page);
await page.route("**/runtime-config.js", (route) =>
route.fulfill({
contentType: "application/javascript",
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
TJWATER_AUTH_MODE: "disabled",
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
TJWATER_AGENT_API_BASE_URL: "http://127.0.0.1:8787",
TJWATER_MAP_URL: "https://stream-map.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_MAPBOX_ACCESS_TOKEN: "",
TJWATER_ENABLE_MSW: "false"
})};`
})
);
await page.route("https://stream-map.invalid/**", (route) => route.fulfill({ status: 204 }));
await page.route("**/api/v1/agent/chat/**", async (route) => {
if (route.request().method() === "OPTIONS") {
await route.fulfill({
status: 204,
headers: {
"Access-Control-Allow-Headers": "Authorization, Content-Type, X-Project-Id",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Origin": "*"
}
});
return;
}
const path = new URL(route.request().url()).pathname;
if (path.endsWith("/stream")) {
await route.continue({ url: streamServerUrl });
return;
}
const body = path.endsWith("/models")
? {
default_model: "test/model",
models: [
{
id: "test/model",
label: "测试模型",
description: "流式稳定性测试",
icon: "bolt"
}
]
}
: path.endsWith("/sessions")
? { sessions: [] }
: {};
await route.fulfill({
headers: { "Access-Control-Allow-Origin": "*" },
json: body
});
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
await expect(
page.getByRole("button", { name: "选择 Agent 模型" }).filter({ visible: true }).first()
).toContainText("测试模型", { timeout: 10_000 });
const prompt = page
.getByPlaceholder(/输入调度问题/)
.filter({ visible: true })
.first();
await expect(prompt).toBeVisible();
await prompt.fill("检查流式响应稳定性");
const sendButton = page
.getByRole("button", { name: "发送 Agent 指令" })
.filter({ visible: true })
.first();
await expect(sendButton).toBeEnabled();
await page.evaluate(() => {
type StreamSample = {
clientHeight: number;
planItemTransform: string | null;
planSectionTransform: string | null;
planTransform: string | null;
scrollHeight: number;
scrollTop: number;
time: number;
transform: string;
};
const samples: StreamSample[] = [];
const startedAt = performance.now();
const state = window as typeof window & {
__agentStreamSamples?: StreamSample[];
__agentStreamSamplingDone?: boolean;
};
state.__agentStreamSamples = samples;
state.__agentStreamSamplingDone = false;
const sample = () => {
const message = document.querySelector<HTMLElement>(".agent-panel-message");
const wrapper = message?.parentElement?.parentElement;
const scroll = document.querySelector<HTMLElement>(".agent-conversation-scroll");
const plan = Array.from(document.querySelectorAll<HTMLElement>(".agent-panel-nested")).find(
(element) => element.textContent?.includes("计划")
);
const planWrapper = plan?.parentElement;
const planSection = planWrapper?.parentElement;
const planItem = plan?.querySelector("li");
if (message && wrapper && scroll) {
samples.push({
clientHeight: scroll.clientHeight,
planItemTransform: planItem ? getComputedStyle(planItem).transform : null,
planSectionTransform: planSection ? getComputedStyle(planSection).transform : null,
planTransform: planWrapper ? getComputedStyle(planWrapper).transform : null,
scrollHeight: scroll.scrollHeight,
scrollTop: scroll.scrollTop,
time: performance.now() - startedAt,
transform: getComputedStyle(wrapper).transform
});
}
if (!state.__agentStreamSamplingDone) {
requestAnimationFrame(sample);
}
};
requestAnimationFrame(sample);
});
await sendButton.click();
const userMessage = page.locator(".agent-panel-user-message");
const assistantMessage = page.locator(".agent-panel-message").first();
await expect(userMessage).toHaveCSS("background-color", "oklch(0.965 0.018 255)");
expect(
await userMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
).not.toBe(
await assistantMessage.evaluate((element) => getComputedStyle(element).backgroundColor)
);
await expect
.poll(() => userMessage.evaluate((element) => getComputedStyle(element).boxShadow))
.toBe("none");
await expect
.poll(() => assistantMessage.evaluate((element) => getComputedStyle(element).boxShadow))
.toBe(await userMessage.evaluate((element) => getComputedStyle(element).boxShadow));
const operationalBrief = page.locator(".agent-panel-control").filter({
has: page.getByRole("button", { name: "预览影响", exact: true })
});
await expect(operationalBrief).toBeVisible();
await expect(operationalBrief).toContainText("Agent 分析中");
await expect(operationalBrief.getByRole("button", { name: "预览影响" })).toBeDisabled();
await expect(operationalBrief.getByRole("button", { name: "检查证据" })).toBeDisabled();
await expect(operationalBrief).toHaveCSS("position", "static");
await expect(operationalBrief).toHaveCSS("border-radius", "12px");
// Desktop uses an inline brief above the conversation, not the legacy floating overlay.
await expect(page.locator(".agent-panel-shell")).toHaveClass(/surface-dock/);
await expect(page.locator(".agent-panel-operational-float")).toHaveCount(0);
await expect
.poll(() =>
operationalBrief.evaluate((element) => {
const conversation = document.querySelector(".agent-panel-conversation")!;
return conversation.getBoundingClientRect().top - element.getBoundingClientRect().bottom;
})
)
.toBeGreaterThanOrEqual(0);
const liveProgress = page.getByRole("list", { name: "最近执行进度" });
await expect(liveProgress.locator("li")).toHaveCount(3);
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
await expect(liveProgress).toHaveCSS("height", "156px");
await expect.poll(() => liveProgress.evaluate((list) => list.style.height)).toBe("auto");
await page.evaluate(() => {
const state = window as typeof window & {
__agentMinVisibleProgressRows?: number;
__agentProgressGeometry?: {
maxContainerHeight: number;
maxListHeight: number;
minContainerHeight: number;
minListHeight: number;
};
__agentProgressWindowSampleDone?: boolean;
};
const startedAt = performance.now();
state.__agentMinVisibleProgressRows = Number.POSITIVE_INFINITY;
state.__agentProgressGeometry = {
maxContainerHeight: Number.NEGATIVE_INFINITY,
maxListHeight: Number.NEGATIVE_INFINITY,
minContainerHeight: Number.POSITIVE_INFINITY,
minListHeight: Number.POSITIVE_INFINITY
};
state.__agentProgressWindowSampleDone = false;
const sample = () => {
const list = document.querySelector<HTMLOListElement>('ol[aria-label="最近执行进度"]');
const rows = list?.children.length ?? 0;
state.__agentMinVisibleProgressRows = Math.min(
state.__agentMinVisibleProgressRows ?? rows,
rows
);
if (list && state.__agentProgressGeometry) {
const listHeight = list.getBoundingClientRect().height;
const containerHeight = list.parentElement?.getBoundingClientRect().height ?? 0;
state.__agentProgressGeometry.minListHeight = Math.min(
state.__agentProgressGeometry.minListHeight,
listHeight
);
state.__agentProgressGeometry.maxListHeight = Math.max(
state.__agentProgressGeometry.maxListHeight,
listHeight
);
state.__agentProgressGeometry.minContainerHeight = Math.min(
state.__agentProgressGeometry.minContainerHeight,
containerHeight
);
state.__agentProgressGeometry.maxContainerHeight = Math.max(
state.__agentProgressGeometry.maxContainerHeight,
containerHeight
);
}
if (performance.now() - startedAt < 400) {
requestAnimationFrame(sample);
} else {
state.__agentProgressWindowSampleDone = true;
}
};
requestAnimationFrame(sample);
});
triggerProgressAppend();
await expect(liveProgress.getByText("正在判断供水风险", { exact: true })).toBeVisible();
await expect(liveProgress.locator("li")).toHaveCount(3);
await expect(liveProgress.getByText("正在分析运行上下文", { exact: true })).toHaveCount(0);
await expect
.poll(() =>
page.evaluate(
() =>
(window as typeof window & { __agentProgressWindowSampleDone?: boolean })
.__agentProgressWindowSampleDone ?? false
)
)
.toBe(true);
expect(
await page.evaluate(
() =>
(window as typeof window & { __agentMinVisibleProgressRows?: number })
.__agentMinVisibleProgressRows
)
).toBeGreaterThanOrEqual(3);
const progressGeometry = await page.evaluate(
() =>
(
window as typeof window & {
__agentProgressGeometry?: {
maxContainerHeight: number;
maxListHeight: number;
minContainerHeight: number;
minListHeight: number;
};
}
).__agentProgressGeometry
);
expect(progressGeometry).toBeDefined();
expect(progressGeometry!.maxListHeight - progressGeometry!.minListHeight).toBeLessThanOrEqual(
PROGRESS_GEOMETRY_TOLERANCE_PX
);
expect(
progressGeometry!.maxContainerHeight - progressGeometry!.minContainerHeight
).toBeLessThanOrEqual(PROGRESS_GEOMETRY_TOLERANCE_PX);
const liveProgressRowHeights = await liveProgress
.locator("li")
.evaluateAll((rows) => rows.map((row) => row.getBoundingClientRect().height));
expect(liveProgressRowHeights.every((height) => Math.abs(height - 48) <= 0.01)).toBe(true);
await expect(liveProgress.locator("li").first().locator("span.min-w-0 > span").first()).toHaveCSS(
"white-space",
"nowrap"
);
const progressToggle = page
.getByRole("button", { name: /执行进度/ })
.filter({ visible: true })
.first();
await expect(progressToggle).toBeEnabled();
await expect(progressToggle).toHaveAttribute("data-state", "closed");
await expect
.poll(async () => (await progressToggle.boundingBox())?.height ?? 0)
.toBeGreaterThanOrEqual(40);
await progressToggle.click();
await expect(progressToggle).toHaveAttribute("data-state", "open");
const expandedLiveProgress = page.getByRole("list", { name: "全部执行进度" });
await expect(expandedLiveProgress.locator("li")).toHaveCount(4);
await expect(expandedLiveProgress.locator(".status-badge")).toHaveCount(0);
await expect(expandedLiveProgress.locator('[data-progress-status="running"]').first()).toHaveCSS(
"color",
"oklch(0.546 0.245 262.881)"
);
await expect(expandedLiveProgress.getByText("正在分析运行上下文", { exact: true })).toBeVisible();
await expect(
expandedLiveProgress.getByText("正在核对用水过程与供水分区响应关系", { exact: true })
).toHaveCSS("white-space", "normal");
await expandedLiveProgress.evaluate(async (element) => {
const animations = element
.getAnimations({ subtree: true })
.filter((animation) => animation.effect?.getTiming().iterations !== Infinity);
await Promise.all(animations.map((animation) => animation.finished.catch(() => undefined)));
});
await progressToggle.click();
await expect(progressToggle).toHaveAttribute("data-state", "closed");
await expect(page.getByRole("list", { name: "最近执行进度" }).locator("li")).toHaveCount(3);
triggerProgressFailure();
await expect(liveProgress.getByText("液位连续性核对失败", { exact: true })).toBeVisible();
await expect(page.locator('[data-progress-status="error"]')).toHaveCSS("transform", "none");
resumeStream();
await expect(
page.getByText("分析完成", { exact: true }).filter({ visible: true }).first()
).toBeVisible({
timeout: 10_000
});
await expect(progressToggle.locator("../..")).toContainText("步骤4/4");
await expect(page.getByRole("list", { name: "最近执行进度" })).toHaveCount(0);
await expect(
page.getByText("供水风险判断完成", { exact: true }).filter({ visible: true })
).toHaveCount(0);
await expect
.poll(async () =>
page.evaluate(() => {
const scroll = document.querySelector<HTMLElement>(".agent-conversation-scroll");
return scroll
? Math.abs(scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop)
: Number.POSITIVE_INFINITY;
})
)
.toBeLessThanOrEqual(2);
await expect
.poll(() =>
page.evaluate(() => {
const samples = (
window as typeof window & {
__agentStreamSamples?: Array<{
clientHeight: number;
scrollHeight: number;
scrollTop: number;
}>;
}
).__agentStreamSamples;
const sample = samples?.at(-1);
return sample
? Math.abs(sample.scrollHeight - sample.clientHeight - sample.scrollTop)
: Number.POSITIVE_INFINITY;
})
)
.toBeLessThanOrEqual(2);
await page.evaluate(() => {
(window as typeof window & { __agentStreamSamplingDone?: boolean }).__agentStreamSamplingDone =
true;
});
const samples = await page.evaluate(
() =>
(
window as typeof window & {
__agentStreamSamples?: Array<{
clientHeight: number;
planItemTransform: string | null;
planSectionTransform: string | null;
planTransform: string | null;
scrollHeight: number;
scrollTop: number;
time: number;
transform: string;
}>;
}
).__agentStreamSamples ?? []
);
expect(samples.length).toBeGreaterThan(5);
const finalSample = samples.at(-1);
expect(finalSample).toBeDefined();
expect(finalSample).toMatchObject({
planItemTransform: "none",
planSectionTransform: "none",
planTransform: "none",
transform: "none"
});
expect(
Math.abs(finalSample!.scrollHeight - finalSample!.clientHeight - finalSample!.scrollTop)
).toBeLessThanOrEqual(2);
await progressToggle.click();
const expandedProgress = page.getByRole("list", { name: "全部执行进度" });
await expect(expandedProgress.locator("li")).toHaveCount(4);
await expect(
page.getByText("正在分析运行上下文", { exact: true }).filter({ visible: true }).first()
).toBeVisible();
const expandedLongTitle = expandedProgress.getByText("正在核对用水过程与供水分区响应关系", {
exact: true
});
await expect(expandedLongTitle).toHaveCSS("white-space", "normal");
await expect(expandedProgress.locator("li").nth(1)).not.toHaveCSS("height", "48px");
await progressToggle.click();
await prompt.fill("验证展开状态在流结束后保持");
await sendButton.click();
const latestProgressToggle = page
.getByRole("button", { name: /执行进度/ })
.filter({ visible: true })
.last();
const latestProgressRegion = latestProgressToggle.locator("../..");
const latestExpandedProgress = latestProgressRegion.getByRole("list", {
name: "全部执行进度"
});
await expect(latestProgressRegion).toContainText("最近3/3");
triggerProgressAppend();
await expect(
page.getByRole("list", { name: "最近执行进度" }).getByText("正在判断供水风险", { exact: true })
).toBeVisible();
await latestProgressToggle.click();
await expect(latestProgressRegion).toContainText("全部4/4");
resumeStream();
await expect(latestProgressRegion).toContainText("全部4/4", { timeout: 10_000 });
await expect(latestExpandedProgress).toBeVisible();
await expect(latestExpandedProgress.getByText("供水风险判断完成", { exact: true })).toBeVisible();
const conversationScroll = page.locator(".agent-conversation-scroll");
await conversationScroll.hover();
await page.mouse.wheel(0, -2400);
const scrollToLatestButton = page.getByRole("button", { name: "滚动到最新消息" });
await expect(scrollToLatestButton).toBeVisible();
const scrollButtonBox = (await scrollToLatestButton.boundingBox())!;
await page.mouse.move(scrollButtonBox.x + scrollButtonBox.width / 2, scrollButtonBox.y + scrollButtonBox.height / 2);
await page.mouse.down();
const pressedScrollButtonBox = (await scrollToLatestButton.boundingBox())!;
expect(pressedScrollButtonBox.x).toBe(scrollButtonBox.x);
expect(pressedScrollButtonBox.y).toBe(scrollButtonBox.y + 1);
await page.mouse.move(scrollButtonBox.x - 20, scrollButtonBox.y);
await page.mouse.up();
await expect(scrollToLatestButton).toHaveCSS("background-color", "rgba(255, 255, 255, 0.94)");
await expect
.poll(() =>
scrollToLatestButton.evaluate((element) => getComputedStyle(element).backdropFilter)
)
.toContain("blur(");
await expect
.poll(() => scrollToLatestButton.evaluate((element) => getComputedStyle(element).boxShadow))
.not.toBe("none");
const readAcrylicMaterial = (element: HTMLElement) => {
const style = getComputedStyle(element);
return {
backdropFilter: style.backdropFilter,
backgroundColor: style.backgroundColor,
backgroundImage: style.backgroundImage,
boxShadow: style.boxShadow
};
};
await expect(prompt).toBeVisible();
await expect(page.locator(".agent-panel-composer")).toHaveCount(0);
await expect
.poll(() =>
prompt.evaluate((element) => {
const form = element.closest("form")!;
const scroll = document.querySelector(".agent-conversation-scroll")!;
return form.getBoundingClientRect().top - scroll.getBoundingClientRect().bottom;
})
)
.toBeGreaterThanOrEqual(0);
const selectableAssistantMessage = page.getByRole("heading", { name: "诊断结果" }).first();
await selectableAssistantMessage.evaluate((element) => {
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
let textNode = walker.nextNode();
while (textNode && !textNode.textContent?.trim()) {
textNode = walker.nextNode();
}
if (!textNode?.textContent) {
throw new Error("Assistant message does not contain selectable text");
}
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, Math.min(8, textNode.textContent.length));
const selection = window.getSelection();
selection?.removeAllRanges();
selection?.addRange(range);
element.dispatchEvent(new PointerEvent("pointerup", { bubbles: true }));
});
const speechSelectionIconMotion = await page.evaluate(async () => {
const waitForAnimationFrame = () =>
new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
const deadline = performance.now() + 1_000;
let button: HTMLButtonElement | undefined;
while (!button && performance.now() < deadline) {
button = Array.from(document.querySelectorAll<HTMLButtonElement>("button")).find((element) =>
element.textContent?.includes("从这里开始朗读")
);
if (!button) await waitForAnimationFrame();
}
const icon = button?.querySelector("svg");
if (!button || !icon) {
throw new Error("Speech selection action did not render with an icon");
}
const samples: Array<{ x: number; y: number }> = [];
const startedAt = performance.now();
while (performance.now() - startedAt < 220) {
const rect = icon.getBoundingClientRect();
samples.push({
x: rect.x + rect.width / 2,
y: rect.y + rect.height / 2
});
await waitForAnimationFrame();
}
const xValues = samples.map((sample) => sample.x);
const yValues = samples.map((sample) => sample.y);
return {
xSpread: Math.max(...xValues) - Math.min(...xValues),
ySpread: Math.max(...yValues) - Math.min(...yValues)
};
});
expect(speechSelectionIconMotion.xSpread).toBeLessThanOrEqual(0.1);
expect(speechSelectionIconMotion.ySpread).toBeLessThanOrEqual(0.1);
const speechSelectionButton = page.getByRole("button", { name: "从这里开始朗读" });
await expect(speechSelectionButton).toBeVisible();
await expect(speechSelectionButton).toHaveAttribute("data-agent-presentation", "desktop-dock");
await expect(speechSelectionButton).toHaveCSS("border-top-width", "0px");
expect(await speechSelectionButton.evaluate(readAcrylicMaterial)).toEqual(
await scrollToLatestButton.evaluate(readAcrylicMaterial)
);
await page.screenshot({ path: "test-results/agent-history-ui/desktop-conversation.png" });
});
function createLongMarkdownResponse() {
let content = "## 诊断结果\n\n";
for (let index = 1; index <= 6; index += 1) {
content += `### 区域 ${index}\n\n`;
content +=
"当前管网运行状态总体稳定,但部分区域存在水位持续升高、流量突变和监测数据缺失。需要结合实时工况核对监测点连续性。\n\n";
content += "- 检查泵站运行状态与实时流量。\n";
content += "- 核对液位计连续性和异常峰值。\n";
content += "- 结合用水过程判断供水风险。\n\n";
}
return `${content}建议优先处置高风险点位,并持续跟踪后续变化。`;
}
function splitIntoCodePointChunks(content: string, chunkSize: number) {
const codePoints = Array.from(content);
const chunks: string[] = [];
for (let index = 0; index < codePoints.length; index += chunkSize) {
chunks.push(codePoints.slice(index, index + chunkSize).join(""));
}
return chunks;
}
function resumeStream() {
if (!releaseStream) {
throw new Error("Stream checkpoint was not reached");
}
const resume = releaseStream;
releaseStream = null;
resume();
}
function triggerProgressAppend() {
if (!appendProgress) {
throw new Error("Progress append checkpoint was not reached");
}
const append = appendProgress;
appendProgress = null;
append();
}
function triggerProgressFailure() {
if (!failProgress) {
throw new Error("Progress failure checkpoint was not reached");
}
const fail = failProgress;
failProgress = null;
fail();
}