新增: 任务推进链(7态状态机+advance_task CAS原子写)+软删除+前后端7态对齐
This commit is contained in:
256
crates/df-nodes/src/task_state_machine.rs
Normal file
256
crates/df-nodes/src/task_state_machine.rs
Normal file
@@ -0,0 +1,256 @@
|
||||
//! 任务推进状态机 — 7 态合法转换定义(F-260616-01)
|
||||
//!
|
||||
//! 独立模块,非挂在 df-core::TaskStatus enum 上(对齐 D-260616-03「推进链业务逻辑落
|
||||
//! df-nodes」)。本模块只做「给定 from/to 是否合法」的纯函数判定,不触碰存储层
|
||||
//! (原子写 SQL 在 task_advance_node.rs 完成,见 F-260616-02)。
|
||||
//!
|
||||
//! 7 态(与 df-core::TaskStatus / 前端对齐,D-260616-01):
|
||||
//! todo / in_progress / in_review / testing / done / blocked / cancelled
|
||||
//!
|
||||
//! 闸门链主路径: todo → in_progress → in_review → testing → done
|
||||
//!
|
||||
//! 合法转换矩阵(以报告 docs/05-代码审查/任务执行与推进能力分析-2026-06-16.md §8
|
||||
//! 与决策 docs/02-架构设计/任务推进链实施路径-2026-06-16.md F-01 为准):
|
||||
//! - todo → in_progress(开始), cancelled(取消)
|
||||
//! - in_progress → in_review(提交审查), blocked(阻塞), cancelled
|
||||
//! - in_review → testing(审查通过进测试), in_progress(退回修改, review_rounds+1),
|
||||
//! blocked, cancelled
|
||||
//! - testing → done(测试通过), in_review(退回重审, review_rounds+1),
|
||||
//! blocked, cancelled
|
||||
//! - done → 终态(原则上不可变;若需重开走 cancelled 或新任务)
|
||||
//! - blocked → in_progress(解除阻塞继续), cancelled
|
||||
//! - cancelled → 终态
|
||||
|
||||
// ============================================================
|
||||
// 状态字符串常量 — 与 df-core::TaskStatus::as_str 一一对应
|
||||
// ============================================================
|
||||
//
|
||||
// 不复用 df-core::TaskStatus enum(独立模块定位 + 避免推进链判定耦合存储枚举类型),
|
||||
// 但字符串值严格对齐(df-core::TaskStatus::as_str 产出的小写 snake_case),
|
||||
// 保证状态机判定的 from/to 与数据库 status 列存值语义一致。
|
||||
|
||||
/// 待开始
|
||||
pub const TODO: &str = "todo";
|
||||
/// 进行中
|
||||
pub const IN_PROGRESS: &str = "in_progress";
|
||||
/// 代码审查中
|
||||
pub const IN_REVIEW: &str = "in_review";
|
||||
/// 测试中
|
||||
pub const TESTING: &str = "testing";
|
||||
/// 已完成(终态)
|
||||
pub const DONE: &str = "done";
|
||||
/// 已阻塞
|
||||
pub const BLOCKED: &str = "blocked";
|
||||
/// 已取消(终态)
|
||||
pub const CANCELLED: &str = "cancelled";
|
||||
|
||||
/// 全部合法状态值(供输入校验与错误提示复用)
|
||||
pub const ALL_STATES: &[&str] = &[
|
||||
TODO,
|
||||
IN_PROGRESS,
|
||||
IN_REVIEW,
|
||||
TESTING,
|
||||
DONE,
|
||||
BLOCKED,
|
||||
CANCELLED,
|
||||
];
|
||||
|
||||
/// 字符串是否为合法状态值
|
||||
pub fn is_valid_state(s: &str) -> bool {
|
||||
ALL_STATES.contains(&s)
|
||||
}
|
||||
|
||||
/// 判定从 `from` 到 `to` 的状态转换是否合法(状态机核心)。
|
||||
///
|
||||
/// 终态(done/cancelled)无任何合法后继;非法或未知状态入参一律返回 false
|
||||
/// (调用方 advance_task 在前置校验已拦截非法 status,此处保守拒绝防漏)。
|
||||
///
|
||||
/// 注意:仅判「是否合法」,不判「是否是退回」——退回(导致 review_rounds+1)
|
||||
/// 的识别见 [`is_regression`],advance_task 据此决定是否一并 SET review_rounds+=1。
|
||||
pub fn can_transition(from: &str, to: &str) -> bool {
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// 转换表静态构造一次(运行时常量,无锁开销分摊)。行=from,列=to。
|
||||
// 表条目为 true 即合法转换。未列入的 (from, to) 一律 false。
|
||||
static TABLE: OnceLock<std::collections::HashMap<(&'static str, &'static str), bool>> =
|
||||
OnceLock::new();
|
||||
let table = TABLE.get_or_init(|| {
|
||||
let mut m = std::collections::HashMap::new();
|
||||
let allowed: &[(&str, &str)] = &[
|
||||
// todo → in_progress(开始), cancelled(取消)
|
||||
(TODO, IN_PROGRESS),
|
||||
(TODO, CANCELLED),
|
||||
// in_progress → in_review(提交审查), blocked(阻塞), cancelled
|
||||
(IN_PROGRESS, IN_REVIEW),
|
||||
(IN_PROGRESS, BLOCKED),
|
||||
(IN_PROGRESS, CANCELLED),
|
||||
// in_review → testing(审查通过进测试), in_progress(退回修改),
|
||||
// blocked, cancelled
|
||||
(IN_REVIEW, TESTING),
|
||||
(IN_REVIEW, IN_PROGRESS),
|
||||
(IN_REVIEW, BLOCKED),
|
||||
(IN_REVIEW, CANCELLED),
|
||||
// testing → done(测试通过), in_review(退回重审), blocked, cancelled
|
||||
(TESTING, DONE),
|
||||
(TESTING, IN_REVIEW),
|
||||
(TESTING, BLOCKED),
|
||||
(TESTING, CANCELLED),
|
||||
// done → 终态,无后继
|
||||
// blocked → in_progress(解除阻塞继续), cancelled
|
||||
(BLOCKED, IN_PROGRESS),
|
||||
(BLOCKED, CANCELLED),
|
||||
// cancelled → 终态,无后继
|
||||
];
|
||||
for (f, t) in allowed {
|
||||
m.insert((*f, *t), true);
|
||||
}
|
||||
m
|
||||
});
|
||||
|
||||
table.get(&(from, to)).copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
/// 判定一次转换是否为「退回」(review_rounds 应 +1)。
|
||||
///
|
||||
/// 退回语义:任务从前向推进阶段回退到更早的推进阶段,意味着上一轮产出未过闸门、
|
||||
/// 需重做。具体两类:
|
||||
/// - in_review → in_progress(审查退回修改)
|
||||
/// - testing → in_review(测试退回重审)
|
||||
///
|
||||
/// 其余合法转换(前向推进 / 进出 blocked / 进 cancelled)均不累加 review_rounds。
|
||||
pub fn is_regression(from: &str, to: &str) -> bool {
|
||||
matches!((from, to), (IN_REVIEW, IN_PROGRESS) | (TESTING, IN_REVIEW))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 单元测试 — 转换矩阵 + 边界
|
||||
// ============================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---------- can_transition 闸门主路径 ----------
|
||||
|
||||
#[test]
|
||||
fn main_path_todo_to_done_all_forward() {
|
||||
assert!(can_transition(TODO, IN_PROGRESS));
|
||||
assert!(can_transition(IN_PROGRESS, IN_REVIEW));
|
||||
assert!(can_transition(IN_REVIEW, TESTING));
|
||||
assert!(can_transition(TESTING, DONE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancel_from_any_non_terminal() {
|
||||
for s in [TODO, IN_PROGRESS, IN_REVIEW, TESTING, BLOCKED] {
|
||||
assert!(can_transition(s, CANCELLED), "应允许 {s} → cancelled");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_round_trip() {
|
||||
assert!(can_transition(IN_PROGRESS, BLOCKED));
|
||||
assert!(can_transition(BLOCKED, IN_PROGRESS));
|
||||
assert!(can_transition(IN_REVIEW, BLOCKED));
|
||||
assert!(can_transition(TESTING, BLOCKED));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regression_paths_allowed() {
|
||||
assert!(can_transition(IN_REVIEW, IN_PROGRESS));
|
||||
assert!(can_transition(TESTING, IN_REVIEW));
|
||||
}
|
||||
|
||||
// ---------- 非法转换被拒 ----------
|
||||
|
||||
#[test]
|
||||
fn illegal_forward_skips_rejected() {
|
||||
// 不允许跳过闸门(直奔 done)
|
||||
assert!(!can_transition(TODO, DONE));
|
||||
assert!(!can_transition(TODO, TESTING));
|
||||
assert!(!can_transition(TODO, IN_REVIEW));
|
||||
assert!(!can_transition(IN_PROGRESS, DONE));
|
||||
assert!(!can_transition(IN_PROGRESS, TESTING));
|
||||
assert!(!can_transition(IN_REVIEW, DONE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_states_have_no_successors() {
|
||||
// done / cancelled 是终态,任何后继都拒绝
|
||||
for term in [DONE, CANCELLED] {
|
||||
for to in ALL_STATES {
|
||||
assert!(!can_transition(term, to), "终态 {term} 不应有后继 → {to}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backward_to_todo_rejected() {
|
||||
// 不允许回退到 todo(开始即不可撤回)
|
||||
for s in [IN_PROGRESS, IN_REVIEW, TESTING, BLOCKED] {
|
||||
assert!(!can_transition(s, TODO), "不应允许 {s} → todo");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocked_only_to_in_progress_or_cancelled() {
|
||||
assert!(can_transition(BLOCKED, IN_PROGRESS));
|
||||
assert!(can_transition(BLOCKED, CANCELLED));
|
||||
// 解除阻塞不能直接跳到 in_review/testing/done
|
||||
assert!(!can_transition(BLOCKED, IN_REVIEW));
|
||||
assert!(!can_transition(BLOCKED, TESTING));
|
||||
assert!(!can_transition(BLOCKED, DONE));
|
||||
}
|
||||
|
||||
// ---------- 未知状态 ----------
|
||||
|
||||
#[test]
|
||||
fn unknown_states_rejected() {
|
||||
assert!(!can_transition("unknown", TODO));
|
||||
assert!(!can_transition(TODO, "unknown"));
|
||||
assert!(!can_transition("", ""));
|
||||
}
|
||||
|
||||
// ---------- is_regression ----------
|
||||
|
||||
#[test]
|
||||
fn regression_only_on_review_back_edges() {
|
||||
assert!(is_regression(IN_REVIEW, IN_PROGRESS));
|
||||
assert!(is_regression(TESTING, IN_REVIEW));
|
||||
// 前向推进不累加
|
||||
assert!(!is_regression(TODO, IN_PROGRESS));
|
||||
assert!(!is_regression(IN_PROGRESS, IN_REVIEW));
|
||||
assert!(!is_regression(IN_REVIEW, TESTING));
|
||||
assert!(!is_regression(TESTING, DONE));
|
||||
// 进出 blocked 不累加
|
||||
assert!(!is_regression(IN_PROGRESS, BLOCKED));
|
||||
assert!(!is_regression(BLOCKED, IN_PROGRESS));
|
||||
// 进 cancelled 不累加
|
||||
assert!(!is_regression(IN_REVIEW, CANCELLED));
|
||||
// 不合法的"退回"(实际上不存在的转换)也不应判定为 regression
|
||||
assert!(!is_regression(TESTING, IN_PROGRESS));
|
||||
}
|
||||
|
||||
// ---------- is_valid_state ----------
|
||||
|
||||
#[test]
|
||||
fn is_valid_state_accepts_7_known() {
|
||||
for s in ALL_STATES {
|
||||
assert!(is_valid_state(s));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_valid_state_rejects_unknown() {
|
||||
assert!(!is_valid_state(""));
|
||||
assert!(!is_valid_state("unknown"));
|
||||
assert!(!is_valid_state("TODO")); // 大写
|
||||
assert!(!is_valid_state("merged")); // 历史 5 态残留
|
||||
assert!(!is_valid_state("abandoned"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_states_has_seven_entries() {
|
||||
assert_eq!(ALL_STATES.len(), 7);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user