Files
DevFlow/crates/df-ai/src/planner.rs
绝尘 cf7fe867ee 优化: aichat B路线Phase1规划开关启用(PLANNING_ENABLED=true)
主loop走filter_tool_defs_planned规划(agentic/mod.rs:633接入点就绪)
双层开关可回退:PLANNING_ENABLED(主loop)+PLAN_HINT_ENABLED(plan_hint函数)
任一关闭退扁平旧行为;feature_flags_default_on测试断言同步
2026-06-22 01:24:14 +08:00

1022 lines
37 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Plan-driven 规划骨架Phase 0 · 维度 0.8 + 0.7
//!
//! 论证依据:`docs/02-架构设计/单对话并行多轮-设计-2026-06-20.md`(§三 Plan 数据结构 /
//! §十三 0.7 规划校验 / 0.8 planner.rs+
//! `docs/02-架构设计/单对话并行多轮-Phase0落地路线图-2026-06-20.md`0.8 + 0.7 节)。
//!
//! ## 定位
//! **纯函数模块**(零 IO 零状态:无 `.await` / 无 db / 无 provider / 无全局状态),
//! 仅数据结构 + 算法。便于单测 + Phase 1 无副作用接入。
//!
//! 提供四项能力:
//! 1. [`SubTask`] / [`Plan`] —— 规划 DAG 数据结构(可并行组 group + 顺序依赖 deps
//! 2. [`Plan::validate`] —— 五类非法形态校验(环/悬空/空/重复 id/超深)。
//! 3. [`Plan::to_layers`] —— Kahn 拓扑分层算法,输出可并行层(喂 Phase 2/3 execution 调度)。
//! 4. [`planning_enabled`] —— feature flag 占位(关时单链 ReAct = 旧行为Phase 1 接入用)。
//!
//! ## 设计原则(对齐设计文档 §十三 决策推翻)
//! - **单一真相源**Plan 只存 `deps`**不冗余存 `parallel_groups`**(设计文档明确推翻
//! 旧「deps + parallel_groups 并存」结构 → 删 parallel_groups运行时 Kahn 分层派生)。
//! `group` 字段是规划期的「提示性并行组」(可选 hint与 deps DAG 并行不矛盾即合法。
//! - **非法收集非短路**validate 一次性收集全部违规,便于规划 LLM 一次性修正。
//! - **Kahn 无副作用**纯算法不校验validate 先行),输入非法时行为见各方法文档。
//!
//! ## 不改现有文件
//! 本文件独立编译;`intent.rs` / `coordinator.rs` 接入留 Phase 1/2plan_hint.rs 另起)。
// ---- feature flag 占位 ------------------------------------------------------
/// 规划能力总开关(主 loop 规划开关,门控 filter_tool_defs_planned 接入)。
///
/// **当前 `true`** = 主 loop 走 Plan 规划agentic/mod.rs:633 调 filter_tool_defs_planned
/// Phase 1plan_hint 接入主 loop已落地翻 true 启用。
/// 注:`PLAN_HINT_ENABLED`(plan_hint 函数 flag,Phase0a 就绪 true)单一定义在 `plan_hint.rs`,
/// 本模块仅 `PLANNING_ENABLED`(主 loop 规划开关),消除此前双定义矛盾。
/// 双层开关:即使 PLANNING_ENABLED=true,若 plan_hint 内部 PLAN_HINT_ENABLED 关闭,
/// filter_tool_defs_planned 仍退扁平(任一关闭退旧行为,可回退)。
pub const PLANNING_ENABLED: bool = true;
/// Plan 层级硬上限(防爆深,防规划 LLM 退化成超长链)。
///
/// 超过此值的依赖链判 [`PlanError::TooDeep`]。
/// 8 层覆盖典型软件任务分解需求→设计→编码→测试→…Phase 2 go/no-go 后可调。
pub const MAX_PLAN_DEPTH: usize = 8;
use std::collections::{HashMap, HashSet};
// ---- 类型别名 ---------------------------------------------------------------
/// 子任务标识。String 而非裸 &str规划期 LLM 自由生成,不依赖静态工具名表。
pub type SubTaskId = String;
/// 并行组标识(可选 hint。规划 LLM 标注「这组任务意图上可并行」,
/// Kahn 分层不依赖它(只看 deps DAG但保留供 UI/审计展示。
pub type GroupId = String;
// ---- SubTask ----------------------------------------------------------------
/// 规划子任务DAG 中的一个节点。
///
/// 字段对齐设计文档 §三 `SubTask`id / tools / deps+ 并行组 hint
/// - `id`任务内唯一标识validate 拒重复)。
/// - `tool_hint`可用工具名子集intent/plan_hint 提示,空 Vec 由 validate 拒为
/// [`PlanError::SubtaskWithoutTools`] —— 对齐设计文档枚举;执行期不被强制,仅提示 LLM
/// - `deps`:依赖子任务 id 列表DAG 前驱)。未解析的 id不存在于 Plan
/// [`PlanError::UnknownDep`](悬空)。
/// - `group`:可选并行组 hint设计文档 §三 parallel_groups 的非冗余形态——只作提示,
/// 不与 deps 并列做真相源)。
/// - `intent`:自由文本意图描述(规划 LLM 给出的「这步做什么」,便于审计/日志)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubTask {
pub id: SubTaskId,
/// 可用工具名子集hint非强制。空 → validate 拒(除非允许无工具子任务)。
pub tool_hint: Vec<String>,
/// 依赖前驱 id。DAG 边dep → self。
pub deps: Vec<SubTaskId>,
/// 并行组 hint可选。同 group 的任务意图上可并行,但 Kahn 不看它。
pub group: Option<GroupId>,
/// 意图自由文本(规划 LLM 描述,审计/日志用)。
pub intent: String,
}
impl SubTask {
/// 构造便捷器最小子任务id + intent无工具无依赖
#[allow(dead_code)]
pub fn new(id: impl Into<SubTaskId>, intent: impl Into<String>) -> Self {
Self {
id: id.into(),
tool_hint: Vec::new(),
deps: Vec::new(),
group: None,
intent: intent.into(),
}
}
/// 链式设置 tool_hint。
#[allow(dead_code)]
pub fn with_tools(mut self, tools: Vec<String>) -> Self {
self.tool_hint = tools;
self
}
/// 链式设置 deps。
#[allow(dead_code)]
pub fn with_deps(mut self, deps: Vec<SubTaskId>) -> Self {
self.deps = deps;
self
}
/// 链式设置 group。
#[allow(dead_code)]
pub fn with_group(mut self, group: impl Into<GroupId>) -> Self {
self.group = Some(group.into());
self
}
}
// ---- Plan -------------------------------------------------------------------
/// 规划 DAG子任务集合。
///
/// **不冗余存 `parallel_groups`**(设计文档 §十三 决策推翻:删 parallel_groups
/// 只存 deps运行时 [`Plan::to_layers`] Kahn 分层派生——单一真相源)。
///
/// 合法性由 [`Plan::validate`] 校验;非法 Plan 调 [`Plan::to_layers`] 行为见其文档。
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Plan {
pub tasks: Vec<SubTask>,
}
impl Plan {
/// 空构造。
#[allow(dead_code)]
pub fn new() -> Self {
Self { tasks: Vec::new() }
}
/// 从 tasks 构造。
#[allow(dead_code)]
pub fn from_tasks(tasks: Vec<SubTask>) -> Self {
Self { tasks }
}
/// 任务数。
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.tasks.len()
}
/// 是否空(空 Plan 判非法:[`PlanError::Empty`])。
#[allow(dead_code)]
pub fn is_empty(&self) -> bool {
self.tasks.is_empty()
}
/// 校验 Plan 合法性(五类非法 + 设计文档四枚举)。
///
/// **收集非短路**:一次性返回所有违规(`Vec<PlanError>`),便于规划 LLM 一次修正。
/// 合法 → `Ok(())`。
///
/// 五类非法(对齐路线图 0.7+ 设计文档枚举映射:
/// | 类别 | 枚举 | 判据 |
/// |---|---|---|
/// | 空 | [`PlanError::Empty`] | tasks 为空 |
/// | 重复 id | [`PlanError::DuplicateId`] | 同 id 出现 ≥2 次 |
/// | 悬空 dep | [`PlanError::UnknownDep`] | deps 引用不存在的 id设计文档枚举 |
/// | 自环 dep | [`PlanError::UnknownDep`] 归类 | self-depA 依赖 A视为悬空+环) |
/// | 环 | [`PlanError::CycleDetected`] | deps 形成环(设计文档枚举) |
/// | 无工具 | [`PlanError::SubtaskWithoutTools`] | tool_hint 空(设计文档枚举,可由 `require_tools=false` 关) |
/// | 超深 | [`PlanError::TooDeep`] | 最长依赖链深度 > [`MAX_PLAN_DEPTH`] |
///
/// 注:[`PlanError::OrphanSubtask`](设计文档枚举)当前归一不报——
/// 无 deps 且无后继的孤立任务在 DAG 中合法入口任务设计文档「Orphan」语义
/// 偏「脱网」Phase 2 go/no-go 后再定是否拒(保留枚举占位,符合预留保留原则)。
pub fn validate(&self) -> Result<(), Vec<PlanError>> {
let errs = self.validate_with(ValidateOptions::default());
if errs.is_empty() {
Ok(())
} else {
Err(errs)
}
}
/// 带选项的校验(`require_tools` 开关plan_hint 场景可允许无工具子任务)。
pub fn validate_with(&self, opts: ValidateOptions) -> Vec<PlanError> {
let mut errs: Vec<PlanError> = Vec::new();
// 1) 空
if self.tasks.is_empty() {
errs.push(PlanError::Empty);
return errs; // 空 Plan 后续检查无意义
}
// 构建 id 索引(同时检测重复 id
let mut id_index: HashMap<&SubTaskId, usize> = HashMap::new();
for (i, t) in self.tasks.iter().enumerate() {
if t.id.is_empty() {
errs.push(PlanError::EmptyId { index: i });
continue;
}
if id_index.contains_key(&t.id) {
errs.push(PlanError::DuplicateId {
id: t.id.clone(),
});
} else {
id_index.insert(&t.id, i);
}
}
// 2) 无工具(可选)
if opts.require_tools {
for t in &self.tasks {
if t.tool_hint.is_empty() {
errs.push(PlanError::SubtaskWithoutTools { id: t.id.clone() });
}
}
}
// 3) 悬空 dep / 自环(自环视为 UnknownDep
for t in &self.tasks {
for dep in &t.deps {
if dep == &t.id {
errs.push(PlanError::SelfDep { id: t.id.clone() });
} else if !id_index.contains_key(dep) {
errs.push(PlanError::UnknownDep {
from: t.id.clone(),
unknown: dep.clone(),
});
}
}
}
// 4) 环 + 超深(先验无悬空才能可靠跑环检测:悬空已报,环检测只看可解析边)
if !errs.iter().any(|e| {
matches!(
e,
PlanError::UnknownDep { .. } | PlanError::SelfDep { .. }
)
}) {
if let Some(cycle) = self.detect_cycle() {
errs.push(PlanError::CycleDetected { cycle });
}
// 超深(最长链 = Kahn 层数);环存在时层数无意义,跳过
if !errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })) {
if let Some(depth) = self.longest_chain_depth() {
if depth > opts.max_depth {
errs.push(PlanError::TooDeep {
depth,
max: opts.max_depth,
});
}
}
}
}
errs
}
/// Kahn 拓扑分层:输出可并行层(同层任务无相互依赖,可并行执行)。
///
/// **算法**:入度 0 入当前层 → 移除(其后继入度 -1→ 下层。纯算法无副作用。
///
/// **非法 Plan 行为**(调用方应先 validate
/// - 环:返回 `Err(CycleDetected)`Kahn 无法消除环边,剩余节点 = 环成员)。
/// - 悬空 dep忽略悬空边视作无该前驱照常分层。
/// - 空 Plan返回 `Ok(vec![])`。
///
/// 返回层的顺序 = 执行顺序(第 0 层先执行,同层内可并行)。
/// 每层 `Vec<SubTask>` 为克隆Kahn 消费入度,不消费原 Plan
pub fn to_layers(&self) -> Result<Vec<Vec<SubTask>>, CycleError> {
if self.tasks.is_empty() {
return Ok(Vec::new());
}
let id_to_task: HashMap<&SubTaskId, &SubTask> =
self.tasks.iter().map(|t| (&t.id, t)).collect();
// 入度:仅计可解析的 deps悬空 dep 不计入,避免卡死)
let mut indegree: HashMap<&SubTaskId, usize> = HashMap::new();
for t in &self.tasks {
indegree.entry(&t.id).or_insert(0);
}
for t in &self.tasks {
for dep in &t.deps {
// dep 存在且非自环才计
if dep != &t.id && id_to_task.contains_key(dep) {
*indegree.entry(&t.id).or_insert(0) += 1;
}
}
}
let mut layers: Vec<Vec<SubTask>> = Vec::new();
let mut placed: HashSet<&SubTaskId> = HashSet::new();
loop {
// 当前层:入度 0 且未放置(按原 tasks 顺序稳定)
let current: Vec<&SubTask> = self
.tasks
.iter()
.filter(|t| !placed.contains(&t.id))
.filter(|t| *indegree.get(&t.id).unwrap_or(&0) == 0)
.collect();
if current.is_empty() {
break;
}
// 放置并减后继入度
for t in &current {
placed.insert(&t.id);
}
// 后继入度 -1遍历所有任务若其 deps 含当前层任一 id则 -1
let current_ids: HashSet<&SubTaskId> =
current.iter().map(|t| &t.id).collect();
for t in &self.tasks {
if placed.contains(&t.id) {
continue;
}
for dep in &t.deps {
if current_ids.contains(dep) {
if let Some(v) = indegree.get_mut(&t.id) {
*v = v.saturating_sub(1);
}
}
}
}
layers.push(current.iter().map(|t| (*t).clone()).collect());
}
// 剩余未放置 = 环成员
if placed.len() < self.tasks.len() {
let cycle: Vec<SubTaskId> = self
.tasks
.iter()
.filter(|t| !placed.contains(&t.id))
.map(|t| t.id.clone())
.collect();
return Err(CycleError { cycle });
}
Ok(layers)
}
// ---- 内部算法辅助 ----
/// 环检测DFS 三色标记法)。假设无悬空 dep调用方已过滤
fn detect_cycle(&self) -> Option<Vec<SubTaskId>> {
let id_to_task: HashMap<&SubTaskId, &SubTask> =
self.tasks.iter().map(|t| (&t.id, t)).collect();
// 颜色0=白(未访问) 1=灰(栈中) 2=黑(完成)
let mut color: HashMap<&SubTaskId, u8> = HashMap::new();
for t in &self.tasks {
color.insert(&t.id, 0);
}
// 记录路径用于回溯环
let mut stack: Vec<&SubTaskId> = Vec::new();
// finished 节点(防重复 DFS
let mut finished: HashSet<&SubTaskId> = HashSet::new();
for t in &self.tasks {
if *color.get(&t.id).unwrap() != 0 {
continue;
}
if let Some(cycle) = dfs_cycle(&t.id, &id_to_task, &mut color, &mut stack, &mut finished)
{
return Some(cycle);
}
}
None
}
/// 最长依赖链深度(= Kahn 层数)。无环前提。返回 None 表示空 Plan。
fn longest_chain_depth(&self) -> Option<usize> {
match self.to_layers() {
Ok(layers) => {
if layers.is_empty() {
None
} else {
Some(layers.len())
}
}
// 环存在:返回 Nonedepth 无意义,由 CycleDetected 单独报)
Err(_) => None,
}
}
}
// ---- validate 选项 ----------------------------------------------------------
/// validate 行为选项。
#[derive(Debug, Clone, Copy)]
pub struct ValidateOptions {
/// 是否要求每个子任务有 tool_hint。plan_hint 场景可关(允许纯编排节点)。
pub require_tools: bool,
/// 依赖链深度上限。超 → TooDeep。
pub max_depth: usize,
}
impl Default for ValidateOptions {
fn default() -> Self {
Self {
require_tools: true,
max_depth: MAX_PLAN_DEPTH,
}
}
}
// ---- 错误类型 ---------------------------------------------------------------
/// Plan 校验错误(设计文档 §十三 0.7 枚举 + 路线图 0.7 五类非法补充)。
///
/// 对齐设计文档枚举:`CycleDetected / UnknownDep / OrphanSubtask / SubtaskWithoutTools`
/// 并补充路线图要求的五类非法Empty/DuplicateId/TooDeep+ 实现细化SelfDep/EmptyId
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PlanError {
/// 空 Plan路线图五类非法之一
Empty,
/// 子任务 id 为空字符串。
EmptyId { index: usize },
/// 重复 id路线图五类非法之一
DuplicateId { id: SubTaskId },
/// 悬空依赖deps 引用不存在的 id设计文档枚举 UnknownDep
UnknownDep { from: SubTaskId, unknown: SubTaskId },
/// 自环依赖A 依赖 A归为悬空的特例单独报便于定位
SelfDep { id: SubTaskId },
/// 环(设计文档枚举 CycleDetected。`cycle` = 环上节点 id 序列。
CycleDetected { cycle: Vec<SubTaskId> },
/// 子任务无工具(设计文档枚举 SubtaskWithoutTools
SubtaskWithoutTools { id: SubTaskId },
/// 依赖链超深(路线图五类非法之一)。
TooDeep { depth: usize, max: usize },
/// 孤立子任务(设计文档枚举 OrphanSubtask 占位,当前 validate 不报,保留供 Phase 2 决策)。
#[allow(dead_code)]
OrphanSubtask { id: SubTaskId },
}
impl std::fmt::Display for PlanError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PlanError::Empty => write!(f, "Plan 为空"),
PlanError::EmptyId { index } => write!(f, "第 {} 个子任务 id 为空", index),
PlanError::DuplicateId { id } => write!(f, "重复子任务 id: {}", id),
PlanError::UnknownDep { from, unknown } => {
write!(f, "子任务 {} 依赖不存在的 id: {}", from, unknown)
}
PlanError::SelfDep { id } => write!(f, "子任务 {} 自环依赖", id),
PlanError::CycleDetected { cycle } => {
write!(f, "检测到环: {}", cycle.join(" -> "))
}
PlanError::SubtaskWithoutTools { id } => write!(f, "子任务 {} 无 tool_hint", id),
PlanError::TooDeep { depth, max } => {
write!(f, "依赖链深度 {} 超上限 {}", depth, max)
}
PlanError::OrphanSubtask { id } => write!(f, "孤立子任务: {}", id),
}
}
}
impl std::error::Error for PlanError {}
/// Kahn 分层时检测到环([`Plan::to_layers`] 返回)。
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CycleError {
/// 环上(或因环无法放置的)节点 id 列表。
pub cycle: Vec<SubTaskId>,
}
impl std::fmt::Display for CycleError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Kahn 分层遇到环: {}", self.cycle.join(" -> "))
}
}
impl std::error::Error for CycleError {}
// ---- DFS 环检测辅助(模块级,借用 checker 规避 self-borrow ----------------
fn dfs_cycle<'a>(
start: &'a SubTaskId,
id_to_task: &HashMap<&'a SubTaskId, &'a SubTask>,
color: &mut HashMap<&'a SubTaskId, u8>,
stack: &mut Vec<&'a SubTaskId>,
finished: &mut HashSet<&'a SubTaskId>,
) -> Option<Vec<SubTaskId>> {
// 显式栈 DFS避免递归深度大时不爆栈
// stack 元素:(节点, 其 deps 的迭代位置)——此处简化:每节点完整遍历 deps
// 改用工作栈 + 回溯标记
struct Frame<'b> {
node: &'b SubTaskId,
dep_idx: usize,
}
let mut work: Vec<Frame<'a>> = vec![Frame { node: start, dep_idx: 0 }];
color.insert(start, 1); // 灰
stack.push(start);
while let Some(frame) = work.last_mut() {
let node = frame.node;
let task = match id_to_task.get(node) {
Some(t) => *t,
None => {
// 不应发生(调用方已过滤悬空),兜底回溯
work.pop();
color.insert(node, 2);
stack.pop();
continue;
}
};
let deps = &task.deps;
if frame.dep_idx < deps.len() {
let next_id = &deps[frame.dep_idx];
frame.dep_idx += 1;
// 自环跳过SelfDep 已单独报)
if next_id == node {
continue;
}
let next = match id_to_task.get(next_id) {
Some(t) => *t,
None => continue, // 悬空跳过
};
let next_color = *color.get(&next.id).unwrap_or(&0);
match next_color {
1 => {
// 命中灰节点 → 环。回溯 stack 找环段。
let mut cycle: Vec<SubTaskId> = Vec::new();
let mut found = false;
for s in stack.iter() {
if *s == &next.id {
found = true;
}
if found {
cycle.push((*s).clone());
}
}
cycle.push(next.id.clone()); // 闭合
return Some(cycle);
}
0 => {
color.insert(&next.id, 1);
stack.push(&next.id);
work.push(Frame {
node: &next.id,
dep_idx: 0,
});
}
_ => {} // 黑:已 finish跳过
}
} else {
// 当前节点 deps 耗尽 → 完成
let done_node = frame.node;
color.insert(done_node, 2);
finished.insert(done_node);
work.pop();
stack.pop();
}
}
None
}
// ---- 单测(对齐 intent.rs 52 测密度)---------------------------------------
#[cfg(test)]
mod tests {
use super::*;
// 辅助:构造一个带工具的最小子任务
fn t(id: &str) -> SubTask {
SubTask {
id: id.to_string(),
tool_hint: vec!["read_file".to_string()],
deps: Vec::new(),
group: None,
intent: format!("task-{}", id),
}
}
fn t_dep(id: &str, deps: &[&str]) -> SubTask {
let mut s = t(id);
s.deps = deps.iter().map(|s| s.to_string()).collect();
s
}
// 变体deps 用 String动态 id 场景,如循环构造的 n0/n1/...
fn t_dep_s<I: Into<String>>(id: &str, deps: I) -> SubTask {
let mut s = t(id);
s.deps = vec![deps.into()];
s
}
// ===== 0.8 Plan/SubTask 结构 =====
#[test]
fn subtask_builder_chains() {
let s = SubTask::new("a", "do a")
.with_tools(vec!["read_file".into()])
.with_deps(vec!["b".into()])
.with_group("g1");
assert_eq!(s.id, "a");
assert_eq!(s.intent, "do a");
assert_eq!(s.tool_hint, vec!["read_file".to_string()]);
assert_eq!(s.deps, vec!["b".to_string()]);
assert_eq!(s.group.as_deref(), Some("g1"));
}
#[test]
fn subtask_default_builder_minimal() {
let s = SubTask::new("x", "");
assert!(s.tool_hint.is_empty());
assert!(s.deps.is_empty());
assert!(s.group.is_none());
}
#[test]
fn plan_new_empty() {
let p = Plan::new();
assert!(p.is_empty());
assert_eq!(p.len(), 0);
}
#[test]
fn plan_from_tasks_len() {
let p = Plan::from_tasks(vec![t("a"), t("b")]);
assert_eq!(p.len(), 2);
assert!(!p.is_empty());
}
#[test]
fn plan_clone_eq() {
let p = Plan::from_tasks(vec![t("a")]);
let q = p.clone();
assert_eq!(p, q);
}
#[test]
fn feature_flags_default_on() {
// PLANNING_ENABLED 当前开(主 loop 走 filter_tool_defs_planned 规划),Phase 1 已落地。
// PLAN_HINT_ENABLED(plan_hint 函数 flag)在 plan_hint.rs 单一定义,此处不再断言。
assert!(PLANNING_ENABLED);
}
#[test]
fn max_plan_depth_is_reasonable() {
// 8 层覆盖典型软件任务分解
assert!(MAX_PLAN_DEPTH >= 4);
assert!(MAX_PLAN_DEPTH <= 16);
}
// ===== 0.7 validate: 合法 Plan 通过 =====
#[test]
fn validate_single_task_ok() {
let p = Plan::from_tasks(vec![t("a")]);
assert!(p.validate().is_ok(), "单任务无依赖应合法");
}
#[test]
fn validate_linear_chain_ok() {
// a -> b -> c
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]);
assert!(p.validate().is_ok(), "线性链应合法");
}
#[test]
fn validate_diamond_ok() {
// a -> b,c -> d (菱形)
let p = Plan::from_tasks(vec![
t("a"),
t_dep("b", &["a"]),
t_dep("c", &["a"]),
t_dep("d", &["b", "c"]),
]);
assert!(p.validate().is_ok(), "菱形 DAG 应合法");
}
#[test]
fn validate_parallel_group_ok() {
// 同 group 三个无依赖任务
let mut a = t("a");
a.group = Some("g1".into());
let mut b = t("b");
b.group = Some("g1".into());
let mut c = t("c");
c.group = Some("g1".into());
let p = Plan::from_tasks(vec![a, b, c]);
assert!(p.validate().is_ok(), "并行组无依赖应合法");
}
#[test]
fn validate_require_tools_off_allows_empty_tools() {
let mut s = t("a");
s.tool_hint.clear();
let p = Plan::from_tasks(vec![s]);
let errs = p.validate_with(ValidateOptions {
require_tools: false,
max_depth: MAX_PLAN_DEPTH,
});
assert!(errs.is_empty(), "关 require_tools 后无工具应放过");
}
// ===== 0.7 validate: 五类非法各拒 =====
#[test]
fn validate_empty_rejected() {
let p = Plan::new();
let r = p.validate();
assert_eq!(r, Err(vec![PlanError::Empty]));
}
#[test]
fn validate_duplicate_id_rejected() {
let p = Plan::from_tasks(vec![t("a"), t("a")]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() }));
}
#[test]
fn validate_dangling_dep_rejected() {
// b 依赖不存在的 x
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::UnknownDep {
from: "b".into(),
unknown: "x".into()
}));
}
#[test]
fn validate_self_dep_rejected() {
let p = Plan::from_tasks(vec![t_dep("a", &["a"])]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::SelfDep { id: "a".into() }));
}
#[test]
fn validate_cycle_two_node_rejected() {
// a <-> b
let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]);
let errs = p.validate().unwrap_err();
assert!(
errs.iter().any(|e| matches!(e, PlanError::CycleDetected { .. })),
"双向环应拒为 CycleDetected, errs={:?}", errs
);
}
#[test]
fn validate_cycle_three_node_rejected() {
// a -> b -> c -> a
let p = Plan::from_tasks(vec![
t_dep("a", &["c"]),
t_dep("b", &["a"]),
t_dep("c", &["b"]),
]);
let errs = p.validate().unwrap_err();
assert!(errs
.iter()
.any(|e| matches!(e, PlanError::CycleDetected { .. })));
}
#[test]
fn validate_cycle_self_loop_is_selfdep_not_cycle() {
// 自环优先报 SelfDep不进入环检测validate 早退条件)
let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t("b")]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::SelfDep { id: "a".into() }));
// SelfDep 触发环检测跳过(不误报)
assert!(!errs
.iter()
.any(|e| matches!(e, PlanError::CycleDetected { .. })));
}
#[test]
fn validate_subtask_without_tools_rejected() {
let mut s = t("a");
s.tool_hint.clear();
let p = Plan::from_tasks(vec![s]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() }));
}
#[test]
fn validate_too_deep_rejected() {
// 9 节点线性链(深度 9 > MAX_PLAN_DEPTH=8
let mut tasks: Vec<SubTask> = vec![t("n0")];
for i in 1..9 {
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
}
let p = Plan::from_tasks(tasks);
let errs = p.validate().unwrap_err();
assert!(
errs.iter().any(|e| matches!(e, PlanError::TooDeep { depth: 9, max: 8 })),
"9 层应判 TooDeep, errs={:?}", errs
);
}
#[test]
fn validate_depth_exactly_at_limit_ok() {
// 8 节点线性链(深度 8 == MAX_PLAN_DEPTH不超应合法
let mut tasks: Vec<SubTask> = vec![t("n0")];
for i in 1..8 {
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
}
let p = Plan::from_tasks(tasks);
assert!(p.validate().is_ok(), "8 层(==上限)应合法");
}
#[test]
fn validate_empty_id_rejected() {
let mut s = t("a");
s.id = String::new();
let p = Plan::from_tasks(vec![s]);
let errs = p.validate().unwrap_err();
assert!(errs
.iter()
.any(|e| matches!(e, PlanError::EmptyId { index: 0 })));
}
#[test]
fn validate_collects_multiple_errors() {
// 同时:重复 id + 悬空 dep + 无工具
let mut no_tools = t("a");
no_tools.tool_hint.clear();
let p = Plan::from_tasks(vec![
no_tools, // 无工具
t("a"), // 重复 id同 "a"
t_dep("b", &["x"]), // 悬空 x
]);
let errs = p.validate().unwrap_err();
assert!(errs.len() >= 3, "应一次收集 ≥3 类违规, got {} 条: {:?}", errs.len(), errs);
assert!(errs.contains(&PlanError::DuplicateId { id: "a".into() }));
assert!(errs.contains(&PlanError::SubtaskWithoutTools { id: "a".into() }));
assert!(errs.contains(&PlanError::UnknownDep {
from: "b".into(),
unknown: "x".into()
}));
}
#[test]
fn validate_dangling_does_not_mask_cycle() {
// 悬空 + 环共存:悬空报,环因早退不报(设计:悬空优先,环检测跳过)
let p = Plan::from_tasks(vec![
t_dep("a", &["b"]),
t_dep("b", &["a"]),
t_dep("c", &["x"]), // 悬空
]);
let errs = p.validate().unwrap_err();
assert!(errs.contains(&PlanError::UnknownDep {
from: "c".into(),
unknown: "x".into()
}));
}
#[test]
fn plan_error_display_nonempty() {
let e = PlanError::CycleDetected {
cycle: vec!["a".into(), "b".into(), "a".into()],
};
let s = format!("{}", e);
assert!(s.contains(""));
assert!(s.contains("a -> b"));
}
#[test]
fn cycle_error_display() {
let e = CycleError {
cycle: vec!["a".into()],
};
assert!(format!("{}", e).contains(""));
}
// ===== 0.7 Kahn to_layers =====
#[test]
fn layers_empty_plan() {
let p = Plan::new();
assert_eq!(p.to_layers().unwrap(), Vec::<Vec<SubTask>>::new());
}
#[test]
fn layers_single_task_one_layer() {
let p = Plan::from_tasks(vec![t("a")]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 1);
assert_eq!(layers[0].len(), 1);
assert_eq!(layers[0][0].id, "a");
}
#[test]
fn layers_linear_chain_n_layers() {
// a -> b -> c → 3 层各 1 节点
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["a"]), t_dep("c", &["b"])]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 3);
assert_eq!(layers[0][0].id, "a");
assert_eq!(layers[1][0].id, "b");
assert_eq!(layers[2][0].id, "c");
}
#[test]
fn layers_parallel_group_one_layer() {
// a,b,c 无依赖 → 同一层(可并行)
let p = Plan::from_tasks(vec![t("a"), t("b"), t("c")]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 1, "无依赖应单层: {:?}", layers);
let ids: Vec<&str> = layers[0].iter().map(|t| t.id.as_str()).collect();
assert_eq!(ids, vec!["a", "b", "c"]); // 保持原顺序稳定
}
#[test]
fn layers_diamond_two_then_one() {
// a -> b,c -> d
// 层 0: [a], 层 1: [b, c](并行), 层 2: [d]
let p = Plan::from_tasks(vec![
t("a"),
t_dep("b", &["a"]),
t_dep("c", &["a"]),
t_dep("d", &["b", "c"]),
]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 3);
assert_eq!(layer_ids(&layers, 0), vec!["a"]);
assert_eq!(layer_ids(&layers, 1), vec!["b", "c"]);
assert_eq!(layer_ids(&layers, 2), vec!["d"]);
}
#[test]
fn layers_preserves_input_order_within_layer() {
// 多个无依赖节点,同层应保持 Plan 内顺序(稳定)
let p = Plan::from_tasks(vec![t("z"), t("y"), t("x")]);
let layers = p.to_layers().unwrap();
assert_eq!(layer_ids(&layers, 0), vec!["z", "y", "x"]);
}
#[test]
fn layers_cycle_returns_err() {
// a <-> b
let p = Plan::from_tasks(vec![t_dep("a", &["b"]), t_dep("b", &["a"])]);
let err = p.to_layers().unwrap_err();
// 环成员 a,b 均无法放置
let cycle_set: HashSet<&str> =
err.cycle.iter().map(|s| s.as_str()).collect();
assert!(cycle_set.contains("a"));
assert!(cycle_set.contains("b"));
}
#[test]
fn layers_self_dep_treated_as_no_indeg() {
// 自环A 依赖 A。Kahn 视为无该前驱不计入度A 仍可放置
let p = Plan::from_tasks(vec![t_dep("a", &["a"]), t_dep("b", &["a"])]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 2);
assert_eq!(layer_ids(&layers, 0), vec!["a"]);
assert_eq!(layer_ids(&layers, 1), vec!["b"]);
}
#[test]
fn layers_dangling_dep_ignored() {
// b 依赖不存在的 x —— Kahn 忽略悬空边b 视为入度 0 入首层
let p = Plan::from_tasks(vec![t("a"), t_dep("b", &["x"])]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 1, "悬空边忽略后 a,b 同层: {:?}", layers);
let ids: HashSet<&str> =
layers[0].iter().map(|t| t.id.as_str()).collect();
assert!(ids.contains("a"));
assert!(ids.contains("b"));
}
#[test]
fn layers_deep_chain_count_matches_validate_depth() {
// 5 节点链 → 5 层 = longest_chain_depth
let mut tasks: Vec<SubTask> = vec![t("n0")];
for i in 1..5 {
tasks.push(t_dep_s(&format!("n{}", i), format!("n{}", i - 1)));
}
let p = Plan::from_tasks(tasks);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 5);
}
#[test]
fn layers_complex_dag() {
// 复杂 DAG:
// a b (层0)
// \ / \
// c d (层1: c 依赖 a,b; d 依赖 b)
// | |
// e f (层2: e 依赖 c; f 依赖 d)
// \ /
// g (层3: g 依赖 e,f)
let p = Plan::from_tasks(vec![
t("a"),
t("b"),
t_dep("c", &["a", "b"]),
t_dep("d", &["b"]),
t_dep("e", &["c"]),
t_dep("f", &["d"]),
t_dep("g", &["e", "f"]),
]);
let layers = p.to_layers().unwrap();
assert_eq!(layers.len(), 4);
assert_eq!(layer_ids(&layers, 0), vec!["a", "b"]);
assert_eq!(layer_ids(&layers, 1), vec!["c", "d"]);
assert_eq!(layer_ids(&layers, 2), vec!["e", "f"]);
assert_eq!(layer_ids(&layers, 3), vec!["g"]);
}
#[test]
fn layers_returns_clones_not_references() {
// 返回 Vec<SubTask>owned修改不影响原 Plan
let p = Plan::from_tasks(vec![t("a")]);
let mut layers = p.to_layers().unwrap();
layers[0][0].intent = "modified".into();
assert_eq!(p.tasks[0].intent, "task-a", "原 Plan 不应被改");
}
// 辅助
fn layer_ids(layers: &[Vec<SubTask>], i: usize) -> Vec<&str> {
layers[i].iter().map(|t| t.id.as_str()).collect()
}
}