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
This commit is contained in:
fmq
2026-08-06 20:51:21 +08:00
parent cd370d88e7
commit d16b3d3cdc
61 changed files with 10268 additions and 5881 deletions
+57 -107
View File
@@ -266,6 +266,17 @@ async fn test_l2_node_token_issue_reissue_flow() {
);
// 旧 token 仍失效(已被覆盖)
assert!(db.find_node_by_token(&token).await.is_none());
// M2registration_secret 被首次 take 轮换后,旧 secret 无法取走 reissue 产生的新 token。
// 首次 take(上文)已把 secret 轮换为无人知晓的新值,旧 secret(注册时下发的)立即失效。
let stolen = db
.take_pending_node_token("node-l2-test", secret.as_deref())
.await
.unwrap();
assert!(
stolen.is_none(),
"被轮换的旧 registration_secret 不得取走 reissue 后的新 tokenM2 一次性凭据)"
);
}
/// 验证中间件对 node token 的端到端鉴权:
@@ -1323,9 +1334,9 @@ fn make_import_multipart(
body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
body.extend_from_slice(seed_bytes);
body.extend_from_slice(b"\r\n");
// 收敛途径字段(cold_run/seed_step):模拟 import_results 工具判定后透传的途径。
// 大气收敛途径字段(cold_run/seed_step):模拟 import_results 工具判定后透传的途径。
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
body.extend_from_slice(b"Content-Disposition: form-data; name=\"success_method\"\r\n");
body.extend_from_slice(b"Content-Disposition: form-data; name=\"tlusty_success_method\"\r\n");
body.extend_from_slice(b"Content-Type: text/plain\r\n\r\n");
body.extend_from_slice(success_method.as_bytes());
body.extend_from_slice(b"\r\n");
@@ -1433,7 +1444,7 @@ async fn test_import_seed_admin_endpoint() {
.await
.unwrap()
.expect("grid_points 应存在");
assert_eq!(gp.0, "converged", "导入的收敛点应为 converged 状态");
assert_eq!(gp.0, "completed", "导入的收敛点应为 converged 状态");
// 3. 幂等:重复导入同名点不应报错,状态仍 converged。
let body_bytes = make_import_multipart(
@@ -1462,7 +1473,7 @@ async fn test_import_seed_admin_endpoint() {
.await
.unwrap()
.unwrap();
assert_eq!(gp.0, "converged");
assert_eq!(gp.0, "completed");
// 4. 未收敛点 → 200,但不写 .7、grid_points 维持 pending(未建 converged)。
let conv_fail = make_legacy_conv_json("t20000_g5.0_he-2_c-4_n-4_o-4_fail", false);
@@ -1570,8 +1581,8 @@ async fn test_import_seed_python_legacy_conv_json() {
.await
.unwrap()
.expect("grid_points 应存在");
assert_eq!(row.status, "converged");
assert_eq!(row.success_method.as_deref(), Some("cold_run"));
assert_eq!(row.status, "completed");
assert_eq!(row.tlusty_success_method.as_deref(), Some("cold_run"));
assert_eq!(
row.last_elapsed_sec,
Some(715.0),
@@ -1585,7 +1596,7 @@ async fn test_import_seed_python_legacy_conv_json() {
#[tokio::test]
async fn test_node_disable_enable_flow() {
use common::models::{GridAxisValue, GridPointParams, TaskSpec, TaskType};
use common::models::{GridAxisValue, GridPointParams, TaskSpec};
use uuid::Uuid;
let temp_dir = tempfile::tempdir().unwrap();
@@ -1668,7 +1679,6 @@ async fn test_node_disable_enable_flow() {
logn: GridAxisValue::from_value(-2.0),
logo: GridAxisValue::from_value(-2.0),
},
task_type: TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 60,
workflow_name: Some("wf_test".to_string()),
@@ -2120,13 +2130,13 @@ async fn test_admin_set_node_quota_and_heartbeat_sync() {
/// 测试专用:走真实写路径派发并回报一个网格点任务。
///
/// `insert_task` → `record_task_report` 会回填 `grid_points.success_method`、
/// `insert_task` → `record_task_report` 会回填 `grid_points.tlusty_success_method`、
/// `attempt_count` 与 `tasks.completed_at`,与生产链路一致(不绕过任何状态机逻辑)。
async fn dispatch_and_report(
db: &Database,
wf: &str,
p: &common::models::GridPointParams,
task_type: common::models::TaskType,
strategy: &str,
seed: Option<String>,
converged: bool,
) {
@@ -2135,11 +2145,15 @@ async fn dispatch_and_report(
task_id,
point_name: p.model_name(),
params: p.clone(),
task_type,
seed_point_name: seed,
timeout_sec: 7200,
workflow_name: Some(wf.to_string()),
wave: 0,
// Phase 6 起策略链首项即"当前策略"(归因/过滤全派生自它)。
tlusty_config: common::models::PhaseConfig {
strategies: vec![strategy.to_string()],
..common::models::PhaseConfig::default_tlusty()
},
..Default::default()
};
db.insert_task(&spec).await.unwrap();
@@ -2153,7 +2167,7 @@ async fn dispatch_and_report(
} else {
common::models::TaskStatus::Failed
},
converged,
result_valid: converged,
max_relc: if converged { Some(0.0005) } else { Some(9.5e5) },
atmosphere_has_nan: false,
elapsed_sec: 120.0,
@@ -2267,33 +2281,17 @@ async fn test_wf_stats_endpoint() {
.await
.unwrap();
dispatch_and_report(
&db,
"wf_stats",
&p_cold,
common::models::TaskType::ColdRun,
None,
true,
)
.await;
dispatch_and_report(&db, "wf_stats", &p_cold, "cold_run", None, true).await;
dispatch_and_report(
&db,
"wf_stats",
&p_seed,
common::models::TaskType::SeedStep,
"seed_step",
Some(p_cold.model_name()),
true,
)
.await;
dispatch_and_report(
&db,
"wf_stats",
&p_failed,
common::models::TaskType::ColdRun,
None,
false,
)
.await;
dispatch_and_report(&db, "wf_stats", &p_failed, "cold_run", None, false).await;
db.mark_grid_point_imported(&p_imported.model_name(), "wf_stats", None, "cold_run")
.await
.unwrap();
@@ -2347,7 +2345,7 @@ async fn test_wf_stats_endpoint() {
assert_eq!(data["pending"], 1, "pending 与 queued 必须分开计数");
assert_eq!(data["queued"], 1);
assert_eq!(data["running"], 1);
assert_eq!(data["converged"], 3);
assert_eq!(data["completed"], 3);
assert_eq!(data["failed"], 1);
assert_eq!(data["cold_run_converged"], 2);
assert_eq!(data["seed_step_converged"], 1);
@@ -2356,8 +2354,8 @@ async fn test_wf_stats_endpoint() {
assert_eq!(waves.len(), 3);
assert_eq!(waves[0]["wave"], 0);
assert_eq!(waves[0]["total"], 3);
assert_eq!(waves[0]["converged"], 0);
assert_eq!(waves[1]["converged"], 2);
assert_eq!(waves[0]["completed"], 0);
assert_eq!(waves[1]["completed"], 2);
assert_eq!(waves[2]["failed"], 1);
// 有已完成任务且有剩余点 → ETA 可估算。
// P3 后 avg 取精确 elapsed_sec(夹具每次回报 120s),无在线节点按串行兜底:
@@ -2436,48 +2434,24 @@ async fn seed_obs_fixture(db: &Database, db_path: &std::path::Path, wf: &str) ->
}
db.upsert_grid_point(&p_imported, 1, wf).await.unwrap();
dispatch_and_report(
db,
wf,
&p_cold,
common::models::TaskType::ColdRun,
None,
true,
)
.await;
dispatch_and_report(db, wf, &p_cold, "cold_run", None, true).await;
dispatch_and_report(
db,
wf,
&p_seed,
common::models::TaskType::SeedStep,
"seed_step",
Some(p_cold.model_name()),
true,
)
.await;
dispatch_and_report(
db,
wf,
&p_failed,
common::models::TaskType::ColdRun,
None,
false,
)
.await;
dispatch_and_report(db, wf, &p_failed, "cold_run", None, false).await;
// rescued:先冷启动失败,再种子步进救回(2 次尝试,最终 converged/seed_step
dispatch_and_report(db, wf, &p_rescued, "cold_run", None, false).await;
dispatch_and_report(
db,
wf,
&p_rescued,
common::models::TaskType::ColdRun,
None,
false,
)
.await;
dispatch_and_report(
db,
wf,
&p_rescued,
common::models::TaskType::SeedStep,
"seed_step",
Some(p_cold.model_name()),
true,
)
@@ -2490,7 +2464,7 @@ async fn seed_obs_fixture(db: &Database, db_path: &std::path::Path, wf: &str) ->
conn.execute(
"UPDATE tasks SET created_at = datetime('now','-60 seconds'),
completed_at = datetime('now','-60 seconds')
WHERE workflow_name = ?1 AND NOT (point_name = ?2 AND task_type = 'seed_step')",
WHERE workflow_name = ?1 AND NOT (point_name = ?2 AND json_extract(tlusty_strategies, '$[0]') = 'seed_step')",
rusqlite::params![wf, p_rescued.model_name()],
)
.unwrap();
@@ -2670,19 +2644,14 @@ async fn test_wf_points_endpoint() {
// ---- 10. 最近尝试 JOINrescued 双尝试取最新(seed_step 救回) ----
let (_, data) = get_points(&app, "/api/workflows/wf_pts/points").await;
let rescued = find_point(&data, &n.rescued);
assert_eq!(rescued["status"], "converged");
assert_eq!(rescued["success_method"], "seed_step");
assert_eq!(rescued["status"], "completed");
assert_eq!(rescued["tlusty_success_method"], "seed_step");
assert_eq!(rescued["attempt_count"], 2, "两次尝试都应计数");
assert_eq!(
rescued["last_task_type"], "seed_step",
"最近尝试应为种子步进"
);
assert_eq!(rescued["seed_point_name"], n.cold, "种子来源应为 cold 点");
assert_eq!(rescued["last_max_relc"], 0.0005);
assert_eq!(rescued["last_elapsed_sec"], 120.0, "真实墙钟耗时应落库");
// pending 点无任何尝试 → last_* 全 null
let pending = find_point(&data, &n.pending);
assert!(pending["last_task_type"].is_null());
assert!(pending["last_completed_at"].is_null());
assert!(pending["last_elapsed_sec"].is_null());
assert_eq!(pending["attempt_count"], 0);
@@ -2782,13 +2751,13 @@ async fn test_point_detail_endpoint() {
let (st, data) = get_detail(&app, &uri).await;
assert_eq!(st, StatusCode::OK);
assert_eq!(data["point"]["name"], n.cold);
assert_eq!(data["point"]["status"], "converged");
assert_eq!(data["point"]["success_method"], "cold_run");
assert_eq!(data["point"]["status"], "completed");
assert_eq!(data["point"]["tlusty_success_method"], "cold_run");
let attempts = data["attempts"].as_array().unwrap();
assert_eq!(attempts.len(), 1);
assert_eq!(attempts[0]["task_type"], "cold_run");
assert!(attempts[0]["seed_point_name"].is_null(), "冷启动无种子来源");
assert_eq!(attempts[0]["status"], "completed");
assert_eq!(data["conv"]["converged"], true, "conv.json 应被解析");
assert_eq!(data["conv"]["result_valid"], true, "conv.json 应被解析");
assert_eq!(data["conv"]["final_max_relc"], 0.000321);
// ---- 3. rescued 点:2 次尝试按时间升序(冷启失败 → 种子步进救回),conv 为 null ----
@@ -2798,10 +2767,9 @@ async fn test_point_detail_endpoint() {
assert_eq!(data["point"]["attempt_count"], 2);
let attempts = data["attempts"].as_array().unwrap();
assert_eq!(attempts.len(), 2);
assert_eq!(attempts[0]["task_type"], "cold_run", "首次应为冷启动");
assert!(attempts[0]["seed_point_name"].is_null(), "首次冷启动无种子");
assert_eq!(attempts[0]["status"], "failed");
assert_eq!(attempts[0]["elapsed_sec"], 120.0, "每次尝试耗时应落库");
assert_eq!(attempts[1]["task_type"], "seed_step", "第二次应为种子步进");
assert_eq!(attempts[1]["status"], "completed");
assert_eq!(
attempts[1]["seed_point_name"], n.cold,
@@ -2878,15 +2846,7 @@ async fn test_list_workflows_inline_stats() {
let p2 = wf_stats_test_params(25000.0, -4.0);
db.upsert_grid_point(&p1, 0, "wf_list_a").await.unwrap();
db.upsert_grid_point(&p2, 0, "wf_list_a").await.unwrap();
dispatch_and_report(
&db,
"wf_list_a",
&p1,
common::models::TaskType::ColdRun,
None,
true,
)
.await;
dispatch_and_report(&db, "wf_list_a", &p1, "cold_run", None, true).await;
// 无 token → 401
let res = app
@@ -2923,7 +2883,7 @@ async fn test_list_workflows_inline_stats() {
let wf_b = list.iter().find(|w| w["name"] == "wf_list_b").unwrap();
assert_eq!(wf_a["stats"]["total"], 2);
assert_eq!(wf_a["stats"]["converged"], 1);
assert_eq!(wf_a["stats"]["completed"], 1);
assert_eq!(wf_a["stats"]["cold_run_converged"], 1);
assert_eq!(wf_a["stats"]["failed"], 0);
assert_eq!(wf_a["stats"]["running"], 0);
@@ -2985,15 +2945,7 @@ async fn test_wf_progress_endpoint() {
db.record_progress_snapshot("wf_prog").await.unwrap(),
"首次记录应写入"
);
dispatch_and_report(
&db,
"wf_prog",
&p1,
common::models::TaskType::ColdRun,
None,
true,
)
.await;
dispatch_and_report(&db, "wf_prog", &p1, "cold_run", None, true).await;
assert!(
db.record_progress_snapshot("wf_prog").await.unwrap(),
"计数变化应写入"
@@ -3008,7 +2960,7 @@ async fn test_wf_progress_endpoint() {
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE workflow_progress_snapshots SET ts = datetime('now', '-2 hours') \
WHERE workflow_name = 'wf_prog' AND converged = 0",
WHERE workflow_name = 'wf_prog' AND completed = 0",
[],
)
.unwrap();
@@ -3057,7 +3009,7 @@ async fn test_wf_progress_endpoint() {
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute(
"UPDATE workflow_progress_snapshots SET ts = datetime('now', '-10 days') \
WHERE workflow_name = 'wf_prog' AND converged = 0",
WHERE workflow_name = 'wf_prog' AND completed = 0",
[],
)
.unwrap();
@@ -3171,7 +3123,6 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
task_id: uuid::Uuid::new_v4(),
point_name: name.clone(),
params: params.clone(),
task_type: common::models::TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 7200,
workflow_name: Some("wf_flip".to_string()),
@@ -3201,7 +3152,7 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
params: Some(params.clone()),
node_id: "node-flip".to_string(),
status: common::models::TaskStatus::Completed,
converged: true,
result_valid: true,
max_relc: Some(0.0005),
atmosphere_has_nan: false,
elapsed_sec: 120.0,
@@ -3231,7 +3182,7 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
.unwrap()
.unwrap()
.0,
"converged"
"completed"
);
// ---- 任务 B:迟到的重复失败报告(涡旋残留任务的典型行为)----
@@ -3239,7 +3190,6 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
task_id: uuid::Uuid::new_v4(),
point_name: name.clone(),
params: params.clone(),
task_type: common::models::TaskType::ColdRun,
seed_point_name: None,
timeout_sec: 7200,
workflow_name: Some("wf_flip".to_string()),
@@ -3269,7 +3219,7 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
params: Some(params.clone()),
node_id: "node-flip".to_string(),
status: common::models::TaskStatus::Failed,
converged: false,
result_valid: false,
max_relc: Some(9.5e5),
atmosphere_has_nan: false,
elapsed_sec: 130.0,
@@ -3305,7 +3255,7 @@ async fn test_duplicate_failure_report_cannot_flip_converged() {
.unwrap()
.unwrap()
.0,
"converged",
"completed",
"迟到失败报告不得翻黑 converged 点"
);
assert!(
@@ -3384,7 +3334,7 @@ async fn test_save_workflow_validates_stage_configs() {
// 1. 双阶段全关 → 400
let both_off = format!(
"{}\ntlusty:\n enabled: false\n policy: skip_converged\n strategies: [cold_run, seed_step]\n\
"{}\ntlusty_stage:\n enabled: false\n policy: skip_converged\n strategies: [cold_run, seed_step]\n\
synspec_stage:\n enabled: false\n policy: skip_converged\n strategies: [standard]\n",
base_yaml
);
@@ -3393,7 +3343,7 @@ async fn test_save_workflow_validates_stage_configs() {
// 2. 启用阶段空策略链 → 400
let empty_chain = format!(
"{}\ntlusty:\n enabled: true\n policy: skip_converged\n strategies: []\n",
"{}\ntlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: []\n",
base_yaml
);
let res = post_save("wf_val_b", &empty_chain).await;
@@ -3405,7 +3355,7 @@ async fn test_save_workflow_validates_stage_configs() {
// 3. 合法:TLUSTY 关 + SYNSPEC 启(设计 §2.2 场景 B:仅更新光谱)→ 200
let syn_only = format!(
"{}\ntlusty:\n enabled: false\n policy: skip_converged\n strategies: [cold_run, seed_step]\n\
"{}\ntlusty_stage:\n enabled: false\n policy: skip_converged\n strategies: [cold_run, seed_step]\n\
synspec_stage:\n enabled: true\n policy: force_recompute\n strategies: [standard]\n",
base_yaml
);