diff --git a/crates/df-nodes/src/human_node.rs b/crates/df-nodes/src/human_node.rs index e1b0c51..9f8f9a7 100644 --- a/crates/df-nodes/src/human_node.rs +++ b/crates/df-nodes/src/human_node.rs @@ -195,6 +195,40 @@ mod tests { assert_eq!(out.data["comment"], json!("好的")); } + #[tokio::test] + async fn request_is_emitted_to_bus() { + // B-03b-R8: 验证 HumanNode 真发出 HumanApprovalRequest 到事件总线 + // (R6 修复前 :41 send 缺 await → Request 未进 channel,此测会超时失败) + let bus = EventBus::new(); + let mut rx = bus.subscribe(); // subscribe 先于 execute send(broadcast 不回放) + let ctx = make_ctx(&bus, "exec-1", "node-h", json!({ "timeout_secs": 1 })); + let bus_clone = bus.clone(); + let handle = tokio::spawn(async move { HumanNode.execute(ctx).await }); + + // execute send Request 后 rx 应收到 + let received = tokio::time::timeout(Duration::from_millis(500), rx.recv()).await; + assert!( + received.is_ok(), + "应收到 HumanApprovalRequest(R6 Request 真发出),实际超时" + ); + match received.unwrap().unwrap() { + WorkflowEvent::HumanApprovalRequest { + execution_id, + node_id, + title, + .. + } => { + assert_eq!(execution_id, "exec-1"); + assert_eq!(node_id, "node-h"); + assert_eq!(title, "请确认"); + } + other => panic!("期望 HumanApprovalRequest,收到 {:?}", other), + } + // 发 Response 让 execute 正常返回(防 task 泄漏) + send_response(&bus_clone, "exec-1", "node-h", "同意", None).await; + let _ = handle.await; + } + #[tokio::test] async fn mismatched_execution_id_filtered_then_timeout() { // execution_id 不匹配:Response 被过滤,短超时验证 → Err 超时 diff --git a/src-tauri/src/commands/workflow.rs b/src-tauri/src/commands/workflow.rs index d5cc7f1..9af3cec 100644 --- a/src-tauri/src/commands/workflow.rs +++ b/src-tauri/src/commands/workflow.rs @@ -132,11 +132,20 @@ pub async fn run_workflow( state_registry.lock().await.remove(&exec_id); // 执行失败时补发 WorkflowFailed(执行器内部只发 NodeFailed) + // failed_node 从状态机取首个失败/取消节点(R9⑪: 原 String::new() 空值) if let Some(error) = error { + use df_core::types::NodeStatus; + let failed_node = executor + .state_machine() + .snapshot() + .into_iter() + .find(|(_, s)| matches!(s, NodeStatus::Failed | NodeStatus::Cancelled)) + .map(|(id, _)| id) + .unwrap_or_default(); event_bus .send(WorkflowEvent::WorkflowFailed { error, - failed_node: String::new(), + failed_node, }) .await; } diff --git a/src/api/types.ts b/src/api/types.ts index a6be189..73f38cb 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -121,10 +121,16 @@ export interface WorkflowRecord { } /** 工作流事件载荷(后端 emit 的结构) */ +/** 工作流事件类型字面量联合(对应后端 WorkflowEvent serde tag=type rename_all=snake_case,R7收窄防写错 type 字符串) */ +export type WorkflowEventType = + | 'node_started' | 'node_progress' | 'node_output' | 'node_completed' | 'node_failed' + | 'workflow_paused' | 'workflow_completed' | 'workflow_failed' + | 'human_approval_request' | 'human_approval_response' + export interface WorkflowEventPayload { execution_id: string event: { - type: string + type: WorkflowEventType [key: string]: unknown } }