@microsoft/m365agentsplayground-cli 0.2.26 → 0.2.27-alpha.20260518-462fbee.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/README.md +8 -21
- package/{build → dist}/conversationTypes.d.ts +5 -0
- package/{build → dist}/index.d.ts +1 -1
- package/dist/index.js +8 -0
- package/dist/index.js.LICENSE.txt +157 -0
- package/dist/responseCapture.d.ts +42 -0
- package/{build → dist}/serverManager.d.ts +1 -2
- package/dist/start-server.js +9 -0
- package/dist/start-server.js.LICENSE.txt +157 -0
- package/{build → dist}/testClient.d.ts +1 -1
- package/{build → dist}/types.d.ts +9 -0
- package/package.json +22 -12
- package/build/cardValidator.d.ts.map +0 -1
- package/build/cardValidator.js +0 -46
- package/build/conversationServer.d.ts.map +0 -1
- package/build/conversationServer.js +0 -136
- package/build/conversationTypes.d.ts.map +0 -1
- package/build/conversationTypes.js +0 -5
- package/build/index.d.ts.map +0 -1
- package/build/index.js +0 -25
- package/build/notificationSender.d.ts.map +0 -1
- package/build/notificationSender.js +0 -83
- package/build/responseCapture.d.ts +0 -29
- package/build/responseCapture.d.ts.map +0 -1
- package/build/responseCapture.js +0 -119
- package/build/runConversation.d.ts.map +0 -1
- package/build/runConversation.js +0 -351
- package/build/serverManager.d.ts.map +0 -1
- package/build/serverManager.js +0 -149
- package/build/start-server.d.ts.map +0 -1
- package/build/start-server.js +0 -23
- package/build/testClient.d.ts.map +0 -1
- package/build/testClient.js +0 -434
- package/build/types.d.ts.map +0 -1
- package/build/types.js +0 -7
- package/build/websocketClient.d.ts.map +0 -1
- package/build/websocketClient.js +0 -129
- package/src/cardValidator.ts +0 -56
- package/src/conversationServer.ts +0 -147
- package/src/conversationTypes.ts +0 -157
- package/src/index.ts +0 -37
- package/src/notificationSender.ts +0 -103
- package/src/responseCapture.ts +0 -145
- package/src/runConversation.ts +0 -382
- package/src/serverManager.ts +0 -172
- package/src/start-server.ts +0 -26
- package/src/testClient.ts +0 -512
- package/src/types.ts +0 -155
- package/src/websocketClient.ts +0 -153
- package/tsconfig.json +0 -14
- /package/{build → dist}/cardValidator.d.ts +0 -0
- /package/{build → dist}/conversationServer.d.ts +0 -0
- /package/{build → dist}/notificationSender.d.ts +0 -0
- /package/{build → dist}/runConversation.d.ts +0 -0
- /package/{build → dist}/start-server.d.ts +0 -0
- /package/{build → dist}/websocketClient.d.ts +0 -0
package/src/runConversation.ts
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Conversation execution logic for the test harness.
|
|
3
|
-
*
|
|
4
|
-
* Runs a sequence of turns against a bot through a fresh TestClient
|
|
5
|
-
* and returns the bot's responses. No eval logic — just execution.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import * as http from "http";
|
|
9
|
-
import * as https from "https";
|
|
10
|
-
import { TestClient } from "./testClient";
|
|
11
|
-
import { sendNotificationAndWait } from "./notificationSender";
|
|
12
|
-
import { validateAdaptiveCard } from "./cardValidator";
|
|
13
|
-
import type {
|
|
14
|
-
E2EConfig,
|
|
15
|
-
PersonaConfig,
|
|
16
|
-
ConversationInput,
|
|
17
|
-
Turn,
|
|
18
|
-
TurnResult,
|
|
19
|
-
TurnAttachment,
|
|
20
|
-
ConversationResult,
|
|
21
|
-
} from "./conversationTypes";
|
|
22
|
-
|
|
23
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
24
|
-
// Logging (stderr only — stdout is reserved for structured output)
|
|
25
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export function log(msg: string): void {
|
|
28
|
-
process.stderr.write(`[agents-simulator] ${msg}\n`);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function logError(msg: string): void {
|
|
32
|
-
process.stderr.write(`[agents-simulator][ERROR] ${msg}\n`);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
36
|
-
// Persona resolution
|
|
37
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function resolvePersona(
|
|
40
|
-
turn: Turn,
|
|
41
|
-
input: ConversationInput,
|
|
42
|
-
config: E2EConfig
|
|
43
|
-
): PersonaConfig | undefined {
|
|
44
|
-
const personaName = turn.persona ?? input.persona;
|
|
45
|
-
if (!personaName || !config.personas) return undefined;
|
|
46
|
-
|
|
47
|
-
const persona = config.personas[personaName];
|
|
48
|
-
if (!persona) {
|
|
49
|
-
log(`Warning: persona "${personaName}" not found in config, using default`);
|
|
50
|
-
return undefined;
|
|
51
|
-
}
|
|
52
|
-
return persona;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
56
|
-
// Bot auth preflight check
|
|
57
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Send a bare probe request to the bot endpoint (no Authorization header).
|
|
61
|
-
* Returns the HTTP status code, or throws if the bot is unreachable.
|
|
62
|
-
*
|
|
63
|
-
* Bot Framework SDK behaviour:
|
|
64
|
-
* - MicrosoftAppId is empty → anonymous mode → accepts any request → 200/202
|
|
65
|
-
* - MicrosoftAppId is set → validates JWT → rejects bare request → 401
|
|
66
|
-
*/
|
|
67
|
-
async function probeBot(botEndpoint: string): Promise<number> {
|
|
68
|
-
return new Promise((resolve, reject) => {
|
|
69
|
-
const url = new URL(botEndpoint);
|
|
70
|
-
const lib = url.protocol === "https:" ? https : http;
|
|
71
|
-
|
|
72
|
-
// Use type:"typing" — the Bot Framework adapter acks it immediately without
|
|
73
|
-
// invoking the AI planner (no LLM call, no sendActivity, instant response).
|
|
74
|
-
// Bot returns 200/202 for unauthenticated bots, 401 for authenticated ones.
|
|
75
|
-
const body = JSON.stringify({ type: "typing" });
|
|
76
|
-
const req = lib.request(
|
|
77
|
-
{
|
|
78
|
-
hostname: url.hostname,
|
|
79
|
-
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
|
80
|
-
path: url.pathname,
|
|
81
|
-
method: "POST",
|
|
82
|
-
headers: {
|
|
83
|
-
"Content-Type": "application/json",
|
|
84
|
-
"Content-Length": Buffer.byteLength(body),
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
(res) => resolve(res.statusCode ?? 0)
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
req.on("error", reject);
|
|
91
|
-
req.setTimeout(5000, () => {
|
|
92
|
-
req.destroy(new Error(`Connection to bot timed out: ${botEndpoint}`));
|
|
93
|
-
});
|
|
94
|
-
req.write(body);
|
|
95
|
-
req.end();
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Verify the bot is reachable and accepting unauthenticated requests.
|
|
101
|
-
* Throws a descriptive error when the bot returns 401 or is unreachable.
|
|
102
|
-
*/
|
|
103
|
-
async function checkBotReachable(botEndpoint: string): Promise<void> {
|
|
104
|
-
let statusCode: number;
|
|
105
|
-
try {
|
|
106
|
-
statusCode = await probeBot(botEndpoint);
|
|
107
|
-
} catch (err) {
|
|
108
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
109
|
-
throw new Error(
|
|
110
|
-
`Cannot reach bot at ${botEndpoint}: ${msg}\n` +
|
|
111
|
-
` → Make sure the bot is running before starting a conversation.`
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (statusCode === 401) {
|
|
116
|
-
throw new Error(
|
|
117
|
-
`Bot at ${botEndpoint} requires authentication (HTTP 401).\n` +
|
|
118
|
-
` → For local testing, start the bot with BOT_ID="" BOT_PASSWORD="" so it\n` +
|
|
119
|
-
` accepts requests without a Bot Framework JWT token.\n` +
|
|
120
|
-
` → Example: BOT_ID= BOT_PASSWORD= node ./src/index.ts`
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
126
|
-
// Conversation execution
|
|
127
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Extract and validate Adaptive Card attachments from bot responses.
|
|
131
|
-
* Only processes "application/vnd.microsoft.card.adaptive" attachments.
|
|
132
|
-
*/
|
|
133
|
-
function extractAttachments(
|
|
134
|
-
responses: { attachments?: Array<{ contentType?: string; content?: unknown }> }[]
|
|
135
|
-
): TurnAttachment[] {
|
|
136
|
-
const result: TurnAttachment[] = [];
|
|
137
|
-
for (const r of responses) {
|
|
138
|
-
for (const att of r.attachments ?? []) {
|
|
139
|
-
if (att.contentType === "application/vnd.microsoft.card.adaptive" && att.content) {
|
|
140
|
-
const content = att.content as Record<string, unknown>;
|
|
141
|
-
const card_errors = validateAdaptiveCard(content);
|
|
142
|
-
result.push({ contentType: att.contentType, content, card_errors });
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
return result;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Run a single conversation through a fresh TestClient.
|
|
151
|
-
*
|
|
152
|
-
* Creates an isolated TestClient, sends each turn sequentially, and
|
|
153
|
-
* collects the bot's responses. On failure, remaining turns are skipped.
|
|
154
|
-
*/
|
|
155
|
-
export async function runConversation(
|
|
156
|
-
config: E2EConfig,
|
|
157
|
-
scenario: string,
|
|
158
|
-
input: ConversationInput
|
|
159
|
-
): Promise<ConversationResult> {
|
|
160
|
-
const results: TurnResult[] = [];
|
|
161
|
-
const convStart = Date.now();
|
|
162
|
-
const timeout = config.timeout ?? 120000;
|
|
163
|
-
|
|
164
|
-
const turns = input.turns || [];
|
|
165
|
-
if (turns.length === 0) {
|
|
166
|
-
return {
|
|
167
|
-
type: "conversation_result",
|
|
168
|
-
scenario,
|
|
169
|
-
status: "Errored",
|
|
170
|
-
duration_seconds: 0,
|
|
171
|
-
turns: [],
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// Preflight: verify the bot is reachable and not requiring auth before
|
|
176
|
-
// spending timeout budget on the first real turn.
|
|
177
|
-
try {
|
|
178
|
-
await checkBotReachable(config.botEndpoint);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
181
|
-
logError(message);
|
|
182
|
-
return {
|
|
183
|
-
type: "conversation_result",
|
|
184
|
-
scenario,
|
|
185
|
-
status: "Errored",
|
|
186
|
-
duration_seconds: (Date.now() - convStart) / 1000,
|
|
187
|
-
turns: turns.map((t) => ({
|
|
188
|
-
test_id: t.test_id,
|
|
189
|
-
prompt: t.prompt,
|
|
190
|
-
actual_response: null,
|
|
191
|
-
attachments: [],
|
|
192
|
-
status: "Errored" as const,
|
|
193
|
-
error_message: message,
|
|
194
|
-
duration_seconds: 0,
|
|
195
|
-
})),
|
|
196
|
-
};
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const client = new TestClient({
|
|
200
|
-
botEndpoint: config.botEndpoint,
|
|
201
|
-
timeout,
|
|
202
|
-
bot: config.bot,
|
|
203
|
-
deliveryMode: config.deliveryMode ?? "expectReplies",
|
|
204
|
-
chatType: config.chatType ?? "personal",
|
|
205
|
-
streamingSettleDelayMs: config.streamingSettleDelayMs ?? 0,
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
log(`Starting client for: ${scenario}`);
|
|
210
|
-
await client.start();
|
|
211
|
-
log(`Client started on port ${client.getPort()}`);
|
|
212
|
-
|
|
213
|
-
// Maps turn test_id → messageId of the bot's first card response in that turn
|
|
214
|
-
const cardMessageIds = new Map<string, string>();
|
|
215
|
-
|
|
216
|
-
for (let i = 0; i < turns.length; i++) {
|
|
217
|
-
const turn = turns[i];
|
|
218
|
-
const turnType = turn.turn_type ?? "chat";
|
|
219
|
-
const turnStart = Date.now();
|
|
220
|
-
|
|
221
|
-
log(
|
|
222
|
-
` Turn ${i + 1}/${turns.length} [${turn.test_id}] (${turnType}): ${turn.prompt.substring(
|
|
223
|
-
0,
|
|
224
|
-
80
|
|
225
|
-
)}...`
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
let responseText: string;
|
|
230
|
-
let attachments: TurnAttachment[] = [];
|
|
231
|
-
|
|
232
|
-
if (turnType === "chat") {
|
|
233
|
-
const responses = await client.sendMessage(turn.prompt);
|
|
234
|
-
responseText = responses
|
|
235
|
-
.map((r) => r.text ?? "")
|
|
236
|
-
.filter((t) => t.length > 0)
|
|
237
|
-
.join("\n\n");
|
|
238
|
-
attachments = extractAttachments(responses);
|
|
239
|
-
// Track the first card-containing message ID for card_action turns
|
|
240
|
-
const firstCardMsg = responses.find((r) =>
|
|
241
|
-
r.attachments?.some((a) => a.contentType === "application/vnd.microsoft.card.adaptive")
|
|
242
|
-
);
|
|
243
|
-
if (firstCardMsg?.messageId) {
|
|
244
|
-
cardMessageIds.set(turn.test_id, firstCardMsg.messageId);
|
|
245
|
-
}
|
|
246
|
-
} else if (turnType === "card_action") {
|
|
247
|
-
const cardAction = turn.card_action;
|
|
248
|
-
if (!cardAction) {
|
|
249
|
-
throw new Error(
|
|
250
|
-
`Turn "${turn.test_id}" has turn_type "card_action" but no card_action field.`
|
|
251
|
-
);
|
|
252
|
-
}
|
|
253
|
-
if (!cardAction.verb) {
|
|
254
|
-
throw new Error(`Turn "${turn.test_id}" card_action is missing required "verb" field.`);
|
|
255
|
-
}
|
|
256
|
-
// Resolve the target message ID from the referenced turn
|
|
257
|
-
let replyToId: string | undefined;
|
|
258
|
-
if (cardAction.reply_to_turn != null) {
|
|
259
|
-
replyToId = cardMessageIds.get(cardAction.reply_to_turn);
|
|
260
|
-
if (!replyToId) {
|
|
261
|
-
throw new Error(
|
|
262
|
-
`card_action.reply_to_turn "${cardAction.reply_to_turn}" did not produce a card message. ` +
|
|
263
|
-
`Ensure that turn sends a card before using card_action.`
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
} else {
|
|
267
|
-
// Use the most recently stored card messageId
|
|
268
|
-
const entries = [...cardMessageIds.values()];
|
|
269
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
270
|
-
replyToId = entries[entries.length - 1];
|
|
271
|
-
}
|
|
272
|
-
if (!replyToId) {
|
|
273
|
-
throw new Error(
|
|
274
|
-
`card_action for turn "${turn.test_id}" could not find any card message to target. ` +
|
|
275
|
-
`Ensure a prior turn produces an Adaptive Card.`
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
if (cardAction.action_type === "Action.Submit") {
|
|
279
|
-
const invokeResponse = await client.submitCardForm(replyToId, cardAction.data ?? {});
|
|
280
|
-
responseText = JSON.stringify(invokeResponse);
|
|
281
|
-
} else {
|
|
282
|
-
const invokeResponse = await client.clickCardButton(
|
|
283
|
-
replyToId,
|
|
284
|
-
cardAction.verb,
|
|
285
|
-
cardAction.data ?? {}
|
|
286
|
-
);
|
|
287
|
-
responseText = JSON.stringify(invokeResponse);
|
|
288
|
-
}
|
|
289
|
-
} else {
|
|
290
|
-
const persona = resolvePersona(turn, input, config);
|
|
291
|
-
responseText = await sendNotificationAndWait(
|
|
292
|
-
client.getConversationId(),
|
|
293
|
-
turn,
|
|
294
|
-
persona,
|
|
295
|
-
timeout
|
|
296
|
-
);
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
const duration = (Date.now() - turnStart) / 1000;
|
|
300
|
-
log(` [OK] (${duration.toFixed(1)}s): ${responseText.substring(0, 100)}...`);
|
|
301
|
-
|
|
302
|
-
results.push({
|
|
303
|
-
test_id: turn.test_id,
|
|
304
|
-
prompt: turn.prompt,
|
|
305
|
-
actual_response: responseText || null,
|
|
306
|
-
attachments,
|
|
307
|
-
status: "Completed",
|
|
308
|
-
duration_seconds: duration,
|
|
309
|
-
});
|
|
310
|
-
} catch (err) {
|
|
311
|
-
const duration = (Date.now() - turnStart) / 1000;
|
|
312
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
313
|
-
const isTimeout = message.includes("Timeout");
|
|
314
|
-
|
|
315
|
-
logError(` Turn ${turn.test_id} failed: ${message}`);
|
|
316
|
-
|
|
317
|
-
results.push({
|
|
318
|
-
test_id: turn.test_id,
|
|
319
|
-
prompt: turn.prompt,
|
|
320
|
-
actual_response: null,
|
|
321
|
-
attachments: [],
|
|
322
|
-
status: isTimeout ? "TimedOut" : "Errored",
|
|
323
|
-
error_message: message,
|
|
324
|
-
duration_seconds: duration,
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
for (let j = i + 1; j < turns.length; j++) {
|
|
328
|
-
results.push({
|
|
329
|
-
test_id: turns[j].test_id,
|
|
330
|
-
prompt: turns[j].prompt,
|
|
331
|
-
actual_response: null,
|
|
332
|
-
attachments: [],
|
|
333
|
-
status: "Skipped",
|
|
334
|
-
duration_seconds: 0,
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
break;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
} catch (err) {
|
|
341
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
342
|
-
logError(`Client startup failed for ${scenario}: ${message}`);
|
|
343
|
-
|
|
344
|
-
for (const turn of turns) {
|
|
345
|
-
results.push({
|
|
346
|
-
test_id: turn.test_id,
|
|
347
|
-
prompt: turn.prompt,
|
|
348
|
-
actual_response: null,
|
|
349
|
-
attachments: [],
|
|
350
|
-
status: "Errored",
|
|
351
|
-
error_message: message,
|
|
352
|
-
duration_seconds: 0,
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
} finally {
|
|
356
|
-
try {
|
|
357
|
-
await client.stop();
|
|
358
|
-
} catch {
|
|
359
|
-
// Ignore stop errors.
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
const convDuration = (Date.now() - convStart) / 1000;
|
|
364
|
-
|
|
365
|
-
const statuses = new Set(results.map((t) => t.status));
|
|
366
|
-
let overallStatus: "Completed" | "TimedOut" | "Errored";
|
|
367
|
-
if (statuses.has("TimedOut")) {
|
|
368
|
-
overallStatus = "TimedOut";
|
|
369
|
-
} else if (statuses.has("Errored")) {
|
|
370
|
-
overallStatus = "Errored";
|
|
371
|
-
} else {
|
|
372
|
-
overallStatus = "Completed";
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
return {
|
|
376
|
-
type: "conversation_result",
|
|
377
|
-
scenario,
|
|
378
|
-
status: overallStatus,
|
|
379
|
-
duration_seconds: convDuration,
|
|
380
|
-
turns: results,
|
|
381
|
-
};
|
|
382
|
-
}
|
package/src/serverManager.ts
DELETED
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import express, { Express } from "express";
|
|
2
|
-
import { Server } from "http";
|
|
3
|
-
import { AddressInfo } from "net";
|
|
4
|
-
import { mountBackend, Factory, ConfigManager } from "server";
|
|
5
|
-
import { DeliveryMode } from "schema";
|
|
6
|
-
import { ResponseCapture } from "./responseCapture";
|
|
7
|
-
import { TestClientConfig } from "./types";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Manages the shared Express server lifecycle for the test harness.
|
|
11
|
-
*
|
|
12
|
-
* The server package's Factory is a process-wide singleton — calling
|
|
13
|
-
* mountBackend() more than once replaces ConfigManager while leaving
|
|
14
|
-
* every other cached manager/service stale. ServerManager therefore
|
|
15
|
-
* starts the Express server exactly once and reuses it for all
|
|
16
|
-
* TestClient instances in the same process.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
// Fixed conversation ID used only for the initial ConfigManager bootstrap.
|
|
20
|
-
// Individual TestClients register their own unique conversations in
|
|
21
|
-
// ConversationManager (see TestClient.registerUniqueConversation).
|
|
22
|
-
const BOOTSTRAP_CONVERSATION_ID = "playground-cli-bootstrap";
|
|
23
|
-
|
|
24
|
-
export class ServerManager {
|
|
25
|
-
// ── Shared process-level state ───────────────────────────────────────
|
|
26
|
-
private static server: Server | null = null;
|
|
27
|
-
private static app: Express | null = null;
|
|
28
|
-
private static _port = 0;
|
|
29
|
-
private static _serviceUrl = "";
|
|
30
|
-
private static _started = false;
|
|
31
|
-
private static _responseCapture: ResponseCapture | null = null;
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Start the shared test server (no-op if already running).
|
|
35
|
-
*
|
|
36
|
-
* The first call boots Express + mountBackend. Subsequent calls
|
|
37
|
-
* return immediately, reusing the existing server.
|
|
38
|
-
*/
|
|
39
|
-
async start(config: TestClientConfig): Promise<void> {
|
|
40
|
-
if (ServerManager._started) {
|
|
41
|
-
return;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const app = express();
|
|
45
|
-
ServerManager.app = app;
|
|
46
|
-
|
|
47
|
-
// Create HTTP server first
|
|
48
|
-
ServerManager.server = app.listen(config.port ?? 0);
|
|
49
|
-
|
|
50
|
-
// Wait for server to be listening
|
|
51
|
-
await new Promise<void>((resolve, reject) => {
|
|
52
|
-
ServerManager.server?.once("listening", resolve);
|
|
53
|
-
ServerManager.server?.once("error", reject);
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
// Get the assigned port
|
|
57
|
-
const address = ServerManager.server?.address() as AddressInfo;
|
|
58
|
-
ServerManager._port = address.port;
|
|
59
|
-
ServerManager._serviceUrl = `http://localhost:${ServerManager._port}/_connector`;
|
|
60
|
-
|
|
61
|
-
// Get default config and merge bot config if provided
|
|
62
|
-
const defaultConfig = ConfigManager.getDefaultConfig({
|
|
63
|
-
path: "",
|
|
64
|
-
conversationId: BOOTSTRAP_CONVERSATION_ID,
|
|
65
|
-
});
|
|
66
|
-
if (config.bot) {
|
|
67
|
-
defaultConfig.bot = {
|
|
68
|
-
...defaultConfig.bot,
|
|
69
|
-
...config.bot,
|
|
70
|
-
};
|
|
71
|
-
// Also update the root tenantId if bot.tenantId is provided
|
|
72
|
-
if (config.bot.tenantId) {
|
|
73
|
-
defaultConfig.tenantId = config.bot.tenantId;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (!ServerManager.server) {
|
|
78
|
-
throw new Error("Server failed to start");
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Map delivery mode string to enum value
|
|
82
|
-
const deliveryMode =
|
|
83
|
-
config.deliveryMode === "expectReplies"
|
|
84
|
-
? DeliveryMode.ExpectReplies
|
|
85
|
-
: config.deliveryMode === "default"
|
|
86
|
-
? DeliveryMode.Default
|
|
87
|
-
: undefined;
|
|
88
|
-
|
|
89
|
-
mountBackend(app, ServerManager.server, {
|
|
90
|
-
configFileOptions: { configFile: defaultConfig },
|
|
91
|
-
appConfig: {
|
|
92
|
-
endpoint: config.botEndpoint,
|
|
93
|
-
},
|
|
94
|
-
debugConfig: {
|
|
95
|
-
eventsRecordingEnabled: false,
|
|
96
|
-
deliveryMode,
|
|
97
|
-
},
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// Add error-handling middleware for connector routes.
|
|
101
|
-
app.use(
|
|
102
|
-
(
|
|
103
|
-
err: Error,
|
|
104
|
-
_req: express.Request,
|
|
105
|
-
res: express.Response,
|
|
106
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
107
|
-
_next: express.NextFunction
|
|
108
|
-
) => {
|
|
109
|
-
console.error("[agents-simulator] Unhandled error in connector:", err);
|
|
110
|
-
if (!res.headersSent) {
|
|
111
|
-
res.status(500).json({
|
|
112
|
-
error: {
|
|
113
|
-
code: "InternalError",
|
|
114
|
-
message: err.message || "Unknown error",
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
// Hook the shared ResponseCapture into BotConnectorService once.
|
|
122
|
-
const capture = ServerManager.getResponseCapture();
|
|
123
|
-
capture.hookIntoBotConnectorService(Factory.getBotConnectorService());
|
|
124
|
-
|
|
125
|
-
ServerManager._started = true;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* No-op — the shared server stays running for the process lifetime.
|
|
130
|
-
*
|
|
131
|
-
* Per-client cleanup (WebSockets, conversation messages) is handled
|
|
132
|
-
* by TestClient.stop().
|
|
133
|
-
*
|
|
134
|
-
* TODO(server-package): Add a Factory.reset() so the harness can
|
|
135
|
-
* fully tear down between test suites if needed.
|
|
136
|
-
*/
|
|
137
|
-
async stop(): Promise<void> {
|
|
138
|
-
// Intentionally empty.
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get the port the server is running on
|
|
143
|
-
*/
|
|
144
|
-
get port(): number {
|
|
145
|
-
return ServerManager._port;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Get the service URL for the connector API
|
|
150
|
-
*/
|
|
151
|
-
get serviceUrl(): string {
|
|
152
|
-
return ServerManager._serviceUrl;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Get the Factory instance for accessing services
|
|
157
|
-
*/
|
|
158
|
-
getFactory(): typeof Factory {
|
|
159
|
-
return Factory;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Shared ResponseCapture instance — keyed by conversationId, so
|
|
164
|
-
* concurrent TestClients don't interfere with each other.
|
|
165
|
-
*/
|
|
166
|
-
static getResponseCapture(): ResponseCapture {
|
|
167
|
-
if (!ServerManager._responseCapture) {
|
|
168
|
-
ServerManager._responseCapture = new ResponseCapture();
|
|
169
|
-
}
|
|
170
|
-
return ServerManager._responseCapture;
|
|
171
|
-
}
|
|
172
|
-
}
|
package/src/start-server.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Starts the Conversation Server.
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* node build/start-server.js [--port <port>]
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { createConversationServer } from "./conversationServer";
|
|
11
|
-
|
|
12
|
-
function parsePort(): number {
|
|
13
|
-
const args = process.argv.slice(2);
|
|
14
|
-
for (let i = 0; i < args.length; i++) {
|
|
15
|
-
if (args[i] === "--port" && args[i + 1]) {
|
|
16
|
-
return parseInt(args[++i], 10);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
return 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const port = parsePort();
|
|
23
|
-
|
|
24
|
-
void createConversationServer({ port }).then((server) => {
|
|
25
|
-
process.stdout.write(JSON.stringify({ port: server.port }) + "\n");
|
|
26
|
-
});
|