@harness-mix/cli 0.2.3 → 0.2.4
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/CHANGELOG.md +13 -0
- package/README.md +469 -467
- package/output/native-build/desktop-controller.mjs +1 -1
- package/output/native-build/renderer-extension.js +23 -4
- package/package.json +11 -9
- package/scripts/antigravity-adapter-test.cjs +647 -626
- package/scripts/codex-adapter-test.cjs +162 -127
- package/scripts/collaboration-test.cjs +274 -262
- package/scripts/jsonl-stdin-test.cjs +40 -31
- package/scripts/kiro-cursor-adapters-test.cjs +124 -100
- package/scripts/native-acp-depth-test.cjs +30 -5
- package/scripts/native-update-apply-test.cjs +269 -215
- package/scripts/native-update.cjs +78 -0
- package/scripts/native-vendor-adapters-test.cjs +196 -154
- package/scripts/salvage-rollout-writes.cjs +72 -0
- package/scripts/zcode-adapter-test.cjs +329 -0
- package/scripts/zcode-live-probe.cjs +66 -0
- package/src/main/adapters/antigravity.js +1428 -1418
- package/src/main/adapters/codex.js +656 -649
- package/src/main/adapters/native-acp-command.js +51 -48
- package/src/main/adapters/native-acp.js +47 -12
- package/src/main/adapters/qoder.js +12 -8
- package/src/main/adapters/zcode.js +921 -10
- package/src/main/host/collaboration.js +723 -715
- package/src/main/host/jsonl.js +130 -120
- package/src/main/native/config.js +9 -9
- package/src/main/native/launcher.js +252 -237
- package/src/main/native/process-utils.js +157 -57
- package/src/main/native/protocol.js +1221 -1187
- package/src/main/native/update-state.js +123 -110
- package/src/main/native/updater.js +460 -394
- package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
- package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
- package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
|
@@ -1,31 +1,40 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const { JsonlProcess } = require('../src/main/host/jsonl');
|
|
3
|
-
|
|
4
|
-
// JsonlProcess stdin EPIPE 防护:子进程异常退出/管道破裂后,迟到的 stdin.write 会在
|
|
5
|
-
// 流上异步抛 'error'(EPIPE);Writable 无 error 监听时 Node 将其当作未捕获异常直接
|
|
6
|
-
// crash 宿主进程。修复后错误进入诊断通道,真实失败仍由 exit/error 路径统一结算。
|
|
7
|
-
(async () => {
|
|
8
|
-
let crashed = null;
|
|
9
|
-
const onCrash = error => { crashed = error; };
|
|
10
|
-
process.on('uncaughtException', onCrash);
|
|
11
|
-
const diags = [];
|
|
12
|
-
try {
|
|
13
|
-
const proc = new JsonlProcess(process.execPath, ['-e', 'process.exit(0)'], {}, {
|
|
14
|
-
onDiagnostic(line) { diags.push(String(line)); },
|
|
15
|
-
});
|
|
16
|
-
await new Promise(resolve => proc.child.on('exit', resolve));
|
|
17
|
-
// 迟到写入(管道已破)+ 直接注入流错误:两种形态都不得成为未捕获异常
|
|
18
|
-
proc.notify('late.notify', {});
|
|
19
|
-
proc.send({ type: 'late' });
|
|
20
|
-
proc.child.stdin.emit('error', Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }));
|
|
21
|
-
await new Promise(resolve => setTimeout(resolve, 100));
|
|
22
|
-
assert.equal(crashed, null, `stdin EPIPE 不得成为未捕获异常: ${crashed?.message ?? ''}`);
|
|
23
|
-
assert.ok(diags.some(line => /stdin/.test(line)), 'stdin 流错误进入诊断通道');
|
|
24
|
-
console.log('jsonl-stdin-test: late writes and EPIPE after child exit are swallowed as diagnostics');
|
|
25
|
-
} finally {
|
|
26
|
-
process.removeListener('uncaughtException', onCrash);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { JsonlProcess } = require('../src/main/host/jsonl');
|
|
3
|
+
|
|
4
|
+
// JsonlProcess stdin EPIPE 防护:子进程异常退出/管道破裂后,迟到的 stdin.write 会在
|
|
5
|
+
// 流上异步抛 'error'(EPIPE);Writable 无 error 监听时 Node 将其当作未捕获异常直接
|
|
6
|
+
// crash 宿主进程。修复后错误进入诊断通道,真实失败仍由 exit/error 路径统一结算。
|
|
7
|
+
(async () => {
|
|
8
|
+
let crashed = null;
|
|
9
|
+
const onCrash = error => { crashed = error; };
|
|
10
|
+
process.on('uncaughtException', onCrash);
|
|
11
|
+
const diags = [];
|
|
12
|
+
try {
|
|
13
|
+
const proc = new JsonlProcess(process.execPath, ['-e', 'process.exit(0)'], {}, {
|
|
14
|
+
onDiagnostic(line) { diags.push(String(line)); },
|
|
15
|
+
});
|
|
16
|
+
await new Promise(resolve => proc.child.on('exit', resolve));
|
|
17
|
+
// 迟到写入(管道已破)+ 直接注入流错误:两种形态都不得成为未捕获异常
|
|
18
|
+
proc.notify('late.notify', {});
|
|
19
|
+
proc.send({ type: 'late' });
|
|
20
|
+
proc.child.stdin.emit('error', Object.assign(new Error('write EPIPE'), { code: 'EPIPE' }));
|
|
21
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
22
|
+
assert.equal(crashed, null, `stdin EPIPE 不得成为未捕获异常: ${crashed?.message ?? ''}`);
|
|
23
|
+
assert.ok(diags.some(line => /stdin/.test(line)), 'stdin 流错误进入诊断通道');
|
|
24
|
+
console.log('jsonl-stdin-test: late writes and EPIPE after child exit are swallowed as diagnostics');
|
|
25
|
+
} finally {
|
|
26
|
+
process.removeListener('uncaughtException', onCrash);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 子进程退出时,挂起中的请求必须以带 harnessExited 标记的错误结算——
|
|
30
|
+
// protocol.inspect 依赖该标记把确定性失败标为不可重试,抑制渲染层的重开循环。
|
|
31
|
+
{
|
|
32
|
+
const proc = new JsonlProcess(process.execPath, ['-e', 'setTimeout(() => process.exit(0), 50)'], {}, {});
|
|
33
|
+
const pending = proc.request('never/answered', {});
|
|
34
|
+
await assert.rejects(pending, error => error.harnessExited === true && /进程已退出/.test(error.message));
|
|
35
|
+
console.log('jsonl-stdin-test: pending requests reject with the harnessExited marker');
|
|
36
|
+
}
|
|
37
|
+
})().catch(error => {
|
|
38
|
+
console.error(error);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
});
|
|
@@ -1,100 +1,124 @@
|
|
|
1
|
-
const assert = require('node:assert/strict');
|
|
2
|
-
const { acpAdapter, catalog } = require('../src/main/adapters/acp');
|
|
3
|
-
const { nativeCommand } = require('../src/main/adapters/native-acp-command');
|
|
4
|
-
|
|
5
|
-
if (process.argv.includes('--fixture')) {
|
|
6
|
-
const readline = require('node:readline');
|
|
7
|
-
const write = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
|
|
8
|
-
const configs = [{ id: 'model', currentValue: 'sonnet', options: [{ value: 'sonnet', name: 'Sonnet' }] }, { id: 'effortLevel', currentValue: 'medium', options: [{ value: 'high', name: 'High' }] }];
|
|
9
|
-
let prompt;
|
|
10
|
-
readline.createInterface({ input: process.stdin }).on('line', line => {
|
|
11
|
-
const r = JSON.parse(line);
|
|
12
|
-
if (r.id === 'permission') {
|
|
13
|
-
assert.equal(r.result.outcome.optionId, 'native-once');
|
|
14
|
-
write({ method: 'session/update', params: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'native reply' } } } });
|
|
15
|
-
write({ id: prompt, result: { stopReason: 'end_turn' } });
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
let result = {};
|
|
19
|
-
if (r.method === 'initialize') result = { agentCapabilities: { loadSession: true } };
|
|
20
|
-
else if (r.method === 'session/new' || r.method === 'session/load') result = { sessionId: r.params.sessionId || 'native-session', configOptions: configs };
|
|
21
|
-
else if (r.method === 'session/set_config_option') { configs.find(c => c.id === r.params.configId).currentValue = r.params.value; result = { configOptions: configs }; }
|
|
22
|
-
else if (r.method === 'session/prompt') {
|
|
23
|
-
prompt = r.id;
|
|
24
|
-
write({ id: 'permission', method: 'session/request_permission', params: { toolCall: { title: 'Test native permission' }, options: [{ kind: 'allow_once', optionId: 'native-once' }] } });
|
|
25
|
-
return;
|
|
26
|
-
} else if (r.method === 'session/cancel') return;
|
|
27
|
-
if (r.id !== undefined) write({ id: r.id, result });
|
|
28
|
-
});
|
|
29
|
-
} else {
|
|
30
|
-
(async () => {
|
|
31
|
-
for (const [id, key, argv] of [
|
|
32
|
-
['qoder', 'HARNESS_MIX_QODER_EXECUTABLE', ['--acp']],
|
|
33
|
-
['
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
process.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
process.env
|
|
57
|
-
assert.deepEqual(
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
process.env
|
|
72
|
-
assert.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
assert.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
1
|
+
const assert = require('node:assert/strict');
|
|
2
|
+
const { acpAdapter, catalog } = require('../src/main/adapters/acp');
|
|
3
|
+
const { nativeCommand } = require('../src/main/adapters/native-acp-command');
|
|
4
|
+
|
|
5
|
+
if (process.argv.includes('--fixture')) {
|
|
6
|
+
const readline = require('node:readline');
|
|
7
|
+
const write = value => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', ...value }) + '\n');
|
|
8
|
+
const configs = [{ id: 'model', currentValue: 'sonnet', options: [{ value: 'sonnet', name: 'Sonnet' }] }, { id: 'effortLevel', currentValue: 'medium', options: [{ value: 'high', name: 'High' }] }];
|
|
9
|
+
let prompt;
|
|
10
|
+
readline.createInterface({ input: process.stdin }).on('line', line => {
|
|
11
|
+
const r = JSON.parse(line);
|
|
12
|
+
if (r.id === 'permission') {
|
|
13
|
+
assert.equal(r.result.outcome.optionId, 'native-once');
|
|
14
|
+
write({ method: 'session/update', params: { update: { sessionUpdate: 'agent_message_chunk', content: { type: 'text', text: 'native reply' } } } });
|
|
15
|
+
write({ id: prompt, result: { stopReason: 'end_turn' } });
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let result = {};
|
|
19
|
+
if (r.method === 'initialize') result = { agentCapabilities: { loadSession: true } };
|
|
20
|
+
else if (r.method === 'session/new' || r.method === 'session/load') result = { sessionId: r.params.sessionId || 'native-session', configOptions: configs };
|
|
21
|
+
else if (r.method === 'session/set_config_option') { configs.find(c => c.id === r.params.configId).currentValue = r.params.value; result = { configOptions: configs }; }
|
|
22
|
+
else if (r.method === 'session/prompt') {
|
|
23
|
+
prompt = r.id;
|
|
24
|
+
write({ id: 'permission', method: 'session/request_permission', params: { toolCall: { title: 'Test native permission' }, options: [{ kind: 'allow_once', optionId: 'native-once' }] } });
|
|
25
|
+
return;
|
|
26
|
+
} else if (r.method === 'session/cancel') return;
|
|
27
|
+
if (r.id !== undefined) write({ id: r.id, result });
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
(async () => {
|
|
31
|
+
for (const [id, key, argv] of [
|
|
32
|
+
['qoder', 'HARNESS_MIX_QODER_EXECUTABLE', ['--acp']],
|
|
33
|
+
['trae', 'HARNESS_MIX_TRAE_EXECUTABLE', []],
|
|
34
|
+
]) {
|
|
35
|
+
const previous = process.env[key];
|
|
36
|
+
try {
|
|
37
|
+
delete process.env[key];
|
|
38
|
+
if (id !== 'qoder') assert.throws(() => nativeCommand(id, argv), /ACP/);
|
|
39
|
+
process.env[key] = __filename + '.missing';
|
|
40
|
+
assert.throws(() => nativeCommand(id, argv), /未安装/);
|
|
41
|
+
process.env[key] = __filename;
|
|
42
|
+
assert.deepEqual(nativeCommand(id, argv), { command: process.execPath, args: [__filename, ...argv] });
|
|
43
|
+
} finally { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; }
|
|
44
|
+
}
|
|
45
|
+
// ZCode 走原生 app-server 适配器,不再是 ACP;解析只认无头 zcode.cjs /
|
|
46
|
+
// HARNESS_MIX_ZCODE_EXECUTABLE,旧 ACP 键与 nativeCommand('zcode') 已移除。
|
|
47
|
+
assert.throws(() => nativeCommand('zcode', []), /Unknown native CLI/);
|
|
48
|
+
{
|
|
49
|
+
const zcode = require('../src/main/adapters/zcode');
|
|
50
|
+
assert.equal(zcode.manifest.capabilities.approvals, true);
|
|
51
|
+
assert.equal(zcode.manifest.capabilities.fork, false);
|
|
52
|
+
assert.deepEqual(zcode.manifest.integrations.skills, { global: ['.zcode/skills', '.agents/skills'], project: ['.zcode/skills', '.agents/skills'] });
|
|
53
|
+
const previous = process.env.HARNESS_MIX_ZCODE_EXECUTABLE;
|
|
54
|
+
try {
|
|
55
|
+
delete process.env.HARNESS_MIX_ZCODE_EXECUTABLE;
|
|
56
|
+
process.env.HARNESS_MIX_ZCODE_EXECUTABLE = __filename;
|
|
57
|
+
assert.deepEqual(zcode.resolveLaunch(), { command: process.execPath, args: [__filename, 'app-server', '--stdio'] });
|
|
58
|
+
// 无覆盖且无桌面版捆绑 CLI 时必须明确报错(有的机器装有桌面版,此时允许回退)
|
|
59
|
+
} finally { if (previous === undefined) delete process.env.HARNESS_MIX_ZCODE_EXECUTABLE; else process.env.HARNESS_MIX_ZCODE_EXECUTABLE = previous; }
|
|
60
|
+
}
|
|
61
|
+
for (const name of ['kiro', 'cursor']) {
|
|
62
|
+
const module = require('../src/main/adapters/' + name);
|
|
63
|
+
assert.equal(module.manifest.capabilities.fork, name === 'kiro');
|
|
64
|
+
assert.equal(module.manifest.capabilities.questions, true);
|
|
65
|
+
const key = name === 'kiro' ? 'HARNESS_MIX_KIRO_EXECUTABLE' : 'HARNESS_MIX_CURSOR_EXECUTABLE';
|
|
66
|
+
const previous = process.env[key];
|
|
67
|
+
try {
|
|
68
|
+
process.env[key] = __filename + '.missing';
|
|
69
|
+
assert.throws(() => nativeCommand(module.manifest.id, ['acp']), /未安装/);
|
|
70
|
+
assert.equal((await module.create().inspect()).available, false);
|
|
71
|
+
process.env[key] = process.execPath;
|
|
72
|
+
assert.deepEqual(nativeCommand(module.manifest.id, ['acp']), { command: process.execPath, args: ['acp'] });
|
|
73
|
+
} finally { if (previous === undefined) delete process.env[key]; else process.env[key] = previous; }
|
|
74
|
+
}
|
|
75
|
+
// Qoder:官方 qodercli --acp。能力按 1.1.58 实测声明(fork 走标准 Zed 参数,
|
|
76
|
+
// 思考档位是 reasoning_effort 配置项;CLI 从不发送 usage_update)。
|
|
77
|
+
{
|
|
78
|
+
const module = require('../src/main/adapters/qoder');
|
|
79
|
+
const caps = module.manifest.capabilities;
|
|
80
|
+
assert.deepEqual(module.manifest.integrations.skills, { global: ['.qoder/skills'], project: ['.qoder/skills'] });
|
|
81
|
+
for (const [capability, expected] of [['fork', true], ['questions', false], ['thinkingLevels', true], ['usage', false], ['contextUsage', false], ['resume', true], ['permissionModes', true], ['models', true], ['attachments', true], ['approvals', true], ['compaction', false]])
|
|
82
|
+
assert.equal(caps[capability], expected, `qoder ${capability}`);
|
|
83
|
+
}
|
|
84
|
+
// Cline:官方 `cline --acp`;诚实能力声明(无 plan/原生 diff/thinking 档/独立提问),
|
|
85
|
+
// 保留 resume 与 plan/act 权限模式;命令只认 cline 与 HARNESS_MIX_CLINE_EXECUTABLE。
|
|
86
|
+
{
|
|
87
|
+
const module = require('../src/main/adapters/cline');
|
|
88
|
+
const caps = module.manifest.capabilities;
|
|
89
|
+
assert.deepEqual(module.manifest.integrations.skills, { global: ['.cline/skills'], project: ['.cline/skills'] });
|
|
90
|
+
for (const [capability, expected] of [['fork', false], ['questions', false], ['thinkingLevels', false], ['plan', false], ['nativeDiff', false], ['usage', false], ['resume', true], ['permissionModes', true], ['models', true], ['attachments', true], ['approvals', true]])
|
|
91
|
+
assert.equal(caps[capability], expected, `cline ${capability}`);
|
|
92
|
+
const previous = process.env.HARNESS_MIX_CLINE_EXECUTABLE;
|
|
93
|
+
try {
|
|
94
|
+
delete process.env.HARNESS_MIX_CLINE_EXECUTABLE;
|
|
95
|
+
process.env.HARNESS_MIX_CLINE_EXECUTABLE = __filename + '.missing';
|
|
96
|
+
assert.throws(() => nativeCommand('cline', ['--acp']), /未安装/);
|
|
97
|
+
assert.equal((await module.create().inspect()).available, false);
|
|
98
|
+
process.env.HARNESS_MIX_CLINE_EXECUTABLE = process.execPath;
|
|
99
|
+
assert.deepEqual(nativeCommand('cline', ['--acp']), { command: process.execPath, args: ['--acp'] });
|
|
100
|
+
} finally { if (previous === undefined) delete process.env.HARNESS_MIX_CLINE_EXECUTABLE; else process.env.HARNESS_MIX_CLINE_EXECUTABLE = previous; }
|
|
101
|
+
}
|
|
102
|
+
const adapter = acpAdapter({ id: 'fixture', name: 'Fixture', bin: () => ({ command: process.execPath, args: [__filename, '--fixture'] }), args: [], requestTimeoutMs: 5000 }).create();
|
|
103
|
+
const events = [];
|
|
104
|
+
let session;
|
|
105
|
+
const emit = event => {
|
|
106
|
+
events.push(event);
|
|
107
|
+
if (event.kind === 'approval') queueMicrotask(() => adapter.respond(session, event.requestId, { confirmed: true }));
|
|
108
|
+
};
|
|
109
|
+
try {
|
|
110
|
+
session = await adapter.open({ thread: { cwd: process.cwd() }, emit });
|
|
111
|
+
assert.equal(catalog(session).models[0].id, 'sonnet');
|
|
112
|
+
await adapter.setThinkingLevel(session, 'high');
|
|
113
|
+
assert.equal(session.state.configOptions[1].currentValue, 'high');
|
|
114
|
+
await adapter.send(session, 'hello', { emit });
|
|
115
|
+
assert.ok(events.some(e => e.text === 'native reply'));
|
|
116
|
+
assert.equal(events.filter(e => e.kind === 'completed').length, 1);
|
|
117
|
+
await adapter.close(session);
|
|
118
|
+
session = await adapter.open({ thread: { cwd: process.cwd(), restore: true, nativeSessionId: 'native-session' }, emit });
|
|
119
|
+
assert.equal(session.nativeSessionId, 'native-session');
|
|
120
|
+
await adapter.cancel(session);
|
|
121
|
+
} finally { if (session) await adapter.close(session); }
|
|
122
|
+
console.log('Kiro/Cursor: discovery isolation, config, native approval, streaming, resume and cancel PASS (fixture, not live model)');
|
|
123
|
+
})().catch(error => { console.error(error); process.exitCode = 1; });
|
|
124
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const assert = require('node:assert/strict');
|
|
2
|
-
const { nativeAcp, project } = require('../src/main/adapters/native-acp');
|
|
2
|
+
const { nativeAcp, project, catalog } = require('../src/main/adapters/native-acp');
|
|
3
3
|
const { AcpInteractions } = require('../src/main/adapters/acp-interactions');
|
|
4
4
|
const { historyUsage, branch, latestAssistantAfterUser } = require('../src/main/adapters/codebuddy-history');
|
|
5
5
|
const { projectUsage } = require('../src/main/native/usage');
|
|
@@ -25,10 +25,11 @@ if (process.argv.includes('--fixture')) {
|
|
|
25
25
|
result = { sessionId: sid, configOptions: configs };
|
|
26
26
|
}
|
|
27
27
|
if (r.method === 'session/set_config_option') {
|
|
28
|
-
|
|
28
|
+
// 模拟 qodercli 的模型切换重置思考档位(Max→Flash 实测重置回默认档)
|
|
29
|
+
configs = configs.map(c => c.id === r.params.configId ? { ...c, currentValue: r.params.value } : (r.params.configId === 'model' && c.id === 'thought_level' ? { ...c, currentValue: 'medium' } : c));
|
|
29
30
|
result = { configOptions: configs };
|
|
30
31
|
}
|
|
31
|
-
if (r.method === 'session/fork') { assert.equal(r.params._meta?.kiro?.messageId, 'native-end'); result = { sessionId: randomUUID() }; }
|
|
32
|
+
if (r.method === 'session/fork') { if (r.params._meta) assert.equal(r.params._meta?.kiro?.messageId, 'native-end'); result = { sessionId: randomUUID() }; }
|
|
32
33
|
if (r.method === '_kiro/session/context') result = { usagePercentage: 23 };
|
|
33
34
|
if (r.method === 'session/prompt') {
|
|
34
35
|
if (r.params.prompt[0].text === 'image') assert.deepEqual(r.params.prompt[1], { type: 'image', data: 'AA==', mimeType: 'image/png' });
|
|
@@ -48,7 +49,7 @@ if (process.argv.includes('--fixture')) {
|
|
|
48
49
|
} else {
|
|
49
50
|
(async () => {
|
|
50
51
|
for (const vendor of ['codebuddy', 'kiro-cli', 'cursor-cli', 'qoder', 'zcode', 'trae', 'cline']) {
|
|
51
|
-
const module = nativeAcp({ id: vendor, name: vendor, args: [], timeoutMs: 2000, command: () => ({ command: process.execPath, args: [__filename, '--fixture'] }) });
|
|
52
|
+
const module = nativeAcp({ id: vendor, name: vendor, args: [], timeoutMs: 2000, command: () => ({ command: process.execPath, args: [__filename, '--fixture'] }), ...(vendor === 'qoder' ? { capabilities: { fork: true } } : {}) });
|
|
52
53
|
const adapter = module.create(), events = [];
|
|
53
54
|
let s;
|
|
54
55
|
try {
|
|
@@ -58,6 +59,11 @@ if (process.argv.includes('--fixture')) {
|
|
|
58
59
|
events.length = 0;
|
|
59
60
|
await adapter.setModel(s, { id: 'other' });
|
|
60
61
|
if (vendor !== 'cursor-cli') await adapter.setThinkingLevel(s, 'high');
|
|
62
|
+
if (vendor === 'qoder') {
|
|
63
|
+
// 模型切换重置原生 effort 后,已确认的思考档位必须重放(fixture 模拟了重置)
|
|
64
|
+
await adapter.setModel(s, { id: 'native[variant]' });
|
|
65
|
+
assert.equal(s.state.configOptions.find(c => c.id === 'thought_level').currentValue, 'high', '模型切换后确认的思考档位必须重放');
|
|
66
|
+
}
|
|
61
67
|
const first = adapter.send(s, 'wait', { emit: e => events.push(e) });
|
|
62
68
|
await assert.rejects(adapter.send(s, 'busy', { emit: () => {} }), /busy/);
|
|
63
69
|
await assert.rejects(adapter.setModel(s, { id: 'other' }), /busy/);
|
|
@@ -84,6 +90,12 @@ if (process.argv.includes('--fixture')) {
|
|
|
84
90
|
await assert.rejects(adapter.fork({ cwd: process.cwd(), nativeSessionId: s.nativeSessionId }, { emit: () => {} }), /Compacted/);
|
|
85
91
|
} finally { if (saved === undefined) delete process.env.KIRO_HOME; else process.env.KIRO_HOME = saved; }
|
|
86
92
|
}
|
|
93
|
+
if (vendor === 'qoder') {
|
|
94
|
+
// qoder fork 走标准 Zed 参数(无 kiro checkpoint _meta)
|
|
95
|
+
const forked = await adapter.fork({ cwd: process.cwd(), nativeSessionId: s.nativeSessionId }, { emit: () => {} });
|
|
96
|
+
assert.notEqual(forked.session.nativeSessionId, s.nativeSessionId);
|
|
97
|
+
await adapter.close(forked.session);
|
|
98
|
+
}
|
|
87
99
|
} finally { await adapter.close(s); }
|
|
88
100
|
}
|
|
89
101
|
const events = [], s = { active: true, nativeSessionId: 's', emit: e => events.push(e) };
|
|
@@ -98,10 +110,15 @@ if (process.argv.includes('--fixture')) {
|
|
|
98
110
|
const unresponsive = nativeAcp({ id: 'cursor-cli', name: 'Cursor', args: [], timeoutMs: 500,
|
|
99
111
|
command: () => ({ command: process.execPath, args: [__filename, '--fixture', '--ignore-cancel'] }) }).create();
|
|
100
112
|
const stalled = await unresponsive.open({ thread: { cwd: process.cwd() }, emit: () => {} });
|
|
113
|
+
const stalledSid = stalled.nativeSessionId;
|
|
101
114
|
const stalledTurn = unresponsive.send(stalled, 'wait', { emit: () => {} });
|
|
102
115
|
const failedTurn = assert.rejects(stalledTurn);
|
|
103
116
|
await assert.rejects(unresponsive.cancel(stalled)); await failedTurn;
|
|
104
|
-
assert.ok(stalled.fault);
|
|
117
|
+
assert.ok(stalled.fault);
|
|
118
|
+
// 进程死后下一次 send 透明重连(restore 同一原生会话),不再永久毒化线程
|
|
119
|
+
await unresponsive.send(stalled, 'again', { emit: () => {} });
|
|
120
|
+
assert.equal(stalled.nativeSessionId, stalledSid);
|
|
121
|
+
assert.equal(stalled.fault, null);
|
|
105
122
|
await unresponsive.close(stalled);
|
|
106
123
|
const idle = nativeAcp({ id: 'codebuddy', name: 'CodeBuddy', args: [], timeoutMs: 1000, turnIdleTimeoutMs: 100,
|
|
107
124
|
command: () => ({ command: process.execPath, args: [__filename, '--fixture'] }) }).create();
|
|
@@ -157,6 +174,14 @@ if (process.argv.includes('--fixture')) {
|
|
|
157
174
|
assert.equal(latestAssistantAfterUser([{ type: 'message', id: 'new-u', role: 'user', content: [{ type: 'input_text', text: 'new' }] },
|
|
158
175
|
{ type: 'message', id: 'new-a', role: 'assistant', content: [{ type: 'output_text', text: 'native final' }] }], 'new-u'), 'native final');
|
|
159
176
|
assert.throws(() => branch([{ type: 'message', id: 'a', parentId: 'a' }]), /parent chain/);
|
|
177
|
+
// qoder 的思考档位 id 是 reasoning_effort(category=model),必须照常映射
|
|
178
|
+
const qoderCatalog = catalog({ vendor: 'qoder', state: { configOptions: [
|
|
179
|
+
{ id: 'model', currentValue: 'qfmodel', options: [{ value: 'qfmodel', name: 'Qwen3.8-Flash' }] },
|
|
180
|
+
{ id: 'reasoning_effort', category: 'model', currentValue: 'xhigh', options: [{ value: 'xhigh', name: 'Extra High' }, { value: 'none', name: 'None' }] },
|
|
181
|
+
{ id: 'mode', currentValue: 'default', options: [{ value: 'default' }, { value: 'yolo' }] },
|
|
182
|
+
], models: null, modes: null } });
|
|
183
|
+
assert.deepEqual(qoderCatalog.thinkingLevels.map(o => o.id), ['xhigh', 'none']);
|
|
184
|
+
assert.deepEqual(qoderCatalog.permissionModes.map(o => o.id), ['default', 'yolo']);
|
|
160
185
|
// 进度事件重置 idle,heartbeat-only 不重置:心跳场景下 idle 应当照常起效。
|
|
161
186
|
const heartbeatOnly = nativeAcp({ id: 'codebuddy', name: 'CodeBuddy', args: [], timeoutMs: 1000, turnIdleTimeoutMs: 80, turnPromptTimeoutMs: 60_000, cancelGraceMs: 1000,
|
|
162
187
|
command: () => ({ command: process.execPath, args: [__filename, '--fixture', '--ignore-prompt', '--heartbeat'] }) }).create();
|