/* 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>'); }); test('escapeHtml 顺序敏感:& 先转义,不二次转义', () => { assert.equal(escapeHtml('a & b'), 'a & b'); assert.equal(escapeHtml('&'), '&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'), '运行中'); assert.equal(statusBadge('completed'), '已完成'); assert.equal(statusBadge('initializing'), '初始化中'); assert.equal(statusBadge('paused'), '已暂停'); assert.equal(statusBadge('idle'), '闲置'); assert.equal(statusBadge('unknown-status'), '闲置'); // 兜底 }); test('pointStatusBadge 覆盖网格点状态', () => { assert.equal(pointStatusBadge('completed'), '已完成'); assert.equal(pointStatusBadge('failed'), '失败'); assert.equal(pointStatusBadge('running'), '运行中'); assert.equal(pointStatusBadge('queued'), '排队中'); assert.equal(pointStatusBadge('pending'), '未开始'); assert.equal(pointStatusBadge('???'), '未知'); }); test('pointMethodBadge 识别收敛途径(与后端整体归因契约对齐)', () => { assert.equal(pointMethodBadge('cold_run'), '冷启动'); assert.equal(pointMethodBadge('seed_step'), '种子步进'); // 其它策略名(synspec-only 点归因的光谱策略,如 "standard")原样展示 assert.equal(pointMethodBadge('standard'), 'standard'); // 含 HTML 元字符须转义(XSS 防护) assert.equal( pointMethodBadge(''), '<img src=x onerror=alert(1)>' ); assert.equal(pointMethodBadge(null), ''); assert.equal(pointMethodBadge(''), ''); }); 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); });