Files
next-tjwater-frontend/tests/browser/investigation-main-chat.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

213 lines
16 KiB
TypeScript

import { responsesFixture } from "./support/responses-fixture";
import { expect, test, type Locator } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
// Product display stays UTC+8 even when the browser itself is configured for UTC.
test.use({ timezoneId: "UTC" });
const taskId = "22222222-2222-4222-8222-222222222222";
const runId = "33333333-3333-4333-8333-333333333333";
const projectId = "11111111-1111-4111-8111-111111111111";
const now = "2026-10-13T08:00:00Z";
const run = { run_id: runId, task_id: taskId, generation: 2, execution_request_id: runId, status: "succeeded", revision: 1,
agent_session_id: "investigation-run", lease_expires_at: null, heartbeat_at: null, started_at: now, finished_at: now,
failure_code: null, failure_detail: null, dispatch_attempts: 1, next_dispatch_at: now, created_at: now, updated_at: now };
const task = { project_id: projectId, task_id: taskId, task_type: "event_investigation", workflow: "water_quality_complaint",
lifecycle_status: "completed", active_run: null, latest_run: run, status: "completed", priority: "normal", revision: 1,
execution_generation: 2, execution_request_id: runId, title: "居民异味投诉调查", summary: "现场记录待复核",
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "water_quality_complaint" },
trigger: { kind: "manual", occurred_at: now, detail: "居民异味投诉" }, scheduled_for: null, agent_session_id: "investigation-run",
agent_lease_expires_at: null, current_plan_revision: null, current_report_revision: null, created_at: now, updated_at: now,
latest_plan: null, latest_report: {
revision: 3, execution_generation: 1, source_session_id: "investigation-run", published_at: now, published_by: "agent",
report: { title: "居民异味投诉调查报告", current_conclusion: "现场记录待复核", conclusion_boundary: "仅覆盖已有监测记录",
evidence_refs: ["monitor-1"], recommendations: [], pending_items: [], stage: "final", outcome: "concluded",
stop_reason: null, data_gaps: [], diagram: null }
}, latest_progress: null,
evidence: [{ evidence_id: "monitor-1", kind: "system_data", summary: "上游监测数据质量已核对", source_label: "SCADA 水质监测",
source_ref: "WQ-01", observed_at: now, recorded_at: now, recorded_by: "agent", has_snapshot: false }],
actions: [], access_requests: [], work_order_snapshot: null,
permissions: { can_publish_artifacts: false, can_propose_actions: false, can_sync_external_feedback: false,
can_decide_work_orders: false, can_execute_work_orders: false, can_decide_commands: false, can_execute_commands: false, can_decide_access: false } };
test("证据提问在预览、展开及更多依据中使用统一实体按钮", async ({ page }, testInfo) => {
await page.emulateMedia({ reducedMotion: "reduce" });
await page.route("http://127.0.0.1:8787/api/v1/**", (route) => route.fulfill({ json: { items: [] } }));
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.setContent('<html lang="zh-CN"><head><link rel="stylesheet" href="/src/styles.css"></head><body><main id="report" style="max-width:640px;margin:auto;padding:16px"></main></body></html>');
await page.evaluate(async ({ fixture, cachePath }) => {
const reactModule = await import(`${cachePath}/react.js`);
const React = reactModule.default ?? reactModule;
const clientModule = await import(`${cachePath}/react-dom_client.js`);
const { createRoot } = clientModule.default ?? clientModule;
const { InvestigationTaskReport } = await import("/src/features/workbench/components/investigation-task-report.tsx");
const evidence = Array.from({ length: 4 }, (_, index) => ({ ...fixture.evidence[0], evidence_id: `monitor-${index + 1}` }));
createRoot(document.getElementById("report")).render(React.createElement(InvestigationTaskReport, {
task: { ...fixture, evidence, latest_report: { ...fixture.latest_report, report: { ...fixture.latest_report.report, evidence_refs: evidence.map((item) => item.evidence_id) } } },
onContinue: () => undefined, renderAction: () => null,
renderEvidence: (item: { summary: string }) => React.createElement("p", null, item.summary)
}));
}, { fixture: task, cachePath: `/node_modules/.vite-playwright-${process.env.PLAYWRIGHT_PORT || "5191"}/deps` });
const preview = page.getByRole("region", { name: "主要依据", exact: true });
await expect(page.getByText("任务更新于 2026/10/13 16:00:00", { exact: true })).toBeVisible();
await expect(preview).toContainText("2026/10/13 16:00:00");
for (const width of [1440, 320]) {
await page.setViewportSize({ width, height: 900 });
const buttons = preview.getByRole("button", { name: "围绕此证据提问", exact: true });
await expect(buttons).toHaveCount(3);
for (const button of await buttons.all()) {
await expect(button).toHaveAttribute("data-slot", "button");
await expect(button).toHaveCSS("border-top-width", "1px");
await expect(button).toHaveCSS("height", "32px");
}
await preview.screenshot({ path: testInfo.outputPath(`evidence-buttons-${width}.png`) });
await preview.getByRole("button", { name: "展开主要依据" }).click();
const dialog = page.getByRole("dialog");
await dialog.getByText("更多引用依据 · 1 条", { exact: true }).click();
const expandedButtons = dialog.getByRole("button", { name: "围绕此证据提问", exact: true });
await expect(expandedButtons).toHaveCount(4);
for (const button of await expandedButtons.all()) {
await expect(button).toBeVisible();
await expect(button).toHaveAttribute("data-slot", "button");
await expect(button).toHaveCSS("border-top-width", "1px");
expect(await button.evaluate((element) => element.getBoundingClientRect().right <= innerWidth)).toBe(true);
}
await dialog.screenshot({ path: testInfo.outputPath(`evidence-buttons-expanded-${width}.png`) });
await page.keyboard.press("Escape");
await expect(dialog).toHaveCount(0);
}
});
async function expectTaskReferenceLayout(panel: Locator) {
await expect(panel).toHaveAttribute("data-agent-starting", "true");
await expect(panel.getByRole("group", { name: "对话引用" })).toBeVisible();
await expect(panel.getByRole("group", { name: "推荐问题" })).toHaveCount(0);
await expect(panel.locator("form")).toHaveCount(1);
// Existing task state keeps the normal reading area, as required by agent-chat-ui-design.md.
const status = panel.getByRole("region", { name: "任务协作状态" });
await expect(status).toBeVisible();
await expect.poll(async () => {
const panelBox = (await panel.boundingBox())!;
const composer = (await panel.locator("form").boundingBox())!;
return panelBox.y + panelBox.height - composer.y - composer.height;
}).toBeLessThan(32);
const statusBox = (await status.boundingBox())!;
const composerBox = (await panel.locator("form").boundingBox())!;
expect(composerBox.y).toBeGreaterThanOrEqual(statusBox.y + statusBox.height);
}
test("单一协作入口展示任务引用,保留草稿且仅手动发送,并适配窄屏", async ({ page }, testInfo) => {
test.setTimeout(60_000);
await page.emulateMedia({ reducedMotion: "reduce" });
await mockWorkbenchContext(page);
await mockAgentApi(page, { sessions: [{ session_id: "ordinary-main", title: "供水核查主聊天", purpose: "ordinary_chat", created_at: now, updated_at: now }] });
let submitted = 0;
let inboxReads = 0;
let incoming = false;
page.on("request", (request) => {
if (request.method() === "POST" && (new URL(request.url()).pathname.endsWith("/responses") || new URL(request.url()).pathname.endsWith("/investigations"))) submitted += 1;
});
const { latest_plan, latest_report, latest_progress, evidence, actions, access_requests, work_order_snapshot, permissions, ...summary } = task;
void latest_plan; void latest_report; void latest_progress; void evidence; void actions; void access_requests; void work_order_snapshot; void permissions;
await page.route("**/workbench/sync-snapshot", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
schema: "workbench-sync-snapshot@1", event_cursor: 0, runtime: { availability: "not_configured", source: "none", scenario_availability: "not_configured",
artifact_availability: "not_configured", schedule_availability: "not_configured", work_order_availability: "not_configured", alert_availability: "not_configured",
generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: [] }, tasks: [summary], notifications: [], generated_at: now
} }));
await page.route("**/operational-tasks/**", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: task }));
await page.route("**/agent-messages?**", (route) => {
inboxReads += 1;
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: { items: incoming ? [{ message_id: "msg1", task_id: taskId, run_id: runId,
source_session_id: "investigation-run", target_session_id: "ordinary-main", direction: "to_chat", text: "已核对上游监测,请补充现场采样记录。",
status: "pending", in_reply_to: null, created_at: now, failure_detail: null }] : [], next_cursor: null } });
});
await page.route("**/agent-messages/stream", (route) => route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, contentType: "text/event-stream", body: 'event: agent_message\ndata: {"session_id":"ordinary-main"}\n\n' }));
await page.goto("/", { waitUntil: "domcontentloaded" });
await page.getByRole("button", { name: /展开 Agent 助手面板/ }).click();
const agent = page.getByRole("complementary", { name: "Agent 命令面板" });
await expect(agent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0);
await agent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
const prompt = agent.locator("textarea");
await prompt.fill("保留的现场补充草稿");
await page.getByRole("button", { name: "工况任务", exact: true }).click();
const feed = page.getByRole("region", { name: "工况任务", exact: true });
await expect(feed).toBeVisible();
await feed.getByRole("button", { name: "展开工况任务" }).click();
await feed.getByRole("button", { name: /调查任务/ }).click();
await feed.getByText("居民异味投诉调查", { exact: true }).first().click();
await expect(feed.getByText("16:00", { exact: true })).toBeVisible();
await expect(feed.getByRole("region", { name: "调查任务标识" })).toHaveCount(0);
await expect(feed.getByRole("button", { name: "在主聊天中编辑" })).toHaveCount(0);
await expect(feed).not.toContainText(taskId);
await expect(feed).not.toContainText(runId);
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveCount(1);
await expect(feed.getByRole("button", { name: "Agent 协作", exact: true })).toHaveAttribute("title", "打开调查协作对话,引用此任务,不会直接发送。");
await feed.screenshot({ path: testInfo.outputPath("investigation-entry-desktop.png") });
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
await expect(agent.getByText("任务协作问答", { exact: true }).first()).toBeVisible();
const reference = agent.getByRole("group", { name: "对话引用" });
await expect(reference).toContainText("居民异味投诉调查");
await expect(reference).toContainText("第 1 轮 · 报告第 3 版");
await expect(reference).not.toContainText(/当前|第 2 轮|已结束|调查完成|正在调查/);
await expect(prompt).toHaveAttribute("placeholder", "可询问调查进展、讨论报告或补充信息");
await expect(prompt).toHaveValue("保留的现场补充草稿");
await expect(prompt).toBeFocused();
expect(submitted).toBe(0);
await feed.getByRole("button", { name: "Agent 协作", exact: true }).click();
await expect(prompt).toBeFocused();
await expect(prompt).toHaveValue("保留的现场补充草稿");
await feed.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click();
await expect(prompt).toHaveValue("保留的现场补充草稿\n\n请解释“上游监测数据质量已核对”这条证据的范围、来源和判断影响。");
await expect(prompt).toBeFocused();
await expect(reference).toContainText("已引用证据");
await expectTaskReferenceLayout(agent);
expect(submitted).toBe(0);
await agent.screenshot({ path: testInfo.outputPath("investigation-reference-desktop.png") });
await feed.getByRole("button", { name: /定时任务/ }).click();
await page.setViewportSize({ width: 375, height: 900 });
await page.getByRole("button", { name: "打开 Agent 面板", exact: true }).click();
const mobileAgent = page.getByRole("complementary", { name: "Agent 命令面板" });
await expect(mobileAgent.getByRole("heading", { name: "我已就绪,请描述任务" })).toHaveCount(0);
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toContainText("居民异味投诉调查");
await expectTaskReferenceLayout(mobileAgent);
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-narrow.png") });
await page.setViewportSize({ width: 320, height: 900 });
await expectTaskReferenceLayout(mobileAgent);
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-reference-320.png") });
expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true);
await page.setViewportSize({ width: 375, height: 900 });
let sentContext: unknown;
await page.route("**/api/v1/responses", async (route) => {
if (route.request().method() !== "POST") return route.fallback();
sentContext = route.request().postDataJSON().tjwater.task_context;
expect(route.request().postDataJSON().conversation).toBe(`collaboration-${taskId}`);
await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "X-Vercel-AI-UI-Message-Stream": "v1" }, contentType: "text/event-stream",
body: responsesFixture([{ type: "start", messageId: "reply" }, { type: "finish", finishReason: "stop" }]) });
});
expect(submitted).toBe(0);
await mobileAgent.getByRole("button", { name: "发送 Agent 指令" }).click();
await expect.poll(() => sentContext).toEqual({ report_revision: 3, evidence_id: "monitor-1" });
expect(submitted).toBe(1);
await expect(mobileAgent).toHaveAttribute("data-agent-starting", "false");
await expect(mobileAgent.locator(".agent-panel-user-message")).toContainText("保留的现场补充草稿");
await expect.poll(async () => {
const panelBox = (await mobileAgent.boundingBox())!;
const composer = (await mobileAgent.locator("form").boundingBox())!;
return panelBox.y + panelBox.height - composer.y - composer.height;
}).toBeLessThan(32);
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
incoming = true;
await mobileAgent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
await page.getByRole("dialog", { name: "历史对话" }).getByRole("button", { name: /^供水核查主聊天/ }).click();
await expect(mobileAgent.getByRole("group", { name: "对话引用" })).toHaveCount(0);
await expect(mobileAgent.getByText("已核对上游监测,请补充现场采样记录。")).toBeVisible();
await expect(mobileAgent.getByText("待接收", { exact: true })).toBeVisible();
await expect(mobileAgent.getByRole("button", { name: "停止 Agent 分析" })).toHaveCount(0);
expect(inboxReads).toBeGreaterThan(1);
await mobileAgent.screenshot({ path: testInfo.outputPath("investigation-inbox-narrow.png") });
expect(await mobileAgent.evaluate((element) => element.scrollWidth <= element.clientWidth + 1)).toBe(true);
expect(submitted).toBe(1);
});