@harness-mix/cli 0.2.2 → 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.
Files changed (45) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +469 -467
  3. package/output/native-build/desktop-controller.mjs +1 -1
  4. package/output/native-build/renderer-extension.js +23 -4
  5. package/package.json +16 -9
  6. package/scripts/antigravity-adapter-test.cjs +647 -626
  7. package/scripts/codex-adapter-test.cjs +162 -127
  8. package/scripts/collaboration-test.cjs +274 -262
  9. package/scripts/core-review-test.cjs +68 -0
  10. package/scripts/delegation-await-test.cjs +76 -0
  11. package/scripts/jsonl-stdin-test.cjs +40 -0
  12. package/scripts/kiro-cursor-adapters-test.cjs +124 -100
  13. package/scripts/native-acp-depth-test.cjs +30 -5
  14. package/scripts/native-protocol-test.cjs +14 -1
  15. package/scripts/native-update-apply-test.cjs +269 -215
  16. package/scripts/native-update.cjs +78 -0
  17. package/scripts/native-vendor-adapters-test.cjs +196 -154
  18. package/scripts/salvage-rollout-writes.cjs +72 -0
  19. package/scripts/send-cancel-race-test.cjs +80 -0
  20. package/scripts/send-pre-turn-cancel-test.cjs +100 -0
  21. package/scripts/stuck-turn-test.cjs +6 -1
  22. package/scripts/zcode-adapter-test.cjs +329 -0
  23. package/scripts/zcode-live-probe.cjs +66 -0
  24. package/src/main/adapters/antigravity.js +1428 -1415
  25. package/src/main/adapters/codex.js +656 -649
  26. package/src/main/adapters/native-acp-command.js +51 -48
  27. package/src/main/adapters/native-acp.js +47 -12
  28. package/src/main/adapters/qoder.js +12 -8
  29. package/src/main/adapters/zcode.js +921 -10
  30. package/src/main/harness-adapter/event-normalizer.js +5 -2
  31. package/src/main/host/collaboration.js +723 -715
  32. package/src/main/host/jsonl.js +130 -116
  33. package/src/main/host/runtime.js +30 -14
  34. package/src/main/native/config.js +9 -9
  35. package/src/main/native/host.js +2 -0
  36. package/src/main/native/launcher.js +252 -237
  37. package/src/main/native/process-utils.js +157 -57
  38. package/src/main/native/protocol.js +1221 -1177
  39. package/src/main/native/secure-store.js +2 -0
  40. package/src/main/native/update-state.js +123 -110
  41. package/src/main/native/updater.js +460 -394
  42. package/src/main/workspace/core-review.js +13 -5
  43. package/src/native-ui/desktop-control/src/renderer-cdp-control-session.ts +358 -358
  44. package/src/native-ui/renderer-extension/src/renderer-binding-probe.ts +3211 -3181
  45. package/src/native-ui/renderer-extension/src/settings/connections-page.ts +2 -2
