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('
');
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((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);
});