优化: df-ai/df-storage/df-nodes 底层加固: 上下文/模型/节点/仓储
df-ai: - context/mod.rs + manager_tests.rs: 上下文管理调整 + 测试同步(48 行) - openai_compat.rs: OpenAI 兼容层增强(+51) - presets/models.json: 模型预设 +18 df-storage: - crud/conversation_repo.rs: 会话仓储(loop_state 相关) - crud/task_repo.rs: 任务仓储 - crud/mod.rs: 导出调整 df-nodes: - task_advance_node.rs: 任务推进节点 验收: cargo check -p devflow EXIT 0。
This commit is contained in:
@@ -67,6 +67,26 @@ pub async fn advance_task_atomic(
|
||||
.await?
|
||||
.ok_or_else(|| df_types::error::Error::NotFound(format!("任务 {} 不存在", id)))?;
|
||||
|
||||
// 2.5 前置校验:归档任务冻结,不可推进(与 CAS SQL 的 archived=0 条件同源)。
|
||||
//
|
||||
// 背景:advance_status_atomic 的 CAS SQL 带 `AND archived = 0`,归档任务必然
|
||||
// affected==0 → 返回 None → 下游误报「已变更(并发推进或旁路修改)」,诊断信息
|
||||
// 完全误导(真实原因是归档,非并发)。此处前置拦截,给出精确可操作错误。
|
||||
//
|
||||
// 软删(deleted_at IS NOT NULL)任务 get_by_id 仍能读到(TASK_COLS 不 SELECT
|
||||
// deleted_at,TaskRecord 无该字段),但 CAS 的 `deleted_at IS NULL` 同样必败。
|
||||
// 因 TaskRecord 不带 deleted_at,此处无法直接判定;由 advance_status_atomic
|
||||
// 的 CAS 失败回查区分(见该函数注释)。
|
||||
if current.archived {
|
||||
return Err(df_types::error::Error::InvalidState {
|
||||
current: format!(
|
||||
"任务 {} 已归档(archived=true),归档任务冻结不可推进;如需推进请先取消归档",
|
||||
id
|
||||
),
|
||||
expected: target_status.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 状态机校验(三类拒绝,错误类型区分供前端分辨):
|
||||
// - 同态(from==to):Validation「相同状态,无需推进」(空操作,非状态机违例)
|
||||
// - 非法转换(跳态/终态无后继等):InvalidState「非法状态转换 X→Y」
|
||||
@@ -112,11 +132,45 @@ pub async fn advance_task_atomic(
|
||||
.advance_status_atomic(id, from, target_status, bump)
|
||||
.await?;
|
||||
|
||||
// 5. CAS 失败:并发已改动 status(或任务被删),拒绝静默成功。
|
||||
updated.ok_or_else(|| df_types::error::Error::InvalidState {
|
||||
current: "已变更(并发推进或旁路修改)".to_string(),
|
||||
expected: from.to_string(),
|
||||
})
|
||||
// 5. CAS 失败:affected==0 有四种可能,回查区分,给出精确错误(不再一律误报「并发」)。
|
||||
//
|
||||
// CAS SQL 条件:`WHERE id=? AND status=? AND deleted_at IS NULL AND archived=0`。
|
||||
// affected==0 时逐一排查,避免「已变更(并发推进或旁路修改)」这个误导性文案:
|
||||
// a) deleted_at 非 NULL → 任务在回收站(回查 is_deleted)
|
||||
// b) archived=1 → 已归档(前置 2.5 已拦,此处兜底防 TOCTOU)
|
||||
// c) status 真被并发改动 → 回读当前 status 对比 from
|
||||
// d) 任务被删(id 不存在)→ get_by_id 返 None
|
||||
let Some(updated) = updated else {
|
||||
// a) 回收站任务(最易被误报为「并发」的一类,优先排查)
|
||||
if repo.is_deleted(id).await? {
|
||||
return Err(df_types::error::Error::InvalidState {
|
||||
current: format!(
|
||||
"任务 {} 在回收站(deleted_at 非 NULL),回收站任务不可推进;如需推进请先恢复",
|
||||
id
|
||||
),
|
||||
expected: from.to_string(),
|
||||
});
|
||||
}
|
||||
// b/c/d) 回读最新记录,区分归档 / 真并发 / 已删除
|
||||
let latest = repo.get_by_id(id).await?;
|
||||
return Err(match latest {
|
||||
None => df_types::error::Error::NotFound(format!("任务 {} 不存在", id)),
|
||||
Some(rec) if rec.archived => df_types::error::Error::InvalidState {
|
||||
current: format!("任务 {} 已归档(archived=true),不可推进;如需推进请先取消归档", id),
|
||||
expected: from.to_string(),
|
||||
},
|
||||
Some(rec) => df_types::error::Error::InvalidState {
|
||||
current: format!(
|
||||
"状态已变更(并发推进或旁路修改):期望 {:?},实际 {:?}",
|
||||
from,
|
||||
rec.status.as_str()
|
||||
),
|
||||
expected: from.to_string(),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -646,6 +700,69 @@ mod tests {
|
||||
assert!(r.is_none(), "expected 与库内不符应 CAS 失败");
|
||||
}
|
||||
|
||||
// ---------- 归档/软删任务的精确错误(治「已变更(并发推进或旁路修改)」误导) ----------
|
||||
|
||||
#[tokio::test]
|
||||
async fn archived_task_rejected_with_precise_error() {
|
||||
// 归档任务(archived=true)推进应被前置校验拦截,给出「已归档」精确错误,
|
||||
// 而非落到 CAS 失败报「已变更(并发推进或旁路修改)」误导文案。
|
||||
let repo = setup().await;
|
||||
let mut r = rec("t1", TaskStatus::Todo);
|
||||
r.archived = true;
|
||||
repo.insert(r).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap_err();
|
||||
match err {
|
||||
df_types::error::Error::InvalidState { current, .. } => {
|
||||
assert!(
|
||||
current.contains("已归档"),
|
||||
"归档任务错误应含「已归档」,实际: {current}"
|
||||
);
|
||||
assert!(
|
||||
!current.contains("并发推进或旁路修改"),
|
||||
"归档任务不应误报并发,实际: {current}"
|
||||
);
|
||||
}
|
||||
other => panic!("归档任务拒绝应是 InvalidState,实际: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn soft_deleted_task_rejected_with_precise_error() {
|
||||
// 回收站任务(deleted_at 非 NULL)推进:get_by_id 仍能读到(不过滤软删),
|
||||
// 状态机校验通过,但 CAS SQL 带 deleted_at IS NULL 必败 → 回查 is_deleted
|
||||
// 区分,给出「在回收站」精确错误,而非误报「已变更(并发推进或旁路修改)」。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
// 软删该任务(走 soft_delete,设 deleted_at)
|
||||
let ok = repo.soft_delete("t1").await.unwrap();
|
||||
assert!(ok, "soft_delete 应命中");
|
||||
assert!(repo.is_deleted("t1").await.unwrap(), "软删后 is_deleted 应为 true");
|
||||
let err = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap_err();
|
||||
match err {
|
||||
df_types::error::Error::InvalidState { current, .. } => {
|
||||
assert!(
|
||||
current.contains("回收站"),
|
||||
"软删任务错误应含「回收站」,实际: {current}"
|
||||
);
|
||||
assert!(
|
||||
!current.contains("并发推进或旁路修改"),
|
||||
"软删任务不应误报并发,实际: {current}"
|
||||
);
|
||||
}
|
||||
other => panic!("软删任务拒绝应是 InvalidState,实际: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn is_deleted_false_for_active_task() {
|
||||
// 活跃任务 is_deleted 应为 false(不误判),保证回查区分逻辑不误伤正常任务。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
assert!(!repo.is_deleted("t1").await.unwrap(), "活跃任务 is_deleted 应为 false");
|
||||
// 不存在的任务:is_deleted 返回 false(不存在 ≠ 软删)
|
||||
assert!(!repo.is_deleted("nope").await.unwrap(), "不存在任务 is_deleted 应为 false");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn blocked_round_trip_does_not_bump() {
|
||||
let repo = setup().await;
|
||||
|
||||
Reference in New Issue
Block a user