feat(workbench): refine dynamic task collaboration
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user