重构: 后端 df-ai/commands 拆分+df-nodes/workflow 改造+P0 bug 修复
- df-ai: context 历史中毒三档自愈 sanitize_messages(AC3)+anthropic_compat tool_use_id None 跳过(AC1/AC2)+删 router/stream 死码
- df-core: events 加 select_type+decisions 多选审批契约(F-260615-01)
- df-execute: shell run_command 工具复用(F-260615-05)
- df-nodes: human_node 多选校验+2 端到端测(F-01)+取消跳 set_failed(B-03b-R1/R2/R8)
- df-workflow: executor/dag/state cancel 闭环(B-06/07/03a/b)+provider approve options(R-PD-5)
- df-storage: find_path_conflict 抽公共(R-PD-11)+COLS 常量断言
- df-ideas: 删 IdeaPromoter/PromotionPolicy 死码(R-PD-14)
- src-tauri/commands/ai: secret keyring 迁移(FR-S1/R-PD-4)+GeneratingGuard RAII+disarm(B-09/26)+newConversation 软复位(B-10)+stream 心跳/stop select/空回复判错(B-02/04/05/15)+run_command(F-05)+mask audit(AR-3)
- src-tauri/commands/{project,task,workflow,mod,lib,state}: task detail IPC(F-02)+approve decisions+task list 联动(B-29)
- Cargo.lock+Cargo.toml 依赖同步
This commit is contained in:
@@ -123,7 +123,13 @@ impl DagExecutor {
|
||||
for (node_id, result, duration) in results {
|
||||
match result {
|
||||
Ok(output) => {
|
||||
self.state_machine.set_completed(node_id.clone())?;
|
||||
// 已取消的节点跳过 set_completed:
|
||||
// Ok 后 join_all 让出执行权,窗口内 cancel_workflow_node IPC → set_cancelled 改 Cancelled,
|
||||
// 随后此处 set_completed 走 transition 命中(Cancelled→Completed)非法 → bail 致已批准审批报失败(R-P1-3 TOCTOU)
|
||||
// 与 Err 分支 is_cancelled 短路对称:已取消节点保持 Cancelled 终态
|
||||
if !self.state_machine.is_cancelled(&node_id) {
|
||||
self.state_machine.set_completed(node_id.clone())?;
|
||||
}
|
||||
self.event_bus
|
||||
.send(WorkflowEvent::NodeCompleted {
|
||||
node_id: node_id.clone(),
|
||||
@@ -314,4 +320,53 @@ mod tests {
|
||||
NodeStatus::Cancelled
|
||||
);
|
||||
}
|
||||
|
||||
/// 测试节点:执行中通过共享 node_status 自取消(模拟外部 IPC set_cancelled),返回 Ok
|
||||
/// 模拟 HumanNode select! 收合法 HumanApprovalResponse 后 return Ok,但 join_all 让出窗口内
|
||||
/// cancel_workflow_node IPC 把节点改 Cancelled 的 TOCTOU 场景。
|
||||
struct CancelSelfThenOkNode;
|
||||
|
||||
#[async_trait]
|
||||
impl Node for CancelSelfThenOkNode {
|
||||
async fn execute(&self, ctx: NodeContext) -> NodeResult {
|
||||
ctx.node_status.set_cancelled(ctx.node_id.clone());
|
||||
Ok(NodeOutput::empty())
|
||||
}
|
||||
|
||||
fn schema(&self) -> NodeSchema {
|
||||
NodeSchema {
|
||||
params: serde_json::Value::Null,
|
||||
output: serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
fn node_type(&self) -> &str {
|
||||
"cancel_self_then_ok"
|
||||
}
|
||||
}
|
||||
|
||||
/// R-P1-3:Ok 后取消的 TOCTOU 防护。节点返回 Ok 但执行期间已被 set_cancelled,
|
||||
/// executor Ok 分支必须跳过 set_completed(transition Cancelled→Completed 非法会 bail),
|
||||
/// 状态保持 Cancelled,run 返回 Ok(已批准审批不应误报失败)。
|
||||
#[tokio::test]
|
||||
async fn test_cancelled_node_skips_set_completed() {
|
||||
use df_core::types::NodeStatus;
|
||||
let mut dag = Dag::new();
|
||||
dag.add_node("y".to_string(), Box::new(CancelSelfThenOkNode));
|
||||
|
||||
let mut executor = DagExecutor::new(EventBus::new(), "test-cancel-ok".to_string());
|
||||
let result = executor.run(&dag, serde_json::Value::Null).await;
|
||||
|
||||
// run 返回 Ok —— Ok 节点不应因 TOCTOU 取消被误判为失败
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Ok 后取消应短路 set_completed 而非 bail,实际 err: {:?}",
|
||||
result.err()
|
||||
);
|
||||
// 状态保持 Cancelled(未被 set_completed 覆盖为 Completed,也不 bail)—— R-P1-3 修法核心验证
|
||||
assert_eq!(
|
||||
executor.state_machine.get(&"y".to_string()),
|
||||
NodeStatus::Cancelled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user