fix(workbench): retry approved task actions after dispatch failure
Generic Container CI/CD / test-build-publish (push) Successful in 1m13s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m13s

This commit is contained in:
2026-11-07 22:20:04 +08:00
parent 8a63926284
commit 4e3ed5098e
5 changed files with 136 additions and 5 deletions
@@ -130,6 +130,48 @@ function taskPanel(task: OperationalTask, overrides: Partial<ComponentProps<type
return { ...props, rerender };
}
it.each(["failed", "unsupported"] as const)("retries an approved %s action using its current version", async (status) => {
let finish!: () => void;
const onExecute = vi.fn(() => new Promise<void>((resolve) => { finish = resolve; }));
const task: OperationalTask = { ...taskBase,
permissions: { ...taskBase.permissions, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], approval_status: "approved", execution_status: status }]
};
const props = taskPanel(task, { onExecute });
const section = screen.getByRole("region", { name: "当前需处理" });
expect(section).toHaveTextContent("待重试");
const retry = within(section).getByRole("button", { name: "重试执行" });
fireEvent.click(retry);
expect(retry).toBeDisabled();
fireEvent.click(retry);
expect(onExecute).toHaveBeenCalledExactlyOnceWith(task.actions[0].action_id, task.actions[0].version);
expect(props.onDecision).not.toHaveBeenCalled();
finish();
await waitFor(() => expect(retry).toBeEnabled());
props.rerender(<AgentExecutionPlanPanel {...props} task={{ ...task,
actions: [{ ...task.actions[0], version: 4, execution_status: "submitted" }]
}} />);
expect(screen.queryByRole("button", { name: "重试执行" })).not.toBeInTheDocument();
expect(screen.getByRole("button", { name: "核对执行回执" })).toBeVisible();
});
it("keeps retry disabled without work-order execution permission", () => {
const props = taskPanel({ ...taskBase, actions: [{ ...taskBase.actions[0],
approval_status: "approved", execution_status: "unsupported"
}] });
const retry = screen.getByRole("button", { name: "重试执行" });
expect(retry).toBeDisabled();
fireEvent.click(retry);
expect(props.onExecute).not.toHaveBeenCalled();
});
it.each(["pending", "rejected"] as const)("does not retry a %s action", (approval) => {
taskPanel({ ...taskBase, permissions: { ...taskBase.permissions, can_execute_work_orders: true },
actions: [{ ...taskBase.actions[0], approval_status: approval, execution_status: "failed" }]
});
expect(screen.queryByRole("button", { name: "重试执行" })).not.toBeInTheDocument();
});
it("keeps fixed question answers in the report and focuses them from a data gap", () => {
const task: OperationalTask = { ...taskBase, actions: [], active_run: null, lifecycle_status: "waiting_human",
questions: [{ question_request_id: "70000000-0000-4000-8000-000000000001", task_id: taskBase.task_id,
@@ -33,6 +33,7 @@ import type {
OperationalTaskDataSnapshot
} from "../api/operational-task-client";
import type { TaskExecutionHistoryTarget } from "../api/operational-task-client";
import { isRetryableInvestigationAction } from "../utils/investigation-presentation";
type ExecutionNode = {
id: string;
@@ -568,6 +569,7 @@ export function HumanActionCard({ task, action, onDecision, onExecute, onDraftUp
<div className="mt-3 flex flex-wrap justify-end gap-2">
{action.approval_status === "pending" && canDecide ? <><Button variant="outline" size="sm" disabled={submitting !== null} onClick={() => void run("reject")}>拒绝动作</Button>{!command && onDraftUpdate ? <Button variant="outline" size="sm" disabled={submitting !== null || editing} onClick={() => { setDraft(workOrderDraft(action.payload)); setEditing(true); }}>编辑草稿</Button> : null}<Button size="sm" disabled={submitting !== null || editing || savedVersion === action.version || (!command && !!onConfirmDispatch && !canExecute)} onClick={() => void run(!command && onConfirmDispatch ? "confirm" : "approve")}>{submitting === "approve" || submitting === "confirm" ? <Loader2 className="animate-spin" /> : null}{command ? "批准下发命令" : onConfirmDispatch ? "确认并创建预约工单" : "批准创建预约工单"}</Button></> : null}
{action.approval_status === "approved" && action.execution_status === "not_submitted" ? <Button size="sm" disabled={!canExecute || submitting !== null} title={!canExecute ? "当前账号无此动作执行权限" : undefined} onClick={() => void run("execute")}>{submitting === "execute" ? <Loader2 className="animate-spin" /> : null}{command ? "下发命令" : "创建预约工单"}</Button> : null}
{isRetryableInvestigationAction(action) ? <Button size="sm" disabled={!canExecute || submitting !== null || stale} title={!canExecute ? "当前账号无此动作执行权限" : undefined} onClick={() => void run("execute")}>{submitting === "execute" ? <Loader2 className="animate-spin" /> : <RefreshCw size={14} />}重试执行</Button> : null}
{action.approval_status === "approved" && ["submitted", "unknown"].includes(action.execution_status) ? <Button variant="outline" size="sm" disabled={!canExecute || submitting !== null} onClick={() => void run("reconcile")}>{submitting === "reconcile" ? <Loader2 className="animate-spin" /> : <RefreshCw size={14} />}核对执行回执</Button> : null}
</div>
</div>
@@ -18,7 +18,8 @@ import type {
import {
investigationDiagram,
investigationStatus,
isPendingInvestigationAction
isPendingInvestigationAction,
isRetryableInvestigationAction
} from "../utils/investigation-presentation";
import { InvestigationDiagramView } from "./investigation-diagram-view";
import { InvestigationExpandCard } from "./investigation-expand-card";
@@ -433,9 +434,11 @@ export function InvestigationTaskReport({
{actionTitle(action)} ·{" "}
{action.approval_status === "pending"
? "待审核"
: action.execution_status === "not_submitted"
? "待执行"
: "待核对回执"}
: isRetryableInvestigationAction(action)
? "待重试"
: action.execution_status === "not_submitted"
? "待执行"
: "待核对回执"}
</span>
}
>
@@ -71,6 +71,7 @@ export function investigationStatus(task: OperationalTask): string {
if (task.lifecycle_status === "cancelled") return "本轮已取消";
if (task.questions?.some((question) => question.status === "pending" && question.execution_generation === task.execution_generation)) return "等待回答";
if (task.actions.some((action) => action.approval_status === "pending")) return "待人工审核";
if (task.actions.some(isRetryableInvestigationAction)) return "待重试动作";
if (
task.actions.some(
(action) =>
@@ -105,6 +106,11 @@ export function isPendingInvestigationAction(action: OperationalTask["actions"][
return (
action.approval_status === "pending" ||
(action.approval_status === "approved" &&
["not_submitted", "submitted", "unknown"].includes(action.execution_status))
["not_submitted", "submitted", "unknown"].includes(action.execution_status)) ||
isRetryableInvestigationAction(action)
);
}
export function isRetryableInvestigationAction(action: OperationalTask["actions"][number]) {
return action.approval_status === "approved" && ["failed", "unsupported"].includes(action.execution_status);
}