Files
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

287 lines
11 KiB
JavaScript
Raw Permalink 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.
/* DCTS wfEnginePanel 阶段配置 YAML 解析/序列化单测(node:test,零依赖)
*
* 覆盖 utils/yamlStage.js 的注释处理、块边界、往返保真——这些是审查 #4/#5
* (顶层注释吞块、行内注释污染解析)修复的回归守护。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
defaultStage,
extractTopBlock,
parseStageFromYaml,
resolveTlustyFromYaml,
serializeStageBlock,
applyStageBlocks,
validateStageConfigs,
} from '../src/utils/yamlStage.js';
test('defaultStagetlusty 默认 [cold_run, seed_step]synspec 默认 [standard]', () => {
assert.deepEqual(defaultStage('tlusty_stage'), {
enabled: true, policy: 'skip_converged', strategies: ['cold_run', 'seed_step'],
});
assert.deepEqual(defaultStage('synspec_stage'), {
enabled: true, policy: 'skip_converged', strategies: ['standard'],
});
});
test('extractTopBlock:顶层注释(列 0 #)终止块,不吞入下一块(审查 #4)', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
' policy: skip_converged',
'# 这是顶层注释,应终止 tlusty 块',
'synspec_stage:',
' enabled: false',
' policy: force_recompute',
].join('\n');
const t = extractTopBlock(yaml, 'tlusty_stage');
assert.equal(t.lines.length, 3, 'tlusty 块应只含自身 3 行,不含顶层注释与后续块');
assert.ok(!t.lines.some(l => l.includes('synspec_stage')), '不吞入 synspec_stage');
const s = extractTopBlock(yaml, 'synspec_stage');
assert.ok(s, 'synspec_stage 块应能被独立定位(未被 tlusty 吞掉)');
assert.equal(s.lines.length, 3);
});
test('extractTopBlock:空行属块体,缩进注释属块体', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
'',
' # 缩进注释,属块体',
' policy: skip_converged',
].join('\n');
const t = extractTopBlock(yaml, 'tlusty_stage');
assert.equal(t.lines.length, 5);
});
test('extractTopBlockkey 含正则元字符按字面匹配,`.` 不通配(防御性转义)', () => {
// 用不会真实出现的 `te.lusty` 验证 `.` 被转义:若未转义,`teXlusty:` 会被误匹配。
const yaml = [
'teXlusty:',
' enabled: true',
'te.lusty:',
' enabled: false',
].join('\n');
// 字面匹配 te.lusty → 命中第三行,而非被通配成 teXlusty。
const t = extractTopBlock(yaml, 'te.lusty');
assert.ok(t, '含 `.` 的 key 应能定位其自身块');
assert.equal(t.startIdx, 2, '应命中字面 te.lusty: 行,而非被通配成 teXlusty:');
assert.ok(!t.lines.some(l => l.includes('teXlusty')), '不应误匹配通配目标 teXlusty');
});
test('extractTopBlockkey 含 `+` 等其它元字符仍按字面匹配', () => {
// `+` 在正则里是量词;未转义会抛 SyntaxError 或误匹配。此处验证按字面定位。
const yaml = [
'a+b:',
' enabled: true',
].join('\n');
const t = extractTopBlock(yaml, 'a+b');
assert.ok(t, '含 `+` 的 key 应按字面匹配而不抛错');
assert.equal(t.startIdx, 0);
assert.equal(t.lines.length, 2);
});
test('parseStageFromYaml:行内注释不污染 policy 解析(审查 #5', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
' policy: skip_converged # 旧值 force_recompute 已废弃',
' strategies: [cold_run, seed_step]',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty_stage');
assert.equal(t.policy, 'skip_converged', '应取真值而非注释里的 force_recompute');
assert.equal(t.enabled, true);
assert.deepEqual(t.strategies, ['cold_run', 'seed_step']);
});
test('parseStageFromYaml:整行注释里的 policy 不被当真值(审查 #5', () => {
const yaml = [
'tlusty_stage:',
' # policy: force_recompute <- 注释掉的旧值',
' policy: skip_failed',
' strategies: [cold_run]',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty_stage');
assert.equal(t.policy, 'skip_failed', '应跳过整行注释,取真值 skip_failed');
});
test('parseStageFromYamlenabled 行内注释', () => {
const yaml = [
'synspec_stage:',
' enabled: false # 仅计算大气',
' policy: skip_converged',
' strategies: [standard]',
].join('\n');
const s = parseStageFromYaml(yaml, 'synspec_stage');
assert.equal(s.enabled, false, 'enabled=false 应正确解析(注释不干扰)');
});
test('parseStageFromYaml:块式 strategies 列表', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
' policy: skip_converged',
' strategies:',
' - cold_run',
' - seed_step',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty_stage');
assert.deepEqual(t.strategies, ['cold_run', 'seed_step']);
});
test('parseStageFromYaml:块不存在返回 null', () => {
const yaml = 'grid:\n teff: [20000]\n';
assert.equal(parseStageFromYaml(yaml, 'tlusty_stage'), null);
});
test('resolveTlustyFromYaml:无 tlusty 块时按 seed_step_fallback 推断(审查 #2 修复)', () => {
// seed_step_fallback: false → [cold_run](不回退),与服务端 resolve_tlusty_config 同口径。
const yamlFalse = [
'grid:',
' teff: [20000]',
'seed_step_fallback: false',
'synspec:',
' wstart: 3000.0',
].join('\n');
assert.deepEqual(resolveTlustyFromYaml(yamlFalse), {
enabled: true, policy: 'skip_converged', strategies: ['cold_run'],
}, 'seed_step_fallback:false 应推断 [cold_run](面板保存不得静默启用回退)');
// seed_step_fallback: true → 默认链 [cold_run, seed_step]。
const yamlTrue = 'seed_step_fallback: true\n';
assert.deepEqual(resolveTlustyFromYaml(yamlTrue), defaultStage('tlusty_stage'));
// 缺省(无该字段)→ 默认链。
const yamlAbsent = 'grid:\n teff: [20000]\n';
assert.deepEqual(resolveTlustyFromYaml(yamlAbsent), defaultStage('tlusty_stage'));
});
test('resolveTlustyFromYaml:有 tlusty 块时优先块(不受旧字段干扰)', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
' policy: force_recompute',
' strategies: [cold_run]',
'seed_step_fallback: true',
].join('\n');
assert.deepEqual(resolveTlustyFromYaml(yaml), {
enabled: true, policy: 'force_recompute', strategies: ['cold_run'],
}, '显式 tlusty 块优先');
});
test('resolveTlustyFromYaml:注释行不误判 seed_step_fallback', () => {
const yaml = '# seed_step_fallback: false(注释,非真实配置)\ngrid:\n teff: [20000]\n';
assert.deepEqual(resolveTlustyFromYaml(yaml), defaultStage('tlusty_stage'),
'注释行被忽略,按缺省推断默认链');
});
test('applyStageBlocks:替换已存在块,保留其他配置(往返保真)', () => {
const yaml = [
'grid:',
' teff: [20000]',
'timeout_sec: 7200',
'tlusty_stage:',
' enabled: true',
' policy: skip_converged',
' strategies: [cold_run]',
'synspec_stage:',
' enabled: true',
' policy: skip_converged',
' strategies: [standard]',
].join('\n');
const out = applyStageBlocks(yaml,
{ enabled: false, policy: 'force_recompute', strategies: ['seed_step'] },
{ enabled: true, policy: 'skip_converged', strategies: ['standard'] },
);
// grid 与 timeout_sec 必须保留。
assert.match(out, /teff: \[20000\]/);
assert.match(out, /timeout_sec: 7200/);
// tlusty 块被替换为新值。
assert.match(out, /tlusty_stage:\n enabled: false\n policy: force_recompute\n strategies: \[seed_step\]/);
// synspec_stage 块保留/替换正确。
assert.match(out, /synspec_stage:\n enabled: true\n policy: skip_converged\n strategies: \[standard\]/);
});
test('applyStageBlocks:块不存在时追加到末尾', () => {
const yaml = 'grid:\n teff: [20000]\n';
const out = applyStageBlocks(yaml, defaultStage('tlusty_stage'), defaultStage('synspec_stage'));
assert.match(out, /tlusty_stage:/);
assert.match(out, /synspec_stage:/);
assert.match(out, /teff: \[20000\]/, '原配置保留');
});
test('applyStageBlocks:两块间有顶层注释时不互相破坏(审查 #4 回归)', () => {
const yaml = [
'tlusty_stage:',
' enabled: true',
' policy: skip_converged',
' strategies: [cold_run, seed_step]',
'# 分隔注释',
'synspec_stage:',
' enabled: true',
' policy: skip_converged',
' strategies: [standard]',
].join('\n');
// 修改 tlusty 不应破坏 synspec_stage(旧实现会把注释+synspec 吞进 tlusty 块后覆盖丢失)。
const out = applyStageBlocks(yaml,
{ enabled: false, policy: 'skip_converged', strategies: ['cold_run'] },
defaultStage('synspec_stage'),
);
assert.match(out, /synspec_stage:\n enabled: true/, 'synspec_stage 块必须存活');
assert.match(out, /tlusty_stage:\n enabled: false/, 'tlusty 块已更新');
});
test('serializeStageBlockflow 序列输出格式', () => {
const s = serializeStageBlock('tlusty_stage', {
enabled: true, policy: 'skip_converged', strategies: ['cold_run', 'seed_step'],
});
assert.equal(s, 'tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [cold_run, seed_step]');
});
test('端到端往返:解析 → 序列化 → 再解析 保持语义', () => {
const orig = [
'tlusty_stage:',
' enabled: true',
' policy: force_recompute',
' strategies: [cold_run, seed_step]',
].join('\n');
const parsed = parseStageFromYaml(orig, 'tlusty_stage');
const serialized = serializeStageBlock('tlusty_stage', parsed);
const reparsed = parseStageFromYaml(serialized, 'tlusty_stage');
assert.deepEqual(parsed, reparsed, '往返保真');
});
test('validateStageConfigs:双阶段全关拦截(修复审查 #5)', () => {
const err = validateStageConfigs(
{ enabled: false, policy: 'skip_converged', strategies: ['cold_run'] },
{ enabled: false, policy: 'skip_converged', strategies: ['standard'] },
);
assert.ok(err && err.includes('至少应启用一个计算阶段'), `应拦截双关,实际: ${err}`);
assert.equal(validateStageConfigs(
{ enabled: true, policy: 'skip_converged', strategies: ['cold_run'] },
{ enabled: false, policy: 'skip_converged', strategies: ['standard'] },
), null, '单阶段启用合法');
});
test('validateStageConfigs:启用阶段空策略链拦截(修复审查 #4)', () => {
const tlustyErr = validateStageConfigs(
{ enabled: true, policy: 'skip_converged', strategies: [] },
{ enabled: true, policy: 'skip_converged', strategies: ['standard'] },
);
assert.ok(tlustyErr && tlustyErr.includes('TLUSTY'), `应拦截 TLUSTY 空链,实际: ${tlustyErr}`);
const synspecErr = validateStageConfigs(
{ enabled: true, policy: 'skip_converged', strategies: ['cold_run'] },
{ enabled: true, policy: 'skip_converged', strategies: [] },
);
assert.ok(synspecErr && synspecErr.includes('SYNSPEC'), `应拦截 SYNSPEC 空链,实际: ${synspecErr}`);
// 禁用阶段的空链不拦截(该阶段不执行)。
assert.equal(validateStageConfigs(
{ enabled: false, policy: 'skip_converged', strategies: [] },
{ enabled: true, policy: 'skip_converged', strategies: ['standard'] },
), null, '禁用阶段空链合法');
});