zen-gitsync 2.18.21 → 2.18.23
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.
- package/package.json +2 -1
- package/src/cli/ai/agent.js +9 -2
- package/src/cli/ai/extensions.js +1 -1
- package/src/cli/ai/runtime.test.js +57 -1
- package/src/cli/ai/termui.js +11 -2
- package/src/cli/ai/tools.js +266 -5
- package/src/cli/ai/tools.test.js +81 -0
- package/src/cli/ai/transport.js +1 -1
- package/src/cli/ai/turn.js +59 -3
- package/src/config.js +60 -4
- package/src/mcp/install.js +548 -529
- package/src/ui/public/assets/{AgentView-zjXqXR6_.css → AgentView-Cb5CmQTA.css} +1 -1
- package/src/ui/public/assets/AgentView-DQOp7_6G.js +1 -0
- package/src/ui/public/assets/{AppVersionBadge-CAjnmlwK.js → AppVersionBadge-BBVS3HeA.js} +2 -2
- package/src/ui/public/assets/{BranchSelector-Bx7ZWY7i.js → BranchSelector-BwmMgaSS.js} +1 -1
- package/src/ui/public/assets/{ChatSilenceBadge-BGRTUX_g.js → ChatSilenceBadge-BnaLbCwp.js} +6 -6
- package/src/ui/public/assets/{CommitForm-BArPY3IJ.js → CommitForm-BtRxJodG.js} +1 -1
- package/src/ui/public/assets/{CommonDialog-C5Q6F8ql.js → CommonDialog-Qg_owAyQ.js} +1 -1
- package/src/ui/public/assets/{EditorView-x7dr5MPK.js → EditorView-DL3Lj4ho.js} +1 -1
- package/src/ui/public/assets/{FlowExecutionViewer-BHz9hKxX.js → FlowExecutionViewer-BsxAtCqd.js} +1 -1
- package/src/ui/public/assets/{FlowOrchestrationWorkspace-F7YGzQV-.js → FlowOrchestrationWorkspace-lcqnsQE-.js} +1 -1
- package/src/ui/public/assets/{LogList-DBc9xebA.js → LogList-CW3-fShz.js} +1 -1
- package/src/ui/public/assets/{MindmapView-Dz-gyQCR.js → MindmapView-ymN6XI7n.js} +1 -1
- package/src/ui/public/assets/{MonitorView-CvPb2Dji.js → MonitorView-C4fFNPb2.js} +1 -1
- package/src/ui/public/assets/{ProjectManagerView-BiV62lNF.js → ProjectManagerView-ph_imDtI.js} +1 -1
- package/src/ui/public/assets/{ProjectStartupButton--Q-S0DWw.js → ProjectStartupButton-C2dqnR-6.js} +1 -1
- package/src/ui/public/assets/{RemoteManagerDialog-BGib3iis.js → RemoteManagerDialog-DFkRW2O4.js} +1 -1
- package/src/ui/public/assets/{RemoteRepoCard-CzIWKirJ.js → RemoteRepoCard-BBnMjldy.js} +1 -1
- package/src/ui/public/assets/{SourceMapView-CuAuq9vv.js → SourceMapView-CQcK1acs.js} +1 -1
- package/src/ui/public/assets/{SvgIcon-9MndOhWP.js → SvgIcon-XBkgNKCj.js} +1 -1
- package/src/ui/public/assets/{TaskExecutorPicker-BNLwo3aN.js → TaskExecutorPicker-B6p57RJY.js} +1 -1
- package/src/ui/public/assets/{UserInputNode-B1wOq19j.js → UserInputNode-CkXLP8y4.js} +1 -1
- package/src/ui/public/assets/WorkbenchView-MUfM9DER.css +1 -0
- package/src/ui/public/assets/WorkbenchView-cucUmAwl.js +6 -0
- package/src/ui/public/assets/{_plugin-vue_export-helper-B-yhZnp1.js → _plugin-vue_export-helper-B3dZvHQf.js} +3 -3
- package/src/ui/public/assets/configStore-HDlssep1.js +1 -0
- package/src/ui/public/assets/{dagre-BD_lsknB.js → dagre-JTV5zPzI.js} +3 -3
- package/src/ui/public/assets/{element-plus-axsKECfO.js → element-plus-CN9J6t-d.js} +1 -1
- package/src/ui/public/assets/{flow-mindmap-B4ponDC1.js → flow-mindmap-CyZrdnd4.js} +1 -1
- package/src/ui/public/assets/index-CAWXBaPa.js +59 -0
- package/src/ui/public/assets/index-CigPOqV6.css +1 -0
- package/src/ui/public/assets/{monaco-Bu9hROZs.js → monaco-irXl5Eu9.js} +1 -1
- package/src/ui/public/assets/{office-docx-Bb8autKU.js → office-docx-C48cuZOS.js} +1 -1
- package/src/ui/public/assets/{office-excel-G4ohUqnx.js → office-excel-sA-LZnfv.js} +1 -1
- package/src/ui/public/assets/{office-pptx-CLAxHUkH.js → office-pptx-BLPWYpxZ.js} +1 -1
- package/src/ui/public/assets/{relativeTime-DnD6Qv_n.js → relativeTime-Ce-gekf_.js} +1 -1
- package/src/ui/public/assets/{taskExecutionExport-D6VIBn4K.js → taskExecutionExport-Hfww5P9_.js} +3 -3
- package/src/ui/public/assets/toolsStore-oZEAoHnG.js +1 -0
- package/src/ui/public/assets/{useWorkbenchProjects-VSSECz1J.js → useWorkbenchProjects-BifctigH.js} +1 -1
- package/src/ui/public/assets/{vendor-DMJR8zxK.js → vendor-Bs5KA16r.js} +220 -220
- package/src/ui/public/assets/{vendor-B2ISauoC.css → vendor-CAhoE06w.css} +1 -1
- package/src/ui/public/assets/{vue-flow-BgHev8Va.js → vue-flow-NLvet82_.js} +1 -1
- package/src/ui/public/index.html +14 -14
- package/src/ui/server/routes/config.js +13 -0
- package/src/ui/server/routes/fileOpen.js +1851 -1736
- package/src/ui/server/routes/fileOpen.test.js +39 -1
- package/src/ui/server/routes/workbench/agentChat.js +48 -8
- package/src/ui/server/routes/workbench/agentChatShared.test.js +584 -496
- package/src/ui/server/routes/workbench/agentEngines.js +7 -4
- package/src/ui/server/routes/workbench/executorModels.js +25 -4
- package/src/ui/server/routes/workbench/executorModels.test.js +557 -530
- package/src/ui/server/routes/workbench/jobActivity.js +2 -2
- package/src/ui/server/routes/workbench/taskRunner.codearts.test.js +308 -0
- package/src/ui/server/routes/workbench/taskRunner.codebuddy.test.js +317 -317
- package/src/ui/server/routes/workbench/taskRunner.js +280 -7
- package/src/ui/server/routes/workbench/taskRunner.opencode.test.js +130 -130
- package/src/ui/server/routes/workbench/taskRunner.pi.test.js +298 -298
- package/src/ui/public/assets/AgentView-BEtr0bKC.js +0 -1
- package/src/ui/public/assets/WorkbenchView-DGa_j7-z.css +0 -1
- package/src/ui/public/assets/WorkbenchView-DnHUwx2h.js +0 -6
- package/src/ui/public/assets/configStore-D9y28eJA.js +0 -1
- package/src/ui/public/assets/index-ArXbQKZJ.js +0 -59
- package/src/ui/public/assets/index-iTgY1Kwp.css +0 -1
- package/src/ui/public/assets/toolsStore-BWcMcQnZ.js +0 -1
|
@@ -1,317 +1,317 @@
|
|
|
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
|
-
// codebuddy 执行器(CodeBuddy Code CLI)的回归测试(2026-10-12)。
|
|
17
|
-
//
|
|
18
|
-
// 钉住的口径(全部来自当天实测,CLI 2.164.0):
|
|
19
|
-
// 1. 白名单 / 归一化里有 codebuddy(config.js 与 taskRunner 两份清单一致)
|
|
20
|
-
// 2. spawn argv:-p + stream-json + -y + **--port 0**(避开桌面版占用的 62443)
|
|
21
|
-
// + 续聊时 --resume <id>;**不出现** --verbose(cbc 不需要,claude 才需要)
|
|
22
|
-
// 3. 子进程环境:DISABLE_AUTOUPDATER / CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS
|
|
23
|
-
// 必须带上(丢了不会报错,只会某天任务跑到一半被自动更新换掉二进制);
|
|
24
|
-
// 桌面版注入的五个前缀族 + 单键必须剥掉 —— CODEBUDDY_HOST 与
|
|
25
|
-
// CODEBUDDY_FORCE_LITE_WB_BUNDLE 是 2026-10-12 实测的两个认证杀手
|
|
26
|
-
// (后者命中桌面版专属 lite-wb bundle 分流);官方认证通道
|
|
27
|
-
// CODEBUDDY_API_KEY / CODEBUDDY_BASE_URL 必须放行
|
|
28
|
-
// 4. 事件映射:init → session_id;assistant 的 text/thinking/tool_use 分派;
|
|
29
|
-
// user 里的 tool_result 回指;cbc 特有的 system/status、ai-title、result 一律忽略
|
|
30
|
-
// 5. 工具调用是"先 tool_use(running)、后 user(tool_result)"两条,不是一次性终态
|
|
31
|
-
// (runExternalTurn 的 announced 补 start 机制就是为后者准备的,两条式直接命中)
|
|
32
|
-
//
|
|
33
|
-
// 隔离策略同 taskRunner.opencode.test.js:import taskRunner.js 之前把
|
|
34
|
-
// USERPROFILE/HOME 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
|
|
35
|
-
|
|
36
|
-
import { test } from 'node:test';
|
|
37
|
-
import assert from 'node:assert/strict';
|
|
38
|
-
import path from 'node:path';
|
|
39
|
-
import os from 'node:os';
|
|
40
|
-
import { mkdtempSync } from 'node:fs';
|
|
41
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
-
|
|
43
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
-
const projectRoot = path.resolve(__dirname, '../../../../..');
|
|
45
|
-
|
|
46
|
-
// ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
|
|
47
|
-
const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-taskrunner-cb-test-'));
|
|
48
|
-
process.env.USERPROFILE = sandbox;
|
|
49
|
-
process.env.HOME = sandbox;
|
|
50
|
-
delete process.env.HOMEDRIVE;
|
|
51
|
-
delete process.env.HOMEPATH;
|
|
52
|
-
|
|
53
|
-
const {
|
|
54
|
-
createCodebuddyEventHandler,
|
|
55
|
-
buildCodebuddyArgs,
|
|
56
|
-
buildCodebuddyEnv,
|
|
57
|
-
normalizeTaskExecutor,
|
|
58
|
-
TASK_EXECUTORS,
|
|
59
|
-
} = await import(
|
|
60
|
-
pathToFileURL(path.join(projectRoot, 'src/ui/server/routes/workbench/taskRunner.js')).href
|
|
61
|
-
);
|
|
62
|
-
|
|
63
|
-
/** 可断言的 appendOutput/appendThinking 收集器 */
|
|
64
|
-
function makeSink() {
|
|
65
|
-
const sink = { output: [], thinking: [] };
|
|
66
|
-
sink.appendOutput = (t) => { if (t) sink.output.push(t); };
|
|
67
|
-
sink.appendThinking = (t) => { if (t) sink.thinking.push(t); };
|
|
68
|
-
return sink;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** tracker 探针:只记 handler 喂了哪些 record(不碰真 tracker 的落盘/推送) */
|
|
72
|
-
function makeTrackerSpy() {
|
|
73
|
-
const records = [];
|
|
74
|
-
return {
|
|
75
|
-
records,
|
|
76
|
-
record: (r) => records.push(r),
|
|
77
|
-
flush: () => {},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
test('normalizeTaskExecutor:codebuddy 在白名单里,非法值回落 claude', () => {
|
|
82
|
-
assert.deepEqual(TASK_EXECUTORS, ['claude', 'opencode', 'codex', 'codebuddy', 'pi']);
|
|
83
|
-
assert.equal(normalizeTaskExecutor('codebuddy'), 'codebuddy');
|
|
84
|
-
assert.equal(normalizeTaskExecutor('CodeBuddy'), 'codebuddy');
|
|
85
|
-
assert.equal(normalizeTaskExecutor('codebuddy '), 'codebuddy');
|
|
86
|
-
// 历史默认不能变:老配置/老 job 缺 executor 时仍落在 claude
|
|
87
|
-
assert.equal(normalizeTaskExecutor(undefined), 'claude');
|
|
88
|
-
assert.equal(normalizeTaskExecutor('cursor'), 'claude');
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('buildCodebuddyArgs:基础 argv 带 -p/stream-json/-y/--port 0,不带 --verbose', () => {
|
|
92
|
-
const args = buildCodebuddyArgs('');
|
|
93
|
-
assert.deepEqual(args, ['-p', '--output-format', 'stream-json', '-y', '--port', '0']);
|
|
94
|
-
// claude 的 stream-json 必须配 --verbose,cbc 不需要 —— 多传一个少传一个都是坑,
|
|
95
|
-
// 这里双向钉住:不许出现 verbose,也不许漏掉 port 0
|
|
96
|
-
assert.ok(!args.includes('--verbose'));
|
|
97
|
-
assert.equal(args[args.indexOf('--port') + 1], '0');
|
|
98
|
-
// 不传模型:跟随 CLI 自己的配置(与另三家同一口径)
|
|
99
|
-
assert.ok(!args.includes('--model'));
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('buildCodebuddyArgs:有续聊 id 时 --resume 在前', () => {
|
|
103
|
-
const args = buildCodebuddyArgs('sess-abc');
|
|
104
|
-
assert.deepEqual(args.slice(0, 2), ['--resume', 'sess-abc']);
|
|
105
|
-
assert.ok(args.includes('-p'));
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
test('buildCodebuddyEnv:禁自动更新 + 禁后台任务 + LANG,前缀级剥掉桌面版注入变量', () => {
|
|
109
|
-
// 模拟"g gui 在 WorkBuddy 终端里被拉起":父进程带着桌面版整套注入变量。
|
|
110
|
-
// 覆盖五个前缀族 + 单键,每族挑代表;逐键断言剥离不留死角。
|
|
111
|
-
const had = {};
|
|
112
|
-
const pollution = {
|
|
113
|
-
// ★ 两个实测认证杀手
|
|
114
|
-
CODEBUDDY_HOST: 'workbuddy-desktop',
|
|
115
|
-
CODEBUDDY_FORCE_LITE_WB_BUNDLE: '1',
|
|
116
|
-
CODEBUDDY_FORCE_HEADLESS_BUNDLE: '1',
|
|
117
|
-
// 其余 CODEBUDDY_* 家族代表(认证辅助 / 网关 / 设备指纹 / 配置目录 / 会话追踪)
|
|
118
|
-
CODEBUDDY_API_KEY_HELPER_DISABLED: '1',
|
|
119
|
-
CODEBUDDY_GATEWAY_PASSWORD: 'secret',
|
|
120
|
-
CODEBUDDY_QIMEI36: 'device-fp',
|
|
121
|
-
CODEBUDDY_CONFIG_DIR: 'C:\\Users\\xuze\\.workbuddy',
|
|
122
|
-
CODEBUDDY_MCP_CONFIG: '{"mcpServers":{}}',
|
|
123
|
-
CODEBUDDY_SESSION_ID: 'some-session',
|
|
124
|
-
CODEBUDDY_CURRENT_MODEL_ID: 'some-model',
|
|
125
|
-
// WORKBUDDY_* / CLIENT_INFO_* / ACC_* / TENCENT_DOCS_LOCAL_* 族代表
|
|
126
|
-
WORKBUDDY_PAC_RPC_TOKEN: 'secret',
|
|
127
|
-
WORKBUDDY_PRODUCT_NAME: 'WorkBuddy',
|
|
128
|
-
CLIENT_INFO_PRODUCT_NAME: 'WorkBuddy',
|
|
129
|
-
CLIENT_INFO_MACHINE_ID: 'machine-id',
|
|
130
|
-
ACC_PRODUCT_CONFIG_PATH: 'C:\\Temp\\acc-product-config-v3.json',
|
|
131
|
-
TENCENT_DOCS_LOCAL_MCP: 'http://127.0.0.1:39099/mcp',
|
|
132
|
-
TENCENT_DOCS_LOCAL_SERVER: 'http://127.0.0.1:39099',
|
|
133
|
-
// 单键族:遥测追踪 / 沙箱 / 网关 / 运行时路径
|
|
134
|
-
BAGGAGE: 'codebuddy.session_id=xxx',
|
|
135
|
-
TRACEPARENT: '00-abc-def-01',
|
|
136
|
-
SANDBOX_CENTER_IPC_ADDRESS: '\\\\.\\pipe\\WorkBuddy_SandboxCenter_x',
|
|
137
|
-
SERVER__PORT: '52710',
|
|
138
|
-
EXEPATH: 'C:\\Users\\xuze\\AppData\\Local\\Programs\\WorkBuddy\\resources\\vendor\\PortableGit\\bin',
|
|
139
|
-
ELECTRON_RUN_AS_NODE: '1',
|
|
140
|
-
// 反例:用户合法认证通道(官方文档 headless 认证)必须保留
|
|
141
|
-
CODEBUDDY_API_KEY: 'user-key',
|
|
142
|
-
CODEBUDDY_BASE_URL: 'https://example.com/api',
|
|
143
|
-
// 反例:用户自己永久设置的变量不在剥离范围
|
|
144
|
-
DB_HOST: '192.168.3.44',
|
|
145
|
-
};
|
|
146
|
-
for (const [k, v] of Object.entries(pollution)) { had[k] = process.env[k]; process.env[k] = v; }
|
|
147
|
-
try {
|
|
148
|
-
const env = buildCodebuddyEnv();
|
|
149
|
-
// ★ 两个杀手必须剥掉:带上任一 CLI 会跑桌面版 bundle / 找桌面版凭据 → Authentication
|
|
150
|
-
// required(2026-10-12 逐变量二分 + 日志取证)。这两条测试就是那道防线。
|
|
151
|
-
assert.equal(env.CODEBUDDY_HOST, undefined);
|
|
152
|
-
assert.equal(env.CODEBUDDY_FORCE_LITE_WB_BUNDLE, undefined);
|
|
153
|
-
assert.equal(env.CODEBUDDY_FORCE_HEADLESS_BUNDLE, undefined);
|
|
154
|
-
for (const k of ['CODEBUDDY_API_KEY_HELPER_DISABLED', 'CODEBUDDY_GATEWAY_PASSWORD',
|
|
155
|
-
'CODEBUDDY_QIMEI36', 'CODEBUDDY_CONFIG_DIR', 'CODEBUDDY_MCP_CONFIG',
|
|
156
|
-
'CODEBUDDY_SESSION_ID', 'CODEBUDDY_CURRENT_MODEL_ID']) {
|
|
157
|
-
assert.equal(env[k], undefined, `${k} 应被 CODEBUDDY_ 前缀剥掉`);
|
|
158
|
-
}
|
|
159
|
-
for (const k of ['WORKBUDDY_PAC_RPC_TOKEN', 'WORKBUDDY_PRODUCT_NAME',
|
|
160
|
-
'CLIENT_INFO_PRODUCT_NAME', 'CLIENT_INFO_MACHINE_ID', 'ACC_PRODUCT_CONFIG_PATH',
|
|
161
|
-
'TENCENT_DOCS_LOCAL_MCP', 'TENCENT_DOCS_LOCAL_SERVER',
|
|
162
|
-
'BAGGAGE', 'TRACEPARENT', 'SANDBOX_CENTER_IPC_ADDRESS', 'SERVER__PORT',
|
|
163
|
-
'EXEPATH', 'ELECTRON_RUN_AS_NODE']) {
|
|
164
|
-
assert.equal(env[k], undefined, `${k} 应被剥掉`);
|
|
165
|
-
}
|
|
166
|
-
// 用户合法认证通道放行(白名单)
|
|
167
|
-
assert.equal(env.CODEBUDDY_API_KEY, 'user-key');
|
|
168
|
-
assert.equal(env.CODEBUDDY_BASE_URL, 'https://example.com/api');
|
|
169
|
-
// 用户自己的变量不受影响
|
|
170
|
-
assert.equal(env.DB_HOST, '192.168.3.44');
|
|
171
|
-
// 自己的三个设定仍在(且顺序上是在剥离之后设置的 —— 先设会被前缀规则误伤)
|
|
172
|
-
assert.equal(env.DISABLE_AUTOUPDATER, '1');
|
|
173
|
-
assert.equal(env.CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS, '1');
|
|
174
|
-
assert.equal(env.LANG, 'zh_CN.UTF-8');
|
|
175
|
-
} finally {
|
|
176
|
-
for (const [k, v] of Object.entries(had)) {
|
|
177
|
-
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('codebuddy 事件:init 落 session_id,text/thinking/tool_use 分派', () => {
|
|
183
|
-
const job = {};
|
|
184
|
-
const sink = makeSink();
|
|
185
|
-
const tracker = makeTrackerSpy();
|
|
186
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
187
|
-
|
|
188
|
-
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-1', apiKeySource: 'www.workbuddy.cn', tools: [] });
|
|
189
|
-
handle({ type: 'assistant', message: { content: [{ type: 'text', text: '你好' }] } });
|
|
190
|
-
handle({ type: 'assistant', message: { content: [{ type: 'thinking', thinking: '先想想' }] } });
|
|
191
|
-
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tu-1', name: 'Bash', input: { command: 'ls' } }] } });
|
|
192
|
-
|
|
193
|
-
assert.equal(job.claudeSessionId, 'cbc-sess-1');
|
|
194
|
-
assert.deepEqual(sink.output, ['你好']);
|
|
195
|
-
assert.deepEqual(sink.thinking, ['先想想']);
|
|
196
|
-
assert.equal(tracker.records.length, 1);
|
|
197
|
-
assert.deepEqual(tracker.records[0], { id: 'tu-1', name: 'Bash', input: { command: 'ls' }, status: 'running' });
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
test('codebuddy 事件:user 里的 tool_result 回指 tool_use_id,is_error 映射 error', () => {
|
|
201
|
-
const job = {};
|
|
202
|
-
const sink = makeSink();
|
|
203
|
-
const tracker = makeTrackerSpy();
|
|
204
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
205
|
-
|
|
206
|
-
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: 'a.txt\nb.txt' }] } });
|
|
207
|
-
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-2', is_error: true, content: 'boom' }] } });
|
|
208
|
-
|
|
209
|
-
assert.equal(tracker.records.length, 2);
|
|
210
|
-
assert.equal(tracker.records[0].id, 'tu-1');
|
|
211
|
-
assert.equal(tracker.records[0].status, 'done');
|
|
212
|
-
assert.match(String(tracker.records[0].output), /a\.txt/);
|
|
213
|
-
assert.equal(tracker.records[1].status, 'error');
|
|
214
|
-
assert.match(String(tracker.records[1].error), /boom/);
|
|
215
|
-
// tool_result 是回指,不当正文吐出来
|
|
216
|
-
assert.deepEqual(sink.output, []);
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
test('codebuddy 事件:cbc 特有事件(system/status、ai-title、result)一律忽略', () => {
|
|
220
|
-
const job = {};
|
|
221
|
-
const sink = makeSink();
|
|
222
|
-
const tracker = makeTrackerSpy();
|
|
223
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
224
|
-
|
|
225
|
-
handle({ type: 'system', subtype: 'status', status: 'compacting', session_id: 's' });
|
|
226
|
-
handle({ type: 'ai-title', title: '某个标题' });
|
|
227
|
-
handle({ type: 'result', subtype: 'success', is_error: false, session_id: 's', usage: {} });
|
|
228
|
-
// 未知/畸形事件也不能抛错(NDJSON 分行解析里抛错会打死整轮对话)
|
|
229
|
-
handle(null);
|
|
230
|
-
handle('not-an-object');
|
|
231
|
-
handle({ type: 'assistant' });
|
|
232
|
-
handle({ type: 'assistant', message: { content: 'string-not-array' } });
|
|
233
|
-
handle({ type: 'user', message: { content: 'plain user prompt' } });
|
|
234
|
-
|
|
235
|
-
assert.deepEqual(sink.output, []);
|
|
236
|
-
assert.deepEqual(sink.thinking, []);
|
|
237
|
-
assert.equal(tracker.records.length, 0);
|
|
238
|
-
assert.equal(job.claudeSessionId, undefined);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
test('codebuddy 事件:工具调用是两条式(running → tool_result),不是一次性终态', () => {
|
|
242
|
-
const job = {};
|
|
243
|
-
const sink = makeSink();
|
|
244
|
-
const tracker = makeTrackerSpy();
|
|
245
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
246
|
-
|
|
247
|
-
// 实测序列(2026-10-12):Glob running → Glob 结果 → Read running → Read 结果 → 正文
|
|
248
|
-
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'call_1', name: 'Glob', input: { pattern: '**/a.txt' } }] } });
|
|
249
|
-
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'call_1', content: [{ type: 'text', text: '["a.txt"]' }] }] } });
|
|
250
|
-
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'call_2', name: 'Read', input: { file_path: 'a.txt' } }] } });
|
|
251
|
-
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'call_2', content: [{ type: 'text', text: 'hello' }] }] } });
|
|
252
|
-
handle({ type: 'assistant', message: { content: [{ type: 'text', text: '读完了' }] } });
|
|
253
|
-
|
|
254
|
-
assert.deepEqual(tracker.records.map(r => [r.id, r.status]), [
|
|
255
|
-
['call_1', 'running'],
|
|
256
|
-
['call_1', 'done'],
|
|
257
|
-
['call_2', 'running'],
|
|
258
|
-
['call_2', 'done'],
|
|
259
|
-
]);
|
|
260
|
-
// tool_result.content 是块数组时抽文本(与 claude 的 extractToolText 同口径)
|
|
261
|
-
assert.match(String(tracker.records[1].output), /a\.txt/);
|
|
262
|
-
assert.deepEqual(sink.output, ['读完了']);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
test('codebuddy 事件:init 重复推送(实测出现过两条)只认第一条', () => {
|
|
266
|
-
const job = {};
|
|
267
|
-
const sink = makeSink();
|
|
268
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, makeTrackerSpy());
|
|
269
|
-
|
|
270
|
-
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-x' });
|
|
271
|
-
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-x' });
|
|
272
|
-
|
|
273
|
-
assert.equal(job.claudeSessionId, 'cbc-sess-x');
|
|
274
|
-
assert.deepEqual(sink.output, []);
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
// 2026-10-21 实测(CLI 2.168.0,一轮派了 Explore 子智能体):一轮里出现 4 条 init ——
|
|
278
|
-
// 主会话(78 件工具)→ 子会话(36 件工具)→ 子会话再补一条 → 结尾主会话又补一条。
|
|
279
|
-
// 子会话**不注册进会话库**(CLI 日志 "Child session skipped registration"),
|
|
280
|
-
// 拿它的 id 去 --resume 只会得到 {"type":"error","error":"No conversation found with
|
|
281
|
-
// session ID: …"} 且退出码 0 —— 用户看到的就是"续接不输出、直接结束"。
|
|
282
|
-
test('codebuddy 事件:子智能体的 init 不能顶掉主会话 id(--resume 全靠它)', () => {
|
|
283
|
-
const job = {};
|
|
284
|
-
const sink = makeSink();
|
|
285
|
-
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, makeTrackerSpy());
|
|
286
|
-
|
|
287
|
-
handle({ type: 'system', subtype: 'init', session_id: 'main-sess', tools: new Array(78).fill('t') });
|
|
288
|
-
// 子会话 init(工具集更小,且紧跟一条带 parent_tool_use_id 的 assistant)
|
|
289
|
-
handle({ type: 'system', subtype: 'init', session_id: 'sub-sess', tools: new Array(36).fill('t') });
|
|
290
|
-
handle({ type: 'assistant', parent_tool_use_id: 'call_1', message: { content: [{ type: 'text', text: '子智能体的汇报' }] } });
|
|
291
|
-
// 结尾主会话再补一条 init
|
|
292
|
-
handle({ type: 'system', subtype: 'init', session_id: 'main-sess' });
|
|
293
|
-
|
|
294
|
-
assert.equal(job.claudeSessionId, 'main-sess', '续接标识必须留在主会话上');
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test('codebuddy 事件:error / result.is_error 落 agentError(退出码 0 也不能当完成)', () => {
|
|
298
|
-
// 形状取自实测:用错的会话 id 续接时 CLI 只吐这一条,然后 exit 0
|
|
299
|
-
const job = {};
|
|
300
|
-
const handle = createCodebuddyEventHandler(job, () => {}, () => {}, makeTrackerSpy());
|
|
301
|
-
handle({ type: 'error', error: 'No conversation found with session ID: sub-sess' });
|
|
302
|
-
assert.equal(job.agentError, 'No conversation found with session ID: sub-sess');
|
|
303
|
-
|
|
304
|
-
// result 的失败分支:errors 是错误对象/字符串数组,没有 result 字段
|
|
305
|
-
const job2 = {};
|
|
306
|
-
const handle2 = createCodebuddyEventHandler(job2, () => {}, () => {}, makeTrackerSpy());
|
|
307
|
-
handle2({ type: 'result', subtype: 'error_during_execution', is_error: true, errors: ['Authentication required'] });
|
|
308
|
-
assert.equal(job2.agentError, 'Authentication required');
|
|
309
|
-
|
|
310
|
-
// success 的 result 不能误判成失败(它带 result 正文,is_error=false)
|
|
311
|
-
const job3 = {};
|
|
312
|
-
const sink3 = makeSink();
|
|
313
|
-
const handle3 = createCodebuddyEventHandler(job3, sink3.appendOutput, sink3.appendThinking, makeTrackerSpy());
|
|
314
|
-
handle3({ type: 'result', subtype: 'success', is_error: false, result: 'ok' });
|
|
315
|
-
assert.equal(job3.agentError, undefined);
|
|
316
|
-
assert.deepEqual(sink3.output, [], 'result.result 是终态快照,不是新的正文分片');
|
|
317
|
-
});
|
|
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
|
+
// codebuddy 执行器(CodeBuddy Code CLI)的回归测试(2026-10-12)。
|
|
17
|
+
//
|
|
18
|
+
// 钉住的口径(全部来自当天实测,CLI 2.164.0):
|
|
19
|
+
// 1. 白名单 / 归一化里有 codebuddy(config.js 与 taskRunner 两份清单一致)
|
|
20
|
+
// 2. spawn argv:-p + stream-json + -y + **--port 0**(避开桌面版占用的 62443)
|
|
21
|
+
// + 续聊时 --resume <id>;**不出现** --verbose(cbc 不需要,claude 才需要)
|
|
22
|
+
// 3. 子进程环境:DISABLE_AUTOUPDATER / CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS
|
|
23
|
+
// 必须带上(丢了不会报错,只会某天任务跑到一半被自动更新换掉二进制);
|
|
24
|
+
// 桌面版注入的五个前缀族 + 单键必须剥掉 —— CODEBUDDY_HOST 与
|
|
25
|
+
// CODEBUDDY_FORCE_LITE_WB_BUNDLE 是 2026-10-12 实测的两个认证杀手
|
|
26
|
+
// (后者命中桌面版专属 lite-wb bundle 分流);官方认证通道
|
|
27
|
+
// CODEBUDDY_API_KEY / CODEBUDDY_BASE_URL 必须放行
|
|
28
|
+
// 4. 事件映射:init → session_id;assistant 的 text/thinking/tool_use 分派;
|
|
29
|
+
// user 里的 tool_result 回指;cbc 特有的 system/status、ai-title、result 一律忽略
|
|
30
|
+
// 5. 工具调用是"先 tool_use(running)、后 user(tool_result)"两条,不是一次性终态
|
|
31
|
+
// (runExternalTurn 的 announced 补 start 机制就是为后者准备的,两条式直接命中)
|
|
32
|
+
//
|
|
33
|
+
// 隔离策略同 taskRunner.opencode.test.js:import taskRunner.js 之前把
|
|
34
|
+
// USERPROFILE/HOME 指向 mkdtemp 沙箱(taskRunner → jobStore import 时就会 hydrate)。
|
|
35
|
+
|
|
36
|
+
import { test } from 'node:test';
|
|
37
|
+
import assert from 'node:assert/strict';
|
|
38
|
+
import path from 'node:path';
|
|
39
|
+
import os from 'node:os';
|
|
40
|
+
import { mkdtempSync } from 'node:fs';
|
|
41
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
42
|
+
|
|
43
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
44
|
+
const projectRoot = path.resolve(__dirname, '../../../../..');
|
|
45
|
+
|
|
46
|
+
// ---- 环境隔离:必须在 import taskRunner.js 之前完成 ----
|
|
47
|
+
const sandbox = mkdtempSync(path.join(os.tmpdir(), 'zen-taskrunner-cb-test-'));
|
|
48
|
+
process.env.USERPROFILE = sandbox;
|
|
49
|
+
process.env.HOME = sandbox;
|
|
50
|
+
delete process.env.HOMEDRIVE;
|
|
51
|
+
delete process.env.HOMEPATH;
|
|
52
|
+
|
|
53
|
+
const {
|
|
54
|
+
createCodebuddyEventHandler,
|
|
55
|
+
buildCodebuddyArgs,
|
|
56
|
+
buildCodebuddyEnv,
|
|
57
|
+
normalizeTaskExecutor,
|
|
58
|
+
TASK_EXECUTORS,
|
|
59
|
+
} = await import(
|
|
60
|
+
pathToFileURL(path.join(projectRoot, 'src/ui/server/routes/workbench/taskRunner.js')).href
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** 可断言的 appendOutput/appendThinking 收集器 */
|
|
64
|
+
function makeSink() {
|
|
65
|
+
const sink = { output: [], thinking: [] };
|
|
66
|
+
sink.appendOutput = (t) => { if (t) sink.output.push(t); };
|
|
67
|
+
sink.appendThinking = (t) => { if (t) sink.thinking.push(t); };
|
|
68
|
+
return sink;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** tracker 探针:只记 handler 喂了哪些 record(不碰真 tracker 的落盘/推送) */
|
|
72
|
+
function makeTrackerSpy() {
|
|
73
|
+
const records = [];
|
|
74
|
+
return {
|
|
75
|
+
records,
|
|
76
|
+
record: (r) => records.push(r),
|
|
77
|
+
flush: () => {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
test('normalizeTaskExecutor:codebuddy 在白名单里,非法值回落 claude', () => {
|
|
82
|
+
assert.deepEqual(TASK_EXECUTORS, ['claude', 'opencode', 'codex', 'codebuddy', 'pi', 'codearts']);
|
|
83
|
+
assert.equal(normalizeTaskExecutor('codebuddy'), 'codebuddy');
|
|
84
|
+
assert.equal(normalizeTaskExecutor('CodeBuddy'), 'codebuddy');
|
|
85
|
+
assert.equal(normalizeTaskExecutor('codebuddy '), 'codebuddy');
|
|
86
|
+
// 历史默认不能变:老配置/老 job 缺 executor 时仍落在 claude
|
|
87
|
+
assert.equal(normalizeTaskExecutor(undefined), 'claude');
|
|
88
|
+
assert.equal(normalizeTaskExecutor('cursor'), 'claude');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('buildCodebuddyArgs:基础 argv 带 -p/stream-json/-y/--port 0,不带 --verbose', () => {
|
|
92
|
+
const args = buildCodebuddyArgs('');
|
|
93
|
+
assert.deepEqual(args, ['-p', '--output-format', 'stream-json', '-y', '--port', '0']);
|
|
94
|
+
// claude 的 stream-json 必须配 --verbose,cbc 不需要 —— 多传一个少传一个都是坑,
|
|
95
|
+
// 这里双向钉住:不许出现 verbose,也不许漏掉 port 0
|
|
96
|
+
assert.ok(!args.includes('--verbose'));
|
|
97
|
+
assert.equal(args[args.indexOf('--port') + 1], '0');
|
|
98
|
+
// 不传模型:跟随 CLI 自己的配置(与另三家同一口径)
|
|
99
|
+
assert.ok(!args.includes('--model'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('buildCodebuddyArgs:有续聊 id 时 --resume 在前', () => {
|
|
103
|
+
const args = buildCodebuddyArgs('sess-abc');
|
|
104
|
+
assert.deepEqual(args.slice(0, 2), ['--resume', 'sess-abc']);
|
|
105
|
+
assert.ok(args.includes('-p'));
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('buildCodebuddyEnv:禁自动更新 + 禁后台任务 + LANG,前缀级剥掉桌面版注入变量', () => {
|
|
109
|
+
// 模拟"g gui 在 WorkBuddy 终端里被拉起":父进程带着桌面版整套注入变量。
|
|
110
|
+
// 覆盖五个前缀族 + 单键,每族挑代表;逐键断言剥离不留死角。
|
|
111
|
+
const had = {};
|
|
112
|
+
const pollution = {
|
|
113
|
+
// ★ 两个实测认证杀手
|
|
114
|
+
CODEBUDDY_HOST: 'workbuddy-desktop',
|
|
115
|
+
CODEBUDDY_FORCE_LITE_WB_BUNDLE: '1',
|
|
116
|
+
CODEBUDDY_FORCE_HEADLESS_BUNDLE: '1',
|
|
117
|
+
// 其余 CODEBUDDY_* 家族代表(认证辅助 / 网关 / 设备指纹 / 配置目录 / 会话追踪)
|
|
118
|
+
CODEBUDDY_API_KEY_HELPER_DISABLED: '1',
|
|
119
|
+
CODEBUDDY_GATEWAY_PASSWORD: 'secret',
|
|
120
|
+
CODEBUDDY_QIMEI36: 'device-fp',
|
|
121
|
+
CODEBUDDY_CONFIG_DIR: 'C:\\Users\\xuze\\.workbuddy',
|
|
122
|
+
CODEBUDDY_MCP_CONFIG: '{"mcpServers":{}}',
|
|
123
|
+
CODEBUDDY_SESSION_ID: 'some-session',
|
|
124
|
+
CODEBUDDY_CURRENT_MODEL_ID: 'some-model',
|
|
125
|
+
// WORKBUDDY_* / CLIENT_INFO_* / ACC_* / TENCENT_DOCS_LOCAL_* 族代表
|
|
126
|
+
WORKBUDDY_PAC_RPC_TOKEN: 'secret',
|
|
127
|
+
WORKBUDDY_PRODUCT_NAME: 'WorkBuddy',
|
|
128
|
+
CLIENT_INFO_PRODUCT_NAME: 'WorkBuddy',
|
|
129
|
+
CLIENT_INFO_MACHINE_ID: 'machine-id',
|
|
130
|
+
ACC_PRODUCT_CONFIG_PATH: 'C:\\Temp\\acc-product-config-v3.json',
|
|
131
|
+
TENCENT_DOCS_LOCAL_MCP: 'http://127.0.0.1:39099/mcp',
|
|
132
|
+
TENCENT_DOCS_LOCAL_SERVER: 'http://127.0.0.1:39099',
|
|
133
|
+
// 单键族:遥测追踪 / 沙箱 / 网关 / 运行时路径
|
|
134
|
+
BAGGAGE: 'codebuddy.session_id=xxx',
|
|
135
|
+
TRACEPARENT: '00-abc-def-01',
|
|
136
|
+
SANDBOX_CENTER_IPC_ADDRESS: '\\\\.\\pipe\\WorkBuddy_SandboxCenter_x',
|
|
137
|
+
SERVER__PORT: '52710',
|
|
138
|
+
EXEPATH: 'C:\\Users\\xuze\\AppData\\Local\\Programs\\WorkBuddy\\resources\\vendor\\PortableGit\\bin',
|
|
139
|
+
ELECTRON_RUN_AS_NODE: '1',
|
|
140
|
+
// 反例:用户合法认证通道(官方文档 headless 认证)必须保留
|
|
141
|
+
CODEBUDDY_API_KEY: 'user-key',
|
|
142
|
+
CODEBUDDY_BASE_URL: 'https://example.com/api',
|
|
143
|
+
// 反例:用户自己永久设置的变量不在剥离范围
|
|
144
|
+
DB_HOST: '192.168.3.44',
|
|
145
|
+
};
|
|
146
|
+
for (const [k, v] of Object.entries(pollution)) { had[k] = process.env[k]; process.env[k] = v; }
|
|
147
|
+
try {
|
|
148
|
+
const env = buildCodebuddyEnv();
|
|
149
|
+
// ★ 两个杀手必须剥掉:带上任一 CLI 会跑桌面版 bundle / 找桌面版凭据 → Authentication
|
|
150
|
+
// required(2026-10-12 逐变量二分 + 日志取证)。这两条测试就是那道防线。
|
|
151
|
+
assert.equal(env.CODEBUDDY_HOST, undefined);
|
|
152
|
+
assert.equal(env.CODEBUDDY_FORCE_LITE_WB_BUNDLE, undefined);
|
|
153
|
+
assert.equal(env.CODEBUDDY_FORCE_HEADLESS_BUNDLE, undefined);
|
|
154
|
+
for (const k of ['CODEBUDDY_API_KEY_HELPER_DISABLED', 'CODEBUDDY_GATEWAY_PASSWORD',
|
|
155
|
+
'CODEBUDDY_QIMEI36', 'CODEBUDDY_CONFIG_DIR', 'CODEBUDDY_MCP_CONFIG',
|
|
156
|
+
'CODEBUDDY_SESSION_ID', 'CODEBUDDY_CURRENT_MODEL_ID']) {
|
|
157
|
+
assert.equal(env[k], undefined, `${k} 应被 CODEBUDDY_ 前缀剥掉`);
|
|
158
|
+
}
|
|
159
|
+
for (const k of ['WORKBUDDY_PAC_RPC_TOKEN', 'WORKBUDDY_PRODUCT_NAME',
|
|
160
|
+
'CLIENT_INFO_PRODUCT_NAME', 'CLIENT_INFO_MACHINE_ID', 'ACC_PRODUCT_CONFIG_PATH',
|
|
161
|
+
'TENCENT_DOCS_LOCAL_MCP', 'TENCENT_DOCS_LOCAL_SERVER',
|
|
162
|
+
'BAGGAGE', 'TRACEPARENT', 'SANDBOX_CENTER_IPC_ADDRESS', 'SERVER__PORT',
|
|
163
|
+
'EXEPATH', 'ELECTRON_RUN_AS_NODE']) {
|
|
164
|
+
assert.equal(env[k], undefined, `${k} 应被剥掉`);
|
|
165
|
+
}
|
|
166
|
+
// 用户合法认证通道放行(白名单)
|
|
167
|
+
assert.equal(env.CODEBUDDY_API_KEY, 'user-key');
|
|
168
|
+
assert.equal(env.CODEBUDDY_BASE_URL, 'https://example.com/api');
|
|
169
|
+
// 用户自己的变量不受影响
|
|
170
|
+
assert.equal(env.DB_HOST, '192.168.3.44');
|
|
171
|
+
// 自己的三个设定仍在(且顺序上是在剥离之后设置的 —— 先设会被前缀规则误伤)
|
|
172
|
+
assert.equal(env.DISABLE_AUTOUPDATER, '1');
|
|
173
|
+
assert.equal(env.CODEBUDDY_CODE_DISABLE_BACKGROUND_TASKS, '1');
|
|
174
|
+
assert.equal(env.LANG, 'zh_CN.UTF-8');
|
|
175
|
+
} finally {
|
|
176
|
+
for (const [k, v] of Object.entries(had)) {
|
|
177
|
+
if (v === undefined) delete process.env[k]; else process.env[k] = v;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('codebuddy 事件:init 落 session_id,text/thinking/tool_use 分派', () => {
|
|
183
|
+
const job = {};
|
|
184
|
+
const sink = makeSink();
|
|
185
|
+
const tracker = makeTrackerSpy();
|
|
186
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
187
|
+
|
|
188
|
+
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-1', apiKeySource: 'www.workbuddy.cn', tools: [] });
|
|
189
|
+
handle({ type: 'assistant', message: { content: [{ type: 'text', text: '你好' }] } });
|
|
190
|
+
handle({ type: 'assistant', message: { content: [{ type: 'thinking', thinking: '先想想' }] } });
|
|
191
|
+
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'tu-1', name: 'Bash', input: { command: 'ls' } }] } });
|
|
192
|
+
|
|
193
|
+
assert.equal(job.claudeSessionId, 'cbc-sess-1');
|
|
194
|
+
assert.deepEqual(sink.output, ['你好']);
|
|
195
|
+
assert.deepEqual(sink.thinking, ['先想想']);
|
|
196
|
+
assert.equal(tracker.records.length, 1);
|
|
197
|
+
assert.deepEqual(tracker.records[0], { id: 'tu-1', name: 'Bash', input: { command: 'ls' }, status: 'running' });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('codebuddy 事件:user 里的 tool_result 回指 tool_use_id,is_error 映射 error', () => {
|
|
201
|
+
const job = {};
|
|
202
|
+
const sink = makeSink();
|
|
203
|
+
const tracker = makeTrackerSpy();
|
|
204
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
205
|
+
|
|
206
|
+
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-1', content: 'a.txt\nb.txt' }] } });
|
|
207
|
+
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'tu-2', is_error: true, content: 'boom' }] } });
|
|
208
|
+
|
|
209
|
+
assert.equal(tracker.records.length, 2);
|
|
210
|
+
assert.equal(tracker.records[0].id, 'tu-1');
|
|
211
|
+
assert.equal(tracker.records[0].status, 'done');
|
|
212
|
+
assert.match(String(tracker.records[0].output), /a\.txt/);
|
|
213
|
+
assert.equal(tracker.records[1].status, 'error');
|
|
214
|
+
assert.match(String(tracker.records[1].error), /boom/);
|
|
215
|
+
// tool_result 是回指,不当正文吐出来
|
|
216
|
+
assert.deepEqual(sink.output, []);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('codebuddy 事件:cbc 特有事件(system/status、ai-title、result)一律忽略', () => {
|
|
220
|
+
const job = {};
|
|
221
|
+
const sink = makeSink();
|
|
222
|
+
const tracker = makeTrackerSpy();
|
|
223
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
224
|
+
|
|
225
|
+
handle({ type: 'system', subtype: 'status', status: 'compacting', session_id: 's' });
|
|
226
|
+
handle({ type: 'ai-title', title: '某个标题' });
|
|
227
|
+
handle({ type: 'result', subtype: 'success', is_error: false, session_id: 's', usage: {} });
|
|
228
|
+
// 未知/畸形事件也不能抛错(NDJSON 分行解析里抛错会打死整轮对话)
|
|
229
|
+
handle(null);
|
|
230
|
+
handle('not-an-object');
|
|
231
|
+
handle({ type: 'assistant' });
|
|
232
|
+
handle({ type: 'assistant', message: { content: 'string-not-array' } });
|
|
233
|
+
handle({ type: 'user', message: { content: 'plain user prompt' } });
|
|
234
|
+
|
|
235
|
+
assert.deepEqual(sink.output, []);
|
|
236
|
+
assert.deepEqual(sink.thinking, []);
|
|
237
|
+
assert.equal(tracker.records.length, 0);
|
|
238
|
+
assert.equal(job.claudeSessionId, undefined);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test('codebuddy 事件:工具调用是两条式(running → tool_result),不是一次性终态', () => {
|
|
242
|
+
const job = {};
|
|
243
|
+
const sink = makeSink();
|
|
244
|
+
const tracker = makeTrackerSpy();
|
|
245
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, tracker);
|
|
246
|
+
|
|
247
|
+
// 实测序列(2026-10-12):Glob running → Glob 结果 → Read running → Read 结果 → 正文
|
|
248
|
+
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'call_1', name: 'Glob', input: { pattern: '**/a.txt' } }] } });
|
|
249
|
+
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'call_1', content: [{ type: 'text', text: '["a.txt"]' }] }] } });
|
|
250
|
+
handle({ type: 'assistant', message: { content: [{ type: 'tool_use', id: 'call_2', name: 'Read', input: { file_path: 'a.txt' } }] } });
|
|
251
|
+
handle({ type: 'user', message: { content: [{ type: 'tool_result', tool_use_id: 'call_2', content: [{ type: 'text', text: 'hello' }] }] } });
|
|
252
|
+
handle({ type: 'assistant', message: { content: [{ type: 'text', text: '读完了' }] } });
|
|
253
|
+
|
|
254
|
+
assert.deepEqual(tracker.records.map(r => [r.id, r.status]), [
|
|
255
|
+
['call_1', 'running'],
|
|
256
|
+
['call_1', 'done'],
|
|
257
|
+
['call_2', 'running'],
|
|
258
|
+
['call_2', 'done'],
|
|
259
|
+
]);
|
|
260
|
+
// tool_result.content 是块数组时抽文本(与 claude 的 extractToolText 同口径)
|
|
261
|
+
assert.match(String(tracker.records[1].output), /a\.txt/);
|
|
262
|
+
assert.deepEqual(sink.output, ['读完了']);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('codebuddy 事件:init 重复推送(实测出现过两条)只认第一条', () => {
|
|
266
|
+
const job = {};
|
|
267
|
+
const sink = makeSink();
|
|
268
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, makeTrackerSpy());
|
|
269
|
+
|
|
270
|
+
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-x' });
|
|
271
|
+
handle({ type: 'system', subtype: 'init', session_id: 'cbc-sess-x' });
|
|
272
|
+
|
|
273
|
+
assert.equal(job.claudeSessionId, 'cbc-sess-x');
|
|
274
|
+
assert.deepEqual(sink.output, []);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 2026-10-21 实测(CLI 2.168.0,一轮派了 Explore 子智能体):一轮里出现 4 条 init ——
|
|
278
|
+
// 主会话(78 件工具)→ 子会话(36 件工具)→ 子会话再补一条 → 结尾主会话又补一条。
|
|
279
|
+
// 子会话**不注册进会话库**(CLI 日志 "Child session skipped registration"),
|
|
280
|
+
// 拿它的 id 去 --resume 只会得到 {"type":"error","error":"No conversation found with
|
|
281
|
+
// session ID: …"} 且退出码 0 —— 用户看到的就是"续接不输出、直接结束"。
|
|
282
|
+
test('codebuddy 事件:子智能体的 init 不能顶掉主会话 id(--resume 全靠它)', () => {
|
|
283
|
+
const job = {};
|
|
284
|
+
const sink = makeSink();
|
|
285
|
+
const handle = createCodebuddyEventHandler(job, sink.appendOutput, sink.appendThinking, makeTrackerSpy());
|
|
286
|
+
|
|
287
|
+
handle({ type: 'system', subtype: 'init', session_id: 'main-sess', tools: new Array(78).fill('t') });
|
|
288
|
+
// 子会话 init(工具集更小,且紧跟一条带 parent_tool_use_id 的 assistant)
|
|
289
|
+
handle({ type: 'system', subtype: 'init', session_id: 'sub-sess', tools: new Array(36).fill('t') });
|
|
290
|
+
handle({ type: 'assistant', parent_tool_use_id: 'call_1', message: { content: [{ type: 'text', text: '子智能体的汇报' }] } });
|
|
291
|
+
// 结尾主会话再补一条 init
|
|
292
|
+
handle({ type: 'system', subtype: 'init', session_id: 'main-sess' });
|
|
293
|
+
|
|
294
|
+
assert.equal(job.claudeSessionId, 'main-sess', '续接标识必须留在主会话上');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('codebuddy 事件:error / result.is_error 落 agentError(退出码 0 也不能当完成)', () => {
|
|
298
|
+
// 形状取自实测:用错的会话 id 续接时 CLI 只吐这一条,然后 exit 0
|
|
299
|
+
const job = {};
|
|
300
|
+
const handle = createCodebuddyEventHandler(job, () => {}, () => {}, makeTrackerSpy());
|
|
301
|
+
handle({ type: 'error', error: 'No conversation found with session ID: sub-sess' });
|
|
302
|
+
assert.equal(job.agentError, 'No conversation found with session ID: sub-sess');
|
|
303
|
+
|
|
304
|
+
// result 的失败分支:errors 是错误对象/字符串数组,没有 result 字段
|
|
305
|
+
const job2 = {};
|
|
306
|
+
const handle2 = createCodebuddyEventHandler(job2, () => {}, () => {}, makeTrackerSpy());
|
|
307
|
+
handle2({ type: 'result', subtype: 'error_during_execution', is_error: true, errors: ['Authentication required'] });
|
|
308
|
+
assert.equal(job2.agentError, 'Authentication required');
|
|
309
|
+
|
|
310
|
+
// success 的 result 不能误判成失败(它带 result 正文,is_error=false)
|
|
311
|
+
const job3 = {};
|
|
312
|
+
const sink3 = makeSink();
|
|
313
|
+
const handle3 = createCodebuddyEventHandler(job3, sink3.appendOutput, sink3.appendThinking, makeTrackerSpy());
|
|
314
|
+
handle3({ type: 'result', subtype: 'success', is_error: false, result: 'ok' });
|
|
315
|
+
assert.equal(job3.agentError, undefined);
|
|
316
|
+
assert.deepEqual(sink3.output, [], 'result.result 是终态快照,不是新的正文分片');
|
|
317
|
+
});
|