@@ -1,127 +1,162 @@
1
- const assert = require('node:assert/strict');
2
- const { projectNotification, queueRequest, usageView, modelView } = require('../src/main/adapters/codex');
3
-
4
- function fakeSession() {
5
- return {
6
- nativeSessionId: 'thread-native',
7
- pendingApprovals: new Map(),
8
- state: {
9
- turn: null, compaction: null, nativeTurnId: null, usage: undefined,
10
- itemText: new Map(), reasoningItems: new Set(), toolOutput: new Map(),
11
- },
12
- };
13
- }
14
-
15
- (async () => {
16
- const session = fakeSession();
17
- const events = [];
18
- const emit = event => events.push(event);
19
- projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'turn-1' } } }, session, emit);
20
- projectNotification({ method: 'item/agentMessage/delta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'answer-1', delta: '你好' } }, session, emit);
21
- projectNotification({ method: 'item/reasoning/summaryTextDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'reason-1', delta: '分析' } }, session, emit);
22
- projectNotification({ method: 'item/started', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'inProgress' } } }, session, emit);
23
- projectNotification({ method: 'item/commandExecution/outputDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-1', delta: 'clean' } }, session, emit);
24
- projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'completed', aggregatedOutput: 'clean' } } }, session, emit);
25
- projectNotification({ method: 'thread/tokenUsage/updated', params: { threadId: 'thread-native', turnId: 'turn-1', tokenUsage: { last: { totalTokens: 250 }, total: { inputTokens: 200, outputTokens: 50, cachedInputTokens: 25 }, modelContextWindow: 1000 } } }, session, emit);
26
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, session, emit);
27
-
28
- assert.ok(events.some(event => event.kind === 'text-delta' && event.text === '你好'));
29
- assert.ok(events.some(event => event.kind === 'thinking-delta' && event.text === '分析'));
30
- assert.ok(events.some(event => event.kind === 'tool' && event.toolCallId === 'tool-1' && event.state === 'done' && event.title === 'exec_command' && event.input === 'git status'));
31
- assert.ok(events.some(event => event.kind === 'usage' && event.usage.contextPercent === 25));
32
- assert.ok(events.some(event => event.kind === 'completed' && event.nativeRef.checkpointId === 'turn-1'));
33
-
34
- const questionSession = fakeSession();
35
- const questions = [];
36
- const answer = queueRequest({ id: 7, method: 'item/tool/requestUserInput', params: {
37
- threadId: 'thread-native', turnId: 'turn-1', itemId: 'question-1', isBlocking: true,
38
- questions: [
39
- { id: 'framework', header: '框架', question: '选择框架', options: [{ label: 'React' }, { label: 'Vue' }] },
40
- { id: 'name', header: '名称', question: '输入名称', options: null },
41
- ],
42
- } }, questionSession, event => questions.push(event));
43
- assert.equal(questions.length, 2);
44
- const adapter = require('../src/main/adapters/codex').create();
45
- await adapter.respond(questionSession, 'codex-7-framework', { optionId: 'React' });
46
- await adapter.respond(questionSession, 'codex-7-name', { value: 'Harness Mix' });
47
- assert.deepEqual(await answer, { answers: { framework: { answers: ['React'] }, name: { answers: ['Harness Mix'] } } });
48
-
49
- const approvalSession = fakeSession();
50
- const approval = queueRequest({ id: 8, method: 'item/commandExecution/requestApproval', params: {
51
- threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-2', command: 'npm test', availableDecisions: ['accept', 'decline'],
52
- } }, approvalSession, () => {});
53
- await adapter.respond(approvalSession, 'codex-8', { optionId: 'accept' });
54
- assert.deepEqual(await approval, { decision: 'accept' });
55
- const mcpSession = fakeSession();
56
- const mcpEvents = [];
57
- const mcpApproval = queueRequest({ id: 9, method: 'mcpServer/elicitation/request', params: {
58
- threadId: 'thread-native', serverName: 'harness-mix', mode: 'form', message: 'Allow tool?',
59
- requestedSchema: { type: 'object', properties: {}, additionalProperties: false },
60
- } }, mcpSession, event => mcpEvents.push(event));
61
- assert.equal(mcpEvents[0].kind, 'approval');
62
- assert.equal(mcpSession.pendingApprovals.size, 1, 'MCP requests await a native user response');
63
- await adapter.respond(mcpSession, 'codex-9', { optionId: 'decline' });
64
- assert.deepEqual(await mcpApproval, { action: 'decline', content: null });
65
-
66
- const compactSession = fakeSession();
67
- const compactEvents = [];
68
- compactSession.host = { async request(method) {
69
- assert.equal(method, 'thread/compact/start');
70
- projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'compact-turn' } } }, compactSession, event => compactEvents.push(event));
71
- projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'compact-turn', item: { type: 'contextCompaction', id: 'compact-item' } } }, compactSession, event => compactEvents.push(event));
72
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'compact-turn', status: 'completed' } } }, compactSession, event => compactEvents.push(event));
73
- return {};
74
- } };
75
- await adapter.executeCommand(compactSession, 'compact', { emit: event => compactEvents.push(event) });
76
- const compactDone = compactEvents.find(event => event.kind === 'completed');
77
- assert.equal(compactDone.nativeRef.checkpointId, 'compact-turn');
78
- assert.ok(compactEvents.some(event => event.kind === 'text-delta' && /Codex 压缩/.test(event.text)));
79
-
80
- assert.equal(usageView({ last: { totalTokens: 50 }, total: {}, modelContextWindow: 200 }).contextPercent, 25);
81
- assert.deepEqual(modelView({ model: 'gpt-x', displayName: 'GPT X', supportedReasoningEfforts: [], isDefault: true }).id, 'gpt-x');
82
-
83
- // 权限透传:Desktop 选择(回合级)优先,缺失时回退线程级设置,均转发到原生 turn/start
84
- const sendSession = fakeSession();
85
- sendSession.model = { id: 'gpt-x' };
86
- sendSession.turnPermissions = { approvalPolicy: 'on-request', sandboxPolicy: { type: 'readOnly' } };
87
- const sentRequests = [];
88
- sendSession.host = { async request(method, params) { sentRequests.push({ method, params }); return { turn: { id: `turn-${sentRequests.length}` } }; } };
89
- const first = adapter.send(sendSession, '第一轮', {}, { images: [], turnPermissions: { approvalPolicy: 'never', approvalsReviewer: 'guardian', sandboxPolicy: { type: 'dangerFullAccess' } } });
90
- const turnStart = sentRequests.find(r => r.method === 'turn/start');
91
- assert.equal(turnStart.params.approvalPolicy, 'never', '回合级权限覆盖优先转发');
92
- assert.equal(turnStart.params.approvalsReviewer, 'guardian_subagent', 'Desktop 的 guardian 归一化为原生枚举');
93
- assert.deepEqual(turnStart.params.sandboxPolicy, { type: 'dangerFullAccess' });
94
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
95
- await first;
96
- sentRequests.length = 0;
97
- const second = adapter.send(sendSession, '第二轮', {}, { images: [] });
98
- const fallbackStart = sentRequests.find(r => r.method === 'turn/start');
99
- assert.equal(fallbackStart.params.approvalPolicy, 'on-request', '无线索级覆盖时回退线程级设置');
100
- assert.deepEqual(fallbackStart.params.sandboxPolicy, { type: 'readOnly' });
101
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
102
- await second;
103
-
104
- // Queue resume can race app-server's active-turn cleanup. Retry only that transient
105
- // without surfacing a failed Core turn or duplicating unrelated failures.
106
- const retrySession = fakeSession();
107
- let startAttempts = 0;
108
- retrySession.host = { async request(method) {
109
- assert.equal(method, 'turn/start');
110
- startAttempts++;
111
- if (startAttempts < 3) throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
112
- return { turn: { id: 'turn-after-settlement' } };
113
- } };
114
- const retried = adapter.send(retrySession, '编辑后的排队消息', {}, { images: [] });
115
- while (startAttempts < 3) await new Promise(resolve => setTimeout(resolve, 10));
116
- assert.equal(retrySession.state.nativeTurnId, 'turn-after-settlement');
117
- projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-after-settlement', status: 'completed' } } }, retrySession, () => {});
118
- await retried;
119
- assert.equal(startAttempts, 3, 'Only the transient app-server busy race is retried');
120
-
121
- const fatalSession = fakeSession();
122
- let fatalAttempts = 0;
123
- fatalSession.host = { async request() { fatalAttempts++; throw new Error('Authentication required'); } };
124
- await assert.rejects(adapter.send(fatalSession, '不可重试的错误', {}, { images: [] }), /Authentication required/);
125
- assert.equal(fatalAttempts, 1, 'Unrelated app-server errors are never retried or hidden');
126
- console.log('codex adapter: native notifications, usage, multi-question and approvals passed');
127
- })().catch(error => { console.error(error); process.exitCode = 1; });
1
+ const assert = require('node:assert/strict');
2
+ const { projectNotification, queueRequest, usageView, modelView } = require('../src/main/adapters/codex');
3
+
4
+ function fakeSession() {
5
+ return {
6
+ nativeSessionId: 'thread-native',
7
+ pendingApprovals: new Map(),
8
+ state: {
9
+ turn: null, compaction: null, nativeTurnId: null, usage: undefined,
10
+ itemText: new Map(), reasoningItems: new Set(), toolOutput: new Map(),
11
+ },
12
+ };
13
+ }
14
+
15
+ (async () => {
16
+ const session = fakeSession();
17
+ const events = [];
18
+ const emit = event => events.push(event);
19
+ projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'turn-1' } } }, session, emit);
20
+ projectNotification({ method: 'item/agentMessage/delta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'answer-1', delta: '你好' } }, session, emit);
21
+ projectNotification({ method: 'item/reasoning/summaryTextDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'reason-1', delta: '分析' } }, session, emit);
22
+ projectNotification({ method: 'item/started', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'inProgress' } } }, session, emit);
23
+ projectNotification({ method: 'item/commandExecution/outputDelta', params: { threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-1', delta: 'clean' } }, session, emit);
24
+ projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'turn-1', item: { type: 'commandExecution', id: 'tool-1', command: 'git status', status: 'completed', aggregatedOutput: 'clean' } } }, session, emit);
25
+ projectNotification({ method: 'thread/tokenUsage/updated', params: { threadId: 'thread-native', turnId: 'turn-1', tokenUsage: { last: { totalTokens: 250 }, total: { inputTokens: 200, outputTokens: 50, cachedInputTokens: 25 }, modelContextWindow: 1000 } } }, session, emit);
26
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, session, emit);
27
+
28
+ assert.ok(events.some(event => event.kind === 'text-delta' && event.text === '你好'));
29
+ assert.ok(events.some(event => event.kind === 'thinking-delta' && event.text === '分析'));
30
+ assert.ok(events.some(event => event.kind === 'tool' && event.toolCallId === 'tool-1' && event.state === 'done' && event.title === 'exec_command' && event.input === 'git status'));
31
+ assert.ok(events.some(event => event.kind === 'usage' && event.usage.contextPercent === 25));
32
+ assert.ok(events.some(event => event.kind === 'completed' && event.nativeRef.checkpointId === 'turn-1'));
33
+
34
+ const questionSession = fakeSession();
35
+ const questions = [];
36
+ const answer = queueRequest({ id: 7, method: 'item/tool/requestUserInput', params: {
37
+ threadId: 'thread-native', turnId: 'turn-1', itemId: 'question-1', isBlocking: true,
38
+ questions: [
39
+ { id: 'framework', header: '框架', question: '选择框架', options: [{ label: 'React' }, { label: 'Vue' }] },
40
+ { id: 'name', header: '名称', question: '输入名称', options: null },
41
+ ],
42
+ } }, questionSession, event => questions.push(event));
43
+ assert.equal(questions.length, 2);
44
+ const adapter = require('../src/main/adapters/codex').create();
45
+ await adapter.respond(questionSession, 'codex-7-framework', { optionId: 'React' });
46
+ await adapter.respond(questionSession, 'codex-7-name', { value: 'Harness Mix' });
47
+ assert.deepEqual(await answer, { answers: { framework: { answers: ['React'] }, name: { answers: ['Harness Mix'] } } });
48
+
49
+ const approvalSession = fakeSession();
50
+ const approval = queueRequest({ id: 8, method: 'item/commandExecution/requestApproval', params: {
51
+ threadId: 'thread-native', turnId: 'turn-1', itemId: 'tool-2', command: 'npm test', availableDecisions: ['accept', 'decline'],
52
+ } }, approvalSession, () => {});
53
+ await adapter.respond(approvalSession, 'codex-8', { optionId: 'accept' });
54
+ assert.deepEqual(await approval, { decision: 'accept' });
55
+ const mcpSession = fakeSession();
56
+ const mcpEvents = [];
57
+ const mcpApproval = queueRequest({ id: 9, method: 'mcpServer/elicitation/request', params: {
58
+ threadId: 'thread-native', serverName: 'harness-mix', mode: 'form', message: 'Allow tool?',
59
+ requestedSchema: { type: 'object', properties: {}, additionalProperties: false },
60
+ } }, mcpSession, event => mcpEvents.push(event));
61
+ assert.equal(mcpEvents[0].kind, 'approval');
62
+ assert.equal(mcpSession.pendingApprovals.size, 1, 'MCP requests await a native user response');
63
+ await adapter.respond(mcpSession, 'codex-9', { optionId: 'decline' });
64
+ assert.deepEqual(await mcpApproval, { action: 'decline', content: null });
65
+
66
+ const compactSession = fakeSession();
67
+ const compactEvents = [];
68
+ compactSession.host = { async request(method) {
69
+ assert.equal(method, 'thread/compact/start');
70
+ projectNotification({ method: 'turn/started', params: { threadId: 'thread-native', turn: { id: 'compact-turn' } } }, compactSession, event => compactEvents.push(event));
71
+ projectNotification({ method: 'item/completed', params: { threadId: 'thread-native', turnId: 'compact-turn', item: { type: 'contextCompaction', id: 'compact-item' } } }, compactSession, event => compactEvents.push(event));
72
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'compact-turn', status: 'completed' } } }, compactSession, event => compactEvents.push(event));
73
+ return {};
74
+ } };
75
+ await adapter.executeCommand(compactSession, 'compact', { emit: event => compactEvents.push(event) });
76
+ const compactDone = compactEvents.find(event => event.kind === 'completed');
77
+ assert.equal(compactDone.nativeRef.checkpointId, 'compact-turn');
78
+ assert.ok(compactEvents.some(event => event.kind === 'text-delta' && /Codex 压缩/.test(event.text)));
79
+
80
+ assert.equal(usageView({ last: { totalTokens: 50 }, total: {}, modelContextWindow: 200 }).contextPercent, 25);
81
+ assert.deepEqual(modelView({ model: 'gpt-x', displayName: 'GPT X', supportedReasoningEfforts: [], isDefault: true }).id, 'gpt-x');
82
+
83
+ // 权限透传:Desktop 选择(回合级)优先,缺失时回退线程级设置,均转发到原生 turn/start
84
+ const sendSession = fakeSession();
85
+ sendSession.model = { id: 'gpt-x' };
86
+ sendSession.turnPermissions = { approvalPolicy: 'on-request', sandboxPolicy: { type: 'readOnly' } };
87
+ const sentRequests = [];
88
+ sendSession.host = { async request(method, params) { sentRequests.push({ method, params }); return { turn: { id: `turn-${sentRequests.length}` } }; } };
89
+ const first = adapter.send(sendSession, '第一轮', {}, { images: [], turnPermissions: { approvalPolicy: 'never', approvalsReviewer: 'guardian', sandboxPolicy: { type: 'dangerFullAccess' } } });
90
+ const turnStart = sentRequests.find(r => r.method === 'turn/start');
91
+ assert.equal(turnStart.params.approvalPolicy, 'never', '回合级权限覆盖优先转发');
92
+ assert.equal(turnStart.params.approvalsReviewer, 'guardian_subagent', 'Desktop 的 guardian 归一化为原生枚举');
93
+ assert.deepEqual(turnStart.params.sandboxPolicy, { type: 'dangerFullAccess' });
94
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
95
+ await first;
96
+ sentRequests.length = 0;
97
+ const second = adapter.send(sendSession, '第二轮', {}, { images: [] });
98
+ const fallbackStart = sentRequests.find(r => r.method === 'turn/start');
99
+ assert.equal(fallbackStart.params.approvalPolicy, 'on-request', '无线索级覆盖时回退线程级设置');
100
+ assert.deepEqual(fallbackStart.params.sandboxPolicy, { type: 'readOnly' });
101
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-1', status: 'completed' } } }, sendSession, () => {});
102
+ await second;
103
+
104
+ // Queue resume can race app-server's active-turn cleanup. Retry only that transient
105
+ // without surfacing a failed Core turn or duplicating unrelated failures.
106
+ const retrySession = fakeSession();
107
+ let startAttempts = 0;
108
+ retrySession.host = { async request(method) {
109
+ assert.equal(method, 'turn/start');
110
+ startAttempts++;
111
+ if (startAttempts < 3) throw new Error("Agent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.");
112
+ return { turn: { id: 'turn-after-settlement' } };
113
+ } };
114
+ const retried = adapter.send(retrySession, '编辑后的排队消息', {}, { images: [] });
115
+ while (startAttempts < 3) await new Promise(resolve => setTimeout(resolve, 10));
116
+ assert.equal(retrySession.state.nativeTurnId, 'turn-after-settlement');
117
+ projectNotification({ method: 'turn/completed', params: { threadId: 'thread-native', turn: { id: 'turn-after-settlement', status: 'completed' } } }, retrySession, () => {});
118
+ await retried;
119
+ assert.equal(startAttempts, 3, 'Only the transient app-server busy race is retried');
120
+
121
+ const fatalSession = fakeSession();
122
+ let fatalAttempts = 0;
123
+ fatalSession.host = { async request() { fatalAttempts++; throw new Error('Authentication required'); } };
124
+ await assert.rejects(adapter.send(fatalSession, '不可重试的错误', {}, { images: [] }), /Authentication required/);
125
+ assert.equal(fatalAttempts, 1, 'Unrelated app-server errors are never retried or hidden');
126
+
127
+ // cancel() 必须能结算每种挂起的原生请求形状:requestUserInput 的条目经由 group
128
+ // 结算(条目本身没有 resolve,直接调用会 TypeError 并吞掉后续 interrupt)。
129
+ const cancelSession = fakeSession();
130
+ const cancelledAnswer = queueRequest({ id: 21, method: 'item/tool/requestUserInput', params: {
131
+ threadId: 'thread-native', turnId: 'turn-c', itemId: 'q-c', isBlocking: true,
132
+ questions: [{ id: 'pick', header: '选择', question: '选择一项', options: [{ label: 'A' }, { label: 'B' }] }],
133
+ } }, cancelSession, () => {});
134
+ const cancelledApproval = queueRequest({ id: 22, method: 'item/commandExecution/requestApproval', params: {
135
+ threadId: 'thread-native', turnId: 'turn-c', itemId: 't-c', command: 'npm test', availableDecisions: ['accept', 'decline'],
136
+ } }, cancelSession, () => {});
137
+ const cancelledElicitation = queueRequest({ id: 23, method: 'mcpServer/elicitation/request', params: {
138
+ threadId: 'thread-native', serverName: 'harness-mix', mode: 'select', message: 'Allow?',
139
+ } }, cancelSession, () => {});
140
+ const cancelRequests = [];
141
+ cancelSession.host = { request: (method) => { cancelRequests.push(method); return Promise.resolve({}); } };
142
+ cancelSession.state.turn = { resolve() {}, reject() {} };
143
+ await adapter.cancel(cancelSession);
144
+ assert.equal(cancelSession.pendingApprovals.size, 0);
145
+ assert.deepEqual(await cancelledAnswer, { answers: {} }, 'group 条目以空答案结算');
146
+ assert.deepEqual(await cancelledApproval, { decision: 'decline' });
147
+ assert.deepEqual(await cancelledElicitation, { action: 'cancel', content: null });
148
+ assert.equal(cancelRequests.length, 0, 'turn/start 尚在途(无 nativeTurnId)时不发 interrupt');
149
+ assert.equal(cancelSession.state.turn, null, '本地回合被释放,线程不会卡在「当前回合尚未结束」');
150
+
151
+ // nativeTurnId 已知时:interrupt 发出后释放本地回合。
152
+ const interruptSession = fakeSession();
153
+ const interrupts = [];
154
+ interruptSession.host = { request: (method) => { interrupts.push(method); return Promise.resolve({}); } };
155
+ interruptSession.state.nativeTurnId = 'turn-live';
156
+ interruptSession.state.turn = { resolve() {}, reject() {} };
157
+ await adapter.cancel(interruptSession);
158
+ assert.deepEqual(interrupts, ['turn/interrupt']);
159
+ assert.equal(interruptSession.state.turn, null);
160
+
161
+ console.log('codex adapter: native notifications, usage, multi-question, approvals and cancel shapes passed');
162
+ })().catch(error => { console.error(error); process.exitCode = 1; });