feat(all): 任务引擎双阶段解耦、僵尸涡旋修复、动态 CPU 配额与前端详情页重构

将 TLUSTY/SYNSPEC 拆为各自独立的 enabled/policy/strategies 阶段,
以策略链自动弹栈取代单级 seed_step 布尔回退;定向修复 2026-08-02
僵尸任务涡旋事故;新增节点并发配额热调;前端详情页从 1412 行巨型
视图拆为薄控制器 + detail 子模块,并补齐工具层与单测。

引擎与调度(task_engine_decoupling_design.md)
- models.rs: 新增 StagePolicy / EngineStageConfig / TaskSpec 阶段字段、
  normalize_compat() 校正旧版在途消息策略链、failed_stage 归因
- scheduler.rs: resolve_dispatchable_chain 派发门控、
  trigger_strategy_fallback 按 failed_stage 精确弹栈;启动期
  force_recompute/skip_converged(默认)/skip_failed 三策略
- db.rs: tasks 表 +7 列持久化阶段配置;终态守卫
  (mark_grid_point_running 仅 pending/queued→running;
  record_task_report 拒绝迟到失败翻黑 converged);策略弹栈快照

僵尸涡旋修复(runbook-20260802-zombie-vortex-fix.md)
- 全链路跨库活性交叉校验:派发/claim/孤儿回收/回退统一查 MQ 队列活性,
  活则放行、死则清僵尸,结构性消除"每点重复派发"
- stop/重启卫生:清队列同步 delete_tasks_by_ids,杜绝遗留 pending 行
- report_task: 幂等吸收 + 409 区分迟到冗余结果,仅 state_changed 时回退
- MQ: NULL workflow_name 回填 __legacy__、requeue 后迟到上报被 403 竞态修复

动态 CPU 配额(dynamic_cpu_slots_design.md)
- admin.rs: POST /admin/nodes/:id/quota(Option<Option<i32>> 区分
  缺字段/显式 null);nodes 表 +admin_max_slots
- worker.rs: effective_max_slots = min(admin, physical),心跳下发原子生效

科学产物保全(tlusty_result_artifacts.md)
- runner.rs: SYNSPEC 启动前快照 fort.12/fort.14 → .bfac/.emflux 防覆盖
- 半失败点(大气收敛+光谱失败)改判 Failed 并写入 note;仅 SYNSPEC
  场景不再恒判失败;撤销归档 LRU 200 上限改为永久保留
- executor.rs: 透传 synspec_params 数值参数(此前固定 None)

前端(dashboard/)
- workflowDetail.js 1412→328 行,拆出 views/detail/{ctx,overview,
  pointsTable,parSets,pointPanel}.js,AbortController 治理监听/请求生命周期
- 删除 wfActions.js,新增 wfEnginePanel.js(双阶段三维配置编辑面板)
- 新增 utils/{errors,format,icons,polling,yamlStage}.js 纯函数模块
- 路由级动态 import 代码分割;节点配额三点菜单 + Modal 管理
- 首次引入 node:test 单测(format/polling/yamlStage/psCache,644 行)
- 系统性补齐 a11y:skip-link、ARIA、Tab 键盘漫游、toast 关闭、退出动画

文档与工具
- 新增 6 篇设计/调研:引擎解耦、动态配额、涡旋 runbook、
  光谱正确性分析、收敛判断、产物归档
- PIPELINE/design/api/database 等协同重写为分布式 C/S 架构口径
- scripts/fetch_results.sh 跨节点产物备份;import_results 按 cno 升序导入
- workflows/sdB_cno.yaml: 新增 tlusty/synspec_stage 配置块,修正 wstart 笔误
This commit is contained in:
fmq
2026-08-04 23:40:52 +08:00
parent c8fd24b120
commit cd370d88e7
74 changed files with 13386 additions and 3033 deletions
+259
View File
@@ -0,0 +1,259 @@
/* 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'), {
enabled: true, policy: 'skip_converged', strategies: ['cold_run', 'seed_step'],
});
assert.deepEqual(defaultStage('synspec'), {
enabled: true, policy: 'skip_converged', strategies: ['standard'],
});
});
test('extractTopBlock:顶层注释(列 0 #)终止块,不吞入下一块(审查 #4)', () => {
const yaml = [
'tlusty:',
' enabled: true',
' policy: skip_converged',
'# 这是顶层注释,应终止 tlusty 块',
'synspec_stage:',
' enabled: false',
' policy: force_recompute',
].join('\n');
const t = extractTopBlock(yaml, 'tlusty');
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:',
' enabled: true',
'',
' # 缩进注释,属块体',
' policy: skip_converged',
].join('\n');
const t = extractTopBlock(yaml, 'tlusty');
assert.equal(t.lines.length, 5);
});
test('parseStageFromYaml:行内注释不污染 policy 解析(审查 #5', () => {
const yaml = [
'tlusty:',
' enabled: true',
' policy: skip_converged # 旧值 force_recompute 已废弃',
' strategies: [cold_run, seed_step]',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty');
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:',
' # policy: force_recompute <- 注释掉的旧值',
' policy: skip_failed',
' strategies: [cold_run]',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty');
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:',
' enabled: true',
' policy: skip_converged',
' strategies:',
' - cold_run',
' - seed_step',
].join('\n');
const t = parseStageFromYaml(yaml, 'tlusty');
assert.deepEqual(t.strategies, ['cold_run', 'seed_step']);
});
test('parseStageFromYaml:块不存在返回 null', () => {
const yaml = 'grid:\n teff: [20000]\n';
assert.equal(parseStageFromYaml(yaml, 'tlusty'), 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'));
// 缺省(无该字段)→ 默认链。
const yamlAbsent = 'grid:\n teff: [20000]\n';
assert.deepEqual(resolveTlustyFromYaml(yamlAbsent), defaultStage('tlusty'));
});
test('resolveTlustyFromYaml:有 tlusty 块时优先块(不受旧字段干扰)', () => {
const yaml = [
'tlusty:',
' 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'),
'注释行被忽略,按缺省推断默认链');
});
test('applyStageBlocks:替换已存在块,保留其他配置(往返保真)', () => {
const yaml = [
'grid:',
' teff: [20000]',
'timeout_sec: 7200',
'tlusty:',
' 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:\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'), defaultStage('synspec'));
assert.match(out, /tlusty:/);
assert.match(out, /synspec_stage:/);
assert.match(out, /teff: \[20000\]/, '原配置保留');
});
test('applyStageBlocks:两块间有顶层注释时不互相破坏(审查 #4 回归)', () => {
const yaml = [
'tlusty:',
' 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'),
);
assert.match(out, /synspec_stage:\n enabled: true/, 'synspec_stage 块必须存活');
assert.match(out, /tlusty:\n enabled: false/, 'tlusty 块已更新');
});
test('serializeStageBlockflow 序列输出格式', () => {
const s = serializeStageBlock('tlusty', {
enabled: true, policy: 'skip_converged', strategies: ['cold_run', 'seed_step'],
});
assert.equal(s, 'tlusty:\n enabled: true\n policy: skip_converged\n strategies: [cold_run, seed_step]');
});
test('端到端往返:解析 → 序列化 → 再解析 保持语义', () => {
const orig = [
'tlusty:',
' enabled: true',
' policy: force_recompute',
' strategies: [cold_run, seed_step]',
].join('\n');
const parsed = parseStageFromYaml(orig, 'tlusty');
const serialized = serializeStageBlock('tlusty', parsed);
const reparsed = parseStageFromYaml(serialized, 'tlusty');
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, '禁用阶段空链合法');
});