assuremind 1.0.2 → 1.1.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/cli/index.js CHANGED
@@ -140,7 +140,7 @@ var init_config = __esm({
140
140
  });
141
141
  HealingConfigSchema = import_zod.z.object({
142
142
  enabled: import_zod.z.boolean(),
143
- maxLevel: import_zod.z.number().int().min(1).max(6),
143
+ maxLevel: import_zod.z.number().int().min(1).max(5),
144
144
  dailyBudget: import_zod.z.number().positive(),
145
145
  autoPR: import_zod.z.boolean()
146
146
  });
@@ -185,7 +185,39 @@ var init_config = __esm({
185
185
  profiles: import_zod.z.array(EnvironmentProfileSchema).default([]),
186
186
  activeProfile: import_zod.z.string().optional(),
187
187
  /** Playwright device descriptor name for emulation (e.g. 'iPhone 15 Pro'). */
188
- device: import_zod.z.string().optional()
188
+ device: import_zod.z.string().optional(),
189
+ /**
190
+ * Playwright MCP integration for AI-sighted code generation.
191
+ *
192
+ * CI/CD strategy (default):
193
+ * - MCP ON for code generation → AI sees real page elements → 90-95% selector accuracy
194
+ * - MCP OFF for test execution → runner executes pre-generated code, no MCP overhead
195
+ *
196
+ * MCP is ONLY used during generation (SmartRouter, Studio, API generate endpoints).
197
+ * The test runner (engine/runner.ts → code-runner.ts → AsyncFunction) never touches MCP —
198
+ * it simply executes the pre-generated Playwright code as-is.
199
+ */
200
+ mcp: import_zod.z.object({
201
+ /** Enable MCP-enhanced generation (real page snapshots for AI). ON by default. */
202
+ enabled: import_zod.z.boolean(),
203
+ /** Run MCP browser in headless mode. */
204
+ headless: import_zod.z.boolean(),
205
+ /** Two-phase: execute action via MCP first, then convert to script. */
206
+ actThenScript: import_zod.z.boolean(),
207
+ /** Pre-run element validation using MCP snapshots. */
208
+ proactiveHealing: import_zod.z.boolean(),
209
+ /** Timeout per MCP tool call in ms. */
210
+ actionTimeout: import_zod.z.number().int().positive(),
211
+ /** Idle timeout before MCP browser auto-disconnects in ms. */
212
+ idleTimeout: import_zod.z.number().int().positive()
213
+ }).default({
214
+ enabled: true,
215
+ headless: true,
216
+ actThenScript: false,
217
+ proactiveHealing: false,
218
+ actionTimeout: 15e3,
219
+ idleTimeout: 3e4
220
+ })
189
221
  });
190
222
  DEFAULT_CONFIG = {
191
223
  baseUrl: "http://localhost:3000",
@@ -218,7 +250,15 @@ var init_config = __esm({
218
250
  json: true
219
251
  },
220
252
  studioPort: 4400,
221
- profiles: []
253
+ profiles: [],
254
+ mcp: {
255
+ enabled: true,
256
+ headless: true,
257
+ actThenScript: false,
258
+ proactiveHealing: false,
259
+ actionTimeout: 15e3,
260
+ idleTimeout: 3e4
261
+ }
222
262
  };
223
263
  }
224
264
  });
