@langwatch/scenario 0.2.13 → 0.4.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.
@@ -1,37 +1,638 @@
1
+ // src/integrations/vitest/setup.ts
2
+ import fs3 from "fs";
3
+ import path3 from "path";
4
+ import { beforeEach, afterEach } from "vitest";
5
+
6
+ // src/events/event-bus.ts
1
7
  import {
2
- EventBus
3
- } from "../../chunk-6SKQWXT7.mjs";
4
- import {
5
- Logger
6
- } from "../../chunk-OL4RFXV4.mjs";
7
- import "../../chunk-7P6ASYW6.mjs";
8
+ concatMap,
9
+ EMPTY,
10
+ catchError,
11
+ Subject,
12
+ tap,
13
+ map
14
+ } from "rxjs";
8
15
 
9
- // src/integrations/vitest/setup.ts
10
- import fs from "fs";
16
+ // src/events/event-alert-message-logger.ts
17
+ import * as fs2 from "fs";
18
+ import * as os from "os";
19
+ import * as path2 from "path";
20
+ import open from "open";
21
+
22
+ // src/config/env.ts
23
+ import { z } from "zod/v4";
24
+
25
+ // src/config/log-levels.ts
26
+ var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
27
+ LogLevel2["ERROR"] = "ERROR";
28
+ LogLevel2["WARN"] = "WARN";
29
+ LogLevel2["INFO"] = "INFO";
30
+ LogLevel2["DEBUG"] = "DEBUG";
31
+ return LogLevel2;
32
+ })(LogLevel || {});
33
+ var LOG_LEVELS = Object.values(LogLevel);
34
+
35
+ // src/config/env.ts
36
+ var envSchema = z.object({
37
+ /**
38
+ * LangWatch API key for event reporting.
39
+ * If not provided, events will not be sent to LangWatch.
40
+ */
41
+ LANGWATCH_API_KEY: z.string().optional(),
42
+ /**
43
+ * LangWatch endpoint URL for event reporting.
44
+ * Defaults to the production LangWatch endpoint.
45
+ */
46
+ LANGWATCH_ENDPOINT: z.string().url().optional().default("https://app.langwatch.ai"),
47
+ /**
48
+ * Disables simulation report info messages when set to any truthy value.
49
+ * Useful for CI/CD environments or when you want cleaner output.
50
+ */
51
+ SCENARIO_DISABLE_SIMULATION_REPORT_INFO: z.string().optional().transform((val) => Boolean(val)),
52
+ /**
53
+ * Node environment - affects logging and behavior.
54
+ * Defaults to 'development' if not specified.
55
+ */
56
+ NODE_ENV: z.enum(["development", "production", "test"]).default("development"),
57
+ /**
58
+ * Case-insensitive log level for the scenario package.
59
+ * Defaults to 'info' if not specified.
60
+ */
61
+ LOG_LEVEL: z.string().toUpperCase().pipe(z.nativeEnum(LogLevel)).optional().default("INFO" /* INFO */),
62
+ /**
63
+ * Scenario batch run ID.
64
+ * If not provided, a random ID will be generated.
65
+ */
66
+ SCENARIO_BATCH_RUN_ID: z.string().optional()
67
+ });
68
+ function getEnv() {
69
+ return envSchema.parse(process.env);
70
+ }
71
+
72
+ // src/config/load.ts
73
+ import fs from "fs/promises";
11
74
  import path from "path";
