feat(agent): support Responses steering and project-aware tools
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.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { responsesFixture } from "./support/responses-fixture";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import type { OperationalTask, OperationalTaskNotification, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client";
|
||||
@@ -89,8 +90,8 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
const firstReport = task.latest_report!;
|
||||
let notifications: OperationalTaskNotification[] = [];
|
||||
const summary = (item: OperationalTask): OperationalTaskSummary => {
|
||||
const { latest_plan, latest_report, evidence, actions, access_requests, permissions, work_order_snapshot, ...value } = item;
|
||||
void latest_plan; void latest_report; void evidence; void actions; void access_requests; void permissions; void work_order_snapshot;
|
||||
const { latest_plan, latest_report, latest_progress, evidence, actions, access_requests, permissions, work_order_snapshot, ...value } = item;
|
||||
void latest_plan; void latest_report; void latest_progress; void evidence; void actions; void access_requests; void permissions; void work_order_snapshot;
|
||||
return value;
|
||||
};
|
||||
await page.route("http://server.test/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: {
|
||||
@@ -194,7 +195,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
};
|
||||
const inlineControls = await captureDiagramControls(diagram);
|
||||
const assertControlGeometry = (controls: typeof inlineControls) => {
|
||||
expect(controls).toHaveLength(4);
|
||||
expect(controls).toHaveLength(3);
|
||||
for (const control of controls) {
|
||||
expect({ width: control.width, height: control.height, radius: control.radius,
|
||||
iconWidth: control.iconWidth, iconHeight: control.iconHeight }).toEqual({
|
||||
@@ -205,27 +206,20 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
const assertInlineControlRail = async (surface: typeof diagram) => {
|
||||
const mermaidBlock = surface.locator('[data-streamdown="mermaid-block"]');
|
||||
const graph = surface.locator('[data-streamdown="mermaid"]');
|
||||
const fullscreenButton = surface.getByRole("button", { name: "全屏查看", exact: true });
|
||||
const fullscreenButton = surface.getByRole("button", { name: "放大查看", exact: true });
|
||||
const zoomInButton = surface.getByRole("button", { name: "Zoom in", exact: true });
|
||||
const fullscreenGroup = surface.locator('[data-streamdown="mermaid-block-actions"]');
|
||||
const zoomGroup = zoomInButton.locator("..");
|
||||
const [blockBox, graphBox, fullscreenBox, zoomInBox, fullscreenGroupBox, zoomGroupBox] = await Promise.all([
|
||||
const [blockBox, graphBox, fullscreenBox, zoomInBox, zoomGroupBox] = await Promise.all([
|
||||
mermaidBlock.boundingBox(), graph.boundingBox(), fullscreenButton.boundingBox(), zoomInButton.boundingBox(),
|
||||
fullscreenGroup.boundingBox(), zoomGroup.boundingBox()
|
||||
zoomGroup.boundingBox()
|
||||
]);
|
||||
expect(blockBox).not.toBeNull();
|
||||
expect(graphBox).not.toBeNull();
|
||||
expect(fullscreenBox).not.toBeNull();
|
||||
expect(zoomInBox).not.toBeNull();
|
||||
expect(fullscreenGroupBox).not.toBeNull();
|
||||
expect(zoomGroupBox).not.toBeNull();
|
||||
expect(Math.round(graphBox!.y - blockBox!.y)).toBeLessThanOrEqual(10);
|
||||
expect(Math.round(fullscreenBox!.x)).toBe(Math.round(zoomInBox!.x));
|
||||
expect(Math.round(zoomInBox!.y - fullscreenBox!.y - fullscreenBox!.height)).toBe(18);
|
||||
expect(Math.round(fullscreenGroupBox!.x)).toBe(Math.round(zoomGroupBox!.x));
|
||||
expect(Math.round(fullscreenGroupBox!.width)).toBe(Math.round(zoomGroupBox!.width));
|
||||
expect(Math.round(zoomGroupBox!.y - fullscreenGroupBox!.y - fullscreenGroupBox!.height)).toBe(8);
|
||||
expect(Math.round(blockBox!.x + blockBox!.width - fullscreenGroupBox!.x - fullscreenGroupBox!.width)).toBe(18);
|
||||
expect(fullscreenBox!.y + fullscreenBox!.height).toBeLessThanOrEqual(blockBox!.y);
|
||||
expect(Math.round(graphBox!.x + graphBox!.width - zoomGroupBox!.x - zoomGroupBox!.width)).toBe(8);
|
||||
};
|
||||
assertControlGeometry(inlineControls);
|
||||
@@ -259,31 +253,30 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
await expectThemeColor(zoomIn, "outline-color", "--action-focus-ring");
|
||||
await zoomIn.locator("..").screenshot({ path: testInfo.outputPath("diagram-controls-keyboard-focus.png") });
|
||||
await testInfo.attach("styled-investigation-diagram", { body: await diagram.screenshot({ path: testInfo.outputPath("styled-investigation-diagram.png") }), contentType: "image/png" });
|
||||
await diagram.getByRole("button", { name: "全屏查看", exact: true }).click();
|
||||
const fullscreen = page.locator('body > div[role="button"]').filter({ has: page.getByRole("button", { name: "退出全屏", exact: true }) });
|
||||
await diagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
const fullscreen = page.getByRole("dialog", { name: "调查脉络", exact: true });
|
||||
await expect(fullscreen.locator("svg[id]")).toHaveCount(1);
|
||||
const fullscreenControls = await captureDiagramControls(fullscreen);
|
||||
assertControlGeometry(fullscreenControls);
|
||||
const assertFullscreenCloseFrame = async () => {
|
||||
const close = fullscreen.getByRole("button", { name: "退出全屏", exact: true });
|
||||
const close = fullscreen.getByRole("button", { name: "关闭详情", exact: true });
|
||||
const frame = await close.evaluate((button) => {
|
||||
const rect = button.getBoundingClientRect();
|
||||
const panel = button.closest('[role="dialog"]')!.getBoundingClientRect();
|
||||
return {
|
||||
top: rect.top,
|
||||
right: window.innerWidth - rect.right,
|
||||
boxShadow: getComputedStyle(button).boxShadow
|
||||
top: rect.top - panel.top,
|
||||
right: panel.right - rect.right
|
||||
};
|
||||
});
|
||||
expect(Math.round(frame.top)).toBe(21);
|
||||
expect(Math.round(frame.right)).toBe(21);
|
||||
expect(frame.boxShadow).not.toBe("none");
|
||||
expect(frame.top).toBeGreaterThanOrEqual(12);
|
||||
expect(frame.right).toBeGreaterThanOrEqual(12);
|
||||
};
|
||||
await assertFullscreenCloseFrame();
|
||||
await writeFile(testInfo.outputPath("diagram-control-geometry.json"), JSON.stringify({ viewport: page.viewportSize(), inline: inlineControls, fullscreen: fullscreenControls }, null, 2));
|
||||
await testInfo.attach("styled-investigation-fullscreen", { body: await page.screenshot({ path: testInfo.outputPath("styled-investigation-fullscreen.png") }), contentType: "image/png" });
|
||||
await fullscreen.getByRole("button", { name: "退出全屏", exact: true }).click();
|
||||
await fullscreen.getByRole("button", { name: "关闭详情", exact: true }).click();
|
||||
await expect(fullscreen).toHaveCount(0);
|
||||
await diagram.getByRole("button", { name: "全屏查看", exact: true }).click();
|
||||
await diagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
await expect(fullscreen).toBeVisible();
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(fullscreen).toHaveCount(0);
|
||||
@@ -347,14 +340,18 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
report_reference: { execution_generation: 1, revision: 2 }, resolved_at: null, resolution_reason: null,
|
||||
status: "unread", version: 1, required_permission: null, created_at: now, read_at: null, read_by: null }];
|
||||
await panel.getByRole("button", { name: "刷新工况任务", exact: true }).click();
|
||||
await expect(panel.getByRole("button", { name: "查看新报告" })).toBeVisible();
|
||||
await expect(panel.getByText("新报告", { exact: true })).toBeVisible();
|
||||
expect(notifications[0].status).toBe("unread");
|
||||
await expect(panel.getByLabel("任务报告", { exact: true })
|
||||
.getByText("上游记录未见同期异常,需现场核查用户端。", { exact: true })).toBeVisible();
|
||||
await panel.getByRole("button", { name: "查看新报告" }).click();
|
||||
// Live is the default; only explicitly choosing a historical version freezes it.
|
||||
await expect(panel.getByRole("button", { name: "查看新报告" })).toHaveCount(0);
|
||||
await expect.poll(() => notifications[0].status).toBe("read");
|
||||
await expect(panel.getByText("本轮已结束 · 无法形成结论", { exact: true })).toBeVisible();
|
||||
// Existing investigations may only have a same-round saved plan, not an embedded diagram.
|
||||
await expect(diagram).toBeAttached();
|
||||
await diagram.scrollIntoViewIfNeeded();
|
||||
await expect(diagram.locator("svg[id]")).toHaveCount(1);
|
||||
await expect(diagram).toContainText("多源数据核对");
|
||||
await expect(diagram.locator("svg g.node.tjRunning")).toHaveCount(0);
|
||||
await testInfo.attach("legacy-investigation-diagram", { body: await diagram.screenshot({
|
||||
path: testInfo.outputPath("legacy-investigation-diagram.png") }), contentType: "image/png" });
|
||||
await expect(panel.getByLabel("数据缺口")).toContainText("无法区分管网与用户端异常");
|
||||
await expect(panel.getByText("已结束 · 证据不足", { exact: true }).first()).toHaveAttribute("data-status-tone", "warning");
|
||||
await expect(panel.getByRole("button", { name: "确认并创建预约工单" })).toHaveCount(0);
|
||||
@@ -364,12 +361,12 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
await panel.getByRole("heading", { name: "当前结果", exact: true }).scrollIntoViewIfNeeded();
|
||||
await testInfo.attach("insufficient-report-desktop", { body: await panel.screenshot({
|
||||
path: testInfo.outputPath("insufficient-report-desktop.png") }), contentType: "image/png" });
|
||||
await panel.getByText("任务时间线", { exact: true }).click();
|
||||
const timeline = panel.getByLabel("任务时间线记录");
|
||||
await panel.getByRole("button", { name: "展开任务时间线" }).click();
|
||||
const timeline = page.getByRole("dialog", { name: "任务时间线" }).getByLabel("任务时间线记录");
|
||||
await expect(timeline).toContainText("采样工单已创建");
|
||||
await testInfo.attach("investigation-timeline-desktop", { body: await timeline.screenshot({
|
||||
path: testInfo.outputPath("investigation-timeline-desktop.png") }), contentType: "image/png" });
|
||||
await panel.getByText("任务时间线", { exact: true }).click();
|
||||
await page.getByRole("dialog", { name: "任务时间线" }).getByRole("button", { name: "关闭详情" }).click();
|
||||
|
||||
for (const width of [375, 320]) {
|
||||
await page.setViewportSize({ width, height: 720 });
|
||||
@@ -389,12 +386,12 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
assertControlGeometry(await captureDiagramControls(mobileDiagram));
|
||||
await assertInlineControlRail(mobileDiagram);
|
||||
if (width === 320) {
|
||||
await mobileDiagram.getByRole("button", { name: "全屏查看", exact: true }).click();
|
||||
await mobileDiagram.getByRole("button", { name: "放大查看", exact: true }).click();
|
||||
await expect(fullscreen.locator("svg[id]")).toHaveCount(1);
|
||||
assertControlGeometry(await captureDiagramControls(fullscreen));
|
||||
await assertFullscreenCloseFrame();
|
||||
await testInfo.attach("diagram-fullscreen-320", { body: await page.screenshot({ path: testInfo.outputPath("diagram-fullscreen-320.png") }), contentType: "image/png" });
|
||||
await fullscreen.getByRole("button", { name: "退出全屏", exact: true }).evaluate((button) => button.click());
|
||||
await fullscreen.getByRole("button", { name: "关闭详情", exact: true }).evaluate((button) => button.click());
|
||||
await expect(fullscreen).toHaveCount(0);
|
||||
}
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual(width);
|
||||
@@ -405,21 +402,26 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
});
|
||||
}
|
||||
await page.setViewportSize({ width: 1280, height: 720 });
|
||||
await panel.getByText("报告版本", { exact: true }).click();
|
||||
await expect(panel.getByRole("button", { name: /报告第 2 版 · 最新版/ })).toBeVisible();
|
||||
await panel.getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
|
||||
await panel.getByRole("button", { name: "展开报告版本" }).click();
|
||||
const versions = page.getByRole("dialog", { name: "报告版本" });
|
||||
await expect(versions.getByRole("button", { name: /报告第 2 版 · 最新版/ })).toBeVisible();
|
||||
await versions.getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
|
||||
await expect(panel.getByLabel("任务报告", { exact: true })
|
||||
.getByText("上游记录未见同期异常,需现场核查用户端。", { exact: true })).toBeVisible();
|
||||
await page.route("**/api/v1/agent/chat/stream", async (route) => {
|
||||
await page.route("**/api/v1/responses", async (route) => {
|
||||
if (route.request().method() !== "POST") return route.fallback();
|
||||
expect(route.request().postDataJSON().task_context).toEqual({ report_revision: 1, evidence_id: "monitor-1" });
|
||||
expect(route.request().postDataJSON().tjwater.task_context).toEqual({ report_revision: 1, evidence_id: "monitor-1" });
|
||||
await route.fulfill({ headers: { "Access-Control-Allow-Origin": "*", "Content-Type": "text/event-stream", "X-Vercel-AI-UI-Message-Stream": "v1" },
|
||||
body: [{ type: "start", messageId: "selected-report-answer" },
|
||||
body: responsesFixture([{ type: "start", messageId: "selected-report-answer" },
|
||||
{ type: "data-task_context", data: { task_id: taskId, execution_generation: 1, report_revision: 1 } },
|
||||
{ type: "text-start", id: "answer" }, { type: "text-delta", id: "answer", delta: "这条依据仅覆盖上游监测范围。" },
|
||||
{ type: "text-end", id: "answer" }, { type: "finish", finishReason: "stop" }].map((part) => `data: ${JSON.stringify(part)}\n\n`).join("") });
|
||||
{ type: "text-end", id: "answer" }, { type: "finish", finishReason: "stop" }]) });
|
||||
});
|
||||
await panel.getByLabel("主要依据").getByRole("button", { name: "围绕此证据提问" }).click();
|
||||
await expect(agentPanel.getByRole("group", { name: "对话引用" })).toContainText("报告第 1 版");
|
||||
await expect(agentPanel.locator("textarea")).toBeFocused();
|
||||
await expect(agentPanel.locator("textarea")).toHaveValue(/请解释.*这条证据的范围、来源和判断影响/);
|
||||
await agentPanel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
||||
await expect(agentPanel).toContainText("依据:第 1 轮 · 报告第 1 版");
|
||||
task = { ...task, revision: 9, execution_generation: 2, current_report_revision: 3,
|
||||
latest_report: { ...firstReport, revision: 3, execution_generation: 2, report: {
|
||||
@@ -430,7 +432,7 @@ test("调查报告展示 SVG 脉络、处置进展和报告版本,缺数正常
|
||||
await page.getByRole("button", { name: "折叠 Agent 面板", exact: true }).click();
|
||||
const reopen = panel.getByRole("button", { name: "展开工况任务" });
|
||||
if (await reopen.isVisible()) await reopen.click();
|
||||
const viewNew = panel.getByRole("button", { name: "查看新报告" });
|
||||
const viewNew = panel.getByRole("button", { name: "返回实时内容" });
|
||||
if (await viewNew.isVisible()) await viewNew.click();
|
||||
await panel.getByLabel("调查脉络", { exact: true }).scrollIntoViewIfNeeded();
|
||||
await expect(panel.getByText("图示暂不可用,仍可查看结论、证据和待审核动作。", { exact: true })).toBeVisible();
|
||||
|
||||
Reference in New Issue
Block a user