293 lines
21 KiB
TypeScript
293 lines
21 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();
|
|
const reference = panel.getByRole("group", { name: "对话引用" });
|
|
await expect(reference).toHaveCSS("background-color", "rgba(0, 0, 0, 0)");
|
|
await expect(reference).toHaveCSS("font-size", "14px");
|
|
expect((await reference.boundingBox())!.height).toBeLessThanOrEqual(48);
|
|
expect(await reference.evaluate((element) => element.scrollWidth <= element.clientWidth)).toBe(true);
|
|
await expect(panel.locator("form").getByText("任务协作问答", { exact: true })).toHaveCount(0);
|
|
await expect(panel.getByRole("group", { name: "推荐问题" })).toHaveCount(0);
|
|
await expect(panel.locator("form")).toHaveCount(1);
|
|
await expect(panel.locator("form")).not.toHaveCSS("box-shadow", "none");
|
|
const form = panel.locator("form");
|
|
const appearance = () => form.evaluate((element) => {
|
|
const style = getComputedStyle(element);
|
|
return { border: style.border, shadow: style.boxShadow, outline: style.outlineStyle };
|
|
});
|
|
await panel.getByRole("textbox").evaluate((element) => element.blur());
|
|
const unfocusedAppearance = await appearance();
|
|
await panel.getByRole("textbox").focus();
|
|
const focusedAppearance = await appearance();
|
|
expect(focusedAppearance.border).toBe(unfocusedAppearance.border);
|
|
expect(focusedAppearance.outline).toBe(unfocusedAppearance.outline);
|
|
expect(focusedAppearance.shadow).not.toBe(unfocusedAppearance.shadow);
|
|
expect(unfocusedAppearance.outline).toBe("none");
|
|
for (const detail of await reference.locator("[data-agent-reference-detail] > span").all()) {
|
|
expect((await detail.boundingBox())!.height).toBeLessThanOrEqual(20);
|
|
}
|
|
const modelBox = (await panel.getByRole("button", { name: "选择 Agent 模型" }).boundingBox())!;
|
|
const submitBox = (await panel.getByRole("button", { name: "发送 Agent 指令" }).boundingBox())!;
|
|
const approval = panel.getByRole("button", { name: "权限批准模式" });
|
|
await expect(approval).toBeEnabled();
|
|
await expect(approval).toHaveText("请求批准");
|
|
const approvalBox = (await approval.boundingBox())!;
|
|
expect(approvalBox.x).toBeLessThan(modelBox.x);
|
|
expect(approvalBox.y).toBe(submitBox.y);
|
|
await expect(approval.locator("svg").first()).toHaveCSS("width", "16px");
|
|
await expect(approval.locator("svg").last()).toHaveCSS("width", "12px");
|
|
await expect(panel.getByRole("button", { name: "选择 Agent 模型" }).locator("svg").first()).toHaveCSS("width", "16px");
|
|
const sendIcon = panel.getByRole("button", { name: "发送 Agent 指令" }).locator("svg");
|
|
await expect(sendIcon).toHaveCSS("width", "20px");
|
|
const iconBox = (await sendIcon.boundingBox())!;
|
|
expect(Math.abs(iconBox.x + iconBox.width / 2 - submitBox.x - submitBox.width / 2)).toBeLessThan(0.5);
|
|
expect(Math.abs(iconBox.y + iconBox.height / 2 - submitBox.y - submitBox.height / 2)).toBeLessThan(0.5);
|
|
expect(modelBox.x).toBeLessThan(submitBox.x);
|
|
expect(modelBox.y).toBe(submitBox.y);
|
|
const formBox = (await form.boundingBox())!;
|
|
const toolsBox = (await panel.locator(".agent-panel-input-tools").boundingBox())!;
|
|
expect(Math.abs((toolsBox.x - formBox.x) - (formBox.x + formBox.width - toolsBox.x - toolsBox.width))).toBeLessThan(1);
|
|
await expect(panel.getByRole("region", { name: "任务协作状态" })).toHaveCount(0);
|
|
await expect(panel.getByRole("button", { name: "确认并创建预约工单" })).toHaveCount(0);
|
|
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);
|
|
}
|
|
|
|
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 collaborationOpens = 0;
|
|
const renamedCollaborationTitle = "异味投诉现场复核记录";
|
|
let finishReopen!: () => void;
|
|
const reopenGate = new Promise<void>((resolve) => { finishReopen = resolve; });
|
|
await page.route(`**/task/${taskId}/collaboration/session`, async (route) => {
|
|
collaborationOpens += 1;
|
|
if (collaborationOpens === 2) await reopenGate;
|
|
return route.fulfill({ headers: { "Access-Control-Allow-Origin": "*" }, json: {
|
|
session_id: `collaboration-${taskId}`, purpose: "task_collaboration", task_id: taskId,
|
|
title: collaborationOpens <= 2 ? task.title : renamedCollaborationTitle,
|
|
created: collaborationOpens === 1, status: "active",
|
|
} });
|
|
});
|
|
await page.route(`**/session/collaboration-${taskId}/title`, (route) => route.fulfill({
|
|
headers: { "Access-Control-Allow-Origin": "*" }, json: { id: `collaboration-${taskId}`, title: route.request().postDataJSON().title },
|
|
}));
|
|
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();
|
|
// The session fixture still has a legacy title; the creation receipt is authoritative.
|
|
await expect(agent.locator(".agent-chat-context-bar > p")).toHaveText(task.title);
|
|
await expect(agent.locator("form").getByText("任务协作问答", { exact: true })).toHaveCount(0);
|
|
const reference = agent.getByRole("group", { name: "对话引用" });
|
|
await expect(reference).toContainText("居民异味投诉调查");
|
|
await expect(reference).toContainText("第 1 轮 · 报告第 3 版");
|
|
expect((await reference.boundingBox())!.height).toBe(28);
|
|
const titleBox = (await reference.locator("[data-agent-reference-title]").boundingBox())!;
|
|
const detailBox = (await reference.locator("[data-agent-reference-detail]").boundingBox())!;
|
|
expect(Math.abs(titleBox.y - detailBox.y)).toBeLessThanOrEqual(1);
|
|
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.poll(() => collaborationOpens).toBe(2);
|
|
await agent.getByRole("button", { name: "打开 Agent 历史记录" }).click();
|
|
const renameDialog = page.getByRole("dialog", { name: "历史对话" });
|
|
await renameDialog.getByRole("button", { name: `会话操作:${task.title}` }).click();
|
|
await page.getByRole("menuitem", { name: "重命名对话", exact: true }).click();
|
|
await renameDialog.getByRole("textbox", { name: "会话标题", exact: true }).fill(renamedCollaborationTitle);
|
|
await renameDialog.getByRole("button", { name: "保存对话标题" }).click();
|
|
await expect(renameDialog.locator("button.agent-history-session-trigger").filter({ hasText: renamedCollaborationTitle })).toBeVisible();
|
|
await page.keyboard.press("Escape");
|
|
finishReopen();
|
|
await expect(prompt).toBeFocused();
|
|
await expect(agent.locator(".agent-chat-context-bar > p")).toHaveText(renamedCollaborationTitle);
|
|
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;
|
|
const approval = mobileAgent.getByRole("button", { name: "权限批准模式" });
|
|
await approval.click();
|
|
await page.getByRole("menuitem", { name: /自动批准本轮工具权限请求/ }).click();
|
|
await expect(approval).toHaveText("始终允许");
|
|
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().tjwater.approval_mode).toBe("always");
|
|
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 expect(page.getByRole("dialog", { name: "历史对话" }).locator("button.agent-history-session-trigger").filter({ hasText: renamedCollaborationTitle })).toBeVisible();
|
|
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);
|
|
});
|