修复: status→enum + type alias(SMELL-P1-6)

This commit is contained in:
2026-06-28 04:42:59 +08:00
parent 48c966f6f7
commit d1b9488853
21 changed files with 304 additions and 126 deletions

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
],
)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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();

View File

@@ -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>,

View File

@@ -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]