修复: status→enum + type alias(SMELL-P1-6)
This commit is contained in:
@@ -17,7 +17,7 @@ use std::sync::Arc;
|
||||
use df_storage::crud::{IdeaRepo, ProjectRepo, TaskRepo};
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::{IdeaRecord, ProjectRecord, TaskRecord};
|
||||
use df_types::types::new_id;
|
||||
use df_types::types::{IdeaStatus, ProjectStatus, TaskStatus, new_id};
|
||||
use futures::future::BoxFuture;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
@@ -235,10 +235,11 @@ fn create_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
Err(r) => return Box::pin(std::future::ready(r)),
|
||||
};
|
||||
let description = arg_str_or(&args, "description", "");
|
||||
let status = arg_str_or(&args, "status", "active");
|
||||
let status = arg_str_or(&args, "status", "planning");
|
||||
medium_audit("create_project", &name);
|
||||
Box::pin(async move {
|
||||
let now = now_millis();
|
||||
let status = ProjectStatus::from_db_str(&status).unwrap_or_default();
|
||||
let rec = ProjectRecord {
|
||||
id: new_id(),
|
||||
name,
|
||||
@@ -269,7 +270,7 @@ fn update_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
};
|
||||
let name = arg_str_or(&args, "name", "");
|
||||
let description = arg_str_or(&args, "description", "");
|
||||
let status = arg_str_or(&args, "status", "active");
|
||||
let status = arg_str_or(&args, "status", "planning");
|
||||
medium_audit("update_project", &id);
|
||||
Box::pin(async move {
|
||||
let repo = ProjectRepo::new(&db);
|
||||
@@ -280,6 +281,7 @@ fn update_project(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult>
|
||||
Err(e) => return err_str(e),
|
||||
};
|
||||
let now = now_millis();
|
||||
let status = ProjectStatus::from_db_str(&status).unwrap_or_default();
|
||||
let rec = ProjectRecord {
|
||||
id: id.clone(),
|
||||
name,
|
||||
@@ -355,7 +357,7 @@ fn list_tasks(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
list.retain(|t| t.project_id == *pid);
|
||||
}
|
||||
if let Some(st) = &status_filter {
|
||||
list.retain(|t| t.status == *st);
|
||||
list.retain(|t| t.status.as_str() == st.as_str());
|
||||
}
|
||||
json_ok(json!({ "tasks": list, "count": list.len() }))
|
||||
}
|
||||
@@ -384,7 +386,7 @@ fn create_task(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
project_id,
|
||||
title,
|
||||
description,
|
||||
status: "todo".to_owned(),
|
||||
status: TaskStatus::Todo,
|
||||
priority,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
@@ -529,7 +531,7 @@ fn create_idea(ctx: &Ctx, args: Value) -> BoxFuture<'static, CallToolResult> {
|
||||
id: new_id(),
|
||||
title,
|
||||
description,
|
||||
status: "draft".to_owned(),
|
||||
status: IdeaStatus::Draft,
|
||||
priority,
|
||||
score: None,
|
||||
tags: None,
|
||||
|
||||
@@ -273,6 +273,7 @@ mod tests {
|
||||
use df_storage::crud::{ProjectRepo, TaskRepo};
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::{ProjectRecord, TaskRecord};
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
use serde_json::json;
|
||||
|
||||
// ============================================================
|
||||
@@ -287,7 +288,7 @@ mod tests {
|
||||
id: "p1".to_string(),
|
||||
name: "proj".to_string(),
|
||||
description: "".to_string(),
|
||||
status: "planning".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
@@ -302,7 +303,7 @@ mod tests {
|
||||
project_id: "p1".to_string(),
|
||||
title: "t1".to_string(),
|
||||
description: "实现登录接口".to_string(),
|
||||
status: "testing".to_string(),
|
||||
status: TaskStatus::Testing,
|
||||
priority: 2,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
|
||||
@@ -174,14 +174,15 @@ mod tests {
|
||||
use super::*;
|
||||
use df_storage::crud::ProjectRepo;
|
||||
use df_storage::models::{ProjectRecord, TaskRecord};
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
|
||||
fn rec(id: &str, status: &str) -> TaskRecord {
|
||||
fn rec(id: &str, status: TaskStatus) -> TaskRecord {
|
||||
TaskRecord {
|
||||
id: id.to_string(),
|
||||
project_id: "p1".to_string(),
|
||||
title: format!("t-{id}"),
|
||||
description: "".to_string(),
|
||||
status: status.to_string(),
|
||||
status,
|
||||
priority: 2,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
@@ -206,7 +207,7 @@ mod tests {
|
||||
id: "p1".to_string(),
|
||||
name: "proj".to_string(),
|
||||
description: "".to_string(),
|
||||
status: "planning".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
@@ -221,43 +222,43 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn forward_path_todo_to_done() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
// 主路径逐级推进
|
||||
let r = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap();
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.status.as_str(), "in_progress");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
let r = advance_task_atomic(&repo, "t1", "in_review").await.unwrap();
|
||||
assert_eq!(r.status, "in_review");
|
||||
assert_eq!(r.status.as_str(), "in_review");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
let r = advance_task_atomic(&repo, "t1", "testing").await.unwrap();
|
||||
assert_eq!(r.status, "testing");
|
||||
assert_eq!(r.status.as_str(), "testing");
|
||||
let r = advance_task_atomic(&repo, "t1", "done").await.unwrap();
|
||||
assert_eq!(r.status, "done");
|
||||
assert_eq!(r.status.as_str(), "done");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regression_in_review_to_in_progress_bumps_rounds() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "in_review")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::InReview)).await.unwrap();
|
||||
let r = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap();
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.status.as_str(), "in_progress");
|
||||
assert_eq!(r.review_rounds, 1, "退回应 +1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn regression_testing_to_in_review_bumps_rounds() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "testing")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Testing)).await.unwrap();
|
||||
let r = advance_task_atomic(&repo, "t1", "in_review").await.unwrap();
|
||||
assert_eq!(r.status, "in_review");
|
||||
assert_eq!(r.status.as_str(), "in_review");
|
||||
assert_eq!(r.review_rounds, 1, "退回应 +1");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn multiple_regressions_accumulate() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "in_review")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::InReview)).await.unwrap();
|
||||
// in_review → in_progress (+1) → in_review (前向,不动) → in_progress (+1=2)
|
||||
advance_task_atomic(&repo, "t1", "in_progress").await.unwrap();
|
||||
advance_task_atomic(&repo, "t1", "in_review").await.unwrap();
|
||||
@@ -269,7 +270,7 @@ mod tests {
|
||||
async fn illegal_skip_rejected() {
|
||||
// CR-01-D: 非法转换(跳态)归 InvalidState,且消息含 from→to 上下文与「非法状态转换」。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "done").await.unwrap_err();
|
||||
match err {
|
||||
df_types::error::Error::InvalidState { current, expected } => {
|
||||
@@ -285,7 +286,7 @@ mod tests {
|
||||
async fn terminal_done_no_successor() {
|
||||
// CR-01-D: 终态无后继也是非法转换路径,归 InvalidState(同 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "done")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Done)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "todo").await.unwrap_err();
|
||||
match err {
|
||||
df_types::error::Error::InvalidState { current, .. } => {
|
||||
@@ -301,7 +302,7 @@ mod tests {
|
||||
// 同态属空操作,归 Validation「相同状态,无需推进」;
|
||||
// 非法转换归 InvalidState「非法状态转换 X→Y」(见 illegal_skip_rejected)。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "in_progress")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::InProgress)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap_err();
|
||||
match err {
|
||||
df_types::error::Error::Validation(msg) => {
|
||||
@@ -314,7 +315,7 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn invalid_target_rejected() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
let err = advance_task_atomic(&repo, "t1", "merged").await.unwrap_err();
|
||||
assert!(matches!(err, df_types::error::Error::Validation(_)));
|
||||
}
|
||||
@@ -332,7 +333,7 @@ mod tests {
|
||||
// 注:这并非 CAS 并发失败场景(真 CAS 失败由 cas_returns_none_when_status_mismatch 覆盖),
|
||||
// 而是验证读后改路径在 from=当前库内 status 时正常推进。
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
// 另一路推进把 status 改成 in_progress(模拟并发推进,走 CAS 合法路径;
|
||||
// F-03 收口后 status 不在 update_field 白名单,模拟并发改态须走 advance_status_atomic)
|
||||
repo.advance_status_atomic("t1", "todo", "in_progress", false)
|
||||
@@ -340,13 +341,13 @@ mod tests {
|
||||
.unwrap();
|
||||
// 读出来是 in_progress,推进到 in_review 合法 → 正常成功
|
||||
let r = advance_task_atomic(&repo, "t1", "in_review").await.unwrap();
|
||||
assert_eq!(r.status, "in_review");
|
||||
assert_eq!(r.status.as_str(), "in_review");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cas_returns_none_when_status_mismatch() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "todo")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::Todo)).await.unwrap();
|
||||
// 直接调底层:expected 传错(模拟读到 todo 但实际已被改成 in_progress)
|
||||
let r = repo
|
||||
.advance_status_atomic("t1", "todo", "in_review", false)
|
||||
@@ -365,12 +366,12 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn blocked_round_trip_does_not_bump() {
|
||||
let repo = setup().await;
|
||||
repo.insert(rec("t1", "in_progress")).await.unwrap();
|
||||
repo.insert(rec("t1", TaskStatus::InProgress)).await.unwrap();
|
||||
let r = advance_task_atomic(&repo, "t1", "blocked").await.unwrap();
|
||||
assert_eq!(r.status, "blocked");
|
||||
assert_eq!(r.status.as_str(), "blocked");
|
||||
assert_eq!(r.review_rounds, 0, "进 blocked 不累加");
|
||||
let r = advance_task_atomic(&repo, "t1", "in_progress").await.unwrap();
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.status.as_str(), "in_progress");
|
||||
assert_eq!(r.review_rounds, 0, "解除 blocked 不累加");
|
||||
}
|
||||
|
||||
@@ -460,24 +461,24 @@ mod tests {
|
||||
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();
|
||||
repo.insert(rec("c1", TaskStatus::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.status.as_str(), "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();
|
||||
repo.insert(rec("c2", TaskStatus::InReview)).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.status.as_str(), "testing");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
|
||||
// done: testing → done(②-3 done 模板最终核对通过)
|
||||
repo.insert(rec("c3", "testing")).await.unwrap();
|
||||
repo.insert(rec("c3", TaskStatus::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.status.as_str(), "done");
|
||||
assert_eq!(r.review_rounds, 0);
|
||||
}
|
||||
|
||||
@@ -487,32 +488,32 @@ mod tests {
|
||||
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();
|
||||
repo.insert(rec("f1", TaskStatus::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.status.as_str(), "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();
|
||||
repo.insert(rec("f2", TaskStatus::InReview)).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.status.as_str(), "in_progress");
|
||||
assert_eq!(r.review_rounds, 1);
|
||||
|
||||
// in_progress 模板失败:regression_target("in_progress")=None(CR-13-O1-b),
|
||||
// 回调返回 None → 跳过推进,任务保留 in_progress 等人介入。
|
||||
// 验证:callback 返回 None,不调 advance_task_atomic。
|
||||
repo.insert(rec("f3", "in_progress")).await.unwrap();
|
||||
repo.insert(rec("f3", TaskStatus::InProgress)).await.unwrap();
|
||||
let to = callback_advance_target("failed", "in_progress");
|
||||
assert_eq!(to, None, "in_progress 失败应跳过推进(None),实际: {to:?}");
|
||||
// 任务状态未被改动(仍是 in_progress)
|
||||
let still = repo.get_by_id("f3").await.unwrap().unwrap();
|
||||
assert_eq!(still.status, "in_progress", "None 退回不应改动 status");
|
||||
assert_eq!(still.status.as_str(), "in_progress", "None 退回不应改动 status");
|
||||
}
|
||||
|
||||
/// ②-4 回调 failed+done 无退回映射验证:callback_advance_target 返回 None。
|
||||
|
||||
@@ -9,6 +9,7 @@ use df_types::error::Result;
|
||||
|
||||
use crate::db::Database;
|
||||
use crate::models::{IdeaRecord, KnowledgeEventRecord, KnowledgeRecord};
|
||||
use df_types::types::IdeaStatus;
|
||||
|
||||
use super::impl_repo;
|
||||
use super::{now_millis_str, storage_err, validate_column_name};
|
||||
@@ -93,7 +94,10 @@ fn idea_from_row(row: &Row<'_>) -> std::result::Result<IdeaRecord, rusqlite::Err
|
||||
id: row.get("id")?,
|
||||
title: row.get("title")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
status: {
|
||||
let s: String = row.get("status")?;
|
||||
IdeaStatus::from_db_str(&s).unwrap_or_default()
|
||||
},
|
||||
priority: row.get("priority")?,
|
||||
score: row.get("score")?,
|
||||
tags: row.get("tags")?,
|
||||
@@ -200,7 +204,7 @@ impl_repo!(
|
||||
"INSERT INTO ideas (id, title, description, status, priority, score, tags, source, promoted_to, ai_analysis, scores, related_ids, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14)",
|
||||
params![
|
||||
rec.id, rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.id, rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.score, rec.tags, rec.source, rec.promoted_to, rec.ai_analysis,
|
||||
rec.scores, rec.related_ids, rec.created_at, rec.updated_at
|
||||
],
|
||||
@@ -210,7 +214,7 @@ impl_repo!(
|
||||
conn.execute(
|
||||
"UPDATE ideas SET title = ?1, description = ?2, status = ?3, priority = ?4, score = ?5, tags = ?6, source = ?7, promoted_to = ?8, ai_analysis = ?9, scores = ?10, related_ids = ?11, updated_at = ?12 WHERE id = ?13",
|
||||
params![
|
||||
rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.score, rec.tags, rec.source, rec.promoted_to, rec.ai_analysis,
|
||||
rec.scores, rec.related_ids, rec.updated_at, rec.id
|
||||
],
|
||||
@@ -1403,7 +1407,7 @@ mod tests {
|
||||
id: id.to_string(),
|
||||
title: title.to_string(),
|
||||
description: String::new(),
|
||||
status: "draft".to_string(),
|
||||
status: IdeaStatus::Draft,
|
||||
priority: 1,
|
||||
score: None,
|
||||
tags: None,
|
||||
|
||||
@@ -219,6 +219,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::crud::ProjectRepo;
|
||||
use crate::models::ProjectRecord;
|
||||
use df_types::types::ProjectStatus;
|
||||
|
||||
/// 构造内存 DB + 占位 project(project_events.project_id FK 要求 projects 存在)。
|
||||
async fn setup() -> (crate::db::Database, ProjectEventRepo) {
|
||||
@@ -232,7 +233,7 @@ mod tests {
|
||||
id: "proj-1".to_string(),
|
||||
name: "proj-1".to_string(),
|
||||
description: String::new(),
|
||||
status: "active".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
|
||||
@@ -6,6 +6,7 @@ use rusqlite::{params, Connection, OptionalExtension, Row};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_types::error::Result;
|
||||
use df_types::types::ProjectStatus;
|
||||
|
||||
use crate::db::Database;
|
||||
use crate::models::{
|
||||
@@ -80,7 +81,10 @@ fn project_from_row(row: &Row<'_>) -> std::result::Result<ProjectRecord, rusqlit
|
||||
id: row.get("id")?,
|
||||
name: row.get("name")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
status: {
|
||||
let s: String = row.get("status")?;
|
||||
ProjectStatus::from_db_str(&s).unwrap_or_default()
|
||||
},
|
||||
idea_id: row.get("idea_id")?,
|
||||
path: row.get("path")?,
|
||||
stack: row.get("stack")?,
|
||||
@@ -162,7 +166,7 @@ impl_repo!(
|
||||
"INSERT INTO projects (id, name, description, status, idea_id, path, stack, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)",
|
||||
params![
|
||||
rec.id, rec.name, rec.description, rec.status, rec.idea_id,
|
||||
rec.id, rec.name, rec.description, rec.status.as_str(), rec.idea_id,
|
||||
rec.path, rec.stack, rec.created_at, rec.updated_at
|
||||
],
|
||||
)
|
||||
@@ -171,7 +175,7 @@ impl_repo!(
|
||||
conn.execute(
|
||||
"UPDATE projects SET name = ?1, description = ?2, status = ?3, idea_id = ?4, path = ?5, stack = ?6, updated_at = ?7 WHERE id = ?8",
|
||||
params![
|
||||
rec.name, rec.description, rec.status, rec.idea_id,
|
||||
rec.name, rec.description, rec.status.as_str(), rec.idea_id,
|
||||
rec.path, rec.stack, rec.updated_at, rec.id
|
||||
],
|
||||
)
|
||||
|
||||
@@ -295,6 +295,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::crud::ProjectRepo;
|
||||
use crate::models::ProjectRecord;
|
||||
use df_types::types::ProjectStatus;
|
||||
|
||||
/// 构造内存 DB + 占位 project(project_services.project_id FK 要求 projects 存在)。
|
||||
async fn setup() -> (crate::db::Database, ProjectServiceRepo) {
|
||||
@@ -308,7 +309,7 @@ mod tests {
|
||||
id: "proj-1".to_string(),
|
||||
name: "proj-1".to_string(),
|
||||
description: String::new(),
|
||||
status: "active".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
|
||||
@@ -328,6 +328,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::crud::TaskRepo;
|
||||
use crate::models::TaskRecord;
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
|
||||
/// 构造一条 TaskRecord fixture(18 字段全填,queue 默认 todo)。
|
||||
fn trec(id: &str, project_id: &str) -> TaskRecord {
|
||||
@@ -336,7 +337,7 @@ mod tests {
|
||||
project_id: project_id.to_string(),
|
||||
title: format!("task-{id}"),
|
||||
description: String::new(),
|
||||
status: "todo".to_string(),
|
||||
status: TaskStatus::Todo,
|
||||
priority: 1,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
@@ -369,7 +370,7 @@ mod tests {
|
||||
id: "proj-1".to_string(),
|
||||
name: "proj-1".to_string(),
|
||||
description: String::new(),
|
||||
status: "active".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
|
||||
@@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use df_types::error::Result;
|
||||
use df_types::types::TaskStatus;
|
||||
|
||||
use crate::db::Database;
|
||||
use crate::models::TaskRecord;
|
||||
@@ -24,7 +25,10 @@ fn task_from_row(row: &Row<'_>) -> std::result::Result<TaskRecord, rusqlite::Err
|
||||
project_id: row.get("project_id")?,
|
||||
title: row.get("title")?,
|
||||
description: row.get("description")?,
|
||||
status: row.get("status")?,
|
||||
status: {
|
||||
let s: String = row.get("status")?;
|
||||
TaskStatus::from_db_str(&s).unwrap_or_default()
|
||||
},
|
||||
priority: row.get("priority")?,
|
||||
branch_name: row.get("branch_name")?,
|
||||
assignee: row.get("assignee")?,
|
||||
@@ -122,7 +126,7 @@ impl_repo!(
|
||||
"INSERT INTO tasks (id, project_id, title, description, status, priority, branch_name, assignee, workflow_def_id, base_branch, review_rounds, output_json, idea_id, queue, parent_id, content_json, created_at, updated_at)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18)",
|
||||
params![
|
||||
rec.id, rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.id, rec.project_id, rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||
rec.review_rounds, rec.output_json, rec.idea_id,
|
||||
rec.queue, rec.parent_id, rec.content_json,
|
||||
@@ -134,7 +138,7 @@ impl_repo!(
|
||||
conn.execute(
|
||||
"UPDATE tasks SET project_id = ?1, title = ?2, description = ?3, status = ?4, priority = ?5, branch_name = ?6, assignee = ?7, workflow_def_id = ?8, base_branch = ?9, review_rounds = ?10, output_json = ?11, idea_id = ?12, queue = ?13, parent_id = ?14, content_json = ?15, updated_at = ?16 WHERE id = ?17",
|
||||
params![
|
||||
rec.project_id, rec.title, rec.description, rec.status, rec.priority,
|
||||
rec.project_id, rec.title, rec.description, rec.status.as_str(), rec.priority,
|
||||
rec.branch_name, rec.assignee, rec.workflow_def_id, rec.base_branch,
|
||||
rec.review_rounds, rec.output_json, rec.idea_id,
|
||||
rec.queue, rec.parent_id, rec.content_json,
|
||||
@@ -566,20 +570,21 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::crud::ProjectRepo;
|
||||
use crate::models::ProjectRecord;
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
|
||||
/// 构造一条 TaskRecord fixture(queue/parent_id/status 可定制,V29 新维度 + 聚合测试用 status)。
|
||||
fn trec(id: &str, queue: &str, parent_id: Option<&str>) -> TaskRecord {
|
||||
trec_full(id, queue, parent_id, "todo")
|
||||
trec_full(id, queue, parent_id, TaskStatus::Todo)
|
||||
}
|
||||
|
||||
/// 全参 fixture(聚合测试需自定义 status 时用)。
|
||||
fn trec_full(id: &str, queue: &str, parent_id: Option<&str>, status: &str) -> TaskRecord {
|
||||
fn trec_full(id: &str, queue: &str, parent_id: Option<&str>, status: TaskStatus) -> TaskRecord {
|
||||
TaskRecord {
|
||||
id: id.to_string(),
|
||||
project_id: "proj-1".to_string(),
|
||||
title: format!("task-{id}"),
|
||||
description: String::new(),
|
||||
status: status.to_string(),
|
||||
status,
|
||||
priority: 1,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
@@ -605,7 +610,7 @@ mod tests {
|
||||
id: "proj-1".to_string(),
|
||||
name: "proj-1".to_string(),
|
||||
description: String::new(),
|
||||
status: "active".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
@@ -778,13 +783,13 @@ mod tests {
|
||||
async fn set_status_for_aggregation_writes_status() {
|
||||
let repo = setup().await;
|
||||
// 父任务初始 todo(queue=todo, parent_id=None 容器模型)
|
||||
repo.insert(trec_full("parent", "todo", None, "todo"))
|
||||
repo.insert(trec_full("parent", "todo", None, TaskStatus::Todo))
|
||||
.await
|
||||
.unwrap();
|
||||
let ok = repo.set_status_for_aggregation("parent", "in_progress").await.unwrap();
|
||||
let ok = repo.set_status_for_aggregation("parent", "in_progress");
|
||||
assert!(ok, "应命中写入");
|
||||
let after = repo.get_by_id("parent").await.unwrap().unwrap();
|
||||
assert_eq!(after.status, "in_progress", "status 应被聚合写入更新");
|
||||
assert_eq!(after.status.as_str(), "in_progress", "status 应被聚合写入更新");
|
||||
assert_eq!(after.review_rounds, 0, "父聚合写入不动 review_rounds");
|
||||
}
|
||||
|
||||
@@ -792,7 +797,7 @@ mod tests {
|
||||
async fn set_status_for_aggregation_skips_soft_deleted() {
|
||||
// 软删任务(回收站)不进聚合写入(WHERE deleted_at IS NULL),返回 false
|
||||
let repo = setup().await;
|
||||
repo.insert(trec_full("parent", "todo", None, "todo"))
|
||||
repo.insert(trec_full("parent", "todo", None, TaskStatus::Todo))
|
||||
.await
|
||||
.unwrap();
|
||||
repo.soft_delete("parent").await.unwrap();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//! 数据模型定义 — 与数据库表对应的 Rust 结构体
|
||||
|
||||
use df_ai_core::model::{deserialize_model_configs, ModelConfig};
|
||||
use df_types::types::{IdeaStatus, LinkType, NodeType, ProjectStatus, TaskStatus};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================
|
||||
@@ -13,7 +14,7 @@ pub struct IdeaRecord {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub status: IdeaStatus,
|
||||
pub priority: i32,
|
||||
pub score: Option<f64>,
|
||||
pub tags: Option<String>, // JSON 数组
|
||||
@@ -53,7 +54,7 @@ pub struct ProjectRecord {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub status: ProjectStatus,
|
||||
pub idea_id: Option<String>,
|
||||
/// 绑定的本地代码目录(绝对路径,可空=未绑定,第二步导入历史项目时复用)
|
||||
pub path: Option<String>,
|
||||
@@ -74,7 +75,7 @@ pub struct TaskRecord {
|
||||
pub project_id: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
pub status: String,
|
||||
pub status: TaskStatus,
|
||||
pub priority: i32,
|
||||
pub branch_name: Option<String>,
|
||||
pub assignee: Option<String>,
|
||||
@@ -134,14 +135,14 @@ pub struct TaskRecord {
|
||||
/// - `blocks`:source 阻塞 target(source 不完成则 target 无法推进)→ 依赖的反向声明
|
||||
/// - `relates_to`:弱关联,无执行约束 → 上下文提示
|
||||
/// - `remark`:可选备注。
|
||||
/// - 循环依赖(A→B→A)在 `TaskLinkRepo::create_link` 应用层 BFS 检测拒绝(非 DB 约束,
|
||||
/// 对标设计 D8:task_links 数据量小,检测成本低)。跨项目依赖允许。
|
||||
/// 循环依赖(A→B→A)在 `TaskLinkRepo::create_link` 应用层 BFS 检测拒绝(非 DB 约束,
|
||||
/// 对标设计 D8:task_links 数据量小,检测成本高)。跨项目依赖允许。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TaskLinkRecord {
|
||||
pub id: String,
|
||||
pub source_id: String,
|
||||
pub target_id: String,
|
||||
pub link_type: String,
|
||||
pub link_type: LinkType,
|
||||
pub remark: Option<String>,
|
||||
pub created_at: String,
|
||||
}
|
||||
@@ -265,7 +266,7 @@ pub struct NodeExecutionRecord {
|
||||
pub id: String,
|
||||
pub workflow_id: String,
|
||||
pub node_id: String,
|
||||
pub node_type: String,
|
||||
pub node_type: NodeType,
|
||||
pub status: String,
|
||||
pub input_json: Option<String>,
|
||||
pub output_json: Option<String>,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
use df_storage::crud::{BranchRepo, ProjectRepo, ReleaseRepo, TaskRepo};
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::{BranchRecord, ProjectRecord, ReleaseRecord, TaskRecord};
|
||||
use df_types::types::{ProjectStatus, TaskStatus};
|
||||
|
||||
// ---------- fixtures ----------
|
||||
|
||||
@@ -20,7 +21,7 @@ fn project(id: &str) -> ProjectRecord {
|
||||
id: id.to_string(),
|
||||
name: format!("proj-{id}"),
|
||||
description: "desc".to_string(),
|
||||
status: "active".to_string(),
|
||||
status: ProjectStatus::Planning,
|
||||
idea_id: None,
|
||||
path: None,
|
||||
stack: None,
|
||||
@@ -35,7 +36,7 @@ fn task(id: &str, project_id: &str) -> TaskRecord {
|
||||
project_id: project_id.to_string(),
|
||||
title: format!("task-{id}"),
|
||||
description: "desc".to_string(),
|
||||
status: "todo".to_string(),
|
||||
status: TaskStatus::Todo,
|
||||
priority: 1,
|
||||
branch_name: None,
|
||||
assignee: None,
|
||||
@@ -268,7 +269,7 @@ async fn update_field_rejects_tasks_status() {
|
||||
|
||||
// 对照:status 未被改写,仍为初始值(task fixture 的初始 status)
|
||||
let rec = tasks.get_by_id("t1").await.unwrap().unwrap();
|
||||
assert_ne!(rec.status, "done", "白名单拒绝后 status 不应被改写");
|
||||
assert_ne!(rec.status.as_str(), "done", "白名单拒绝后 status 不应被改写");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -26,6 +26,12 @@ pub type PluginId = String;
|
||||
pub type ExecutionId = String;
|
||||
/// 决策 ID
|
||||
pub type DecisionId = String;
|
||||
/// 节点类型(如 ai / human / ai_self_review)
|
||||
pub type NodeType = String;
|
||||
/// 任务链接类型(如 depends_on / blocks / relates_to)
|
||||
pub type LinkType = String;
|
||||
/// 工具调用类型(如 function)
|
||||
pub type ToolCallType = String;
|
||||
|
||||
// ============================================================
|
||||
// 时间工具
|
||||
|
||||
Reference in New Issue
Block a user