- 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
287 lines
11 KiB
JavaScript
287 lines
11 KiB
JavaScript
/* 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('defaultStage:tlusty 默认 [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('extractTopBlock:key 含正则元字符按字面匹配,`.` 不通配(防御性转义)', () => {
|
||
// 用不会真实出现的 `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('extractTopBlock:key 含 `+` 等其它元字符仍按字面匹配', () => {
|
||
// `+` 在正则里是量词;未转义会抛 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('parseStageFromYaml:enabled 行内注释', () => {
|
||
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('serializeStageBlock:flow 序列输出格式', () => {
|
||
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, '禁用阶段空链合法');
|
||
});
|