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/CONTRIBUTING.md +5 -4
- package/LICENSE +20 -20
- package/README.md +93 -310
- package/dist/cli/index.js +1401 -304
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +59 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +43 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +43 -3
- package/dist/index.mjs.map +1 -1
- package/docs/CLI-REFERENCE.md +18 -1
- package/docs/GETTING-STARTED.md +9 -2
- package/docs/STUDIO.md +37 -3
- package/package.json +3 -1
- package/templates/{AUTOMIND.md → ASSUREMIND.md} +31 -3
- package/templates/docs/CLI-REFERENCE.md +19 -2
- package/templates/docs/GETTING-STARTED.md +9 -3
- package/templates/docs/STUDIO.md +43 -2
- package/ui/dist/assets/index-By2Hw5l2.css +1 -0
- package/ui/dist/assets/index-DaQ-JHje.js +819 -0
- package/ui/dist/favicon.svg +36 -36
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-CdtAorWT.js +0 -819
- package/ui/dist/assets/index-KjpMCzao.css +0 -1
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(
|
|
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: "
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2463
|
-
const code = await provider.generateCode(instruction,
|
|
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
|
-
|
|
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:
|
|
3424
|
+
return { system: SYSTEM_PROMPT2, user };
|
|
2593
3425
|
}
|
|
2594
|
-
var
|
|
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
|
-
|
|
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:
|
|
3577
|
+
return { system: SYSTEM_PROMPT3, user };
|
|
2746
3578
|
}
|
|
2747
|
-
var OUTLINE_SYSTEM, OUTLINE_SCHEMA, STEPS_SYSTEM, STEPS_SCHEMA,
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3686
|
+
return { system: SYSTEM_PROMPT4, user };
|
|
2838
3687
|
}
|
|
2839
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
3747
|
+
return { system: SYSTEM_PROMPT5, user };
|
|
2890
3748
|
}
|
|
2891
|
-
var
|
|
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
|
-
|
|
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
|
-
|
|
5084
|
+
logger16.info({ fast: fastProviderName, primary: String(env.AI_PROVIDER) }, "Tiered mode active");
|
|
4227
5085
|
} catch (err) {
|
|
4228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5170
|
+
logger16.info({ suiteId }, "Auto code generation complete");
|
|
4304
5171
|
} catch (codeGenErr) {
|
|
4305
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5243
|
+
logger16.info({ jobId, completed, failed, total }, "Bulk story generation finished");
|
|
4372
5244
|
return doneEvent;
|
|
4373
5245
|
}
|
|
4374
|
-
var import_path12, import_fs_extra10,
|
|
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
|
-
|
|
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
|
|
4456
|
-
const
|
|
4457
|
-
|
|
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
|
|
4488
|
-
|
|
4489
|
-
)
|
|
4490
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6494
|
+
logger20.info({ runId }, "Run result deleted");
|
|
5602
6495
|
}
|
|
5603
|
-
var import_path18, import_fs_extra13,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6840
|
+
logger22.debug({ err }, "Tracing stop failed \u2014 ignoring");
|
|
5784
6841
|
}
|
|
5785
6842
|
}
|
|
5786
6843
|
await context.close().catch((err) => {
|
|
5787
|
-
|
|
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
|
-
|
|
6853
|
+
logger22.debug({ browser: name }, "Browser closed");
|
|
5797
6854
|
} catch (err) {
|
|
5798
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7009
|
+
logger24.info({ runId, count: this.events.length }, "Healing report written");
|
|
5953
7010
|
} catch (err) {
|
|
5954
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
7240
|
+
logger25.debug({ logFile: logFileName }, "Log file copied to Allure results");
|
|
6184
7241
|
} catch {
|
|
6185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
6662
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7993
|
+
logger27.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
|
|
6937
7994
|
continue;
|
|
6938
7995
|
}
|
|
6939
|
-
|
|
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
|
-
|
|
8009
|
+
logger27.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
|
|
6953
8010
|
return result;
|
|
6954
8011
|
}
|
|
6955
8012
|
} catch (err) {
|
|
6956
|
-
|
|
8013
|
+
logger27.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
|
|
6957
8014
|
}
|
|
6958
8015
|
}
|
|
6959
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
10669
|
+
logger30.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
|
|
9613
10670
|
continue;
|
|
9614
10671
|
}
|
|
9615
10672
|
} catch (err) {
|
|
9616
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11223
|
+
logger30.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
|
|
10130
11224
|
return resolved;
|
|
10131
11225
|
} catch (err) {
|
|
10132
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11365
|
+
logger31.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
|
|
10270
11366
|
}).catch((err) => {
|
|
10271
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11629
|
+
logger32.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
|
|
10534
11630
|
return { title, baseUrl, endpoints };
|
|
10535
11631
|
}
|
|
10536
|
-
var import_js_yaml,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12335
|
+
logger35.warn({ warnings, jiraKey }, "Xray createTest warnings");
|
|
11240
12336
|
}
|
|
11241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12414
|
+
logger35.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
|
|
11319
12415
|
}
|
|
11320
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12494
|
+
logger35.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
|
|
11399
12495
|
break;
|
|
11400
12496
|
}
|
|
11401
|
-
|
|
12497
|
+
logger35.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
|
|
11402
12498
|
}
|
|
11403
12499
|
}
|
|
11404
12500
|
if (added > 0) {
|
|
11405
|
-
|
|
12501
|
+
logger35.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
|
|
11406
12502
|
} else if (steps.length > 0) {
|
|
11407
|
-
|
|
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
|
-
|
|
12516
|
+
logger35.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
|
|
11421
12517
|
} catch (err) {
|
|
11422
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12566
|
+
logger35.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
|
|
11471
12567
|
} catch (err) {
|
|
11472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12689
|
+
logger35.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
|
|
11594
12690
|
return;
|
|
11595
12691
|
}
|
|
11596
|
-
|
|
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
|
-
|
|
12706
|
+
logger35.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
|
|
11611
12707
|
} catch (addErr) {
|
|
11612
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
12771
|
+
logger35.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
|
|
11676
12772
|
return items[0].issueId;
|
|
11677
12773
|
}
|
|
11678
|
-
|
|
12774
|
+
logger35.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
|
|
11679
12775
|
} catch (err) {
|
|
11680
|
-
|
|
12776
|
+
logger35.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
|
|
11681
12777
|
}
|
|
11682
12778
|
}
|
|
11683
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13100
|
+
logger37.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
|
|
12005
13101
|
} catch (err) {
|
|
12006
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
13513
|
+
reply.status(422).send({
|
|
12418
13514
|
error: "Xray integration not configured",
|
|
12419
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13954
|
+
logger40.info({ messageLength: cleaned.length }, "AI commit message generated");
|
|
12856
13955
|
return cleaned || "chore: update files";
|
|
12857
13956
|
} catch (err) {
|
|
12858
|
-
|
|
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
|
-
|
|
13984
|
+
logger40.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
|
|
12886
13985
|
return result;
|
|
12887
13986
|
} catch (err) {
|
|
12888
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14926
|
+
logger43.info("Server closed");
|
|
13828
14927
|
}
|
|
13829
14928
|
};
|
|
13830
14929
|
}
|
|
13831
|
-
var import_fastify, import_cors, import_websocket,
|
|
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
|
-
|
|
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
|
-
|
|
13879
|
-
|
|
13880
|
-
|
|
13881
|
-
|
|
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 {
|