没有构建步骤,没有打包器,没有任何东西对 lib/client.js 做类型检查。所以测试是唯一的安全网,
它的形状也就跟着这个事实长:纯规则用断言钉死,UI 用真实渲染跑一遍。
npm test # node --test test/*.test.mjs
npm run test:render # node test/render-smoke.mjs
单个文件:node --test test/tree.test.mjs。
测试全部用 node:test + node:assert,没有第三方测试框架、没有 DOM 实现依赖——
render-smoke 自带一个极小的假 document。
| 文件 | 行数 | 钉住的东西 |
|---|---|---|
test/tree.test.mjs |
580 | 树投影的规则 R1–R5:嵌套、计数、降级、环、sessionHome |
test/tree-mirror.test.mjs |
178 | 客户端只持有遍历、不持有规则——同一问题在两处必须同答案 |
test/sessions.test.mjs |
229 | 合并视图:一个会话”是什么”的唯一判定处 |
test/store.test.mjs |
422 | 侧车存储、原子写、revision、损坏隔离,以及改它的两个规则模块 |
test/revision.test.mjs |
183 | 乐观锁的两端:侧车拒绝过期写,HTTP 层把 409 送到客户端 |
test/shortcuts.test.mjs |
159 | 快捷方式永不移动目标;主题 store 的往返 |
test/flags.test.mjs |
183 | 收藏/待办标记 → 快捷方式的一次性迁移:幂等、写失败先留旧标记、过期标记不中断启动 |
test/theme.test.mjs |
104 | 主题 store 只交出窗口用的 token,且拒绝注入 |
test/theme-report.test.mjs |
132 | 面板上报的那一半:DSH 把调色板声明在 body 上、值是 var() 引用 |
test/bridge.test.mjs |
143 | 总线的边界:容量、30 秒时效、游标、坏订阅者 |
test/settings.test.mjs |
293 | 设置文档的默认值、合并、归一化;日期格式、大小单位、搜索索引的开关都是封闭词表。旧文档里的 search.indexCache 作为未知键被丢弃,不生效 |
test/settings-page.test.mjs |
1058 | 设置页真实渲染:打开方式、搜索索引组(只剩「空闲时自动建立」一个开关、进度、重建/删除、失败文案)、AI 字段组(模型与强度都是从宿主目录画出来的 <select>、换模型要清掉强度、三个「生成哪几项」开关各自保存、一键补齐的三种答案:入队 / 没活干 / 被拒、累计花费是 token 不是钱)、「关于」组、危险操作组(DELETE 才亮红按钮)、数据目录、显示格式、写请求的 body |
test/display-format.test.mjs |
98 | 两种日期格式与两种大小单位真正打印出来的样子:距今时间只走分钟 / 小时 / 天三档(不出现月、年、「昨天」),固定 KB 不换单位 |
test/session-state.test.mjs |
117 | DSH 自己的界面读的那一个布尔集合 |
test/subagents.test.mjs |
80 | 子智能体绑定四问;fork 带 parentSession 但是独立会话 |
test/trash.test.mjs |
425 | 回收站:删除、恢复阶梯、清空、对账 |
test/move.test.mjs |
523 | 跨 workspace 移动/复制的三件事一个事务 |
test/datadir.test.mjs |
405 | 数据目录迁移:预检、七步、回滚、.location |
test/component.test.mjs |
369 | 组件状态机:单飞锁、复用、校验、pending 清扫 |
test/export.test.mjs |
296 | 会话日志 → Markdown / JSONL:真实形状(time、data.message.content、独立 tool/call 事件、source.kind)才算数——助手回复与工具调用曾经因为读错字段整类消失;注入上下文与系统提示不进正文;压缩摘要与被替换片段只报告、不丢弃 |
test/summary-apply.test.mjs |
103 | 直接驱动 POST /summaries/:id/apply 这个真实路由(用内存侧车代替磁盘):描述是追加在用户备注后面而不是覆盖、重复点两次不会贴两遍、标签只并勾选的那几个并写进标签库、不勾等于全并、过期勾选回 nothing-to-apply、字段名不认识回 bad-request、记录里没有的字段不能应用 |
test/summaries.test.mjs |
921 | AI 字段而不联网:stub 掉 llm 服务,断言送进模型的正文(只有双方发言,思考过程与工具输出从不出现,超长时头 55% 尾 45%)、提示词只列被要的字段(关掉的字段不在契约里)、回答的解析与校验(围栏与前后废话都读得出来、缺字段算缺失、reasoning 块不影响正文、工具调用块旁边的文字照收、只有工具调用则重试后报 llm/tool-only)、“乱答重试一次且两次都记账、两次都败不落盘”、待补清单只看描述与标签(备注抵描述、手写标签抵标签、手改标题不算)、标签先裁到 8 字再去重、生成时先看已有标签库并把它写进提示词、alignTags() 把同义词归到库里已有的写法、mergeNote() 追加而不是覆盖、pickTags() 只并勾选的且过期勾选算拒绝、队列单飞与取消(已写好的保留、一条失败不拖垮整批)、重复入队只算一次、token 账本(失败的尝试也记 runs)、模型目录 TTL 与单 provider 失败隔离、effort 是适配器自己的词所以换模型就丢;每个拒绝码映射到的 HTTP 状态(401 / 409 / 413 / 422 / 429 / 501 / 未知 500) |
test/detached-window.test.mjs |
561 | 独立窗口的页面、路由、冒充 DSH 的 shim,以及 ?lang= 决定的那一半文案 |
test/client-launch.test.mjs |
595 | 两个入口对着 stub 宿主跑;两条”线上才炸”的路径 |
test/row-rename.test.mjs |
185 | 列表行必须打开自己的重命名编辑器,而不是别人的 |
test/row-rail.test.mjs |
169 | 子会话引导线的几何:必须落在上面那个折叠箭头的中心(深度 0 与非 0 是两条式子) |
test/list-containers.test.mjs |
211 | 列表里哪些容器行该出现:搜索放宽到整棵树,筛选变成「含命中」的结果视图;「会话名」关掉后文件夹只能靠装着命中进来;「当前目录」时放宽整棵树这件事也不做 |
test/content-index.test.mjs |
483 | 会话内容索引:按 path|size|activityAt 失效、逐份让出事件循环、两个 work() 只读一份、异步读必须 await、命中数与片段;只索引用户与助手的文字(真实日志形状:工具输出、注入上下文都不该被搜到);磁盘缓存传了 store 就写(没有开关)、写盘后重启不再读日志、坏版本与写不进去都不阻断搜索、没给 store 就只在内存里;空闲构建器只在开关打开时动手 |
test/search-scope.test.mjs |
307 | 搜索范围的位置(所有位置 / 当前目录)与四个字段:默认三项且位置为所有位置、会话内容只看 host 命中、搜索缺省是整库范围、选了当前目录才收窄、弹窗把位置放在字段之上、「搜索索引设置」一键跳到设置页、面板确实把范围传给了列表 |
test/marquee.test.mjs |
519 | 鼠标框选:矩形归一化(反向拖也成立)、任何重叠都算命中而仅相邻不算、零面积等于点击(什么都不选)、集合没变不重报、按在控件上不起框、没选中的行是表面(起框)而已选中的行留给自己拖(rowDraggable)、没有复选框的行不进集合、ctrl 是并集、靠近边缘自动滚动且到顶就停、右键不起框也不会被当成清空、被调用方中途停掉不算点击、列表滚动后不靠 mousemove 也要重算命中、起始点钉在行上而不是屏幕上(滚一格框就跟着长高,滚两格不重复累加)、祖先把面板推走后框仍贴着指针(每帧重读 pane 矩形) |
test/double-press.test.mjs |
48 | 双击时限:同一行两次按下都在 DOUBLE_CLICK_MS 内才算、慢一拍就是两次单击(而且那次慢按成为新的一对的开始)、换一行不配对、一次双击用掉这一对所以第三下重新开始、没有名字的目标永远不算 |
test/drag-set.test.mjs |
67 | 多选拖动的那一次拆分:容器与会话按树和索引分流而不是按 id 前缀、陌生 id 丢掉而不是猜、重复 id 只走一次、容器永远不会被当会话、没有索引时非容器都算会话、空集拆成空集 |
test/quick-access.test.mjs |
81 | 快速访问是一条序列:只有真实容器能当书签、整条一起写、陌生 id 跳过、没动过的书签保留原位 |
test/filter-dialog.test.mjs |
115 | 筛选对话框的两个出口:重置还要按应用,取消是一步;没东西可取消时它不存在 |
test/heads.test.mjs |
109 | 会话 header 扫描:换过格式版本的日志仍找得到、同 id 只出一份、无日志目录跳过 |
test/logfacts.test.mjs |
212 | 日志只答一个问题:有没有过一轮对话,只读头时的窗口边界,以及「一轮没关」不再被当成状态 |
test/annotations.test.mjs |
32 | 注解 id 守卫:__proto__ / constructor / prototype 不能变成幻影条目 |
test/reveal.test.mjs |
237 | reveal 白名单:只开自己列出来的路径(UNC 路径是凭据泄漏,不是便利) |
test/update.test.mjs |
135 | 更新检查:数字段比较、release 解析、5 分钟缓存、失败码而不是假”已是最新” |
test/about.test.mjs |
236 | 链接白名单与降级链,以及缓存清理只清自己的(quarantine 与会话不动,组件在装就跳过) |
test/locale-parity.test.mjs |
88 | 中英两份字典键集合一致、占位符一致、英文标签放得下;设置页的 token 块必须自带 --sm-danger |
test/panel-copy.test.mjs |
189 | 面板每一句中文都有英文对照:字典覆盖全部 T()、没有裸中文残留(注释与样式表除外)、占位符不丢;固定目录与默认工作区也走字典;shim 同理(例外是 L()) |
test/reset.test.mjs |
208 | 清除数据:一个字都没动之前先验 DELETE、五份侧车与回收站本体一起删、组件与数据目录本身留下、迁移路径写回 |
test/projcache.test.mjs |
156 | 投影缓存的复用键(size + mtime):没动过的文件不读第二遍,而任何改动都得被发现——最硬的一条是第二次写入等长内容并把旧 mtime 放回去,只信键的读取器仍会答出旧标题;另有 reader 单飞与清缓存 |
test/workspace-bands.test.mjs |
107 | 「所有会话」按工作区切带:行落到哪个工作区(子会话跟父会话)、带序按树 DFS(含拖进用户文件夹的工作区仍留在文件夹位置)、树不认识的工作区排在已知之后、没有工作区的沉底、标题取工作区名字而视图过期时回落到 id |
test/render-smoke.mjs |
1513 | 每个组件真实渲染一次(不是 node --test 的一部分);详情栏的「AI 字段」段必须紧跟在「备注」之后并带着那一枚按钮,存了字段之后要画出标题/描述/标签三行、每个标签都是已勾选的开关并写着「合并所选(2)」;框选那窗 .selrect 必须在树里、空白处按下才起框;没选中的会话/文件夹/工作区行 draggable 必须是 false,选中或勾选后仍是 true;真起一次拖动后,勾选集(会话与容器两个半边)必须原样交到落点,浏览器读不到 DataTransfer 时仍从载荷里答;双击打开落在第二次按下上(一次按下只能选中);状态点与 DSH 同一阶梯(空闲 / 进行中 / 等待审批 / 计划待审 / 等待回答 / 已完成 / 子智能体运行中),「已中断」「异常」两个自造档位不许回来 |
规则和 IO 分开测。 lib/domain/* 全是纯函数,可以脱离内核和文件系统断言;
lib/host/* 才碰磁盘,用临时目录。test/tree.test.mjs 的头注释说得很直接:
树投影是 P1 里唯一”编码规则而不是 IO”的部分,所以它是唯一值得用断言钉死的部分。
“三份规则”是被测试记住的教训。 test/tree-mirror.test.mjs 存在的原因是曾经有三份
“节点能去哪”的规则(lib/domain/tree.js、lib/client.js、已删除的静态壳),它们会漂移。
现在客户端只允许持有遍历(子树、父链),任何决策表都必须来自 host 算好的字段。
线上才炸的东西单独钉。 test/client-launch.test.mjs 的头注释写明:这里钉的两件事
都是”在现场炸的,不是在测试里炸的”。比如独立弹窗模式下双击窗口里的会话——bridgeActions
为 null 时请求会静默消失,因为面板根本没挂载。这类 bug 只有对着 stub 宿主跑一遍入口才会露出来。
UI 也要真渲染。 render-smoke.mjs 对每个组件渲染一次,用假的 hooks 返回值。
test/row-rename.test.mjs 补的正是它漏掉的一类:渲染一次看不出”点第二行却打开了第一行的
编辑器”这种交互错误。
DSH_SE_DATA_DIR 或直接把 paths 传进 openDocuments();mirrorOverride / pinnedOverride 之类注入,不会真的去下 150 MB;render-smoke.mjs 自带假 document,不依赖 jsdom。npm test && npm run test:render
两条都过才算过。render-smoke 不在 npm test 里,因为它比断言慢得多,而它检查的是
“渲染不抛异常”这一件事。