zen-gitsync 2.18.20 → 2.18.22

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 (88) hide show
  1. package/package.json +4 -1
  2. package/src/cli/ai/agent.js +9 -2
  3. package/src/cli/ai/extensions.js +1 -1
  4. package/src/cli/ai/runtime.test.js +57 -1
  5. package/src/cli/ai/termui.js +11 -2
  6. package/src/cli/ai/tools.js +266 -5
  7. package/src/cli/ai/tools.test.js +81 -0
  8. package/src/cli/ai/transport.js +1 -1
  9. package/src/cli/ai/turn.js +59 -3
  10. package/src/config.js +60 -4
  11. package/src/mcp/install.js +548 -529
  12. package/src/ui/public/assets/AgentView-DdiyBMDD.js +26 -0
  13. package/src/ui/public/assets/{AgentView-BE7GD8rc.css → AgentView-_DUYxFmp.css} +1 -1
  14. package/src/ui/public/assets/{AppVersionBadge-86tbKU20.js → AppVersionBadge-CSEM45L4.js} +2 -2
  15. package/src/ui/public/assets/{BranchSelector-CsrkmTCA.js → BranchSelector-cSx2ZxYM.js} +1 -1
  16. package/src/ui/public/assets/ChatSilenceBadge-Dtv23ZmV.js +16 -0
  17. package/src/ui/public/assets/{CommitForm-DBK0GXEh.js → CommitForm-CrAsbDkq.js} +2 -2
  18. package/src/ui/public/assets/{CommonDialog-BkuDIWro.js → CommonDialog-BtO3uKdw.js} +1 -1
  19. package/src/ui/public/assets/{EditorView-CnWsMdHg.css → EditorView-B-t-nuf0.css} +1 -1
  20. package/src/ui/public/assets/EditorView-BesiWTV2.js +10 -0
  21. package/src/ui/public/assets/{FlowExecutionViewer-ClUkAmA2.js → FlowExecutionViewer-CfU9DLFG.js} +1 -1
  22. package/src/ui/public/assets/{FlowOrchestrationWorkspace-B3sOK3fz.js → FlowOrchestrationWorkspace-D3CSRMvG.js} +1 -1
  23. package/src/ui/public/assets/{LogList-6aO0-2xh.js → LogList-CT-SRIS3.js} +4 -4
  24. package/src/ui/public/assets/MindmapView-BCGq2zdh.js +35 -0
  25. package/src/ui/public/assets/MindmapView-CoUbyadF.css +1 -0
  26. package/src/ui/public/assets/{MonitorView-fcNrgVaU.js → MonitorView-BuBJJVhR.js} +1 -1
  27. package/src/ui/public/assets/{ProjectManagerView-CZfrML6A.js → ProjectManagerView-DlSfRn9N.js} +2 -2
  28. package/src/ui/public/assets/{ProjectStartupButton-D5MREz6F.js → ProjectStartupButton-DKZttDb3.js} +1 -1
  29. package/src/ui/public/assets/RemoteManagerDialog-CVrhhUPg.js +4 -0
  30. package/src/ui/public/assets/{RemoteRepoCard-CTnmfv2y.js → RemoteRepoCard-CmdYDF24.js} +1 -1
  31. package/src/ui/public/assets/{SourceMapView-B4q5qiE8.js → SourceMapView-DClgTmda.js} +1 -1
  32. package/src/ui/public/assets/{SvgIcon-WpEiC1rI.js → SvgIcon-D5fhBiJr.js} +1 -1
  33. package/src/ui/public/assets/TaskExecutorPicker-CzvWdS_K.js +1 -0
  34. package/src/ui/public/assets/TaskExecutorPicker-Dn8SgrTZ.css +1 -0
  35. package/src/ui/public/assets/{UserInputNode-Deoc3UvQ.js → UserInputNode-CNuW_Dtu.js} +1 -1
  36. package/src/ui/public/assets/WorkbenchView-D61DxNnn.css +1 -0
  37. package/src/ui/public/assets/WorkbenchView-WKNPn-oz.js +341 -0
  38. package/src/ui/public/assets/_plugin-vue_export-helper-E2AaPXn7.js +19 -0
  39. package/src/ui/public/assets/configStore-JIX2MOKo.js +1 -0
  40. package/src/ui/public/assets/{dagre-CpKxpMNH.js → dagre-yiQDclZv.js} +3 -3
  41. package/src/ui/public/assets/{element-plus-Bz6lYur-.js → element-plus-BeZBQw4X.js} +1 -1
  42. package/src/ui/public/assets/{flow-mindmap-cTg78Krq.js → flow-mindmap-CGjPqCv0.js} +1 -1
  43. package/src/ui/public/assets/{index-cDiVh_ZL.js → index-Cjv2Z6nF.js} +134 -125
  44. package/src/ui/public/assets/index-IjYv2goK.css +1 -0
  45. package/src/ui/public/assets/{monaco-CvMddat7.js → monaco-CMQow_j-.js} +1 -1
  46. package/src/ui/public/assets/{office-docx-VOB-i4K2.js → office-docx-nuGPI15f.js} +1 -1
  47. package/src/ui/public/assets/{office-excel-CJ2JvfA8.js → office-excel-CK-HOYi8.js} +1 -1
  48. package/src/ui/public/assets/{office-pptx-Z88k9qnx.js → office-pptx-9onMeLux.js} +1 -1
  49. package/src/ui/public/assets/{relativeTime-CoXAs57j.js → relativeTime-DHhE9Ihn.js} +1 -1
  50. package/src/ui/public/assets/taskExecutionExport-DlBMELzX.js +15 -0
  51. package/src/ui/public/assets/toolsStore-h5iqPyST.js +1 -0
  52. package/src/ui/public/assets/{useWorkbenchProjects-BS59r5Rj.js → useWorkbenchProjects-DW3vBHpK.js} +1 -1
  53. package/src/ui/public/assets/{vendor-CSFGMcMB.css → vendor-CAhoE06w.css} +1 -1
  54. package/src/ui/public/assets/{vendor-C2k0GBzj.js → vendor-KxSgeiUX.js} +303 -303
  55. package/src/ui/public/assets/{vue-flow-Cnxq2qrs.js → vue-flow-BMMiguH1.js} +1 -1
  56. package/src/ui/public/index.html +14 -14
  57. package/src/ui/server/routes/config.js +13 -0
  58. package/src/ui/server/routes/fileOpen.js +1851 -1736
  59. package/src/ui/server/routes/fileOpen.test.js +39 -1
  60. package/src/ui/server/routes/workbench/agentChat.js +48 -8
  61. package/src/ui/server/routes/workbench/agentChatShared.test.js +584 -496
  62. package/src/ui/server/routes/workbench/agentEngines.js +7 -4
  63. package/src/ui/server/routes/workbench/executorModels.js +25 -4
  64. package/src/ui/server/routes/workbench/executorModels.test.js +557 -530
  65. package/src/ui/server/routes/workbench/jobActivity.js +2 -2
  66. package/src/ui/server/routes/workbench/taskRunner.codearts.test.js +252 -0
  67. package/src/ui/server/routes/workbench/taskRunner.codebuddy.test.js +317 -317
  68. package/src/ui/server/routes/workbench/taskRunner.js +216 -7
  69. package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +130 -130
  70. package/src/ui/server/routes/workbench/taskRunner.pi.test.js +298 -298
  71. package/src/ui/public/assets/AgentView-Du9akUeZ.js +0 -12
  72. package/src/ui/public/assets/ChatSilenceBadge-BBoFeSqe.js +0 -21
  73. package/src/ui/public/assets/EditorAgentPanel-BMUphuG0.css +0 -1
  74. package/src/ui/public/assets/EditorAgentPanel-KiHbQPJP.js +0 -34
  75. package/src/ui/public/assets/EditorView-B18B4yNJ.js +0 -13
  76. package/src/ui/public/assets/MindmapView-BDI3D_1h.css +0 -1
  77. package/src/ui/public/assets/MindmapView-aJ9TTijE.js +0 -4
  78. package/src/ui/public/assets/RecentDirectoriesChat-DBGoUlkn.css +0 -1
  79. package/src/ui/public/assets/RecentDirectoriesChat-wfkSF6NU.js +0 -3
  80. package/src/ui/public/assets/RemoteManagerDialog-BBwNlK9J.js +0 -4
  81. package/src/ui/public/assets/WorkbenchView-CVqXb__l.css +0 -1
  82. package/src/ui/public/assets/WorkbenchView-CijzpDWN.js +0 -360
  83. package/src/ui/public/assets/_plugin-vue_export-helper-BhdsvhKo.js +0 -19
  84. package/src/ui/public/assets/agentConversations-DaAtF1v4.js +0 -10
  85. package/src/ui/public/assets/configStore-Drlw8KtR.js +0 -1
  86. package/src/ui/public/assets/index-DSQ6mp_N.css +0 -1
  87. package/src/ui/public/assets/toolsStore-CnX34C96.js +0 -1
  88. package/src/ui/public/assets/useOrchestrator-BXmxJsRl.js +0 -1
