zen-gitsync 2.17.16 → 2.17.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/paths.js +13 -0
  4. package/src/ui/public/assets/AgentView-AMFKSDeH.css +1 -0
  5. package/src/ui/public/assets/AgentView-DQAB0ypS.js +13 -0
  6. package/src/ui/public/assets/AppVersionBadge-B5VG1_Lu.js +2 -0
  7. package/src/ui/public/assets/AppVersionBadge-DDYQqoy7.css +1 -0
  8. package/src/ui/public/assets/BranchSelector-Cj6athRO.js +1 -0
  9. package/src/ui/public/assets/BranchSelector-F4ibldMN.css +1 -0
  10. package/src/ui/public/assets/CommitForm-4kI2csgs.css +1 -0
  11. package/src/ui/public/assets/CommitForm-a2tMsfF9.js +6 -0
  12. package/src/ui/public/assets/CommonDialog-I-GCqRMC.js +1 -0
  13. package/src/ui/public/assets/CommonDialog-w6EmBEXv.css +1 -0
  14. package/src/ui/public/assets/EditorView-DBqhjm84.css +1 -0
  15. package/src/ui/public/assets/EditorView-wTi53Nxc.js +16 -0
  16. package/src/ui/public/assets/FileDiffViewer-B7i4BmbH.js +15 -0
  17. package/src/ui/public/assets/FileDiffViewer-n5ea9qMe.css +1 -0
  18. package/src/ui/public/assets/{FlowExecutionViewer-Drw4hlJG.js → FlowExecutionViewer-xZjrf8e1.js} +1 -1
  19. package/src/ui/public/assets/FlowOrchestrationWorkspace-DwuSSxJ1.css +1 -0
  20. package/src/ui/public/assets/FlowOrchestrationWorkspace-R0QKmUpG.js +21 -0
  21. package/src/ui/public/assets/IconButton-B6eqvssJ.js +1 -0
  22. package/src/ui/public/assets/IconButton-RlXqKOeu.css +1 -0
  23. package/src/ui/public/assets/LogList-BPqurE9g.js +7 -0
  24. package/src/ui/public/assets/LogList-XspUhx-C.css +1 -0
  25. package/src/ui/public/assets/MindmapView-Diq2Cu4O.css +1 -0
  26. package/src/ui/public/assets/MindmapView-NdFNlkTF.js +2 -0
  27. package/src/ui/public/assets/MonitorView-DuSZa8a3.js +1 -0
  28. package/src/ui/public/assets/MonitorView-DwUwJXnY.css +1 -0
  29. package/src/ui/public/assets/ProjectStartupButton-BrfKVP-w.js +1 -0
  30. package/src/ui/public/assets/ProjectStartupButton-CB8IOg1E.css +1 -0
  31. package/src/ui/public/assets/RemoteManagerDialog-BI7fRsOQ.js +4 -0
  32. package/src/ui/public/assets/RemoteManagerDialog-D4WphMtO.css +1 -0
  33. package/src/ui/public/assets/RemoteRepoCard-B4E8Ud3F.js +7 -0
  34. package/src/ui/public/assets/RemoteRepoCard-o01IrbVz.css +1 -0
  35. package/src/ui/public/assets/SourceMapView-B0OzHjxP.js +3 -0
  36. package/src/ui/public/assets/SourceMapView-CfDydtsu.css +1 -0
  37. package/src/ui/public/assets/SvgIcon-B2h7B2yF.js +1 -0
  38. package/src/ui/public/assets/TemplateManager-DO7EPfj8.css +1 -0
  39. package/src/ui/public/assets/TemplateManager-mIWE3Qf8.js +1 -0
  40. package/src/ui/public/assets/UserInputNode-BafC-B2U.js +1 -0
  41. package/src/ui/public/assets/UserInputNode-DDNSarag.css +1 -0
  42. package/src/ui/public/assets/WorkbenchView-BU2kJCAA.css +1 -0
  43. package/src/ui/public/assets/WorkbenchView-BseGz8hJ.js +56 -0
  44. package/src/ui/public/assets/{_plugin-vue_export-helper-dg2VUnSQ.js → _plugin-vue_export-helper-Dvuzpv2Y.js} +7 -7
  45. package/src/ui/public/assets/agentConversations-BEKqkGcm.js +5 -0
  46. package/src/ui/public/assets/{configStore-DtjAc3w9.js → configStore-CX0FkQ94.js} +1 -1
  47. package/src/ui/public/assets/element-plus-C1xzgNSR.css +1 -0
  48. package/src/ui/public/assets/{element-plus-DxBPQwL7.js → element-plus-D7LDHcAn.js} +3 -3
  49. package/src/ui/public/assets/flow-mindmap-zXXF3qDh.js +139 -0
  50. package/src/ui/public/assets/gitStore-CMuWWr2d.js +6 -0
  51. package/src/ui/public/assets/index-CB2cIiHl.js +115 -0
  52. package/src/ui/public/assets/index-DIU5200G.css +1 -0
  53. package/src/ui/public/assets/{office-docx-CZXLkCf5.js → office-docx-C8sYW8O1.js} +5 -5
  54. package/src/ui/public/assets/{office-excel-CBKszjVA.js → office-excel-DpZn5My2.js} +1 -1
  55. package/src/ui/public/assets/{office-pptx-NeUSnN3k.js → office-pptx-BnYBTwT7.js} +1 -1
  56. package/src/ui/public/assets/previewSandbox-CxLaNg4X.css +1 -0
  57. package/src/ui/public/assets/previewSandbox-DxR4iN9P.js +0 -0
  58. package/src/ui/public/assets/toolsStore-DMCSd287.js +1 -0
  59. package/src/ui/public/assets/{useThemeObserver-E7Hk50l3.js → useThemeObserver-QpveGpnn.js} +1 -1
  60. package/src/ui/public/assets/{vendor-DnB9EJa3.css → vendor-C-GCxMMu.css} +1 -1
  61. package/src/ui/public/assets/{vendor-bdHhLiED.js → vendor-CLmPUj3K.js} +8 -8
  62. package/src/ui/public/assets/vue-flow-CxnoC7Uq.js +39 -0
  63. package/src/ui/public/fonts/jetbrains-mono-latin.woff2 +0 -0
  64. package/src/ui/public/fonts/plus-jakarta-sans-latin-ext.woff2 +0 -0
  65. package/src/ui/public/fonts/plus-jakarta-sans-latin-italic.woff2 +0 -0
  66. package/src/ui/public/fonts/plus-jakarta-sans-latin.woff2 +0 -0
  67. package/src/ui/public/index.html +28 -27
  68. package/src/ui/server/index.js +28 -2
  69. package/src/ui/server/routes/aiContext/collectors.js +360 -0
  70. package/src/ui/server/routes/aiContext/collectors.test.js +297 -0
  71. package/src/ui/server/routes/aiContext/index.js +444 -0
  72. package/src/ui/server/routes/aiContext/index.test.js +433 -0
  73. package/src/ui/server/routes/aiContext/jobRefresh.js +110 -0
  74. package/src/ui/server/routes/aiContext/jobRefresh.test.js +191 -0
  75. package/src/ui/server/routes/aiContext/render.js +366 -0
  76. package/src/ui/server/routes/aiContext/render.test.js +255 -0
  77. package/src/ui/server/routes/aiContext/wiring.js +80 -0
  78. package/src/ui/server/routes/fs.js +43 -0
  79. package/src/ui/server/routes/mindmap.js +36 -24
  80. package/src/ui/server/routes/monitor.js +44 -35
  81. package/src/ui/server/routes/remoteRepos.js +57 -48
  82. package/src/ui/server/routes/workbench/agentChat.js +94 -10
  83. package/src/ui/server/routes/workbench/agentChatContext.test.js +171 -0
  84. package/src/ui/server/routes/workbench/agentChatShared.test.js +33 -3
  85. package/src/ui/server/routes/workbench/agentEngines.js +77 -0
  86. package/src/ui/server/routes/workbench/agentEngines.test.js +65 -0
  87. package/src/ui/server/routes/workbench/agentRoutes.js +208 -48
  88. package/src/ui/server/routes/workbench/agentRoutes.test.js +160 -4
  89. package/src/ui/server/routes/workbench/agentSessionStore.js +3 -0
  90. package/src/ui/server/routes/workbench/envContext.js +61 -64
  91. package/src/ui/server/routes/workbench/envContext.test.js +133 -63
  92. package/src/ui/server/routes/workbench/index.js +78 -32
  93. package/src/ui/server/routes/workbench/runExternalTurn.js +279 -0
  94. package/src/ui/server/routes/workbench/runExternalTurn.test.js +275 -0
  95. package/src/ui/server/routes/workbench/shared.js +17 -0
  96. package/src/ui/server/utils/agentAttachments.js +169 -0
  97. package/src/ui/server/utils/agentAttachments.test.js +140 -0
  98. package/src/ui/public/assets/AgentView-DOyN97Ve.js +0 -5
  99. package/src/ui/public/assets/AgentView-DkM2KXuc.css +0 -1
  100. package/src/ui/public/assets/AppVersionBadge-CJrDr7GV.css +0 -1
  101. package/src/ui/public/assets/AppVersionBadge-oON3sAj4.js +0 -2
  102. package/src/ui/public/assets/BranchSelector-B0eLkAdv.css +0 -1
  103. package/src/ui/public/assets/BranchSelector-Cpj7eGqY.js +0 -1
  104. package/src/ui/public/assets/CommitForm-CQ0K8Yy2.css +0 -1
  105. package/src/ui/public/assets/CommitForm-D_ovYhVw.js +0 -6
  106. package/src/ui/public/assets/CommonDialog-ChZbqfRF.js +0 -1
  107. package/src/ui/public/assets/CommonDialog-jP0E9mco.css +0 -1
  108. package/src/ui/public/assets/EditorView-BPzpI0GV.js +0 -1
  109. package/src/ui/public/assets/EditorView-DZMLvQds.css +0 -1
  110. package/src/ui/public/assets/FileDiffViewer-B9EkuKdF.css +0 -1
  111. package/src/ui/public/assets/FileDiffViewer-B_qEKA9L.js +0 -15
  112. package/src/ui/public/assets/FlowOrchestrationWorkspace-C7yTD1gj.js +0 -21
  113. package/src/ui/public/assets/FlowOrchestrationWorkspace-CYGp-PqS.css +0 -1
  114. package/src/ui/public/assets/IconButton-DEARmMkZ.js +0 -1
  115. package/src/ui/public/assets/IconButton-tjkOjzkC.css +0 -1
  116. package/src/ui/public/assets/LogList-BA_gowxE.js +0 -7
  117. package/src/ui/public/assets/LogList-BlJplIi1.css +0 -1
  118. package/src/ui/public/assets/MindmapView-8X6z245q.css +0 -1
  119. package/src/ui/public/assets/MindmapView-K-XcLLXr.js +0 -2
  120. package/src/ui/public/assets/MonitorView-C0RL5K9W.js +0 -1
  121. package/src/ui/public/assets/MonitorView-DetlFFXq.css +0 -1
  122. package/src/ui/public/assets/ProjectStartupButton-DC36_vvU.js +0 -1
  123. package/src/ui/public/assets/ProjectStartupButton-QLPltCK5.css +0 -1
  124. package/src/ui/public/assets/RemoteManagerDialog-BauAeH2w.css +0 -1
  125. package/src/ui/public/assets/RemoteManagerDialog-Bkqja660.js +0 -4
  126. package/src/ui/public/assets/RemoteRepoCard-Bsm7cj_H.js +0 -7
  127. package/src/ui/public/assets/RemoteRepoCard-CG-jARTe.css +0 -1
  128. package/src/ui/public/assets/SourceMapView-CN4Hkyp4.css +0 -1
  129. package/src/ui/public/assets/SourceMapView-Y5NKdIk6.js +0 -3
  130. package/src/ui/public/assets/SvgIcon-D_mFHXP4.js +0 -1
  131. package/src/ui/public/assets/TemplateManager-BzKVXZhy.js +0 -1
  132. package/src/ui/public/assets/TemplateManager-DMuG4i3G.css +0 -1
  133. package/src/ui/public/assets/UserInputNode-BPLK4aYx.js +0 -1
  134. package/src/ui/public/assets/UserInputNode-BVgQkB2l.css +0 -1
  135. package/src/ui/public/assets/WorkbenchView-B1BJhWHi.css +0 -1
  136. package/src/ui/public/assets/WorkbenchView-xbGdiVMQ.js +0 -56
  137. package/src/ui/public/assets/element-plus-Cwwq-Gje.css +0 -1
  138. package/src/ui/public/assets/flow-mindmap-BqFb1WO5.js +0 -139
  139. package/src/ui/public/assets/gitStore-BZo9_zyy.js +0 -6
  140. package/src/ui/public/assets/index-CHuCYLWm.js +0 -111
  141. package/src/ui/public/assets/index-CmbatbSR.css +0 -1
  142. package/src/ui/public/assets/previewSandbox-BTTTXooN.js +0 -0
  143. package/src/ui/public/assets/previewSandbox-CgdwPcuW.css +0 -1
  144. package/src/ui/public/assets/vue-flow-fLUQ0_kE.js +0 -39
