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