129 lines
6.9 KiB
TypeScript
129 lines
6.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
||
import { parseAnswerUiBlock } from "../../src/features/agent/answer-ui/contract";
|
||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||
|
||
// 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/agent/chat`;
|
||
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_API_BASE_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`, { 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`, {
|
||
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 panel.getByRole("button", { name: "选择对话归属" }).click();
|
||
await page.getByRole("menuitem", { name: "个人会话", exact: true }).click();
|
||
await expect(panel.getByRole("button", { name: "选择 Agent 模型" })).toBeEnabled();
|
||
await panel.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
||
await panel
|
||
.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}/stream` && 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("button", { 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)}`);
|
||
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}/stream` && response.request().method() === "POST"
|
||
);
|
||
await panel.getByRole("button", { name: "发送补充资料" }).click();
|
||
const submitted = await secondResponse;
|
||
const submittedBody = submitted.request().postDataJSON() as {
|
||
messages: Array<{ role: string; parts: Array<{ type: string; text?: string }> }>;
|
||
};
|
||
expect(
|
||
submittedBody.messages
|
||
.filter((message) => message.role === "user")
|
||
.at(-1)
|
||
?.parts.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)}`);
|
||
expect(deleted.status()).toBe(204);
|
||
}
|
||
});
|