将 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 笔误
86 lines
3.1 KiB
JavaScript
86 lines
3.1 KiB
JavaScript
/* DCTS 平行集合图 sessionStorage 缓存单测(node:test,零依赖)
|
||
*
|
||
* 覆盖 parSets.js 的 savePsCache / restorePsCache:刷新页面后恢复数据、单槽位清理、
|
||
* 损坏数据回退。node 无 sessionStorage,测试前用内存 Map stub。
|
||
*/
|
||
|
||
import { test } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
|
||
// 内存 sessionStorage stub(setItem 可注入配额错误用于 QuotaExceeded 分支)
|
||
let store = new Map();
|
||
let quotaExceeded = false;
|
||
globalThis.sessionStorage = {
|
||
get length() { return store.size; },
|
||
key: (i) => [...store.keys()][i] ?? null,
|
||
getItem: (k) => (store.has(k) ? store.get(k) : null),
|
||
setItem: (k, v) => {
|
||
if (quotaExceeded) throw new Error('QuotaExceededError');
|
||
store.set(k, String(v));
|
||
},
|
||
removeItem: (k) => { store.delete(k); },
|
||
};
|
||
|
||
const { savePsCache, restorePsCache } = await import('../src/views/detail/parSets.js');
|
||
|
||
function ctx(name, points = [], ts = null) {
|
||
return { name, psState: { points, ts } };
|
||
}
|
||
|
||
test('savePsCache 写入 JSON 并可由 restorePsCache 恢复', () => {
|
||
store.clear();
|
||
const before = Date.now();
|
||
const c = ctx('sdB_cno', [{ name: 'p1', teff: 20000, status: 'converged' }]);
|
||
savePsCache(c);
|
||
|
||
// 持久化 key 存在
|
||
assert.ok(store.has('dcts_ps_points_sdB_cno'));
|
||
// 恢复后 points 一致;快照时间由 savePsCache 记录为保存时刻(Date.now 附近)
|
||
const c2 = ctx('sdB_cno');
|
||
assert.equal(restorePsCache(c2), true);
|
||
assert.deepEqual(c2.psState.points, c.psState.points);
|
||
assert.ok(Number.isFinite(c2.psState.ts) && c2.psState.ts >= before);
|
||
assert.ok(Math.abs(c2.psState.ts - Date.now()) < 5000);
|
||
});
|
||
|
||
test('多工作流缓存并存:切换工作流不删除彼此的缓存', () => {
|
||
store.clear();
|
||
savePsCache(ctx('wf_a', [{ name: 'a' }]));
|
||
savePsCache(ctx('wf_b', [{ name: 'b' }]));
|
||
savePsCache(ctx('wf_a', [{ name: 'a2' }])); // 再回 wf_a 更新,writes 覆盖自身 key
|
||
|
||
// wf_a 与 wf_b 的缓存都保留
|
||
assert.ok(store.has('dcts_ps_points_wf_a'));
|
||
assert.ok(store.has('dcts_ps_points_wf_b'));
|
||
|
||
// 各自恢复出各自的数据(不串)
|
||
const ca = ctx('wf_a');
|
||
const cb = ctx('wf_b');
|
||
assert.equal(restorePsCache(ca), true);
|
||
assert.equal(restorePsCache(cb), true);
|
||
assert.deepEqual(ca.psState.points, [{ name: 'a2' }]); // 更新后是最新值
|
||
assert.deepEqual(cb.psState.points, [{ name: 'b' }]);
|
||
});
|
||
|
||
test('无缓存 / 空 key 时 restorePsCache 返回 false', () => {
|
||
store.clear();
|
||
const c = ctx('no_such_wf');
|
||
assert.equal(restorePsCache(c), false);
|
||
assert.deepEqual(c.psState.points, []);
|
||
});
|
||
|
||
test('损坏的 JSON / 非数组数据返回 false(降级为重新拉取)', () => {
|
||
store.clear();
|
||
store.set('dcts_ps_points_bad_json', 'not-json{{{');
|
||
store.set('dcts_ps_points_bad_shape', JSON.stringify({ foo: 1 }));
|
||
assert.equal(restorePsCache(ctx('bad_json')), false);
|
||
assert.equal(restorePsCache(ctx('bad_shape')), false);
|
||
});
|
||
|
||
test('QuotaExceeded 时 savePsCache 静默失败,不抛错', () => {
|
||
store.clear();
|
||
quotaExceeded = true;
|
||
assert.doesNotThrow(() => savePsCache(ctx('big_wf', new Array(10000).fill({ x: 1 }))));
|
||
quotaExceeded = false;
|
||
});
|