@@ -0,0 +1,297 @@
1
+ // aiContext/collectors.js 单元测试。
2
+ //
3
+ // IO 全部走注入的假实现,不联网、不 spawn、不碰真实用户数据。
4
+ // 重点覆盖的是**误读路径**:CLI 没装 / 没登录 / 拉取报错时,
5
+ // 快照文件必须把"取不到"写清楚,绝不能留一片空白 —— 空白会被模型读成"用户没有仓库"。
6
+ import { test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import {
9
+ TASKS_FILE_LIMIT,
10
+ collectCustomCommands,
11
+ collectGit,
12
+ collectMindmap,
13
+ collectRemote,
14
+ collectSystem,
15
+ collectWorkbenchTasks,
16
+ } from './collectors.js';
17
+ import { TASK_COLUMNS } from '../workbench/projectRegistry.js';
18
+
19
+ // ── ① Git ────────────────────────────────────────────────────
20
+ test('collectGit:干净仓库 → 摘要点明"工作区干净"', async () => {
21
+ const r = await collectGit({
22
+ repoPath: 'D:/ws/x',
23
+ probe: async () => ({ exists: true, isGitRepo: true, branch: 'main', upstream: 'origin/main', hasUpstream: true, ahead: 0, behind: 0, changed: 0, staged: 0, unstaged: 0, untracked: 0 }),
24
+ });
25
+ assert.equal(r.ok, true);
26
+ assert.match(r.summary, /分支 main/);
27
+ assert.match(r.summary, /工作区干净/);
28
+ assert.match(r.lines.join('\n'), /未提交改动: 0/);
29
+ });
30
+
31
+ test('collectGit:有改动 / 非仓库 / 探测抛错 三条降级路径', async () => {
32
+ const dirty = await collectGit({
33
+ repoPath: 'D:/ws/x',
34
+ probe: async () => ({ exists: true, isGitRepo: true, branch: 'dev', hasUpstream: false, changed: 3, staged: 1, unstaged: 1, untracked: 1 }),
35
+ });
36
+ assert.equal(dirty.ok, true);
37
+ assert.match(dirty.summary, /未提交 3 个文件/);
38
+ assert.match(dirty.summary, /无上游分支/);
39
+
40
+ const notRepo = await collectGit({ repoPath: 'D:/ws/x', probe: async () => ({ exists: true, isGitRepo: false }) });
41
+ assert.equal(notRepo.ok, true);
42
+ assert.match(notRepo.summary, /不是 git 仓库/);
43
+
44
+ const boom = await collectGit({ repoPath: 'D:/ws/x', probe: async () => { throw new Error('git 不存在'); } });
45
+ assert.equal(boom.ok, false);
46
+ assert.match(boom.error, /git 不存在/);
47
+ });
48
+
49
+ test('collectGit:没有项目路径时不硬编造,直接标失败', async () => {
50
+ const r = await collectGit({ repoPath: '', probe: async () => ({}) });
51
+ assert.equal(r.ok, false);
52
+ assert.match(r.error, /未拿到当前项目路径/);
53
+ });
54
+
55
+ // ── ②③ 远程仓库 ──────────────────────────────────────────────
56
+ test('collectRemote:CLI 没装 → 必须显式写明"取不到",且点明不是"没有仓库"', () => {
57
+ const r = collectRemote({ provider: 'github', state: { installed: false } });
58
+ assert.equal(r.ok, true);
59
+ assert.match(r.summary, /未检测到 gh CLI/);
60
+ const body = r.lines.join('\n');
61
+ assert.match(body, /未安装/);
62
+ assert.match(body, /不是"用户没有仓库"/);
63
+ });
64
+
65
+ test('collectRemote:装了没登录 → 给出登录命令', () => {
66
+ const r = collectRemote({ provider: 'gitee', state: { installed: true, version: '0.3.1', authenticated: false } });
67
+ assert.match(r.summary, /未登录/);
68
+ const body = r.lines.join('\n');
69
+ assert.match(body, /gitee auth login/);
70
+ assert.match(body, /不要索要 token/);
71
+ });
72
+
73
+ test('collectRemote:已登录 → 列仓库,并把拉取报错带出来', () => {
74
+ const r = collectRemote({
75
+ provider: 'github',
76
+ state: {
77
+ installed: true, version: '2.101.0', authenticated: true, user: 'xz333221', truncated: false,
78
+ repos: [
79
+ { name: 'zen-gitsync', visibility: 'public', language: 'JavaScript', updatedAt: '2026-09-27T10:00:00Z', description: 'Git GUI' },
80
+ { name: 'flow-mindmap', visibility: 'private' },
81
+ ],
82
+ },
83
+ });
84
+ assert.match(r.summary, /2 个仓库/);
85
+ assert.match(r.summary, /账号 xz333221/);
86
+ assert.match(r.summary, /如 zen-gitsync、flow-mindmap/, '摘要要带前几个仓库名 —— 用户问的就是"有哪些项目"');
87
+ const body = r.lines.join('\n');
88
+ assert.match(body, /- zen-gitsync \| public \| JavaScript \| 更新于 2026-09-27/);
89
+ assert.match(body, /- flow-mindmap \| private/);
90
+
91
+ const withError = collectRemote({
92
+ provider: 'github',
93
+ state: { installed: true, authenticated: true, user: 'u', repos: [], error: '网络超时' },
94
+ });
95
+ assert.match(withError.lines.join('\n'), /上次拉取报错: 网络超时(清单可能不完整)/);
96
+ });
97
+
98
+ test('collectRemote:空清单要写明"该账号下没有可见仓库",而不是留空', () => {
99
+ const r = collectRemote({ provider: 'github', state: { installed: true, authenticated: true, user: 'u', repos: [] } });
100
+ assert.match(r.lines.join('\n'), /该账号下没有可见仓库/);
101
+ // 没有仓库时不许留下"(如 等)"这种半截预览 —— 那会被读成"有仓库但名字是空的"
102
+ assert.ok(!r.summary.includes('如 '), `空清单的摘要不该有预览: ${r.summary}`);
103
+ });
104
+
105
+ test('collectRemote:摘要预览最多带 REPOS_SUMMARY_PREVIEW 个,超出用"等"收尾', () => {
106
+ const repos = Array.from({ length: 12 }, (_, i) => ({ name: `repo-${String(i).padStart(2, '0')}` }));
107
+ const r = collectRemote({ provider: 'gitee', state: { installed: true, authenticated: true, user: 'xuze333221', repos } });
108
+ assert.match(r.summary, /12 个仓库/);
109
+ assert.match(r.summary, /如 repo-00、repo-01、repo-02、repo-03、repo-04 等/);
110
+ assert.ok(!r.summary.includes('repo-05'), '第 6 个开始就不该出现在摘要里(全量在文件里)');
111
+ // 摘要天生要短:注入块每次对话都带着它,不能变成仓库清单的副本
112
+ assert.ok(r.summary.length <= 200, `摘要 ${r.summary.length} 字符,太长了`);
113
+ });
114
+
115
+ // ── ④ 自定义命令 ─────────────────────────────────────────────
116
+ test('collectCustomCommands:列出名称与命令,空配置给出明确说明', () => {
117
+ const r = collectCustomCommands({
118
+ config: { customCommands: [{ id: '1', name: 'dev', command: 'npm run dev', description: '起开发服务' }] },
119
+ });
120
+ assert.equal(r.ok, true);
121
+ assert.equal(r.summary, '共 1 条');
122
+ assert.match(r.lines[0], /- dev → `npm run dev` — 起开发服务/);
123
+
124
+ const empty = collectCustomCommands({ config: {} });
125
+ assert.equal(empty.summary, '共 0 条');
126
+ assert.match(empty.lines[0], /没有配置自定义命令/);
127
+ });
128
+
129
+ // ── ⑤ 工作台任务 ─────────────────────────────────────────────
130
+ test('collectWorkbenchTasks:统计口径与看板一致(复用的就是同一份实现)', () => {
131
+ const tasks = [
132
+ { id: 't1', title: '待办任务', projectPath: 'D:/ws/a', updatedAt: '2026-09-28T10:00:00Z' },
133
+ { id: 't2', title: '跑着的任务', description: '描述内容', projectPath: 'D:/ws/a', updatedAt: '2026-09-28T11:00:00Z' },
134
+ { id: 't3', title: '做完了', projectPath: 'D:/ws/b', updatedAt: '2026-09-27T10:00:00Z' },
135
+ ];
136
+ const jobs = [
137
+ { id: 'j1', taskId: 't2', status: 'running', startedAt: '2026-09-28T11:00:00Z' },
138
+ { id: 'j2', taskId: 't3', status: 'done', endedAt: '2026-09-27T11:00:00Z' },
139
+ ];
140
+ const r = collectWorkbenchTasks({ tasks, jobs, recentDirs: ['D:/ws/a'] });
141
+ assert.equal(r.ok, true);
142
+ assert.match(r.summary, /全部项目合计 3 条 —— 待处理 1 \/ 进行中 1 \/ 已完成 1/);
143
+ assert.match(r.summary, /正在执行 1 个/);
144
+
145
+ const body = r.lines.join('\n');
146
+ assert.match(body, /## 项目清单(共 2 个)/);
147
+ assert.match(body, /## 任务明细(共 3 条)/);
148
+ assert.match(body, /\[跑着的任务\]/);
149
+ assert.match(body, /最近执行 running/);
150
+ assert.match(body, /描述内容/);
151
+ });
152
+
153
+ test('collectWorkbenchTasks:无任务时也给出完整骨架(不返回 undefined 字段)', () => {
154
+ const r = collectWorkbenchTasks({});
155
+ assert.equal(r.ok, true);
156
+ assert.match(r.summary, /全部项目合计 0 条/);
157
+ assert.match(r.summary, /当前无执行中的任务/);
158
+ });
159
+
160
+ test('collectWorkbenchTasks:任务过多时截断并写明还有多少条', () => {
161
+ const tasks = Array.from({ length: TASKS_FILE_LIMIT + 5 }, (_, i) => ({
162
+ id: `t${i}`, title: `任务${i}`, projectPath: 'D:/ws/a', updatedAt: `2026-09-28T${String(i % 24).padStart(2, '0')}:00:00Z`,
163
+ }));
164
+ const r = collectWorkbenchTasks({ tasks });
165
+ assert.match(r.lines.join('\n'), /…还有 5 条未列出/);
166
+ });
167
+
168
+ // ── tasks 板块的结构化 data:envContext(编排台视角)唯一的数据来源 ──────
169
+ //
170
+ // 这一条守的是"别再长出第二份实现":项目清单 / 各列计数 / 合计只算一遍,
171
+ // 结构化结果随板块一起交出去,envContext 拿它渲染,而不再自己读一遍 tasks.json。
172
+ test('collectWorkbenchTasks:带上结构化 data(项目/计数/合计只在算一遍)', () => {
173
+ const tasks = [
174
+ { id: 't1', projectPath: 'D:/ws/a' },
175
+ { id: 't2', projectPath: 'D:/ws/a' },
176
+ { id: 't3', projectPath: 'D:/ws/b' },
177
+ { id: 't4', projectPath: '' }, // 无归属 → 只进合计,不造项目行
178
+ ];
179
+ const jobs = [
180
+ { id: 'j1', taskId: 't3', status: 'done', endedAt: '2026-09-27T11:00:00Z' },
181
+ { id: 'j2', taskId: 't1', status: 'running', startedAt: '2026-09-28T11:00:00Z' },
182
+ ];
183
+ const r = collectWorkbenchTasks({ tasks, jobs, recentDirs: ['D:/ws/a'] });
184
+
185
+ assert.ok(r.data, '必须带 data');
186
+ // 必须是**纯对象**:它会跟着快照缓存一路传下去,Map 一旦被 JSON.stringify 就静默变 {}
187
+ assert.equal(r.data.statsByKey instanceof Map, false);
188
+ assert.equal(typeof r.data.statsByKey, 'object');
189
+
190
+ // 项目:a 来自任务 + 最近目录,b 只来自任务
191
+ assert.equal(r.data.projects.length, 2);
192
+ // 合计把无归属的 t4 也算上
193
+ assert.equal(r.data.total.total, 4);
194
+ assert.equal(r.data.runningJobs, 1);
195
+ assert.equal(r.data.taskCount, 4);
196
+
197
+ // data 与摘要同源:合计那一段文字里的数字必须能对上
198
+ const [, todo, doing, done] = (r.summary.match(/待处理 (\d+) \/ 进行中 (\d+) \/ 已完成 (\d+)/) || []).map(Number);
199
+ assert.equal(r.data.total.todo, todo);
200
+ assert.equal(r.data.total.doing, doing);
201
+ assert.equal(r.data.total.done, done);
202
+
203
+ // statsByKey 求和 == total(结构性自洽)
204
+ const sum = { total: 0 };
205
+ for (const k of TASK_COLUMNS) sum[k] = 0;
206
+ for (const s of Object.values(r.data.statsByKey)) {
207
+ sum.total += s.total || 0;
208
+ for (const k of TASK_COLUMNS) sum[k] += s[k] || 0;
209
+ }
210
+ assert.equal(sum.total, r.data.total.total);
211
+ for (const k of TASK_COLUMNS) assert.equal(sum[k], r.data.total[k]);
212
+ });
213
+
214
+ test('collectWorkbenchTasks:失败/无数据时 data 缺席(envContext 据此降级)', () => {
215
+ // 空入参也要给出 data(不是 undefined),否则 envContext 会误判成"取数失败"
216
+ const r = collectWorkbenchTasks({});
217
+ assert.ok(r.data);
218
+ assert.deepEqual(r.data.projects, []);
219
+ assert.equal(r.data.total.total, 0);
220
+ assert.equal(r.data.runningJobs, 0);
221
+
222
+ // 其余板块不该被塞 data —— 只有 tasks 需要
223
+ assert.equal(collectCustomCommands({ config: {} }).data, undefined);
224
+ assert.equal(collectSystem({}).data, undefined);
225
+ });
226
+
227
+ // ── ⑥ 系统状态 ───────────────────────────────────────────────
228
+ test('collectSystem:CPU / 内存 / 磁盘 / 端口都要落到文件里', () => {
229
+ const r = collectSystem({
230
+ overview: {
231
+ cpu: { usage: 7.72, cores: 16, model: 'i9' },
232
+ memory: { total: 32 * 1024 ** 3, used: 20 * 1024 ** 3, usagePercent: 62.3 },
233
+ disks: { total: 1, free: 1, used: 1, usagePercent: 1, drives: [{ mount: 'C:', total: 952 * 1024 ** 3, used: 173.9 * 1024 ** 3, usagePercent: 18.2 }] },
234
+ system: { hostname: 'XUZE-QICAHONG', platform: 'win32', arch: 'x64', nodeVersion: 'v24.20.0' },
235
+ },
236
+ ports: [{ protocol: 'TCP', localAddress: '127.0.0.1', port: 4502, pid: 27088, process: 'node.exe' }],
237
+ });
238
+ assert.equal(r.ok, true);
239
+ assert.match(r.summary, /CPU 7\.7%/);
240
+ assert.match(r.summary, /内存 62\.3%/);
241
+ assert.match(r.summary, /1 个监听端口/);
242
+ const body = r.lines.join('\n');
243
+ assert.match(body, /- CPU 占用: 7\.7%(16 核 · i9)/);
244
+ assert.match(body, /- C: 已用 173\.9GB/);
245
+ assert.match(body, /TCP 127\.0\.0\.1:4502 PID 27088 node\.exe/);
246
+ });
247
+
248
+ test('collectSystem:磁盘为 null(查询失败降级)时不炸', () => {
249
+ const r = collectSystem({ overview: { cpu: {}, memory: {}, disks: null, system: {} }, ports: [] });
250
+ assert.equal(r.ok, true);
251
+ assert.match(r.lines.join('\n'), /没有查到监听端口/);
252
+ });
253
+
254
+ test('collectSystem:拿不到概览 → 标失败', () => {
255
+ const r = collectSystem({});
256
+ assert.equal(r.ok, false);
257
+ assert.match(r.error, /未拿到系统概览/);
258
+ });
259
+
260
+ // ── ⑦ 思维导图 ───────────────────────────────────────────────
261
+ test('collectMindmap:按目录分组列出导图,单目录失败不影响其它目录', async () => {
262
+ const r = await collectMindmap({
263
+ dirs: ['D:/ws/mm', 'D:/gone'],
264
+ listDir: async (dir) => {
265
+ if (dir === 'D:/gone') throw new Error('目录不存在');
266
+ return [{ title: '智能体', name: '智能体.mindmap.json', size: 2048, mtime: Date.parse('2026-09-08T00:41:00Z') }];
267
+ },
268
+ });
269
+ assert.equal(r.ok, true);
270
+ assert.match(r.summary, /2 个目录 · 1 个导图(1 个目录读取失败)/);
271
+ const body = r.lines.join('\n');
272
+ assert.match(body, /## D:\/ws\/mm(1 个)/);
273
+ assert.match(body, /- 智能体 \| 2026-09-08/);
274
+ assert.match(body, /- D:\/gone: 目录不存在/);
275
+ });
276
+
277
+ test('collectMindmap:目录全部读不到 → 整块判失败,不能报成"0 个导图"', async () => {
278
+ const r = await collectMindmap({
279
+ dirs: ['D:/gone', 'D:/gone2'],
280
+ listDir: async () => { throw new Error('目录不存在'); },
281
+ });
282
+ assert.equal(r.ok, false, '全失败必须是失败态,否则会被读成"没有思维导图"');
283
+ assert.match(r.error, /全部 2 个目录都读取失败/);
284
+ assert.match(r.lines.join('\n'), /- D:\/gone: 目录不存在/, '细节仍要留在文件里');
285
+ });
286
+
287
+ test('collectMindmap:没配目录时给明确说明,不是空白', async () => {
288
+ const r = await collectMindmap({ dirs: [] });
289
+ assert.equal(r.ok, true);
290
+ assert.match(r.summary, /未配置思维导图目录/);
291
+ });
292
+
293
+ test('collectMindmap:目录为空时也说明"这个目录下没有"', async () => {
294
+ const r = await collectMindmap({ dirs: ['D:/ws/mm'], listDir: async () => [] });
295
+ assert.match(r.summary, /1 个目录 · 0 个导图/);
296
+ assert.match(r.lines.join('\n'), /这个目录下没有 \.mindmap\.json/);
297
+ });