feat(workbench): refine dynamic task collaboration

This commit is contained in:
2026-09-24 16:36:12 +08:00
parent b64d2b78bb
commit 87cc7785da
25 changed files with 1216 additions and 470 deletions
@@ -6,7 +6,7 @@ import type { OperationalTask } from "../api/operational-task-client";
import { AgentExecutionPlanPanel, ExecutionPlanSteps } from "./agent-execution-plan-panel";
describe("execution step cards", () => {
it("shows parallel cards, exact join dependencies and escaped model content", () => {
it("shows exact historical dependencies without assigning an unrecorded branch type", () => {
const plan: AgentExecutionPlan = {
schema_version: "execution-plan@1", session_id: "s1", revision: 1,
updated_at: "2026-09-23T01:00:00.000Z", title: "调查", reason: "核验",
@@ -17,19 +17,15 @@ describe("execution step cards", () => {
edges: [{ from: "0", to: "1", label: "" }, { from: "0", to: "2", label: "" }, { from: "1", to: "3", label: "上游正常" }, { from: "2", to: "3", label: "投诉集中" }]
};
const { container } = render(<ExecutionPlanSteps plan={plan} />);
expect(screen.getByText("并行处理")).toBeInTheDocument();
expect(screen.getByLabelText("并行分枝 2 项")).toHaveAttribute("data-execution-layout", "branch");
expect(screen.getByText("2 条独立分枝")).toBeInTheDocument();
expect(screen.getByText(/历史流程按原始依赖展示/)).toBeInTheDocument();
expect(screen.queryByText(/并行处理/)).not.toBeInTheDocument();
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(4);
expect(container.querySelector("script")).toBeNull();
expect(screen.queryByText(/前置:/)).not.toBeInTheDocument();
expect(screen.getByText("分枝 1")).toBeInTheDocument();
expect(screen.getByText("分枝 2")).toBeInTheDocument();
expect(screen.queryByText("每条分枝按自身依赖推进,不要求同层节点同时完成。")).not.toBeInTheDocument();
expect(screen.getByText("前置:上游(上游正常)、邻区(投诉集中)")).toBeInTheDocument();
expect(screen.getByText("待人工/资料")).toBeInTheDocument();
});
it("renders a four-way fork and a later two-way fork without inventing cross-branch edges", () => {
it("keeps all historical nodes and their recorded dependencies", () => {
const node = (id: string, title: string, status: AgentExecutionPlan["nodes"][number]["status"] = "completed") => ({
id, title, status, owner: "agent" as const, detail: `${title}详情`, evidence: []
});
@@ -58,11 +54,10 @@ describe("execution step cards", () => {
};
const { container } = render(<ExecutionPlanSteps plan={plan} />);
expect(screen.getByLabelText("并行分枝 4 项")).toBeVisible();
expect(screen.getByLabelText("并行分枝 2 项")).toBeVisible();
expect(screen.getAllByText("分枝汇合")).toHaveLength(2);
expect(screen.getByText("前置:上游复核、邻区投诉、阀门排查、运维记录")).toBeVisible();
expect(screen.getByText("前置:水箱记录、现场采样")).toBeVisible();
expect(screen.queryByText(/并行处理/)).not.toBeInTheDocument();
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(plan.nodes.length);
expect(screen.queryByText(/前置工作与条件|前置:/)).not.toBeInTheDocument();
});
});
@@ -96,7 +91,8 @@ const taskBase = {
},
evidence: [{ evidence_id: "e-1", kind: "system_data", summary: "上游余氯处于正常范围", source_label: "SCADA 水质监测", source_ref: "WQ-01", observed_at: "2026-09-23T00:55:00Z", recorded_at: "2026-09-23T01:08:00Z", recorded_by: "agent", has_snapshot: false }],
actions: [{ action_id: "44444444-4444-4444-8444-444444444444", task_id: "22222222-2222-4222-8222-222222222222", version: 3, kind: "create_work_order", node_id: null, reason: "需要现场采样", payload: { title: "现场采样", location: "演示小区", scheduled_at: "2026-09-23T02:00:00Z" }, evidence_refs: ["e-1"], required_approval_permission: "operational_action.work_order.approve", required_execution_permission: "operational_action.work_order.execute", approval_status: "pending", execution_status: "not_submitted", execution_request_id: null, proposed_by: "agent", proposed_at: "2026-09-23T01:09:00Z", decided_by: null, decided_at: null, decision_comment: null, executed_by: null, executed_at: null, receipt: null, execution_detail: 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 },
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 },
access_requests: [],
work_order_snapshot: null
} as OperationalTask;
@@ -111,11 +107,74 @@ function taskPanel(task: OperationalTask, overrides: Partial<ComponentProps<type
onLoadEvidenceSnapshot: vi.fn(async () => { throw new Error("no snapshot"); }),
...overrides
};
render(<AgentExecutionPlanPanel {...props} />);
return props;
const { rerender } = render(<AgentExecutionPlanPanel {...props} />);
return { ...props, rerender };
}
describe("operational task approval and evidence", () => {
it("shows the selected v2 route first and keeps unchosen work available", () => {
const node = (id: string, title: string, status: "completed" | "waiting" | "skipped") => ({
id, title, status, owner: "agent" as const, detail: title
});
const plan = {
nodes: [
{ ...node("risk", "判断紧急风险", "completed"), kind: "decision" as const, selected_edge_id: "urgent" },
{ ...node("urgent", "立即现场采样", "waiting"), kind: "activity" as const },
{ ...node("routine", "常规核查", "skipped"), kind: "activity" as const }
],
edges: [
{ id: "urgent", from: "risk", to: "urgent", label: "有紧急风险" },
{ id: "routine", from: "risk", to: "routine", label: "无紧急风险" }
]
};
render(<ExecutionPlanSteps plan={plan} />);
expect(screen.getByText(/路线选择:/).parentElement).toHaveTextContent("有紧急风险");
expect(screen.getByText("其他规划路线 1 项")).toBeVisible();
expect(screen.getAllByText("立即现场采样")[0]).toBeVisible();
});
it("groups nested parallel work at its recorded joins", () => {
const node = (id: string, kind: "activity" | "decision" | "parallel_split" | "parallel_join") => ({
id, title: id, detail: `${id}详情`, owner: "agent" as const, status: "completed" as const, kind
});
const plan = {
nodes: [node("start", "parallel_split"), node("field", "activity"), node("agent", "activity"),
node("read", "parallel_split"), node("upstream", "activity"), node("network", "activity"),
node("evidence", "parallel_join"), node("result", "parallel_join")],
edges: [
{ id: "e1", from: "start", to: "field", condition: "现场" },
{ id: "e2", from: "start", to: "agent", condition: "Agent" },
{ id: "e3", from: "field", to: "result" },
{ id: "e4", from: "agent", to: "read" },
{ id: "e5", from: "read", to: "upstream", condition: "上游" },
{ id: "e6", from: "read", to: "network", condition: "管网" },
{ id: "e7", from: "upstream", to: "evidence" },
{ id: "e8", from: "network", to: "evidence" },
{ id: "e9", from: "evidence", to: "result" }
]
};
const { container } = render(<ExecutionPlanSteps plan={plan} />);
expect(screen.getAllByLabelText("并行处理 2 路")).toHaveLength(2);
expect(container.querySelectorAll('[data-execution-node="evidence"]')).toHaveLength(1);
expect(container.querySelectorAll('[data-execution-node="result"]')).toHaveLength(1);
expect(container.querySelectorAll("[data-execution-node]")).toHaveLength(plan.nodes.length);
});
it("lets a reviewer edit a work-order draft before one-click dispatch", async () => {
const onDraftUpdate = vi.fn(async () => undefined);
const onConfirmDispatch = vi.fn(async () => undefined);
const initialTask = { ...taskBase, permissions: { ...taskBase.permissions, can_decide_work_orders: true, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], payload: { title: "旧草稿", description: "现场采样", work_order_type: "general", priority: "normal" } }] };
const props = taskPanel(initialTask, { onDraftUpdate, onConfirmDispatch });
fireEvent.click(screen.getByRole("button", { name: "编辑草稿" }));
fireEvent.change(screen.getByRole("textbox", { name: "工单标题" }), { target: { value: "人工修订草稿" } });
fireEvent.click(screen.getByRole("button", { name: "保存草稿" }));
await waitFor(() => expect(onDraftUpdate).toHaveBeenCalledWith(taskBase.actions[0].action_id, 3, expect.objectContaining({ title: "人工修订草稿" })));
await waitFor(() => expect(screen.getByRole("button", { name: "确认并下发工单" })).toBeDisabled());
props.rerender(<AgentExecutionPlanPanel {...props} task={{ ...initialTask, actions: [{ ...initialTask.actions[0], version: 4, payload: { ...initialTask.actions[0].payload, title: "人工修订草稿" } }] }} />);
fireEvent.click(screen.getByRole("button", { name: "确认并下发工单" }));
await waitFor(() => expect(onConfirmDispatch).toHaveBeenCalledWith(taskBase.actions[0].action_id, 4));
});
it("distinguishes the current report from a previous execution without exposing generation numbers", () => {
taskPanel(taskBase);
expect(screen.getByText("当前结论", { exact: true })).toBeVisible();