12
- import { beforeEach, afterEach } from "vitest";
13
- var logger = Logger.create("integrations:vitest:setup");
75
+ import { pathToFileURL } from "url";
76
+
77
+ // src/domain/core/config.ts
78
+ import { z as z3 } from "zod/v4";
79
+
80
+ // src/domain/core/schemas/model.schema.ts
81
+ import { z as z2 } from "zod/v4";
82
+
83
+ // src/domain/core/constants.ts
84
+ var DEFAULT_TEMPERATURE = 0;
85
+
86
+ // src/domain/core/schemas/model.schema.ts
87
+ var modelSchema = z2.object({
88
+ model: z2.custom((val) => Boolean(val), {
89
+ message: "A model is required. Configure it in scenario.config.js defaultModel or pass directly to the agent."
90
+ }).describe("The OpenAI Language Model to use for generating responses."),
91
+ temperature: z2.number().min(0).max(1).optional().describe("The temperature for the language model.").default(DEFAULT_TEMPERATURE),
92
+ maxTokens: z2.number().optional().describe("The maximum number of tokens to generate.")
93
+ });
94
+
95
+ // src/domain/core/config.ts
96
+ var headless = typeof process !== "undefined" ? process.env.SCENARIO_HEADLESS === "true" : false;
97
+ var scenarioProjectConfigSchema = z3.object({
98
+ defaultModel: modelSchema.optional(),
99
+ headless: z3.boolean().optional().default(headless)
100
+ }).strict();
101
+
102
+ // src/config/load.ts
103
+ async function loadScenarioProjectConfig() {
104
+ const cwd = process.cwd();
105
+ const configNames = [
106
+ "scenario.config.js",
107
+ "scenario.config.mjs"
108
+ ];
109
+ for (const name of configNames) {
110
+ const fullPath = path.join(cwd, name);
111
+ try {
112
+ await fs.access(fullPath);
113
+ const configModule = await import(pathToFileURL(fullPath).href);
114
+ const config2 = configModule.default || configModule;
115
+ const parsed = scenarioProjectConfigSchema.safeParse(config2);
116
+ if (!parsed.success) {
117
+ throw new Error(
118
+ `Invalid config file ${name}: ${JSON.stringify(parsed.error.format(), null, 2)}`
119
+ );
120
+ }
121
+ return parsed.data;
122
+ } catch (error) {
123
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
124
+ continue;
125
+ }
126
+ throw error;
127
+ }
128
+ }
129
+ return await scenarioProjectConfigSchema.parseAsync({});
130
+ }
131
+
132
+ // src/utils/logger.ts
133
+ var Logger = class _Logger {
134
+ constructor(context) {
135
+ this.context = context;
136
+ }
137
+ /**
138
+ * Creates a logger with context (e.g., class name)
139
+ */
140
+ static create(context) {
141
+ return new _Logger(context);
142
+ }
143
+ /**
144
+ * Returns the current log level from environment.
145
+ * Uses a getter for clarity and idiomatic usage.
146
+ */
147
+ get LOG_LEVEL() {
148
+ return getEnv().LOG_LEVEL;
149
+ }
150
+ /**
151
+ * Returns the index of the given log level in the LOG_LEVELS array.
152
+ * @param level - The log level to get the index for.
153
+ * @returns The index of the log level in the LOG_LEVELS array.
154
+ */
155
+ getLogLevelIndexFor(level) {
156
+ return LOG_LEVELS.indexOf(level);
157
+ }
158
+ /**
159
+ * Checks if logging should occur based on LOG_LEVEL env var
160
+ */
161
+ shouldLog(level) {
162
+ const currentLevelIndex = this.getLogLevelIndexFor(this.LOG_LEVEL);
163
+ const requestedLevelIndex = this.getLogLevelIndexFor(level);
164
+ return currentLevelIndex >= 0 && requestedLevelIndex <= currentLevelIndex;
165
+ }
166
+ formatMessage(message) {
167
+ return this.context ? `[${this.context}] ${message}` : message;
168
+ }
169
+ error(message, data) {
170
+ if (this.shouldLog("ERROR" /* ERROR */)) {
171
+ const formattedMessage = this.formatMessage(message);
172
+ if (data) {
173
+ console.error(formattedMessage, data);
174
+ } else {
175
+ console.error(formattedMessage);
176
+ }
177
+ }
178
+ }
179
+ warn(message, data) {
180
+ if (this.shouldLog("WARN" /* WARN */)) {
181
+ const formattedMessage = this.formatMessage(message);
182
+ if (data) {
183
+ console.warn(formattedMessage, data);
184
+ } else {
185
+ console.warn(formattedMessage);
186
+ }
187
+ }
188
+ }
189
+ info(message, data) {
190
+ if (this.shouldLog("INFO" /* INFO */)) {
191
+ const formattedMessage = this.formatMessage(message);
192
+ if (data) {
193
+ console.info(formattedMessage, data);
194
+ } else {
195
+ console.info(formattedMessage);
196
+ }
197
+ }
198
+ }
199
+ debug(message, data) {
200
+ if (this.shouldLog("DEBUG" /* DEBUG */)) {
201
+ const formattedMessage = this.formatMessage(message);
202
+ if (data) {
203
+ console.log(formattedMessage, data);
204
+ } else {
205
+ console.log(formattedMessage);
206
+ }
207
+ }
208
+ }
209
+ };
210
+
211
+ // src/config/get-project-config.ts
212
+ var logger = new Logger("scenario.config");
213
+ var configLoaded = false;
214
+ var config = null;
215
+ var configLoadPromise = null;
216
+ async function loadProjectConfig() {
217
+ if (configLoaded) {
218
+ return;
219
+ }
220
+ if (configLoadPromise) {
221
+ return configLoadPromise;
222
+ }
223
+ configLoadPromise = (async () => {
224
+ try {
225
+ config = await loadScenarioProjectConfig();
226
+ logger.debug("loaded scenario project config", { config });
227
+ } catch (error) {
228
+ logger.error("error loading scenario project config", { error });
229
+ } finally {
230
+ configLoaded = true;
231
+ }
232
+ })();
233
+ return configLoadPromise;
234
+ }
235
+ async function getProjectConfig() {
236
+ await loadProjectConfig();
237
+ return config;
238
+ }
239
+
240
+ // src/utils/ids.ts
241
+ import crypto from "crypto";
242
+ import process2 from "process";
243
+ import { generate, parse } from "xksuid";
244
+ var batchRunId;
245
+ function getBatchRunId() {
246
+ if (batchRunId) {
247
+ return batchRunId;
248
+ }
249
+ if (process2.env.SCENARIO_BATCH_RUN_ID) {
250
+ return batchRunId = process2.env.SCENARIO_BATCH_RUN_ID;
251
+ }
252
+ if (process2.env.VITEST_WORKER_ID || process2.env.JEST_WORKER_ID) {
253
+ const parentProcessId = process2.ppid;
254
+ const now = /* @__PURE__ */ new Date();
255
+ const year = now.getUTCFullYear();
256
+ const week = String(getISOWeekNumber(now)).padStart(2, "0");
257
+ const raw = `${parentProcessId}_${year}_w${week}`;
258
+ const hash = crypto.createHash("sha256").update(raw).digest("hex").slice(0, 12);
259
+ return batchRunId = `scenariobatch_${hash}`;
260
+ }
261
+ return batchRunId = `scenariobatch_${generate()}`;
262
+ }
263
+ function getISOWeekNumber(date) {
264
+ const tmp = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
265
+ const dayNum = tmp.getUTCDay() || 7;
266
+ tmp.setUTCDate(tmp.getUTCDate() + 4 - dayNum);
267
+ const yearStart = new Date(Date.UTC(tmp.getUTCFullYear(), 0, 1));
268
+ const weekNo = Math.ceil(((tmp.getTime() - yearStart.getTime()) / 864e5 + 1) / 7);
269
+ return weekNo;
270
+ }
271
+
272
+ // src/events/event-alert-message-logger.ts
273
+ var EventAlertMessageLogger = class {
274
+ /**
275
+ * Creates a coordination file to prevent duplicate messages across processes.
276
+ * Returns true if this process should show the message (first one to create the file).
277
+ */
278
+ createCoordinationFile(type) {
279
+ try {
280
+ const batchId = getBatchRunId();
281
+ const tmpDir = os.tmpdir();
282
+ const fileName = `scenario-${type}-${batchId}`;
283
+ const filePath = path2.join(tmpDir, fileName);
284
+ fs2.writeFileSync(filePath, process.pid.toString(), { flag: "wx" });
285
+ return true;
286
+ } catch {
287
+ return false;
288
+ }
289
+ }
290
+ /**
291
+ * Shows a fancy greeting message about simulation reporting status.
292
+ * Only shows once per batch run to avoid spam.
293
+ */
294
+ handleGreeting() {
295
+ if (this.isGreetingDisabled()) {
296
+ return;
297
+ }
298
+ if (!this.createCoordinationFile("greeting")) {
299
+ return;
300
+ }
301
+ this.displayGreeting();
302
+ }
303
+ /**
304
+ * Shows a fancy message about how to watch the simulation.
305
+ * Called when a run started event is received with a session ID.
306
+ */
307
+ async handleWatchMessage(params) {
308
+ if (this.isGreetingDisabled()) {
309
+ return;
310
+ }
311
+ if (!this.createCoordinationFile(`watch-${params.scenarioSetId}`)) {
312
+ return;
313
+ }
314
+ await this.displayWatchMessage(params);
315
+ }
316
+ isGreetingDisabled() {
317
+ return getEnv().SCENARIO_DISABLE_SIMULATION_REPORT_INFO === true;
318
+ }
319
+ displayGreeting() {
320
+ const separator = "\u2500".repeat(60);
321
+ const env = getEnv();
322
+ if (!env.LANGWATCH_API_KEY) {
323
+ console.log(`
324
+ ${separator}`);
325
+ console.log("\u{1F3AD} Running Scenario Tests");
326
+ console.log(`${separator}`);
327
+ console.log("\u27A1\uFE0F LangWatch API key not configured");
328
+ console.log(" Simulations will only output final results");
329
+ console.log("");
330
+ console.log("\u{1F4A1} To visualize conversations in real time:");
331
+ console.log(" \u2022 Set LANGWATCH_API_KEY environment variable");
332
+ console.log(" \u2022 Or configure apiKey in scenario.config.js");
333
+ console.log("");
334
+ console.log(`${separator}
335
+ `);
336
+ }
337
+ }
338
+ async displayWatchMessage(params) {
339
+ const separator = "\u2500".repeat(60);
340
+ const setUrl = params.setUrl;
341
+ const batchUrl = `${setUrl}/${getBatchRunId()}`;
342
+ console.log(`
343
+ ${separator}`);
344
+ console.log("\u{1F3AD} Running Scenario Tests");
345
+ console.log(`${separator}`);
346
+ console.log(`Follow it live: ${batchUrl}`);
347
+ console.log(`${separator}
348
+ `);
349
+ const projectConfig = await getProjectConfig();
350
+ if (!(projectConfig == null ? void 0 : projectConfig.headless)) {
351
+ try {
352
+ open(batchUrl);
353
+ } catch (_) {
354
+ }
355
+ }
356
+ }
357
+ };
358
+
359
+ // src/events/schema.ts
360
+ import { EventType, MessagesSnapshotEventSchema } from "@ag-ui/core";
361
+ import { z as z4 } from "zod";
362
+ var Verdict = /* @__PURE__ */ ((Verdict2) => {
363
+ Verdict2["SUCCESS"] = "success";
364
+ Verdict2["FAILURE"] = "failure";
365
+ Verdict2["INCONCLUSIVE"] = "inconclusive";
366
+ return Verdict2;
367
+ })(Verdict || {});
368
+ var ScenarioRunStatus = /* @__PURE__ */ ((ScenarioRunStatus2) => {
369
+ ScenarioRunStatus2["SUCCESS"] = "SUCCESS";
370
+ ScenarioRunStatus2["ERROR"] = "ERROR";
371
+ ScenarioRunStatus2["CANCELLED"] = "CANCELLED";
372
+ ScenarioRunStatus2["IN_PROGRESS"] = "IN_PROGRESS";
373
+ ScenarioRunStatus2["PENDING"] = "PENDING";
374
+ ScenarioRunStatus2["FAILED"] = "FAILED";
375
+ return ScenarioRunStatus2;
376
+ })(ScenarioRunStatus || {});
377
+ var baseEventSchema = z4.object({
378
+ type: z4.nativeEnum(EventType),
379
+ timestamp: z4.number(),
380
+ rawEvent: z4.any().optional()
381
+ });
382
+ var batchRunIdSchema = z4.string();
383
+ var scenarioRunIdSchema = z4.string();
384
+ var scenarioIdSchema = z4.string();
385
+ var baseScenarioEventSchema = baseEventSchema.extend({
386
+ batchRunId: batchRunIdSchema,
387
+ scenarioId: scenarioIdSchema,
388
+ scenarioRunId: scenarioRunIdSchema,
389
+ scenarioSetId: z4.string().optional().default("default")
390
+ });
391
+ var scenarioRunStartedSchema = baseScenarioEventSchema.extend({
392
+ type: z4.literal("SCENARIO_RUN_STARTED" /* RUN_STARTED */),
393
+ metadata: z4.object({
394
+ name: z4.string().optional(),
395
+ description: z4.string().optional()
396
+ })
397
+ });
398
+ var scenarioResultsSchema = z4.object({
399
+ verdict: z4.nativeEnum(Verdict),
400
+ reasoning: z4.string().optional(),
401
+ metCriteria: z4.array(z4.string()),
402
+ unmetCriteria: z4.array(z4.string()),
403
+ error: z4.string().optional()
404
+ });
405
+ var scenarioRunFinishedSchema = baseScenarioEventSchema.extend({
406
+ type: z4.literal("SCENARIO_RUN_FINISHED" /* RUN_FINISHED */),
407
+ status: z4.nativeEnum(ScenarioRunStatus),
408
+ results: scenarioResultsSchema.optional().nullable()
409
+ });
410
+ var scenarioMessageSnapshotSchema = MessagesSnapshotEventSchema.merge(
411
+ baseScenarioEventSchema.extend({
412
+ type: z4.literal("SCENARIO_MESSAGE_SNAPSHOT" /* MESSAGE_SNAPSHOT */)
413
+ })
414
+ );
415
+ var scenarioEventSchema = z4.discriminatedUnion("type", [
416
+ scenarioRunStartedSchema,
417
+ scenarioRunFinishedSchema,
418
+ scenarioMessageSnapshotSchema
419
+ ]);
420
+ var successSchema = z4.object({ success: z4.boolean() });
421
+ var errorSchema = z4.object({ error: z4.string() });
422
+ var stateSchema = z4.object({
423
+ state: z4.object({
424
+ messages: z4.array(z4.any()),
425
+ status: z4.string()
426
+ })
427
+ });
428
+ var runsSchema = z4.object({ runs: z4.array(z4.string()) });
429
+ var eventsSchema = z4.object({ events: z4.array(scenarioEventSchema) });
430
+
431
+ // src/events/event-reporter.ts
432
+ var EventReporter = class {
433
+ apiKey;
434
+ eventsEndpoint;
435
+ eventAlertMessageLogger;
436
+ logger = new Logger("scenario.events.EventReporter");
437
+ isEnabled;
438
+ constructor(config2) {
439
+ this.apiKey = config2.apiKey ?? "";
440
+ this.eventsEndpoint = new URL("/api/scenario-events", config2.endpoint);
441
+ this.eventAlertMessageLogger = new EventAlertMessageLogger();
442
+ this.eventAlertMessageLogger.handleGreeting();
443
+ this.isEnabled = this.apiKey.length > 0 && this.eventsEndpoint.href.length > 0;
444
+ }
445
+ /**
446
+ * Posts an event to the configured endpoint.
447
+ * Logs success/failure but doesn't throw - event posting shouldn't break scenario execution.
448
+ */
449
+ async postEvent(event) {
450
+ if (!this.isEnabled) return {};
451
+ const result = {};
452
+ this.logger.debug(`[${event.type}] Posting event`, { event });
453
+ const processedEvent = this.processEventForApi(event);
454
+ try {
455
+ const response = await fetch(this.eventsEndpoint.href, {
456
+ method: "POST",
457
+ body: JSON.stringify(processedEvent),
458
+ headers: {
459
+ "Content-Type": "application/json",
460
+ "X-Auth-Token": this.apiKey
461
+ }
462
+ });
463
+ this.logger.debug(
464
+ `[${event.type}] Event POST response status: ${response.status}`
465
+ );
466
+ if (response.ok) {
467
+ const data = await response.json();
468
+ this.logger.debug(`[${event.type}] Event POST response:`, data);
469
+ result.setUrl = data.url;
470
+ } else {
471
+ const errorText = await response.text();
472
+ this.logger.error(`[${event.type}] Event POST failed:`, {
473
+ endpoint: this.eventsEndpoint.href,
474
+ status: response.status,
475
+ statusText: response.statusText,
476
+ error: errorText,
477
+ event: JSON.stringify(processedEvent)
478
+ });
479
+ }
480
+ } catch (error) {
481
+ this.logger.error(`[${event.type}] Event POST error:`, {
482
+ error,
483
+ event: JSON.stringify(processedEvent),
484
+ endpoint: this.eventsEndpoint.href
485
+ });
486
+ }
487
+ return result;
488
+ }
489
+ /**
490
+ * Processes event data to ensure API compatibility.
491
+ * Converts message content objects to strings when needed.
492
+ */
493
+ processEventForApi(event) {
494
+ if (event.type === "SCENARIO_MESSAGE_SNAPSHOT" /* MESSAGE_SNAPSHOT */) {
495
+ return {
496
+ ...event,
497
+ messages: event.messages.map((message) => ({
498
+ ...message,
499
+ content: typeof message.content !== "string" ? JSON.stringify(message.content) : message.content
500
+ }))
501
+ };
502
+ }
503
+ return event;
504
+ }
505
+ };
506
+
507
+ // src/events/event-bus.ts
508
+ var EventBus = class _EventBus {
509
+ static registry = /* @__PURE__ */ new Set();
510
+ events$ = new Subject();
511
+ eventReporter;
512
+ eventAlertMessageLogger;
513
+ processingPromise = null;
514
+ logger = new Logger("scenario.events.EventBus");
515
+ static globalListeners = [];
516
+ constructor(config2) {
517
+ this.eventReporter = new EventReporter(config2);
518
+ this.eventAlertMessageLogger = new EventAlertMessageLogger();
519
+ _EventBus.registry.add(this);
520
+ for (const listener of _EventBus.globalListeners) {
521
+ listener(this);
522
+ }
523
+ }
524
+ static getAllBuses() {
525
+ return _EventBus.registry;
526
+ }
527
+ static addGlobalListener(listener) {
528
+ _EventBus.globalListeners.push(listener);
529
+ }
530
+ /**
531
+ * Publishes an event into the processing pipeline.
532
+ */
533
+ publish(event) {
534
+ this.logger.debug(`[${event.type}] Publishing event`, {
535
+ event
536
+ });
537
+ this.events$.next(event);
538
+ }
539
+ /**
540
+ * Begins listening for and processing events.
541
+ * Returns a promise that resolves when a RUN_FINISHED event is fully processed.
542
+ */
543
+ listen() {
544
+ this.logger.debug("Listening for events");
545
+ if (this.processingPromise) {
546
+ return this.processingPromise;
547
+ }
548
+ this.processingPromise = new Promise((resolve, reject) => {
549
+ this.events$.pipe(
550
+ // Post events and get results
551
+ concatMap(async (event) => {
552
+ this.logger.debug(`[${event.type}] Processing event`, { event });
553
+ const result = await this.eventReporter.postEvent(event);
554
+ return { event, result };
555
+ }),
556
+ // Handle watch messages reactively
557
+ tap(async ({ event, result }) => {
558
+ if (event.type === "SCENARIO_RUN_STARTED" /* RUN_STARTED */ && result.setUrl) {
559
+ await this.eventAlertMessageLogger.handleWatchMessage({
560
+ scenarioSetId: event.scenarioSetId,
561
+ scenarioRunId: event.scenarioRunId,
562
+ setUrl: result.setUrl
563
+ });
564
+ }
565
+ }),
566
+ // Extract just the event for downstream processing
567
+ map(({ event }) => event),
568
+ catchError((error) => {
569
+ this.logger.error("Error in event stream:", error);
570
+ return EMPTY;
571
+ })
572
+ ).subscribe({
573
+ next: (event) => {
574
+ this.logger.debug(`[${event.type}] Event processed`, { event });
575
+ if (event.type === "SCENARIO_RUN_FINISHED" /* RUN_FINISHED */) {
576
+ resolve();
577
+ }
578
+ },
579
+ error: (error) => {
580
+ this.logger.error("Error in event stream:", error);
581
+ reject(error);
582
+ }
583
+ });
584
+ });
585
+ return this.processingPromise;
586
+ }
587
+ /**
588
+ * Stops accepting new events and drains the processing queue.
589
+ */
590
+ async drain() {
591
+ this.logger.debug("Draining event stream");
592
+ this.events$.complete();
593
+ if (this.processingPromise) {
594
+ await this.processingPromise;
595
+ }
596
+ }
597
+ /**
598
+ * Subscribes to an event stream.
599
+ * @param source$ - The event stream to subscribe to.
600
+ */
601
+ subscribeTo(source$) {
602
+ this.logger.debug("Subscribing to event stream");
603
+ return source$.subscribe(this.events$);
604
+ }
605
+ /**
606
+ * Expose the events$ observable for external subscription (read-only).
607
+ */
608
+ get eventsObservable() {
609
+ return this.events$.asObservable();
610
+ }
611
+ };
612
+
613
+ // src/integrations/vitest/setup.ts
614
+ var logger2 = Logger.create("integrations:vitest:setup");
14
615
  function getProjectRoot() {
15
616
  return process.cwd();
16
617
  }
