Files
next-tjwater-frontend/tests/browser/agent-response-inputs.e2e.ts
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
2026-10-19 18:46:58 +08:00

137 lines
8.2 KiB
TypeScript

import { createServer, type ServerResponse } from "node:http";
import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { responseFixtureWriter } from "./support/responses-fixture";
import { expectThemeColor } from "./support/theme-colors";
for (const width of [1440, 375, 320]) {
test(`steers and queues without stopping, and edits unconsumed messages at ${width}px`, async ({ page }, testInfo) => {
const pending = new Map<string, { id: string; text: string; delivery: string; status: string }>();
const admitted: Array<{ id: string; text: string; delivery: string }> = [];
let response: ServerResponse | undefined;
let encode = responseFixtureWriter();
const send = (chunk: Record<string, unknown>) => response!.write(encode(chunk));
const server = createServer((_request, output) => {
response = output;
encode = responseFixtureWriter();
output.writeHead(200, { "Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache", "Content-Type": "text/event-stream" });
send({ type: "start", messageId: "input-answer" });
send({ type: "data-progress", data: { session_id: "input-session", title: "正在核对压力", status: "running" } });
send({ type: "text-start", id: "answer" });
send({ type: "text-delta", id: "answer", delta: "已开始核对压力。" });
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const address = server.address();
if (!address || typeof address === "string") throw new Error("missing fixture address");
try {
await page.setViewportSize({ width, height: 900 });
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockAgentApi(page);
let primaryRequests = 0;
let abortRequests = 0;
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
const request = route.request();
if (request.method() === "OPTIONS") { await route.fallback(); return; }
const path = new URL(request.url()).pathname;
const headers = { "Access-Control-Allow-Origin": "*" };
if (path.endsWith("/responses")) {
const body = request.postDataJSON();
if (!body.tjwater?.delivery) {
primaryRequests++;
await route.continue({ url: `http://127.0.0.1:${address.port}/responses` });
} else {
const item = { id: body.tjwater.input_id, text: body.input, delivery: body.tjwater.delivery, status: "pending" };
admitted.push(item); pending.set(item.id, item);
await route.fulfill({ status: 202, headers, json: item });
}
return;
}
if (path.endsWith("/inputs")) { await route.fulfill({ headers, json: { items: [...pending.values()] } }); return; }
if (path.includes("/inputs/") && request.method() === "PATCH") {
const item = pending.get(path.split("/").at(-1)!);
if (!item) { await route.fulfill({ status: 409, headers, json: { message: "Consumed" } }); return; }
item.delivery = request.postDataJSON().delivery;
await route.fulfill({ headers, json: item });
return;
}
if (path.endsWith("/abort")) {
abortRequests++; pending.clear(); response?.end();
await route.fulfill({ headers, json: { aborted: true } });
return;
}
await route.fallback();
});
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
const composer = page.getByRole("textbox", { name: "询问供水问题,或描述需要处理的任务" });
await composer.fill("核对压力变化");
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
const deliveryMode = page.getByRole("button", { name: "运行中消息发送方式" });
await expect(deliveryMode).toHaveText("引导当前任务");
await deliveryMode.focus();
await page.keyboard.press("ArrowDown");
await expect(page.getByRole("menuitemradio", { name: "引导当前任务" })).toHaveAttribute("aria-checked", "true");
const queueOption = page.getByRole("menuitemradio", { name: "排队处理" });
await queueOption.hover();
await expectThemeColor(queueOption, "background-color", "--action-selection-soft-hover");
await expectThemeColor(queueOption, "border-color", "--action-border-hover");
await page.getByRole("menu").screenshot({ path: testInfo.outputPath("delivery-menu.png"), animations: "disabled" });
await page.keyboard.press("Escape");
await expect(deliveryMode).toBeFocused();
await expect(deliveryMode).toHaveText("引导当前任务");
await expect(page.getByRole("button", { name: "停止 Agent 指令" })).toBeVisible();
await composer.fill("补充当前任务");
await page.getByRole("button", { name: "引导当前 Agent 任务" }).click();
const pendingMode = page.getByRole("button", { name: "消息发送方式:补充当前任务" });
await expect(pendingMode).toHaveText("引导当前任务");
await pendingMode.click();
await page.getByRole("menuitemradio", { name: "排队处理" }).click();
await expect(pendingMode).toHaveText("排队处理");
await pendingMode.click();
await expect(page.getByRole("menuitemradio", { name: "排队处理" })).toHaveAttribute("aria-checked", "true");
const steerOption = page.getByRole("menuitemradio", { name: "引导当前任务" });
await steerOption.hover();
await expectThemeColor(steerOption, "background-color", "--action-selection-soft-hover");
await page.screenshot({ path: testInfo.outputPath("pending-delivery-menu.png"), animations: "disabled" });
await page.keyboard.press("Home");
await page.keyboard.press("Enter");
await expect(pendingMode).toHaveText("引导当前任务");
await expect(pendingMode).toBeFocused();
await deliveryMode.click();
await page.getByRole("menuitemradio", { name: "排队处理" }).click();
await composer.fill("下一轮核对流量");
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
await expect(page.getByRole("button", { name: "消息发送方式:下一轮核对流量" })).toHaveText("排队处理");
expect(admitted.map((item) => item.delivery)).toEqual(["steer", "queue"]);
expect(primaryRequests).toBe(1);
expect(abortRequests).toBe(0);
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
for (const item of pending.values()) {
send({ type: "data-user_input", data: { ...item, session_id: "input-session", status: "delivered" } });
}
pending.clear();
await expect(page.getByRole("list", { name: "待处理消息" })).toHaveCount(0);
send({ type: "text-delta", id: "answer", delta: "已结合补充信息完成核对。" });
send({ type: "text-end", id: "answer" });
send({ type: "finish" }); response!.end();
await expect(page.getByRole("button", { name: "发送 Agent 指令" })).toBeVisible();
await expect(page.getByText("补充当前任务", { exact: true })).toBeVisible();
await expect(page.getByText("下一轮核对流量", { exact: true })).toBeVisible();
await composer.fill("继续核对压力");
await page.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect(page.getByRole("button", { name: "停止 Agent 指令" })).toBeVisible();
await composer.fill("等待处理的后续问题");
await page.getByRole("button", { name: "将消息加入 Agent 队列" }).click();
await expect(page.getByRole("button", { name: "消息发送方式:等待处理的后续问题" })).toBeVisible();
await page.getByRole("button", { name: "停止 Agent 指令" }).click();
await expect(page.getByRole("list", { name: "待处理消息" })).toHaveCount(0);
expect(abortRequests).toBe(1);
} finally {
response?.end(); server.closeAllConnections();
await new Promise<void>((resolve) => server.close(() => resolve()));
}
});
}