fix: preserve investigation execution history by round
This commit is contained in:
@@ -0,0 +1,261 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import type { OperationalTask, OperationalTaskSummary } from "../../src/features/workbench/api/operational-task-client";
|
||||
import { mockAgentApi } from "./support/mock-agent-api";
|
||||
|
||||
const projectId = "324ff7ee-c2d4-5184-a84b-48fb14a2933e";
|
||||
const taskId = "82000000-0000-4000-8000-000000000011";
|
||||
const runId = "82500000-0000-4000-8000-000000000011";
|
||||
const previousRunId = "82500000-0000-4000-8000-000000000010";
|
||||
const sessionId = "investigation-execution-session";
|
||||
const previousSessionId = "investigation-execution-session-previous";
|
||||
const now = "2026-10-30T08:00:00Z";
|
||||
|
||||
const latestRun: NonNullable<OperationalTask["latest_run"]> = {
|
||||
run_id: runId,
|
||||
task_id: taskId,
|
||||
generation: 2,
|
||||
execution_request_id: "85000000-0000-4000-8000-000000000011",
|
||||
status: "succeeded",
|
||||
revision: 4,
|
||||
agent_session_id: sessionId,
|
||||
agent_instance_id: "agent-supply-1",
|
||||
lease_expires_at: null,
|
||||
heartbeat_at: now,
|
||||
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 previousRun: NonNullable<OperationalTask["latest_run"]> = {
|
||||
...latestRun,
|
||||
run_id: previousRunId,
|
||||
generation: 1,
|
||||
execution_request_id: "85000000-0000-4000-8000-000000000010",
|
||||
agent_session_id: previousSessionId,
|
||||
created_at: "2026-10-29T08:00:00Z",
|
||||
updated_at: "2026-10-29T08:05:00Z"
|
||||
};
|
||||
|
||||
const latestReport: NonNullable<OperationalTask["latest_report"]> = {
|
||||
revision: 2,
|
||||
execution_generation: 2,
|
||||
source_session_id: sessionId,
|
||||
published_at: now,
|
||||
published_by: "agent",
|
||||
report: {
|
||||
title: "南桥爆管调查报告",
|
||||
current_conclusion: "第二轮已完成现场记录复核",
|
||||
conclusion_boundary: "以第二轮调查数据为准",
|
||||
evidence_refs: [],
|
||||
recommendations: [],
|
||||
pending_items: []
|
||||
}
|
||||
};
|
||||
|
||||
const previousReport: NonNullable<OperationalTask["latest_report"]> = {
|
||||
...latestReport,
|
||||
revision: 1,
|
||||
execution_generation: 1,
|
||||
source_session_id: previousSessionId,
|
||||
published_at: "2026-10-29T08:05:00Z",
|
||||
report: {
|
||||
...latestReport.report,
|
||||
current_conclusion: "第一轮完成压力与流量核对",
|
||||
conclusion_boundary: "尚未取得现场阀门记录"
|
||||
}
|
||||
};
|
||||
|
||||
const task: OperationalTask = {
|
||||
project_id: projectId,
|
||||
task_id: taskId,
|
||||
task_type: "event_investigation",
|
||||
workflow: "burst-report",
|
||||
lifecycle_status: "completed",
|
||||
active_run: null,
|
||||
latest_run: latestRun,
|
||||
status: "completed",
|
||||
priority: "normal",
|
||||
revision: 2,
|
||||
execution_generation: 2,
|
||||
execution_request_id: "85000000-0000-4000-8000-000000000011",
|
||||
title: "南桥爆管调查",
|
||||
summary: "核对压力、流量和现场反馈",
|
||||
source: { kind: "operator", external_id: null, external_revision: null, event_kind: "burst" },
|
||||
trigger: { kind: "manual", occurred_at: now, detail: "调试调查执行历史" },
|
||||
scheduled_for: null,
|
||||
agent_session_id: sessionId,
|
||||
agent_lease_expires_at: null,
|
||||
current_plan_revision: null,
|
||||
current_report_revision: 2,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
latest_plan: null,
|
||||
latest_report: latestReport,
|
||||
latest_progress: null,
|
||||
evidence: [],
|
||||
actions: [],
|
||||
access_requests: [],
|
||||
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
|
||||
},
|
||||
work_order_snapshot: null
|
||||
};
|
||||
|
||||
const summary = (item: OperationalTask): OperationalTaskSummary => {
|
||||
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;
|
||||
};
|
||||
|
||||
test("调查任务执行记录自动刷新并可创建调试会话追加消息", async ({ page }, testInfo) => {
|
||||
await page.emulateMedia({ reducedMotion: "reduce" });
|
||||
await mockAgentApi(page);
|
||||
let executionHistoryReads = 0;
|
||||
let debugBranchCreates = 0;
|
||||
const openedBindings: Array<{ runId: string | null; sessionId: string | null }> = [];
|
||||
let debugBranchPayload: unknown;
|
||||
await page.route(`http://127.0.0.1:8787/api/v1/task/${taskId}/execution-session?**`, async (route) => {
|
||||
executionHistoryReads += 1;
|
||||
const refreshed = executionHistoryReads > 1;
|
||||
const query = new URL(route.request().url()).searchParams;
|
||||
openedBindings.push({ runId: query.get("run_id"), sessionId: query.get("session_id") });
|
||||
await route.fulfill({
|
||||
headers: { "Access-Control-Allow-Origin": "*" },
|
||||
json: {
|
||||
id: previousSessionId,
|
||||
session_id: previousSessionId,
|
||||
title: "南桥爆管调查 · 第一轮执行记录",
|
||||
purpose: "task_execution",
|
||||
task_id: taskId,
|
||||
status: "active",
|
||||
created_at: now,
|
||||
updated_at: refreshed ? "2026-10-30T08:01:00Z" : now,
|
||||
messages: [
|
||||
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||
{ id: "execution-assistant", role: "assistant", parts: [
|
||||
{ type: "data-progress", data: { id: "pressure-flow", phase: "tool", title: "核对第一轮压力与流量", detail: "查询监测记录并校验时间范围。", status: "completed" } },
|
||||
{ type: "text", text: "第一轮已完成压力与流量证据核对" }
|
||||
] },
|
||||
...(refreshed ? [{ id: "execution-refresh", role: "assistant", parts: [{ type: "text", text: "第一轮自动同步发现现场阀门记录" }] }] : [])
|
||||
],
|
||||
is_streaming: !refreshed,
|
||||
run_status: refreshed ? "completed" : "running",
|
||||
read_only: true
|
||||
}
|
||||
});
|
||||
});
|
||||
page.on("request", (request) => {
|
||||
if (request.method() === "POST" && request.url().endsWith(`/task/${taskId}/execution-session/debug`)) {
|
||||
debugBranchCreates += 1;
|
||||
debugBranchPayload = request.postDataJSON();
|
||||
}
|
||||
});
|
||||
await page.route("**/runtime-config.js", (route) => route.fulfill({
|
||||
contentType: "text/javascript",
|
||||
body: `globalThis.__TJWATER_CONFIG__=${JSON.stringify({
|
||||
TJWATER_AUTH_MODE: "disabled",
|
||||
TJWATER_SERVER_API_BASE_URL: "http://server.test",
|
||||
TJWATER_MAP_URL: "http://map.test/geoserver",
|
||||
TJWATER_GEOSERVER_WORKSPACE: "tjwater_next"
|
||||
})}`
|
||||
}));
|
||||
await page.route("http://server.test/api/v1/projects**", (route) => route.fulfill({ json: {
|
||||
items: [{ project_id: projectId, gs_workspace: "tjwater_next", status: "active" }], total: 1, limit: 1000, offset: 0
|
||||
} }));
|
||||
await page.route("http://server.test/api/v1/scada-devices**", (route) => route.fulfill({ json: { items: [], total: 0, limit: 1000, offset: 0 } }));
|
||||
await page.route("http://server.test/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ 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(task)],
|
||||
notifications: [],
|
||||
generated_at: now
|
||||
} }));
|
||||
await page.route("http://server.test/api/v1/workbench/events**", (route) => route.fulfill({ contentType: "text/event-stream", body: ": keepalive\n\n" }));
|
||||
await page.route("http://server.test/api/v1/operational-task-notifications**", (route) => route.fulfill({ json: { items: [], limit: 100, offset: 0, next_offset: null } }));
|
||||
await page.route("http://server.test/api/v1/operational-tasks**", (route) => {
|
||||
const path = new URL(route.request().url()).pathname;
|
||||
if (path.endsWith("/operational-tasks/schedules")) return route.fulfill({ json: { items: [] } });
|
||||
if (path.endsWith(`/operational-tasks/${taskId}/reports`)) return route.fulfill({ json: {
|
||||
items: [latestReport, previousReport], next_offset: null
|
||||
} });
|
||||
if (path.endsWith(`/operational-tasks/${taskId}/runs`)) return route.fulfill({ json: {
|
||||
items: [latestRun, previousRun], next_cursor: null
|
||||
} });
|
||||
if (path.endsWith(`/operational-tasks/${taskId}`)) return route.fulfill({ json: task });
|
||||
if (path.endsWith("/operational-tasks")) return route.fulfill({ json: { items: [summary(task)], limit: 50, offset: 0, next_offset: null } });
|
||||
return route.fulfill({ status: 404, json: {} });
|
||||
});
|
||||
await page.route("http://map.test/**", (route) => route.fulfill({ status: 204 }));
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("button", { name: "工况任务", exact: true }).click();
|
||||
const taskPanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
const expand = taskPanel.getByRole("button", { name: "展开工况任务" });
|
||||
if (await expand.isVisible()) await expand.click();
|
||||
await taskPanel.getByRole("button", { name: /调查任务/ }).click();
|
||||
await taskPanel.getByText("南桥爆管调查", { exact: true }).first().click();
|
||||
await expect(taskPanel.getByText("调查任务 · 第 2 轮", { exact: true })).toBeVisible();
|
||||
await taskPanel.getByRole("button", { name: "展开报告版本" }).click();
|
||||
await page.getByRole("dialog", { name: "报告版本" }).getByRole("button", { name: /第 1 轮 · 报告第 1 版/ }).click();
|
||||
await expect(taskPanel.getByText("调查任务 · 第 1 轮", { exact: true })).toBeVisible();
|
||||
|
||||
const executionHistory = taskPanel.getByRole("button", { name: "执行记录" });
|
||||
await expect(executionHistory).toBeVisible();
|
||||
await executionHistory.click();
|
||||
|
||||
const agentPanel = page.getByRole("complementary", { name: "Agent 命令面板" });
|
||||
await expect(agentPanel.getByRole("note").getByText("执行记录自动同步", { exact: true })).toBeVisible();
|
||||
await expect(agentPanel).toContainText("第一轮已完成压力与流量证据核对");
|
||||
const processToggle = agentPanel.getByRole("button", { name: "查看处理过程", exact: true }).first();
|
||||
await expect(processToggle).toBeVisible();
|
||||
await processToggle.click();
|
||||
await expect(agentPanel.getByText("核对第一轮压力与流量", { exact: true })).toBeVisible();
|
||||
await expect(agentPanel.locator("textarea")).toBeVisible();
|
||||
await expect(agentPanel).toContainText("第一轮自动同步发现现场阀门记录", { timeout: 5_000 });
|
||||
expect(executionHistoryReads).toBeGreaterThanOrEqual(2);
|
||||
expect(openedBindings).toEqual(expect.arrayContaining([
|
||||
{ runId: previousRunId, sessionId: previousSessionId }
|
||||
]));
|
||||
|
||||
await agentPanel.locator("textarea").fill("继续核对失败前最后一次工具调用");
|
||||
await agentPanel.getByRole("button", { name: "发送 Agent 指令" }).click();
|
||||
await expect.poll(() => debugBranchCreates).toBe(1);
|
||||
expect(debugBranchPayload).toEqual({ run_id: previousRunId, session_id: previousSessionId });
|
||||
await expect(agentPanel).toContainText("继续核对失败前最后一次工具调用");
|
||||
await expect(agentPanel.getByRole("note").getByText("执行记录自动同步", { exact: true })).toHaveCount(0);
|
||||
await testInfo.attach("desktop-execution-history", {
|
||||
body: await agentPanel.screenshot(), contentType: "image/png"
|
||||
});
|
||||
|
||||
await page.getByRole("button", { name: "折叠 Agent 面板", exact: true }).click();
|
||||
await page.setViewportSize({ width: 375, height: 720 });
|
||||
const openTasks = page.getByRole("button", { name: "打开工况任务", exact: true });
|
||||
if (await openTasks.isVisible()) await openTasks.click();
|
||||
const mobilePanel = page.getByRole("region", { name: "工况任务", exact: true });
|
||||
const mobileExpand = mobilePanel.getByRole("button", { name: "展开工况任务" });
|
||||
if (await mobileExpand.isVisible()) await mobileExpand.click();
|
||||
await mobilePanel.getByRole("button", { name: /调查任务/ }).click();
|
||||
await expect(mobilePanel.getByRole("button", { name: "执行记录" })).toBeVisible();
|
||||
await testInfo.attach("mobile-execution-history-button", {
|
||||
body: await page.getByRole("region", { name: "工况任务抽屉" }).screenshot(), contentType: "image/png"
|
||||
});
|
||||
});
|
||||
@@ -21,6 +21,7 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
icon: "bolt" as const
|
||||
};
|
||||
const triggeredSessions = new Map<string, { sessionId: string; triggerId: string }>();
|
||||
const executionDebugSessions = new Map<string, { taskId: string; sourceSessionId: string }>();
|
||||
await page.route("http://127.0.0.1:8787/api/v1/**", async (route) => {
|
||||
if (route.request().method() === "OPTIONS") {
|
||||
await route.fulfill({
|
||||
@@ -70,6 +71,58 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
return;
|
||||
}
|
||||
|
||||
const executionHistoryMatch = pathname.match(/\/task\/([^/]+)\/execution-session$/);
|
||||
if (executionHistoryMatch && route.request().method() === "GET") {
|
||||
const url = new URL(route.request().url());
|
||||
const sessionId = url.searchParams.get("session_id") ?? "task-execution-session";
|
||||
await route.fulfill({
|
||||
headers: corsHeaders,
|
||||
json: {
|
||||
id: sessionId,
|
||||
session_id: sessionId,
|
||||
title: "水质投诉动态调查 · 执行记录",
|
||||
purpose: "task_execution",
|
||||
task_id: executionHistoryMatch[1],
|
||||
status: "active",
|
||||
created_at: "2026-09-22T02:00:00Z",
|
||||
updated_at: "2026-09-22T02:10:00Z",
|
||||
messages: [
|
||||
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||
{ id: "execution-assistant", role: "assistant", parts: [{ type: "text", text: "已完成压力与流量证据核对" }] }
|
||||
],
|
||||
is_streaming: false,
|
||||
run_status: "completed",
|
||||
read_only: true
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const executionDebugMatch = pathname.match(/\/task\/([^/]+)\/execution-session\/debug$/);
|
||||
if (executionDebugMatch && route.request().method() === "POST") {
|
||||
const body = (route.request().postDataJSON() ?? {}) as { session_id?: string };
|
||||
const sessionId = `execution-debug-${executionDebugMatch[1]}`;
|
||||
const created = !executionDebugSessions.has(sessionId);
|
||||
executionDebugSessions.set(sessionId, {
|
||||
taskId: executionDebugMatch[1],
|
||||
sourceSessionId: body.session_id ?? "task-execution-session"
|
||||
});
|
||||
await route.fulfill({
|
||||
status: created ? 201 : 200,
|
||||
headers: corsHeaders,
|
||||
json: {
|
||||
session_id: sessionId,
|
||||
title: "水质投诉动态调查 · 调试",
|
||||
purpose: "task_collaboration",
|
||||
task_id: executionDebugMatch[1],
|
||||
parent_session_id: body.session_id,
|
||||
status: "active",
|
||||
created
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/session") && route.request().method() === "POST") {
|
||||
const body = (route.request().postDataJSON() ?? {}) as {
|
||||
trigger_context?: { trigger_id?: string };
|
||||
@@ -98,25 +151,34 @@ export async function mockAgentApi(page: Page, options: MockAgentApiOptions = {}
|
||||
const sessionMatch = pathname.match(/\/session\/([^/]+)$/);
|
||||
if (sessionMatch && route.request().method() === "GET") {
|
||||
const collaboration = sessionMatch[1].startsWith("collaboration-");
|
||||
const executionDebug = executionDebugSessions.get(sessionMatch[1]);
|
||||
const stored = options.sessions?.find((session) => (session.session_id ?? session.id) === sessionMatch[1]);
|
||||
await route.fulfill({
|
||||
headers: corsHeaders,
|
||||
json: {
|
||||
id: sessionMatch[1],
|
||||
session_id: sessionMatch[1],
|
||||
title: stored?.title ?? (collaboration ? "任务协作问答" : "水质投诉事件调查"),
|
||||
purpose: stored?.purpose ?? (collaboration ? "task_collaboration" : "ordinary_chat"),
|
||||
task_id: collaboration ? sessionMatch[1].slice("collaboration-".length) : undefined,
|
||||
title: stored?.title ?? (executionDebug ? "水质投诉动态调查 · 调试" : collaboration ? "任务协作问答" : "水质投诉事件调查"),
|
||||
purpose: stored?.purpose ?? (executionDebug || collaboration ? "task_collaboration" : "ordinary_chat"),
|
||||
task_id: executionDebug?.taskId ?? (collaboration ? sessionMatch[1].slice("collaboration-".length) : undefined),
|
||||
status: "active",
|
||||
created_at: "2026-09-22T02:00:00Z",
|
||||
updated_at: "2026-09-22T02:00:00Z",
|
||||
messages: [],
|
||||
messages: executionDebug ? [
|
||||
{ id: "execution-user", role: "user", parts: [{ type: "text", text: "执行后台调查任务" }] },
|
||||
{ id: "execution-assistant", role: "assistant", parts: [{ type: "text", text: "已完成压力与流量证据核对" }] }
|
||||
] : [],
|
||||
is_streaming: false
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (/\/session\/[^/]+\/interactions$/.test(pathname)) {
|
||||
await route.fulfill({ headers: corsHeaders, json: { items: [] } });
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.endsWith("/responses")) {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
|
||||
Reference in New Issue
Block a user