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:
@@ -10,7 +10,7 @@ import assert from 'node:assert/strict';
|
||||
import {
|
||||
escapeHtml, parseIso, parseTsMs, fmtTime, fmtDateTime, fmtFullTs,
|
||||
fmtDuration, fmtDur, fmtRelc, csvEscape, fmtClock,
|
||||
statusBadge, pointStatusBadge, pointMethodBadge,
|
||||
statusBadge, pointStatusBadge, pointMethodBadge, overallMethod,
|
||||
} from '../src/utils/format.js';
|
||||
|
||||
// ===== escapeHtml =====
|
||||
@@ -142,7 +142,7 @@ test('statusBadge 覆盖全部后端工作流状态', () => {
|
||||
});
|
||||
|
||||
test('pointStatusBadge 覆盖网格点状态', () => {
|
||||
assert.equal(pointStatusBadge('converged'), '<span class="status-badge online">已收敛</span>');
|
||||
assert.equal(pointStatusBadge('completed'), '<span class="status-badge online">已完成</span>');
|
||||
assert.equal(pointStatusBadge('failed'), '<span class="status-badge danger">失败</span>');
|
||||
assert.equal(pointStatusBadge('running'), '<span class="status-badge warning">运行中</span>');
|
||||
assert.equal(pointStatusBadge('queued'), '<span class="status-badge info">排队中</span>');
|
||||
@@ -150,10 +150,10 @@ test('pointStatusBadge 覆盖网格点状态', () => {
|
||||
assert.equal(pointStatusBadge('???'), '<span class="status-badge secondary">未知</span>');
|
||||
});
|
||||
|
||||
test('pointMethodBadge 识别收敛途径(与后端 success_method 契约对齐)', () => {
|
||||
test('pointMethodBadge 识别收敛途径(与后端整体归因契约对齐)', () => {
|
||||
assert.equal(pointMethodBadge('cold_run'), '<span class="method-badge cold">冷启动</span>');
|
||||
assert.equal(pointMethodBadge('seed_step'), '<span class="method-badge seed">种子步进</span>');
|
||||
// 其它策略名(synspec-only 任务归因的 success_method,如 "standard")原样展示
|
||||
// 其它策略名(synspec-only 点归因的光谱策略,如 "standard")原样展示
|
||||
assert.equal(pointMethodBadge('standard'), '<span class="method-badge syn">standard</span>');
|
||||
// 含 HTML 元字符须转义(XSS 防护)
|
||||
assert.equal(
|
||||
@@ -163,3 +163,18 @@ test('pointMethodBadge 识别收敛途径(与后端 success_method 契约对
|
||||
assert.equal(pointMethodBadge(null), '<span class="text-hint">—</span>');
|
||||
assert.equal(pointMethodBadge(''), '<span class="text-hint">—</span>');
|
||||
});
|
||||
|
||||
test('overallMethod 派生整体归因(tlusty 阶段优先,synspec-only 退回光谱策略)', () => {
|
||||
// 正常双阶段点:TLUSTY 阶段策略
|
||||
assert.equal(overallMethod({ tlusty_success_method: 'seed_step' }),
|
||||
'seed_step');
|
||||
// synspec-only 点:tlusty 为 NULL,退回光谱策略
|
||||
assert.equal(overallMethod({ tlusty_success_method: null, synspec_success_method: 'standard' }),
|
||||
'standard');
|
||||
// 双阶段都落库时优先大气侧
|
||||
assert.equal(overallMethod({ tlusty_success_method: 'cold_run', synspec_success_method: 'standard' }),
|
||||
'cold_run');
|
||||
// 两者皆空 → null(`??` 保留右侧 null)
|
||||
assert.equal(overallMethod({ tlusty_success_method: null, synspec_success_method: null }),
|
||||
null);
|
||||
});
|
||||
|
||||
@@ -117,6 +117,41 @@ test('triggerNow() 立即触发一次并续排', async () => {
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
test('triggerNow() 与在途 schedule 不并发执行 fn(inFlight 守卫)', async () => {
|
||||
const stub = installDocStub();
|
||||
const timers = captureTimers();
|
||||
let active = 0; // 当前正在执行的 fn 数(应恒 ≤1)
|
||||
let maxActive = 0;
|
||||
let calls = 0;
|
||||
// fn 是慢异步:用 gate 控制其完成时机,确保 triggerNow 落在它在途时。
|
||||
let resolveFn = null;
|
||||
const fn = () => {
|
||||
active++;
|
||||
maxActive = Math.max(maxActive, active);
|
||||
calls++;
|
||||
return new Promise((resolve) => { resolveFn = resolve; });
|
||||
};
|
||||
const p = createPoller(fn, { baseMs: 5000 });
|
||||
const startP = p.start(); // 触发首次 schedule(fn 在途,停在 await)
|
||||
// 让 start 的 schedule 进入 fn()——通过一次微任务边界。
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
// 此时 fn 在途(calls=1,active=1)。triggerNow 应受 inFlight 守卫直接 return,
|
||||
// 不再起一个新的 schedule 并发跑 fn。
|
||||
const triggerP = p.triggerNow();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
// 完成 fn(在途的 schedule 才会推进)。
|
||||
resolveFn(true);
|
||||
await Promise.all([startP, triggerP]);
|
||||
active = 0; // fn 已完成
|
||||
assert.equal(calls, 1, 'triggerNow 不得并发触发第二次 fn(inFlight 守卫)');
|
||||
assert.equal(maxActive, 1, 'fn 不得并发执行(maxActive 应为 1)');
|
||||
p.stop();
|
||||
timers.restore();
|
||||
stub.restore();
|
||||
});
|
||||
|
||||
test('退避上限 maxMs 生效(长时间连续失败不无限增长)', async () => {
|
||||
const stub = installDocStub();
|
||||
const timers = captureTimers();
|
||||
|
||||
@@ -30,7 +30,7 @@ function ctx(name, points = [], ts = null) {
|
||||
test('savePsCache 写入 JSON 并可由 restorePsCache 恢复', () => {
|
||||
store.clear();
|
||||
const before = Date.now();
|
||||
const c = ctx('sdB_cno', [{ name: 'p1', teff: 20000, status: 'converged' }]);
|
||||
const c = ctx('sdB_cno', [{ name: 'p1', teff: 20000, status: 'completed' }]);
|
||||
savePsCache(c);
|
||||
|
||||
// 持久化 key 存在
|
||||
|
||||
@@ -17,17 +17,17 @@ import {
|
||||
} from '../src/utils/yamlStage.js';
|
||||
|
||||
test('defaultStage:tlusty 默认 [cold_run, seed_step],synspec 默认 [standard]', () => {
|
||||
assert.deepEqual(defaultStage('tlusty'), {
|
||||
assert.deepEqual(defaultStage('tlusty_stage'), {
|
||||
enabled: true, policy: 'skip_converged', strategies: ['cold_run', 'seed_step'],
|
||||
});
|
||||
assert.deepEqual(defaultStage('synspec'), {
|
||||
assert.deepEqual(defaultStage('synspec_stage'), {
|
||||
enabled: true, policy: 'skip_converged', strategies: ['standard'],
|
||||
});
|
||||
});
|
||||
|
||||
test('extractTopBlock:顶层注释(列 0 #)终止块,不吞入下一块(审查 #4)', () => {
|
||||
const yaml = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: skip_converged',
|
||||
'# 这是顶层注释,应终止 tlusty 块',
|
||||
@@ -35,7 +35,7 @@ test('extractTopBlock:顶层注释(列 0 #)终止块,不吞入下一块
|
||||
' enabled: false',
|
||||
' policy: force_recompute',
|
||||
].join('\n');
|
||||
const t = extractTopBlock(yaml, 'tlusty');
|
||||
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');
|
||||
|
||||
@@ -46,24 +46,51 @@ test('extractTopBlock:顶层注释(列 0 #)终止块,不吞入下一块
|
||||
|
||||
test('extractTopBlock:空行属块体,缩进注释属块体', () => {
|
||||
const yaml = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
'',
|
||||
' # 缩进注释,属块体',
|
||||
' policy: skip_converged',
|
||||
].join('\n');
|
||||
const t = extractTopBlock(yaml, 'tlusty');
|
||||
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:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: skip_converged # 旧值 force_recompute 已废弃',
|
||||
' strategies: [cold_run, seed_step]',
|
||||
].join('\n');
|
||||
const t = parseStageFromYaml(yaml, 'tlusty');
|
||||
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']);
|
||||
@@ -71,12 +98,12 @@ test('parseStageFromYaml:行内注释不污染 policy 解析(审查 #5)',
|
||||
|
||||
test('parseStageFromYaml:整行注释里的 policy 不被当真值(审查 #5)', () => {
|
||||
const yaml = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' # policy: force_recompute <- 注释掉的旧值',
|
||||
' policy: skip_failed',
|
||||
' strategies: [cold_run]',
|
||||
].join('\n');
|
||||
const t = parseStageFromYaml(yaml, 'tlusty');
|
||||
const t = parseStageFromYaml(yaml, 'tlusty_stage');
|
||||
assert.equal(t.policy, 'skip_failed', '应跳过整行注释,取真值 skip_failed');
|
||||
});
|
||||
|
||||
@@ -93,20 +120,20 @@ test('parseStageFromYaml:enabled 行内注释', () => {
|
||||
|
||||
test('parseStageFromYaml:块式 strategies 列表', () => {
|
||||
const yaml = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: skip_converged',
|
||||
' strategies:',
|
||||
' - cold_run',
|
||||
' - seed_step',
|
||||
].join('\n');
|
||||
const t = parseStageFromYaml(yaml, 'tlusty');
|
||||
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'), null);
|
||||
assert.equal(parseStageFromYaml(yaml, 'tlusty_stage'), null);
|
||||
});
|
||||
|
||||
test('resolveTlustyFromYaml:无 tlusty 块时按 seed_step_fallback 推断(审查 #2 修复)', () => {
|
||||
@@ -124,16 +151,16 @@ test('resolveTlustyFromYaml:无 tlusty 块时按 seed_step_fallback 推断(
|
||||
|
||||
// seed_step_fallback: true → 默认链 [cold_run, seed_step]。
|
||||
const yamlTrue = 'seed_step_fallback: true\n';
|
||||
assert.deepEqual(resolveTlustyFromYaml(yamlTrue), defaultStage('tlusty'));
|
||||
assert.deepEqual(resolveTlustyFromYaml(yamlTrue), defaultStage('tlusty_stage'));
|
||||
|
||||
// 缺省(无该字段)→ 默认链。
|
||||
const yamlAbsent = 'grid:\n teff: [20000]\n';
|
||||
assert.deepEqual(resolveTlustyFromYaml(yamlAbsent), defaultStage('tlusty'));
|
||||
assert.deepEqual(resolveTlustyFromYaml(yamlAbsent), defaultStage('tlusty_stage'));
|
||||
});
|
||||
|
||||
test('resolveTlustyFromYaml:有 tlusty 块时优先块(不受旧字段干扰)', () => {
|
||||
const yaml = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: force_recompute',
|
||||
' strategies: [cold_run]',
|
||||
@@ -146,7 +173,7 @@ test('resolveTlustyFromYaml:有 tlusty 块时优先块(不受旧字段干扰
|
||||
|
||||
test('resolveTlustyFromYaml:注释行不误判 seed_step_fallback', () => {
|
||||
const yaml = '# seed_step_fallback: false(注释,非真实配置)\ngrid:\n teff: [20000]\n';
|
||||
assert.deepEqual(resolveTlustyFromYaml(yaml), defaultStage('tlusty'),
|
||||
assert.deepEqual(resolveTlustyFromYaml(yaml), defaultStage('tlusty_stage'),
|
||||
'注释行被忽略,按缺省推断默认链');
|
||||
});
|
||||
|
||||
@@ -155,7 +182,7 @@ test('applyStageBlocks:替换已存在块,保留其他配置(往返保真
|
||||
'grid:',
|
||||
' teff: [20000]',
|
||||
'timeout_sec: 7200',
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: skip_converged',
|
||||
' strategies: [cold_run]',
|
||||
@@ -172,22 +199,22 @@ test('applyStageBlocks:替换已存在块,保留其他配置(往返保真
|
||||
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\]/);
|
||||
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'), defaultStage('synspec'));
|
||||
assert.match(out, /tlusty:/);
|
||||
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:',
|
||||
'tlusty_stage:',
|
||||
' enabled: true',
|
||||
' policy: skip_converged',
|
||||
' strategies: [cold_run, seed_step]',
|
||||
@@ -200,29 +227,29 @@ test('applyStageBlocks:两块间有顶层注释时不互相破坏(审查 #4
|
||||
// 修改 tlusty 不应破坏 synspec_stage(旧实现会把注释+synspec 吞进 tlusty 块后覆盖丢失)。
|
||||
const out = applyStageBlocks(yaml,
|
||||
{ enabled: false, policy: 'skip_converged', strategies: ['cold_run'] },
|
||||
defaultStage('synspec'),
|
||||
defaultStage('synspec_stage'),
|
||||
);
|
||||
assert.match(out, /synspec_stage:\n enabled: true/, 'synspec_stage 块必须存活');
|
||||
assert.match(out, /tlusty:\n enabled: false/, 'tlusty 块已更新');
|
||||
assert.match(out, /tlusty_stage:\n enabled: false/, 'tlusty 块已更新');
|
||||
});
|
||||
|
||||
test('serializeStageBlock:flow 序列输出格式', () => {
|
||||
const s = serializeStageBlock('tlusty', {
|
||||
const s = serializeStageBlock('tlusty_stage', {
|
||||
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]');
|
||||
assert.equal(s, 'tlusty_stage:\n enabled: true\n policy: skip_converged\n strategies: [cold_run, seed_step]');
|
||||
});
|
||||
|
||||
test('端到端往返:解析 → 序列化 → 再解析 保持语义', () => {
|
||||
const orig = [
|
||||
'tlusty:',
|
||||
'tlusty_stage:',
|
||||
' 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');
|
||||
const parsed = parseStageFromYaml(orig, 'tlusty_stage');
|
||||
const serialized = serializeStageBlock('tlusty_stage', parsed);
|
||||
const reparsed = parseStageFromYaml(serialized, 'tlusty_stage');
|
||||
assert.deepEqual(parsed, reparsed, '往返保真');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user