新增: AI Chat多项增强(审批去重/编辑重发/导出/实体引用/会话置顶搜索)+任务推进链df-nodes落地
This commit is contained in:
@@ -30,9 +30,12 @@ use crate::task_state_machine::{can_transition, is_regression, is_valid_state, A
|
||||
/// - `target_status`:目标状态(7 态之一,snake_case)
|
||||
///
|
||||
/// 流程:
|
||||
/// 1. 任务存在性:找不到 → InvalidState 错误(复用 Error::InvalidState 语义,前端可据此提示)
|
||||
/// 1. 任务存在性:找不到 → NotFound 错误(前端可据此提示)
|
||||
/// 2. target_status 合法性:非 7 态 → Validation 错误(防脏数据直入推进链)
|
||||
/// 3. 状态机:can_transition(from, to) 不合法 → InvalidState 错误(含 from/to 上下文)
|
||||
/// 3. 状态机(三类拒绝,错误区分供前端分辨):
|
||||
/// - 同态拒绝(from==to):Validation「相同状态,无需推进」(非状态机违例,是空操作)
|
||||
/// - 非法转换(跳态/终态后继等):InvalidState「非法状态转换 X→Y」(含 from/to 上下文)
|
||||
/// - can_transition 闸门矩阵判否即此分支
|
||||
/// 4. 原子写:advance_status_atomic CAS,to 是退回转换时 bump_rounds=true
|
||||
/// 5. CAS 失败(affected==0):状态已被并发改动 → InvalidState 错误(防 TOCTOU 静默成功)
|
||||
///
|
||||
@@ -57,17 +60,18 @@ pub async fn advance_task_atomic(
|
||||
.await?
|
||||
.ok_or_else(|| df_core::error::Error::NotFound(format!("任务 {} 不存在", id)))?;
|
||||
|
||||
// 3. 状态机校验(同态拒绝:todo→todo 等,can_transition 返回 false)。
|
||||
// 3. 状态机校验(三类拒绝,错误类型区分供前端分辨):
|
||||
// - 同态(from==to):Validation「相同状态,无需推进」(空操作,非状态机违例)
|
||||
// - 非法转换(跳态/终态无后继等):InvalidState「非法状态转换 X→Y」
|
||||
let from = current.status.as_str();
|
||||
if from == target_status {
|
||||
return Err(df_core::error::Error::InvalidState {
|
||||
current: from.to_string(),
|
||||
expected: target_status.to_string(),
|
||||
});
|
||||
return Err(df_core::error::Error::Validation(format!(
|
||||
"相同状态 {from:?},无需推进"
|
||||
)));
|
||||
}
|
||||
if !can_transition(from, target_status) {
|
||||
return Err(df_core::error::Error::InvalidState {
|
||||
current: from.to_string(),
|
||||
current: format!("{from}→{target_status}(非法状态转换)"),
|
||||
expected: target_status.to_string(),
|
||||
});
|
||||
}
|
||||
@@ -258,26 +262,48 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn illegal_skip_rejected() {
|
||||
// CR-01-D: 非法转换(跳态)归 InvalidState,且消息含 from→to 上下文与「非法状态转换」。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "done").await.unwrap_err();
|
||||
assert!(matches!(err, df_core::error::Error::InvalidState { .. }));
|
||||
match err {
|
||||
df_core::error::Error::InvalidState { current, expected } => {
|
||||
assert!(current.contains("todo→done"), "非法转换消息应含 from→to,实际: {current}");
|
||||
assert!(current.contains("非法状态转换"), "消息应含「非法状态转换」,实际: {current}");
|
||||
assert_eq!(expected, "done");
|
||||
}
|
||||
other => panic!("非法转换应是 InvalidState,实际: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_done_no_successor() {
|
||||
// CR-01-D: 终态无后继也是非法转换路径,归 InvalidState(同 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "done")).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "todo").await.unwrap_err();
|
||||
assert!(matches!(err, df_core::error::Error::InvalidState { .. }));
|
||||
match err {
|
||||
df_core::error::Error::InvalidState { current, .. } => {
|
||||
assert!(current.contains("done→todo"), "终态后继消息应含 from→to,实际: {current}");
|
||||
}
|
||||
other => panic!("终态后继应是 InvalidState,实际: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn same_status_rejected() {
|
||||
// CR-01-D: 同态拒绝(from==to)与非法转换(跳态)错误类型区分。
|
||||
// 同态属空操作,归 Validation「相同状态,无需推进」;
|
||||
// 非法转换归 InvalidState「非法状态转换 X→Y」(见 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "in_progress")).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap_err();
|
||||
assert!(matches!(err, df_core::error::Error::InvalidState { .. }));
|
||||
match err {
|
||||
df_core::error::Error::Validation(msg) => {
|
||||
assert!(msg.contains("相同状态"), "同态消息应含「相同状态」,实际: {msg}");
|
||||
}
|
||||
other => panic!("同态拒绝应是 Validation,实际: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -339,4 +365,191 @@ mod tests {
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.review_rounds, 0, "解除 blocked 不累加");
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// CR-06 观察项 ② 测试补强 — ②-3(completed)/②-4(failed) 回调推进覆盖
|
||||
//
|
||||
// workflow.rs 的 spawn 闭包回调依赖三段纯计算:
|
||||
// 1. template_for(target) 选模板(已由 task_workflow_templates.rs 测试覆盖)
|
||||
// 2. regression_target(target) 推失败退回态(workflow.rs 私有,此处镜像锁定契约)
|
||||
// 3. advance_task_atomic(repo, tid, to) 落库(本模块核心,下方覆盖各回调 to)
|
||||
//
|
||||
// workflow.rs spawn 闭包集成测试需 mock AppState/AppHandle/EventBus,df-nodes crate
|
||||
// 无此基础设施 → 降级为纯函数级 + 状态机集成级(零 mock,稳)。回调三段计算全覆盖,
|
||||
// 唯一未覆盖的是「闭包组装 + tracing」胶水代码(非业务逻辑)。
|
||||
// ============================================================
|
||||
|
||||
/// 镜像 workflow.rs::regression_target(workflow.rs L44-51 私有函数)的失败退回映射。
|
||||
///
|
||||
/// 此处是契约镜像(非导入):若 workflow.rs 改映射而忘同步,这里会红,提醒两处一致。
|
||||
/// 映射:testing→in_review / in_review→in_progress / in_progress→todo / 其他→None。
|
||||
///
|
||||
/// 为什么不 pub regression_target 复用? 它是 workflow.rs(app crate)的私有函数,
|
||||
/// df-nodes 不依赖 app crate(单向依赖:app → df-nodes)。把退回映射当 df-nodes 的
|
||||
/// 业务契约之一镜像锁定,符合「推进链业务逻辑落 df-nodes」(D-260616-03)定位。
|
||||
fn regression_target(target: &str) -> Option<&'static str> {
|
||||
match target {
|
||||
"testing" => Some("in_review"),
|
||||
"in_review" => Some("in_progress"),
|
||||
"in_progress" => Some("todo"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 镜像 workflow.rs spawn 闭包的回调决策(completed→target / failed→regression_target)。
|
||||
/// 抽出纯函数便于直接断言,与 workflow.rs L267-273 的 match 逻辑一一对应。
|
||||
///
|
||||
/// 返回 Option<String>(非 &str):completed 分支回传入的 target_status(非 'static),
|
||||
/// failed 分支回 regression_target 的常量。统一 String 避免生命周期冲突。
|
||||
fn callback_advance_target(status: &str, target_status: &str) -> Option<String> {
|
||||
match status {
|
||||
"completed" => Some(target_status.to_string()),
|
||||
"failed" => regression_target(target_status).map(String::from),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn regression_target_mapping_matches_contract() {
|
||||
// 锁定 workflow.rs ②-4 失败退回映射表(逐项)。
|
||||
assert_eq!(regression_target("testing"), Some("in_review"));
|
||||
assert_eq!(regression_target("in_review"), Some("in_progress"));
|
||||
assert_eq!(regression_target("in_progress"), Some("todo"));
|
||||
// done 是终态前向目标,失败无可退态(workflow.rs 注释:done→None)
|
||||
assert_eq!(regression_target("done"), None);
|
||||
// todo 是起点态无可退;blocked/cancelled 非推进链目标 → None
|
||||
assert_eq!(regression_target("todo"), None);
|
||||
assert_eq!(regression_target("blocked"), None);
|
||||
assert_eq!(regression_target("cancelled"), None);
|
||||
// 未知态防御性 → None
|
||||
assert_eq!(regression_target("merged"), None);
|
||||
assert_eq!(regression_target(""), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_completed_advances_to_target() {
|
||||
// ②-3:工作流 completed → 推进到 target_status(无论 target 是哪个)
|
||||
assert_eq!(
|
||||
callback_advance_target("completed", "in_progress").as_deref(),
|
||||
Some("in_progress")
|
||||
);
|
||||
assert_eq!(
|
||||
callback_advance_target("completed", "testing").as_deref(),
|
||||
Some("testing")
|
||||
);
|
||||
assert_eq!(
|
||||
callback_advance_target("completed", "done").as_deref(),
|
||||
Some("done")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_failed_advances_to_regression_target() {
|
||||
// ②-4:工作流 failed → 推进到 regression_target(target)
|
||||
assert_eq!(
|
||||
callback_advance_target("failed", "testing").as_deref(),
|
||||
Some("in_review")
|
||||
);
|
||||
assert_eq!(
|
||||
callback_advance_target("failed", "in_review").as_deref(),
|
||||
Some("in_progress")
|
||||
);
|
||||
assert_eq!(
|
||||
callback_advance_target("failed", "in_progress").as_deref(),
|
||||
Some("todo")
|
||||
);
|
||||
// failed + done:无退回映射 → None(回调跳过,workflow.rs 会 warn 跳过日志)
|
||||
assert_eq!(callback_advance_target("failed", "done"), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn callback_non_terminal_status_skips() {
|
||||
// 工作流非终态(running/cancelled 等)不应触发回调 — workflow.rs match 的 _ 分支。
|
||||
// (cancelled 在 workflow.rs 实际归入 failed 路径因 status 字段取值,这里锁定
|
||||
// 唯一终态语义:仅 "completed"/"failed" 两态触发推进。)
|
||||
for non_terminal in ["running", "pending", ""] {
|
||||
assert_eq!(
|
||||
callback_advance_target(non_terminal, "in_progress"),
|
||||
None,
|
||||
"非终态 {non_terminal:?} 不应触发回调推进"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ②-3 回调落库验证:completed → advance_task_atomic(target) 成功推进 + 不累加 rounds。
|
||||
/// 覆盖三个 target 的前向推进(in_progress/testing/done)从各自合法 from 态触发。
|
||||
#[tokio::test]
|
||||
async fn callback_completed_advance_lands_in_db() {
|
||||
// in_progress: todo → in_progress(②-3 in_progress 模板完成)
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("c1", "todo")).await.unwrap();
|
||||
let to = callback_advance_target("completed", "in_progress").unwrap();
|
||||
let r = advance_task_atomic(&repo, "c1", &to).await.unwrap();
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.review_rounds, 0, "②-3 前向推进不累加 review_rounds");
|
||||
|
||||
// testing: in_review → testing(②-3 testing 模板自审+核对通过)
|
||||
repo.insert(rec("c2", "in_review")).await.unwrap();
|
||||
let to = callback_advance_target("completed", "testing").unwrap();
|
||||
let r = advance_task_atomic(&repo, "c2", &to).await.unwrap();
|
||||
assert_eq!(r.status, "testing");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
|
||||
// done: testing → done(②-3 done 模板最终核对通过)
|
||||
repo.insert(rec("c3", "testing")).await.unwrap();
|
||||
let to = callback_advance_target("completed", "done").unwrap();
|
||||
let r = advance_task_atomic(&repo, "c3", &to).await.unwrap();
|
||||
assert_eq!(r.status, "done");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
}
|
||||
|
||||
/// ②-4 回调落库验证:failed → advance_task_atomic(regression_target) 退回 + 累加 rounds。
|
||||
/// 关键差异:②-4 退回转换是 regression,review_rounds += 1(与 ②-3 前向推进 0 增量对比)。
|
||||
#[tokio::test]
|
||||
async fn callback_failed_regression_lands_in_db_with_rounds_bump() {
|
||||
// testing 模板失败:任务当前 testing → 退回 in_review(rounds+1)
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("f1", "testing")).await.unwrap();
|
||||
let to = callback_advance_target("failed", "testing").unwrap();
|
||||
assert_eq!(to, "in_review", "testing 失败应退回 in_review");
|
||||
let r = advance_task_atomic(&repo, "f1", &to).await.unwrap();
|
||||
assert_eq!(r.status, "in_review");
|
||||
assert_eq!(r.review_rounds, 1, "②-4 退回应累加 review_rounds(+1)");
|
||||
|
||||
// in_review 模板失败(注:in_review 非 callback target,但映射存在性仍锁定):
|
||||
// in_review → in_progress。此处验证 regression_target 对 in_review 的映射,
|
||||
// 即便当前推进链 testing 模板失败也可能退到 in_progress(链式退回)。
|
||||
repo.insert(rec("f2", "in_review")).await.unwrap();
|
||||
let to = callback_advance_target("failed", "in_review").unwrap();
|
||||
assert_eq!(to, "in_progress");
|
||||
let r = advance_task_atomic(&repo, "f2", &to).await.unwrap();
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.review_rounds, 1);
|
||||
|
||||
// in_progress 模板失败:任务当前 in_progress → 退回 todo(rounds 不累加,
|
||||
// 因 todo→in_progress 是前向,in_progress→todo 是非法转换 —— 见状态机)
|
||||
// 关键:regression_target("in_progress")=todo,但状态机不允许 in_progress→todo,
|
||||
// 故回调 advance_task_atomic 会报 InvalidState。验证这一防御性行为:
|
||||
repo.insert(rec("f3", "in_progress")).await.unwrap();
|
||||
let to = callback_advance_target("failed", "in_progress").unwrap();
|
||||
assert_eq!(to, "todo");
|
||||
let err = advance_task_atomic(&repo, "f3", &to).await.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, df_core::error::Error::InvalidState { .. }),
|
||||
"in_progress→todo 是非法转换,回调应被状态机拦截(InvalidState),实际: {err:?}"
|
||||
);
|
||||
// 任务状态未被改动(仍是 in_progress)
|
||||
let still = repo.get_by_id("f3").await.unwrap().unwrap();
|
||||
assert_eq!(still.status, "in_progress", "非法退回不应改动 status");
|
||||
}
|
||||
|
||||
/// ②-4 回调 failed+done 无退回映射验证:callback_advance_target 返回 None。
|
||||
/// workflow.rs 此分支仅 tracing::warn! 跳过,不调 advance_task_atomic。
|
||||
/// 锁定:done 失败不会推进任务(终态前向目标失败,无可退态)。
|
||||
#[tokio::test]
|
||||
async fn callback_failed_done_skips_advance() {
|
||||
let to = callback_advance_target("failed", "done");
|
||||
assert_eq!(to, None, "done 失败无退回映射 → 回调跳过(None)");
|
||||
// 不调 advance_task_atomic(workflow.rs L294-302 的 else 分支)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user