/* 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, '禁用阶段空链合法'); });