Files
next-tjwater-frontend/tests/browser/agent-answer-ui-real.e2e.ts
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

129 lines
6.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { expect, test } from "@playwright/test";
import { parseAnswerUiBlock } from "../../src/features/agent/answer-ui/contract";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
import { TEST_PROJECT_ID } from "./support/mock-scada-api";
// Explicit opt-in: two real conversation turns against a local dev Agent; ordinary learning checks may also run.
const realAgentUrl = process.env.TJWATER_ANSWER_UI_REAL_AGENT_URL;
test("real model publishes local components, restores them and receives form input", async ({
page,
request
}) => {
test.skip(
!realAgentUrl,
"Set TJWATER_ANSWER_UI_REAL_AGENT_URL to an isolated local development Agent."
);
test.setTimeout(300_000);
const target = new URL(realAgentUrl!);
expect(["127.0.0.1", "localhost"]).toContain(target.hostname);
const base = `${target.origin}/api/v1`;
const headers = { "X-Project-Id": TEST_PROJECT_ID };
const health = await request.get(`${target.origin}/health`);
expect(health.ok()).toBe(true);
expect(await health.json()).toMatchObject({ ready: true, warmed_up: true });
await mockWorkbenchContext(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_URL: target.origin,
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver",
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
TJWATER_ENABLE_MSW: "false"
})};`
})
);
const created = await request.post(`${base}/session`, { headers, data: {} });
expect(created.ok()).toBe(true);
const id = (await created.json()).session_id as string;
const title = `本地交互回答验收 ${Date.now()}`;
const renamed = await request.patch(`${base}/session/${encodeURIComponent(id)}/title`, {
headers, data: { title }
});
expect(renamed.ok()).toBe(true);
page.on("pageerror", (error) => console.error("Real answer browser error:", error.message));
try {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const panel = page.getByRole("complementary", { name: "Agent 命令面板" });
await expect(panel.getByRole("button", { name: "选择 Agent 模型" })).toBeEnabled();
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
await page.getByRole("dialog", { name: "历史对话", exact: true })
.getByRole("button", { name: new RegExp(title) })
.first()
.click();
await panel
.locator("textarea")
.fill(
"这是本地界面功能验收,请仅使用我提供的演练资料,不代表真实现场事实:监测点 DEMO-P001,08:00 压力 0.30 MPa,09:00 压力 0.28 MPa,10:00 压力 0.18 MPa。请用 present_answer 发布本地交互式小结,必须包括 Chart 压力趋势(保留时间、单位和演练来源)、默认折叠的 Evidence(kind=demo),以及 FollowUpForm 收集一项‘现场位置’(text 类型、必填,不预填值)。最后提交 final_answer,并用 ui_block_ids 引用该界面块,使图表、证据、表单保留在最终报告中;其他过程折叠。再给出简短的演练结论,说明尚待现场核验。不要查询数据库、创建任务、写业务证据或保存记忆,只使用本轮文字、present_answer 和 final_answer。"
);
const requestedModelLabel = process.env.TJWATER_ANSWER_UI_REAL_MODEL_LABEL;
if (requestedModelLabel) {
await panel.getByRole("button", { name: "选择 Agent 模型" }).click();
await page.getByRole("menuitem", { name: new RegExp(requestedModelLabel) }).click();
}
const firstResponse = page.waitForResponse(
(response) => response.url() === `${base}/responses` && response.request().method() === "POST"
);
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
const stream = await firstResponse;
expect(stream.ok()).toBe(true);
// Assert rendered and persisted state; an HTTP response alone cannot prove rendering.
await expect(panel.locator("[data-answer-ui]").first()).toBeVisible({ timeout: 180_000 });
await expect(panel.getByRole("button", { name: "发送 Agent 指令" })).toBeVisible({
timeout: 180_000
});
await panel.getByRole("tab", { name: "数据", exact: true }).first().click();
await expect(panel.getByRole("table").first()).toContainText("0.18");
await expect(panel.getByRole("table").first()).toContainText("10:00");
await expect(panel.getByLabel("现场位置(必填)")).toHaveValue("");
const storedResponse = await request.get(`${base}/session/${encodeURIComponent(id)}`, { headers });
expect(storedResponse.ok()).toBe(true);
const stored = await storedResponse.json();
expect(stored.is_streaming).toBe(false);
const uiParts = stored.messages.flatMap(
(message: { parts?: Array<{ type: string; data?: Record<string, unknown> }> }) =>
message.parts?.filter((part) => part.type === "data-answer_ui") ?? []
);
expect(uiParts.length).toBeGreaterThan(0);
for (const part of uiParts) {
const { version, block_id, fallback_text, spec } = part.data;
expect(parseAnswerUiBlock({ version, block_id, fallback_text, spec })).not.toBeNull();
}
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
await panel
.getByRole("button", { name: new RegExp(stored.title) })
.first()
.click();
await expect(panel.getByLabel("现场位置(必填)")).toHaveValue("");
await panel
.getByLabel("现场位置(必填)")
.fill("演练:东侧阀门旁,未实地核验。本轮仍为功能验收,请不要写业务证据或记忆,只确认收到。");
const secondResponse = page.waitForResponse(
(response) => response.url() === `${base}/responses` && response.request().method() === "POST"
);
await panel.getByRole("button", { name: "发送补充资料" }).click();
const submitted = await secondResponse;
const submittedBody = submitted.request().postDataJSON() as {
input: Array<{ role: string; content: Array<{ type: string; text?: string }> }>;
};
expect(
submittedBody.input
.filter((message) => message.role === "user")
.at(-1)
?.content.some((part) => part.text?.includes("现场位置:演练:东侧阀门旁"))
).toBe(true);
expect(submitted.ok()).toBe(true);
await expect(panel.getByRole("button", { name: "发送 Agent 指令" })).toBeVisible({
timeout: 180_000
});
await panel.screenshot({ path: "test-results/answer-ui-real-model.png" });
} finally {
const deleted = await request.delete(`${base}/session/${encodeURIComponent(id)}`, { headers });
expect(deleted.status()).toBe(204);
}
});