修复: CR-01审查待修项①类7项(CSS补testing/blocked徽章+CAS防回收站推进+DRY+注释+测试改名+advancing接文案)+新增待审查.md审查队列(职责分离:主会话写/审查agent读)+todo销账
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user