@@ -331,7 +371,7 @@ var init_init = __esm({
331
371
  { dest: ".gitignore", template: "gitignore" },
332
372
  { dest: "autotest.config.ts", template: "autotest.config.ts" },
333
373
  { dest: "variables/global.json", template: "global-variables.json" },
334
- { dest: "AUTOMIND.md", template: "AUTOMIND.md" },
374
+ { dest: "ASSUREMIND.md", template: "ASSUREMIND.md" },
335
375
  { dest: "docs/GETTING-STARTED.md", template: "docs/GETTING-STARTED.md" },
336
376
  { dest: "docs/STUDIO.md", template: "docs/STUDIO.md" },
337
377
  { dest: "docs/CLI-REFERENCE.md", template: "docs/CLI-REFERENCE.md" }
@@ -923,18 +963,24 @@ var init_static = __esm({
923
963
  function sendError(reply, statusCode, message, err) {
924
964
  const code = err instanceof AssuremindError ? err.code : "INTERNAL_ERROR";
925
965
  const detail = err instanceof Error ? err.message : err !== void 0 ? String(err) : void 0;
926
- if (statusCode >= 500) {
966
+ const isConfigError = err instanceof ConfigError || CONFIG_ERROR_CODES.has(code);
967
+ const effectiveStatus = isConfigError ? 422 : statusCode;
968
+ const isJiraConfig = detail?.includes("JIRA_NOT_CONFIGURED") || detail?.includes("Jira not configured");
969
+ const isXrayConfig = detail?.includes("XRAY_NOT_CONFIGURED") || detail?.includes("Xray not configured");
970
+ const featureHint = isJiraConfig ? "JIRA_NOT_CONFIGURED" : isXrayConfig ? "XRAY_NOT_CONFIGURED" : isConfigError ? "AI_NOT_CONFIGURED" : void 0;
971
+ if (effectiveStatus >= 500) {
927
972
  logger5.error({ err }, message);
928
973
  } else {
929
974
  logger5.debug({ err }, message);
930
975
  }
931
- return reply.status(statusCode).send({
976
+ return reply.status(effectiveStatus).send({
932
977
  error: message,
933
978
  ...detail && detail !== message && { detail },
934
- code
979
+ code,
980
+ ...featureHint && { featureHint }
935
981
  });
936
982
  }
937
- var logger5;
983
+ var logger5, CONFIG_ERROR_CODES;
938
984
  var init_utils2 = __esm({
939
985
  "src/server/utils.ts"() {
940
986
  "use strict";
@@ -942,6 +988,12 @@ var init_utils2 = __esm({
942
988
  init_errors();
943
989
  init_logger();
944
990
  logger5 = createChildLogger("server");
991
+ CONFIG_ERROR_CODES = /* @__PURE__ */ new Set([
992
+ "ENV_VALIDATION_FAILED",
993
+ "PROVIDER_ENV_MISSING",
994
+ "UNKNOWN_PROVIDER",
995
+ "CONFIG_ERROR"
996
+ ]);
945
997
  }
946
998
  });
947
999
 
@@ -987,6 +1039,48 @@ async function configRoutes(fastify) {
987
1039
  return reply.send({ provider: "unknown", model: "unknown" });
988
1040
  }
989
1041
  });
1042
+ fastify.get("/config/status", async (_req, reply) => {
1043
+ const env = tryGetEnv();
1044
+ const jiraBaseUrl = process.env["JIRA_BASE_URL"];
1045
+ const jiraEmail = process.env["JIRA_EMAIL"];
1046
+ const jiraApiToken = process.env["JIRA_API_TOKEN"];
1047
+ const jiraConfigured = !!(jiraBaseUrl && jiraEmail && jiraApiToken);
1048
+ const xrayProjectKey = process.env["XRAY_PROJECT_KEY"];
1049
+ const xrayConfigured = jiraConfigured && !!xrayProjectKey;
1050
+ return reply.send({
1051
+ ai: {
1052
+ configured: !!env,
1053
+ provider: env ? String(env.AI_PROVIDER) : null
1054
+ },
1055
+ jira: {
1056
+ configured: jiraConfigured
1057
+ },
1058
+ xray: {
1059
+ configured: xrayConfigured
1060
+ }
1061
+ });
1062
+ });
1063
+ fastify.get("/mcp/status", async (_req, reply) => {
1064
+ try {
1065
+ const config = await readConfig(rootDir);
1066
+ const mcpEnabled = config.mcp?.enabled ?? false;
1067
+ let packageAvailable = false;
1068
+ try {
1069
+ require.resolve("@playwright/mcp");
1070
+ packageAvailable = true;
1071
+ } catch {
1072
+ }
1073
+ return reply.send({
1074
+ enabled: mcpEnabled,
1075
+ packageAvailable,
1076
+ headless: config.mcp?.headless ?? true,
1077
+ actThenScript: config.mcp?.actThenScript ?? false,
1078
+ proactiveHealing: config.mcp?.proactiveHealing ?? false
1079
+ });
1080
+ } catch (err) {
1081
+ return sendError(reply, 500, "Failed to check MCP status", err);
1082
+ }
1083
+ });
990
1084
  }
991
1085
  var PatchConfigSchema;
992
1086
  var init_config2 = __esm({
@@ -2398,8 +2492,308 @@ var init_batch_processor = __esm({
2398
2492
  }
2399
2493
  });
2400
2494
 
2495
+ // src/mcp/snapshot-parser.ts
2496
+ function parseSnapshot(raw) {
2497
+ if (!raw || !raw.trim()) {
2498
+ return { title: "", interactiveElements: "", accessibilityTree: "" };
2499
+ }
2500
+ const lines = raw.split("\n");
2501
+ let title = "";
2502
+ const elements = [];
2503
+ for (const line of lines) {
2504
+ const trimmed = line.trim();
2505
+ if (!trimmed || trimmed === "-") continue;
2506
+ const pageMatch = trimmed.match(/^-?\s*Page\s+"([^"]*)"/i);
2507
+ if (pageMatch) {
2508
+ title = pageMatch[1] ?? "";
2509
+ continue;
2510
+ }
2511
+ const elementMatch = trimmed.match(
2512
+ /^-?\s*(\w+)\s+"([^"]*)"(?:\s+\[([^\]]*)\])?/
2513
+ );
2514
+ if (!elementMatch) continue;
2515
+ const role = elementMatch[1].toLowerCase();
2516
+ let name = elementMatch[2] ?? "";
2517
+ const attrs = elementMatch[3] ?? "";
2518
+ if (name.length > MAX_TEXT_LEN) {
2519
+ name = name.slice(0, MAX_TEXT_LEN - 1) + "\u2026";
2520
+ }
2521
+ if (!name && !attrs) continue;
2522
+ const isInteractive = INTERACTIVE_ROLES.has(role);
2523
+ const isContext = CONTEXT_ROLES.has(role);
2524
+ if (isInteractive || isContext) {
2525
+ elements.push({ role, name, attrs, isInteractive });
2526
+ }
2527
+ }
2528
+ const interactive = elements.filter((e) => e.isInteractive);
2529
+ const context = elements.filter((e) => !e.isInteractive);
2530
+ const sorted = [...interactive, ...context].slice(0, MAX_ELEMENTS);
2531
+ const interactiveLines = sorted.map((e) => {
2532
+ let line = `${e.role} "${e.name}"`;
2533
+ if (e.attrs) line += ` [${e.attrs}]`;
2534
+ return line;
2535
+ });
2536
+ return {
2537
+ title,
2538
+ interactiveElements: interactiveLines.join("\n"),
2539
+ accessibilityTree: raw.slice(0, 8e3)
2540
+ // Trim full tree for context window
2541
+ };
2542
+ }
2543
+ var MAX_ELEMENTS, MAX_TEXT_LEN, INTERACTIVE_ROLES, CONTEXT_ROLES;
2544
+ var init_snapshot_parser = __esm({
2545
+ "src/mcp/snapshot-parser.ts"() {
2546
+ "use strict";
2547
+ init_cjs_shims();
2548
+ MAX_ELEMENTS = 120;
2549
+ MAX_TEXT_LEN = 80;
2550
+ INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
2551
+ "button",
2552
+ "link",
2553
+ "textbox",
2554
+ "checkbox",
2555
+ "radio",
2556
+ "combobox",
2557
+ "listbox",
2558
+ "menuitem",
2559
+ "menuitemcheckbox",
2560
+ "menuitemradio",
2561
+ "option",
2562
+ "searchbox",
2563
+ "slider",
2564
+ "spinbutton",
2565
+ "switch",
2566
+ "tab",
2567
+ "treeitem"
2568
+ ]);
2569
+ CONTEXT_ROLES = /* @__PURE__ */ new Set([
2570
+ "heading",
2571
+ "img",
2572
+ "navigation",
2573
+ "dialog",
2574
+ "alert",
2575
+ "alertdialog",
2576
+ "status",
2577
+ "progressbar",
2578
+ "table",
2579
+ "row",
2580
+ "cell",
2581
+ "columnheader",
2582
+ "rowheader"
2583
+ ]);
2584
+ }
2585
+ });
2586
+
2587
+ // src/ai/prompts/mcp-action.ts
2588
+ function buildMcpActionPrompt(instruction, snapshot) {
2589
+ const user = `Current page accessibility snapshot:
2590
+ ${snapshot.slice(0, 6e3)}
2591
+
2592
+ Step to execute: "${instruction}"
2593
+
2594
+ Plan the MCP browser actions needed. Return JSON with the "actions" array.`;
2595
+ return { system: SYSTEM_PROMPT, user };
2596
+ }
2597
+ var SYSTEM_PROMPT;
2598
+ var init_mcp_action = __esm({
2599
+ "src/ai/prompts/mcp-action.ts"() {
2600
+ "use strict";
2601
+ init_cjs_shims();
2602
+ SYSTEM_PROMPT = `You are a browser automation expert. Given a test step instruction and an accessibility snapshot of the current page, plan the exact browser actions needed to accomplish the step.
2603
+
2604
+ \u2501\u2501\u2501 OUTPUT FORMAT \u2501\u2501\u2501
2605
+ Return ONLY valid JSON \u2014 no markdown, no explanations.
2606
+
2607
+ {
2608
+ "actions": [
2609
+ { "tool": "browser_click", "args": { "element": "button 'Login'", "ref": "s1e4" } },
2610
+ { "tool": "browser_type", "args": { "element": "textbox 'Username'", "ref": "s1e2", "text": "Admin" } }
2611
+ ]
2612
+ }
2613
+
2614
+ \u2501\u2501\u2501 AVAILABLE TOOLS \u2501\u2501\u2501
2615
+ browser_click \u2014 { element: string, ref: string } Click an element
2616
+ browser_type \u2014 { element: string, ref: string, text: string } Type text into a field
2617
+ browser_select_option \u2014 { element: string, ref: string, values: string[] } Select dropdown option
2618
+ browser_hover \u2014 { element: string, ref: string } Hover over element
2619
+ browser_press_key \u2014 { key: string } Press a keyboard key (Enter, Tab, Escape...)
2620
+ browser_navigate \u2014 { url: string } Navigate to a URL
2621
+
2622
+ \u2501\u2501\u2501 RULES \u2501\u2501\u2501
2623
+ 1. Use EXACT element descriptions and refs from the accessibility snapshot
2624
+ 2. For "enter/type/fill" instructions \u2192 use browser_type
2625
+ 3. For "click/press/select" instructions \u2192 use browser_click
2626
+ 4. For navigation instructions \u2192 use browser_navigate
2627
+ 5. Keep actions minimal \u2014 only what's needed for the instruction
2628
+ 6. Order matters \u2014 actions execute sequentially
2629
+ 7. For dropdown/select instructions \u2192 use browser_select_option
2630
+ 8. If a step requires pressing Enter after typing \u2192 add browser_press_key with key "Enter"
2631
+
2632
+ \u2501\u2501\u2501 ELEMENT MATCHING \u2501\u2501\u2501
2633
+ Match elements from the snapshot by their role and name:
2634
+ - "Enter username" \u2192 find textbox with name containing "username" or "user"
2635
+ - "Click Login button" \u2192 find button with name containing "Login"
2636
+ - "Select country" \u2192 find combobox/listbox with name containing "country"
2637
+
2638
+ Always use the ref value from the snapshot (e.g., "s1e4") \u2014 this is the unique element identifier.`;
2639
+ }
2640
+ });
2641
+
2642
+ // src/mcp/action-mapper.ts
2643
+ async function planMcpActions(provider, instruction, snapshot) {
2644
+ try {
2645
+ const { system, user } = buildMcpActionPrompt(instruction, snapshot);
2646
+ const fakeContext = {
2647
+ url: "",
2648
+ title: "",
2649
+ interactiveElements: "",
2650
+ htmlSnapshot: "",
2651
+ previousSteps: [{ instruction: "SYSTEM", code: system }],
2652
+ variables: {}
2653
+ };
2654
+ const raw = await provider.generateCode(`[MCP_ACTION_PLAN] ${user}`, fakeContext);
2655
+ const cleaned = raw.replace(/```json?\s*/g, "").replace(/```/g, "").trim();
2656
+ const parsed = JSON.parse(cleaned);
2657
+ if (!parsed.actions || !Array.isArray(parsed.actions)) {
2658
+ logger11.warn("AI returned invalid action plan \u2014 no actions array");
2659
+ return [];
2660
+ }
2661
+ const actions = parsed.actions.filter(
2662
+ (a) => a.tool && typeof a.tool === "string" && a.args
2663
+ );
2664
+ logger11.info({ instruction, actionCount: actions.length }, "MCP action plan created");
2665
+ return actions;
2666
+ } catch (err) {
2667
+ const msg = err instanceof Error ? err.message : String(err);
2668
+ logger11.warn({ err: msg, instruction }, "Failed to plan MCP actions");
2669
+ return [];
2670
+ }
2671
+ }
2672
+ var logger11;
2673
+ var init_action_mapper = __esm({
2674
+ "src/mcp/action-mapper.ts"() {
2675
+ "use strict";
2676
+ init_cjs_shims();
2677
+ init_mcp_action();
2678
+ init_logger();
2679
+ logger11 = createChildLogger("action-mapper");
2680
+ }
2681
+ });
2682
+
2683
+ // src/mcp/action-to-script.ts
2684
+ function convertActionsToScript(actions) {
2685
+ const lines = [];
2686
+ for (const action of actions) {
2687
+ const line = convertSingleAction(action);
2688
+ if (line) {
2689
+ lines.push(line);
2690
+ }
2691
+ }
2692
+ if (lines.length === 0) {
2693
+ logger12.warn("No actions could be converted to script");
2694
+ return "";
2695
+ }
2696
+ return lines.join("\n");
2697
+ }
2698
+ function convertSingleAction(action) {
2699
+ const args = action.args ?? {};
2700
+ switch (action.tool) {
2701
+ case "browser_navigate": {
2702
+ const url = String(args.url ?? "");
2703
+ if (!url) return null;
2704
+ return `await page.goto('${escapeStr(url)}', { waitUntil: 'networkidle' });`;
2705
+ }
2706
+ case "browser_click": {
2707
+ const locator = elementToLocator(args);
2708
+ if (!locator) return null;
2709
+ return `await ${locator}.click();`;
2710
+ }
2711
+ case "browser_type": {
2712
+ const locator = elementToLocator(args);
2713
+ const text = String(args.text ?? "");
2714
+ if (!locator || !text) return null;
2715
+ return `await ${locator}.fill('${escapeStr(text)}');`;
2716
+ }
2717
+ case "browser_select_option": {
2718
+ const locator = elementToLocator(args);
2719
+ const values = args.values;
2720
+ if (!locator || !values?.length) return null;
2721
+ const valStr = values.map((v) => `'${escapeStr(v)}'`).join(", ");
2722
+ return `await ${locator}.selectOption([${valStr}]);`;
2723
+ }
2724
+ case "browser_hover": {
2725
+ const locator = elementToLocator(args);
2726
+ if (!locator) return null;
2727
+ return `await ${locator}.hover();`;
2728
+ }
2729
+ case "browser_press_key": {
2730
+ const key = String(args.key ?? "");
2731
+ if (!key) return null;
2732
+ return `await page.keyboard.press('${escapeStr(key)}');`;
2733
+ }
2734
+ case "browser_wait": {
2735
+ const time = Number(args.time ?? 1e3);
2736
+ return `await page.waitForTimeout(${time});`;
2737
+ }
2738
+ default: {
2739
+ logger12.debug({ tool: action.tool }, "Unknown MCP action \u2014 skipping");
2740
+ return null;
2741
+ }
2742
+ }
2743
+ }
2744
+ function elementToLocator(args) {
2745
+ const element = String(args.element ?? "");
2746
+ if (!element) return null;
2747
+ const match = element.match(/^(\w+)\s+['"]([^'"]*)['"]/);
2748
+ if (match) {
2749
+ const role = match[1].toLowerCase();
2750
+ const name = match[2];
2751
+ const roleMap = {
2752
+ button: "button",
2753
+ link: "link",
2754
+ textbox: "textbox",
2755
+ checkbox: "checkbox",
2756
+ radio: "radio",
2757
+ combobox: "combobox",
2758
+ listbox: "listbox",
2759
+ menuitem: "menuitem",
2760
+ option: "option",
2761
+ searchbox: "searchbox",
2762
+ slider: "slider",
2763
+ spinbutton: "spinbutton",
2764
+ switch: "switch",
2765
+ tab: "tab",
2766
+ heading: "heading"
2767
+ };
2768
+ const pwRole = roleMap[role];
2769
+ if (pwRole && name) {
2770
+ return `page.getByRole('${pwRole}', { name: '${escapeStr(name)}' })`;
2771
+ }
2772
+ if ((role === "textbox" || role === "searchbox") && name) {
2773
+ return `page.getByLabel('${escapeStr(name)}')`;
2774
+ }
2775
+ }
2776
+ const textMatch = element.match(/['"]([^'"]+)['"]/);
2777
+ if (textMatch) {
2778
+ return `page.getByText('${escapeStr(textMatch[1])}')`;
2779
+ }
2780
+ return null;
2781
+ }
2782
+ function escapeStr(s) {
2783
+ return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
2784
+ }
2785
+ var logger12;
2786
+ var init_action_to_script = __esm({
2787
+ "src/mcp/action-to-script.ts"() {
2788
+ "use strict";
2789
+ init_cjs_shims();
2790
+ init_logger();
2791
+ logger12 = createChildLogger("action-to-script");
2792
+ }
2793
+ });
2794
+
2401
2795
  // src/ai/smart-router.ts
2402
- var logger11, SmartRouter;
2796
+ var logger13, SmartRouter;
2403
2797
  var init_smart_router = __esm({
2404
2798
  "src/ai/smart-router.ts"() {
2405
2799
  "use strict";
@@ -2411,7 +2805,10 @@ var init_smart_router = __esm({
2411
2805
  init_sanitize();
2412
2806
  init_logger();
2413
2807
  init_step_type_detector();
2414
- logger11 = createChildLogger("smart-router");
2808
+ init_snapshot_parser();
2809
+ init_action_mapper();
2810
+ init_action_to_script();
2811
+ logger13 = createChildLogger("smart-router");
2415
2812
  SmartRouter = class {
2416
2813
  templateEngine;
2417
2814
  cache;
@@ -2419,6 +2816,8 @@ var init_smart_router = __esm({
2419
2816
  batchProcessor;
2420
2817
  primaryProvider;
2421
2818
  fastProvider;
2819
+ mcpSession;
2820
+ actThenScript;
2422
2821
  stats = {
2423
2822
  template: 0,
2424
2823
  cache: 0,
@@ -2430,6 +2829,8 @@ var init_smart_router = __esm({
2430
2829
  constructor(config) {
2431
2830
  this.primaryProvider = config.primaryProvider;
2432
2831
  this.fastProvider = config.fastProvider ?? null;
2832
+ this.mcpSession = config.mcpSession ?? null;
2833
+ this.actThenScript = config.actThenScript ?? false;
2433
2834
  this.cache = config.cache ?? new CodeCache();
2434
2835
  this.templateEngine = new TemplateEngine();
2435
2836
  this.classifier = new ComplexityClassifier();
@@ -2443,7 +2844,7 @@ var init_smart_router = __esm({
2443
2844
  const templateCode = this.templateEngine.tryMatch(instruction);
2444
2845
  if (templateCode !== null) {
2445
2846
  this.stats.template++;
2446
- logger11.debug({ instruction }, "Template match");
2847
+ logger13.debug({ instruction }, "Template match");
2447
2848
  return { code: fixAntiPatterns(templateCode), strategy: "template", cost: 0, model: "local" };
2448
2849
  }
2449
2850
  }
@@ -2451,16 +2852,26 @@ var init_smart_router = __esm({
2451
2852
  const cached = this.cache.get(cacheKey);
2452
2853
  if (cached !== null) {
2453
2854
  this.stats.cache++;
2454
- logger11.debug({ instruction }, "Cache hit");
2855
+ logger13.debug({ instruction }, "Cache hit");
2455
2856
  const fixed = fixButtonSelectors(instruction, fixAntiPatterns(cached.code));
2456
2857
  return { code: fixed, strategy: "cache", cost: 0, model: "local" };
2457
2858
  }
2859
+ const enrichedContext = this.mcpSession ? await this.enrichWithMcp(instruction, pageContext) : pageContext;
2860
+ if (this.actThenScript && this.mcpSession && enrichedContext.accessibilityTree) {
2861
+ const actResult = await this.tryActThenScript(instruction, enrichedContext);
2862
+ if (actResult !== null) {
2863
+ this.stats.primary++;
2864
+ this.cache.set(cacheKey, actResult, pageContext.url);
2865
+ return { code: actResult, strategy: "primary", cost: 3e-3, model: "mcp-act" };
2866
+ }
2867
+ logger13.debug({ instruction }, "Act-then-script failed \u2014 falling back to AI generation");
2868
+ }
2458
2869
  const complexity = this.classifier.classify(instruction);
2459
2870
  const usesFast = this.fastProvider !== null && complexity !== "complex";
2460
2871
  const provider = usesFast ? this.fastProvider : this.primaryProvider;
2461
2872
  const strategy = usesFast ? "fast" : "primary";
2462
- logger11.debug({ instruction, complexity, strategy }, "AI call");
2463
- const code = await provider.generateCode(instruction, pageContext);
2873
+ logger13.debug({ instruction, complexity, strategy }, "AI call");
2874
+ const code = await provider.generateCode(instruction, enrichedContext);
2464
2875
  this.cache.set(cacheKey, code, pageContext.url);
2465
2876
  if (usesFast) {
2466
2877
  this.stats.fast++;
@@ -2515,6 +2926,418 @@ var init_smart_router = __esm({
2515
2926
  getPrimaryProvider() {
2516
2927
  return this.primaryProvider;
2517
2928
  }
2929
+ /** Exposes the MCP session (e.g. for proactive healing or diagnostics). */
2930
+ getMcpSession() {
2931
+ return this.mcpSession;
2932
+ }
2933
+ // ─── MCP enrichment ─────────────────────────────────────────────────────────
2934
+ /**
2935
+ * Enrich a PageContext with live accessibility data from the MCP browser.
2936
+ * On any failure, returns the original context unchanged (silent fallback).
2937
+ */
2938
+ async enrichWithMcp(_instruction, ctx) {
2939
+ try {
2940
+ const session = this.mcpSession;
2941
+ if (ctx.url && (!session.currentUrl || !this.isSameOriginPath(session.currentUrl, ctx.url))) {
2942
+ logger13.debug({ url: ctx.url }, "MCP: navigating to target URL");
2943
+ await session.navigate(ctx.url);
2944
+ }
2945
+ const raw = await session.snapshot();
2946
+ if (!raw) {
2947
+ logger13.debug("MCP: snapshot returned null \u2014 using blind context");
2948
+ return ctx;
2949
+ }
2950
+ const parsed = parseSnapshot(raw);
2951
+ logger13.info(
2952
+ { elements: parsed.interactiveElements.split("\n").length, title: parsed.title },
2953
+ "MCP: enriched context with live page elements"
2954
+ );
2955
+ return {
2956
+ ...ctx,
2957
+ title: parsed.title || ctx.title,
2958
+ interactiveElements: parsed.interactiveElements || ctx.interactiveElements,
2959
+ accessibilityTree: parsed.accessibilityTree
2960
+ };
2961
+ } catch (err) {
2962
+ const msg = err instanceof Error ? err.message : String(err);
2963
+ logger13.warn({ err: msg }, "MCP enrichment failed \u2014 using blind context");
2964
+ return ctx;
2965
+ }
2966
+ }
2967
+ // ─── Act-then-script ───────────────────────────────────────────────────────
2968
+ /**
2969
+ * Two-phase generation: ask AI to plan MCP actions → execute on real browser
2970
+ * → convert successful actions to Playwright code.
2971
+ *
2972
+ * Returns the generated code on success, or null to fall back to standard AI.
2973
+ */
2974
+ async tryActThenScript(instruction, ctx) {
2975
+ try {
2976
+ const session = this.mcpSession;
2977
+ const snapshot = ctx.accessibilityTree ?? "";
2978
+ if (!snapshot) return null;
2979
+ const actions = await planMcpActions(this.primaryProvider, instruction, snapshot);
2980
+ if (actions.length === 0) return null;
2981
+ for (const action of actions) {
2982
+ const args = action.args ?? {};
2983
+ let result = null;
2984
+ switch (action.tool) {
2985
+ case "browser_click":
2986
+ result = await session.click(String(args.element ?? ""), String(args.ref ?? ""));
2987
+ break;
2988
+ case "browser_type":
2989
+ result = await session.fill(String(args.element ?? ""), String(args.ref ?? ""), String(args.text ?? ""));
2990
+ break;
2991
+ case "browser_select_option":
2992
+ result = await session.selectOption(String(args.element ?? ""), String(args.ref ?? ""), args.values ?? []);
2993
+ break;
2994
+ case "browser_hover":
2995
+ result = await session.hover(String(args.element ?? ""), String(args.ref ?? ""));
2996
+ break;
2997
+ case "browser_press_key":
2998
+ result = await session.pressKey(String(args.key ?? ""));
2999
+ break;
3000
+ case "browser_navigate":
3001
+ result = await session.navigate(String(args.url ?? ""));
3002
+ break;
3003
+ default:
3004
+ logger13.debug({ tool: action.tool }, "Unknown MCP action in act-then-script");
3005
+ continue;
3006
+ }
3007
+ if (result === null) {
3008
+ logger13.debug({ tool: action.tool, instruction }, "MCP action failed during act-then-script");
3009
+ return null;
3010
+ }
3011
+ }
3012
+ const code = convertActionsToScript(actions);
3013
+ if (!code) return null;
3014
+ logger13.info({ instruction, actions: actions.length }, "Act-then-script succeeded");
3015
+ return code;
3016
+ } catch (err) {
3017
+ const msg = err instanceof Error ? err.message : String(err);
3018
+ logger13.warn({ err: msg, instruction }, "Act-then-script failed");
3019
+ return null;
3020
+ }
3021
+ }
3022
+ isSameOriginPath(a, b) {
3023
+ try {
3024
+ const urlA = new URL(a);
3025
+ const urlB = new URL(b);
3026
+ return urlA.origin === urlB.origin && urlA.pathname === urlB.pathname;
3027
+ } catch {
3028
+ return a === b;
3029
+ }
3030
+ }
3031
+ };
3032
+ }
3033
+ });
3034
+
3035
+ // src/mcp/mcp-client.ts
3036
+ var import_client, import_stdio, logger14, McpClient;
3037
+ var init_mcp_client = __esm({
3038
+ "src/mcp/mcp-client.ts"() {
3039
+ "use strict";
3040
+ init_cjs_shims();
3041
+ import_client = require("@modelcontextprotocol/sdk/client/index.js");
3042
+ import_stdio = require("@modelcontextprotocol/sdk/client/stdio.js");
3043
+ init_logger();
3044
+ logger14 = createChildLogger("mcp-client");
3045
+ McpClient = class {
3046
+ client = null;
3047
+ transport = null;
3048
+ _connected = false;
3049
+ headless;
3050
+ actionTimeout;
3051
+ constructor(config = {}) {
3052
+ this.headless = config.headless ?? true;
3053
+ this.actionTimeout = config.actionTimeout ?? 15e3;
3054
+ }
3055
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
3056
+ get connected() {
3057
+ return this._connected;
3058
+ }
3059
+ async connect() {
3060
+ if (this._connected) return;
3061
+ const args = ["@playwright/mcp@latest"];
3062
+ if (this.headless) args.push("--headless");
3063
+ this.transport = new import_stdio.StdioClientTransport({
3064
+ command: "npx",
3065
+ args
3066
+ });
3067
+ this.client = new import_client.Client(
3068
+ { name: "assuremind-studio", version: "1.0.0" },
3069
+ { capabilities: {} }
3070
+ );
3071
+ await this.client.connect(this.transport);
3072
+ this._connected = true;
3073
+ logger14.info({ headless: this.headless }, "MCP client connected");
3074
+ }
3075
+ async disconnect() {
3076
+ if (!this._connected || !this.client) return;
3077
+ try {
3078
+ await this.client.close();
3079
+ } catch {
3080
+ }
3081
+ this.client = null;
3082
+ this.transport = null;
3083
+ this._connected = false;
3084
+ logger14.info("MCP client disconnected");
3085
+ }
3086
+ // ─── Browser Navigation ─────────────────────────────────────────────────────
3087
+ /** Navigate to a URL and return the page snapshot. */
3088
+ async navigate(url) {
3089
+ return this.callTool("browser_navigate", { url });
3090
+ }
3091
+ /** Take an accessibility snapshot of the current page. */
3092
+ async snapshot() {
3093
+ return this.callTool("browser_snapshot", {});
3094
+ }
3095
+ // ─── Browser Actions ────────────────────────────────────────────────────────
3096
+ /** Click an element by its accessibility ref. */
3097
+ async click(element, ref) {
3098
+ return this.callTool("browser_click", { element, ref });
3099
+ }
3100
+ /** Fill a text field by accessibility ref. */
3101
+ async fill(element, ref, value) {
3102
+ return this.callTool("browser_type", { element, ref, text: value });
3103
+ }
3104
+ /** Select an option from a dropdown. */
3105
+ async selectOption(element, ref, values) {
3106
+ return this.callTool("browser_select_option", { element, ref, values });
3107
+ }
3108
+ /** Hover over an element. */
3109
+ async hover(element, ref) {
3110
+ return this.callTool("browser_hover", { element, ref });
3111
+ }
3112
+ /** Press a keyboard key. */
3113
+ async pressKey(key) {
3114
+ return this.callTool("browser_press_key", { key });
3115
+ }
3116
+ // ─── Diagnostics ────────────────────────────────────────────────────────────
3117
+ /** Take a screenshot (returns base64 image content). */
3118
+ async screenshot() {
3119
+ return this.callTool("browser_take_screenshot", {});
3120
+ }
3121
+ /** Get console logs from the page. */
3122
+ async consoleLogs() {
3123
+ return this.callTool("browser_console_messages", {});
3124
+ }
3125
+ /** Get network request/response log. */
3126
+ async networkRequests() {
3127
+ return this.callTool("browser_network_requests", {});
3128
+ }
3129
+ // ─── Wait helpers ───────────────────────────────────────────────────────────
3130
+ /** Wait for a condition then return a fresh snapshot. */
3131
+ async waitAndSnapshot(timeMs) {
3132
+ if (timeMs) {
3133
+ return this.callTool("browser_wait", { time: timeMs });
3134
+ }
3135
+ return this.callTool("browser_wait", { time: 1e3 });
3136
+ }
3137
+ // ─── Internal ───────────────────────────────────────────────────────────────
3138
+ /**
3139
+ * Call an MCP tool by name with arguments.
3140
+ * Returns the text content on success, or null on any error/timeout.
3141
+ */
3142
+ async callTool(name, args) {
3143
+ if (!this._connected || !this.client) {
3144
+ logger14.warn({ tool: name }, "MCP callTool called but client not connected");
3145
+ return null;
3146
+ }
3147
+ try {
3148
+ const result = await Promise.race([
3149
+ this.client.callTool({ name, arguments: args }),
3150
+ new Promise(
3151
+ (_, reject) => setTimeout(() => reject(new Error(`MCP tool "${name}" timed out after ${this.actionTimeout}ms`)), this.actionTimeout)
3152
+ )
3153
+ ]);
3154
+ if (result && Array.isArray(result.content)) {
3155
+ const textParts = result.content.filter((c) => c.type === "text").map((c) => c.text);
3156
+ return textParts.join("\n") || null;
3157
+ }
3158
+ return null;
3159
+ } catch (err) {
3160
+ const msg = err instanceof Error ? err.message : String(err);
3161
+ logger14.warn({ tool: name, error: msg }, "MCP tool call failed");
3162
+ return null;
3163
+ }
3164
+ }
3165
+ };
3166
+ }
3167
+ });
3168
+
3169
+ // src/mcp/mcp-session.ts
3170
+ var logger15, McpSession;
3171
+ var init_mcp_session = __esm({
3172
+ "src/mcp/mcp-session.ts"() {
3173
+ "use strict";
3174
+ init_cjs_shims();
3175
+ init_mcp_client();
3176
+ init_logger();
3177
+ logger15 = createChildLogger("mcp-session");
3178
+ McpSession = class {
3179
+ client;
3180
+ idleTimeout;
3181
+ idleTimer = null;
3182
+ mutex = Promise.resolve();
3183
+ _currentUrl = null;
3184
+ constructor(config = {}) {
3185
+ this.client = new McpClient(config);
3186
+ this.idleTimeout = config.idleTimeout ?? 3e4;
3187
+ }
3188
+ /** The URL the MCP browser is currently on (or null if not navigated). */
3189
+ get currentUrl() {
3190
+ return this._currentUrl;
3191
+ }
3192
+ /** Whether the underlying client is connected. */
3193
+ get connected() {
3194
+ return this.client.connected;
3195
+ }
3196
+ // ─── Connection management ──────────────────────────────────────────────────
3197
+ /** Ensure the client is connected (lazy init). */
3198
+ async ensureConnected() {
3199
+ this.resetIdleTimer();
3200
+ if (!this.client.connected) {
3201
+ await this.client.connect();
3202
+ }
3203
+ }
3204
+ /**
3205
+ * Release the session — starts the idle timer.
3206
+ * If no new calls arrive within `idleTimeout`, the client disconnects.
3207
+ */
3208
+ release() {
3209
+ this.startIdleTimer();
3210
+ }
3211
+ /** Force-disconnect immediately. */
3212
+ async disconnect() {
3213
+ this.clearIdleTimer();
3214
+ this._currentUrl = null;
3215
+ await this.client.disconnect();
3216
+ }
3217
+ // ─── Navigation ─────────────────────────────────────────────────────────────
3218
+ /**
3219
+ * Navigate to a URL (skips if already on that URL).
3220
+ * Returns the accessibility snapshot after navigation.
3221
+ */
3222
+ async navigate(url) {
3223
+ return this.withMutex(async () => {
3224
+ await this.ensureConnected();
3225
+ if (this._currentUrl && this.isSameUrl(this._currentUrl, url)) {
3226
+ logger15.debug({ url }, "Already on URL \u2014 skipping navigation");
3227
+ return this.client.snapshot();
3228
+ }
3229
+ const result = await this.client.navigate(url);
3230
+ if (result !== null) {
3231
+ this._currentUrl = url;
3232
+ }
3233
+ return result;
3234
+ });
3235
+ }
3236
+ /** Take a fresh accessibility snapshot of the current page. */
3237
+ async snapshot() {
3238
+ return this.withMutex(async () => {
3239
+ await this.ensureConnected();
3240
+ return this.client.snapshot();
3241
+ });
3242
+ }
3243
+ // ─── Actions (used by act-then-script — Phase 4) ───────────────────────────
3244
+ async click(element, ref) {
3245
+ return this.withMutex(async () => {
3246
+ await this.ensureConnected();
3247
+ return this.client.click(element, ref);
3248
+ });
3249
+ }
3250
+ async fill(element, ref, value) {
3251
+ return this.withMutex(async () => {
3252
+ await this.ensureConnected();
3253
+ return this.client.fill(element, ref, value);
3254
+ });
3255
+ }
3256
+ async selectOption(element, ref, values) {
3257
+ return this.withMutex(async () => {
3258
+ await this.ensureConnected();
3259
+ return this.client.selectOption(element, ref, values);
3260
+ });
3261
+ }
3262
+ async hover(element, ref) {
3263
+ return this.withMutex(async () => {
3264
+ await this.ensureConnected();
3265
+ return this.client.hover(element, ref);
3266
+ });
3267
+ }
3268
+ async pressKey(key) {
3269
+ return this.withMutex(async () => {
3270
+ await this.ensureConnected();
3271
+ return this.client.pressKey(key);
3272
+ });
3273
+ }
3274
+ // ─── Diagnostics ────────────────────────────────────────────────────────────
3275
+ async screenshot() {
3276
+ return this.withMutex(async () => {
3277
+ await this.ensureConnected();
3278
+ return this.client.screenshot();
3279
+ });
3280
+ }
3281
+ async consoleLogs() {
3282
+ return this.withMutex(async () => {
3283
+ await this.ensureConnected();
3284
+ return this.client.consoleLogs();
3285
+ });
3286
+ }
3287
+ async networkRequests() {
3288
+ return this.withMutex(async () => {
3289
+ await this.ensureConnected();
3290
+ return this.client.networkRequests();
3291
+ });
3292
+ }
3293
+ async waitAndSnapshot(timeMs) {
3294
+ return this.withMutex(async () => {
3295
+ await this.ensureConnected();
3296
+ return this.client.waitAndSnapshot(timeMs);
3297
+ });
3298
+ }
3299
+ // ─── Internal helpers ───────────────────────────────────────────────────────
3300
+ /** Simple mutex — ensures only one MCP tool call runs at a time. */
3301
+ async withMutex(fn) {
3302
+ let release;
3303
+ const next = new Promise((resolve) => {
3304
+ release = resolve;
3305
+ });
3306
+ const prev = this.mutex;
3307
+ this.mutex = next;
3308
+ await prev;
3309
+ this.resetIdleTimer();
3310
+ try {
3311
+ return await fn();
3312
+ } finally {
3313
+ release();
3314
+ }
3315
+ }
3316
+ isSameUrl(a, b) {
3317
+ try {
3318
+ const urlA = new URL(a);
3319
+ const urlB = new URL(b);
3320
+ return urlA.origin === urlB.origin && urlA.pathname === urlB.pathname;
3321
+ } catch {
3322
+ return a === b;
3323
+ }
3324
+ }
3325
+ startIdleTimer() {
3326
+ this.clearIdleTimer();
3327
+ this.idleTimer = setTimeout(async () => {
3328
+ logger15.info("MCP session idle \u2014 disconnecting");
3329
+ await this.disconnect();
3330
+ }, this.idleTimeout);
3331
+ }
3332
+ resetIdleTimer() {
3333
+ this.clearIdleTimer();
3334
+ }
3335
+ clearIdleTimer() {
3336
+ if (this.idleTimer) {
3337
+ clearTimeout(this.idleTimer);
3338
+ this.idleTimer = null;
3339
+ }
3340
+ }
2518
3341
  };
2519
3342
  }
2520
3343
  });
@@ -2575,12 +3398,21 @@ function buildStepToCodePrompt(instruction, context) {
2575
3398
  const prevSteps = context.previousSteps.length > 0 ? context.previousSteps.slice(-5).map((s, i) => ` ${i + 1}. "${s.instruction}"
2576
3399
  ${s.code}`).join("\n") : " None";
2577
3400
  const vars = Object.keys(context.variables).length > 0 ? Object.entries(context.variables).map(([k, v]) => ` {{${k}}} = ${v}`).join("\n") : " None";
3401
+ const hasLiveElements = !!(context.interactiveElements && context.accessibilityTree);
3402
+ const elementsSection = hasLiveElements ? `Interactive elements on page (LIVE from actual page \u2014 use exact roles/names):
3403
+ ${context.interactiveElements}
3404
+
3405
+ \u26A0\uFE0F The elements above are LIVE from the actual page via accessibility snapshot.
3406
+ Use exact roles and names from these elements. Do NOT guess selectors.` : `Interactive elements on page (use these to pick selectors):
3407
+ ${context.interactiveElements || " None captured"}`;
3408
+ const accessibilitySection = context.accessibilityTree ? `
3409
+ Full accessibility tree (for additional context):
3410
+ ${context.accessibilityTree.slice(0, 4e3)}` : "";
2578
3411
  const user = `Current page:
2579
3412
  URL: ${context.url}
2580
3413
  Title: ${context.title}
2581
3414
 
2582
- Interactive elements on page (use these to pick selectors):
2583
- ${context.interactiveElements || " None captured"}
3415
+ ${elementsSection}${accessibilitySection}
2584
3416
 
2585
3417
  Previous steps already done (for context):
2586
3418
  ${prevSteps}
@@ -2589,14 +3421,14 @@ Runtime variables:
2589
3421
  ${vars}
2590
3422
 
2591
3423
  Step to implement: "${instruction}"`;
2592
- return { system: SYSTEM_PROMPT, user };
3424
+ return { system: SYSTEM_PROMPT2, user };
2593
3425
  }
2594
- var SYSTEM_PROMPT;
3426
+ var SYSTEM_PROMPT2;
2595
3427
  var init_step_to_code = __esm({
2596
3428
  "src/ai/prompts/step-to-code.ts"() {
2597
3429
  "use strict";
2598
3430
  init_cjs_shims();
2599
- SYSTEM_PROMPT = `You are a Playwright test automation expert. Convert plain English test steps into executable Playwright TypeScript code.
3431
+ SYSTEM_PROMPT2 = `You are a Playwright test automation expert. Convert plain English test steps into executable Playwright TypeScript code.
2600
3432
 
2601
3433
  \u2501\u2501\u2501 OUTPUT RULES \u2501\u2501\u2501
2602
3434
  1. Return ONLY raw executable code \u2014 no markdown, no \`\`\`, no explanations
@@ -2742,9 +3574,9 @@ Generate a JSON test suite following this exact schema:
2742
3574
  ${SCHEMA_GUIDE}
2743
3575
 
2744
3576
  Important: return only the JSON object, nothing else.`;
2745
- return { system: SYSTEM_PROMPT2, user };
3577
+ return { system: SYSTEM_PROMPT3, user };
2746
3578
  }
2747
- var OUTLINE_SYSTEM, OUTLINE_SCHEMA, STEPS_SYSTEM, STEPS_SCHEMA, SYSTEM_PROMPT2, SCHEMA_GUIDE;
3579
+ var OUTLINE_SYSTEM, OUTLINE_SCHEMA, STEPS_SYSTEM, STEPS_SCHEMA, SYSTEM_PROMPT3, SCHEMA_GUIDE;
2748
3580
  var init_story_to_suite = __esm({
2749
3581
  "src/ai/prompts/story-to-suite.ts"() {
2750
3582
  "use strict";
@@ -2780,7 +3612,7 @@ Rules:
2780
3612
  { "instruction": "Verify the Dashboard heading is visible" }
2781
3613
  ]
2782
3614
  }`;
2783
- SYSTEM_PROMPT2 = `You are a QA test planning expert. Convert user stories into structured test suites.
3615
+ SYSTEM_PROMPT3 = `You are a QA test planning expert. Convert user stories into structured test suites.
2784
3616
 
2785
3617
  RULES:
2786
3618
  1. Return ONLY valid JSON \u2014 no markdown, no explanations, no code fences
@@ -2812,7 +3644,25 @@ RULES:
2812
3644
  });
2813
3645
 
2814
3646
  // src/ai/prompts/self-heal.ts
2815
- function buildSelfHealPrompt(instruction, failedCode, error, context) {
3647
+ function buildSelfHealPrompt(instruction, failedCode, error, context, mcpDiagnostics) {
3648
+ const hasLiveElements = !!(context.interactiveElements && context.accessibilityTree);
3649
+ const elementsSection = hasLiveElements ? `Current interactive elements (LIVE from page snapshot \u2014 use exact names):
3650
+ ${context.interactiveElements}` : `Current interactive elements:
3651
+ ${context.interactiveElements || " None captured"}`;
3652
+ const accessibilitySection = context.accessibilityTree ? `
3653
+ Accessibility tree:
3654
+ ${context.accessibilityTree.slice(0, 3e3)}` : "";
3655
+ let diagnosticsSection = "";
3656
+ if (mcpDiagnostics?.consoleLogs) {
3657
+ diagnosticsSection += `
3658
+ Browser console logs (recent errors/warnings):
3659
+ ${mcpDiagnostics.consoleLogs.slice(0, 1500)}`;
3660
+ }
3661
+ if (mcpDiagnostics?.networkRequests) {
3662
+ diagnosticsSection += `
3663
+ Recent network requests:
3664
+ ${mcpDiagnostics.networkRequests.slice(0, 1500)}`;
3665
+ }
2816
3666
  const user = `A test step has failed. Generate replacement Playwright code.
2817
3667
 
2818
3668
  Original instruction: "${instruction}"
@@ -2827,21 +3677,20 @@ Current page state:
2827
3677
  URL: ${context.url}
2828
3678
  Title: ${context.title}
2829
3679
 
2830
- Current interactive elements:
2831
- ${context.interactiveElements || " None captured"}
3680
+ ${elementsSection}
2832
3681
 
2833
3682
  HTML snapshot (partial):
2834
- ${context.htmlSnapshot.slice(0, 3e3)}
3683
+ ${context.htmlSnapshot.slice(0, 3e3)}${accessibilitySection}${diagnosticsSection}
2835
3684
 
2836
3685
  Generate new Playwright code that achieves: "${instruction}"`;
2837
- return { system: SYSTEM_PROMPT3, user };
3686
+ return { system: SYSTEM_PROMPT4, user };
2838
3687
  }
2839
- var SYSTEM_PROMPT3;
3688
+ var SYSTEM_PROMPT4;
2840
3689
  var init_self_heal = __esm({
2841
3690
  "src/ai/prompts/self-heal.ts"() {
2842
3691
  "use strict";
2843
3692
  init_cjs_shims();
2844
- SYSTEM_PROMPT3 = `You are a Playwright test self-healing expert. A test step has failed due to an infrastructure issue (broken selector, element moved, DOM changed) and you must generate new code that will work with the current page state.
3693
+ SYSTEM_PROMPT4 = `You are a Playwright test self-healing expert. A test step has failed due to an infrastructure issue (broken selector, element moved, DOM changed) and you must generate new code that will work with the current page state.
2845
3694
 
2846
3695
  RULES:
2847
3696
  1. Return ONLY raw executable code \u2014 no markdown, no explanations, no code fences
@@ -2862,14 +3711,23 @@ RULES:
2862
3711
  function buildBatchGeneratePrompt(steps, context) {
2863
3712
  const stepsList = steps.map((s) => ` ${s.index}. "${s.instruction}"`).join("\n");
2864
3713
  const vars = Object.keys(context.variables).length > 0 ? Object.entries(context.variables).map(([k, v]) => ` {{${k}}} = ${v}`).join("\n") : " None";
3714
+ const hasLiveElements = !!(context.interactiveElements && context.accessibilityTree);
3715
+ const elementsSection = hasLiveElements ? `Interactive elements on page (LIVE from actual page \u2014 use exact roles/names):
3716
+ ${context.interactiveElements}
3717
+
3718
+ \u26A0\uFE0F The elements above are LIVE from the actual page via accessibility snapshot.
3719
+ Use exact roles and names from these elements. Do NOT guess selectors.` : `Interactive elements on page (use to choose the right selector):
3720
+ ${context.interactiveElements || " None captured"}`;
3721
+ const accessibilitySection = context.accessibilityTree ? `
3722
+ Full accessibility tree (for additional context):
3723
+ ${context.accessibilityTree.slice(0, 4e3)}` : "";
2865
3724
  const user = `Generate Playwright code for these ${steps.length} test steps.
2866
3725
 
2867
3726
  Current page:
2868
3727
  URL: ${context.url}
2869
3728
  Title: ${context.title}
2870
3729
 
2871
- Interactive elements on page (use to choose the right selector):
2872
- ${context.interactiveElements || " None captured"}
3730
+ ${elementsSection}${accessibilitySection}
2873
3731
 
2874
3732
  Runtime variables:
2875
3733
  ${vars}
@@ -2886,14 +3744,14 @@ Return JSON:
2886
3744
  }
2887
3745
 
2888
3746
  Return one result per step. Include ALL ${steps.length} steps.`;
2889
- return { system: SYSTEM_PROMPT4, user };
3747
+ return { system: SYSTEM_PROMPT5, user };
2890
3748
  }
2891
- var SYSTEM_PROMPT4;
3749
+ var SYSTEM_PROMPT5;
2892
3750
  var init_batch_generate = __esm({
2893
3751
  "src/ai/prompts/batch-generate.ts"() {
2894
3752
  "use strict";
2895
3753
  init_cjs_shims();
2896
- SYSTEM_PROMPT4 = `You are a Playwright test automation expert. Generate executable Playwright code for multiple test steps in a single JSON response.
3754
+ SYSTEM_PROMPT5 = `You are a Playwright test automation expert. Generate executable Playwright code for multiple test steps in a single JSON response.
2897
3755
 
2898
3756
  \u2501\u2501\u2501 OUTPUT FORMAT \u2501\u2501\u2501
2899
3757
  1. Return ONLY valid JSON \u2014 no markdown, no \`\`\`, no explanations
@@ -4214,7 +5072,7 @@ function createProvider(overrideProvider, options) {
4214
5072
  );
4215
5073
  }
4216
5074
  }
4217
- function createSmartRouter(rootDir) {
5075
+ function createSmartRouter(rootDir, mcpSession, actThenScript) {
4218
5076
  const env = validateEnv();
4219
5077
  const primary = createProvider();
4220
5078
  let fast;
@@ -4223,19 +5081,19 @@ function createSmartRouter(rootDir) {
4223
5081
  if (fastProviderName) {
4224
5082
  try {
4225
5083
  fast = createProvider(fastProviderName);
4226
- logger12.info({ fast: fastProviderName, primary: String(env.AI_PROVIDER) }, "Tiered mode active");
5084
+ logger16.info({ fast: fastProviderName, primary: String(env.AI_PROVIDER) }, "Tiered mode active");
4227
5085
  } catch (err) {
4228
- logger12.warn({ err }, "Failed to create fast provider \u2014 tiered mode disabled");
5086
+ logger16.warn({ err }, "Failed to create fast provider \u2014 tiered mode disabled");
4229
5087
  }
4230
5088
  }
4231
5089
  }
4232
5090
  const cachePath = rootDir ? import_path12.default.join(rootDir, "results", ".code-cache.json") : void 0;
4233
5091
  const cache = new CodeCache(cachePath);
4234
- return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache });
5092
+ return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache, mcpSession, actThenScript });
4235
5093
  }
4236
5094
  async function generateSuiteFromStory(story, options) {
4237
5095
  const provider = createProvider();
4238
- logger12.info({ provider: provider.name, model: provider.model }, "Generating suite from story");
5096
+ logger16.info({ provider: provider.name, model: provider.model }, "Generating suite from story");
4239
5097
  const generated = await provider.generateTestSuite(story);
4240
5098
  const suiteName = options.suiteName ?? generated.name;
4241
5099
  const { suiteDir, suiteId } = await createSuite(options.outputDir, {
@@ -4244,7 +5102,7 @@ async function generateSuiteFromStory(story, options) {
4244
5102
  tags: generated.tags,
4245
5103
  type: options.type ?? "ui"
4246
5104
  });
4247
- logger12.info({ suiteDir, suiteId, cases: generated.cases.length }, "Suite directory created");
5105
+ logger16.info({ suiteDir, suiteId, cases: generated.cases.length }, "Suite directory created");
4248
5106
  for (const genCase of generated.cases) {
4249
5107
  await createCase(suiteDir, {
4250
5108
  name: genCase.name,
@@ -4262,11 +5120,20 @@ async function generateSuiteFromStory(story, options) {
4262
5120
  }))
4263
5121
  });
4264
5122
  }
4265
- logger12.info({ suiteId, cases: generated.cases.length }, "Auto-generating code for all steps");
5123
+ logger16.info({ suiteId, cases: generated.cases.length }, "Auto-generating code for all steps");
5124
+ let mcpSession;
4266
5125
  try {
4267
5126
  const config = await readConfig(options.rootDir);
4268
5127
  const variables = await resolveVariables(options.rootDir).catch(() => ({}));
4269
- const router = createSmartRouter(options.rootDir);
5128
+ if (config.mcp?.enabled) {
5129
+ mcpSession = new McpSession({
5130
+ headless: config.mcp.headless,
5131
+ actionTimeout: config.mcp.actionTimeout,
5132
+ idleTimeout: config.mcp.idleTimeout
5133
+ });
5134
+ logger16.info({ headless: config.mcp.headless }, "MCP session created for suite generation");
5135
+ }
5136
+ const router = createSmartRouter(options.rootDir, mcpSession, config.mcp?.actThenScript);
4270
5137
  const casePaths = await listCasePaths(suiteDir);
4271
5138
  for (const casePath of casePaths) {
4272
5139
  const tc = await readCase(casePath);
@@ -4296,18 +5163,23 @@ async function generateSuiteFromStory(story, options) {
4296
5163
  });
4297
5164
  if (codeGenCount > 0) {
4298
5165
  await updateCase(casePath, { steps: updatedSteps });
4299
- logger12.info({ caseName: tc.name, generated: codeGenCount, total: tc.steps.length }, "Code generated for case");
5166
+ logger16.info({ caseName: tc.name, generated: codeGenCount, total: tc.steps.length }, "Code generated for case");
4300
5167
  }
4301
5168
  }
4302
5169
  await router.getCache().persist();
4303
- logger12.info({ suiteId }, "Auto code generation complete");
5170
+ logger16.info({ suiteId }, "Auto code generation complete");
4304
5171
  } catch (codeGenErr) {
4305
- logger12.warn({ err: codeGenErr, suiteId }, "Auto code generation failed \u2014 steps saved without code");
5172
+ logger16.warn({ err: codeGenErr, suiteId }, "Auto code generation failed \u2014 steps saved without code");
5173
+ } finally {
5174
+ if (mcpSession) {
5175
+ mcpSession.release();
5176
+ logger16.debug("MCP session released after suite generation");
5177
+ }
4306
5178
  }
4307
5179
  const reportPath = import_path12.default.join(options.rootDir, "results", `generated-suite-${toSlug(suiteName)}.json`);
4308
5180
  await import_fs_extra10.default.ensureDir(import_path12.default.dirname(reportPath));
4309
5181
  await import_fs_extra10.default.writeJson(reportPath, { story, generated, suiteDir, suiteId }, { spaces: 2 });
4310
- logger12.info({ reportPath, suiteId }, "Generation complete");
5182
+ logger16.info({ reportPath, suiteId }, "Generation complete");
4311
5183
  return { suiteId, suiteName };
4312
5184
  }
4313
5185
  async function generateSuitesFromStories(stories, options, ws, concurrency = 3) {
@@ -4318,7 +5190,7 @@ async function generateSuitesFromStories(stories, options, ws, concurrency = 3)
4318
5190
  let failed = 0;
4319
5191
  const failedItems = [];
4320
5192
  ws?.broadcast("story:bulk-start", { jobId, total });
4321
- logger12.info({ jobId, total, concurrency }, "Bulk story generation started");
5193
+ logger16.info({ jobId, total, concurrency }, "Bulk story generation started");
4322
5194
  let active = 0;
4323
5195
  const queue = [...stories.entries()];
4324
5196
  const results = [];
@@ -4342,12 +5214,12 @@ async function generateSuitesFromStories(stories, options, ws, concurrency = 3)
4342
5214
  suiteName: item.suiteName
4343
5215
  });
4344
5216
  completed++;
4345
- logger12.info({ jobId, index, completed, total }, "Story processed");
5217
+ logger16.info({ jobId, index, completed, total }, "Story processed");
4346
5218
  } catch (err) {
4347
5219
  failed++;
4348
5220
  const error = err instanceof Error ? err.message : String(err);
4349
5221
  failedItems.push({ index, story: item.story.slice(0, 120), error });
4350
- logger12.warn({ jobId, index, error }, "Story generation failed \u2014 continuing batch");
5222
+ logger16.warn({ jobId, index, error }, "Story generation failed \u2014 continuing batch");
4351
5223
  }
4352
5224
  ws?.broadcast("story:bulk-progress", {
4353
5225
  jobId,
@@ -4368,10 +5240,10 @@ async function generateSuitesFromStories(stories, options, ws, concurrency = 3)
4368
5240
  await Promise.all(results);
4369
5241
  const doneEvent = { jobId, total, completed, failed, failedItems };
4370
5242
  ws?.broadcast("story:bulk-done", doneEvent);
4371
- logger12.info({ jobId, completed, failed, total }, "Bulk story generation finished");
5243
+ logger16.info({ jobId, completed, failed, total }, "Bulk story generation finished");
4372
5244
  return doneEvent;
4373
5245
  }
4374
- var import_path12, import_fs_extra10, logger12;
5246
+ var import_path12, import_fs_extra10, logger16;
4375
5247
  var init_router = __esm({
4376
5248
  "src/ai/router.ts"() {
4377
5249
  "use strict";
@@ -4383,13 +5255,14 @@ var init_router = __esm({
4383
5255
  init_logger();
4384
5256
  init_smart_router();
4385
5257
  init_code_cache();
5258
+ init_mcp_session();
4386
5259
  init_case_store();
4387
5260
  init_suite_store();
4388
5261
  init_config_store();
4389
5262
  init_variable_store();
4390
5263
  init_sanitize();
4391
5264
  init_step_type_detector();
4392
- logger12 = createChildLogger("router");
5265
+ logger16 = createChildLogger("router");
4393
5266
  }
4394
5267
  });
4395
5268
 
@@ -4443,6 +5316,7 @@ async function stepRoutes(fastify) {
4443
5316
  }
4444
5317
  });
4445
5318
  fastify.post("/suites/:suiteId/cases/:caseId/steps/:stepId/generate", async (req, reply) => {
5319
+ let mcpSession;
4446
5320
  try {
4447
5321
  const caseDir = await resolveCasePath(req.params.suiteId, req.params.caseId);
4448
5322
  if (!caseDir) return reply.status(404).send({ error: `Case "${req.params.caseId}" not found` });
@@ -4452,10 +5326,16 @@ async function stepRoutes(fastify) {
4452
5326
  return reply.status(404).send({ error: `Step "${req.params.stepId}" not found` });
4453
5327
  }
4454
5328
  const variables = await resolveVariables(rootDir).catch(() => ({}));
4455
- const router = createSmartRouter(rootDir);
4456
- const config = await Promise.resolve().then(() => (init_config_store(), config_store_exports)).then(
4457
- (m) => m.readConfig(rootDir)
4458
- );
5329
+ const config = await readConfig(rootDir);
5330
+ const useMcp = req.body?.useMcp ?? config.mcp?.enabled;
5331
+ if (useMcp) {
5332
+ mcpSession = new McpSession({
5333
+ headless: config.mcp?.headless ?? true,
5334
+ actionTimeout: config.mcp?.actionTimeout ?? 15e3,
5335
+ idleTimeout: config.mcp?.idleTimeout ?? 3e4
5336
+ });
5337
+ }
5338
+ const router = createSmartRouter(rootDir, mcpSession, useMcp ? config.mcp?.actThenScript : false);
4459
5339
  const result = await router.generate(step.instruction, {
4460
5340
  url: config.baseUrl,
4461
5341
  title: "",
@@ -4469,12 +5349,15 @@ async function stepRoutes(fastify) {
4469
5349
  );
4470
5350
  await router.getCache().persist();
4471
5351
  const updated = await updateCase(caseDir, { steps: updatedSteps });
4472
- return reply.send({ step: updated.steps.find((s) => s.id === step.id), strategy: result.strategy, cost: result.cost });
5352
+ return reply.send({ step: updated.steps.find((s) => s.id === step.id), strategy: result.strategy, cost: result.cost, mcpUsed: !!mcpSession });
4473
5353
  } catch (err) {
4474
5354
  return sendError(reply, 500, "Failed to generate code for step", err);
5355
+ } finally {
5356
+ mcpSession?.release();
4475
5357
  }
4476
5358
  });
4477
5359
  fastify.post("/suites/:suiteId/cases/:caseId/steps/generate-all", async (req, reply) => {
5360
+ let mcpSession;
4478
5361
  try {
4479
5362
  const caseDir = await resolveCasePath(req.params.suiteId, req.params.caseId);
4480
5363
  if (!caseDir) return reply.status(404).send({ error: `Case "${req.params.caseId}" not found` });
@@ -4484,10 +5367,16 @@ async function stepRoutes(fastify) {
4484
5367
  return reply.send({ message: "All steps already have code", generated: 0 });
4485
5368
  }
4486
5369
  const variables = await resolveVariables(rootDir).catch(() => ({}));
4487
- const config = await Promise.resolve().then(() => (init_config_store(), config_store_exports)).then(
4488
- (m) => m.readConfig(rootDir)
4489
- );
4490
- const router = createSmartRouter(rootDir);
5370
+ const config = await readConfig(rootDir);
5371
+ const useMcp = req.body?.useMcp ?? config.mcp?.enabled;
5372
+ if (useMcp) {
5373
+ mcpSession = new McpSession({
5374
+ headless: config.mcp?.headless ?? true,
5375
+ actionTimeout: config.mcp?.actionTimeout ?? 15e3,
5376
+ idleTimeout: config.mcp?.idleTimeout ?? 3e4
5377
+ });
5378
+ }
5379
+ const router = createSmartRouter(rootDir, mcpSession, useMcp ? config.mcp?.actThenScript : false);
4491
5380
  const batchSteps = emptySteps.map((s) => ({
4492
5381
  instruction: s.instruction,
4493
5382
  context: {
@@ -4519,9 +5408,11 @@ async function stepRoutes(fastify) {
4519
5408
  });
4520
5409
  }
4521
5410
  const updated = await updateCase(caseDir, { steps: updatedSteps });
4522
- return reply.send({ generated: actuallyGenerated, case: updated });
5411
+ return reply.send({ generated: actuallyGenerated, case: updated, mcpUsed: !!mcpSession });
4523
5412
  } catch (err) {
4524
5413
  return sendError(reply, 500, "Failed to generate code for steps", err);
5414
+ } finally {
5415
+ mcpSession?.release();
4525
5416
  }
4526
5417
  });
4527
5418
  fastify.patch("/suites/:suiteId/cases/:caseId/steps/reorder", async (req, reply) => {
@@ -4588,7 +5479,9 @@ var init_steps = __esm({
4588
5479
  init_case_store();
4589
5480
  init_suite_store();
4590
5481
  init_variable_store();
5482
+ init_config_store();
4591
5483
  init_router();
5484
+ init_mcp_session();
4592
5485
  init_utils2();
4593
5486
  AddStepBody = import_zod8.z.object({
4594
5487
  instruction: import_zod8.z.string().min(1),
@@ -4621,7 +5514,7 @@ async function readHooks(suiteDir) {
4621
5514
  const raw = await readJson(filePath);
4622
5515
  const result = SuiteHooksSchema.safeParse(raw);
4623
5516
  if (!result.success) {
4624
- logger13.warn({ path: filePath, errors: result.error.issues }, "Invalid hooks.json \u2014 returning defaults");
5517
+ logger17.warn({ path: filePath, errors: result.error.issues }, "Invalid hooks.json \u2014 returning defaults");
4625
5518
  return { ...EMPTY_HOOKS };
4626
5519
  }
4627
5520
  return result.data;
@@ -4640,7 +5533,7 @@ ${issues}`,
4640
5533
  );
4641
5534
  }
4642
5535
  await atomicWriteJson(filePath, result.data);
4643
- logger13.debug({ path: filePath }, "Hooks written");
5536
+ logger17.debug({ path: filePath }, "Hooks written");
4644
5537
  }
4645
5538
  async function addHookStep(suiteDir, hookType, instruction, order) {
4646
5539
  const hooks = await readHooks(suiteDir);
@@ -4690,7 +5583,7 @@ async function updateHookStep(suiteDir, hookType, stepId, patch) {
4690
5583
  await writeHooks(suiteDir, hooks);
4691
5584
  return hooks;
4692
5585
  }
4693
- var import_path14, import_fs_extra11, import_crypto3, logger13, HOOKS_FILE, EMPTY_HOOKS;
5586
+ var import_path14, import_fs_extra11, import_crypto3, logger17, HOOKS_FILE, EMPTY_HOOKS;
4694
5587
  var init_hooks_store = __esm({
4695
5588
  "src/storage/hooks-store.ts"() {
4696
5589
  "use strict";
@@ -4702,7 +5595,7 @@ var init_hooks_store = __esm({
4702
5595
  init_errors();
4703
5596
  init_logger();
4704
5597
  init_utils();
4705
- logger13 = createChildLogger("hooks-store");
5598
+ logger17 = createChildLogger("hooks-store");
4706
5599
  HOOKS_FILE = "hooks.json";
4707
5600
  EMPTY_HOOKS = {
4708
5601
  before_all: [],
@@ -5041,7 +5934,7 @@ ${issues}`,
5041
5934
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5042
5935
  const filePath = reportFilePath(healingDir, report.runId);
5043
5936
  await atomicWriteJson(filePath, schema.data);
5044
- logger14.info({ runId: report.runId, totalHeals: report.totalHeals }, "Healing report saved");
5937
+ logger18.info({ runId: report.runId, totalHeals: report.totalHeals }, "Healing report saved");
5045
5938
  }
5046
5939
  async function readHealingReport(rootDir, runId) {
5047
5940
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
@@ -5073,7 +5966,7 @@ async function readPendingEvents(rootDir) {
5073
5966
  const raw = await readJson(filePath);
5074
5967
  const result = HealingEventSchema.array().safeParse(raw);
5075
5968
  if (!result.success) {
5076
- logger14.warn({ path: filePath }, "Pending healing file is malformed \u2014 resetting");
5969
+ logger18.warn({ path: filePath }, "Pending healing file is malformed \u2014 resetting");
5077
5970
  return [];
5078
5971
  }
5079
5972
  return result.data;
@@ -5088,7 +5981,7 @@ function pruneToLimit(events) {
5088
5981
  });
5089
5982
  const excess = events.length - MAX_HEALING_EVENTS;
5090
5983
  const toDelete = new Set(byPriority.slice(0, excess).map((e) => e.id));
5091
- logger14.info(
5984
+ logger18.info(
5092
5985
  { excess, deleted: toDelete.size },
5093
5986
  "Auto-pruning healing events to enforce cap"
5094
5987
  );
@@ -5126,7 +6019,7 @@ async function acceptHealingEvent(rootDir, eventId) {
5126
6019
  };
5127
6020
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5128
6021
  await atomicWriteJson(pendingFilePath(healingDir), pending);
5129
- logger14.info({ eventId }, "Healing event accepted");
6022
+ logger18.info({ eventId }, "Healing event accepted");
5130
6023
  return pending[idx];
5131
6024
  }
5132
6025
  async function rejectHealingEvent(rootDir, eventId) {
@@ -5146,7 +6039,7 @@ async function rejectHealingEvent(rootDir, eventId) {
5146
6039
  };
5147
6040
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5148
6041
  await atomicWriteJson(pendingFilePath(healingDir), pending);
5149
- logger14.info({ eventId }, "Healing event rejected");
6042
+ logger18.info({ eventId }, "Healing event rejected");
5150
6043
  return pending[idx];
5151
6044
  }
5152
6045
  async function deleteHealingEvent(rootDir, eventId) {
@@ -5154,7 +6047,7 @@ async function deleteHealingEvent(rootDir, eventId) {
5154
6047
  const filtered = events.filter((e) => e.id !== eventId);
5155
6048
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5156
6049
  await atomicWriteJson(pendingFilePath(healingDir), filtered);
5157
- logger14.info({ eventId }, "Healing event deleted");
6050
+ logger18.info({ eventId }, "Healing event deleted");
5158
6051
  }
5159
6052
  async function deleteHealingEvents(rootDir, eventIds) {
5160
6053
  const events = await readPendingEvents(rootDir);
@@ -5163,7 +6056,7 @@ async function deleteHealingEvents(rootDir, eventIds) {
5163
6056
  const deleted = events.length - filtered.length;
5164
6057
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5165
6058
  await atomicWriteJson(pendingFilePath(healingDir), filtered);
5166
- logger14.info({ count: deleted }, "Healing events bulk-deleted");
6059
+ logger18.info({ count: deleted }, "Healing events bulk-deleted");
5167
6060
  return deleted;
5168
6061
  }
5169
6062
  async function clearAllHealingEvents(rootDir) {
@@ -5171,7 +6064,7 @@ async function clearAllHealingEvents(rootDir) {
5171
6064
  const count = events.length;
5172
6065
  const healingDir = import_path16.default.join(rootDir, HEALING_DIR);
5173
6066
  await atomicWriteJson(pendingFilePath(healingDir), []);
5174
- logger14.info({ count }, "All healing events cleared");
6067
+ logger18.info({ count }, "All healing events cleared");
5175
6068
  return count;
5176
6069
  }
5177
6070
  async function getHealingStats(rootDir) {
@@ -5194,7 +6087,7 @@ async function listHealingReportIds(rootDir) {
5194
6087
  );
5195
6088
  return withStats.sort((a, b) => b.mtime - a.mtime).map((x) => x.id);
5196
6089
  }
5197
- var import_path16, import_fs_extra12, logger14, HEALING_DIR, PENDING_FILE, REPORT_PREFIX, MAX_HEALING_EVENTS, PRUNE_PRIORITY;
6090
+ var import_path16, import_fs_extra12, logger18, HEALING_DIR, PENDING_FILE, REPORT_PREFIX, MAX_HEALING_EVENTS, PRUNE_PRIORITY;
5198
6091
  var init_healing_store = __esm({
5199
6092
  "src/storage/healing-store.ts"() {
5200
6093
  "use strict";
@@ -5205,7 +6098,7 @@ var init_healing_store = __esm({
5205
6098
  init_errors();
5206
6099
  init_logger();
5207
6100
  init_utils();
5208
- logger14 = createChildLogger("healing-store");
6101
+ logger18 = createChildLogger("healing-store");
5209
6102
  HEALING_DIR = import_path16.default.join("results", "healing");
5210
6103
  PENDING_FILE = "pending.json";
5211
6104
  REPORT_PREFIX = "healing-report-";
@@ -5251,7 +6144,7 @@ async function healingRoutes(fastify) {
5251
6144
  try {
5252
6145
  const event = await acceptHealingEvent(rootDir, req.params.id);
5253
6146
  await applyHealToCase(testsDir, event.caseId, event.stepId, event.healedCode);
5254
- logger15.info({ id: req.params.id, caseId: event.caseId, stepId: event.stepId }, "Healing accepted and applied");
6147
+ logger19.info({ id: req.params.id, caseId: event.caseId, stepId: event.stepId }, "Healing accepted and applied");
5255
6148
  return reply.send(event);
5256
6149
  } catch (err) {
5257
6150
  return sendError(reply, 500, "Failed to accept healing event", err);
@@ -5274,7 +6167,7 @@ async function healingRoutes(fastify) {
5274
6167
  await acceptHealingEvent(rootDir, event.id);
5275
6168
  await applyHealToCase(testsDir, event.caseId, event.stepId, event.healedCode);
5276
6169
  results.push({ id: event.id, status: "accepted" });
5277
- logger15.info({ id: event.id }, "Bulk healing accepted");
6170
+ logger19.info({ id: event.id }, "Bulk healing accepted");
5278
6171
  } catch (err) {
5279
6172
  results.push({ id: event.id, status: "failed", error: err instanceof Error ? err.message : String(err) });
5280
6173
  }
@@ -5363,7 +6256,7 @@ async function findCasePath(testsDir, caseId) {
5363
6256
  }
5364
6257
  return null;
5365
6258
  }
5366
- var import_path17, import_zod12, logger15, ApplyFromReportBody, DeleteBulkBody;
6259
+ var import_path17, import_zod12, logger19, ApplyFromReportBody, DeleteBulkBody;
5367
6260
  var init_healing2 = __esm({
5368
6261
  "src/server/routes/healing.ts"() {
5369
6262
  "use strict";
@@ -5374,7 +6267,7 @@ var init_healing2 = __esm({
5374
6267
  init_case_store();
5375
6268
  init_utils2();
5376
6269
  init_logger();
5377
- logger15 = createChildLogger("routes/healing");
6270
+ logger19 = createChildLogger("routes/healing");
5378
6271
  ApplyFromReportBody = import_zod12.z.object({
5379
6272
  reportPath: import_zod12.z.string().min(1)
5380
6273
  });
@@ -5526,7 +6419,7 @@ ${issues}`,
5526
6419
  }
5527
6420
  const filePath = runFilePath(import_path18.default.join(rootDir, RESULTS_DIR), result.runId);
5528
6421
  await atomicWriteJson(filePath, schema.data);
5529
- logger16.info({ runId: result.runId, status: result.status }, "Run result saved");
6422
+ logger20.info({ runId: result.runId, status: result.status }, "Run result saved");
5530
6423
  }
5531
6424
  async function readResult(rootDir, runId) {
5532
6425
  const filePath = runFilePath(import_path18.default.join(rootDir, RESULTS_DIR), runId);
@@ -5583,7 +6476,7 @@ async function listResults(rootDir, limit = 20) {
5583
6476
  suiteIds: result.suites.map((s) => s.suiteId)
5584
6477
  });
5585
6478
  } catch (err) {
5586
- logger16.warn({ runId: id, err }, "Failed to read run result \u2014 skipping");
6479
+ logger20.warn({ runId: id, err }, "Failed to read run result \u2014 skipping");
5587
6480
  }
5588
6481
  }
5589
6482
  return results;
@@ -5598,9 +6491,9 @@ async function deleteResult(rootDir, runId) {
5598
6491
  );
5599
6492
  }
5600
6493
  await import_fs_extra13.default.remove(filePath);
5601
- logger16.info({ runId }, "Run result deleted");
6494
+ logger20.info({ runId }, "Run result deleted");
5602
6495
  }
5603
- var import_path18, import_fs_extra13, logger16, RESULTS_DIR, RUNS_DIR;
6496
+ var import_path18, import_fs_extra13, logger20, RESULTS_DIR, RUNS_DIR;
5604
6497
  var init_result_store = __esm({
5605
6498
  "src/storage/result-store.ts"() {
5606
6499
  "use strict";
@@ -5611,21 +6504,185 @@ var init_result_store = __esm({
5611
6504
  init_errors();
5612
6505
  init_logger();
5613
6506
  init_utils();
5614
- logger16 = createChildLogger("result-store");
6507
+ logger20 = createChildLogger("result-store");
5615
6508
  RESULTS_DIR = "results";
5616
6509
  RUNS_DIR = "runs";
5617
6510
  }
5618
6511
  });
5619
6512
 
6513
+ // src/mcp/proactive-checker.ts
6514
+ function extractSelectors(code) {
6515
+ const results = [];
6516
+ for (const pattern of SELECTOR_PATTERNS) {
6517
+ pattern.regex.lastIndex = 0;
6518
+ let match;
6519
+ while ((match = pattern.regex.exec(code)) !== null) {
6520
+ if (pattern.type === "getByRole") {
6521
+ results.push({
6522
+ selector: match[0],
6523
+ type: pattern.type,
6524
+ role: match[1],
6525
+ name: match[2]
6526
+ });
6527
+ } else {
6528
+ results.push({
6529
+ selector: match[0],
6530
+ type: pattern.type,
6531
+ name: match[1]
6532
+ });
6533
+ }
6534
+ }
6535
+ }
6536
+ return results;
6537
+ }
6538
+ function fuzzyMatch(targetName, snapshotElements, targetRole) {
6539
+ const lowerTarget = targetName.toLowerCase();
6540
+ for (const el of snapshotElements) {
6541
+ const nameMatch = el.match(/"([^"]+)"/);
6542
+ if (nameMatch && nameMatch[1].toLowerCase() === lowerTarget) {
6543
+ if (!targetRole || el.toLowerCase().startsWith(targetRole.toLowerCase())) {
6544
+ return el;
6545
+ }
6546
+ }
6547
+ }
6548
+ for (const el of snapshotElements) {
6549
+ const nameMatch = el.match(/"([^"]+)"/);
6550
+ if (nameMatch) {
6551
+ const elName = nameMatch[1].toLowerCase();
6552
+ if (elName.includes(lowerTarget) || lowerTarget.includes(elName)) {
6553
+ if (!targetRole || el.toLowerCase().startsWith(targetRole.toLowerCase())) {
6554
+ return el;
6555
+ }
6556
+ }
6557
+ }
6558
+ }
6559
+ if (targetRole) {
6560
+ const sameRole = snapshotElements.filter(
6561
+ (el) => el.toLowerCase().startsWith(targetRole.toLowerCase())
6562
+ );
6563
+ if (sameRole.length > 0) {
6564
+ let bestScore = 0;
6565
+ let bestMatch;
6566
+ for (const el of sameRole) {
6567
+ const nameMatch = el.match(/"([^"]+)"/);
6568
+ if (nameMatch) {
6569
+ const score = similarity(lowerTarget, nameMatch[1].toLowerCase());
6570
+ if (score > bestScore) {
6571
+ bestScore = score;
6572
+ bestMatch = el;
6573
+ }
6574
+ }
6575
+ }
6576
+ if (bestScore > 0.3) return bestMatch;
6577
+ }
6578
+ }
6579
+ return void 0;
6580
+ }
6581
+ function similarity(a, b) {
6582
+ if (a === b) return 1;
6583
+ const longer = a.length > b.length ? a : b;
6584
+ const shorter = a.length > b.length ? b : a;
6585
+ if (longer.length === 0) return 1;
6586
+ let matches = 0;
6587
+ for (let i = 0; i < shorter.length; i++) {
6588
+ if (longer.includes(shorter[i])) matches++;
6589
+ }
6590
+ return matches / longer.length;
6591
+ }
6592
+ async function checkSelectors(mcpSession, url, code) {
6593
+ const selectors = extractSelectors(code);
6594
+ if (selectors.length === 0) {
6595
+ return { url, ok: true, total: 0, matched: 0, missing: 0, checks: [] };
6596
+ }
6597
+ await mcpSession.navigate(url);
6598
+ const raw = await mcpSession.snapshot();
6599
+ if (!raw) {
6600
+ logger21.warn({ url }, "Proactive check: snapshot returned null \u2014 skipping validation");
6601
+ return { url, ok: true, total: selectors.length, matched: 0, missing: 0, checks: [] };
6602
+ }
6603
+ const parsed = parseSnapshot(raw);
6604
+ const snapshotElements = parsed.interactiveElements.split("\n").filter(Boolean);
6605
+ const checks = [];
6606
+ for (const sel of selectors) {
6607
+ const targetName = sel.name ?? "";
6608
+ let found = false;
6609
+ for (const el of snapshotElements) {
6610
+ const nameMatch = el.match(/"([^"]+)"/);
6611
+ if (nameMatch) {
6612
+ const elName = nameMatch[1].toLowerCase();
6613
+ if (elName === targetName.toLowerCase()) {
6614
+ if (sel.role) {
6615
+ if (el.toLowerCase().startsWith(sel.role.toLowerCase())) {
6616
+ found = true;
6617
+ break;
6618
+ }
6619
+ } else {
6620
+ found = true;
6621
+ break;
6622
+ }
6623
+ }
6624
+ }
6625
+ }
6626
+ const check2 = {
6627
+ selector: sel.selector,
6628
+ type: sel.type,
6629
+ found
6630
+ };
6631
+ if (!found) {
6632
+ const suggestion = fuzzyMatch(targetName, snapshotElements, sel.role);
6633
+ if (suggestion) {
6634
+ check2.suggestion = suggestion;
6635
+ }
6636
+ }
6637
+ checks.push(check2);
6638
+ }
6639
+ const matched = checks.filter((c) => c.found).length;
6640
+ const missing = checks.filter((c) => !c.found).length;
6641
+ logger21.info(
6642
+ { url, total: checks.length, matched, missing },
6643
+ "Proactive selector check complete"
6644
+ );
6645
+ return {
6646
+ url,
6647
+ ok: missing === 0,
6648
+ total: checks.length,
6649
+ matched,
6650
+ missing,
6651
+ checks
6652
+ };
6653
+ }
6654
+ var logger21, SELECTOR_PATTERNS;
6655
+ var init_proactive_checker = __esm({
6656
+ "src/mcp/proactive-checker.ts"() {
6657
+ "use strict";
6658
+ init_cjs_shims();
6659
+ init_snapshot_parser();
6660
+ init_logger();
6661
+ logger21 = createChildLogger("proactive-checker");
6662
+ SELECTOR_PATTERNS = [
6663
+ // getByRole('button', { name: 'Login' })
6664
+ { regex: /getByRole\(\s*['"](\w+)['"]\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"]/g, type: "getByRole" },
6665
+ // getByLabel('Username')
6666
+ { regex: /getByLabel\(\s*['"]([^'"]+)['"]/g, type: "getByLabel" },
6667
+ // getByPlaceholder('Enter email')
6668
+ { regex: /getByPlaceholder\(\s*['"]([^'"]+)['"]/g, type: "getByPlaceholder" },
6669
+ // getByText('Welcome')
6670
+ { regex: /getByText\(\s*['"]([^'"]+)['"]/g, type: "getByText" },
6671
+ // getByTestId('submit-btn')
6672
+ { regex: /getByTestId\(\s*['"]([^'"]+)['"]/g, type: "getByTestId" }
6673
+ ];
6674
+ }
6675
+ });
6676
+
5620
6677
  // src/engine/browser-manager.ts
5621
- var import_playwright, logger17, BrowserManager;
6678
+ var import_playwright, logger22, BrowserManager;
5622
6679
  var init_browser_manager = __esm({
5623
6680
  "src/engine/browser-manager.ts"() {
5624
6681
  "use strict";
5625
6682
  init_cjs_shims();
5626
6683
  import_playwright = require("playwright");
5627
6684
  init_logger();
5628
- logger17 = createChildLogger("browser-manager");
6685
+ logger22 = createChildLogger("browser-manager");
5629
6686
  BrowserManager = class {
5630
6687
  browsers = /* @__PURE__ */ new Map();
5631
6688
  // ─── Launch ───────────────────────────────────────────────────────────────
@@ -5635,14 +6692,14 @@ var init_browser_manager = __esm({
5635
6692
  const launcher = browserName === "firefox" ? import_playwright.firefox : browserName === "webkit" ? import_playwright.webkit : import_playwright.chromium;
5636
6693
  const browser = await launcher.launch({ headless: config.headless });
5637
6694
  this.browsers.set(browserName, browser);
5638
- logger17.info({ browser: browserName, headless: config.headless }, "Browser launched");
6695
+ logger22.info({ browser: browserName, headless: config.headless }, "Browser launched");
5639
6696
  return browser;
5640
6697
  }
5641
6698
  // ─── Context ──────────────────────────────────────────────────────────────
5642
6699
  async newContext(browser, config, videosDir) {
5643
6700
  const deviceDescriptor = config.device && import_playwright.devices[config.device] ? import_playwright.devices[config.device] : null;
5644
6701
  if (config.device && !deviceDescriptor) {
5645
- logger17.warn({ device: config.device }, "Unknown Playwright device \u2014 falling back to configured viewport");
6702
+ logger22.warn({ device: config.device }, "Unknown Playwright device \u2014 falling back to configured viewport");
5646
6703
  }
5647
6704
  const contextOptions = {
5648
6705
  // Do NOT set baseURL here — it causes Playwright to pre-navigate the page
@@ -5755,7 +6812,7 @@ var init_browser_manager = __esm({
5755
6812
  close: async () => {
5756
6813
  }
5757
6814
  };
5758
- logger17.info("Created API-only request context (zero browser)");
6815
+ logger22.info("Created API-only request context (zero browser)");
5759
6816
  return fakePage;
5760
6817
  }
5761
6818
  /** Closes all API request contexts. */
@@ -5780,11 +6837,11 @@ var init_browser_manager = __esm({
5780
6837
  await context.tracing.stop();
5781
6838
  }
5782
6839
  } catch (err) {
5783
- logger17.debug({ err }, "Tracing stop failed \u2014 ignoring");
6840
+ logger22.debug({ err }, "Tracing stop failed \u2014 ignoring");
5784
6841
  }
5785
6842
  }
5786
6843
  await context.close().catch((err) => {
5787
- logger17.debug({ err }, "Context close failed \u2014 ignoring");
6844
+ logger22.debug({ err }, "Context close failed \u2014 ignoring");
5788
6845
  });
5789
6846
  }
5790
6847
  /** Closes all open browsers and API contexts. Called at the end of every run. */
@@ -5793,9 +6850,9 @@ var init_browser_manager = __esm({
5793
6850
  for (const [name, browser] of this.browsers) {
5794
6851
  try {
5795
6852
  await browser.close();
5796
- logger17.debug({ browser: name }, "Browser closed");
6853
+ logger22.debug({ browser: name }, "Browser closed");
5797
6854
  } catch (err) {
5798
- logger17.warn({ err, browser: name }, "Error closing browser");
6855
+ logger22.warn({ err, browser: name }, "Error closing browser");
5799
6856
  }
5800
6857
  }
5801
6858
  this.browsers.clear();
@@ -5808,13 +6865,13 @@ var init_browser_manager = __esm({
5808
6865
  });
5809
6866
 
5810
6867
  // src/engine/healing-budget.ts
5811
- var logger18, ENVIRONMENTAL_PATTERNS, HealingBudget;
6868
+ var logger23, ENVIRONMENTAL_PATTERNS, HealingBudget;
5812
6869
  var init_healing_budget = __esm({
5813
6870
  "src/engine/healing-budget.ts"() {
5814
6871
  "use strict";
5815
6872
  init_cjs_shims();
5816
6873
  init_logger();
5817
- logger18 = createChildLogger("healing-budget");
6874
+ logger23 = createChildLogger("healing-budget");
5818
6875
  ENVIRONMENTAL_PATTERNS = [
5819
6876
  /timeout/i,
5820
6877
  /ECONNREFUSED/i,
@@ -5837,7 +6894,7 @@ var init_healing_budget = __esm({
5837
6894
  // ─── Main decision ────────────────────────────────────────────────────────
5838
6895
  shouldHeal(failure) {
5839
6896
  if (this.spent >= this.dailyBudget) {
5840
- logger18.warn(
6897
+ logger23.warn(
5841
6898
  { spent: this.spent, budget: this.dailyBudget },
5842
6899
  "Daily healing budget exhausted"
5843
6900
  );
@@ -5871,7 +6928,7 @@ var init_healing_budget = __esm({
5871
6928
  /** Call after each AI healing call with the estimated cost. */
5872
6929
  recordSpend(amountUsd) {
5873
6930
  this.spent += amountUsd;
5874
- logger18.debug({ spend: amountUsd, total: this.spent }, "Healing budget spend recorded");
6931
+ logger23.debug({ spend: amountUsd, total: this.spent }, "Healing budget spend recorded");
5875
6932
  }
5876
6933
  /** Call when a healing attempt itself fails (AI returned bad code, etc.). */
5877
6934
  recordHealFailure(stepId) {
@@ -5893,7 +6950,7 @@ var init_healing_budget = __esm({
5893
6950
  });
5894
6951
 
5895
6952
  // src/engine/healing-report.ts
5896
- var import_crypto4, logger19, HealingReporter;
6953
+ var import_crypto4, logger24, HealingReporter;
5897
6954
  var init_healing_report = __esm({
5898
6955
  "src/engine/healing-report.ts"() {
5899
6956
  "use strict";
@@ -5901,7 +6958,7 @@ var init_healing_report = __esm({
5901
6958
  import_crypto4 = require("crypto");
5902
6959
  init_healing_store();
5903
6960
  init_logger();
5904
- logger19 = createChildLogger("healing-report");
6961
+ logger24 = createChildLogger("healing-report");
5905
6962
  HealingReporter = class {
5906
6963
  events = [];
5907
6964
  /** Records a successful healing event (called by executor after each healed step). */
@@ -5923,7 +6980,7 @@ var init_healing_report = __esm({
5923
6980
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
5924
6981
  };
5925
6982
  this.events.push(event);
5926
- logger19.info(
6983
+ logger24.info(
5927
6984
  { stepId: input.stepId, strategy: input.strategy, level: input.level },
5928
6985
  "Healing event recorded"
5929
6986
  );
@@ -5949,15 +7006,15 @@ var init_healing_report = __esm({
5949
7006
  };
5950
7007
  try {
5951
7008
  await writeHealingReport(rootDir, report);
5952
- logger19.info({ runId, count: this.events.length }, "Healing report written");
7009
+ logger24.info({ runId, count: this.events.length }, "Healing report written");
5953
7010
  } catch (err) {
5954
- logger19.error({ err, runId }, "Failed to write healing report");
7011
+ logger24.error({ err, runId }, "Failed to write healing report");
5955
7012
  }
5956
7013
  for (const event of this.events) {
5957
7014
  try {
5958
7015
  await appendPendingEvent(rootDir, event);
5959
7016
  } catch (err) {
5960
- logger19.warn({ err, eventId: event.id }, "Failed to append pending healing event");
7017
+ logger24.warn({ err, eventId: event.id }, "Failed to append pending healing event");
5961
7018
  }
5962
7019
  }
5963
7020
  }
@@ -6149,7 +7206,7 @@ function buildCategoryHtml(caseName, pageLoads, category) {
6149
7206
  </body>
6150
7207
  </html>`;
6151
7208
  }
6152
- var import_path19, import_crypto5, import_fs_extra14, logger20, AllureReporter;
7209
+ var import_path19, import_crypto5, import_fs_extra14, logger25, AllureReporter;
6153
7210
  var init_allure_reporter = __esm({
6154
7211
  "src/engine/allure-reporter.ts"() {
6155
7212
  "use strict";
@@ -6158,7 +7215,7 @@ var init_allure_reporter = __esm({
6158
7215
  import_crypto5 = require("crypto");
6159
7216
  import_fs_extra14 = __toESM(require("fs-extra"));
6160
7217
  init_logger();
6161
- logger20 = createChildLogger("allure-reporter");
7218
+ logger25 = createChildLogger("allure-reporter");
6162
7219
  AllureReporter = class {
6163
7220
  resultsDir;
6164
7221
  environment;
@@ -6180,9 +7237,9 @@ var init_allure_reporter = __esm({
6180
7237
  logFileName = import_path19.default.basename(runResult.logFilePath);
6181
7238
  await this.copyAttachment(runResult.logFilePath).catch(() => {
6182
7239
  });
6183
- logger20.debug({ logFile: logFileName }, "Log file copied to Allure results");
7240
+ logger25.debug({ logFile: logFileName }, "Log file copied to Allure results");
6184
7241
  } catch {
6185
- logger20.debug("Failed to copy log file to Allure results");
7242
+ logger25.debug("Failed to copy log file to Allure results");
6186
7243
  }
6187
7244
  }
6188
7245
  let written = 0;
@@ -6192,7 +7249,7 @@ var init_allure_reporter = __esm({
6192
7249
  written++;
6193
7250
  }
6194
7251
  }
6195
- logger20.info({ dir: this.resultsDir, written }, "Allure results written");
7252
+ logger25.info({ dir: this.resultsDir, written }, "Allure results written");
6196
7253
  }
6197
7254
  // ─── Internal ─────────────────────────────────────────────────────────────
6198
7255
  async writeTestResult(suite, tc, logFileName) {
@@ -6311,12 +7368,12 @@ async function generateAllureReport(rootDir, runId) {
6311
7368
  const resultsDir = import_path20.default.join(rootDir, "results", "allure-results", runId);
6312
7369
  const reportDir = import_path20.default.join(rootDir, "results", "allure-report", runId);
6313
7370
  if (!await import_fs_extra15.default.pathExists(resultsDir)) {
6314
- logger21.debug({ resultsDir }, "No allure-results dir for run \u2014 skipping report generation");
7371
+ logger26.debug({ resultsDir }, "No allure-results dir for run \u2014 skipping report generation");
6315
7372
  return false;
6316
7373
  }
6317
7374
  const files = await import_fs_extra15.default.readdir(resultsDir);
6318
7375
  if (files.length === 0) {
6319
- logger21.debug({ runId }, "allure-results is empty \u2014 skipping report generation");
7376
+ logger26.debug({ runId }, "allure-results is empty \u2014 skipping report generation");
6320
7377
  return false;
6321
7378
  }
6322
7379
  try {
@@ -6325,21 +7382,21 @@ async function generateAllureReport(rootDir, runId) {
6325
7382
  ["generate", resultsDir, "--output", reportDir, "--clean", "--single-file"],
6326
7383
  isWindows ? { shell: true } : {}
6327
7384
  );
6328
- logger21.info({ reportDir, runId }, "Allure HTML report generated");
7385
+ logger26.info({ reportDir, runId }, "Allure HTML report generated");
6329
7386
  return true;
6330
7387
  } catch (err) {
6331
7388
  const msg = err instanceof Error ? err.message : String(err);
6332
7389
  if (msg.includes("java") || msg.includes("Java") || msg.includes("JAVA")) {
6333
- logger21.warn(
7390
+ logger26.warn(
6334
7391
  "Allure report generation skipped \u2014 Java is not installed. Install Java (https://adoptium.net) to enable Allure HTML reports."
6335
7392
  );
6336
7393
  } else {
6337
- logger21.warn({ err: msg, runId }, "Allure report generation failed");
7394
+ logger26.warn({ err: msg, runId }, "Allure report generation failed");
6338
7395
  }
6339
7396
  return false;
6340
7397
  }
6341
7398
  }
6342
- var import_path20, import_fs_extra15, import_child_process2, import_util, execFileAsync, logger21, isWindows, alluireBin;
7399
+ var import_path20, import_fs_extra15, import_child_process2, import_util, execFileAsync, logger26, isWindows, alluireBin;
6343
7400
  var init_allure_generator = __esm({
6344
7401
  "src/engine/allure-generator.ts"() {
6345
7402
  "use strict";
@@ -6350,7 +7407,7 @@ var init_allure_generator = __esm({
6350
7407
  import_util = require("util");
6351
7408
  init_logger();
6352
7409
  execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
6353
- logger21 = createChildLogger("allure-generator");
7410
+ logger26 = createChildLogger("allure-generator");
6354
7411
  isWindows = process.platform === "win32";
6355
7412
  alluireBin = import_path20.default.resolve(
6356
7413
  __dirname,
@@ -6638,7 +7695,7 @@ async function extractPageContext(page, previousSteps, variables) {
6638
7695
  }
6639
7696
  return lines.join("\n");
6640
7697
  },
6641
- { maxElements: MAX_ELEMENTS, maxTextLen: MAX_TEXT_LEN }
7698
+ { maxElements: MAX_ELEMENTS2, maxTextLen: MAX_TEXT_LEN2 }
6642
7699
  ).catch(() => "");
6643
7700
  const htmlSnapshot = await page.evaluate(
6644
7701
  ({ maxLen }) => document.body?.innerHTML?.slice(0, maxLen) ?? "",
@@ -6653,13 +7710,13 @@ async function extractPageContext(page, previousSteps, variables) {
6653
7710
  variables
6654
7711
  };
6655
7712
  }
6656
- var MAX_ELEMENTS, MAX_TEXT_LEN, MAX_HTML_LEN;
7713
+ var MAX_ELEMENTS2, MAX_TEXT_LEN2, MAX_HTML_LEN;
6657
7714
  var init_context_extractor = __esm({
6658
7715
  "src/engine/context-extractor.ts"() {
6659
7716
  "use strict";
6660
7717
  init_cjs_shims();
6661
- MAX_ELEMENTS = 120;
6662
- MAX_TEXT_LEN = 80;
7718
+ MAX_ELEMENTS2 = 120;
7719
+ MAX_TEXT_LEN2 = 80;
6663
7720
  MAX_HTML_LEN = 8e3;
6664
7721
  }
6665
7722
  });
@@ -6897,7 +7954,7 @@ function parseMultiStrategies(raw) {
6897
7954
  }
6898
7955
  return stripped.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && s.startsWith("await"));
6899
7956
  }
6900
- var logger22, SelfHealer;
7957
+ var logger27, SelfHealer;
6901
7958
  var init_self_healing = __esm({
6902
7959
  "src/engine/self-healing.ts"() {
6903
7960
  "use strict";
@@ -6909,7 +7966,7 @@ var init_self_healing = __esm({
6909
7966
  init_logger();
6910
7967
  init_code_runner();
6911
7968
  init_step_type_detector();
6912
- logger22 = createChildLogger("self-healing");
7969
+ logger27 = createChildLogger("self-healing");
6913
7970
  SelfHealer = class {
6914
7971
  constructor(provider, maxLevel) {
6915
7972
  this.provider = provider;
@@ -6924,7 +7981,7 @@ var init_self_healing = __esm({
6924
7981
  */
6925
7982
  async heal(step, failure, previousSteps, page, variables, baseUrl) {
6926
7983
  if (failure.failureKind === "assertion") {
6927
- logger22.info(
7984
+ logger27.info(
6928
7985
  { stepId: step.id },
6929
7986
  "Assertion failure \u2014 skipping all healing levels (possible application bug)"
6930
7987
  );
@@ -6933,10 +7990,10 @@ var init_self_healing = __esm({
6933
7990
  const isApi = step.stepType === "api" || detectStepType(step.instruction) === "api";
6934
7991
  for (let level = 1; level <= this.maxLevel; level++) {
6935
7992
  if (isApi && (level === 3 || level === 4)) {
6936
- logger22.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
7993
+ logger27.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
6937
7994
  continue;
6938
7995
  }
6939
- logger22.debug({ stepId: step.id, level, isApi }, `Attempting heal level ${level}`);
7996
+ logger27.debug({ stepId: step.id, level, isApi }, `Attempting heal level ${level}`);
6940
7997
  try {
6941
7998
  const result = await this.tryLevel(
6942
7999
  level,
@@ -6949,14 +8006,14 @@ var init_self_healing = __esm({
6949
8006
  isApi
6950
8007
  );
6951
8008
  if (result !== null) {
6952
- logger22.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
8009
+ logger27.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
6953
8010
  return result;
6954
8011
  }
6955
8012
  } catch (err) {
6956
- logger22.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
8013
+ logger27.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
6957
8014
  }
6958
8015
  }
6959
- logger22.warn({ stepId: step.id, maxLevel: this.maxLevel }, "All healing levels exhausted");
8016
+ logger27.warn({ stepId: step.id, maxLevel: this.maxLevel }, "All healing levels exhausted");
6960
8017
  return null;
6961
8018
  }
6962
8019
  // ─── Level dispatcher ─────────────────────────────────────────────────────
@@ -6991,7 +8048,7 @@ var init_self_healing = __esm({
6991
8048
  if (this.provider.supportsVision) {
6992
8049
  try {
6993
8050
  const som = await captureSetOfMarks(page);
6994
- logger22.debug({ stepId: step.id }, "Level 2: using SoM vision healing");
8051
+ logger27.debug({ stepId: step.id }, "Level 2: using SoM vision healing");
6995
8052
  const code2 = await this.provider.analyzeWithSoM(
6996
8053
  som.screenshot,
6997
8054
  som.elementMap,
@@ -7001,7 +8058,7 @@ var init_self_healing = __esm({
7001
8058
  await executeCode(sanitized2, page);
7002
8059
  return { code: sanitized2, level: 2, strategy: "regenerate" };
7003
8060
  } catch (visionErr) {
7004
- logger22.debug({ stepId: step.id, err: visionErr }, "SoM vision failed, falling back to text regeneration");
8061
+ logger27.debug({ stepId: step.id, err: visionErr }, "SoM vision failed, falling back to text regeneration");
7005
8062
  await cleanupSoMOverlay(page);
7006
8063
  }
7007
8064
  }
@@ -7057,12 +8114,12 @@ var init_self_healing = __esm({
7057
8114
  // Produces inspectable Playwright code (not pixel coordinates).
7058
8115
  async level4Visual(step, page, variables, baseUrl) {
7059
8116
  if (!this.provider.supportsVision) {
7060
- logger22.debug({ stepId: step.id }, "Skipping SoM visual healing \u2014 provider lacks vision");
8117
+ logger27.debug({ stepId: step.id }, "Skipping SoM visual healing \u2014 provider lacks vision");
7061
8118
  return null;
7062
8119
  }
7063
8120
  try {
7064
8121
  const som = await captureSetOfMarks(page);
7065
- logger22.debug(
8122
+ logger27.debug(
7066
8123
  { stepId: step.id, elementCount: som.elements.length },
7067
8124
  "Level 4: SoM screenshot captured"
7068
8125
  );
@@ -7076,7 +8133,7 @@ var init_self_healing = __esm({
7076
8133
  return { code: sanitized, level: 4, strategy: "visual" };
7077
8134
  } catch (err) {
7078
8135
  await cleanupSoMOverlay(page);
7079
- logger22.debug({ stepId: step.id, err }, "Level 4 SoM failed");
8136
+ logger27.debug({ stepId: step.id, err }, "Level 4 SoM failed");
7080
8137
  return null;
7081
8138
  }
7082
8139
  }
@@ -7101,7 +8158,7 @@ var init_self_healing = __esm({
7101
8158
  }
7102
8159
  // ─── Level 6: Manual (human in the loop) ─────────────────────────────────
7103
8160
  level6Manual(step, failure) {
7104
- logger22.warn(
8161
+ logger27.warn(
7105
8162
  { stepId: step.id, instruction: step.instruction, error: failure.error },
7106
8163
  "Level 6: manual intervention required \u2014 marking step for human review"
7107
8164
  );
@@ -7126,17 +8183,17 @@ async function executeStep(step, page, ctx) {
7126
8183
  const mergedVars = { ...ctx.variables, ...Object.fromEntries(ctx.sharedVariables) };
7127
8184
  const hasTokens = step.generatedCode.includes("{{");
7128
8185
  if (hasTokens) {
7129
- logger23.warn(
8186
+ logger28.warn(
7130
8187
  { stepId: step.id, variableKeys: Object.keys(mergedVars), variableCount: Object.keys(mergedVars).length },
7131
8188
  "Interpolating variables into step code"
7132
8189
  );
7133
8190
  }
7134
8191
  const code = interpolate(step.generatedCode, mergedVars, ctx.config.baseUrl);
7135
8192
  if (hasTokens) {
7136
- logger23.warn({ stepId: step.id, before: step.generatedCode, after: code }, "Variable interpolation result");
8193
+ logger28.warn({ stepId: step.id, before: step.generatedCode, after: code }, "Variable interpolation result");
7137
8194
  }
7138
8195
  if (!code.trim() && step.stepType !== "mock") {
7139
- logger23.warn({ stepId: step.id }, "Step has no generated code \u2014 skipping");
8196
+ logger28.warn({ stepId: step.id }, "Step has no generated code \u2014 skipping");
7140
8197
  return buildResult(step, "skipped", code, Date.now() - start);
7141
8198
  }
7142
8199
  if (step.stepType === "mock" && step.mockUrl && step.mockResponse) {
@@ -7161,7 +8218,7 @@ async function executeStep(step, page, ctx) {
7161
8218
  const runtimeCtx = {
7162
8219
  setVariable: (key, value) => {
7163
8220
  ctx.sharedVariables.set(key, value);
7164
- logger23.info({ stepId: step.id, key, value: value.slice(0, 50) }, "Runtime variable set");
8221
+ logger28.info({ stepId: step.id, key, value: value.slice(0, 50) }, "Runtime variable set");
7165
8222
  },
7166
8223
  getVariable: (key) => ctx.sharedVariables.get(key)
7167
8224
  };
@@ -7201,7 +8258,7 @@ async function executeStep(step, page, ctx) {
7201
8258
  } catch (err) {
7202
8259
  lastErr = err;
7203
8260
  if (attempt < stepRetries) {
7204
- logger23.debug({ stepId: step.id, attempt, stepRetries }, "Step failed \u2014 retrying");
8261
+ logger28.debug({ stepId: step.id, attempt, stepRetries }, "Step failed \u2014 retrying");
7205
8262
  continue;
7206
8263
  }
7207
8264
  }
@@ -7209,7 +8266,7 @@ async function executeStep(step, page, ctx) {
7209
8266
  const firstErr = lastErr;
7210
8267
  {
7211
8268
  const errorMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
7212
- logger23.debug({ stepId: step.id, error: errorMsg }, "Step failed on first attempt");
8269
+ logger28.debug({ stepId: step.id, error: errorMsg }, "Step failed on first attempt");
7213
8270
  const failAuditUrl = step.runAudit && step.stepType !== "api" ? (() => {
7214
8271
  try {
7215
8272
  return page.url() || void 0;
@@ -7223,7 +8280,7 @@ async function executeStep(step, page, ctx) {
7223
8280
  }
7224
8281
  const failureKind = classifyError(errorMsg);
7225
8282
  if (failureKind === "assertion") {
7226
- logger23.info(
8283
+ logger28.info(
7227
8284
  { stepId: step.id, error: errorMsg },
7228
8285
  "Assertion failure detected \u2014 NOT healing (possible application bug)"
7229
8286
  );
@@ -7232,7 +8289,7 @@ async function executeStep(step, page, ctx) {
7232
8289
  if (!ctx.config.healing.enabled) {
7233
8290
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
7234
8291
  }
7235
- logger23.debug({ stepId: step.id, failureKind }, "Infrastructure failure \u2014 attempting to heal");
8292
+ logger28.debug({ stepId: step.id, failureKind }, "Infrastructure failure \u2014 attempting to heal");
7236
8293
  const failure = {
7237
8294
  stepId: step.id,
7238
8295
  instruction: step.instruction,
@@ -7243,7 +8300,7 @@ async function executeStep(step, page, ctx) {
7243
8300
  };
7244
8301
  const decision = ctx.budget.shouldHeal(failure);
7245
8302
  if (!decision.heal) {
7246
- logger23.info({ stepId: step.id, reason: decision.reason }, "Healing skipped");
8303
+ logger28.info({ stepId: step.id, reason: decision.reason }, "Healing skipped");
7247
8304
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
7248
8305
  }
7249
8306
  const healer = new SelfHealer(ctx.provider, ctx.config.healing.maxLevel);
@@ -7274,7 +8331,7 @@ async function executeStep(step, page, ctx) {
7274
8331
  pageUrl: page.url()
7275
8332
  });
7276
8333
  }
7277
- logger23.info(
8334
+ logger28.info(
7278
8335
  { stepId: step.id, level: healResult.level, strategy: healResult.strategy, codeChanged },
7279
8336
  codeChanged ? "Step passed after healing (code NOT saved \u2014 awaits human review)" : "Step passed after smart retry (no code change)"
7280
8337
  );
@@ -7290,7 +8347,7 @@ async function executeStep(step, page, ctx) {
7290
8347
  }
7291
8348
  } catch (healErr) {
7292
8349
  const healErrMsg = healErr instanceof Error ? healErr.message : String(healErr);
7293
- logger23.warn({ stepId: step.id, error: healErrMsg }, "Healing threw an error");
8350
+ logger28.warn({ stepId: step.id, error: healErrMsg }, "Healing threw an error");
7294
8351
  ctx.budget.recordHealFailure(step.id);
7295
8352
  }
7296
8353
  return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
@@ -7320,7 +8377,7 @@ async function captureScreenshot(page, ctx, stepId, suffix) {
7320
8377
  await page.screenshot({ path: screenshotPath, fullPage: true });
7321
8378
  return screenshotPath;
7322
8379
  } catch (err) {
7323
- logger23.debug({ err }, "Screenshot capture failed");
8380
+ logger28.debug({ err }, "Screenshot capture failed");
7324
8381
  return void 0;
7325
8382
  }
7326
8383
  }
@@ -7332,7 +8389,7 @@ function withTimeout(promise, ms) {
7332
8389
  )
7333
8390
  ]);
7334
8391
  }
7335
- var import_path21, logger23, ASSERTION_PATTERNS, INFRA_PATTERNS;
8392
+ var import_path21, logger28, ASSERTION_PATTERNS, INFRA_PATTERNS;
7336
8393
  var init_executor = __esm({
7337
8394
  "src/engine/executor.ts"() {
7338
8395
  "use strict";
@@ -7344,7 +8401,7 @@ var init_executor = __esm({
7344
8401
  init_step_type_detector();
7345
8402
  init_logger();
7346
8403
  init_code_runner();
7347
- logger23 = createChildLogger("executor");
8404
+ logger28 = createChildLogger("executor");
7348
8405
  ASSERTION_PATTERNS = [
7349
8406
  /expect\(.*\)\.(toBe|toEqual|toContain|toMatch|toHaveText|toHaveValue|toBeChecked|toBeVisible|toBeHidden|toBeEnabled|toBeDisabled|toHaveCount|toHaveAttribute|toHaveClass|toHaveCSS|toHaveURL|toHaveTitle)/i,
7350
8407
  /AssertionError/i,
@@ -7567,7 +8624,7 @@ async function loadDataRows(rootDir, dataSource) {
7567
8624
  }
7568
8625
  const raw = await import_fs_extra16.default.readJson(filePath);
7569
8626
  if (!Array.isArray(raw)) throw new Error("JSON data file must contain an array of objects");
7570
- logger24.info({ path: filePath, rows: raw.length }, "Loaded JSON data rows");
8627
+ logger29.info({ path: filePath, rows: raw.length }, "Loaded JSON data rows");
7571
8628
  return raw.map(
7572
8629
  (row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, String(v)]))
7573
8630
  );
@@ -7590,14 +8647,14 @@ async function loadDataRows(rootDir, dataSource) {
7590
8647
  });
7591
8648
  return row;
7592
8649
  });
7593
- logger24.info({ path: filePath, rows: rows.length }, "Loaded CSV data rows");
8650
+ logger29.info({ path: filePath, rows: rows.length }, "Loaded CSV data rows");
7594
8651
  return rows;
7595
8652
  }
7596
8653
  default:
7597
8654
  return [];
7598
8655
  }
7599
8656
  }
7600
- var import_path22, import_fs_extra16, logger24;
8657
+ var import_path22, import_fs_extra16, logger29;
7601
8658
  var init_data_loader = __esm({
7602
8659
  "src/engine/data-loader.ts"() {
7603
8660
  "use strict";
@@ -7605,7 +8662,7 @@ var init_data_loader = __esm({
7605
8662
  import_path22 = __toESM(require("path"));
7606
8663
  import_fs_extra16 = __toESM(require("fs-extra"));
7607
8664
  init_logger();
7608
- logger24 = createChildLogger("data-loader");
8665
+ logger29 = createChildLogger("data-loader");
7609
8666
  }
7610
8667
  });
7611
8668
 
@@ -9584,7 +10641,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
9584
10641
  if (envUrl) {
9585
10642
  autotestConfig = { ...autotestConfig, baseUrl: envUrl };
9586
10643
  }
9587
- logger25.info({ runId, runConfig, environment: activeEnv, baseUrl: autotestConfig.baseUrl }, "Run started");
10644
+ logger30.info({ runId, runConfig, environment: activeEnv, baseUrl: autotestConfig.baseUrl }, "Run started");
9588
10645
  const runLog = await createRunLogFile(rootDir, runId);
9589
10646
  const logToFile = (level, msg, data) => {
9590
10647
  const ts = (/* @__PURE__ */ new Date()).toISOString();
@@ -9598,7 +10655,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
9598
10655
  try {
9599
10656
  const pairs = await collectWork(testsDir, runConfig);
9600
10657
  if (pairs.length === 0) {
9601
- logger25.warn({ runConfig }, "No tests matched the run config");
10658
+ logger30.warn({ runConfig }, "No tests matched the run config");
9602
10659
  }
9603
10660
  const variables = await loadVariables(rootDir, runConfig.env);
9604
10661
  const sharedVariables = /* @__PURE__ */ new Map();
@@ -9609,16 +10666,53 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
9609
10666
  const rows = await loadDataRows(rootDir, pair.testCase.dataSource);
9610
10667
  if (rows.length > 0) {
9611
10668
  rows.forEach((row, idx) => expandedPairs.push({ ...pair, dataRow: row, dataRowIndex: idx }));
9612
- logger25.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
10669
+ logger30.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
9613
10670
  continue;
9614
10671
  }
9615
10672
  } catch (err) {
9616
- logger25.warn({ err, caseId: pair.testCase.id }, "Failed to load data rows \u2014 running case once");
10673
+ logger30.warn({ err, caseId: pair.testCase.id }, "Failed to load data rows \u2014 running case once");
9617
10674
  }
9618
10675
  }
9619
10676
  expandedPairs.push(pair);
9620
10677
  }
9621
10678
  ws?.broadcast("run:start", { runId, totalTests: expandedPairs.length });
10679
+ if (autotestConfig.mcp?.proactiveHealing && autotestConfig.mcp?.enabled) {
10680
+ logger30.info("Proactive healing enabled \u2014 validating selectors before run");
10681
+ let mcpChecker = null;
10682
+ try {
10683
+ mcpChecker = new McpSession({
10684
+ headless: autotestConfig.mcp.headless,
10685
+ actionTimeout: autotestConfig.mcp.actionTimeout,
10686
+ idleTimeout: autotestConfig.mcp.idleTimeout
10687
+ });
10688
+ for (const pair of expandedPairs) {
10689
+ for (const step of pair.testCase.steps) {
10690
+ if (!step.generatedCode.trim()) continue;
10691
+ const result = await checkSelectors(mcpChecker, autotestConfig.baseUrl, step.generatedCode);
10692
+ if (!result.ok) {
10693
+ ws?.broadcast("healing:proactive", {
10694
+ runId,
10695
+ caseId: pair.testCase.id,
10696
+ stepId: step.id,
10697
+ instruction: step.instruction,
10698
+ missing: result.checks.filter((c) => !c.found).map((c) => ({
10699
+ selector: c.selector,
10700
+ suggestion: c.suggestion
10701
+ }))
10702
+ });
10703
+ logger30.warn(
10704
+ { stepId: step.id, missing: result.missing },
10705
+ `Proactive check: ${result.missing} selector(s) may be broken`
10706
+ );
10707
+ }
10708
+ }
10709
+ }
10710
+ } catch (err) {
10711
+ logger30.warn({ err }, "Proactive healing check failed \u2014 continuing with run");
10712
+ } finally {
10713
+ await mcpChecker?.disconnect();
10714
+ }
10715
+ }
9622
10716
  const smartRouter = createSmartRouter(rootDir);
9623
10717
  const provider = smartRouter.getPrimaryProvider();
9624
10718
  const budget = new HealingBudget(autotestConfig.healing.dailyBudget);
@@ -9628,7 +10722,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
9628
10722
  ({ suite, testCase }) => (isSuiteApiOnly(suite) || isCaseApiOnly(testCase)) && !isSuiteAudit(suite)
9629
10723
  );
9630
10724
  if (allApiOnly) {
9631
- logger25.info("All test cases are API-only \u2014 no browser will be launched");
10725
+ logger30.info("All test cases are API-only \u2014 no browser will be launched");
9632
10726
  logToFile("info", "API-only run detected \u2014 skipping browser launch");
9633
10727
  }
9634
10728
  const browserInstances = /* @__PURE__ */ new Map();
@@ -9738,7 +10832,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
9738
10832
  }
9739
10833
  await healingReporter.flush(rootDir, runId);
9740
10834
  await smartRouter.getCache().persist();
9741
- logger25.info(
10835
+ logger30.info(
9742
10836
  {
9743
10837
  runId,
9744
10838
  passed: runResult.passed,
@@ -9765,7 +10859,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
9765
10859
  let page;
9766
10860
  if (apiOnly) {
9767
10861
  page = await browserManager.createApiOnlyPage(config.baseUrl);
9768
- logger25.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
10862
+ logger30.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
9769
10863
  } else {
9770
10864
  if (!browser) throw new Error("Browser required for UI test cases");
9771
10865
  context = await browserManager.newContext(browser, caseConfig, import_path25.default.join(rootDir, "results", "videos"));
@@ -9987,7 +11081,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
9987
11081
  logToFile?.("info", ` Perf: ${entry.score ?? "--"} | A11y: ${entry.a11yScore ?? "--"} | SEO: ${entry.seoScore ?? "--"} | FCP: ${entry.fcp != null ? (entry.fcp / 1e3).toFixed(2) + "s" : "--"} | LCP: ${entry.lcp != null ? (entry.lcp / 1e3).toFixed(2) + "s" : "--"} | CLS: ${entry.cls?.toFixed(3) ?? "--"} | TTFB: ${entry.ttfb != null ? (entry.ttfb / 1e3).toFixed(2) + "s" : "--"}`);
9988
11082
  } catch (auditErr) {
9989
11083
  const errMsg = auditErr instanceof Error ? auditErr.message : String(auditErr);
9990
- logger25.warn({ url, err: errMsg }, "Lighthouse audit failed for URL");
11084
+ logger30.warn({ url, err: errMsg }, "Lighthouse audit failed for URL");
9991
11085
  logToFile?.("warn", ` Lighthouse failed for ${url}: ${errMsg}`);
9992
11086
  pageLoads.push({
9993
11087
  url,
@@ -10008,7 +11102,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
10008
11102
  }
10009
11103
  } catch (lhErr) {
10010
11104
  const errMsg = lhErr instanceof Error ? lhErr.message : String(lhErr);
10011
- logger25.warn({ err: errMsg }, "Lighthouse import/init failed");
11105
+ logger30.warn({ err: errMsg }, "Lighthouse import/init failed");
10012
11106
  logToFile?.("warn", ` Lighthouse init failed: ${errMsg}`);
10013
11107
  const recordedIndices = new Set(pageLoads.map((p) => p.stepIndex));
10014
11108
  for (const { s, idx } of navigatedSteps) {
@@ -10104,7 +11198,7 @@ async function collectWork(testsDir, runConfig) {
10104
11198
  }
10105
11199
  const suiteDir = await findSuiteDirById(testsDir, suite.id);
10106
11200
  if (!suiteDir) {
10107
- logger25.warn({ suiteId: suite.id }, "Suite directory not found \u2014 skipping");
11201
+ logger30.warn({ suiteId: suite.id }, "Suite directory not found \u2014 skipping");
10108
11202
  continue;
10109
11203
  }
10110
11204
  const cases = await listCases(suiteDir);
@@ -10126,10 +11220,10 @@ async function loadVariables(rootDir, env) {
10126
11220
  try {
10127
11221
  const resolved = await resolveVariables(rootDir, env ?? "dev");
10128
11222
  const count = Object.keys(resolved).length;
10129
- logger25.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
11223
+ logger30.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
10130
11224
  return resolved;
10131
11225
  } catch (err) {
10132
- logger25.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to load variables \u2014 running without variable substitution");
11226
+ logger30.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to load variables \u2014 running without variable substitution");
10133
11227
  return {};
10134
11228
  }
10135
11229
  }
@@ -10200,7 +11294,7 @@ function buildRunResult(runId, startedAt, finishedAt, suites, environment) {
10200
11294
  skipped
10201
11295
  };
10202
11296
  }
10203
- var import_path25, import_crypto6, import_fs_extra17, logger25;
11297
+ var import_path25, import_crypto6, import_fs_extra17, logger30;
10204
11298
  var init_runner = __esm({
10205
11299
  "src/engine/runner.ts"() {
10206
11300
  "use strict";
@@ -10213,6 +11307,8 @@ var init_runner = __esm({
10213
11307
  init_variable_store();
10214
11308
  init_result_store();
10215
11309
  init_router();
11310
+ init_mcp_session();
11311
+ init_proactive_checker();
10216
11312
  init_browser_manager();
10217
11313
  init_healing_budget();
10218
11314
  init_healing_report();
@@ -10223,7 +11319,7 @@ var init_runner = __esm({
10223
11319
  init_data_loader();
10224
11320
  init_step_type_detector();
10225
11321
  init_logger();
10226
- logger25 = createChildLogger("runner");
11322
+ logger30 = createChildLogger("runner");
10227
11323
  }
10228
11324
  });
10229
11325
 
@@ -10257,7 +11353,7 @@ async function runRoutes(fastify) {
10257
11353
  };
10258
11354
  const wsManager = fastify.wsManager;
10259
11355
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
10260
- logger26.info({ runConfig }, "Run requested via API");
11356
+ logger31.info({ runConfig }, "Run requested via API");
10261
11357
  runTests(rootDir, runConfig, autotestConfig, wsManager).then((result) => {
10262
11358
  wsManager.broadcast("run:complete", {
10263
11359
  runId: result.runId,
@@ -10266,9 +11362,9 @@ async function runRoutes(fastify) {
10266
11362
  skipped: result.skipped,
10267
11363
  duration: result.duration
10268
11364
  });
10269
- logger26.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
11365
+ logger31.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
10270
11366
  }).catch((err) => {
10271
- logger26.error({ err }, "API-triggered run failed");
11367
+ logger31.error({ err }, "API-triggered run failed");
10272
11368
  wsManager.broadcast("run:error", {
10273
11369
  error: err instanceof Error ? err.message : String(err)
10274
11370
  });
@@ -10299,7 +11395,7 @@ async function runRoutes(fastify) {
10299
11395
  }
10300
11396
  });
10301
11397
  }
10302
- var import_zod14, logger26, ScreenshotMode, VideoMode, TraceMode, StartRunBody, ListRunsQuery;
11398
+ var import_zod14, logger31, ScreenshotMode, VideoMode, TraceMode, StartRunBody, ListRunsQuery;
10303
11399
  var init_run2 = __esm({
10304
11400
  "src/server/routes/run.ts"() {
10305
11401
  "use strict";
@@ -10310,7 +11406,7 @@ var init_run2 = __esm({
10310
11406
  init_result_store();
10311
11407
  init_utils2();
10312
11408
  init_logger();
10313
- logger26 = createChildLogger("routes/run");
11409
+ logger31 = createChildLogger("routes/run");
10314
11410
  ScreenshotMode = import_zod14.z.enum(["off", "on", "only-on-failure"]);
10315
11411
  VideoMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
10316
11412
  TraceMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
@@ -10400,7 +11496,7 @@ async function parseSwaggerSpec(urlOrJson) {
10400
11496
  specUrl = new URL(specUrl, url).href;
10401
11497
  }
10402
11498
  urls.unshift(specUrl);
10403
- logger27.info({ specUrl, pattern: pattern.source }, "Extracted spec URL from HTML page");
11499
+ logger32.info({ specUrl, pattern: pattern.source }, "Extracted spec URL from HTML page");
10404
11500
  }
10405
11501
  }
10406
11502
  }
@@ -10412,7 +11508,7 @@ async function parseSwaggerSpec(urlOrJson) {
10412
11508
  const errors = [];
10413
11509
  for (const tryUrl of uniqueUrls) {
10414
11510
  try {
10415
- logger27.debug({ tryUrl }, "Trying to fetch OpenAPI spec");
11511
+ logger32.debug({ tryUrl }, "Trying to fetch OpenAPI spec");
10416
11512
  const res = await fetch(tryUrl, {
10417
11513
  headers: { Accept: "application/json, application/yaml, */*" },
10418
11514
  signal: AbortSignal.timeout(15e3)
@@ -10427,7 +11523,7 @@ async function parseSwaggerSpec(urlOrJson) {
10427
11523
  spec = isJson ? JSON.parse(text) : import_js_yaml.default.load(text);
10428
11524
  if (spec && (spec.openapi || spec.swagger || spec.paths)) {
10429
11525
  fetched = true;
10430
- logger27.info({ tryUrl, format: isYaml ? "YAML" : "JSON" }, "Successfully fetched OpenAPI spec");
11526
+ logger32.info({ tryUrl, format: isYaml ? "YAML" : "JSON" }, "Successfully fetched OpenAPI spec");
10431
11527
  break;
10432
11528
  }
10433
11529
  errors.push(`${tryUrl}: Parsed but not a valid OpenAPI spec`);
@@ -10441,7 +11537,7 @@ async function parseSwaggerSpec(urlOrJson) {
10441
11537
  if (parsed && typeof parsed === "object" && (parsed.openapi || parsed.swagger || parsed.paths)) {
10442
11538
  spec = parsed;
10443
11539
  fetched = true;
10444
- logger27.info({ tryUrl, format: "YAML (text/plain)" }, "Successfully fetched OpenAPI spec");
11540
+ logger32.info({ tryUrl, format: "YAML (text/plain)" }, "Successfully fetched OpenAPI spec");
10445
11541
  break;
10446
11542
  }
10447
11543
  } catch {
@@ -10530,17 +11626,17 @@ function parseSpec(spec) {
10530
11626
  });
10531
11627
  }
10532
11628
  }
10533
- logger27.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
11629
+ logger32.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
10534
11630
  return { title, baseUrl, endpoints };
10535
11631
  }
10536
- var import_js_yaml, logger27;
11632
+ var import_js_yaml, logger32;
10537
11633
  var init_swagger_parser = __esm({
10538
11634
  "src/ai/swagger-parser.ts"() {
10539
11635
  "use strict";
10540
11636
  init_cjs_shims();
10541
11637
  import_js_yaml = __toESM(require("js-yaml"));
10542
11638
  init_logger();
10543
- logger27 = createChildLogger("swagger-parser");
11639
+ logger32 = createChildLogger("swagger-parser");
10544
11640
  }
10545
11641
  });
10546
11642
 
@@ -10592,11 +11688,11 @@ function adfToText(node) {
10592
11688
  async function fetchJiraIssue(issueKey) {
10593
11689
  const config = getJiraConfig();
10594
11690
  if (!config) {
10595
- throw new Error("Jira not configured \u2014 set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN in your .env file.");
11691
+ throw new Error("Jira not configured \u2014 set JIRA_BASE_URL, JIRA_EMAIL, and JIRA_API_TOKEN in your .env file. [JIRA_NOT_CONFIGURED]");
10596
11692
  }
10597
11693
  const authHeader = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
10598
11694
  const url = `${config.baseUrl}/rest/api/3/issue/${encodeURIComponent(issueKey)}?fields=summary,description,comment,issuetype,status,priority,labels,customfield_10020`;
10599
- logger28.info({ issueKey, url: url.replace(/\/rest.*/, "/...") }, "Fetching Jira issue");
11695
+ logger33.info({ issueKey, url: url.replace(/\/rest.*/, "/...") }, "Fetching Jira issue");
10600
11696
  const res = await fetch(url, {
10601
11697
  method: "GET",
10602
11698
  headers: {
@@ -10725,19 +11821,19 @@ async function storyRoutes(fastify) {
10725
11821
  try {
10726
11822
  const jiraContent = await fetchJiraIssue(issueKey);
10727
11823
  storyText = buildEnrichedStory(jiraContent, parsed.data.story || void 0);
10728
- logger28.info({ issueKey }, "Enriched story with Jira content");
11824
+ logger33.info({ issueKey }, "Enriched story with Jira content");
10729
11825
  } catch (jiraErr) {
10730
11826
  if (!storyText || storyText.length < 10) {
10731
11827
  return sendError(reply, 400, "Jira fetch failed and no user story provided", jiraErr);
10732
11828
  }
10733
- logger28.warn({ err: jiraErr }, "Failed to fetch Jira issue \u2014 proceeding with user story only");
11829
+ logger33.warn({ err: jiraErr }, "Failed to fetch Jira issue \u2014 proceeding with user story only");
10734
11830
  }
10735
11831
  }
10736
11832
  }
10737
11833
  if (!storyText || storyText.length < 10) {
10738
11834
  return reply.status(400).send({ error: "Not enough content to generate tests. Provide a story or a valid Jira link." });
10739
11835
  }
10740
- logger28.info({ suiteName: parsed.data.suiteName }, "Story generation requested");
11836
+ logger33.info({ suiteName: parsed.data.suiteName }, "Story generation requested");
10741
11837
  const { suiteId, suiteName } = await generateSuiteFromStory(storyText, {
10742
11838
  rootDir,
10743
11839
  outputDir: testsDir,
@@ -10765,7 +11861,7 @@ async function storyRoutes(fastify) {
10765
11861
  const { stories, concurrency } = parsed.data;
10766
11862
  const testsDir = import_path26.default.join(rootDir, "tests");
10767
11863
  const wsManager = fastify.wsManager;
10768
- logger28.info({ total: stories.length, concurrency }, "Bulk story generation requested");
11864
+ logger33.info({ total: stories.length, concurrency }, "Bulk story generation requested");
10769
11865
  const enrichedStories = await Promise.all(
10770
11866
  stories.map(async (item) => {
10771
11867
  if (!item.jiraUrl) return { story: item.story, suiteName: item.suiteName };
@@ -10778,7 +11874,7 @@ async function storyRoutes(fastify) {
10778
11874
  suiteName: item.suiteName ?? jiraContent.summary
10779
11875
  };
10780
11876
  } catch {
10781
- logger28.warn({ issueKey }, "Failed to fetch Jira issue for bulk item \u2014 using raw story");
11877
+ logger33.warn({ issueKey }, "Failed to fetch Jira issue for bulk item \u2014 using raw story");
10782
11878
  return { story: item.story, suiteName: item.suiteName };
10783
11879
  }
10784
11880
  })
@@ -10789,7 +11885,7 @@ async function storyRoutes(fastify) {
10789
11885
  wsManager,
10790
11886
  concurrency
10791
11887
  ).catch((err) => {
10792
- logger28.error({ err }, "Bulk story generation error");
11888
+ logger33.error({ err }, "Bulk story generation error");
10793
11889
  });
10794
11890
  return reply.status(202).send({
10795
11891
  message: `Processing ${stories.length} stories in the background`,
@@ -10831,7 +11927,7 @@ async function storyRoutes(fastify) {
10831
11927
  }
10832
11928
  try {
10833
11929
  const spec = await parseSwaggerSpec(parsed.data.specUrl);
10834
- logger28.info({ title: spec.title, endpoints: spec.endpoints.length }, "Parsed OpenAPI spec");
11930
+ logger33.info({ title: spec.title, endpoints: spec.endpoints.length }, "Parsed OpenAPI spec");
10835
11931
  let endpoints = spec.endpoints;
10836
11932
  if (parsed.data.tags && parsed.data.tags.length > 0) {
10837
11933
  const tagSet = new Set(parsed.data.tags.map((t) => t.toLowerCase()));
@@ -10843,7 +11939,7 @@ async function storyRoutes(fastify) {
10843
11939
  const MAX_ENDPOINTS = 20;
10844
11940
  const totalEndpoints = endpoints.length;
10845
11941
  if (endpoints.length > MAX_ENDPOINTS) {
10846
- logger28.warn({ total: endpoints.length, max: MAX_ENDPOINTS }, "Capping endpoints to max");
11942
+ logger33.warn({ total: endpoints.length, max: MAX_ENDPOINTS }, "Capping endpoints to max");
10847
11943
  endpoints = endpoints.slice(0, MAX_ENDPOINTS);
10848
11944
  }
10849
11945
  const storyLines = [
@@ -10895,7 +11991,7 @@ async function storyRoutes(fastify) {
10895
11991
  }
10896
11992
  });
10897
11993
  }
10898
- var import_path26, import_zod15, logger28, GenerateStoryBody, FetchJiraBody;
11994
+ var import_path26, import_zod15, logger33, GenerateStoryBody, FetchJiraBody;
10899
11995
  var init_story = __esm({
10900
11996
  "src/server/routes/story.ts"() {
10901
11997
  "use strict";
@@ -10906,7 +12002,7 @@ var init_story = __esm({
10906
12002
  init_swagger_parser();
10907
12003
  init_utils2();
10908
12004
  init_logger();
10909
- logger28 = createChildLogger("routes/story");
12005
+ logger33 = createChildLogger("routes/story");
10910
12006
  GenerateStoryBody = import_zod15.z.object({
10911
12007
  story: import_zod15.z.string().default(""),
10912
12008
  suiteName: import_zod15.z.string().min(1).optional(),
@@ -10947,14 +12043,14 @@ async function reportRoutes(fastify) {
10947
12043
  fastify.delete("/report/:runId", async (req, reply) => {
10948
12044
  try {
10949
12045
  await deleteResult(rootDir, req.params.runId);
10950
- logger29.info({ runId: req.params.runId }, "Run result deleted");
12046
+ logger34.info({ runId: req.params.runId }, "Run result deleted");
10951
12047
  return reply.status(204).send();
10952
12048
  } catch {
10953
12049
  return reply.status(404).send({ error: `Report for run "${req.params.runId}" not found` });
10954
12050
  }
10955
12051
  });
10956
12052
  }
10957
- var import_zod16, logger29, ListReportsQuery;
12053
+ var import_zod16, logger34, ListReportsQuery;
10958
12054
  var init_report = __esm({
10959
12055
  "src/server/routes/report.ts"() {
10960
12056
  "use strict";
@@ -10963,7 +12059,7 @@ var init_report = __esm({
10963
12059
  init_result_store();
10964
12060
  init_utils2();
10965
12061
  init_logger();
10966
- logger29 = createChildLogger("routes/report");
12062
+ logger34 = createChildLogger("routes/report");
10967
12063
  ListReportsQuery = import_zod16.z.object({
10968
12064
  limit: import_zod16.z.coerce.number().int().positive().max(100).default(20)
10969
12065
  });
@@ -11070,13 +12166,13 @@ function toAdf(text) {
11070
12166
  }));
11071
12167
  return { type: "doc", version: 1, content: paragraphs };
11072
12168
  }
11073
- var logger30, XrayClient;
12169
+ var logger35, XrayClient;
11074
12170
  var init_client = __esm({
11075
12171
  "src/xray/client.ts"() {
11076
12172
  "use strict";
11077
12173
  init_cjs_shims();
11078
12174
  init_logger();
11079
- logger30 = createChildLogger("xray-client");
12175
+ logger35 = createChildLogger("xray-client");
11080
12176
  XrayClient = class {
11081
12177
  config;
11082
12178
  authHeader;
@@ -11092,7 +12188,7 @@ var init_client = __esm({
11092
12188
  // ─── Jira REST API helpers ──────────────────────────────────────────────────
11093
12189
  async jiraRequest(method, path37, body) {
11094
12190
  const url = `${this.config.jiraBaseUrl}/rest/api/3${path37}`;
11095
- logger30.debug({ method, url }, "Jira API request");
12191
+ logger35.debug({ method, url }, "Jira API request");
11096
12192
  const headers = {
11097
12193
  "Authorization": this.authHeader,
11098
12194
  "Accept": "application/json"
@@ -11107,7 +12203,7 @@ var init_client = __esm({
11107
12203
  });
11108
12204
  if (!res.ok) {
11109
12205
  const text = await res.text();
11110
- logger30.error({ status: res.status, url, body: text }, "Jira API error");
12206
+ logger35.error({ status: res.status, url, body: text }, "Jira API error");
11111
12207
  throw new Error(`Jira API ${method} ${path37} \u2192 ${res.status}: ${text}`);
11112
12208
  }
11113
12209
  if (res.status === 204) return void 0;
@@ -11139,7 +12235,7 @@ var init_client = __esm({
11139
12235
  const token = await this.getXrayToken();
11140
12236
  const baseUrl = token ? "https://xray.cloud.getxray.app/api/v2" : `${this.config.jiraBaseUrl}/rest/raven/2.0/api`;
11141
12237
  const url = `${baseUrl}${path37}`;
11142
- logger30.debug({ method, url }, "Xray API request");
12238
+ logger35.debug({ method, url }, "Xray API request");
11143
12239
  const headers = {
11144
12240
  "Accept": "application/json"
11145
12241
  };
@@ -11158,7 +12254,7 @@ var init_client = __esm({
11158
12254
  });
11159
12255
  if (!res.ok) {
11160
12256
  const text = await res.text();
11161
- logger30.error({ status: res.status, url, body: text }, "Xray API error");
12257
+ logger35.error({ status: res.status, url, body: text }, "Xray API error");
11162
12258
  throw new Error(`Xray API ${method} ${path37} \u2192 ${res.status}: ${text}`);
11163
12259
  }
11164
12260
  if (res.status === 204) return void 0;
@@ -11227,7 +12323,7 @@ var init_client = __esm({
11227
12323
  warnings
11228
12324
  }
11229
12325
  }`;
11230
- logger30.info({ caseName: testCase.name, projectKey: this.config.projectKey, folderPath }, "Creating Test via Xray Cloud GraphQL");
12326
+ logger35.info({ caseName: testCase.name, projectKey: this.config.projectKey, folderPath }, "Creating Test via Xray Cloud GraphQL");
11231
12327
  const result = await this.xrayGraphQL(mutation);
11232
12328
  if (!result?.createTest?.test) {
11233
12329
  throw new Error("Xray createTest returned no test data");
@@ -11236,9 +12332,9 @@ var init_client = __esm({
11236
12332
  const jiraKey = test.jira?.key ?? "";
11237
12333
  const warnings = result.createTest.warnings;
11238
12334
  if (warnings && warnings.length > 0) {
11239
- logger30.warn({ warnings, jiraKey }, "Xray createTest warnings");
12335
+ logger35.warn({ warnings, jiraKey }, "Xray createTest warnings");
11240
12336
  }
11241
- logger30.info({ issueId: test.issueId, jiraKey, stepCount: testCase.steps.length, folderPath }, "Xray Test created via GraphQL");
12337
+ logger35.info({ issueId: test.issueId, jiraKey, stepCount: testCase.steps.length, folderPath }, "Xray Test created via GraphQL");
11242
12338
  return { id: test.issueId, key: jiraKey, self: "" };
11243
12339
  }
11244
12340
  /** Xray Server/DC: create via Jira REST API */
@@ -11251,7 +12347,7 @@ var init_client = __esm({
11251
12347
  issuetype: { name: issueType },
11252
12348
  labels: ["assuremind", ...testCase.tags]
11253
12349
  };
11254
- logger30.info({ caseName: testCase.name, projectKey: this.config.projectKey }, "Creating Xray Test via Jira REST");
12350
+ logger35.info({ caseName: testCase.name, projectKey: this.config.projectKey }, "Creating Xray Test via Jira REST");
11255
12351
  const result = await this.jiraRequest("POST", "/issue", { fields });
11256
12352
  if (testCase.steps.length > 0) {
11257
12353
  await this.addTestStepsServerDC(result.key, testCase.steps);
@@ -11267,7 +12363,7 @@ var init_client = __esm({
11267
12363
  description: toAdf(testCase.description || `Automated test case: ${testCase.name}`),
11268
12364
  labels: ["assuremind", ...testCase.tags]
11269
12365
  };
11270
- logger30.info({ issueKey: testIssue.key, caseName: testCase.name }, "Updating Xray Test");
12366
+ logger35.info({ issueKey: testIssue.key, caseName: testCase.name }, "Updating Xray Test");
11271
12367
  await this.jiraRequest("PUT", `/issue/${testIssue.key}`, { fields });
11272
12368
  if (testCase.steps.length > 0) {
11273
12369
  await this.replaceTestSteps(testIssue, testCase.steps);
@@ -11306,7 +12402,7 @@ var init_client = __esm({
11306
12402
  warnings
11307
12403
  }
11308
12404
  }`;
11309
- logger30.info({ suiteName: suite.name, projectKey: this.config.projectKey, testCount: testIssueIds?.length }, "Creating Test Set via Xray Cloud GraphQL");
12405
+ logger35.info({ suiteName: suite.name, projectKey: this.config.projectKey, testCount: testIssueIds?.length }, "Creating Test Set via Xray Cloud GraphQL");
11310
12406
  const result = await this.xrayGraphQL(mutation);
11311
12407
  if (!result?.createTestSet?.testSet) {
11312
12408
  throw new Error("Xray createTestSet returned no data");
@@ -11315,9 +12411,9 @@ var init_client = __esm({
11315
12411
  const jiraKey = testSet.jira?.key ?? "";
11316
12412
  const warnings = result.createTestSet.warnings;
11317
12413
  if (warnings && warnings.length > 0) {
11318
- logger30.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
12414
+ logger35.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
11319
12415
  }
11320
- logger30.info({ issueId: testSet.issueId, jiraKey }, "Xray Test Set created via GraphQL");
12416
+ logger35.info({ issueId: testSet.issueId, jiraKey }, "Xray Test Set created via GraphQL");
11321
12417
  return { id: testSet.issueId, key: jiraKey, self: "" };
11322
12418
  }
11323
12419
  /** Xray Server/DC: create via Jira REST API */
@@ -11330,7 +12426,7 @@ var init_client = __esm({
11330
12426
  issuetype: { name: issueType },
11331
12427
  labels: ["assuremind", ...suite.tags]
11332
12428
  };
11333
- logger30.info({ suiteName: suite.name, projectKey: this.config.projectKey }, "Creating Xray Test Set via Jira REST");
12429
+ logger35.info({ suiteName: suite.name, projectKey: this.config.projectKey }, "Creating Xray Test Set via Jira REST");
11334
12430
  const result = await this.jiraRequest("POST", "/issue", { fields });
11335
12431
  return { id: result.id, key: result.key, self: result.self };
11336
12432
  }
@@ -11343,7 +12439,7 @@ var init_client = __esm({
11343
12439
  description: toAdf(suite.description || `Automated test suite: ${suite.name}`),
11344
12440
  labels: ["assuremind", ...suite.tags]
11345
12441
  };
11346
- logger30.info({ issueKey, suiteName: suite.name }, "Updating Xray Test Set");
12442
+ logger35.info({ issueKey, suiteName: suite.name }, "Updating Xray Test Set");
11347
12443
  await this.jiraRequest("PUT", `/issue/${issueKey}`, { fields });
11348
12444
  }
11349
12445
  // ─── Test Steps ────────────────────────────────────────────────────────────
@@ -11360,7 +12456,7 @@ var init_client = __esm({
11360
12456
  if (this.isXrayCloud) {
11361
12457
  const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
11362
12458
  if (!xrayId) {
11363
- logger30.warn({ testKey: testIssue.key }, "Cannot add steps \u2014 Test not found in Xray yet");
12459
+ logger35.warn({ testKey: testIssue.key }, "Cannot add steps \u2014 Test not found in Xray yet");
11364
12460
  return;
11365
12461
  }
11366
12462
  await this.addTestStepsCloud(xrayId, steps);
@@ -11395,16 +12491,16 @@ var init_client = __esm({
11395
12491
  } catch (err) {
11396
12492
  const errMsg = err instanceof Error ? err.message : String(err);
11397
12493
  if (errMsg.includes("not found")) {
11398
- logger30.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
12494
+ logger35.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
11399
12495
  break;
11400
12496
  }
11401
- logger30.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
12497
+ logger35.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
11402
12498
  }
11403
12499
  }
11404
12500
  if (added > 0) {
11405
- logger30.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
12501
+ logger35.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
11406
12502
  } else if (steps.length > 0) {
11407
- logger30.warn({ xrayIssueId }, "Failed to add any steps");
12503
+ logger35.warn({ xrayIssueId }, "Failed to add any steps");
11408
12504
  }
11409
12505
  }
11410
12506
  /** Xray Server/DC — use REST API /test/{key}/step */
@@ -11417,14 +12513,14 @@ var init_client = __esm({
11417
12513
  }));
11418
12514
  try {
11419
12515
  await this.xrayRequest("PUT", `/test/${testIssueKey}/step`, xraySteps);
11420
- logger30.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
12516
+ logger35.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
11421
12517
  } catch (err) {
11422
- logger30.warn({ testIssueKey, err }, "Bulk step add failed, trying one-by-one");
12518
+ logger35.warn({ testIssueKey, err }, "Bulk step add failed, trying one-by-one");
11423
12519
  for (const step of xraySteps) {
11424
12520
  try {
11425
12521
  await this.xrayRequest("POST", `/test/${testIssueKey}/step`, step);
11426
12522
  } catch (stepErr) {
11427
- logger30.warn({ testIssueKey, step: step.index, err: stepErr }, "Individual step add failed");
12523
+ logger35.warn({ testIssueKey, step: step.index, err: stepErr }, "Individual step add failed");
11428
12524
  }
11429
12525
  }
11430
12526
  }
@@ -11436,7 +12532,7 @@ var init_client = __esm({
11436
12532
  if (this.isXrayCloud) {
11437
12533
  const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
11438
12534
  if (!xrayId) {
11439
- logger30.warn({ testKey: testIssue.key }, "Cannot replace steps \u2014 Test not found in Xray");
12535
+ logger35.warn({ testKey: testIssue.key }, "Cannot replace steps \u2014 Test not found in Xray");
11440
12536
  return;
11441
12537
  }
11442
12538
  await this.addTestStepsCloud(xrayId, steps);
@@ -11448,7 +12544,7 @@ var init_client = __esm({
11448
12544
  await this.xrayRequest("DELETE", `/test/${testIssue.key}/step/${step.id}`);
11449
12545
  }
11450
12546
  } catch {
11451
- logger30.debug({ testIssueKey: testIssue.key }, "No existing steps to delete (or endpoint unavailable)");
12547
+ logger35.debug({ testIssueKey: testIssue.key }, "No existing steps to delete (or endpoint unavailable)");
11452
12548
  }
11453
12549
  await this.addTestStepsServerDC(testIssue.key, steps);
11454
12550
  }
@@ -11467,9 +12563,9 @@ var init_client = __esm({
11467
12563
  await this.xrayRequest("POST", `/testset/${testSet.key}/test`, {
11468
12564
  add: tests.map((t) => t.key)
11469
12565
  });
11470
- logger30.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
12566
+ logger35.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
11471
12567
  } catch (err) {
11472
- logger30.warn({ testSetKey: testSet.key, err }, "Xray link API failed, trying Jira issue links");
12568
+ logger35.warn({ testSetKey: testSet.key, err }, "Xray link API failed, trying Jira issue links");
11473
12569
  await this.linkViaJiraIssueLinks(testSet.key, tests.map((t) => t.key));
11474
12570
  }
11475
12571
  }
@@ -11479,10 +12575,10 @@ var init_client = __esm({
11479
12575
  * Resolves Xray internal IDs first (they differ from Jira numeric IDs).
11480
12576
  */
11481
12577
  async linkTestsCloud(testSet, tests) {
11482
- logger30.info({ testSetKey: testSet.key, testKeys: tests.map((t) => t.key) }, "Resolving Xray IDs for linking");
12578
+ logger35.info({ testSetKey: testSet.key, testKeys: tests.map((t) => t.key) }, "Resolving Xray IDs for linking");
11483
12579
  const xrayTestSetId = await this.resolveXrayIssueId(testSet.key, "Test Set");
11484
12580
  if (!xrayTestSetId) {
11485
- logger30.error({ testSetKey: testSet.key }, "Cannot link \u2014 Test Set not found in Xray");
12581
+ logger35.error({ testSetKey: testSet.key }, "Cannot link \u2014 Test Set not found in Xray");
11486
12582
  return;
11487
12583
  }
11488
12584
  const xrayTestIds = [];
@@ -11491,11 +12587,11 @@ var init_client = __esm({
11491
12587
  if (xrayTestId) {
11492
12588
  xrayTestIds.push(xrayTestId);
11493
12589
  } else {
11494
- logger30.warn({ testKey: test.key }, "Could not resolve Xray ID for test \u2014 skipping");
12590
+ logger35.warn({ testKey: test.key }, "Could not resolve Xray ID for test \u2014 skipping");
11495
12591
  }
11496
12592
  }
11497
12593
  if (xrayTestIds.length === 0) {
11498
- logger30.warn({ testSetKey: testSet.key }, "No test IDs resolved \u2014 skipping link");
12594
+ logger35.warn({ testSetKey: testSet.key }, "No test IDs resolved \u2014 skipping link");
11499
12595
  return;
11500
12596
  }
11501
12597
  try {
@@ -11511,7 +12607,7 @@ var init_client = __esm({
11511
12607
  }`
11512
12608
  );
11513
12609
  if (result) {
11514
- logger30.info({
12610
+ logger35.info({
11515
12611
  testSetKey: testSet.key,
11516
12612
  xrayTestSetId,
11517
12613
  count: xrayTestIds.length,
@@ -11520,7 +12616,7 @@ var init_client = __esm({
11520
12616
  }, "Tests linked to Test Set via Xray Cloud");
11521
12617
  }
11522
12618
  } catch (err) {
11523
- logger30.error({ testSetKey: testSet.key, xrayTestSetId, err: err instanceof Error ? err.message : String(err) }, "linkTestsCloud failed");
12619
+ logger35.error({ testSetKey: testSet.key, xrayTestSetId, err: err instanceof Error ? err.message : String(err) }, "linkTestsCloud failed");
11524
12620
  }
11525
12621
  }
11526
12622
  sleep(ms) {
@@ -11536,7 +12632,7 @@ var init_client = __esm({
11536
12632
  outwardIssue: { key: testKey }
11537
12633
  });
11538
12634
  } catch (linkErr) {
11539
- logger30.error({ testSetKey, testKey, err: linkErr }, "Failed to link test to test set");
12635
+ logger35.error({ testSetKey, testKey, err: linkErr }, "Failed to link test to test set");
11540
12636
  }
11541
12637
  }
11542
12638
  }
@@ -11549,7 +12645,7 @@ var init_client = __esm({
11549
12645
  */
11550
12646
  async createFolderAndAddTests(folderName, tests) {
11551
12647
  if (!this.isXrayCloud) {
11552
- logger30.debug("Folder creation skipped \u2014 only supported on Xray Cloud");
12648
+ logger35.debug("Folder creation skipped \u2014 only supported on Xray Cloud");
11553
12649
  return;
11554
12650
  }
11555
12651
  if (tests.length === 0) return;
@@ -11571,11 +12667,11 @@ var init_client = __esm({
11571
12667
  }
11572
12668
  }`
11573
12669
  );
11574
- logger30.info({ folderPath, projectId }, "Test Repository folder created");
12670
+ logger35.info({ folderPath, projectId }, "Test Repository folder created");
11575
12671
  } catch (folderErr) {
11576
12672
  const errMsg = folderErr instanceof Error ? folderErr.message : String(folderErr);
11577
12673
  if (errMsg.includes("already exists")) {
11578
- logger30.info({ folderPath }, "Test Repository folder already exists \u2014 reusing");
12674
+ logger35.info({ folderPath }, "Test Repository folder already exists \u2014 reusing");
11579
12675
  } else {
11580
12676
  throw folderErr;
11581
12677
  }
@@ -11586,14 +12682,14 @@ var init_client = __esm({
11586
12682
  if (xrayId) {
11587
12683
  xrayTestIds.push(xrayId);
11588
12684
  } else {
11589
- logger30.warn({ testKey: test.key }, "Could not resolve Xray ID for folder \u2014 skipping");
12685
+ logger35.warn({ testKey: test.key }, "Could not resolve Xray ID for folder \u2014 skipping");
11590
12686
  }
11591
12687
  }
11592
12688
  if (xrayTestIds.length === 0) {
11593
- logger30.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
12689
+ logger35.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
11594
12690
  return;
11595
12691
  }
11596
- logger30.info({ folderPath, xrayTestIds }, "Adding tests to folder with resolved Xray IDs");
12692
+ logger35.info({ folderPath, xrayTestIds }, "Adding tests to folder with resolved Xray IDs");
11597
12693
  try {
11598
12694
  await this.xrayGraphQL(
11599
12695
  `mutation {
@@ -11607,12 +12703,12 @@ var init_client = __esm({
11607
12703
  }
11608
12704
  }`
11609
12705
  );
11610
- logger30.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
12706
+ logger35.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
11611
12707
  } catch (addErr) {
11612
- logger30.warn({ folderPath, err: addErr instanceof Error ? addErr.message : String(addErr) }, "Failed to add tests to folder");
12708
+ logger35.warn({ folderPath, err: addErr instanceof Error ? addErr.message : String(addErr) }, "Failed to add tests to folder");
11613
12709
  }
11614
12710
  } catch (err) {
11615
- logger30.error({ folderName, err: err instanceof Error ? err.message : String(err) }, "FAILED to create folder or add tests \u2014 tests exist but are unorganized");
12711
+ logger35.error({ folderName, err: err instanceof Error ? err.message : String(err) }, "FAILED to create folder or add tests \u2014 tests exist but are unorganized");
11616
12712
  }
11617
12713
  }
11618
12714
  // ─── Xray Cloud GraphQL helper ────────────────────────────────────────────
@@ -11630,12 +12726,12 @@ var init_client = __esm({
11630
12726
  });
11631
12727
  if (!res.ok) {
11632
12728
  const text = await res.text();
11633
- logger30.warn({ status: res.status, body: text }, "Xray Cloud GraphQL request failed");
12729
+ logger35.warn({ status: res.status, body: text }, "Xray Cloud GraphQL request failed");
11634
12730
  throw new Error(`Xray GraphQL \u2192 ${res.status}: ${text}`);
11635
12731
  }
11636
12732
  const result = await res.json();
11637
12733
  if (result.errors && result.errors.length > 0) {
11638
- logger30.warn({ errors: result.errors }, "Xray Cloud GraphQL returned errors");
12734
+ logger35.warn({ errors: result.errors }, "Xray Cloud GraphQL returned errors");
11639
12735
  throw new Error(`Xray GraphQL errors: ${result.errors.map((e) => e.message).join("; ")}`);
11640
12736
  }
11641
12737
  return result.data ?? null;
@@ -11653,7 +12749,7 @@ var init_client = __esm({
11653
12749
  const delays = [0, 5e3, 1e4, 2e4];
11654
12750
  for (let attempt = 0; attempt < delays.length; attempt++) {
11655
12751
  if (delays[attempt] > 0) {
11656
- logger30.info({ jiraKey, attempt, delay: delays[attempt] }, `Waiting ${delays[attempt] / 1e3}s before retry`);
12752
+ logger35.info({ jiraKey, attempt, delay: delays[attempt] }, `Waiting ${delays[attempt] / 1e3}s before retry`);
11657
12753
  await this.sleep(delays[attempt]);
11658
12754
  }
11659
12755
  try {
@@ -11666,21 +12762,21 @@ var init_client = __esm({
11666
12762
  }
11667
12763
  }
11668
12764
  }`;
11669
- logger30.info({ jiraKey, attempt, query: queryName }, `Querying Xray for ${type} issueId`);
12765
+ logger35.info({ jiraKey, attempt, query: queryName }, `Querying Xray for ${type} issueId`);
11670
12766
  const result = await this.xrayGraphQL(gql);
11671
12767
  const queryResult = result?.[resultField];
11672
- logger30.info({ jiraKey, attempt, total: queryResult?.total, resultCount: queryResult?.results?.length, raw: JSON.stringify(queryResult) }, `Xray ${queryName} response`);
12768
+ logger35.info({ jiraKey, attempt, total: queryResult?.total, resultCount: queryResult?.results?.length, raw: JSON.stringify(queryResult) }, `Xray ${queryName} response`);
11673
12769
  const items = queryResult?.results;
11674
12770
  if (items && items.length > 0 && items[0].issueId) {
11675
- logger30.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
12771
+ logger35.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
11676
12772
  return items[0].issueId;
11677
12773
  }
11678
- logger30.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
12774
+ logger35.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
11679
12775
  } catch (err) {
11680
- logger30.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
12776
+ logger35.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
11681
12777
  }
11682
12778
  }
11683
- logger30.error({ jiraKey }, `Could not resolve Xray issueId for ${type} after ${delays.length} attempts`);
12779
+ logger35.error({ jiraKey }, `Could not resolve Xray issueId for ${type} after ${delays.length} attempts`);
11684
12780
  return null;
11685
12781
  }
11686
12782
  // ─── Validation ────────────────────────────────────────────────────────────
@@ -11760,7 +12856,7 @@ async function readXrayMapping(rootDir) {
11760
12856
  const raw = await readJson(filePath);
11761
12857
  const result = XrayMappingFileSchema.safeParse(raw);
11762
12858
  if (!result.success) {
11763
- logger31.warn({ path: filePath, errors: result.error.issues }, "Invalid xray-mapping.json \u2014 returning empty");
12859
+ logger36.warn({ path: filePath, errors: result.error.issues }, "Invalid xray-mapping.json \u2014 returning empty");
11764
12860
  return { projectKey: "", suites: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
11765
12861
  }
11766
12862
  return result.data;
@@ -11770,7 +12866,7 @@ async function writeXrayMapping(rootDir, mapping) {
11770
12866
  await import_fs_extra20.default.ensureDir(import_path29.default.dirname(filePath));
11771
12867
  const data = { ...mapping, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
11772
12868
  await atomicWriteJson(filePath, data);
11773
- logger31.debug({ path: filePath }, "Xray mapping written");
12869
+ logger36.debug({ path: filePath }, "Xray mapping written");
11774
12870
  }
11775
12871
  function findSuiteMapping(mapping, suiteId) {
11776
12872
  return mapping.suites.find((s) => s.suiteId === suiteId);
@@ -11802,7 +12898,7 @@ function upsertCaseMapping(suiteMapping, caseMapping) {
11802
12898
  }
11803
12899
  return { ...suiteMapping, cases };
11804
12900
  }
11805
- var import_path29, import_fs_extra20, logger31, MAPPING_FILE;
12901
+ var import_path29, import_fs_extra20, logger36, MAPPING_FILE;
11806
12902
  var init_xray_store = __esm({
11807
12903
  "src/storage/xray-store.ts"() {
11808
12904
  "use strict";
@@ -11812,7 +12908,7 @@ var init_xray_store = __esm({
11812
12908
  init_xray();
11813
12909
  init_utils();
11814
12910
  init_logger();
11815
- logger31 = createChildLogger("xray-store");
12911
+ logger36 = createChildLogger("xray-store");
11816
12912
  MAPPING_FILE = "xray-mapping.json";
11817
12913
  }
11818
12914
  });
@@ -11833,7 +12929,7 @@ function caseHash(tc) {
11833
12929
  steps: tc.steps.map((s) => ({ instruction: s.instruction, code: s.generatedCode }))
11834
12930
  });
11835
12931
  }
11836
- var import_crypto7, logger32, XrayService;
12932
+ var import_crypto7, logger37, XrayService;
11837
12933
  var init_service = __esm({
11838
12934
  "src/xray/service.ts"() {
11839
12935
  "use strict";
@@ -11844,7 +12940,7 @@ var init_service = __esm({
11844
12940
  init_suite_store();
11845
12941
  init_case_store();
11846
12942
  init_logger();
11847
- logger32 = createChildLogger("xray-service");
12943
+ logger37 = createChildLogger("xray-service");
11848
12944
  XrayService = class {
11849
12945
  client;
11850
12946
  rootDir;
@@ -11952,7 +13048,7 @@ var init_service = __esm({
11952
13048
  const results = [];
11953
13049
  let sm = findSuiteMapping(mapping, suiteId);
11954
13050
  if (sm) {
11955
- logger32.info({ suiteId }, "Suite already mapped in Xray \u2014 delegating to sync");
13051
+ logger37.info({ suiteId }, "Suite already mapped in Xray \u2014 delegating to sync");
11956
13052
  return this.syncSuite(suiteId);
11957
13053
  }
11958
13054
  const folderPath = `/${suite.name}`;
@@ -11981,7 +13077,7 @@ var init_service = __esm({
11981
13077
  await writeXrayMapping(this.rootDir, mapping);
11982
13078
  createdTestIssueIds.push(test.id);
11983
13079
  results.push({ success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" });
11984
- logger32.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
13080
+ logger37.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
11985
13081
  } catch (err) {
11986
13082
  results.push({
11987
13083
  success: false,
@@ -12001,9 +13097,9 @@ var init_service = __esm({
12001
13097
  if (allTestPairs.length > 0) {
12002
13098
  try {
12003
13099
  await this.client.createFolderAndAddTests(suite.name, allTestPairs);
12004
- logger32.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
13100
+ logger37.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
12005
13101
  } catch (err) {
12006
- logger32.warn({ folderPath, err: err instanceof Error ? err.message : String(err) }, "Folder organization failed (non-fatal)");
13102
+ logger37.warn({ folderPath, err: err instanceof Error ? err.message : String(err) }, "Folder organization failed (non-fatal)");
12007
13103
  }
12008
13104
  }
12009
13105
  if (!findSuiteMapping(mapping, suiteId)?.xrayTestSetId) {
@@ -12020,7 +13116,7 @@ var init_service = __esm({
12020
13116
  mapping = upsertSuiteMapping(mapping, sm);
12021
13117
  await writeXrayMapping(this.rootDir, mapping);
12022
13118
  results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
12023
- logger32.info({ suiteId, xrayKey: testSet.key, testCount: createdTestIssueIds.length }, "Test Set created with linked tests");
13119
+ logger37.info({ suiteId, xrayKey: testSet.key, testCount: createdTestIssueIds.length }, "Test Set created with linked tests");
12024
13120
  } catch (err) {
12025
13121
  results.push({
12026
13122
  success: false,
@@ -12089,7 +13185,7 @@ var init_service = __esm({
12089
13185
  { key: caseResult.result.xrayKey, id: caseResult.result.xrayId }
12090
13186
  ]);
12091
13187
  } catch (err) {
12092
- logger32.warn({ err: err instanceof Error ? err.message : String(err) }, "Folder organization failed for single case (non-fatal)");
13188
+ logger37.warn({ err: err instanceof Error ? err.message : String(err) }, "Folder organization failed for single case (non-fatal)");
12093
13189
  }
12094
13190
  }
12095
13191
  if (caseResult.result.xrayId && sm.xrayTestSetIssueId) {
@@ -12099,7 +13195,7 @@ var init_service = __esm({
12099
13195
  [{ key: caseResult.result.xrayKey ?? "", id: caseResult.result.xrayId }]
12100
13196
  );
12101
13197
  } catch (err) {
12102
- logger32.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to link test to test set");
13198
+ logger37.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to link test to test set");
12103
13199
  }
12104
13200
  }
12105
13201
  return this.summarizeResults([caseResult.result]);
@@ -12130,7 +13226,7 @@ var init_service = __esm({
12130
13226
  const updatedSm = upsertCaseMapping(sm, cm);
12131
13227
  const updatedMapping = upsertSuiteMapping(mapping, updatedSm);
12132
13228
  await writeXrayMapping(this.rootDir, updatedMapping);
12133
- logger32.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
13229
+ logger37.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
12134
13230
  return {
12135
13231
  result: { success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" },
12136
13232
  mapping: updatedMapping
@@ -12196,7 +13292,7 @@ var init_service = __esm({
12196
13292
  mapping = upsertSuiteMapping(mapping, sm);
12197
13293
  await writeXrayMapping(this.rootDir, mapping);
12198
13294
  results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
12199
- logger32.info({ suiteId, xrayKey: testSet.key }, "Test Set created during sync (was missing)");
13295
+ logger37.info({ suiteId, xrayKey: testSet.key }, "Test Set created during sync (was missing)");
12200
13296
  } catch (err) {
12201
13297
  results.push({
12202
13298
  success: false,
@@ -12292,11 +13388,11 @@ var init_service = __esm({
12292
13388
  try {
12293
13389
  await this.client.createFolderAndAddTests(suite.name, newTests);
12294
13390
  } catch (folderErr) {
12295
- logger32.warn({ err: folderErr instanceof Error ? folderErr.message : String(folderErr) }, "Folder organization failed for synced tests (non-fatal)");
13391
+ logger37.warn({ err: folderErr instanceof Error ? folderErr.message : String(folderErr) }, "Folder organization failed for synced tests (non-fatal)");
12296
13392
  }
12297
13393
  }
12298
13394
  } catch (err) {
12299
- logger32.warn({ err }, "Failed to link new tests to test set");
13395
+ logger37.warn({ err }, "Failed to link new tests to test set");
12300
13396
  }
12301
13397
  }
12302
13398
  }
@@ -12414,9 +13510,11 @@ async function xrayRoutes(fastify) {
12414
13510
  function requireService(reply) {
12415
13511
  const service = createService();
12416
13512
  if (!service) {
12417
- reply.status(400).send({
13513
+ reply.status(422).send({
12418
13514
  error: "Xray integration not configured",
12419
- details: "Set JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN, and XRAY_PROJECT_KEY in your .env file."
13515
+ detail: "Set JIRA_BASE_URL, JIRA_EMAIL, JIRA_API_TOKEN, and XRAY_PROJECT_KEY in your .env file.",
13516
+ code: "CONFIG_ERROR",
13517
+ featureHint: "XRAY_NOT_CONFIGURED"
12420
13518
  });
12421
13519
  return null;
12422
13520
  }
@@ -12451,7 +13549,7 @@ async function xrayRoutes(fastify) {
12451
13549
  const service = requireService(reply);
12452
13550
  if (!service) return;
12453
13551
  try {
12454
- logger33.info("Creating all suites and cases in Xray");
13552
+ logger38.info("Creating all suites and cases in Xray");
12455
13553
  const result = await service.createAll();
12456
13554
  return reply.send(result);
12457
13555
  } catch (err) {
@@ -12462,7 +13560,7 @@ async function xrayRoutes(fastify) {
12462
13560
  const service = requireService(reply);
12463
13561
  if (!service) return;
12464
13562
  try {
12465
- logger33.info({ suiteId: req.params.suiteId }, "Creating suite in Xray");
13563
+ logger38.info({ suiteId: req.params.suiteId }, "Creating suite in Xray");
12466
13564
  const result = await service.createSuite(req.params.suiteId);
12467
13565
  return reply.send(result);
12468
13566
  } catch (err) {
@@ -12486,7 +13584,7 @@ async function xrayRoutes(fastify) {
12486
13584
  const service = requireService(reply);
12487
13585
  if (!service) return;
12488
13586
  try {
12489
- logger33.info("Syncing all suites and cases with Xray");
13587
+ logger38.info("Syncing all suites and cases with Xray");
12490
13588
  const result = await service.syncAll();
12491
13589
  return reply.send(result);
12492
13590
  } catch (err) {
@@ -12497,7 +13595,7 @@ async function xrayRoutes(fastify) {
12497
13595
  const service = requireService(reply);
12498
13596
  if (!service) return;
12499
13597
  try {
12500
- logger33.info({ suiteId: req.params.suiteId }, "Syncing suite with Xray");
13598
+ logger38.info({ suiteId: req.params.suiteId }, "Syncing suite with Xray");
12501
13599
  const result = await service.syncSuite(req.params.suiteId);
12502
13600
  return reply.send(result);
12503
13601
  } catch (err) {
@@ -12528,7 +13626,7 @@ async function xrayRoutes(fastify) {
12528
13626
  }
12529
13627
  });
12530
13628
  }
12531
- var logger33;
13629
+ var logger38;
12532
13630
  var init_xray2 = __esm({
12533
13631
  "src/server/routes/xray.ts"() {
12534
13632
  "use strict";
@@ -12536,12 +13634,12 @@ var init_xray2 = __esm({
12536
13634
  init_service();
12537
13635
  init_utils2();
12538
13636
  init_logger();
12539
- logger33 = createChildLogger("xray-routes");
13637
+ logger38 = createChildLogger("xray-routes");
12540
13638
  }
12541
13639
  });
12542
13640
 
12543
13641
  // src/git/service.ts
12544
- var import_simple_git, import_fs_extra21, import_path30, logger34, GitService;
13642
+ var import_simple_git, import_fs_extra21, import_path30, logger39, GitService;
12545
13643
  var init_service2 = __esm({
12546
13644
  "src/git/service.ts"() {
12547
13645
  "use strict";
@@ -12550,7 +13648,7 @@ var init_service2 = __esm({
12550
13648
  import_fs_extra21 = __toESM(require("fs-extra"));
12551
13649
  import_path30 = __toESM(require("path"));
12552
13650
  init_logger();
12553
- logger34 = createChildLogger("git-service");
13651
+ logger39 = createChildLogger("git-service");
12554
13652
  GitService = class {
12555
13653
  git;
12556
13654
  rootDir;
@@ -12612,7 +13710,7 @@ var init_service2 = __esm({
12612
13710
  try {
12613
13711
  await this.git.checkout(branch);
12614
13712
  this.broadcastLog("Checkout branch", "done", branch);
12615
- logger34.info({ branch }, "Switched branch");
13713
+ logger39.info({ branch }, "Switched branch");
12616
13714
  } catch (err) {
12617
13715
  const msg = err instanceof Error ? err.message : String(err);
12618
13716
  this.broadcastLog("Checkout branch", "error", msg);
@@ -12630,7 +13728,7 @@ var init_service2 = __esm({
12630
13728
  const hash = result.commit || "unknown";
12631
13729
  this.broadcastLog("Create commit", "done", hash);
12632
13730
  this.broadcastDone("commit");
12633
- logger34.info({ hash, message }, "Commit created");
13731
+ logger39.info({ hash, message }, "Commit created");
12634
13732
  return hash;
12635
13733
  } catch (err) {
12636
13734
  const msg = err instanceof Error ? err.message : String(err);
@@ -12647,7 +13745,7 @@ var init_service2 = __esm({
12647
13745
  await this.git.pull(void 0, void 0, opts);
12648
13746
  this.broadcastLog(label, "done", rebase ? "Rebase successful" : "Pull successful");
12649
13747
  this.broadcastDone("pull");
12650
- logger34.info({ rebase }, "Pull completed");
13748
+ logger39.info({ rebase }, "Pull completed");
12651
13749
  } catch (err) {
12652
13750
  const msg = err instanceof Error ? err.message : String(err);
12653
13751
  if (msg.includes("CONFLICT") || msg.includes("conflict") || msg.includes("could not apply")) {
@@ -12669,7 +13767,7 @@ var init_service2 = __esm({
12669
13767
  await this.git.push();
12670
13768
  this.broadcastLog("Push to origin", "done");
12671
13769
  this.broadcastDone("push");
12672
- logger34.info("Push completed");
13770
+ logger39.info("Push completed");
12673
13771
  } catch (err) {
12674
13772
  const msg = err instanceof Error ? err.message : String(err);
12675
13773
  this.broadcastLog("Push to origin", "error", msg);
@@ -12716,7 +13814,7 @@ var init_service2 = __esm({
12716
13814
  throw pushErr;
12717
13815
  }
12718
13816
  this.broadcastDone("full-sync");
12719
- logger34.info("Full sync completed");
13817
+ logger39.info("Full sync completed");
12720
13818
  } catch (err) {
12721
13819
  this.broadcastDone("full-sync");
12722
13820
  throw err;
@@ -12778,7 +13876,7 @@ var init_service2 = __esm({
12778
13876
  const fullPath = import_path30.default.join(this.rootDir, filePath);
12779
13877
  await import_fs_extra21.default.writeFile(fullPath, resolvedContent, "utf-8");
12780
13878
  await this.git.add(filePath);
12781
- logger34.info({ filePath }, "Conflict resolved");
13879
+ logger39.info({ filePath }, "Conflict resolved");
12782
13880
  }
12783
13881
  async continueRebase() {
12784
13882
  this.broadcastLog("Continuing rebase...", "running");
@@ -12813,7 +13911,8 @@ function fakePageContext(content) {
12813
13911
  title: "Git Operations",
12814
13912
  interactiveElements: "",
12815
13913
  htmlSnapshot: content,
12816
- previousSteps: []
13914
+ previousSteps: [],
13915
+ variables: {}
12817
13916
  };
12818
13917
  }
12819
13918
  async function generateCommitMessage(diff) {
@@ -12852,10 +13951,10 @@ async function generateCommitMessage(diff) {
12852
13951
  cleaned = cleaned.replace(/^["'`]+|["'`]+$/g, "");
12853
13952
  cleaned = cleaned.replace(/^(commit message:?\s*)/i, "");
12854
13953
  cleaned = cleaned.split("\n")[0].trim();
12855
- logger35.info({ messageLength: cleaned.length }, "AI commit message generated");
13954
+ logger40.info({ messageLength: cleaned.length }, "AI commit message generated");
12856
13955
  return cleaned || "chore: update files";
12857
13956
  } catch (err) {
12858
- logger35.warn({ err: err instanceof Error ? err.message : String(err) }, "AI commit message generation failed");
13957
+ logger40.warn({ err: err instanceof Error ? err.message : String(err) }, "AI commit message generation failed");
12859
13958
  return "chore: update files";
12860
13959
  }
12861
13960
  }
@@ -12882,21 +13981,21 @@ ${theirs}`);
12882
13981
  const provider = createProvider(void 0, { temperature: 0.4 });
12883
13982
  let result = await provider.generateCode(instruction, context);
12884
13983
  result = result.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "");
12885
- logger35.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
13984
+ logger40.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
12886
13985
  return result;
12887
13986
  } catch (err) {
12888
- logger35.warn({ filePath, err: err instanceof Error ? err.message : String(err) }, "AI conflict resolution failed");
13987
+ logger40.warn({ filePath, err: err instanceof Error ? err.message : String(err) }, "AI conflict resolution failed");
12889
13988
  throw err;
12890
13989
  }
12891
13990
  }
12892
- var logger35;
13991
+ var logger40;
12893
13992
  var init_ai = __esm({
12894
13993
  "src/git/ai.ts"() {
12895
13994
  "use strict";
12896
13995
  init_cjs_shims();
12897
13996
  init_router();
12898
13997
  init_logger();
12899
- logger35 = createChildLogger("git-ai");
13998
+ logger40 = createChildLogger("git-ai");
12900
13999
  }
12901
14000
  });
12902
14001
 
@@ -12913,7 +14012,7 @@ async function gitRoutes(fastify) {
12913
14012
  _isGitRepo = true;
12914
14013
  } catch {
12915
14014
  _isGitRepo = false;
12916
- logger36.info("Project directory is not a git repository \u2014 git features disabled");
14015
+ logger41.info("Project directory is not a git repository \u2014 git features disabled");
12917
14016
  }
12918
14017
  return _isGitRepo;
12919
14018
  };
@@ -13116,7 +14215,7 @@ async function gitRoutes(fastify) {
13116
14215
  }
13117
14216
  });
13118
14217
  }
13119
- var import_path31, import_zod18, logger36, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
14218
+ var import_path31, import_zod18, logger41, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
13120
14219
  var init_git = __esm({
13121
14220
  "src/server/routes/git.ts"() {
13122
14221
  "use strict";
@@ -13127,7 +14226,7 @@ var init_git = __esm({
13127
14226
  init_ai();
13128
14227
  init_utils2();
13129
14228
  init_logger();
13130
- logger36 = createChildLogger("git-routes");
14229
+ logger41 = createChildLogger("git-routes");
13131
14230
  CheckoutBody = import_zod18.z.object({ branch: import_zod18.z.string().min(1) });
13132
14231
  CommitBody = import_zod18.z.object({ message: import_zod18.z.string().min(1) });
13133
14232
  PullBody = import_zod18.z.object({ rebase: import_zod18.z.boolean().optional() });
@@ -13234,7 +14333,7 @@ async function dataFileRoutes(fastify) {
13234
14333
  }
13235
14334
  } catch {
13236
14335
  }
13237
- logger37.info({ filename: safeName, rowCount }, "Data file uploaded");
14336
+ logger42.info({ filename: safeName, rowCount }, "Data file uploaded");
13238
14337
  return reply.status(201).send({
13239
14338
  filename: safeName,
13240
14339
  path: `data/${safeName}`,
@@ -13252,7 +14351,7 @@ async function dataFileRoutes(fastify) {
13252
14351
  const content = String(body.content ?? "");
13253
14352
  await import_fs_extra22.default.ensureDir(dataDir);
13254
14353
  await import_fs_extra22.default.writeFile(filePath, content, "utf8");
13255
- logger37.info({ filename }, "Data file updated");
14354
+ logger42.info({ filename }, "Data file updated");
13256
14355
  return reply.send({ filename, path: `data/${filename}` });
13257
14356
  } catch (err) {
13258
14357
  return sendError(reply, 500, "Failed to update data file", err);
@@ -13266,14 +14365,14 @@ async function dataFileRoutes(fastify) {
13266
14365
  return reply.status(404).send({ error: `Data file "${filename}" not found` });
13267
14366
  }
13268
14367
  await import_fs_extra22.default.remove(filePath);
13269
- logger37.info({ filename }, "Data file deleted");
14368
+ logger42.info({ filename }, "Data file deleted");
13270
14369
  return reply.status(204).send();
13271
14370
  } catch (err) {
13272
14371
  return sendError(reply, 500, "Failed to delete data file", err);
13273
14372
  }
13274
14373
  });
13275
14374
  }
13276
- var import_path32, import_fs_extra22, logger37, ALLOWED_EXTENSIONS;
14375
+ var import_path32, import_fs_extra22, logger42, ALLOWED_EXTENSIONS;
13277
14376
  var init_data_files = __esm({
13278
14377
  "src/server/routes/data-files.ts"() {
13279
14378
  "use strict";
@@ -13282,7 +14381,7 @@ var init_data_files = __esm({
13282
14381
  import_fs_extra22 = __toESM(require("fs-extra"));
13283
14382
  init_utils2();
13284
14383
  init_logger();
13285
- logger37 = createChildLogger("routes/data-files");
14384
+ logger42 = createChildLogger("routes/data-files");
13286
14385
  ALLOWED_EXTENSIONS = [".json", ".csv"];
13287
14386
  }
13288
14387
  });
@@ -13820,15 +14919,15 @@ async function startServer(options) {
13820
14919
  await fastify.register(stepLibraryRoutes, { prefix: "/api" });
13821
14920
  await registerStatic(fastify, rootDir);
13822
14921
  await fastify.listen({ port, host: "127.0.0.1" });
13823
- logger38.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
14922
+ logger43.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
13824
14923
  return {
13825
14924
  async close() {
13826
14925
  await fastify.close();
13827
- logger38.info("Server closed");
14926
+ logger43.info("Server closed");
13828
14927
  }
13829
14928
  };
13830
14929
  }
13831
- var import_fastify, import_cors, import_websocket, logger38;
14930
+ var import_fastify, import_cors, import_websocket, logger43;
13832
14931
  var init_server = __esm({
13833
14932
  "src/server/index.ts"() {
13834
14933
  "use strict";
@@ -13859,7 +14958,7 @@ var init_server = __esm({
13859
14958
  init_flakiness();
13860
14959
  init_step_library();
13861
14960
  init_logger();
13862
- logger38 = createChildLogger("server");
14961
+ logger43 = createChildLogger("server");
13863
14962
  }
13864
14963
  });
13865
14964
 
@@ -13875,13 +14974,11 @@ async function runStudio(options = {}) {
13875
14974
  });
13876
14975
  process.stdout.write("\n");
13877
14976
  printInfo("Starting Assuremind Studio\u2026\n");
13878
- try {
13879
- validateEnv();
13880
- } catch (err) {
13881
- printError(
13882
- err instanceof ConfigError ? err.message : `Environment validation failed: ${err instanceof Error ? err.message : String(err)}`
14977
+ const env = tryGetEnv();
14978
+ if (!env) {
14979
+ printWarn(
14980
+ "AI provider not configured. The Studio will start, but AI features\n (test generation, code generation, self-healing) require a valid\n AI_PROVIDER and its API key in your .env file.\n See .env.example for details.\n"
13883
14981
  );
13884
- process.exit(1);
13885
14982
  }
13886
14983
  let port;
13887
14984
  try {