17
618
  var projectRoot = getProjectRoot();
18
- var logDir = path.join(projectRoot, ".scenario");
19
- if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
619
+ var logDir = path3.join(projectRoot, ".scenario");
620
+ if (!fs3.existsSync(logDir)) fs3.mkdirSync(logDir, { recursive: true });
20
621
  function getLogFilePath(testName) {
21
- return path.join(logDir, `${testName.replace(/[^a-z0-9]/gi, "_")}.log`);
622
+ return path3.join(logDir, `${testName.replace(/[^a-z0-9]/gi, "_")}.log`);
22
623
  }
23
624
  var currentTestName = "";
24
625
  var subs = [];
25
626
  beforeEach((ctx) => {
26
627
  currentTestName = ctx.task.id;
27
628
  const filePath = getLogFilePath(currentTestName);
28
- if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
629
+ if (fs3.existsSync(filePath)) fs3.unlinkSync(filePath);
29
630
  subs = Array.from(EventBus.getAllBuses()).map(
30
631
  (bus) => bus.eventsObservable.subscribe((event) => {
31
632
  try {
32
- fs.appendFileSync(filePath, JSON.stringify(event) + "\n");
633
+ fs3.appendFileSync(filePath, JSON.stringify(event) + "\n");
33
634
  } catch (error) {
34
- logger.error("Error writing to log file:", error);
635
+ logger2.error("Error writing to log file:", error);
35
636
  }
36
637
  })
37
638
  );
@@ -41,9 +642,9 @@ EventBus.addGlobalListener((bus) => {
41
642
  bus.eventsObservable.subscribe((event) => {
42
643
  const filePath = getLogFilePath(currentTestName);
43
644
  try {
44
- fs.appendFileSync(filePath, JSON.stringify(event) + "\n");
645
+ fs3.appendFileSync(filePath, JSON.stringify(event) + "\n");
45
646
  } catch (error) {
46
- logger.error("Error writing to log file:", error);
647
+ logger2.error("Error writing to log file:", error);
47
648
  }
48
649
  })
49
650
  );