zen-gitsync 2.17.17 → 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 (62) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/src/paths.js +10 -0
  4. package/src/ui/public/assets/{AgentView-CXZUWLPL.css → AgentView-AMFKSDeH.css} +1 -1
  5. package/src/ui/public/assets/AgentView-DQAB0ypS.js +13 -0
  6. package/src/ui/public/assets/{AppVersionBadge-D70AjIBo.js → AppVersionBadge-B5VG1_Lu.js} +2 -2
  7. package/src/ui/public/assets/{BranchSelector-t3fnGS_5.js → BranchSelector-Cj6athRO.js} +1 -1
  8. package/src/ui/public/assets/{CommitForm-DmlTnTwc.js → CommitForm-a2tMsfF9.js} +1 -1
  9. package/src/ui/public/assets/{CommonDialog-CecgvEzd.js → CommonDialog-I-GCqRMC.js} +1 -1
  10. package/src/ui/public/assets/EditorView-wTi53Nxc.js +16 -0
  11. package/src/ui/public/assets/{FileDiffViewer-DH8Y2lUO.js → FileDiffViewer-B7i4BmbH.js} +1 -1
  12. package/src/ui/public/assets/{FlowExecutionViewer-Cie2_TG4.js → FlowExecutionViewer-xZjrf8e1.js} +1 -1
  13. package/src/ui/public/assets/{FlowOrchestrationWorkspace-Bbf8X1KP.js → FlowOrchestrationWorkspace-R0QKmUpG.js} +1 -1
  14. package/src/ui/public/assets/{IconButton-BEiZI4F6.js → IconButton-B6eqvssJ.js} +1 -1
  15. package/src/ui/public/assets/{LogList-DvuC03Tx.js → LogList-BPqurE9g.js} +1 -1
  16. package/src/ui/public/assets/{MindmapView-B6rITyNs.js → MindmapView-NdFNlkTF.js} +1 -1
  17. package/src/ui/public/assets/{MonitorView-q92EZvay.js → MonitorView-DuSZa8a3.js} +1 -1
  18. package/src/ui/public/assets/{ProjectStartupButton-RiIi8TwW.js → ProjectStartupButton-BrfKVP-w.js} +1 -1
  19. package/src/ui/public/assets/{RemoteManagerDialog-qtBotYAQ.js → RemoteManagerDialog-BI7fRsOQ.js} +1 -1
  20. package/src/ui/public/assets/{RemoteRepoCard-DvtzfwHV.js → RemoteRepoCard-B4E8Ud3F.js} +1 -1
  21. package/src/ui/public/assets/{SourceMapView-EEWAmy4U.js → SourceMapView-B0OzHjxP.js} +1 -1
  22. package/src/ui/public/assets/{SvgIcon-Bkc_oGUI.js → SvgIcon-B2h7B2yF.js} +1 -1
  23. package/src/ui/public/assets/{TemplateManager-9e4amMlI.js → TemplateManager-mIWE3Qf8.js} +1 -1
  24. package/src/ui/public/assets/{UserInputNode-4FoJblJi.js → UserInputNode-BafC-B2U.js} +1 -1
  25. package/src/ui/public/assets/{WorkbenchView-BFaTinH8.js → WorkbenchView-BseGz8hJ.js} +5 -5
  26. package/src/ui/public/assets/{_plugin-vue_export-helper-BLGowG5t.js → _plugin-vue_export-helper-Dvuzpv2Y.js} +4 -4
  27. package/src/ui/public/assets/agentConversations-BEKqkGcm.js +5 -0
  28. package/src/ui/public/assets/{configStore-BUXv5i5j.js → configStore-CX0FkQ94.js} +1 -1
  29. package/src/ui/public/assets/{gitStore-BGbEYd0U.js → gitStore-CMuWWr2d.js} +1 -1
  30. package/src/ui/public/assets/{index-Di5cimoI.js → index-CB2cIiHl.js} +12 -12
  31. package/src/ui/public/assets/{previewSandbox-DEQpyVNu.js → previewSandbox-DxR4iN9P.js} +0 -0
  32. package/src/ui/public/index.html +10 -10
  33. package/src/ui/server/index.js +28 -2
  34. package/src/ui/server/routes/aiContext/collectors.js +360 -0
  35. package/src/ui/server/routes/aiContext/collectors.test.js +297 -0
  36. package/src/ui/server/routes/aiContext/index.js +444 -0
  37. package/src/ui/server/routes/aiContext/index.test.js +433 -0
  38. package/src/ui/server/routes/aiContext/jobRefresh.js +110 -0
  39. package/src/ui/server/routes/aiContext/jobRefresh.test.js +191 -0
  40. package/src/ui/server/routes/aiContext/render.js +366 -0
  41. package/src/ui/server/routes/aiContext/render.test.js +255 -0
  42. package/src/ui/server/routes/aiContext/wiring.js +80 -0
  43. package/src/ui/server/routes/fs.js +43 -0
  44. package/src/ui/server/routes/mindmap.js +36 -24
  45. package/src/ui/server/routes/monitor.js +44 -35
  46. package/src/ui/server/routes/remoteRepos.js +57 -48
  47. package/src/ui/server/routes/workbench/agentChat.js +35 -7
  48. package/src/ui/server/routes/workbench/agentChatContext.test.js +66 -1
  49. package/src/ui/server/routes/workbench/agentEngines.js +77 -0
  50. package/src/ui/server/routes/workbench/agentEngines.test.js +65 -0
  51. package/src/ui/server/routes/workbench/agentRoutes.js +169 -44
  52. package/src/ui/server/routes/workbench/agentRoutes.test.js +66 -4
  53. package/src/ui/server/routes/workbench/agentSessionStore.js +3 -0
  54. package/src/ui/server/routes/workbench/envContext.js +61 -64
  55. package/src/ui/server/routes/workbench/envContext.test.js +133 -63
  56. package/src/ui/server/routes/workbench/index.js +78 -32
  57. package/src/ui/server/routes/workbench/runExternalTurn.js +279 -0
  58. package/src/ui/server/routes/workbench/runExternalTurn.test.js +275 -0
  59. package/src/ui/server/routes/workbench/shared.js +17 -0
  60. package/src/ui/public/assets/AgentView-Dhs3eKFY.js +0 -7
  61. package/src/ui/public/assets/EditorView-DVRemzZU.js +0 -16
  62. package/src/ui/public/assets/agentConversations-BQCIv4u1.js +0 -5
@@ -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
+ });