aws-runtime-bridge 1.7.50 → 1.8.0
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/dist/adapter/AcodeSdkAdapter.d.ts +30 -3
- package/dist/adapter/AcodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/AcodeSdkAdapter.js +427 -105
- package/dist/adapter/AcodeSdkAdapter.test.js +1093 -86
- package/dist/adapter/ClaudeSdkAdapter.test.js +31 -8
- package/dist/adapter/CodexSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/CodexSdkAdapter.js +2 -0
- package/dist/adapter/OpencodeSdkAdapter.d.ts.map +1 -1
- package/dist/adapter/OpencodeSdkAdapter.js +2 -0
- package/dist/adapter/SdkProviderSpi.d.ts.map +1 -1
- package/dist/adapter/adapter.test.js +2 -2
- package/dist/adapter/idle-poll-loop.d.ts +4 -4
- package/dist/adapter/idle-poll-loop.d.ts.map +1 -1
- package/dist/adapter/idle-poll-loop.js +3 -3
- package/dist/adapter/idle-poll-loop.test.js +15 -1
- package/dist/adapter/types.d.ts +25 -6
- package/dist/adapter/types.d.ts.map +1 -1
- package/dist/adapter/types.js +221 -110
- package/dist/adapter/types.test.js +10 -3
- package/dist/routes/instance.d.ts.map +1 -1
- package/dist/routes/instance.js +6 -7
- package/dist/routes/runtime-binding.js +2 -2
- package/dist/routes/terminal.d.ts +20 -5
- package/dist/routes/terminal.d.ts.map +1 -1
- package/dist/routes/terminal.js +420 -170
- package/dist/routes/terminal.test.js +380 -208
- package/dist/services/mcp-launch-binding-queue.d.ts +1 -0
- package/dist/services/mcp-launch-binding-queue.d.ts.map +1 -1
- package/dist/services/mcp-launch-binding-queue.js +8 -2
- package/dist/services/mcp-launch-binding-queue.test.js +47 -4
- package/dist/services/session-output.d.ts +10 -6
- package/dist/services/session-output.d.ts.map +1 -1
- package/dist/services/session-output.js +29 -14
- package/dist/services/session-output.test.js +93 -39
- package/package/acode/dist/built-in-file-tools.d.ts +1 -1
- package/package/acode/dist/built-in-file-tools.d.ts.map +1 -1
- package/package/acode/dist/built-in-file-tools.js +388 -50
- package/package/acode/dist/runtime.d.ts +11 -3
- package/package/acode/dist/runtime.d.ts.map +1 -1
- package/package/acode/dist/runtime.js +902 -190
- package/package/acode/dist/types.d.ts +18 -5
- package/package/acode/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.js +3 -2
- package/package/aws-client-agent-mcp/dist/mcp-server.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js +11 -9
- package/package/aws-client-agent-mcp/dist/mcp-server.test.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js +1 -0
- package/package/aws-client-agent-mcp/dist/runtime-launch-binding.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts +2 -0
- package/package/aws-client-agent-mcp/dist/status-reporter.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/status-reporter.js +31 -3
- package/package/aws-client-agent-mcp/dist/status-reporter.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts +1 -1
- package/package/aws-client-agent-mcp/dist/types.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/types.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.d.ts.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.js +2 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.js.map +1 -1
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js +43 -0
- package/package/aws-client-agent-mcp/dist/websocket-client.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { EventEmitter } from
|
|
2
|
-
import { describe, expect, it, vi } from
|
|
3
|
-
import { AcodeSdkAdapter, isFailedAcodeTurnComplete } from
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { AcodeSdkAdapter, isFailedAcodeTurnComplete, } from "./AcodeSdkAdapter.js";
|
|
4
4
|
class FailedTurnRuntime extends EventEmitter {
|
|
5
5
|
constructor(config) {
|
|
6
6
|
super();
|
|
@@ -8,14 +8,14 @@ class FailedTurnRuntime extends EventEmitter {
|
|
|
8
8
|
}
|
|
9
9
|
async start() { }
|
|
10
10
|
async submit(_message) {
|
|
11
|
-
this.emit(
|
|
12
|
-
type:
|
|
11
|
+
this.emit("event", {
|
|
12
|
+
type: "error",
|
|
13
13
|
sessionId: this.config.sessionId,
|
|
14
14
|
timestamp: new Date(0).toISOString(),
|
|
15
|
-
data: { message:
|
|
15
|
+
data: { message: "provider down" },
|
|
16
16
|
});
|
|
17
|
-
this.emit(
|
|
18
|
-
type:
|
|
17
|
+
this.emit("event", {
|
|
18
|
+
type: "turn_complete",
|
|
19
19
|
sessionId: this.config.sessionId,
|
|
20
20
|
timestamp: new Date(0).toISOString(),
|
|
21
21
|
data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
|
|
@@ -31,11 +31,67 @@ class McpStatusRuntime extends EventEmitter {
|
|
|
31
31
|
}
|
|
32
32
|
async start() { }
|
|
33
33
|
async submit(_message) {
|
|
34
|
-
this.emit(
|
|
35
|
-
type:
|
|
34
|
+
this.emit("event", {
|
|
35
|
+
type: "mcp_status",
|
|
36
36
|
sessionId: this.config.sessionId,
|
|
37
37
|
timestamp: new Date(0).toISOString(),
|
|
38
|
-
data: {
|
|
38
|
+
data: {
|
|
39
|
+
action: "call_tool",
|
|
40
|
+
server: "aws-mcp",
|
|
41
|
+
tool: "create_memory",
|
|
42
|
+
toolUseId: "tool-call-1",
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async abort() { }
|
|
47
|
+
async stop() { }
|
|
48
|
+
}
|
|
49
|
+
class McpResultRuntime extends EventEmitter {
|
|
50
|
+
constructor(config) {
|
|
51
|
+
super();
|
|
52
|
+
this.config = config;
|
|
53
|
+
}
|
|
54
|
+
async start() { }
|
|
55
|
+
async submit(_message) {
|
|
56
|
+
this.emit("event", {
|
|
57
|
+
type: "mcp_status",
|
|
58
|
+
sessionId: this.config.sessionId,
|
|
59
|
+
timestamp: new Date(0).toISOString(),
|
|
60
|
+
data: {
|
|
61
|
+
action: "call_tool",
|
|
62
|
+
server: "aws-mcp",
|
|
63
|
+
tool: "create_memory",
|
|
64
|
+
toolUseId: "tool-call-1",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
this.emit("event", {
|
|
68
|
+
type: "mcp_status",
|
|
69
|
+
sessionId: this.config.sessionId,
|
|
70
|
+
timestamp: new Date(0).toISOString(),
|
|
71
|
+
data: {
|
|
72
|
+
action: "tool_result",
|
|
73
|
+
server: "aws-mcp",
|
|
74
|
+
tool: "create_memory",
|
|
75
|
+
toolUseId: "tool-call-1",
|
|
76
|
+
result: '{"uri":"core://agent/profile"}',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async abort() { }
|
|
81
|
+
async stop() { }
|
|
82
|
+
}
|
|
83
|
+
class PollMessageStatusRuntime extends EventEmitter {
|
|
84
|
+
constructor(config) {
|
|
85
|
+
super();
|
|
86
|
+
this.config = config;
|
|
87
|
+
}
|
|
88
|
+
async start() { }
|
|
89
|
+
async submit(_message) {
|
|
90
|
+
this.emit("event", {
|
|
91
|
+
type: "mcp_status",
|
|
92
|
+
sessionId: this.config.sessionId,
|
|
93
|
+
timestamp: new Date(0).toISOString(),
|
|
94
|
+
data: { action: "call_tool", server: "aws-mcp", tool: "poll_message" },
|
|
39
95
|
});
|
|
40
96
|
}
|
|
41
97
|
async abort() { }
|
|
@@ -48,11 +104,133 @@ class CommandStatusRuntime extends EventEmitter {
|
|
|
48
104
|
}
|
|
49
105
|
async start() { }
|
|
50
106
|
async submit(_message) {
|
|
51
|
-
this.emit(
|
|
52
|
-
type:
|
|
107
|
+
this.emit("event", {
|
|
108
|
+
type: "command_status",
|
|
53
109
|
sessionId: this.config.sessionId,
|
|
54
110
|
timestamp: new Date(0).toISOString(),
|
|
55
|
-
data: { action:
|
|
111
|
+
data: { action: "slash_command", command: "review" },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
async abort() { }
|
|
115
|
+
async stop() { }
|
|
116
|
+
}
|
|
117
|
+
class AiRequestRuntime extends EventEmitter {
|
|
118
|
+
constructor(config) {
|
|
119
|
+
super();
|
|
120
|
+
this.config = config;
|
|
121
|
+
}
|
|
122
|
+
async start() { }
|
|
123
|
+
async submit(_message) {
|
|
124
|
+
this.emit("event", {
|
|
125
|
+
type: "ai_request",
|
|
126
|
+
sessionId: this.config.sessionId,
|
|
127
|
+
timestamp: new Date(0).toISOString(),
|
|
128
|
+
data: {
|
|
129
|
+
model: "test-model",
|
|
130
|
+
endpoint: "https://example.invalid/v1/chat/completions",
|
|
131
|
+
stream: true,
|
|
132
|
+
messageCount: 3,
|
|
133
|
+
toolCount: 2,
|
|
134
|
+
lastUserMessagePreview: "inspect status",
|
|
135
|
+
requestBody: '{"model":"test-model"}',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async abort() { }
|
|
140
|
+
async stop() { }
|
|
141
|
+
}
|
|
142
|
+
class ThinkingDeltaRuntime extends EventEmitter {
|
|
143
|
+
constructor(config) {
|
|
144
|
+
super();
|
|
145
|
+
this.config = config;
|
|
146
|
+
}
|
|
147
|
+
async start() { }
|
|
148
|
+
async submit(_message) {
|
|
149
|
+
this.emit("event", {
|
|
150
|
+
type: "thinking_delta",
|
|
151
|
+
sessionId: this.config.sessionId,
|
|
152
|
+
timestamp: new Date(0).toISOString(),
|
|
153
|
+
data: { text: "reasoning chunk" },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async abort() { }
|
|
157
|
+
async stop() { }
|
|
158
|
+
}
|
|
159
|
+
class AssistantDeltaRuntime extends EventEmitter {
|
|
160
|
+
constructor(config) {
|
|
161
|
+
super();
|
|
162
|
+
this.config = config;
|
|
163
|
+
}
|
|
164
|
+
async start() { }
|
|
165
|
+
async submit(_message) {
|
|
166
|
+
this.emit("event", {
|
|
167
|
+
type: "assistant_delta",
|
|
168
|
+
sessionId: this.config.sessionId,
|
|
169
|
+
timestamp: new Date(0).toISOString(),
|
|
170
|
+
data: { text: "answer chunk" },
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async abort() { }
|
|
174
|
+
async stop() { }
|
|
175
|
+
}
|
|
176
|
+
class ResponseBodyTurnCompleteRuntime extends EventEmitter {
|
|
177
|
+
constructor(config) {
|
|
178
|
+
super();
|
|
179
|
+
this.config = config;
|
|
180
|
+
}
|
|
181
|
+
async start() { }
|
|
182
|
+
async submit(_message) {
|
|
183
|
+
this.emit("event", {
|
|
184
|
+
type: "turn_complete",
|
|
185
|
+
sessionId: this.config.sessionId,
|
|
186
|
+
timestamp: new Date(0).toISOString(),
|
|
187
|
+
data: {
|
|
188
|
+
usage: { inputTokens: 9, outputTokens: 3 },
|
|
189
|
+
responseBody: '{"choices":[{"message":{"content":"done"}}]}',
|
|
190
|
+
finishReason: "stop",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
async abort() { }
|
|
195
|
+
async stop() { }
|
|
196
|
+
}
|
|
197
|
+
class ProviderUsageTurnCompleteRuntime extends EventEmitter {
|
|
198
|
+
constructor(config) {
|
|
199
|
+
super();
|
|
200
|
+
this.config = config;
|
|
201
|
+
}
|
|
202
|
+
async start() { }
|
|
203
|
+
async submit(_message) {
|
|
204
|
+
this.emit("event", {
|
|
205
|
+
type: "turn_complete",
|
|
206
|
+
sessionId: this.config.sessionId,
|
|
207
|
+
timestamp: new Date(0).toISOString(),
|
|
208
|
+
data: {
|
|
209
|
+
usage: { prompt_tokens: 31, completion_tokens: 58 },
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async abort() { }
|
|
214
|
+
async stop() { }
|
|
215
|
+
}
|
|
216
|
+
class ThinkingThenAssistantDeltaRuntime extends EventEmitter {
|
|
217
|
+
constructor(config) {
|
|
218
|
+
super();
|
|
219
|
+
this.config = config;
|
|
220
|
+
}
|
|
221
|
+
async start() { }
|
|
222
|
+
async submit(_message) {
|
|
223
|
+
this.emit("event", {
|
|
224
|
+
type: "thinking_delta",
|
|
225
|
+
sessionId: this.config.sessionId,
|
|
226
|
+
timestamp: new Date(0).toISOString(),
|
|
227
|
+
data: { text: "thinking chunk" },
|
|
228
|
+
});
|
|
229
|
+
this.emit("event", {
|
|
230
|
+
type: "assistant_delta",
|
|
231
|
+
sessionId: this.config.sessionId,
|
|
232
|
+
timestamp: new Date(1).toISOString(),
|
|
233
|
+
data: { text: "answer chunk" },
|
|
56
234
|
});
|
|
57
235
|
}
|
|
58
236
|
async abort() { }
|
|
@@ -65,14 +243,14 @@ class McpThenSessionCompleteRuntime extends EventEmitter {
|
|
|
65
243
|
}
|
|
66
244
|
async start() { }
|
|
67
245
|
async submit(_message) {
|
|
68
|
-
this.emit(
|
|
69
|
-
type:
|
|
246
|
+
this.emit("event", {
|
|
247
|
+
type: "mcp_status",
|
|
70
248
|
sessionId: this.config.sessionId,
|
|
71
249
|
timestamp: new Date(0).toISOString(),
|
|
72
|
-
data: { action:
|
|
250
|
+
data: { action: "call_tool", server: "aws-mcp", tool: "send_private" },
|
|
73
251
|
});
|
|
74
|
-
this.emit(
|
|
75
|
-
type:
|
|
252
|
+
this.emit("event", {
|
|
253
|
+
type: "session_complete",
|
|
76
254
|
sessionId: this.config.sessionId,
|
|
77
255
|
timestamp: new Date(0).toISOString(),
|
|
78
256
|
data: { exitCode: 0 },
|
|
@@ -104,8 +282,8 @@ class AutoPollRuntime extends EventEmitter {
|
|
|
104
282
|
async submit(message) {
|
|
105
283
|
AutoPollRuntime.submitMessages.push(message);
|
|
106
284
|
if (AutoPollRuntime.submitMessages.length === 1) {
|
|
107
|
-
this.emit(
|
|
108
|
-
type:
|
|
285
|
+
this.emit("event", {
|
|
286
|
+
type: "turn_complete",
|
|
109
287
|
sessionId: this.config.sessionId,
|
|
110
288
|
timestamp: new Date(0).toISOString(),
|
|
111
289
|
data: { usage: { inputTokens: 1, outputTokens: 1 } },
|
|
@@ -116,158 +294,987 @@ class AutoPollRuntime extends EventEmitter {
|
|
|
116
294
|
async stop() { }
|
|
117
295
|
}
|
|
118
296
|
AutoPollRuntime.submitMessages = [];
|
|
119
|
-
|
|
120
|
-
|
|
297
|
+
class TerminalAfterTurnRuntime extends EventEmitter {
|
|
298
|
+
constructor(config) {
|
|
299
|
+
super();
|
|
300
|
+
this.config = config;
|
|
301
|
+
}
|
|
302
|
+
async start() { }
|
|
303
|
+
async submit(message) {
|
|
304
|
+
TerminalAfterTurnRuntime.submitMessages.push(message);
|
|
305
|
+
this.emit("event", {
|
|
306
|
+
type: "turn_complete",
|
|
307
|
+
sessionId: this.config.sessionId,
|
|
308
|
+
timestamp: new Date(0).toISOString(),
|
|
309
|
+
data: { usage: { inputTokens: 1, outputTokens: 1 } },
|
|
310
|
+
});
|
|
311
|
+
this.emit("event", {
|
|
312
|
+
type: "session_complete",
|
|
313
|
+
sessionId: this.config.sessionId,
|
|
314
|
+
timestamp: new Date(0).toISOString(),
|
|
315
|
+
data: { exitCode: 0 },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
async abort() { }
|
|
319
|
+
async stop() { }
|
|
320
|
+
}
|
|
321
|
+
TerminalAfterTurnRuntime.submitMessages = [];
|
|
322
|
+
class FailingAutoPollRuntime extends EventEmitter {
|
|
323
|
+
constructor(config) {
|
|
324
|
+
super();
|
|
325
|
+
this.config = config;
|
|
326
|
+
}
|
|
327
|
+
async start() { }
|
|
328
|
+
async submit(message) {
|
|
329
|
+
FailingAutoPollRuntime.submitMessages.push(message);
|
|
330
|
+
this.emit("event", {
|
|
331
|
+
type: "error",
|
|
332
|
+
sessionId: this.config.sessionId,
|
|
333
|
+
timestamp: new Date(0).toISOString(),
|
|
334
|
+
data: { message: "mcp tool timeout" },
|
|
335
|
+
});
|
|
336
|
+
this.emit("event", {
|
|
337
|
+
type: "turn_complete",
|
|
338
|
+
sessionId: this.config.sessionId,
|
|
339
|
+
timestamp: new Date(0).toISOString(),
|
|
340
|
+
data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async abort() { }
|
|
344
|
+
async stop() { }
|
|
345
|
+
}
|
|
346
|
+
FailingAutoPollRuntime.submitMessages = [];
|
|
347
|
+
class ResolvedWithoutTerminalEventRuntime extends EventEmitter {
|
|
348
|
+
constructor(config) {
|
|
349
|
+
super();
|
|
350
|
+
this.config = config;
|
|
351
|
+
}
|
|
352
|
+
async start() { }
|
|
353
|
+
async submit(message) {
|
|
354
|
+
ResolvedWithoutTerminalEventRuntime.submitMessages.push(message);
|
|
355
|
+
this.emit("event", {
|
|
356
|
+
type: "command_status",
|
|
357
|
+
sessionId: this.config.sessionId,
|
|
358
|
+
timestamp: new Date(0).toISOString(),
|
|
359
|
+
data: { action: "slash_command", command: "review" },
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
async abort() { }
|
|
363
|
+
async stop() { }
|
|
364
|
+
}
|
|
365
|
+
ResolvedWithoutTerminalEventRuntime.submitMessages = [];
|
|
366
|
+
class BareErrorRuntime extends EventEmitter {
|
|
367
|
+
constructor(config) {
|
|
368
|
+
super();
|
|
369
|
+
this.config = config;
|
|
370
|
+
}
|
|
371
|
+
async start() { }
|
|
372
|
+
async submit(message) {
|
|
373
|
+
BareErrorRuntime.submitMessages.push(message);
|
|
374
|
+
this.emit("event", {
|
|
375
|
+
type: "error",
|
|
376
|
+
sessionId: this.config.sessionId,
|
|
377
|
+
timestamp: new Date(0).toISOString(),
|
|
378
|
+
data: { message: "provider emitted bare error" },
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
async abort() { }
|
|
382
|
+
async stop() { }
|
|
383
|
+
}
|
|
384
|
+
BareErrorRuntime.submitMessages = [];
|
|
385
|
+
class RejectingSubmitRuntime extends EventEmitter {
|
|
386
|
+
constructor(_config) {
|
|
387
|
+
super();
|
|
388
|
+
}
|
|
389
|
+
async start() { }
|
|
390
|
+
async submit(message) {
|
|
391
|
+
RejectingSubmitRuntime.submitMessages.push(message);
|
|
392
|
+
throw new Error("submit rejected before turn_complete");
|
|
393
|
+
}
|
|
394
|
+
async abort() { }
|
|
395
|
+
async stop() { }
|
|
396
|
+
}
|
|
397
|
+
RejectingSubmitRuntime.submitMessages = [];
|
|
398
|
+
class StartFailureRuntime extends EventEmitter {
|
|
399
|
+
constructor(_config) {
|
|
400
|
+
super();
|
|
401
|
+
}
|
|
402
|
+
async start() {
|
|
403
|
+
throw new Error("runtime start failed");
|
|
404
|
+
}
|
|
405
|
+
async submit(_message) { }
|
|
406
|
+
async abort() { }
|
|
407
|
+
async stop() { }
|
|
408
|
+
}
|
|
409
|
+
class FailingPollThenSuccessfulUserRuntime extends EventEmitter {
|
|
410
|
+
constructor(config) {
|
|
411
|
+
super();
|
|
412
|
+
this.config = config;
|
|
413
|
+
}
|
|
414
|
+
async start() { }
|
|
415
|
+
async submit(message) {
|
|
416
|
+
FailingPollThenSuccessfulUserRuntime.submitMessages.push(message);
|
|
417
|
+
if (message.includes("poll_message")) {
|
|
418
|
+
this.emit("event", {
|
|
419
|
+
type: "error",
|
|
420
|
+
sessionId: this.config.sessionId,
|
|
421
|
+
timestamp: new Date(0).toISOString(),
|
|
422
|
+
data: { message: "poll failed once" },
|
|
423
|
+
});
|
|
424
|
+
this.emit("event", {
|
|
425
|
+
type: "turn_complete",
|
|
426
|
+
sessionId: this.config.sessionId,
|
|
427
|
+
timestamp: new Date(0).toISOString(),
|
|
428
|
+
data: { failed: true, usage: { inputTokens: 1, outputTokens: 0 } },
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
this.emit("event", {
|
|
433
|
+
type: "turn_complete",
|
|
434
|
+
sessionId: this.config.sessionId,
|
|
435
|
+
timestamp: new Date(0).toISOString(),
|
|
436
|
+
data: { usage: { inputTokens: 1, outputTokens: 1 } },
|
|
437
|
+
});
|
|
438
|
+
this.emit("event", {
|
|
439
|
+
type: "session_complete",
|
|
440
|
+
sessionId: this.config.sessionId,
|
|
441
|
+
timestamp: new Date(0).toISOString(),
|
|
442
|
+
data: { exitCode: 0 },
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
async abort() { }
|
|
446
|
+
async stop() { }
|
|
447
|
+
}
|
|
448
|
+
FailingPollThenSuccessfulUserRuntime.submitMessages = [];
|
|
449
|
+
class StartupMcpStatusRuntime extends EventEmitter {
|
|
450
|
+
constructor(config) {
|
|
451
|
+
super();
|
|
452
|
+
this.config = config;
|
|
453
|
+
}
|
|
454
|
+
async start() {
|
|
455
|
+
// Mirrors the startup mcp_status event emitted by ACode's
|
|
456
|
+
// connectMcpServers before any user interaction. The R3 check in
|
|
457
|
+
// AcodeSdkAdapter validates the `tools` array (the tools the LLM
|
|
458
|
+
// can actually call) rather than the `servers` config keys, so
|
|
459
|
+
// the runtime must emit the tools form here to exercise that path.
|
|
460
|
+
this.emit("event", {
|
|
461
|
+
type: "mcp_status",
|
|
462
|
+
sessionId: this.config.sessionId,
|
|
463
|
+
timestamp: new Date(0).toISOString(),
|
|
464
|
+
data: {
|
|
465
|
+
tools: StartupMcpStatusRuntime.includeAwsMcp
|
|
466
|
+
? [{ name: "aws-mcp__poll_message", source: "aws-mcp.poll_message" }]
|
|
467
|
+
: [{ name: "other-mcp__other_tool", source: "other-mcp.other_tool" }],
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
async submit(_message) { }
|
|
472
|
+
async abort() { }
|
|
473
|
+
async stop() { }
|
|
474
|
+
}
|
|
475
|
+
// When true (default) the emitted startup mcp_status event includes a
|
|
476
|
+
// tool named 'aws-mcp__poll_message', mirroring the happy-path runtime
|
|
477
|
+
// configuration. When false, the tools list intentionally omits
|
|
478
|
+
// poll_message so the R3 check is forced to take the warning path.
|
|
479
|
+
StartupMcpStatusRuntime.includeAwsMcp = true;
|
|
480
|
+
class DifferentlyNamedMcpRuntime extends EventEmitter {
|
|
481
|
+
// Reproduces the bug-report scenario: the user's MCP is configured
|
|
482
|
+
// under a non-default name ('aws-mcp-server') and the runtime
|
|
483
|
+
// reports that name in the legacy `servers` field, but the LLM-
|
|
484
|
+
// visible tools are still prefixed with the canonical 'aws-mcp__'.
|
|
485
|
+
// The R3 check must validate against the tools (not the server
|
|
486
|
+
// names) so this configuration is accepted as healthy.
|
|
487
|
+
constructor(config) {
|
|
488
|
+
super();
|
|
489
|
+
this.config = config;
|
|
490
|
+
}
|
|
491
|
+
async start() {
|
|
492
|
+
this.emit("event", {
|
|
493
|
+
type: "mcp_status",
|
|
494
|
+
sessionId: this.config.sessionId,
|
|
495
|
+
timestamp: new Date(0).toISOString(),
|
|
496
|
+
data: {
|
|
497
|
+
servers: ["aws-mcp-server"],
|
|
498
|
+
tools: [
|
|
499
|
+
{ name: "aws-mcp__poll_message", source: "aws-mcp.poll_message" },
|
|
500
|
+
{
|
|
501
|
+
name: "aws-mcp__submit_response",
|
|
502
|
+
source: "aws-mcp.submit_response",
|
|
503
|
+
},
|
|
504
|
+
],
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
async submit(_message) { }
|
|
509
|
+
async abort() { }
|
|
510
|
+
async stop() { }
|
|
511
|
+
}
|
|
512
|
+
describe("AcodeSdkAdapter turn completion state", () => {
|
|
513
|
+
it("keeps error status for failed turn_complete events", () => {
|
|
121
514
|
expect(isFailedAcodeTurnComplete({ failed: true })).toBe(true);
|
|
122
515
|
expect(isFailedAcodeTurnComplete({ failed: false })).toBe(false);
|
|
123
516
|
expect(isFailedAcodeTurnComplete({ usage: { inputTokens: 1, outputTokens: 2 } })).toBe(false);
|
|
124
517
|
});
|
|
125
|
-
it(
|
|
518
|
+
it("recovers failed turn_complete to waiting_input so the agent never stays stuck in error (R4)", async () => {
|
|
126
519
|
const adapter = new AcodeSdkAdapter();
|
|
127
520
|
adapter.setRuntimeConstructorForTest(FailedTurnRuntime);
|
|
128
521
|
const events = [];
|
|
129
522
|
const statuses = [];
|
|
130
|
-
adapter.on(
|
|
131
|
-
adapter.on(
|
|
132
|
-
await adapter.startSession(
|
|
133
|
-
command:
|
|
523
|
+
adapter.on("event", (event) => events.push(event));
|
|
524
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
525
|
+
await adapter.startSession("session-failed", {
|
|
526
|
+
command: "acode",
|
|
134
527
|
workingDirectory: process.cwd(),
|
|
135
528
|
autoAccept: true,
|
|
136
529
|
extraMcpServers: {},
|
|
137
530
|
envOverrides: {},
|
|
138
531
|
});
|
|
139
|
-
await adapter.sendMessage(
|
|
140
|
-
expect(events.map((event) => event.type)).toEqual([
|
|
141
|
-
|
|
142
|
-
|
|
532
|
+
await adapter.sendMessage("session-failed", "fail please");
|
|
533
|
+
expect(events.map((event) => event.type)).toEqual(["error"]);
|
|
534
|
+
// R4: the session briefly enters 'error' but then recovers to 'waiting_input'
|
|
535
|
+
// so the agent can retry via the idle poll loop instead of being stuck forever.
|
|
536
|
+
expect(statuses).toEqual([
|
|
537
|
+
"starting",
|
|
538
|
+
"waiting_input",
|
|
539
|
+
"thinking",
|
|
540
|
+
"error",
|
|
541
|
+
"waiting_input",
|
|
542
|
+
]);
|
|
543
|
+
expect(adapter.getSessionStatus("session-failed")).toBe("waiting_input");
|
|
143
544
|
});
|
|
144
|
-
it(
|
|
545
|
+
it("forwards ACode turn_complete response metadata", async () => {
|
|
546
|
+
const adapter = new AcodeSdkAdapter();
|
|
547
|
+
adapter.setRuntimeConstructorForTest(ResponseBodyTurnCompleteRuntime);
|
|
548
|
+
const events = [];
|
|
549
|
+
adapter.on("event", (event) => events.push(event));
|
|
550
|
+
await adapter.startSession("session-response-body", {
|
|
551
|
+
command: "acode",
|
|
552
|
+
workingDirectory: process.cwd(),
|
|
553
|
+
autoAccept: true,
|
|
554
|
+
extraMcpServers: {},
|
|
555
|
+
envOverrides: {},
|
|
556
|
+
});
|
|
557
|
+
await adapter.sendMessage("session-response-body", "answer");
|
|
558
|
+
expect(events).toHaveLength(1);
|
|
559
|
+
expect(events[0]).toMatchObject({
|
|
560
|
+
type: "turn_complete",
|
|
561
|
+
data: {
|
|
562
|
+
usage: { inputTokens: 9, outputTokens: 3 },
|
|
563
|
+
responseBody: '{"choices":[{"message":{"content":"done"}}]}',
|
|
564
|
+
finishReason: "stop",
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
it("normalizes provider-native ACode usage fields for context display", async () => {
|
|
569
|
+
const adapter = new AcodeSdkAdapter();
|
|
570
|
+
adapter.setRuntimeConstructorForTest(ProviderUsageTurnCompleteRuntime);
|
|
571
|
+
const events = [];
|
|
572
|
+
const statuses = [];
|
|
573
|
+
adapter.on("event", (event) => events.push(event));
|
|
574
|
+
adapter.on("status-change", (_sessionId, status, actionInfo) => {
|
|
575
|
+
statuses.push({ status, usage: actionInfo?.usage });
|
|
576
|
+
});
|
|
577
|
+
await adapter.startSession("session-provider-usage", {
|
|
578
|
+
command: "acode",
|
|
579
|
+
workingDirectory: process.cwd(),
|
|
580
|
+
autoAccept: true,
|
|
581
|
+
extraMcpServers: {},
|
|
582
|
+
envOverrides: {},
|
|
583
|
+
});
|
|
584
|
+
await adapter.sendMessage("session-provider-usage", "answer");
|
|
585
|
+
expect(events).toHaveLength(1);
|
|
586
|
+
expect(events[0]).toMatchObject({
|
|
587
|
+
type: "turn_complete",
|
|
588
|
+
data: { usage: { inputTokens: 31, outputTokens: 58 } },
|
|
589
|
+
});
|
|
590
|
+
expect(statuses.at(-1)).toEqual({
|
|
591
|
+
status: "waiting_input",
|
|
592
|
+
usage: { inputTokens: 31, outputTokens: 58 },
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
it("maps ACode MCP status events to tool_using instead of thinking", async () => {
|
|
145
596
|
const adapter = new AcodeSdkAdapter();
|
|
146
597
|
adapter.setRuntimeConstructorForTest(McpStatusRuntime);
|
|
147
598
|
const events = [];
|
|
148
599
|
const statuses = [];
|
|
149
|
-
adapter.on(
|
|
150
|
-
adapter.on(
|
|
151
|
-
statuses.push({
|
|
600
|
+
adapter.on("event", (event) => events.push(event));
|
|
601
|
+
adapter.on("status-change", (_sessionId, status, actionInfo) => {
|
|
602
|
+
statuses.push({
|
|
603
|
+
status,
|
|
604
|
+
actionType: actionInfo?.actionType,
|
|
605
|
+
actionDetail: actionInfo?.actionDetail,
|
|
606
|
+
});
|
|
152
607
|
});
|
|
153
|
-
await adapter.startSession(
|
|
154
|
-
command:
|
|
608
|
+
await adapter.startSession("session-mcp", {
|
|
609
|
+
command: "acode",
|
|
155
610
|
workingDirectory: process.cwd(),
|
|
156
611
|
autoAccept: true,
|
|
157
612
|
extraMcpServers: {},
|
|
158
613
|
envOverrides: {},
|
|
159
614
|
});
|
|
160
|
-
await adapter.sendMessage(
|
|
161
|
-
expect(events.map((event) => event.type)).toEqual([
|
|
615
|
+
await adapter.sendMessage("session-mcp", "call mcp");
|
|
616
|
+
expect(events.map((event) => event.type)).toEqual(["tool_use_start"]);
|
|
162
617
|
expect(events[0]?.data).toMatchObject({
|
|
163
|
-
actionType:
|
|
164
|
-
actionLabel:
|
|
165
|
-
actionDetail:
|
|
618
|
+
actionType: "mcp",
|
|
619
|
+
actionLabel: "调用MCP",
|
|
620
|
+
actionDetail: "aws-mcp/create_memory",
|
|
621
|
+
actionId: "tool-call-1",
|
|
622
|
+
toolUseId: "tool-call-1",
|
|
623
|
+
});
|
|
624
|
+
expect(statuses.at(-2)).toEqual({
|
|
625
|
+
status: "tool_using",
|
|
626
|
+
actionType: "mcp",
|
|
627
|
+
actionDetail: "aws-mcp/create_memory",
|
|
628
|
+
});
|
|
629
|
+
expect(statuses.at(-1)).toEqual({
|
|
630
|
+
status: "waiting_input",
|
|
631
|
+
actionType: undefined,
|
|
632
|
+
actionDetail: undefined,
|
|
633
|
+
});
|
|
634
|
+
expect(adapter.getSessionStatus("session-mcp")).toBe("waiting_input");
|
|
635
|
+
});
|
|
636
|
+
it("clears active MCP tool status when ACode reports a tool result", async () => {
|
|
637
|
+
const adapter = new AcodeSdkAdapter();
|
|
638
|
+
adapter.setRuntimeConstructorForTest(McpResultRuntime);
|
|
639
|
+
const events = [];
|
|
640
|
+
const statuses = [];
|
|
641
|
+
adapter.on("event", (event) => events.push(event));
|
|
642
|
+
adapter.on("status-change", (_sessionId, status, actionInfo) => {
|
|
643
|
+
statuses.push({
|
|
644
|
+
status,
|
|
645
|
+
actionType: actionInfo?.actionType,
|
|
646
|
+
actionDetail: actionInfo?.actionDetail,
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
await adapter.startSession("session-mcp-result", {
|
|
650
|
+
command: "acode",
|
|
651
|
+
workingDirectory: process.cwd(),
|
|
652
|
+
autoAccept: true,
|
|
653
|
+
extraMcpServers: {},
|
|
654
|
+
envOverrides: {},
|
|
655
|
+
});
|
|
656
|
+
await adapter.sendMessage("session-mcp-result", "remember profile");
|
|
657
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
658
|
+
"tool_use_start",
|
|
659
|
+
"tool_use_end",
|
|
660
|
+
]);
|
|
661
|
+
expect(events[1]?.data).toMatchObject({
|
|
662
|
+
actionType: "mcp",
|
|
663
|
+
actionLabel: "调用MCP",
|
|
664
|
+
actionDetail: "aws-mcp/create_memory",
|
|
665
|
+
actionId: "tool-call-1",
|
|
666
|
+
toolUseId: "tool-call-1",
|
|
667
|
+
toolResult: '{"uri":"core://agent/profile"}',
|
|
668
|
+
});
|
|
669
|
+
expect(statuses.at(-2)).toEqual({
|
|
670
|
+
status: "thinking",
|
|
671
|
+
actionType: undefined,
|
|
672
|
+
actionDetail: undefined,
|
|
166
673
|
});
|
|
167
674
|
expect(statuses.at(-1)).toEqual({
|
|
168
|
-
status:
|
|
169
|
-
actionType:
|
|
170
|
-
actionDetail:
|
|
675
|
+
status: "waiting_input",
|
|
676
|
+
actionType: undefined,
|
|
677
|
+
actionDetail: undefined,
|
|
171
678
|
});
|
|
172
|
-
expect(adapter.getSessionStatus(
|
|
679
|
+
expect(adapter.getSessionStatus("session-mcp-result")).toBe("waiting_input");
|
|
173
680
|
});
|
|
174
|
-
it(
|
|
681
|
+
it("maps ACode poll_message MCP status events to waiting_input idle state", async () => {
|
|
682
|
+
const adapter = new AcodeSdkAdapter();
|
|
683
|
+
adapter.setRuntimeConstructorForTest(PollMessageStatusRuntime);
|
|
684
|
+
const events = [];
|
|
685
|
+
const statuses = [];
|
|
686
|
+
adapter.on("event", (event) => events.push(event));
|
|
687
|
+
adapter.on("status-change", (_sessionId, status, actionInfo) => {
|
|
688
|
+
statuses.push({
|
|
689
|
+
status,
|
|
690
|
+
actionType: actionInfo?.actionType,
|
|
691
|
+
actionDetail: actionInfo?.actionDetail,
|
|
692
|
+
});
|
|
693
|
+
});
|
|
694
|
+
await adapter.startSession("session-poll-message", {
|
|
695
|
+
command: "acode",
|
|
696
|
+
workingDirectory: process.cwd(),
|
|
697
|
+
autoAccept: true,
|
|
698
|
+
extraMcpServers: {},
|
|
699
|
+
envOverrides: {},
|
|
700
|
+
});
|
|
701
|
+
await adapter.sendMessage("session-poll-message", "listen");
|
|
702
|
+
expect(events.map((event) => event.type)).toEqual(["tool_use_start"]);
|
|
703
|
+
expect(events[0]?.data).toMatchObject({
|
|
704
|
+
actionType: "idle",
|
|
705
|
+
actionLabel: "监听消息",
|
|
706
|
+
actionDetail: "aws-mcp/poll_message",
|
|
707
|
+
});
|
|
708
|
+
expect(statuses.at(-1)).toEqual({
|
|
709
|
+
status: "waiting_input",
|
|
710
|
+
actionType: "idle",
|
|
711
|
+
actionDetail: "aws-mcp/poll_message",
|
|
712
|
+
});
|
|
713
|
+
expect(adapter.getSessionStatus("session-poll-message")).toBe("waiting_input");
|
|
714
|
+
});
|
|
715
|
+
it("forwards ACode command status as thinking without changing session status", async () => {
|
|
175
716
|
const adapter = new AcodeSdkAdapter();
|
|
176
717
|
adapter.setRuntimeConstructorForTest(CommandStatusRuntime);
|
|
177
718
|
const events = [];
|
|
178
719
|
const statuses = [];
|
|
179
|
-
adapter.on(
|
|
180
|
-
adapter.on(
|
|
181
|
-
await adapter.startSession(
|
|
182
|
-
command:
|
|
720
|
+
adapter.on("event", (event) => events.push(event));
|
|
721
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
722
|
+
await adapter.startSession("session-command-status", {
|
|
723
|
+
command: "acode",
|
|
183
724
|
workingDirectory: process.cwd(),
|
|
184
725
|
autoAccept: true,
|
|
185
726
|
extraMcpServers: {},
|
|
186
727
|
envOverrides: {},
|
|
187
728
|
});
|
|
188
|
-
await adapter.sendMessage(
|
|
729
|
+
await adapter.sendMessage("session-command-status", "/review src");
|
|
189
730
|
expect(events).toHaveLength(1);
|
|
190
|
-
expect(events[0]?.type).toBe(
|
|
191
|
-
expect(String(events[0]?.data.text)).toContain(
|
|
192
|
-
expect(statuses).toEqual([
|
|
193
|
-
|
|
731
|
+
expect(events[0]?.type).toBe("thinking");
|
|
732
|
+
expect(String(events[0]?.data.text)).toContain("slash_command");
|
|
733
|
+
expect(statuses).toEqual([
|
|
734
|
+
"starting",
|
|
735
|
+
"waiting_input",
|
|
736
|
+
"thinking",
|
|
737
|
+
"waiting_input",
|
|
738
|
+
]);
|
|
739
|
+
expect(adapter.getSessionStatus("session-command-status")).toBe("waiting_input");
|
|
740
|
+
});
|
|
741
|
+
it("forwards ACode AI request summaries as provider events", async () => {
|
|
742
|
+
const adapter = new AcodeSdkAdapter();
|
|
743
|
+
adapter.setRuntimeConstructorForTest(AiRequestRuntime);
|
|
744
|
+
const events = [];
|
|
745
|
+
adapter.on("event", (event) => events.push(event));
|
|
746
|
+
await adapter.startSession("session-ai-request", {
|
|
747
|
+
command: "acode",
|
|
748
|
+
workingDirectory: process.cwd(),
|
|
749
|
+
autoAccept: true,
|
|
750
|
+
extraMcpServers: {},
|
|
751
|
+
envOverrides: {},
|
|
752
|
+
});
|
|
753
|
+
await adapter.sendMessage("session-ai-request", "inspect status");
|
|
754
|
+
expect(events).toHaveLength(1);
|
|
755
|
+
expect(events[0]).toMatchObject({
|
|
756
|
+
type: "ai_request",
|
|
757
|
+
data: {
|
|
758
|
+
model: "test-model",
|
|
759
|
+
endpoint: "https://example.invalid/v1/chat/completions",
|
|
760
|
+
stream: true,
|
|
761
|
+
messageCount: 3,
|
|
762
|
+
toolCount: 2,
|
|
763
|
+
lastUserMessagePreview: "inspect status",
|
|
764
|
+
requestBody: '{"model":"test-model"}',
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
it("forwards ACode thinking deltas as realtime thinking provider events", async () => {
|
|
769
|
+
const adapter = new AcodeSdkAdapter();
|
|
770
|
+
adapter.setRuntimeConstructorForTest(ThinkingDeltaRuntime);
|
|
771
|
+
const events = [];
|
|
772
|
+
adapter.on("event", (event) => events.push(event));
|
|
773
|
+
await adapter.startSession("session-thinking-delta", {
|
|
774
|
+
command: "acode",
|
|
775
|
+
workingDirectory: process.cwd(),
|
|
776
|
+
autoAccept: true,
|
|
777
|
+
extraMcpServers: {},
|
|
778
|
+
envOverrides: {},
|
|
779
|
+
});
|
|
780
|
+
await adapter.sendMessage("session-thinking-delta", "think aloud");
|
|
781
|
+
expect(events).toHaveLength(1);
|
|
782
|
+
expect(events[0]).toMatchObject({
|
|
783
|
+
type: "thinking",
|
|
784
|
+
data: { text: "reasoning chunk" },
|
|
785
|
+
});
|
|
194
786
|
});
|
|
195
|
-
it(
|
|
787
|
+
it("forwards ACode assistant deltas as realtime text provider events", async () => {
|
|
788
|
+
const adapter = new AcodeSdkAdapter();
|
|
789
|
+
adapter.setRuntimeConstructorForTest(AssistantDeltaRuntime);
|
|
790
|
+
const events = [];
|
|
791
|
+
adapter.on("event", (event) => events.push(event));
|
|
792
|
+
await adapter.startSession("session-assistant-delta", {
|
|
793
|
+
command: "acode",
|
|
794
|
+
workingDirectory: process.cwd(),
|
|
795
|
+
autoAccept: true,
|
|
796
|
+
extraMcpServers: {},
|
|
797
|
+
envOverrides: {},
|
|
798
|
+
});
|
|
799
|
+
await adapter.sendMessage("session-assistant-delta", "answer now");
|
|
800
|
+
expect(events).toHaveLength(1);
|
|
801
|
+
expect(events[0]).toMatchObject({
|
|
802
|
+
type: "text_delta",
|
|
803
|
+
data: { text: "answer chunk" },
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
it("preserves realtime thinking then answer delta order", async () => {
|
|
807
|
+
const adapter = new AcodeSdkAdapter();
|
|
808
|
+
adapter.setRuntimeConstructorForTest(ThinkingThenAssistantDeltaRuntime);
|
|
809
|
+
const events = [];
|
|
810
|
+
adapter.on("event", (event) => events.push(event));
|
|
811
|
+
await adapter.startSession("session-thinking-answer-order", {
|
|
812
|
+
command: "acode",
|
|
813
|
+
workingDirectory: process.cwd(),
|
|
814
|
+
autoAccept: true,
|
|
815
|
+
extraMcpServers: {},
|
|
816
|
+
envOverrides: {},
|
|
817
|
+
});
|
|
818
|
+
await adapter.sendMessage("session-thinking-answer-order", "think then answer");
|
|
819
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
820
|
+
"thinking",
|
|
821
|
+
"text_delta",
|
|
822
|
+
]);
|
|
823
|
+
expect(events.map((event) => event.data.text)).toEqual([
|
|
824
|
+
"thinking chunk",
|
|
825
|
+
"answer chunk",
|
|
826
|
+
]);
|
|
827
|
+
});
|
|
828
|
+
it("recovers to waiting_input when ACode submit resolves without a terminal turn event", async () => {
|
|
829
|
+
ResolvedWithoutTerminalEventRuntime.submitMessages = [];
|
|
830
|
+
const adapter = new AcodeSdkAdapter();
|
|
831
|
+
adapter.setRuntimeConstructorForTest(ResolvedWithoutTerminalEventRuntime);
|
|
832
|
+
const statuses = [];
|
|
833
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
834
|
+
await adapter.startSession("session-resolved-no-terminal", {
|
|
835
|
+
command: "acode",
|
|
836
|
+
workingDirectory: process.cwd(),
|
|
837
|
+
autoAccept: true,
|
|
838
|
+
extraMcpServers: {},
|
|
839
|
+
envOverrides: {},
|
|
840
|
+
});
|
|
841
|
+
await adapter.sendMessage("session-resolved-no-terminal", "/review src");
|
|
842
|
+
expect(ResolvedWithoutTerminalEventRuntime.submitMessages).toEqual([
|
|
843
|
+
"/review src",
|
|
844
|
+
]);
|
|
845
|
+
expect(statuses).toEqual([
|
|
846
|
+
"starting",
|
|
847
|
+
"waiting_input",
|
|
848
|
+
"thinking",
|
|
849
|
+
"waiting_input",
|
|
850
|
+
]);
|
|
851
|
+
expect(adapter.getSessionStatus("session-resolved-no-terminal")).toBe("waiting_input");
|
|
852
|
+
});
|
|
853
|
+
it("forwards session_complete status after MCP calls so UI can clear active tool state", async () => {
|
|
196
854
|
const adapter = new AcodeSdkAdapter();
|
|
197
855
|
adapter.setRuntimeConstructorForTest(McpThenSessionCompleteRuntime);
|
|
198
856
|
const events = [];
|
|
199
857
|
const statuses = [];
|
|
200
|
-
adapter.on(
|
|
201
|
-
adapter.on(
|
|
202
|
-
statuses.push({
|
|
858
|
+
adapter.on("event", (event) => events.push(event));
|
|
859
|
+
adapter.on("status-change", (_sessionId, status, actionInfo) => {
|
|
860
|
+
statuses.push({
|
|
861
|
+
status,
|
|
862
|
+
actionType: actionInfo?.actionType,
|
|
863
|
+
actionDetail: actionInfo?.actionDetail,
|
|
864
|
+
});
|
|
203
865
|
});
|
|
204
|
-
await adapter.startSession(
|
|
205
|
-
command:
|
|
866
|
+
await adapter.startSession("session-mcp-complete", {
|
|
867
|
+
command: "acode",
|
|
206
868
|
workingDirectory: process.cwd(),
|
|
207
869
|
autoAccept: true,
|
|
208
870
|
extraMcpServers: {},
|
|
209
871
|
envOverrides: {},
|
|
210
872
|
});
|
|
211
|
-
await adapter.sendMessage(
|
|
212
|
-
expect(events.map((event) => event.type)).toEqual([
|
|
873
|
+
await adapter.sendMessage("session-mcp-complete", "send private");
|
|
874
|
+
expect(events.map((event) => event.type)).toEqual([
|
|
875
|
+
"tool_use_start",
|
|
876
|
+
"session_complete",
|
|
877
|
+
]);
|
|
213
878
|
expect(statuses.at(-2)).toEqual({
|
|
214
|
-
status:
|
|
215
|
-
actionType:
|
|
216
|
-
actionDetail:
|
|
879
|
+
status: "tool_using",
|
|
880
|
+
actionType: "mcp",
|
|
881
|
+
actionDetail: "aws-mcp/send_private",
|
|
217
882
|
});
|
|
218
883
|
expect(statuses.at(-1)).toEqual({
|
|
219
|
-
status:
|
|
884
|
+
status: "completed",
|
|
220
885
|
actionType: undefined,
|
|
221
886
|
actionDetail: undefined,
|
|
222
887
|
});
|
|
223
|
-
expect(adapter.getSessionStatus(
|
|
888
|
+
expect(adapter.getSessionStatus("session-mcp-complete")).toBe("completed");
|
|
224
889
|
});
|
|
225
|
-
it(
|
|
890
|
+
it("does not block launch completion while the initial prompt runs in the background", async () => {
|
|
226
891
|
DeferredInitialPromptRuntime.submitCalls = 0;
|
|
227
892
|
DeferredInitialPromptRuntime.receivedInitialPrompt = undefined;
|
|
228
893
|
const adapter = new AcodeSdkAdapter();
|
|
229
894
|
adapter.setRuntimeConstructorForTest(DeferredInitialPromptRuntime);
|
|
230
895
|
const statuses = [];
|
|
231
|
-
adapter.on(
|
|
232
|
-
await adapter.startSession(
|
|
233
|
-
command:
|
|
896
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
897
|
+
await adapter.startSession("session-initial-prompt", {
|
|
898
|
+
command: "acode",
|
|
234
899
|
workingDirectory: process.cwd(),
|
|
235
900
|
autoAccept: true,
|
|
236
|
-
initialPrompt:
|
|
901
|
+
initialPrompt: "start working loop",
|
|
237
902
|
extraMcpServers: {},
|
|
238
903
|
envOverrides: {},
|
|
239
904
|
});
|
|
240
905
|
expect(DeferredInitialPromptRuntime.receivedInitialPrompt).toBeUndefined();
|
|
241
906
|
expect(DeferredInitialPromptRuntime.submitCalls).toBe(1);
|
|
242
|
-
expect(statuses).toEqual([
|
|
243
|
-
expect(adapter.getSessionStatus(
|
|
907
|
+
expect(statuses).toEqual(["starting", "waiting_input", "thinking"]);
|
|
908
|
+
expect(adapter.getSessionStatus("session-initial-prompt")).toBe("thinking");
|
|
244
909
|
});
|
|
245
|
-
it(
|
|
910
|
+
it("continues the message loop by auto-submitting poll_message after a completed turn", async () => {
|
|
246
911
|
vi.useFakeTimers();
|
|
247
912
|
AutoPollRuntime.submitMessages = [];
|
|
248
913
|
const adapter = new AcodeSdkAdapter();
|
|
249
914
|
adapter.setRuntimeConstructorForTest(AutoPollRuntime);
|
|
250
915
|
const statuses = [];
|
|
251
|
-
adapter.on(
|
|
916
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
252
917
|
try {
|
|
253
|
-
await adapter.startSession(
|
|
254
|
-
command:
|
|
918
|
+
await adapter.startSession("session-auto-poll", {
|
|
919
|
+
command: "acode",
|
|
255
920
|
workingDirectory: process.cwd(),
|
|
256
921
|
autoAccept: true,
|
|
257
922
|
extraMcpServers: {},
|
|
258
923
|
envOverrides: {},
|
|
259
924
|
});
|
|
260
|
-
await adapter.sendMessage(
|
|
925
|
+
await adapter.sendMessage("session-auto-poll", "handle current work");
|
|
261
926
|
await vi.advanceTimersByTimeAsync(250);
|
|
262
927
|
expect(AutoPollRuntime.submitMessages).toHaveLength(2);
|
|
263
|
-
expect(AutoPollRuntime.submitMessages[0]).toBe(
|
|
264
|
-
expect(AutoPollRuntime.submitMessages[1]).toContain(
|
|
265
|
-
expect(statuses).toEqual([
|
|
266
|
-
|
|
928
|
+
expect(AutoPollRuntime.submitMessages[0]).toBe("handle current work");
|
|
929
|
+
expect(AutoPollRuntime.submitMessages[1]).toContain("poll_message");
|
|
930
|
+
expect(statuses).toEqual([
|
|
931
|
+
"starting",
|
|
932
|
+
"waiting_input",
|
|
933
|
+
"thinking",
|
|
934
|
+
"waiting_input",
|
|
935
|
+
"thinking",
|
|
936
|
+
"waiting_input",
|
|
937
|
+
]);
|
|
938
|
+
expect(adapter.getSessionStatus("session-auto-poll")).toBe("waiting_input");
|
|
939
|
+
}
|
|
940
|
+
finally {
|
|
941
|
+
vi.useRealTimers();
|
|
942
|
+
adapter.cleanup();
|
|
943
|
+
}
|
|
944
|
+
});
|
|
945
|
+
it("cancels a pending auto-poll when a user message arrives", async () => {
|
|
946
|
+
vi.useFakeTimers();
|
|
947
|
+
AutoPollRuntime.submitMessages = [];
|
|
948
|
+
const adapter = new AcodeSdkAdapter();
|
|
949
|
+
adapter.setRuntimeConstructorForTest(AutoPollRuntime);
|
|
950
|
+
try {
|
|
951
|
+
await adapter.startSession("session-cancel-auto-poll", {
|
|
952
|
+
command: "acode",
|
|
953
|
+
workingDirectory: process.cwd(),
|
|
954
|
+
autoAccept: true,
|
|
955
|
+
extraMcpServers: {},
|
|
956
|
+
envOverrides: {},
|
|
957
|
+
});
|
|
958
|
+
await adapter.sendMessage("session-cancel-auto-poll", "handle current work");
|
|
959
|
+
await adapter.sendMessage("session-cancel-auto-poll", "user follow up");
|
|
960
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
961
|
+
expect(AutoPollRuntime.submitMessages).toEqual([
|
|
962
|
+
"handle current work",
|
|
963
|
+
"user follow up",
|
|
964
|
+
expect.stringContaining("poll_message"),
|
|
965
|
+
]);
|
|
966
|
+
expect(adapter.getSessionStatus("session-cancel-auto-poll")).toBe("waiting_input");
|
|
267
967
|
}
|
|
268
968
|
finally {
|
|
269
969
|
vi.useRealTimers();
|
|
270
970
|
adapter.cleanup();
|
|
271
971
|
}
|
|
272
972
|
});
|
|
973
|
+
it("does not schedule auto-poll after terminal session status", async () => {
|
|
974
|
+
vi.useFakeTimers();
|
|
975
|
+
TerminalAfterTurnRuntime.submitMessages = [];
|
|
976
|
+
const adapter = new AcodeSdkAdapter();
|
|
977
|
+
adapter.setRuntimeConstructorForTest(TerminalAfterTurnRuntime);
|
|
978
|
+
try {
|
|
979
|
+
await adapter.startSession("session-terminal-auto-poll", {
|
|
980
|
+
command: "acode",
|
|
981
|
+
workingDirectory: process.cwd(),
|
|
982
|
+
autoAccept: true,
|
|
983
|
+
extraMcpServers: {},
|
|
984
|
+
envOverrides: {},
|
|
985
|
+
});
|
|
986
|
+
await adapter.sendMessage("session-terminal-auto-poll", "complete");
|
|
987
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
988
|
+
expect(TerminalAfterTurnRuntime.submitMessages).toEqual(["complete"]);
|
|
989
|
+
expect(adapter.getSessionStatus("session-terminal-auto-poll")).toBe("completed");
|
|
990
|
+
}
|
|
991
|
+
finally {
|
|
992
|
+
vi.useRealTimers();
|
|
993
|
+
adapter.cleanup();
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
it("arms auto-poll when session is started without an initial prompt (R1)", async () => {
|
|
997
|
+
vi.useFakeTimers();
|
|
998
|
+
AutoPollRuntime.submitMessages = [];
|
|
999
|
+
const adapter = new AcodeSdkAdapter();
|
|
1000
|
+
adapter.setRuntimeConstructorForTest(AutoPollRuntime);
|
|
1001
|
+
const statuses = [];
|
|
1002
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
1003
|
+
try {
|
|
1004
|
+
// No initialPrompt: previously the session sat at 'waiting_input'
|
|
1005
|
+
// forever because no auto-poll timer was armed and no setIdleCommands
|
|
1006
|
+
// was called yet. R1 fix arms the auto-poll on startSession instead.
|
|
1007
|
+
await adapter.startSession("session-r1-no-initial", {
|
|
1008
|
+
command: "acode",
|
|
1009
|
+
workingDirectory: process.cwd(),
|
|
1010
|
+
autoAccept: true,
|
|
1011
|
+
extraMcpServers: {},
|
|
1012
|
+
envOverrides: {},
|
|
1013
|
+
});
|
|
1014
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1015
|
+
// Status sequence: starting -> waiting_input (startSession) ->
|
|
1016
|
+
// thinking (auto-poll fires) -> waiting_input (turn_complete success
|
|
1017
|
+
// re-arms the loop). The 2nd auto-poll is scheduled but not yet fired
|
|
1018
|
+
// because the advance only covers the first 250ms.
|
|
1019
|
+
expect(AutoPollRuntime.submitMessages).toHaveLength(1);
|
|
1020
|
+
expect(AutoPollRuntime.submitMessages[0]).toContain("poll_message");
|
|
1021
|
+
expect(statuses).toEqual([
|
|
1022
|
+
"starting",
|
|
1023
|
+
"waiting_input",
|
|
1024
|
+
"thinking",
|
|
1025
|
+
"waiting_input",
|
|
1026
|
+
]);
|
|
1027
|
+
expect(adapter.getSessionStatus("session-r1-no-initial")).toBe("waiting_input");
|
|
1028
|
+
}
|
|
1029
|
+
finally {
|
|
1030
|
+
vi.useRealTimers();
|
|
1031
|
+
adapter.cleanup();
|
|
1032
|
+
}
|
|
1033
|
+
});
|
|
1034
|
+
it("recovers a polling-driven session to waiting_input after a failed turn and re-arms with a longer backoff (R2)", async () => {
|
|
1035
|
+
vi.useFakeTimers();
|
|
1036
|
+
FailingAutoPollRuntime.submitMessages = [];
|
|
1037
|
+
const adapter = new AcodeSdkAdapter();
|
|
1038
|
+
adapter.setRuntimeConstructorForTest(FailingAutoPollRuntime);
|
|
1039
|
+
const statuses = [];
|
|
1040
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
1041
|
+
try {
|
|
1042
|
+
await adapter.startSession("session-r2-recover-failed", {
|
|
1043
|
+
command: "acode",
|
|
1044
|
+
workingDirectory: process.cwd(),
|
|
1045
|
+
autoAccept: true,
|
|
1046
|
+
extraMcpServers: {},
|
|
1047
|
+
envOverrides: {},
|
|
1048
|
+
});
|
|
1049
|
+
// R1 arms the auto-poll; advance the normal 250ms delay to fire it.
|
|
1050
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1051
|
+
expect(FailingAutoPollRuntime.submitMessages).toHaveLength(1);
|
|
1052
|
+
// R2 fix runs synchronously during the failing submit: the
|
|
1053
|
+
// 'error' event sets status to 'error', then the 'turn_complete'
|
|
1054
|
+
// (failed) event triggers the recovery path which flips status
|
|
1055
|
+
// back to 'waiting_input' and schedules a 2s delayed re-arm.
|
|
1056
|
+
expect(adapter.getSessionStatus("session-r2-recover-failed")).toBe("waiting_input");
|
|
1057
|
+
expect(statuses).toEqual([
|
|
1058
|
+
"starting",
|
|
1059
|
+
"waiting_input",
|
|
1060
|
+
"thinking",
|
|
1061
|
+
"error",
|
|
1062
|
+
"waiting_input",
|
|
1063
|
+
]);
|
|
1064
|
+
expect(statuses.at(-1)).toBe("waiting_input");
|
|
1065
|
+
// 250ms after recovery is too short for the 2s backoff.
|
|
1066
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1067
|
+
expect(FailingAutoPollRuntime.submitMessages).toHaveLength(1);
|
|
1068
|
+
// After the 2s backoff elapses, the recovery timer fires and submits
|
|
1069
|
+
// a new poll prompt, restoring the message loop.
|
|
1070
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1071
|
+
expect(FailingAutoPollRuntime.submitMessages.length).toBeGreaterThanOrEqual(2);
|
|
1072
|
+
expect(FailingAutoPollRuntime.submitMessages[1]).toContain("poll_message");
|
|
1073
|
+
}
|
|
1074
|
+
finally {
|
|
1075
|
+
vi.useRealTimers();
|
|
1076
|
+
adapter.cleanup();
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
it("recovers from a bare ACode error event so auto-poll does not get stuck terminal", async () => {
|
|
1080
|
+
vi.useFakeTimers();
|
|
1081
|
+
BareErrorRuntime.submitMessages = [];
|
|
1082
|
+
const adapter = new AcodeSdkAdapter();
|
|
1083
|
+
adapter.setRuntimeConstructorForTest(BareErrorRuntime);
|
|
1084
|
+
const statuses = [];
|
|
1085
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
1086
|
+
try {
|
|
1087
|
+
await adapter.startSession("session-bare-error", {
|
|
1088
|
+
command: "acode",
|
|
1089
|
+
workingDirectory: process.cwd(),
|
|
1090
|
+
autoAccept: true,
|
|
1091
|
+
extraMcpServers: {},
|
|
1092
|
+
envOverrides: {},
|
|
1093
|
+
});
|
|
1094
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1095
|
+
expect(BareErrorRuntime.submitMessages).toHaveLength(1);
|
|
1096
|
+
expect(adapter.getSessionStatus("session-bare-error")).toBe("waiting_input");
|
|
1097
|
+
expect(statuses).toEqual([
|
|
1098
|
+
"starting",
|
|
1099
|
+
"waiting_input",
|
|
1100
|
+
"thinking",
|
|
1101
|
+
"error",
|
|
1102
|
+
"waiting_input",
|
|
1103
|
+
]);
|
|
1104
|
+
await vi.advanceTimersByTimeAsync(1999);
|
|
1105
|
+
expect(BareErrorRuntime.submitMessages).toHaveLength(1);
|
|
1106
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
1107
|
+
expect(BareErrorRuntime.submitMessages).toHaveLength(2);
|
|
1108
|
+
expect(BareErrorRuntime.submitMessages[1]).toContain("poll_message");
|
|
1109
|
+
}
|
|
1110
|
+
finally {
|
|
1111
|
+
vi.useRealTimers();
|
|
1112
|
+
adapter.cleanup();
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
it("recovers from direct runtime submit rejection during user turns", async () => {
|
|
1116
|
+
vi.useFakeTimers();
|
|
1117
|
+
RejectingSubmitRuntime.submitMessages = [];
|
|
1118
|
+
const adapter = new AcodeSdkAdapter();
|
|
1119
|
+
adapter.setRuntimeConstructorForTest(RejectingSubmitRuntime);
|
|
1120
|
+
const statuses = [];
|
|
1121
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
1122
|
+
try {
|
|
1123
|
+
await adapter.startSession("session-user-submit-reject", {
|
|
1124
|
+
command: "acode",
|
|
1125
|
+
workingDirectory: process.cwd(),
|
|
1126
|
+
autoAccept: true,
|
|
1127
|
+
extraMcpServers: {},
|
|
1128
|
+
envOverrides: {},
|
|
1129
|
+
});
|
|
1130
|
+
await adapter.sendMessage("session-user-submit-reject", "user work");
|
|
1131
|
+
expect(RejectingSubmitRuntime.submitMessages).toEqual(["user work"]);
|
|
1132
|
+
expect(adapter.getSessionStatus("session-user-submit-reject")).toBe("waiting_input");
|
|
1133
|
+
expect(statuses).toEqual([
|
|
1134
|
+
"starting",
|
|
1135
|
+
"waiting_input",
|
|
1136
|
+
"thinking",
|
|
1137
|
+
"error",
|
|
1138
|
+
"waiting_input",
|
|
1139
|
+
]);
|
|
1140
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1141
|
+
expect(RejectingSubmitRuntime.submitMessages[1]).toContain("poll_message");
|
|
1142
|
+
}
|
|
1143
|
+
finally {
|
|
1144
|
+
vi.useRealTimers();
|
|
1145
|
+
adapter.cleanup();
|
|
1146
|
+
}
|
|
1147
|
+
});
|
|
1148
|
+
it("does not let stale failed-turn recovery poll after a user follow-up arrives", async () => {
|
|
1149
|
+
vi.useFakeTimers();
|
|
1150
|
+
FailingPollThenSuccessfulUserRuntime.submitMessages = [];
|
|
1151
|
+
const adapter = new AcodeSdkAdapter();
|
|
1152
|
+
adapter.setRuntimeConstructorForTest(FailingPollThenSuccessfulUserRuntime);
|
|
1153
|
+
try {
|
|
1154
|
+
await adapter.startSession("session-stale-recovery-user", {
|
|
1155
|
+
command: "acode",
|
|
1156
|
+
workingDirectory: process.cwd(),
|
|
1157
|
+
autoAccept: true,
|
|
1158
|
+
extraMcpServers: {},
|
|
1159
|
+
envOverrides: {},
|
|
1160
|
+
});
|
|
1161
|
+
await vi.advanceTimersByTimeAsync(250);
|
|
1162
|
+
expect(FailingPollThenSuccessfulUserRuntime.submitMessages).toHaveLength(1);
|
|
1163
|
+
await adapter.sendMessage("session-stale-recovery-user", "human follow up");
|
|
1164
|
+
await vi.advanceTimersByTimeAsync(2000);
|
|
1165
|
+
expect(FailingPollThenSuccessfulUserRuntime.submitMessages).toEqual([
|
|
1166
|
+
expect.stringContaining("poll_message"),
|
|
1167
|
+
"human follow up",
|
|
1168
|
+
]);
|
|
1169
|
+
expect(adapter.getSessionStatus("session-stale-recovery-user")).toBe("completed");
|
|
1170
|
+
}
|
|
1171
|
+
finally {
|
|
1172
|
+
vi.useRealTimers();
|
|
1173
|
+
adapter.cleanup();
|
|
1174
|
+
}
|
|
1175
|
+
});
|
|
1176
|
+
it("cleans up sessions when ACode runtime fails during startup", async () => {
|
|
1177
|
+
const adapter = new AcodeSdkAdapter();
|
|
1178
|
+
adapter.setRuntimeConstructorForTest(StartFailureRuntime);
|
|
1179
|
+
const statuses = [];
|
|
1180
|
+
adapter.on("status-change", (_sessionId, status) => statuses.push(status));
|
|
1181
|
+
await expect(adapter.startSession("session-start-failure", {
|
|
1182
|
+
command: "acode",
|
|
1183
|
+
workingDirectory: process.cwd(),
|
|
1184
|
+
autoAccept: true,
|
|
1185
|
+
extraMcpServers: {},
|
|
1186
|
+
envOverrides: {},
|
|
1187
|
+
})).rejects.toThrow("runtime start failed");
|
|
1188
|
+
expect(adapter.hasSession("session-start-failure")).toBe(false);
|
|
1189
|
+
expect(adapter.getSessionStatus("session-start-failure")).toBeUndefined();
|
|
1190
|
+
expect(statuses).toEqual(["starting", "error"]);
|
|
1191
|
+
});
|
|
1192
|
+
it("logs a warning but does not emit a fatal error event when poll_message is missing (R3)", async () => {
|
|
1193
|
+
StartupMcpStatusRuntime.includeAwsMcp = false;
|
|
1194
|
+
const adapter = new AcodeSdkAdapter();
|
|
1195
|
+
adapter.setRuntimeConstructorForTest(StartupMcpStatusRuntime);
|
|
1196
|
+
const events = [];
|
|
1197
|
+
const consoleWarn = vi
|
|
1198
|
+
.spyOn(console, "warn")
|
|
1199
|
+
.mockImplementation(() => undefined);
|
|
1200
|
+
adapter.on("event", (event) => events.push(event));
|
|
1201
|
+
try {
|
|
1202
|
+
await adapter.startSession("session-r3-no-poll-message", {
|
|
1203
|
+
command: "acode",
|
|
1204
|
+
workingDirectory: process.cwd(),
|
|
1205
|
+
autoAccept: true,
|
|
1206
|
+
extraMcpServers: {},
|
|
1207
|
+
envOverrides: {},
|
|
1208
|
+
});
|
|
1209
|
+
// R3 fix: a missing poll_message tool is a configuration smell
|
|
1210
|
+
// (the LLM will fail when it tries to call it), but it must NOT
|
|
1211
|
+
// be promoted to a session-level 'error' event. Doing so would
|
|
1212
|
+
// lock the dashboard timeline into a permanent '发生错误' state
|
|
1213
|
+
// even though the LLM itself never reports a real error.
|
|
1214
|
+
expect(events.find((event) => event.type === "error")).toBeUndefined();
|
|
1215
|
+
expect(consoleWarn).toHaveBeenCalled();
|
|
1216
|
+
const warnText = String(consoleWarn.mock.calls[0]?.[0] ?? "");
|
|
1217
|
+
expect(warnText).toContain("poll_message");
|
|
1218
|
+
}
|
|
1219
|
+
finally {
|
|
1220
|
+
consoleWarn.mockRestore();
|
|
1221
|
+
adapter.cleanup();
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
it("does not log a warning or emit an error event when poll_message is exposed (R3 negative)", async () => {
|
|
1225
|
+
StartupMcpStatusRuntime.includeAwsMcp = true;
|
|
1226
|
+
const adapter = new AcodeSdkAdapter();
|
|
1227
|
+
adapter.setRuntimeConstructorForTest(StartupMcpStatusRuntime);
|
|
1228
|
+
const events = [];
|
|
1229
|
+
const consoleWarn = vi
|
|
1230
|
+
.spyOn(console, "warn")
|
|
1231
|
+
.mockImplementation(() => undefined);
|
|
1232
|
+
adapter.on("event", (event) => events.push(event));
|
|
1233
|
+
try {
|
|
1234
|
+
await adapter.startSession("session-r3-has-poll-message", {
|
|
1235
|
+
command: "acode",
|
|
1236
|
+
workingDirectory: process.cwd(),
|
|
1237
|
+
autoAccept: true,
|
|
1238
|
+
extraMcpServers: {},
|
|
1239
|
+
envOverrides: {},
|
|
1240
|
+
});
|
|
1241
|
+
expect(events.find((event) => event.type === "error")).toBeUndefined();
|
|
1242
|
+
expect(consoleWarn).not.toHaveBeenCalled();
|
|
1243
|
+
}
|
|
1244
|
+
finally {
|
|
1245
|
+
consoleWarn.mockRestore();
|
|
1246
|
+
adapter.cleanup();
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
it("does not log a warning when the MCP server is named differently but still exposes poll_message (R3 user-scenario)", async () => {
|
|
1250
|
+
// Reproduces the production scenario from the bug report: the user
|
|
1251
|
+
// configures the AWS MCP under a non-default name (e.g. 'aws-mcp-server'
|
|
1252
|
+
// or 'agent-mcp'), but aws-mcp's tools are still exposed to the LLM
|
|
1253
|
+
// under names like 'aws-mcp__poll_message'. The old R3 check matched
|
|
1254
|
+
// the literal string 'aws-mcp' against the user-configured server
|
|
1255
|
+
// names, which produced a false-positive 'error' event and locked
|
|
1256
|
+
// the dashboard into a permanent '发生错误' state.
|
|
1257
|
+
const adapter = new AcodeSdkAdapter();
|
|
1258
|
+
adapter.setRuntimeConstructorForTest(DifferentlyNamedMcpRuntime);
|
|
1259
|
+
const events = [];
|
|
1260
|
+
const consoleWarn = vi
|
|
1261
|
+
.spyOn(console, "warn")
|
|
1262
|
+
.mockImplementation(() => undefined);
|
|
1263
|
+
adapter.on("event", (event) => events.push(event));
|
|
1264
|
+
try {
|
|
1265
|
+
await adapter.startSession("session-r3-differently-named-mcp", {
|
|
1266
|
+
command: "acode",
|
|
1267
|
+
workingDirectory: process.cwd(),
|
|
1268
|
+
autoAccept: true,
|
|
1269
|
+
extraMcpServers: {},
|
|
1270
|
+
envOverrides: {},
|
|
1271
|
+
});
|
|
1272
|
+
expect(events.find((event) => event.type === "error")).toBeUndefined();
|
|
1273
|
+
expect(consoleWarn).not.toHaveBeenCalled();
|
|
1274
|
+
}
|
|
1275
|
+
finally {
|
|
1276
|
+
consoleWarn.mockRestore();
|
|
1277
|
+
adapter.cleanup();
|
|
1278
|
+
}
|
|
1279
|
+
});
|
|
273
1280
|
});
|