From 4dc5219a3c5d7e1d5ca2c544e188e64886b0f542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Tue, 16 Jun 2026 03:12:24 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20CR-01=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E5=BE=85=E4=BF=AE=E9=A1=B9=E2=91=A0=E7=B1=BB7=E9=A1=B9(CSS?= =?UTF-8?q?=E8=A1=A5testing/blocked=E5=BE=BD=E7=AB=A0+CAS=E9=98=B2?= =?UTF-8?q?=E5=9B=9E=E6=94=B6=E7=AB=99=E6=8E=A8=E8=BF=9B+DRY+=E6=B3=A8?= =?UTF-8?q?=E9=87=8A+=E6=B5=8B=E8=AF=95=E6=94=B9=E5=90=8D+advancing?= =?UTF-8?q?=E6=8E=A5=E6=96=87=E6=A1=88)+=E6=96=B0=E5=A2=9E=E5=BE=85?= =?UTF-8?q?=E5=AE=A1=E6=9F=A5.md=E5=AE=A1=E6=9F=A5=E9=98=9F=E5=88=97(?= =?UTF-8?q?=E8=81=8C=E8=B4=A3=E5=88=86=E7=A6=BB:=E4=B8=BB=E4=BC=9A?= =?UTF-8?q?=E8=AF=9D=E5=86=99/=E5=AE=A1=E6=9F=A5agent=E8=AF=BB)+todo?= =?UTF-8?q?=E9=94=80=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/df-nodes/src/task_advance_node.rs | 17 +++--- crates/df-storage/src/crud.rs | 10 +++- docs/todo.md | 21 +++++++- docs/待审查.md | 69 ++++++++++++++++++++++++ src-tauri/src/state.rs | 6 +++ src/views/TaskDetail.vue | 5 +- 6 files changed, 116 insertions(+), 12 deletions(-) create mode 100644 docs/待审查.md diff --git a/crates/df-nodes/src/task_advance_node.rs b/crates/df-nodes/src/task_advance_node.rs index 34d469c..921c9a5 100644 --- a/crates/df-nodes/src/task_advance_node.rs +++ b/crates/df-nodes/src/task_advance_node.rs @@ -20,7 +20,7 @@ use df_storage::db::Database; use df_storage::models::TaskRecord; use df_workflow::node::{Node, NodeContext, NodeOutput, NodeResult, NodeSchema}; -use crate::task_state_machine::{can_transition, is_regression, is_valid_state}; +use crate::task_state_machine::{can_transition, is_regression, is_valid_state, ALL_STATES}; /// 推进任务到目标状态(核心逻辑,DAG 节点与 IPC 入口共用)。 /// @@ -45,8 +45,9 @@ pub async fn advance_task_atomic( // 1. target 合法性(7 态之一)。先于读库校验:即便任务不存在,也先拒绝非法状态值。 if !is_valid_state(target_status) { return Err(df_core::error::Error::Validation(format!( - "非法 target_status {:?},合法值: todo/in_progress/in_review/testing/done/blocked/cancelled", - target_status + "非法 target_status {:?},合法值: {}", + target_status, + ALL_STATES.join("/") ))); } @@ -295,15 +296,15 @@ mod tests { } #[tokio::test] - async fn concurrent_cas_change_detected() { - // 模拟 TOCTOU:推进前已被旁路改 status。CAS 的 expected 与库内不符 → None → 报错。 + async fn advance_succeeds_after_prior_status_change() { + // 前序旁路改 status 后,推进链基于「当前」status 判定合法转换并正常写入。 + // 注:这并非 CAS 并发失败场景(真 CAS 失败由 cas_returns_none_when_status_mismatch 覆盖), + // 而是验证读后改路径在 from=当前库内 status 时正常推进。 let repo = setup().await; repo.insert(rec("t1", "todo")).await.unwrap(); // 旁路把 status 改成 in_progress(模拟另一路并发推进) repo.update_field("t1", "status", "in_progress").await.unwrap(); - // 此时读出来是 in_progress,推进到 in_review 是合法的——这测的不是 CAS 失败, - // 而是验证「读后改」路径在 status 一致时正常。CAS 失败路径靠 advance_status_atomic - // 自身的 None 返回覆盖(下方 cas_returns_none_when_status_mismatch 单测)。 + // 读出来是 in_progress,推进到 in_review 合法 → 正常成功 let r = advance_task_atomic(&repo, "t1", "in_review").await.unwrap(); assert_eq!(r.status, "in_review"); } diff --git a/crates/df-storage/src/crud.rs b/crates/df-storage/src/crud.rs index 067a996..37dcd79 100644 --- a/crates/df-storage/src/crud.rs +++ b/crates/df-storage/src/crud.rs @@ -325,6 +325,10 @@ pub fn allowed_columns_for(table: &str) -> Option<&'static [&'static str]> { "tasks" => &[ // id/created_at 不列入:主键与创建时间不可通过通用 update_field 改写 // (防篡改主键/伪造创建时间/跨项目移动)。 + // review_rounds 不列入:它仅 advance_status_atomic 退回转换 + // (in_review→in_progress / testing→in_review)时原子 +1(收口:仅该专用路径可改, + // update_task/update_field 白名单均不含)。后人勿把 review_rounds 补进白名单, + // 否则破坏「review_rounds 唯一写入路径」收口、引入旁路写导致计数错乱。 "project_id", "title", "description", "status", "priority", "branch_name", "assignee", "workflow_def_id", "base_branch", "updated_at", // TODO(B-260616-16): project_id 跨表存在性校验待 commands/task.rs 层补。 @@ -846,12 +850,14 @@ impl TaskRepo { tokio::task::spawn_blocking(move || { let guard = conn.blocking_lock(); // CAS:WHERE id AND status=expected 锁定当前态;affected==0 即并发已改动。 + // deleted_at IS NULL:回收站任务(soft_delete 设了 deleted_at)CAS 必败→affected=0, + // 返回 None,杜绝回收站任务被推进(D-02 软删语义收口,一处关闭)。 let sql = if bump_rounds { "UPDATE tasks SET status = ?1, review_rounds = review_rounds + 1, updated_at = ?2 \ - WHERE id = ?3 AND status = ?4" + WHERE id = ?3 AND status = ?4 AND deleted_at IS NULL" } else { "UPDATE tasks SET status = ?1, updated_at = ?2 \ - WHERE id = ?3 AND status = ?4" + WHERE id = ?3 AND status = ?4 AND deleted_at IS NULL" }; let affected = guard .execute(sql, params![new_status, now, id, expected]) diff --git a/docs/todo.md b/docs/todo.md index ad080ea..4c57335 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -56,6 +56,12 @@ - [ ] B-260616-11 [P2] — **AI 消息 `-`/`+` 列表/diff 无视觉效果**。marked 实测两个出口:①裸 `-`/`+` 混用 → GFM 要求同列表标记一致,marked 拆成**两个独立 `