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.
93 lines
4.1 KiB
TypeScript
93 lines
4.1 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import { mockAgentApi } from "./support/mock-agent-api";
|
|
import { TEST_PROJECT_ID } from "./support/mock-scada-api";
|
|
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
|
|
|
for (const { scope, width } of [
|
|
{ scope: "project", width: 1440 },
|
|
{ scope: "personal", width: 1440 },
|
|
{ scope: "personal", width: 375 }
|
|
] as const) {
|
|
test(`authenticated ${scope} chat sends the matching context at ${width}px`, async ({ page }) => {
|
|
await page.setViewportSize({ width, height: 900 });
|
|
await mockWorkbenchContext(page);
|
|
await mockAgentApi(page);
|
|
if (scope === "personal") {
|
|
await page.route("**/api/v1/projects?*", (route) => route.fulfill({
|
|
headers: { "Access-Control-Allow-Origin": "*" },
|
|
json: { items: [], total: 0, limit: 1000, offset: 0 }
|
|
}));
|
|
}
|
|
await page.route("**/runtime-config.js", (route) => route.fulfill({
|
|
contentType: "application/javascript",
|
|
body: `globalThis.__TJWATER_CONFIG__ = ${JSON.stringify({
|
|
TJWATER_AUTH_MODE: "required",
|
|
TJWATER_KEYCLOAK_ISSUER: "https://authentication.invalid/realms/test",
|
|
TJWATER_KEYCLOAK_CLIENT_ID: "browser-test",
|
|
TJWATER_SERVER_API_BASE_URL: "https://tjwater-api.invalid",
|
|
TJWATER_AGENT_URL: "http://127.0.0.1:8787",
|
|
TJWATER_MAP_URL: "https://workbench-map.invalid/geoserver",
|
|
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next",
|
|
TJWATER_ENABLE_MSW: "false"
|
|
})};`
|
|
}));
|
|
// Exercise the real authentication provider and chat transport without
|
|
// credentials, external authentication, or model inference.
|
|
await page.route("**/keycloak-js.js*", (route) => route.fulfill({
|
|
contentType: "application/javascript",
|
|
body: `export default class Keycloak {
|
|
token = "BROWSER_CONTEXT_TEST_ONLY";
|
|
idTokenParsed = { name: "Browser context test" };
|
|
async init() { return true; }
|
|
async updateToken() { return false; }
|
|
async login() {}
|
|
async logout() {}
|
|
}`
|
|
}));
|
|
const streamHeaders: Array<Record<string, string>> = [];
|
|
const expectedProject = scope === "project" ? TEST_PROJECT_ID : undefined;
|
|
await page.route("**/api/v1/responses", async (route) => {
|
|
if (route.request().method() !== "POST") {
|
|
await route.fallback();
|
|
return;
|
|
}
|
|
const headers = await route.request().allHeaders();
|
|
streamHeaders.push(headers);
|
|
if (headers.authorization !== "Bearer BROWSER_CONTEXT_TEST_ONLY" || headers["x-project-id"] !== expectedProject) {
|
|
await route.fulfill({
|
|
status: 401,
|
|
headers: { "Access-Control-Allow-Origin": "*" },
|
|
json: {
|
|
code: "PRODUCT_CONTEXT_UNAVAILABLE",
|
|
message: "valid Bearer authorization and X-Project-Id are required"
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
await route.fallback();
|
|
});
|
|
|
|
await page.goto("/", { waitUntil: "domcontentloaded" });
|
|
await page.getByRole("button", { name: width < 1024 ? "打开 Agent 面板" : /展开 Agent 助手面板/ }).click();
|
|
const panel = page.locator('aside[aria-label="Agent 命令面板"]').filter({ visible: true });
|
|
if (scope === "personal") {
|
|
await panel.getByRole("button", { name: "选择对话归属" }).click();
|
|
await page.getByRole("menuitem", { name: "个人会话", exact: true }).click();
|
|
await expect(panel.getByRole("button", { name: "选择对话归属" })).toHaveText("个人会话");
|
|
}
|
|
await expect(panel.getByRole("button", { name: "选择 Agent 模型" })).toContainText("测试模型");
|
|
await panel.getByRole("textbox").fill("检查会话项目上下文");
|
|
await panel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
|
|
|
await expect.poll(() => streamHeaders.length).toBe(1);
|
|
expect(streamHeaders[0].authorization).toBe("Bearer BROWSER_CONTEXT_TEST_ONLY");
|
|
expect(streamHeaders[0]["x-project-id"]).toBe(expectedProject);
|
|
await expect(panel.getByText("已收到工况汇总请求。", { exact: true })).toBeVisible();
|
|
await expect(page.getByText("Agent 请求失败", { exact: true })).toHaveCount(0);
|
|
const box = await panel.boundingBox();
|
|
expect(box).not.toBeNull();
|
|
expect(box!.x).toBeGreaterThanOrEqual(0);
|
|
expect(box!.x + box!.width).toBeLessThanOrEqual(width);
|
|
});
|
|
}
|