@@ -216,7 +216,7 @@ export function jobDurationMs(job) {
216
216
  }
217
217
 
218
218
  /**
219
- * 本轮跑的是哪个执行器(`claude` | `opencode` | `codex` | `codebuddy` | `pi`)。
219
+ * 本轮跑的是哪个执行器(`claude` | `opencode` | `codex` | `codebuddy` | `pi` | `codearts`)。
220
220
  *
221
221
  * 只在**认出是已知执行器**时才回值:job.agent 来自 jobs.json(会被手工编辑、也读过
222
222
  * 别实例写的旧记录),回一个前端不认识的字符串只会让卡片上的图标位空着或显示错品牌。
@@ -227,7 +227,7 @@ export function jobDurationMs(job) {
227
227
  * 运行时模块,而本模块被 projectRegistry(5s 轮询的看板)直接引用,没必要为了五个
228
228
  * 字符串把整条执行链拖进来。新增执行器时这里和 taskRunner 一起改。
229
229
  */
230
- const KNOWN_EXECUTORS = ['claude', 'opencode', 'codex', 'codebuddy', 'pi'];
230
+ const KNOWN_EXECUTORS = ['claude', 'opencode', 'codex', 'codebuddy', 'pi', 'codearts'];
231
231
 
232
232
  export function jobAgent(job) {
233
233
  const v = job && typeof job.agent === 'string' ? job.agent.trim().toLowerCase() : '';
@@ -0,0 +1,252 @@
1
+ // Copyright 2026 xz333221
2
+ //
3
+ // Licensed under the Apache License, Version 2.0 (the "License");
4
+ // you may not use this file except in compliance with the License.
5
+ // You may obtain a copy of the License at
6
+ //
7
+ // http://www.apache.org/licenses/LICENSE-2.0
8
+ //
9
+ // Unless required by applicable law or agreed to in writing, software
10
+ // distributed under the License is distributed on an "AS IS" BASIS,
11
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ // See the License for the specific language governing permissions and
13
+ // limitations under the License.
14
+ //
15
+
16
+ // codearts 执行器(华为云码道 CLI)的回归测试(2026-10-25)。
17
+ //
18
+ // 钉住的口径(**event fixtures 全部取自本机 26.10.9 的真实输出**,
19
+ // 不是照文档抄的):
20
+ // 1. 白名单 / 归一化里有 codearts(config.js 与 taskRunner 两份清单一致)
21
+ // 2. spawn argv:run --format json --auto [+ -s <ses_id>];
22
+ // --auto 必须带(默认档是 sandbox,沙箱里 git 读不到真实仓库状态,
23
+ // 而派发链要 agent 自己 commit);不出现 --model(跟随它自己的配置)
24
+ // 3. 子进程环境:必须复刻 codearts.cmd 那串 OPENCODE_* / KERNEL_* ——
25
+ // 少了它们它会去读用户自己那套 opencode 的配置与 db;且刻意**不抄**
26
+ // NODE_TLS_REJECT_UNAUTHORIZED=0(shim 里有,抄进来会污染本进程所有 TLS)
27
+ // 4. 事件映射:它是 opencode 的 fork,事件流与 OpenCode 逐字同构
28
+ // (step_start / text / reasoning / tool_use / step_finish / error),
29
+ // handler 直接转调 createOpencodeEventHandler —— 这里仍钉一遍,
30
+ // 因为它哪天改协议时这条会先红
31
+ // 5. 它会往项目根写 .codeartsdoer/(且该文件不在它自己的 .gitignore 里),
32
+ // launcher 要往 .git/info/exclude 补一行,且要幂等、非仓库目录不炸
33
+ //
34
+ // 隔离策略同 taskRunner.pi.test.js:import taskRunner.js 之前把
35
+ // USERPROFILE/HOME 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
36
+
37
+ import { test } from 'node:test';
38
+ import assert from 'node:assert/strict';
39
+ import path from 'node:path';
40
+ import os from 'node:os';
41
+ import fs from 'node:fs';
42
+ import { mkdtempSync } from 'node:fs';
43
+ import { fileURLToPath, pathToFileURL } from 'node:url';
44
+ import { execFileSync } from 'node:child_process';
45
+
46
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
47
+
48
+ // ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
49
+ const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-taskrunner-codearts-test-'));
50
+ process.env.USERPROFILE = sandbox;
51
+ process.env.HOME = sandbox;
52
+ delete process.env.HOMEDRIVE;
53
+ delete process.env.HOMEPATH;
54
+
55
+ const {
56
+ createCodeArtsEventHandler,
57
+ buildCodeArtsArgs,
58
+ buildCodeArtsEnv,
59
+ resolveCodeArtsExe,
60
+ ensureCodeArtsGitExclude,
61
+ TASK_EXECUTORS,
62
+ EXECUTOR_LAUNCHERS,
63
+ EXECUTOR_HANDLERS,
64
+ normalizeTaskExecutor,
65
+ } = await import(pathToFileURL(path.join(__dirname, 'taskRunner.js')).href);
66
+
67
+ // ── 真实事件 fixture(本机 26.10.9 跑 "运行 git branch --show-current" 的输出)──
68
+ const SES = 'ses_e8d69660cffe8XcrTbjDt67x01';
69
+ const FIXTURE = [
70
+ { type: 'step_start', timestamp: 1792923814179, sessionID: SES, part: { id: 'prt_1', messageID: 'msg_1', sessionID: SES, type: 'step-start' } },
71
+ {
72
+ type: 'tool_use', timestamp: 1792923815513, sessionID: SES,
73
+ part: {
74
+ id: 'prt_2', messageID: 'msg_1', sessionID: SES, type: 'tool', tool: 'bash', callID: 'chatcmpl-tool-b8c7',
75
+ state: {
76
+ status: 'completed',
77
+ input: { command: 'git branch --show-current', description: '显示当前Git分支名' },
78
+ output: 'master\n',
79
+ metadata: { executeMode: 'auto_mode', exit: 0, timedOut: false },
80
+ title: '显示当前Git分支名',
81
+ },
82
+ },
83
+ },
84
+ {
85
+ type: 'step_finish', timestamp: 1792923815810, sessionID: SES,
86
+ part: { id: 'prt_3', reason: 'tool-calls', messageID: 'msg_1', sessionID: SES, type: 'step-finish', tokens: { total: 11257, input: 8634, output: 36 }, cost: 0 },
87
+ },
88
+ { type: 'step_start', timestamp: 1792923817908, sessionID: SES, part: { id: 'prt_4', messageID: 'msg_2', sessionID: SES, type: 'step-start' } },
89
+ { type: 'text', timestamp: 1792923818587, sessionID: SES, part: { id: 'prt_5', messageID: 'msg_2', sessionID: SES, type: 'text', text: 'master' } },
90
+ { type: 'step_finish', timestamp: 1792923818596, sessionID: SES, part: { id: 'prt_6', reason: 'stop', messageID: 'msg_2', sessionID: SES, type: 'step-finish', tokens: { total: 11284 }, cost: 0 } },
91
+ ];
92
+
93
+ /** 造一个和 handler 契约一致的 job + 收集器 */
94
+ function makeHarness() {
95
+ const job = { id: 'job-1', claudeSessionId: '', agentError: '' };
96
+ const out = { text: '', thinking: '', tools: [] };
97
+ const handler = createCodeArtsEventHandler(
98
+ job,
99
+ (t) => { out.text += t; },
100
+ (t) => { out.thinking += t; },
101
+ { record: (call) => out.tools.push(call) }
102
+ );
103
+ return { job, out, handler };
104
+ }
105
+
106
+ test('normalizeTaskExecutor:codearts 在白名单里,非法值回落 claude', () => {
107
+ assert.deepEqual(TASK_EXECUTORS, ['claude', 'opencode', 'codex', 'codebuddy', 'pi', 'codearts']);
108
+ assert.equal(normalizeTaskExecutor('codearts'), 'codearts');
109
+ assert.equal(normalizeTaskExecutor('CodeArts'), 'codearts');
110
+ assert.equal(normalizeTaskExecutor(' codearts '), 'codearts');
111
+ // 历史默认不能变:老配置 / 老 job 缺 executor 时仍落在 claude
112
+ assert.equal(normalizeTaskExecutor(undefined), 'claude');
113
+ assert.equal(normalizeTaskExecutor('cursor'), 'claude');
114
+ });
115
+
116
+ test('launcher / handler 两张表都登记了 codearts(与 TASK_EXECUTORS 对齐)', () => {
117
+ for (const id of TASK_EXECUTORS) {
118
+ assert.equal(typeof EXECUTOR_LAUNCHERS[id], 'function', `${id} 缺 launcher`);
119
+ assert.equal(typeof EXECUTOR_HANDLERS[id], 'function', `${id} 缺 handler`);
120
+ }
121
+ });
122
+
123
+ test('buildCodeArtsArgs:run --format json --auto,不带 --model / --sandbox', () => {
124
+ const args = buildCodeArtsArgs('');
125
+ assert.deepEqual(args, ['run', '--format', 'json', '--auto']);
126
+ // 跟随 CLI 自己的模型配置(与另五家同一口径)
127
+ assert.ok(!args.includes('--model'));
128
+ // ⚠️ 必须 --auto:默认档是 sandbox,沙箱里 git 读不到真实仓库状态,
129
+ // 而派发链要 agent 自己 commit。两个开关互斥,别同时出现。
130
+ assert.ok(!args.includes('--sandbox'));
131
+ });
132
+
133
+ test('buildCodeArtsArgs:有续接 id 时追加 -s <id>(原地续,sessionID 不变)', () => {
134
+ const args = buildCodeArtsArgs(SES);
135
+ assert.deepEqual(args, ['run', '--format', 'json', '--auto', '-s', SES]);
136
+ });
137
+
138
+ test('buildCodeArtsEnv:复刻 shim 的 OPENCODE_* / KERNEL_*,但不抄关证书校验那个开关', () => {
139
+ const had = {};
140
+ const pollution = {
141
+ CODEARTS_CLI_AK: 'ak-from-env',
142
+ CODEARTS_CLI_SK: 'sk-from-env',
143
+ };
144
+ for (const [k, v] of Object.entries(pollution)) { had[k] = process.env[k]; process.env[k] = v; }
145
+ // 这条要断言"我们不主动设",所以先确保父进程里没有(有的话会原样继承,
146
+ // 那属于用户自己的运行环境,不该被 launcher 悄悄改掉)
147
+ had.NODE_TLS_REJECT_UNAUTHORIZED = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
148
+ delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
149
+ try {
150
+ const env = buildCodeArtsEnv();
151
+ // 身份:少了这两个它会去读 ~/.config/opencode 与 ~/.local/share/opencode 的 db
152
+ assert.equal(env.SCENARIO, 'codeartsdoer');
153
+ assert.equal(env.KERNEL_DATA_DIR, path.join(sandbox, '.codeartsdoer', 'cli-data'));
154
+ assert.equal(env.KERNEL_CONFIG_DIR, path.join(sandbox, '.codeartsdoer'));
155
+ assert.equal(env.OPENCODE_CONFIG, path.join(sandbox, '.codeartsdoer', 'codearts_cli.json'));
156
+ assert.equal(env.OPENCODE_CONFIG_FILE, 'codearts_cli.json,codearts_cli.jsonc');
157
+ assert.equal(env.PLUGIN_ENV, 'hc');
158
+ // 关自动更新 / 关遥测(与另几家同一性质)
159
+ assert.equal(env.CODEARTS_DISABLE_AUTO_UPDATE, 'true');
160
+ assert.equal(env.OMO_SEND_ANONYMOUS_TELEMETRY, '0');
161
+ assert.equal(env.LANG, 'zh_CN.UTF-8');
162
+ // AK/SK 从父进程继承(这是它唯一的鉴权方式)
163
+ assert.equal(env.CODEARTS_CLI_AK, 'ak-from-env');
164
+ assert.equal(env.CODEARTS_CLI_SK, 'sk-from-env');
165
+ // ⚠️ shim 里有 NODE_TLS_REJECT_UNAUTHORIZED=0,刻意不抄:
166
+ // 它会关掉本进程派生出的所有 TLS 校验。实测不设也能正常连华为云。
167
+ assert.equal(env.NODE_TLS_REJECT_UNAUTHORIZED, undefined);
168
+ } finally {
169
+ for (const [k, v] of Object.entries(had)) {
170
+ if (v === undefined) delete process.env[k]; else process.env[k] = v;
171
+ }
172
+ }
173
+ });
174
+
175
+ test('resolveCodeArtsExe:沙箱里没装时回落,不抛异常', () => {
176
+ const got = resolveCodeArtsExe();
177
+ assert.equal(typeof got.exe, 'string');
178
+ assert.ok(got.exe.length > 0);
179
+ assert.ok(Array.isArray(got.prefixArgs));
180
+ });
181
+
182
+ test('handler:sessionID → claudeSessionId(续接走同一个字段)', () => {
183
+ const { job, handler } = makeHarness();
184
+ for (const evt of FIXTURE) handler(evt);
185
+ assert.equal(job.claudeSessionId, SES);
186
+ assert.equal(job.agentError, '');
187
+ });
188
+
189
+ test('handler:正文与工具调用(completed → done,output 带上)', () => {
190
+ const { out, handler } = makeHarness();
191
+ for (const evt of FIXTURE) handler(evt);
192
+ assert.equal(out.text, 'master');
193
+ assert.equal(out.tools.length, 1);
194
+ assert.equal(out.tools[0].name, 'bash');
195
+ assert.equal(out.tools[0].status, 'done');
196
+ assert.match(String(out.tools[0].output), /master/);
197
+ });
198
+
199
+ test('handler:error 事件落 agentError(只记第一条)', () => {
200
+ const { job, handler } = makeHarness();
201
+ handler({ type: 'error', sessionID: SES, error: { name: 'ProviderError', data: { message: '额度已用完' } } });
202
+ handler({ type: 'error', sessionID: SES, error: { name: 'Other', data: { message: '第二条不该覆盖' } } });
203
+ assert.equal(job.agentError, '额度已用完');
204
+ });
205
+
206
+ test('handler:非 JSON 行(免费套餐的"模型排队"提示)不会写进正文', () => {
207
+ // 解析层(runSingleSubtask / runExternalTurn)负责 try/catch 跳过非 JSON;
208
+ // 这里钉住"即使错误地喂进来也不会污染正文"。
209
+ const { out, job, handler } = makeHarness();
210
+ assert.doesNotThrow(() => handler(null));
211
+ assert.doesNotThrow(() => handler('模型排队'));
212
+ assert.doesNotThrow(() => handler({ type: 'unknown-event' }));
213
+ assert.equal(out.text, '');
214
+ assert.equal(job.agentError, '');
215
+ });
216
+
217
+ test('ensureCodeArtsGitExclude:写 <repo>/.git/info/exclude,幂等,非仓库目录不炸', () => {
218
+ const repo = mkdtempSync(path.join(os.tmpdir(), 'zen-codearts-exclude-'));
219
+ execFileSync('git', ['init', '-q'], { cwd: repo });
220
+ const excludeFile = path.join(repo, '.git', 'info', 'exclude');
221
+
222
+ ensureCodeArtsGitExclude(repo);
223
+ const first = fs.readFileSync(excludeFile, 'utf8');
224
+ assert.ok(first.split(/\r?\n/).includes('.codeartsdoer/'));
225
+
226
+ // 再跑一次不能重复加行
227
+ ensureCodeArtsGitExclude(repo);
228
+ const second = fs.readFileSync(excludeFile, 'utf8');
229
+ assert.equal(second, first);
230
+
231
+ // 非仓库目录:静默返回
232
+ const plain = mkdtempSync(path.join(os.tmpdir(), 'zen-codearts-plain-'));
233
+ assert.doesNotThrow(() => ensureCodeArtsGitExclude(plain));
234
+ });
235
+
236
+ test('ensureCodeArtsGitExclude:补了 exclude 之后 .codeartsdoer/ 不算脏', () => {
237
+ const repo = mkdtempSync(path.join(os.tmpdir(), 'zen-codearts-dirty-'));
238
+ execFileSync('git', ['init', '-q'], { cwd: repo });
239
+ fs.writeFileSync(path.join(repo, 'a.txt'), 'hi\n');
240
+ execFileSync('git', ['add', '-A'], { cwd: repo });
241
+ execFileSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'init'], { cwd: repo });
242
+
243
+ // 模拟 codearts 跑完留下的东西
244
+ fs.mkdirSync(path.join(repo, '.codeartsdoer'), { recursive: true });
245
+ fs.writeFileSync(path.join(repo, '.codeartsdoer', 'mem-exp-config.json'), '{}\n');
246
+ const dirtyBefore = execFileSync('git', ['status', '--short'], { cwd: repo }).toString();
247
+ assert.match(dirtyBefore, /\.codeartsdoer/);
248
+
249
+ ensureCodeArtsGitExclude(repo);
250
+ const dirtyAfter = execFileSync('git', ['status', '--short'], { cwd: repo }).toString();
251
+ assert.equal(dirtyAfter.trim(), '');
252
+ });