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