Files
DCTS/crates/server/tests/wf_migration_isolation.rs
T
fmq d16b3d3cdc feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通
- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
    版本化迁移运行器(M1~M13)
  - 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
    Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
    新增 tlusty_status/synspec_status 半失败阶段守卫
  - 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
    校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
  - 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
    透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
  - 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
  - dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
  - docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
2026-08-06 20:51:21 +08:00

200 lines
8.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 验证「历史种子导入的工作流名」与「正式工作流名」的隔离关系。
//!
//! 用户意图:import_results 把旧 Python 计算结果导入,标记为已完成,避免重算。
//! 关键问题:导入到工作流 A,之后正式启动工作流 B(同名/异名),B 能否看到 A 标记的 converged
use common::config::GridConfig;
use common::models::GridPointParams;
use mq::sqlite_queue::SqliteTaskQueue;
use server::{db::Database, scheduler::GridScheduler};
use std::sync::Arc;
fn make_params() -> GridPointParams {
use common::models::GridAxisValue;
GridPointParams {
teff: GridAxisValue::from_value(20000.0),
logg: GridAxisValue::from_value(5.0),
loghe: GridAxisValue::from_value(-2.0),
logc: GridAxisValue::from_value(-4.0),
logn: GridAxisValue::from_value(-4.0),
logo: GridAxisValue::from_value(-4.0),
}
}
/// 构造只含一个网格点(t20000_g5.0_he-2_c-4_n-4_o-4)的 config。
fn make_grid_cfg() -> GridConfig {
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
GridConfig::from_yaml_str(yaml).unwrap()
}
async fn setup() -> (Database, Arc<GridScheduler>) {
let tmp = tempfile::tempdir().unwrap();
let db = Database::new(&tmp.path().join("db.db").to_string_lossy())
.await
.unwrap();
let queue = Arc::new(
SqliteTaskQueue::new(&tmp.path().join("q.db").to_string_lossy())
.await
.unwrap(),
);
let sched = Arc::new(GridScheduler::new(db.clone(), queue));
(db, sched)
}
/// 场景 1(正确用法):导入到工作流 "sdB_cno",再用同名 config initialize_grid。
/// 期望:initialize_grid 的 ON CONFLICT(workflow_name, name) DO NOTHING 保留 converged 状态。
#[tokio::test]
async fn test_same_workflow_name_preserves_converged() {
let (db, sched) = setup().await;
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seedupsert + mark_imported,工作流名 = sdB_cno
db.upsert_grid_point_named(name, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(name, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 之后正式启动同名工作流:initialize_grid(sdB_cno)
sched
.initialize_grid(&make_grid_cfg(), "sdB_cno")
.await
.unwrap();
let st = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
assert_eq!(
st.unwrap().0,
"completed",
"同名工作流:导入的 converged 应被保留,避免重算"
);
println!("✓ 场景1(同名):status=converged,旧结果被保留,不会重算");
}
/// 场景 2(错误用法):导入到工作流 "imported",之后正式启动 "sdB_cno"。
/// 期望:sdB_cno 分区下是新插入的 pending 行,看不到 imported 分区的 converged。
#[tokio::test]
async fn test_different_workflow_name_causes_recompute() {
let (db, sched) = setup().await;
let name = "t20000_g5.0_he-2_c-4_n-4_o-4";
let p = make_params();
// 模拟 import_seed:导入到 "imported" 工作流
db.upsert_grid_point_named(name, &p, 0, "imported")
.await
.unwrap();
db.mark_grid_point_imported(name, "imported", None, "cold_run")
.await
.unwrap();
// 之后正式启动 "sdB_cno" 工作流
sched
.initialize_grid(&make_grid_cfg(), "sdB_cno")
.await
.unwrap();
// imported 分区:converged(种子库有,但不会被 sdB_cno 调度看到)
let st_imp = db.get_grid_point_status(name, "imported").await.unwrap();
assert_eq!(st_imp.unwrap().0, "completed");
// sdB_cno 分区:pending(重新算!看不到 imported 的 converged
let st_real = db.get_grid_point_status(name, "sdB_cno").await.unwrap();
assert_eq!(
st_real.unwrap().0,
"pending",
"异名工作流:sdB_cno 看不到 imported 的 converged,会重算"
);
println!("✓ 场景2(异名):sdB_cno 分区 status=pending,会重复计算 —— 验证了工作流名必须匹配");
}
/// 场景 3(真实意图验证):旧网格有部分点已算完(导入为 converged),
/// 新网格比旧网格多了若干点。用同名工作流启动后:
/// - 旧点保持 converged(不重算)
/// - 新点是 pending(会被调度计算)
/// 这正是「同步旧结果避免重算」的核心语义。
#[tokio::test]
async fn test_mixed_grid_import_then_init_avoids_recompute() {
let (db, sched) = setup().await;
let p_old = make_params(); // t20000_g5.0_...
// 模拟 import_seed:旧网格里这个点已收敛,导入到 sdB_cno
db.upsert_grid_point_named("t20000_g5.0_he-2_c-4_n-4_o-4", &p_old, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno", None, "cold_run")
.await
.unwrap();
// 正式启动 sdB_cno,config 比旧网格多了一个新点(t25000)
let yaml = "grid:\n teff: [20000, 25000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
// 旧点:converged(不重算)
let st_old = db
.get_grid_point_status("t20000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
.await
.unwrap();
assert_eq!(
st_old.unwrap().0,
"completed",
"旧点应保持 converged 不重算"
);
// 新点:pending(会被调度)
let st_new = db
.get_grid_point_status("t25000_g5.0_he-2_c-4_n-4_o-4", "sdB_cno")
.await
.unwrap();
assert_eq!(st_new.unwrap().0, "pending", "新点应为 pending 等待计算");
println!("✓ 场景3(混合网格):旧点converged保留 + 新点pending待算 —— 完全符合避免重算的意图");
}
/// 场景 4(精度差异命门):旧 conv.json name=g5(无小数),但配置 logg=5.0 → model_name()=g5.0。
/// 导入工具必须把 name 重写为 g5.0 入库,否则 initialize_grid 插入的 g5.0 行与导入的 g5 行
/// 复合唯一键不匹配,导入的 converged 被孤立、g5.0 被重算。
/// 本测试直接模拟「入库的 grid_points.name = g5.0」(即工具重写后的状态),
/// 验证 initialize_grid(sdB_cno) 后该行保持 converged(不重算)。
#[tokio::test]
async fn test_precision_diff_import_then_init_preserves_converged() {
let (db, sched) = setup().await;
// 配置权威名(logg=5.0 → g5.0,保留小数)
let canonical = "t20000_g5.0_he-2_c-4_n-4_o-4";
use common::models::GridAxisValue;
let p = GridPointParams {
teff: GridAxisValue::from_value(20000.0),
logg: GridAxisValue::from_value(5.0),
loghe: GridAxisValue::from_value(-2.0),
logc: GridAxisValue::from_value(-4.0),
logn: GridAxisValue::from_value(-4.0),
logo: GridAxisValue::from_value(-4.0),
};
// 模拟 import_results 重写 name 后入库:grid_points.name = canonical(g5.0)
db.upsert_grid_point_named(canonical, &p, 0, "sdB_cno")
.await
.unwrap();
db.mark_grid_point_imported(canonical, "sdB_cno", None, "cold_run")
.await
.unwrap();
// 启动同名工作流:initialize_grid 用配置 model_name()(=g5.0) 插入
let yaml = "grid:\n teff: [20000]\n logg: [5.0]\n loghe: [-2]\n logc: [-4]\n logn: [-4]\n logo: [-4]\n";
let cfg = GridConfig::from_yaml_str(yaml).unwrap();
sched.initialize_grid(&cfg, "sdB_cno").await.unwrap();
// 关键断言:name=g5.0 的行保持 convergedON CONFLICT DO NOTHING 命中)
let st = db
.get_grid_point_status(canonical, "sdB_cno")
.await
.unwrap();
assert_eq!(
st.unwrap().0,
"completed",
"精度一致(g5.0=g5.0)时导入的 converged 必须保留,不重算"
);
println!("✓ 场景4(精度差异命门):g5.0 入库 + initialize_grid → converged 保留,避免重算");
}