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

181 lines
7.7 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 纯函数单测(node:test,零依赖,无需 npm install
*
* 覆盖 utils/format.js 的格式化/转义/徽章逻辑——这些是最容易出回归 bug 的纯函数
* (尤其 SQLite UTC 时间解析、指数记数、CSV 转义)。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import {
escapeHtml, parseIso, parseTsMs, fmtTime, fmtDateTime, fmtFullTs,
fmtDuration, fmtDur, fmtRelc, csvEscape, fmtClock,
statusBadge, pointStatusBadge, pointMethodBadge, overallMethod,
} from '../src/utils/format.js';
// ===== escapeHtml =====
test('escapeHtml 转义五个特殊字符', () => {
assert.equal(escapeHtml('<script>&"\'</script>'), '&lt;script&gt;&amp;&quot;&#39;&lt;/script&gt;');
});
test('escapeHtml 顺序敏感:& 先转义,不二次转义', () => {
assert.equal(escapeHtml('a & b'), 'a &amp; b');
assert.equal(escapeHtml('&amp;'), '&amp;amp;'); // 输入本身是实体时也如实转义(不做解码)
});
test('escapeHtml 空值/null 返回空串', () => {
assert.equal(escapeHtml(null), '');
assert.equal(escapeHtml(undefined), '');
assert.equal(escapeHtml(''), '');
});
// ===== 时间解析(SQLite 'YYYY-MM-DD HH:MM:SS' 视为 UTC =====
test('parseIso 解析 SQLite UTC 无时区格式', () => {
const d = parseIso('2026-08-02 15:30:00');
assert.ok(d instanceof Date);
assert.equal(d.toISOString(), '2026-08-02T15:30:00.000Z');
});
test('parseIso 解析带 T 的 ISO 原样', () => {
const d = parseIso('2026-08-02T15:30:00Z');
assert.equal(d.toISOString(), '2026-08-02T15:30:00.000Z');
});
test('parseIso 非法输入返回 null', () => {
assert.equal(parseIso('not-a-date'), null);
assert.equal(parseIso(null), null);
assert.equal(parseIso(''), null);
});
test('parseTsMs 返回 epoch 毫秒,非法输入为 NaN', () => {
assert.equal(parseTsMs('2026-08-02 00:00:00'), Date.UTC(2026, 7, 2));
assert.ok(Number.isNaN(parseTsMs('garbage')));
assert.ok(Number.isNaN(parseTsMs(null)));
});
test('fmtDateTime 输出 MM-DD HH:MM 定宽(按浏览器本地时区)', () => {
// 服务器存 UTC、前端按本地时区显示(既有行为)。断言值用同一 Date 计算,与时区无关。
const d = parseIso('2026-08-02 09:05:07');
const p = (n) => String(n).padStart(2, '0');
assert.equal(
fmtDateTime('2026-08-02 09:05:07'),
`${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`
);
assert.equal(fmtDateTime('bad'), 'bad'); // 解析失败原样返回
assert.equal(fmtDateTime(null), '—');
});
test('fmtFullTs 输出完整时间戳(按浏览器本地时区)', () => {
const d = parseIso('2026-08-02 09:05:07');
const p = (n) => String(n).padStart(2, '0');
assert.equal(
fmtFullTs('2026-08-02 09:05:07'),
`${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`
);
assert.equal(fmtFullTs(null), '');
});
test('fmtTime 非法时间戳回退为转义原文', () => {
assert.equal(fmtTime('garbage'), 'garbage');
assert.equal(fmtTime(null), '—');
});
// ===== 时长格式化 =====
test('fmtDuration(控制条 ETA):分钟粒度', () => {
assert.equal(fmtDuration(30), '1 分钟'); // 30s → round(0.5)=1 分钟(沿用原版算法)
assert.equal(fmtDuration(3600), '1 小时 0 分');
assert.equal(fmtDuration(5400), '1 小时 30 分');
assert.equal(fmtDuration(null), '—');
assert.equal(fmtDuration(NaN), '—');
// ≤0 视为无数据(L3 修复:失败任务 last_elapsed_sec=0.0 不应显示"0 分钟"
assert.equal(fmtDuration(0), '—');
assert.equal(fmtDuration(-5), '—');
});
test('fmtDur(表格单元格):42s / 12m 30s / 1h 20m', () => {
assert.equal(fmtDur(42), '42s');
assert.equal(fmtDur(750), '12m 30s');
assert.equal(fmtDur(4800), '1h 20m');
assert.equal(fmtDur(0), '—'); // ≤0 视为无数据
assert.equal(fmtDur(-5), '—');
assert.equal(fmtDur(''), '—');
assert.equal(fmtDur(null), '—');
});
test('fmtRelc 指数记数(max_relc 相对收敛度)', () => {
assert.equal(fmtRelc(0.00123), '1.23e-3');
assert.equal(fmtRelc(''), '—');
assert.equal(fmtRelc(null), '—');
assert.equal(fmtRelc('abc'), '—');
});
test('fmtClock 心跳时间容错', () => {
assert.match(fmtClock('2026-08-02T09:05:07Z'), /^\d{2}:\d{2}:\d{2}$/);
assert.equal(fmtClock('invalid'), '—');
assert.equal(fmtClock(null), '—');
});
// ===== CSV 转义 =====
test('csvEscape 逗号/引号/换行包裹并翻倍引号', () => {
assert.equal(csvEscape('plain'), 'plain');
assert.equal(csvEscape('a,b'), '"a,b"');
assert.equal(csvEscape('say "hi"'), '"say ""hi"""');
assert.equal(csvEscape('line1\nline2'), '"line1\nline2"');
assert.equal(csvEscape('a\rb'), '"a\rb"');
assert.equal(csvEscape(null), '');
assert.equal(csvEscape(123), '123');
});
// ===== 状态徽章 =====
test('statusBadge 覆盖全部后端工作流状态', () => {
assert.equal(statusBadge('running'), '<span class="status-badge online">运行中</span>');
assert.equal(statusBadge('completed'), '<span class="status-badge completed">已完成</span>');
assert.equal(statusBadge('initializing'), '<span class="status-badge warning">初始化中</span>');
assert.equal(statusBadge('paused'), '<span class="status-badge secondary">已暂停</span>');
assert.equal(statusBadge('idle'), '<span class="status-badge secondary">闲置</span>');
assert.equal(statusBadge('unknown-status'), '<span class="status-badge secondary">闲置</span>'); // 兜底
});
test('pointStatusBadge 覆盖网格点状态', () => {
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>');
assert.equal(pointStatusBadge('pending'), '<span class="status-badge secondary">未开始</span>');
assert.equal(pointStatusBadge('???'), '<span class="status-badge secondary">未知</span>');
});
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 点归因的光谱策略,如 "standard")原样展示
assert.equal(pointMethodBadge('standard'), '<span class="method-badge syn">standard</span>');
// 含 HTML 元字符须转义(XSS 防护)
assert.equal(
pointMethodBadge('<img src=x onerror=alert(1)>'),
'<span class="method-badge syn">&lt;img src=x onerror=alert(1)&gt;</span>'
);
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);
});