assuremind 1.1.2 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CONTRIBUTING.md +13 -5
- package/README.md +89 -1
- package/dist/cli/index.js +2055 -410
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.mts +151 -12
- package/dist/index.d.ts +151 -12
- package/dist/index.js +49 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49 -2
- package/dist/index.mjs.map +1 -1
- package/docs/CLI-REFERENCE.md +104 -0
- package/docs/GETTING-STARTED.md +64 -3
- package/docs/STUDIO.md +186 -0
- package/package.json +1 -1
- package/ui/dist/assets/index-DTtYd1hD.js +837 -0
- package/ui/dist/assets/index-lOAh29q9.css +1 -0
- package/ui/dist/assuremind-logo.png +0 -0
- package/ui/dist/favicon.svg +8 -36
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-By2Hw5l2.css +0 -1
- package/ui/dist/assets/index-DaQ-JHje.js +0 -819
package/dist/cli/index.js
CHANGED
|
@@ -186,6 +186,44 @@ var init_config = __esm({
|
|
|
186
186
|
activeProfile: import_zod.z.string().optional(),
|
|
187
187
|
/** Playwright device descriptor name for emulation (e.g. 'iPhone 15 Pro'). */
|
|
188
188
|
device: import_zod.z.string().optional(),
|
|
189
|
+
/**
|
|
190
|
+
* RAG (Retrieval-Augmented Generation) — semantic memory from past runs.
|
|
191
|
+
*
|
|
192
|
+
* The AI learns from every generation and healing event:
|
|
193
|
+
* - Code Corpus: retrieve similar past steps during generation
|
|
194
|
+
* - Healing Corpus: retrieve similar past fixes during self-healing
|
|
195
|
+
* - Error Catalog: track recurring error patterns for preventive generation
|
|
196
|
+
*/
|
|
197
|
+
rag: import_zod.z.object({
|
|
198
|
+
/** Master switch for RAG memory. */
|
|
199
|
+
enabled: import_zod.z.boolean(),
|
|
200
|
+
/** Code Corpus — semantic retrieval of similar instruction→code mappings. */
|
|
201
|
+
codeCorpus: import_zod.z.object({
|
|
202
|
+
enabled: import_zod.z.boolean(),
|
|
203
|
+
maxEntries: import_zod.z.number().int().positive(),
|
|
204
|
+
similarityThreshold: import_zod.z.number().min(0).max(1),
|
|
205
|
+
directUseThreshold: import_zod.z.number().min(0).max(1)
|
|
206
|
+
}),
|
|
207
|
+
/** Healing Corpus — retrieve similar past healing events during self-healing. */
|
|
208
|
+
healingCorpus: import_zod.z.object({
|
|
209
|
+
enabled: import_zod.z.boolean(),
|
|
210
|
+
maxEntries: import_zod.z.number().int().positive(),
|
|
211
|
+
similarityThreshold: import_zod.z.number().min(0).max(1)
|
|
212
|
+
}),
|
|
213
|
+
/** Error Catalog — aggregate recurring error patterns per URL. */
|
|
214
|
+
errorCatalog: import_zod.z.object({
|
|
215
|
+
enabled: import_zod.z.boolean(),
|
|
216
|
+
maxEntries: import_zod.z.number().int().positive()
|
|
217
|
+
}),
|
|
218
|
+
/** Embedding strategy: 'tfidf' (local, free, offline). */
|
|
219
|
+
embedder: import_zod.z.literal("tfidf")
|
|
220
|
+
}).default({
|
|
221
|
+
enabled: true,
|
|
222
|
+
codeCorpus: { enabled: true, maxEntries: 500, similarityThreshold: 0.65, directUseThreshold: 0.9 },
|
|
223
|
+
healingCorpus: { enabled: true, maxEntries: 300, similarityThreshold: 0.6 },
|
|
224
|
+
errorCatalog: { enabled: true, maxEntries: 200 },
|
|
225
|
+
embedder: "tfidf"
|
|
226
|
+
}),
|
|
189
227
|
/**
|
|
190
228
|
* Playwright MCP integration for AI-sighted code generation.
|
|
191
229
|
*
|
|
@@ -251,6 +289,13 @@ var init_config = __esm({
|
|
|
251
289
|
},
|
|
252
290
|
studioPort: 4400,
|
|
253
291
|
profiles: [],
|
|
292
|
+
rag: {
|
|
293
|
+
enabled: true,
|
|
294
|
+
codeCorpus: { enabled: true, maxEntries: 500, similarityThreshold: 0.65, directUseThreshold: 0.9 },
|
|
295
|
+
healingCorpus: { enabled: true, maxEntries: 300, similarityThreshold: 0.6 },
|
|
296
|
+
errorCatalog: { enabled: true, maxEntries: 200 },
|
|
297
|
+
embedder: "tfidf"
|
|
298
|
+
},
|
|
254
299
|
mcp: {
|
|
255
300
|
enabled: true,
|
|
256
301
|
headless: true,
|
|
@@ -1107,7 +1152,7 @@ var init_suite = __esm({
|
|
|
1107
1152
|
order: import_zod3.z.number().int().positive(),
|
|
1108
1153
|
instruction: import_zod3.z.string().min(1),
|
|
1109
1154
|
generatedCode: import_zod3.z.string(),
|
|
1110
|
-
strategy: import_zod3.z.enum(["template", "cache", "batch", "fast", "primary"]),
|
|
1155
|
+
strategy: import_zod3.z.enum(["template", "cache", "rag", "batch", "fast", "primary", "recorder"]),
|
|
1111
1156
|
stepType: import_zod3.z.enum(["ui", "api", "mock"]).default("ui"),
|
|
1112
1157
|
lastHealed: import_zod3.z.string().nullable(),
|
|
1113
1158
|
timeout: import_zod3.z.number().int().positive().optional(),
|
|
@@ -1115,8 +1160,10 @@ var init_suite = __esm({
|
|
|
1115
1160
|
mockUrl: import_zod3.z.string().optional(),
|
|
1116
1161
|
mockResponse: import_zod3.z.string().optional(),
|
|
1117
1162
|
mockStatus: import_zod3.z.number().int().optional(),
|
|
1118
|
-
runAudit: import_zod3.z.boolean().optional()
|
|
1163
|
+
runAudit: import_zod3.z.boolean().optional(),
|
|
1119
1164
|
// Mark this step as a Lighthouse audit checkpoint
|
|
1165
|
+
soft: import_zod3.z.boolean().optional()
|
|
1166
|
+
// Use expect.soft() — test continues on assertion failure
|
|
1120
1167
|
});
|
|
1121
1168
|
DataSourceSchema = import_zod3.z.object({
|
|
1122
1169
|
type: import_zod3.z.enum(["inline", "json-file", "csv-file"]),
|
|
@@ -2140,6 +2187,27 @@ var init_template_engine = __esm({
|
|
|
2140
2187
|
regex: /^(?:verify|check)\s+(?:the\s+)?["'](.+?)["']\s+(?:field|input)\s+(?:has value|contains|equals)\s+["'](.+?)["']$/i,
|
|
2141
2188
|
gen: (m) => `await expect(page.getByLabel('${m[1]}')).toHaveValue('${m[2]}');`
|
|
2142
2189
|
},
|
|
2190
|
+
// ── Soft Assertions (expect.soft — test continues on failure) ──
|
|
2191
|
+
{
|
|
2192
|
+
regex: /^soft\s+(?:verify|check|assert|ensure|confirm)\s+(?:that\s+)?["'](.+?)["']\s+(?:is\s+)?(?:visible|shown|displayed|present|appears)$/i,
|
|
2193
|
+
gen: (m) => `await expect.soft(page.getByText('${m[1]}')).toBeVisible();`
|
|
2194
|
+
},
|
|
2195
|
+
{
|
|
2196
|
+
regex: /^soft\s+(?:verify|check|assert)\s+(?:that\s+)?["'](.+?)["']\s+(?:is\s+)?(?:not visible|hidden|not shown|gone|disappeared|not present)$/i,
|
|
2197
|
+
gen: (m) => `await expect.soft(page.getByText('${m[1]}')).not.toBeVisible();`
|
|
2198
|
+
},
|
|
2199
|
+
{
|
|
2200
|
+
regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?(?:page\s+)?title\s+(?:is|contains)\s+["'](.+?)["']$/i,
|
|
2201
|
+
gen: (m) => `await expect.soft(page).toHaveTitle(/${m[1]}/);`
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
2204
|
+
regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?url\s+(?:is|contains|matches)\s+["'](.+?)["']$/i,
|
|
2205
|
+
gen: (m) => `await expect.soft(page).toHaveURL(/${(m[1] ?? "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/);`
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
regex: /^soft\s+(?:verify|check)\s+(?:the\s+)?["'](.+?)["']\s+(?:field|input)\s+(?:has value|contains|equals)\s+["'](.+?)["']$/i,
|
|
2209
|
+
gen: (m) => `await expect.soft(page.getByLabel('${m[1]}')).toHaveValue('${m[2]}');`
|
|
2210
|
+
},
|
|
2143
2211
|
// ── Keyboard ──
|
|
2144
2212
|
{
|
|
2145
2213
|
regex: /^press\s+(.+)$/i,
|
|
@@ -2818,9 +2886,11 @@ var init_smart_router = __esm({
|
|
|
2818
2886
|
fastProvider;
|
|
2819
2887
|
mcpSession;
|
|
2820
2888
|
actThenScript;
|
|
2889
|
+
ragManager;
|
|
2821
2890
|
stats = {
|
|
2822
2891
|
template: 0,
|
|
2823
2892
|
cache: 0,
|
|
2893
|
+
rag: 0,
|
|
2824
2894
|
batch: 0,
|
|
2825
2895
|
fast: 0,
|
|
2826
2896
|
primary: 0,
|
|
@@ -2831,6 +2901,7 @@ var init_smart_router = __esm({
|
|
|
2831
2901
|
this.fastProvider = config.fastProvider ?? null;
|
|
2832
2902
|
this.mcpSession = config.mcpSession ?? null;
|
|
2833
2903
|
this.actThenScript = config.actThenScript ?? false;
|
|
2904
|
+
this.ragManager = config.ragManager ?? null;
|
|
2834
2905
|
this.cache = config.cache ?? new CodeCache();
|
|
2835
2906
|
this.templateEngine = new TemplateEngine();
|
|
2836
2907
|
this.classifier = new ComplexityClassifier();
|
|
@@ -2856,7 +2927,33 @@ var init_smart_router = __esm({
|
|
|
2856
2927
|
const fixed = fixButtonSelectors(instruction, fixAntiPatterns(cached.code));
|
|
2857
2928
|
return { code: fixed, strategy: "cache", cost: 0, model: "local" };
|
|
2858
2929
|
}
|
|
2930
|
+
let ragExamples = [];
|
|
2931
|
+
let errorWarnings = [];
|
|
2932
|
+
if (this.ragManager) {
|
|
2933
|
+
try {
|
|
2934
|
+
const matches = this.ragManager.codeCorpus.retrieve(instruction, pageContext.url);
|
|
2935
|
+
if (matches.length > 0 && matches[0].score >= 0.9) {
|
|
2936
|
+
const best = matches[0];
|
|
2937
|
+
this.stats.rag++;
|
|
2938
|
+
logger13.info({ instruction, score: best.score, matchedInstruction: best.instruction }, "RAG direct hit");
|
|
2939
|
+
const fixed = fixButtonSelectors(instruction, fixAntiPatterns(best.code));
|
|
2940
|
+
this.cache.set(cacheKey, fixed, pageContext.url);
|
|
2941
|
+
return { code: fixed, strategy: "rag", cost: 0, model: "local" };
|
|
2942
|
+
}
|
|
2943
|
+
ragExamples = matches.filter((m) => m.score >= 0.65);
|
|
2944
|
+
if (ragExamples.length > 0) {
|
|
2945
|
+
logger13.debug({ instruction, examples: ragExamples.length, topScore: ragExamples[0].score }, "RAG examples found");
|
|
2946
|
+
}
|
|
2947
|
+
errorWarnings = this.ragManager.errorCatalog.getWarnings(pageContext.url);
|
|
2948
|
+
if (errorWarnings.length > 0) {
|
|
2949
|
+
logger13.debug({ url: pageContext.url, warnings: errorWarnings.length }, "RAG error warnings");
|
|
2950
|
+
}
|
|
2951
|
+
} catch (err) {
|
|
2952
|
+
logger13.debug({ err }, "RAG retrieval failed \u2014 continuing without memory");
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2859
2955
|
const enrichedContext = this.mcpSession ? await this.enrichWithMcp(instruction, pageContext) : pageContext;
|
|
2956
|
+
const contextWithRag = ragExamples.length > 0 || errorWarnings.length > 0 ? { ...enrichedContext, _ragExamples: ragExamples, _errorWarnings: errorWarnings } : enrichedContext;
|
|
2860
2957
|
if (this.actThenScript && this.mcpSession && enrichedContext.accessibilityTree) {
|
|
2861
2958
|
const actResult = await this.tryActThenScript(instruction, enrichedContext);
|
|
2862
2959
|
if (actResult !== null) {
|
|
@@ -2870,8 +2967,8 @@ var init_smart_router = __esm({
|
|
|
2870
2967
|
const usesFast = this.fastProvider !== null && complexity !== "complex";
|
|
2871
2968
|
const provider = usesFast ? this.fastProvider : this.primaryProvider;
|
|
2872
2969
|
const strategy = usesFast ? "fast" : "primary";
|
|
2873
|
-
logger13.debug({ instruction, complexity, strategy }, "AI call");
|
|
2874
|
-
const code = await provider.generateCode(instruction,
|
|
2970
|
+
logger13.debug({ instruction, complexity, strategy, ragExamples: ragExamples.length, errorWarnings: errorWarnings.length }, "AI call");
|
|
2971
|
+
const code = await provider.generateCode(instruction, contextWithRag);
|
|
2875
2972
|
this.cache.set(cacheKey, code, pageContext.url);
|
|
2876
2973
|
if (usesFast) {
|
|
2877
2974
|
this.stats.fast++;
|
|
@@ -2910,7 +3007,7 @@ var init_smart_router = __esm({
|
|
|
2910
3007
|
}
|
|
2911
3008
|
// ─── Stats (shown in Studio dashboard) ───────────────────────────────────
|
|
2912
3009
|
getStats() {
|
|
2913
|
-
const freeCalls = this.stats.template + this.stats.cache;
|
|
3010
|
+
const freeCalls = this.stats.template + this.stats.cache + this.stats.rag;
|
|
2914
3011
|
return {
|
|
2915
3012
|
...this.stats,
|
|
2916
3013
|
freeCalls,
|
|
@@ -2930,6 +3027,10 @@ var init_smart_router = __esm({
|
|
|
2930
3027
|
getMcpSession() {
|
|
2931
3028
|
return this.mcpSession;
|
|
2932
3029
|
}
|
|
3030
|
+
/** Exposes the RAG manager for feedback loop (executor records pass/fail). */
|
|
3031
|
+
getRagManager() {
|
|
3032
|
+
return this.ragManager;
|
|
3033
|
+
}
|
|
2933
3034
|
// ─── MCP enrichment ─────────────────────────────────────────────────────────
|
|
2934
3035
|
/**
|
|
2935
3036
|
* Enrich a PageContext with live accessibility data from the MCP browser.
|
|
@@ -3408,6 +3509,17 @@ ${context.interactiveElements || " None captured"}`;
|
|
|
3408
3509
|
const accessibilitySection = context.accessibilityTree ? `
|
|
3409
3510
|
Full accessibility tree (for additional context):
|
|
3410
3511
|
${context.accessibilityTree.slice(0, 4e3)}` : "";
|
|
3512
|
+
const ragCtx = context;
|
|
3513
|
+
const ragExamples = ragCtx._ragExamples ?? [];
|
|
3514
|
+
const errorWarnings = ragCtx._errorWarnings ?? [];
|
|
3515
|
+
const ragSection = ragExamples.length > 0 ? `
|
|
3516
|
+
Similar past steps that worked (use as reference \u2014 adapt, don't copy blindly):
|
|
3517
|
+
${ragExamples.slice(0, 3).map((ex, i) => ` ${i + 1}. "${ex.instruction}" (similarity: ${(ex.score * 100).toFixed(0)}%)
|
|
3518
|
+
${ex.code}`).join("\n")}` : "";
|
|
3519
|
+
const errorSection = errorWarnings.length > 0 ? `
|
|
3520
|
+
Known issues on this page (avoid these patterns):
|
|
3521
|
+
${errorWarnings.slice(0, 3).map((w, i) => ` ${i + 1}. ${w.errorPattern} (occurred ${w.frequency}x)${w.suggestedFix ? `
|
|
3522
|
+
Suggested fix: ${w.suggestedFix}` : ""}`).join("\n")}` : "";
|
|
3411
3523
|
const user = `Current page:
|
|
3412
3524
|
URL: ${context.url}
|
|
3413
3525
|
Title: ${context.title}
|
|
@@ -3420,7 +3532,7 @@ ${prevSteps}
|
|
|
3420
3532
|
Runtime variables:
|
|
3421
3533
|
${vars}
|
|
3422
3534
|
|
|
3423
|
-
Step to implement: "${instruction}"`;
|
|
3535
|
+
Step to implement: "${instruction}"${ragSection}${errorSection}`;
|
|
3424
3536
|
return { system: SYSTEM_PROMPT2, user };
|
|
3425
3537
|
}
|
|
3426
3538
|
var SYSTEM_PROMPT2;
|
|
@@ -3533,6 +3645,12 @@ Input value: await expect(page.getByLabel('Email')).toHaveValue('user@exampl
|
|
|
3533
3645
|
Not visible: await expect(page.getByText('Error')).not.toBeVisible()
|
|
3534
3646
|
Count: await expect(page.getByRole('listitem')).toHaveCount(5)
|
|
3535
3647
|
|
|
3648
|
+
\u2501\u2501\u2501 SOFT ASSERTIONS (test continues on failure, failures collected at end) \u2501\u2501\u2501
|
|
3649
|
+
When the instruction starts with "Soft verify" or "Soft assert" or "Soft check", use expect.soft() instead of expect():
|
|
3650
|
+
await expect.soft(page).toHaveURL(/dashboard/)
|
|
3651
|
+
await expect.soft(page.getByText('Welcome')).toBeVisible()
|
|
3652
|
+
await expect.soft(page.getByLabel('Email')).toHaveValue('user@example.com')
|
|
3653
|
+
|
|
3536
3654
|
For "verify user is on X page" \u2192 use toHaveURL() or check for a heading/element specific to that page.`;
|
|
3537
3655
|
}
|
|
3538
3656
|
});
|
|
@@ -3821,7 +3939,12 @@ Input value: await expect(page.getByLabel('Email')).toHaveValue('user@test.c
|
|
|
3821
3939
|
Not visible: await expect(page.getByText('Error')).not.toBeVisible()
|
|
3822
3940
|
|
|
3823
3941
|
"Verify user is on X page" \u2192 await expect(page).toHaveURL(/x-page-path/)
|
|
3824
|
-
OR await expect(page.getByRole('heading', { name: 'X' })).toBeVisible()
|
|
3942
|
+
OR await expect(page.getByRole('heading', { name: 'X' })).toBeVisible()
|
|
3943
|
+
|
|
3944
|
+
\u2501\u2501\u2501 SOFT ASSERTIONS (test continues on failure) \u2501\u2501\u2501
|
|
3945
|
+
When instruction starts with "Soft verify/assert/check", use expect.soft():
|
|
3946
|
+
await expect.soft(page.getByText('Welcome')).toBeVisible()
|
|
3947
|
+
await expect.soft(page).toHaveURL(/dashboard/)`;
|
|
3825
3948
|
}
|
|
3826
3949
|
});
|
|
3827
3950
|
|
|
@@ -5072,7 +5195,7 @@ function createProvider(overrideProvider, options) {
|
|
|
5072
5195
|
);
|
|
5073
5196
|
}
|
|
5074
5197
|
}
|
|
5075
|
-
function createSmartRouter(rootDir, mcpSession, actThenScript) {
|
|
5198
|
+
function createSmartRouter(rootDir, mcpSession, actThenScript, ragManager) {
|
|
5076
5199
|
const env = validateEnv();
|
|
5077
5200
|
const primary = createProvider();
|
|
5078
5201
|
let fast;
|
|
@@ -5089,7 +5212,7 @@ function createSmartRouter(rootDir, mcpSession, actThenScript) {
|
|
|
5089
5212
|
}
|
|
5090
5213
|
const cachePath = rootDir ? import_path12.default.join(rootDir, "results", ".code-cache.json") : void 0;
|
|
5091
5214
|
const cache = new CodeCache(cachePath);
|
|
5092
|
-
return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache, mcpSession, actThenScript });
|
|
5215
|
+
return new SmartRouter({ primaryProvider: primary, fastProvider: fast, cache, mcpSession, actThenScript, ragManager });
|
|
5093
5216
|
}
|
|
5094
5217
|
async function generateSuiteFromStory(story, options) {
|
|
5095
5218
|
const provider = createProvider();
|
|
@@ -5289,9 +5412,10 @@ async function stepRoutes(fastify) {
|
|
|
5289
5412
|
id: `step-${(0, import_crypto2.randomUUID)().slice(0, 8)}`,
|
|
5290
5413
|
order: insertOrder,
|
|
5291
5414
|
instruction: parsed.data.instruction,
|
|
5292
|
-
generatedCode: "",
|
|
5293
|
-
strategy: "primary",
|
|
5294
|
-
lastHealed: null
|
|
5415
|
+
generatedCode: parsed.data.generatedCode ?? "",
|
|
5416
|
+
strategy: parsed.data.generatedCode ? parsed.data.strategy ?? "recorder" : "primary",
|
|
5417
|
+
lastHealed: null,
|
|
5418
|
+
...parsed.data.soft ? { soft: true } : {}
|
|
5295
5419
|
};
|
|
5296
5420
|
const updatedSteps = [
|
|
5297
5421
|
...tc.steps.map((s) => s.order >= insertOrder ? { ...s, order: s.order + 1 } : s),
|
|
@@ -5344,8 +5468,12 @@ async function stepRoutes(fastify) {
|
|
|
5344
5468
|
previousSteps: tc.steps.filter((s) => s.order < step.order && s.generatedCode).map((s) => ({ instruction: s.instruction, code: s.generatedCode })),
|
|
5345
5469
|
variables
|
|
5346
5470
|
});
|
|
5471
|
+
let finalCode = result.code;
|
|
5472
|
+
if (step.soft && finalCode) {
|
|
5473
|
+
finalCode = finalCode.replace(/\bexpect\(/g, "expect.soft(");
|
|
5474
|
+
}
|
|
5347
5475
|
const updatedSteps = tc.steps.map(
|
|
5348
|
-
(s) => s.id === step.id ? { ...s, generatedCode:
|
|
5476
|
+
(s) => s.id === step.id ? { ...s, generatedCode: finalCode, strategy: result.strategy } : s
|
|
5349
5477
|
);
|
|
5350
5478
|
await router.getCache().persist();
|
|
5351
5479
|
const updated = await updateCase(caseDir, { steps: updatedSteps });
|
|
@@ -5396,7 +5524,8 @@ async function stepRoutes(fastify) {
|
|
|
5396
5524
|
const r = codeMap.get(s.id);
|
|
5397
5525
|
if (r && r.code.trim()) {
|
|
5398
5526
|
actuallyGenerated++;
|
|
5399
|
-
|
|
5527
|
+
const code = s.soft ? r.code.replace(/\bexpect\(/g, "expect.soft(") : r.code;
|
|
5528
|
+
return { ...s, generatedCode: code, strategy: r.strategy };
|
|
5400
5529
|
}
|
|
5401
5530
|
return s;
|
|
5402
5531
|
});
|
|
@@ -5459,6 +5588,16 @@ async function stepRoutes(fastify) {
|
|
|
5459
5588
|
if (parsed.data.auditUrl !== void 0) patch.auditUrl = parsed.data.auditUrl;
|
|
5460
5589
|
if (parsed.data.lighthouseCategories !== void 0) patch.lighthouseCategories = parsed.data.lighthouseCategories;
|
|
5461
5590
|
if (parsed.data.runAudit !== void 0) patch.runAudit = parsed.data.runAudit;
|
|
5591
|
+
if (parsed.data.soft !== void 0) {
|
|
5592
|
+
patch.soft = parsed.data.soft;
|
|
5593
|
+
if (s.generatedCode) {
|
|
5594
|
+
if (parsed.data.soft) {
|
|
5595
|
+
patch.generatedCode = s.generatedCode.replace(/\bexpect\(/g, "expect.soft(");
|
|
5596
|
+
} else {
|
|
5597
|
+
patch.generatedCode = s.generatedCode.replace(/\bexpect\.soft\(/g, "expect(");
|
|
5598
|
+
}
|
|
5599
|
+
}
|
|
5600
|
+
}
|
|
5462
5601
|
return { ...s, ...patch };
|
|
5463
5602
|
});
|
|
5464
5603
|
const updated = await updateCase(caseDir, { steps: updatedSteps });
|
|
@@ -5485,7 +5624,10 @@ var init_steps = __esm({
|
|
|
5485
5624
|
init_utils2();
|
|
5486
5625
|
AddStepBody = import_zod8.z.object({
|
|
5487
5626
|
instruction: import_zod8.z.string().min(1),
|
|
5488
|
-
order: import_zod8.z.number().int().positive().optional()
|
|
5627
|
+
order: import_zod8.z.number().int().positive().optional(),
|
|
5628
|
+
generatedCode: import_zod8.z.string().optional(),
|
|
5629
|
+
strategy: import_zod8.z.enum(["template", "cache", "rag", "batch", "fast", "primary", "recorder"]).optional(),
|
|
5630
|
+
soft: import_zod8.z.boolean().optional()
|
|
5489
5631
|
});
|
|
5490
5632
|
ReorderBody = import_zod8.z.object({
|
|
5491
5633
|
stepIds: import_zod8.z.array(import_zod8.z.string().min(1))
|
|
@@ -5500,7 +5642,8 @@ var init_steps = __esm({
|
|
|
5500
5642
|
mockStatus: import_zod8.z.number().int().optional(),
|
|
5501
5643
|
auditUrl: import_zod8.z.string().optional(),
|
|
5502
5644
|
lighthouseCategories: import_zod8.z.array(import_zod8.z.enum(["performance", "accessibility", "seo"])).optional(),
|
|
5503
|
-
runAudit: import_zod8.z.boolean().optional()
|
|
5645
|
+
runAudit: import_zod8.z.boolean().optional(),
|
|
5646
|
+
soft: import_zod8.z.boolean().optional()
|
|
5504
5647
|
});
|
|
5505
5648
|
}
|
|
5506
5649
|
});
|
|
@@ -6510,6 +6653,726 @@ var init_result_store = __esm({
|
|
|
6510
6653
|
}
|
|
6511
6654
|
});
|
|
6512
6655
|
|
|
6656
|
+
// src/rag/embedder.ts
|
|
6657
|
+
function tokenize(text) {
|
|
6658
|
+
const cleaned = text.toLowerCase().replace(/\{\{[^}]+\}\}/g, " __var__ ").replace(/"([^"]+)"/g, (_m, p1) => ` ${p1} `).replace(/'([^']+)'/g, (_m, p1) => ` ${p1} `).replace(/[^a-z0-9\s-]/g, " ").trim();
|
|
6659
|
+
const words = cleaned.split(/\s+/).filter((w) => w.length > 1 && !STOPWORDS.has(w));
|
|
6660
|
+
const tokens = [...words];
|
|
6661
|
+
for (let i = 0; i < words.length - 1; i++) {
|
|
6662
|
+
tokens.push(`${words[i]}_${words[i + 1]}`);
|
|
6663
|
+
}
|
|
6664
|
+
return tokens;
|
|
6665
|
+
}
|
|
6666
|
+
function hashTerm(term) {
|
|
6667
|
+
const hash = (0, import_crypto4.createHash)("md5").update(term).digest();
|
|
6668
|
+
return hash.readUInt32LE(0) % VECTOR_DIM;
|
|
6669
|
+
}
|
|
6670
|
+
var import_crypto4, import_fs_extra14, VECTOR_DIM, STOPWORDS, TfIdfEmbedder;
|
|
6671
|
+
var init_embedder = __esm({
|
|
6672
|
+
"src/rag/embedder.ts"() {
|
|
6673
|
+
"use strict";
|
|
6674
|
+
init_cjs_shims();
|
|
6675
|
+
import_crypto4 = require("crypto");
|
|
6676
|
+
import_fs_extra14 = require("fs-extra");
|
|
6677
|
+
VECTOR_DIM = 256;
|
|
6678
|
+
STOPWORDS = /* @__PURE__ */ new Set([
|
|
6679
|
+
"the",
|
|
6680
|
+
"a",
|
|
6681
|
+
"an",
|
|
6682
|
+
"is",
|
|
6683
|
+
"are",
|
|
6684
|
+
"was",
|
|
6685
|
+
"were",
|
|
6686
|
+
"be",
|
|
6687
|
+
"been",
|
|
6688
|
+
"being",
|
|
6689
|
+
"have",
|
|
6690
|
+
"has",
|
|
6691
|
+
"had",
|
|
6692
|
+
"do",
|
|
6693
|
+
"does",
|
|
6694
|
+
"did",
|
|
6695
|
+
"will",
|
|
6696
|
+
"would",
|
|
6697
|
+
"could",
|
|
6698
|
+
"should",
|
|
6699
|
+
"may",
|
|
6700
|
+
"might",
|
|
6701
|
+
"shall",
|
|
6702
|
+
"can",
|
|
6703
|
+
"need",
|
|
6704
|
+
"dare",
|
|
6705
|
+
"ought",
|
|
6706
|
+
"to",
|
|
6707
|
+
"of",
|
|
6708
|
+
"in",
|
|
6709
|
+
"for",
|
|
6710
|
+
"on",
|
|
6711
|
+
"with",
|
|
6712
|
+
"at",
|
|
6713
|
+
"by",
|
|
6714
|
+
"from",
|
|
6715
|
+
"as",
|
|
6716
|
+
"into",
|
|
6717
|
+
"through",
|
|
6718
|
+
"during",
|
|
6719
|
+
"before",
|
|
6720
|
+
"after",
|
|
6721
|
+
"above",
|
|
6722
|
+
"below",
|
|
6723
|
+
"between",
|
|
6724
|
+
"out",
|
|
6725
|
+
"off",
|
|
6726
|
+
"over",
|
|
6727
|
+
"under",
|
|
6728
|
+
"again",
|
|
6729
|
+
"further",
|
|
6730
|
+
"then",
|
|
6731
|
+
"once",
|
|
6732
|
+
"that",
|
|
6733
|
+
"this",
|
|
6734
|
+
"these",
|
|
6735
|
+
"those",
|
|
6736
|
+
"and",
|
|
6737
|
+
"but",
|
|
6738
|
+
"or",
|
|
6739
|
+
"nor",
|
|
6740
|
+
"not",
|
|
6741
|
+
"so",
|
|
6742
|
+
"very",
|
|
6743
|
+
"just",
|
|
6744
|
+
"than",
|
|
6745
|
+
"too",
|
|
6746
|
+
"also",
|
|
6747
|
+
"it",
|
|
6748
|
+
"its"
|
|
6749
|
+
]);
|
|
6750
|
+
TfIdfEmbedder = class {
|
|
6751
|
+
constructor(vocabPath) {
|
|
6752
|
+
this.vocabPath = vocabPath;
|
|
6753
|
+
}
|
|
6754
|
+
vocab = { docCount: 0, termDocFreq: {} };
|
|
6755
|
+
dirty = false;
|
|
6756
|
+
/* ── Lifecycle ─────────────────────────────────────────────────────────── */
|
|
6757
|
+
/** Load persisted IDF vocabulary from disk */
|
|
6758
|
+
async load() {
|
|
6759
|
+
try {
|
|
6760
|
+
this.vocab = await (0, import_fs_extra14.readJson)(this.vocabPath);
|
|
6761
|
+
} catch {
|
|
6762
|
+
this.vocab = { docCount: 0, termDocFreq: {} };
|
|
6763
|
+
}
|
|
6764
|
+
}
|
|
6765
|
+
/** Persist IDF vocabulary to disk */
|
|
6766
|
+
async save() {
|
|
6767
|
+
if (!this.dirty) return;
|
|
6768
|
+
await (0, import_fs_extra14.outputJson)(this.vocabPath, this.vocab, { spaces: 0 });
|
|
6769
|
+
this.dirty = false;
|
|
6770
|
+
}
|
|
6771
|
+
/* ── Training ──────────────────────────────────────────────────────────── */
|
|
6772
|
+
/**
|
|
6773
|
+
* Add a document to the IDF computation.
|
|
6774
|
+
* Call this for every instruction ingested into a corpus.
|
|
6775
|
+
*/
|
|
6776
|
+
addDocument(text) {
|
|
6777
|
+
const tokens = new Set(tokenize(text));
|
|
6778
|
+
this.vocab.docCount++;
|
|
6779
|
+
for (const term of tokens) {
|
|
6780
|
+
this.vocab.termDocFreq[term] = (this.vocab.termDocFreq[term] ?? 0) + 1;
|
|
6781
|
+
}
|
|
6782
|
+
this.dirty = true;
|
|
6783
|
+
}
|
|
6784
|
+
/**
|
|
6785
|
+
* Batch-add multiple documents at once (initial corpus load).
|
|
6786
|
+
*/
|
|
6787
|
+
addDocuments(texts) {
|
|
6788
|
+
for (const text of texts) {
|
|
6789
|
+
this.addDocument(text);
|
|
6790
|
+
}
|
|
6791
|
+
}
|
|
6792
|
+
/* ── Embedding ─────────────────────────────────────────────────────────── */
|
|
6793
|
+
/**
|
|
6794
|
+
* Convert a text string into a fixed-dimension TF-IDF vector.
|
|
6795
|
+
* Uses hashing trick to map terms to buckets (no explicit vocabulary limit).
|
|
6796
|
+
*/
|
|
6797
|
+
embed(text) {
|
|
6798
|
+
const tokens = tokenize(text);
|
|
6799
|
+
if (tokens.length === 0) return new Array(VECTOR_DIM).fill(0);
|
|
6800
|
+
const tf = {};
|
|
6801
|
+
for (const token of tokens) {
|
|
6802
|
+
tf[token] = (tf[token] ?? 0) + 1;
|
|
6803
|
+
}
|
|
6804
|
+
const vector = new Array(VECTOR_DIM).fill(0);
|
|
6805
|
+
const N = Math.max(this.vocab.docCount, 1);
|
|
6806
|
+
for (const [term, count] of Object.entries(tf)) {
|
|
6807
|
+
const termFreq = count / tokens.length;
|
|
6808
|
+
const docFreq = this.vocab.termDocFreq[term] ?? 0;
|
|
6809
|
+
const idf = Math.log(1 + N / (1 + docFreq));
|
|
6810
|
+
const tfidf = termFreq * idf;
|
|
6811
|
+
const bucket = hashTerm(term);
|
|
6812
|
+
vector[bucket] += tfidf;
|
|
6813
|
+
}
|
|
6814
|
+
const mag = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
6815
|
+
if (mag > 0) {
|
|
6816
|
+
for (let i = 0; i < vector.length; i++) {
|
|
6817
|
+
vector[i] /= mag;
|
|
6818
|
+
}
|
|
6819
|
+
}
|
|
6820
|
+
return vector;
|
|
6821
|
+
}
|
|
6822
|
+
/** Current corpus size */
|
|
6823
|
+
get documentCount() {
|
|
6824
|
+
return this.vocab.docCount;
|
|
6825
|
+
}
|
|
6826
|
+
};
|
|
6827
|
+
}
|
|
6828
|
+
});
|
|
6829
|
+
|
|
6830
|
+
// src/rag/similarity.ts
|
|
6831
|
+
function cosineSimilarity(a, b) {
|
|
6832
|
+
if (a.length !== b.length || a.length === 0) return 0;
|
|
6833
|
+
let dot = 0;
|
|
6834
|
+
let magA = 0;
|
|
6835
|
+
let magB = 0;
|
|
6836
|
+
for (let i = 0; i < a.length; i++) {
|
|
6837
|
+
dot += a[i] * b[i];
|
|
6838
|
+
magA += a[i] * a[i];
|
|
6839
|
+
magB += b[i] * b[i];
|
|
6840
|
+
}
|
|
6841
|
+
const denom = Math.sqrt(magA) * Math.sqrt(magB);
|
|
6842
|
+
return denom === 0 ? 0 : dot / denom;
|
|
6843
|
+
}
|
|
6844
|
+
var init_similarity = __esm({
|
|
6845
|
+
"src/rag/similarity.ts"() {
|
|
6846
|
+
"use strict";
|
|
6847
|
+
init_cjs_shims();
|
|
6848
|
+
}
|
|
6849
|
+
});
|
|
6850
|
+
|
|
6851
|
+
// src/rag/vector-store.ts
|
|
6852
|
+
var import_fs_extra15, VectorStore;
|
|
6853
|
+
var init_vector_store = __esm({
|
|
6854
|
+
"src/rag/vector-store.ts"() {
|
|
6855
|
+
"use strict";
|
|
6856
|
+
init_cjs_shims();
|
|
6857
|
+
import_fs_extra15 = require("fs-extra");
|
|
6858
|
+
init_similarity();
|
|
6859
|
+
VectorStore = class {
|
|
6860
|
+
constructor(filePath, maxEntries = 500) {
|
|
6861
|
+
this.filePath = filePath;
|
|
6862
|
+
this.maxEntries = maxEntries;
|
|
6863
|
+
}
|
|
6864
|
+
documents = [];
|
|
6865
|
+
dirty = false;
|
|
6866
|
+
/* ── Lifecycle ─────────────────────────────────────────────────────────── */
|
|
6867
|
+
async load() {
|
|
6868
|
+
if (await (0, import_fs_extra15.pathExists)(this.filePath)) {
|
|
6869
|
+
try {
|
|
6870
|
+
const data = await (0, import_fs_extra15.readJson)(this.filePath);
|
|
6871
|
+
this.documents = data.documents ?? [];
|
|
6872
|
+
} catch {
|
|
6873
|
+
this.documents = [];
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
}
|
|
6877
|
+
async persist() {
|
|
6878
|
+
if (!this.dirty) return;
|
|
6879
|
+
const data = { version: 1, documents: this.documents };
|
|
6880
|
+
await (0, import_fs_extra15.outputJson)(this.filePath, data, { spaces: 0 });
|
|
6881
|
+
this.dirty = false;
|
|
6882
|
+
}
|
|
6883
|
+
/* ── CRUD ──────────────────────────────────────────────────────────────── */
|
|
6884
|
+
/** Add a document. Auto-prunes if maxEntries exceeded. */
|
|
6885
|
+
add(id, text, vector, metadata) {
|
|
6886
|
+
this.documents = this.documents.filter((d) => d.id !== id);
|
|
6887
|
+
const now2 = Date.now();
|
|
6888
|
+
this.documents.push({ id, text, vector, metadata, createdAt: now2, updatedAt: now2 });
|
|
6889
|
+
this.dirty = true;
|
|
6890
|
+
if (this.documents.length > this.maxEntries) {
|
|
6891
|
+
this.prune();
|
|
6892
|
+
}
|
|
6893
|
+
}
|
|
6894
|
+
/** Update metadata for an existing document */
|
|
6895
|
+
update(id, metadata) {
|
|
6896
|
+
const doc = this.documents.find((d) => d.id === id);
|
|
6897
|
+
if (!doc) return false;
|
|
6898
|
+
doc.metadata = { ...doc.metadata, ...metadata };
|
|
6899
|
+
doc.updatedAt = Date.now();
|
|
6900
|
+
this.dirty = true;
|
|
6901
|
+
return true;
|
|
6902
|
+
}
|
|
6903
|
+
/** Remove a document by ID */
|
|
6904
|
+
remove(id) {
|
|
6905
|
+
const before = this.documents.length;
|
|
6906
|
+
this.documents = this.documents.filter((d) => d.id !== id);
|
|
6907
|
+
if (this.documents.length !== before) {
|
|
6908
|
+
this.dirty = true;
|
|
6909
|
+
return true;
|
|
6910
|
+
}
|
|
6911
|
+
return false;
|
|
6912
|
+
}
|
|
6913
|
+
/** Get a document by ID */
|
|
6914
|
+
get(id) {
|
|
6915
|
+
return this.documents.find((d) => d.id === id);
|
|
6916
|
+
}
|
|
6917
|
+
/** Check if a document exists */
|
|
6918
|
+
has(id) {
|
|
6919
|
+
return this.documents.some((d) => d.id === id);
|
|
6920
|
+
}
|
|
6921
|
+
/** Number of documents in the store */
|
|
6922
|
+
get size() {
|
|
6923
|
+
return this.documents.length;
|
|
6924
|
+
}
|
|
6925
|
+
/* ── Search ────────────────────────────────────────────────────────────── */
|
|
6926
|
+
/**
|
|
6927
|
+
* Find the top-K most similar documents to a query vector.
|
|
6928
|
+
* Only returns documents above the similarity threshold.
|
|
6929
|
+
*/
|
|
6930
|
+
search(queryVector, topK = 5, threshold = 0.5) {
|
|
6931
|
+
const scored = [];
|
|
6932
|
+
for (const doc of this.documents) {
|
|
6933
|
+
const score = cosineSimilarity(queryVector, doc.vector);
|
|
6934
|
+
if (score >= threshold) {
|
|
6935
|
+
scored.push({
|
|
6936
|
+
id: doc.id,
|
|
6937
|
+
text: doc.text,
|
|
6938
|
+
metadata: doc.metadata,
|
|
6939
|
+
score
|
|
6940
|
+
});
|
|
6941
|
+
}
|
|
6942
|
+
}
|
|
6943
|
+
scored.sort((a, b) => {
|
|
6944
|
+
if (Math.abs(a.score - b.score) > 1e-3) return b.score - a.score;
|
|
6945
|
+
const docA = this.documents.find((d) => d.id === a.id);
|
|
6946
|
+
const docB = this.documents.find((d) => d.id === b.id);
|
|
6947
|
+
return docB.updatedAt - docA.updatedAt;
|
|
6948
|
+
});
|
|
6949
|
+
return scored.slice(0, topK);
|
|
6950
|
+
}
|
|
6951
|
+
/* ── Internal ──────────────────────────────────────────────────────────── */
|
|
6952
|
+
/** Remove oldest entries to stay within maxEntries limit */
|
|
6953
|
+
prune() {
|
|
6954
|
+
const sorted = [...this.documents].sort((a, b) => a.updatedAt - b.updatedAt);
|
|
6955
|
+
const excess = sorted.length - this.maxEntries;
|
|
6956
|
+
if (excess > 0) {
|
|
6957
|
+
const toRemove = new Set(sorted.slice(0, excess).map((d) => d.id));
|
|
6958
|
+
this.documents = this.documents.filter((d) => !toRemove.has(d.id));
|
|
6959
|
+
}
|
|
6960
|
+
}
|
|
6961
|
+
};
|
|
6962
|
+
}
|
|
6963
|
+
});
|
|
6964
|
+
|
|
6965
|
+
// src/rag/code-corpus.ts
|
|
6966
|
+
function generateId(instruction, url) {
|
|
6967
|
+
const normalized = `${instruction.toLowerCase().trim()}|${normalizeUrl2(url)}`;
|
|
6968
|
+
return (0, import_crypto5.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
6969
|
+
}
|
|
6970
|
+
function normalizeUrl2(url) {
|
|
6971
|
+
if (!url) return "";
|
|
6972
|
+
try {
|
|
6973
|
+
const u = new URL(url);
|
|
6974
|
+
const path37 = u.pathname.replace(/\/\d+/g, "/*");
|
|
6975
|
+
return `${u.hostname}${path37}`;
|
|
6976
|
+
} catch {
|
|
6977
|
+
return url;
|
|
6978
|
+
}
|
|
6979
|
+
}
|
|
6980
|
+
function boostBySuccess(score, success, fail) {
|
|
6981
|
+
const total = success + fail;
|
|
6982
|
+
if (total === 0) return score;
|
|
6983
|
+
const successRate = success / total;
|
|
6984
|
+
return score * (1 + 0.05 * successRate);
|
|
6985
|
+
}
|
|
6986
|
+
function toMatch(doc) {
|
|
6987
|
+
return {
|
|
6988
|
+
id: doc.id,
|
|
6989
|
+
instruction: doc.metadata.instruction,
|
|
6990
|
+
code: doc.metadata.code,
|
|
6991
|
+
urlPattern: doc.metadata.urlPattern,
|
|
6992
|
+
score: doc.score,
|
|
6993
|
+
successCount: doc.metadata.successCount,
|
|
6994
|
+
failCount: doc.metadata.failCount
|
|
6995
|
+
};
|
|
6996
|
+
}
|
|
6997
|
+
var import_crypto5, MAX_CONSECUTIVE_FAILS, CodeCorpus;
|
|
6998
|
+
var init_code_corpus = __esm({
|
|
6999
|
+
"src/rag/code-corpus.ts"() {
|
|
7000
|
+
"use strict";
|
|
7001
|
+
init_cjs_shims();
|
|
7002
|
+
import_crypto5 = require("crypto");
|
|
7003
|
+
MAX_CONSECUTIVE_FAILS = 2;
|
|
7004
|
+
CodeCorpus = class {
|
|
7005
|
+
constructor(store, embedder) {
|
|
7006
|
+
this.store = store;
|
|
7007
|
+
this.embedder = embedder;
|
|
7008
|
+
}
|
|
7009
|
+
/* ── Ingest ────────────────────────────────────────────────────────────── */
|
|
7010
|
+
/**
|
|
7011
|
+
* Add a successful instruction → code mapping to the corpus.
|
|
7012
|
+
* Called after a step passes execution.
|
|
7013
|
+
*/
|
|
7014
|
+
ingest(instruction, code, url) {
|
|
7015
|
+
const id = generateId(instruction, url);
|
|
7016
|
+
const urlPattern = normalizeUrl2(url);
|
|
7017
|
+
const embedText = `${instruction} ${urlPattern}`;
|
|
7018
|
+
const existing = this.store.get(id);
|
|
7019
|
+
if (existing) {
|
|
7020
|
+
this.store.update(id, {
|
|
7021
|
+
...existing.metadata,
|
|
7022
|
+
code,
|
|
7023
|
+
successCount: existing.metadata.successCount + 1,
|
|
7024
|
+
consecutiveFails: 0
|
|
7025
|
+
});
|
|
7026
|
+
return;
|
|
7027
|
+
}
|
|
7028
|
+
this.embedder.addDocument(embedText);
|
|
7029
|
+
const vector = this.embedder.embed(embedText);
|
|
7030
|
+
this.store.add(id, embedText, vector, {
|
|
7031
|
+
instruction,
|
|
7032
|
+
code,
|
|
7033
|
+
urlPattern,
|
|
7034
|
+
successCount: 1,
|
|
7035
|
+
failCount: 0,
|
|
7036
|
+
consecutiveFails: 0
|
|
7037
|
+
});
|
|
7038
|
+
}
|
|
7039
|
+
/* ── Retrieve ──────────────────────────────────────────────────────────── */
|
|
7040
|
+
/**
|
|
7041
|
+
* Find similar past instructions in the corpus.
|
|
7042
|
+
* Returns matches ranked by similarity score.
|
|
7043
|
+
*/
|
|
7044
|
+
retrieve(instruction, url = "", topK = 3, threshold = 0.65) {
|
|
7045
|
+
if (this.store.size === 0) return [];
|
|
7046
|
+
const urlPattern = normalizeUrl2(url);
|
|
7047
|
+
const queryText = `${instruction} ${urlPattern}`;
|
|
7048
|
+
const queryVector = this.embedder.embed(queryText);
|
|
7049
|
+
const results = this.store.search(queryVector, topK * 2, threshold);
|
|
7050
|
+
const boosted = results.map((r) => ({
|
|
7051
|
+
...r,
|
|
7052
|
+
score: boostBySuccess(r.score, r.metadata.successCount, r.metadata.failCount)
|
|
7053
|
+
}));
|
|
7054
|
+
boosted.sort((a, b) => b.score - a.score);
|
|
7055
|
+
return boosted.slice(0, topK).map(toMatch);
|
|
7056
|
+
}
|
|
7057
|
+
/* ── Feedback ──────────────────────────────────────────────────────────── */
|
|
7058
|
+
/**
|
|
7059
|
+
* Record the outcome of using a corpus entry.
|
|
7060
|
+
* Removes entries that fail too many times consecutively.
|
|
7061
|
+
*/
|
|
7062
|
+
recordOutcome(id, passed) {
|
|
7063
|
+
const doc = this.store.get(id);
|
|
7064
|
+
if (!doc) return;
|
|
7065
|
+
if (passed) {
|
|
7066
|
+
this.store.update(id, {
|
|
7067
|
+
...doc.metadata,
|
|
7068
|
+
successCount: doc.metadata.successCount + 1,
|
|
7069
|
+
consecutiveFails: 0
|
|
7070
|
+
});
|
|
7071
|
+
} else {
|
|
7072
|
+
const newConsecutive = doc.metadata.consecutiveFails + 1;
|
|
7073
|
+
if (newConsecutive >= MAX_CONSECUTIVE_FAILS) {
|
|
7074
|
+
this.store.remove(id);
|
|
7075
|
+
return;
|
|
7076
|
+
}
|
|
7077
|
+
this.store.update(id, {
|
|
7078
|
+
...doc.metadata,
|
|
7079
|
+
failCount: doc.metadata.failCount + 1,
|
|
7080
|
+
consecutiveFails: newConsecutive
|
|
7081
|
+
});
|
|
7082
|
+
}
|
|
7083
|
+
}
|
|
7084
|
+
/** Number of entries in the corpus */
|
|
7085
|
+
get size() {
|
|
7086
|
+
return this.store.size;
|
|
7087
|
+
}
|
|
7088
|
+
};
|
|
7089
|
+
}
|
|
7090
|
+
});
|
|
7091
|
+
|
|
7092
|
+
// src/rag/healing-corpus.ts
|
|
7093
|
+
function generateId2(instruction, error) {
|
|
7094
|
+
const normalized = `${instruction.toLowerCase().trim()}|${normalizeError(error)}`;
|
|
7095
|
+
return (0, import_crypto6.createHash)("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
7096
|
+
}
|
|
7097
|
+
function normalizeError(error) {
|
|
7098
|
+
return error.replace(/\d+ms/g, "Nms").replace(/\d+/g, "N").replace(/["'][^"']*["']/g, "'...'").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
7099
|
+
}
|
|
7100
|
+
function normalizeUrl3(url) {
|
|
7101
|
+
if (!url) return "";
|
|
7102
|
+
try {
|
|
7103
|
+
const u = new URL(url);
|
|
7104
|
+
return `${u.hostname}${u.pathname.replace(/\/\d+/g, "/*")}`;
|
|
7105
|
+
} catch {
|
|
7106
|
+
return url;
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
function toMatch2(doc) {
|
|
7110
|
+
return {
|
|
7111
|
+
id: doc.id,
|
|
7112
|
+
instruction: doc.metadata.instruction,
|
|
7113
|
+
error: doc.metadata.error,
|
|
7114
|
+
failedCode: doc.metadata.failedCode,
|
|
7115
|
+
healedCode: doc.metadata.healedCode,
|
|
7116
|
+
strategy: doc.metadata.strategy,
|
|
7117
|
+
level: doc.metadata.level,
|
|
7118
|
+
score: doc.score,
|
|
7119
|
+
successCount: doc.metadata.successCount
|
|
7120
|
+
};
|
|
7121
|
+
}
|
|
7122
|
+
var import_crypto6, HealingCorpus;
|
|
7123
|
+
var init_healing_corpus = __esm({
|
|
7124
|
+
"src/rag/healing-corpus.ts"() {
|
|
7125
|
+
"use strict";
|
|
7126
|
+
init_cjs_shims();
|
|
7127
|
+
import_crypto6 = require("crypto");
|
|
7128
|
+
HealingCorpus = class {
|
|
7129
|
+
constructor(store, embedder) {
|
|
7130
|
+
this.store = store;
|
|
7131
|
+
this.embedder = embedder;
|
|
7132
|
+
}
|
|
7133
|
+
/* ── Ingest ────────────────────────────────────────────────────────────── */
|
|
7134
|
+
/**
|
|
7135
|
+
* Record a successful healing event in the corpus.
|
|
7136
|
+
* Called when self-healing produces a working fix.
|
|
7137
|
+
*/
|
|
7138
|
+
ingest(event) {
|
|
7139
|
+
const id = generateId2(event.instruction, event.error);
|
|
7140
|
+
const urlPattern = normalizeUrl3(event.url);
|
|
7141
|
+
const embedText = `${event.instruction} ${normalizeError(event.error)}`;
|
|
7142
|
+
const existing = this.store.get(id);
|
|
7143
|
+
if (existing) {
|
|
7144
|
+
this.store.update(id, {
|
|
7145
|
+
...existing.metadata,
|
|
7146
|
+
healedCode: event.healedCode,
|
|
7147
|
+
strategy: event.strategy,
|
|
7148
|
+
level: event.level,
|
|
7149
|
+
successCount: existing.metadata.successCount + 1
|
|
7150
|
+
});
|
|
7151
|
+
return;
|
|
7152
|
+
}
|
|
7153
|
+
this.embedder.addDocument(embedText);
|
|
7154
|
+
const vector = this.embedder.embed(embedText);
|
|
7155
|
+
this.store.add(id, embedText, vector, {
|
|
7156
|
+
instruction: event.instruction,
|
|
7157
|
+
error: normalizeError(event.error),
|
|
7158
|
+
failedCode: event.failedCode,
|
|
7159
|
+
healedCode: event.healedCode,
|
|
7160
|
+
strategy: event.strategy,
|
|
7161
|
+
level: event.level,
|
|
7162
|
+
urlPattern,
|
|
7163
|
+
successCount: 1
|
|
7164
|
+
});
|
|
7165
|
+
}
|
|
7166
|
+
/* ── Retrieve ──────────────────────────────────────────────────────────── */
|
|
7167
|
+
/**
|
|
7168
|
+
* Find similar past healing events.
|
|
7169
|
+
* Searches by instruction + error pattern similarity.
|
|
7170
|
+
*/
|
|
7171
|
+
retrieve(instruction, error, topK = 3, threshold = 0.6) {
|
|
7172
|
+
if (this.store.size === 0) return [];
|
|
7173
|
+
const queryText = `${instruction} ${normalizeError(error)}`;
|
|
7174
|
+
const queryVector = this.embedder.embed(queryText);
|
|
7175
|
+
const results = this.store.search(queryVector, topK, threshold);
|
|
7176
|
+
return results.map(toMatch2);
|
|
7177
|
+
}
|
|
7178
|
+
/** Number of entries */
|
|
7179
|
+
get size() {
|
|
7180
|
+
return this.store.size;
|
|
7181
|
+
}
|
|
7182
|
+
};
|
|
7183
|
+
}
|
|
7184
|
+
});
|
|
7185
|
+
|
|
7186
|
+
// src/rag/error-catalog.ts
|
|
7187
|
+
function generateId3(errorPattern, urlPattern) {
|
|
7188
|
+
const key = `${errorPattern}|${urlPattern}`;
|
|
7189
|
+
return (0, import_crypto7.createHash)("sha256").update(key).digest("hex").slice(0, 16);
|
|
7190
|
+
}
|
|
7191
|
+
function normalizeError2(error) {
|
|
7192
|
+
return error.replace(/\d+ms/g, "Nms").replace(/\d+/g, "N").replace(/["'][^"']*["']/g, "'...'").replace(/\s+/g, " ").trim().slice(0, 200);
|
|
7193
|
+
}
|
|
7194
|
+
function normalizeUrl4(url) {
|
|
7195
|
+
if (!url) return "";
|
|
7196
|
+
try {
|
|
7197
|
+
const u = new URL(url);
|
|
7198
|
+
return `${u.hostname}${u.pathname.replace(/\/\d+/g, "/*")}`;
|
|
7199
|
+
} catch {
|
|
7200
|
+
return url;
|
|
7201
|
+
}
|
|
7202
|
+
}
|
|
7203
|
+
function toWarning(doc) {
|
|
7204
|
+
const fixes = doc.metadata.commonFixes;
|
|
7205
|
+
return {
|
|
7206
|
+
errorPattern: doc.metadata.errorPattern,
|
|
7207
|
+
urlPattern: doc.metadata.urlPattern,
|
|
7208
|
+
frequency: doc.metadata.frequency,
|
|
7209
|
+
suggestedFix: fixes.length > 0 ? fixes[fixes.length - 1] : null,
|
|
7210
|
+
score: doc.score
|
|
7211
|
+
};
|
|
7212
|
+
}
|
|
7213
|
+
var import_crypto7, ErrorCatalog;
|
|
7214
|
+
var init_error_catalog = __esm({
|
|
7215
|
+
"src/rag/error-catalog.ts"() {
|
|
7216
|
+
"use strict";
|
|
7217
|
+
init_cjs_shims();
|
|
7218
|
+
import_crypto7 = require("crypto");
|
|
7219
|
+
ErrorCatalog = class {
|
|
7220
|
+
constructor(store, embedder) {
|
|
7221
|
+
this.store = store;
|
|
7222
|
+
this.embedder = embedder;
|
|
7223
|
+
}
|
|
7224
|
+
/* ── Record ────────────────────────────────────────────────────────────── */
|
|
7225
|
+
/**
|
|
7226
|
+
* Record a test failure. If the error pattern already exists,
|
|
7227
|
+
* increment frequency. If a fix is provided, add it to commonFixes.
|
|
7228
|
+
*/
|
|
7229
|
+
record(error, url, fixCode) {
|
|
7230
|
+
const errorPattern = normalizeError2(error);
|
|
7231
|
+
const urlPattern = normalizeUrl4(url);
|
|
7232
|
+
const id = generateId3(errorPattern, urlPattern);
|
|
7233
|
+
const embedText = `${errorPattern} ${urlPattern}`;
|
|
7234
|
+
const existing = this.store.get(id);
|
|
7235
|
+
if (existing) {
|
|
7236
|
+
const fixes = [...existing.metadata.commonFixes];
|
|
7237
|
+
if (fixCode && !fixes.includes(fixCode)) {
|
|
7238
|
+
fixes.push(fixCode);
|
|
7239
|
+
if (fixes.length > 5) fixes.shift();
|
|
7240
|
+
}
|
|
7241
|
+
this.store.update(id, {
|
|
7242
|
+
...existing.metadata,
|
|
7243
|
+
frequency: existing.metadata.frequency + 1,
|
|
7244
|
+
lastSeen: Date.now(),
|
|
7245
|
+
commonFixes: fixes
|
|
7246
|
+
});
|
|
7247
|
+
return;
|
|
7248
|
+
}
|
|
7249
|
+
this.embedder.addDocument(embedText);
|
|
7250
|
+
const vector = this.embedder.embed(embedText);
|
|
7251
|
+
this.store.add(id, embedText, vector, {
|
|
7252
|
+
errorPattern,
|
|
7253
|
+
urlPattern,
|
|
7254
|
+
frequency: 1,
|
|
7255
|
+
lastSeen: Date.now(),
|
|
7256
|
+
commonFixes: fixCode ? [fixCode] : []
|
|
7257
|
+
});
|
|
7258
|
+
}
|
|
7259
|
+
/* ── Retrieve ──────────────────────────────────────────────────────────── */
|
|
7260
|
+
/**
|
|
7261
|
+
* Get known error patterns for a URL.
|
|
7262
|
+
* Used during code generation to warn the AI about known pitfalls.
|
|
7263
|
+
*/
|
|
7264
|
+
getWarnings(url, topK = 5) {
|
|
7265
|
+
if (this.store.size === 0) return [];
|
|
7266
|
+
const urlPattern = normalizeUrl4(url);
|
|
7267
|
+
const queryVector = this.embedder.embed(urlPattern);
|
|
7268
|
+
const results = this.store.search(queryVector, topK * 2, 0.3);
|
|
7269
|
+
const recurring = results.filter((r) => r.metadata.frequency >= 2);
|
|
7270
|
+
recurring.sort((a, b) => b.metadata.frequency - a.metadata.frequency);
|
|
7271
|
+
return recurring.slice(0, topK).map(toWarning);
|
|
7272
|
+
}
|
|
7273
|
+
/** Number of tracked patterns */
|
|
7274
|
+
get size() {
|
|
7275
|
+
return this.store.size;
|
|
7276
|
+
}
|
|
7277
|
+
};
|
|
7278
|
+
}
|
|
7279
|
+
});
|
|
7280
|
+
|
|
7281
|
+
// src/rag/rag-manager.ts
|
|
7282
|
+
var import_path19, import_fs_extra16, logger21, RagManager;
|
|
7283
|
+
var init_rag_manager = __esm({
|
|
7284
|
+
"src/rag/rag-manager.ts"() {
|
|
7285
|
+
"use strict";
|
|
7286
|
+
init_cjs_shims();
|
|
7287
|
+
import_path19 = require("path");
|
|
7288
|
+
import_fs_extra16 = require("fs-extra");
|
|
7289
|
+
init_embedder();
|
|
7290
|
+
init_vector_store();
|
|
7291
|
+
init_code_corpus();
|
|
7292
|
+
init_healing_corpus();
|
|
7293
|
+
init_error_catalog();
|
|
7294
|
+
init_logger();
|
|
7295
|
+
logger21 = createChildLogger("rag");
|
|
7296
|
+
RagManager = class _RagManager {
|
|
7297
|
+
codeCorpus;
|
|
7298
|
+
healingCorpus;
|
|
7299
|
+
errorCatalog;
|
|
7300
|
+
embedder;
|
|
7301
|
+
codeStore;
|
|
7302
|
+
healingStore;
|
|
7303
|
+
errorStore;
|
|
7304
|
+
constructor(embedder, codeStore, healingStore, errorStore) {
|
|
7305
|
+
this.embedder = embedder;
|
|
7306
|
+
this.codeStore = codeStore;
|
|
7307
|
+
this.healingStore = healingStore;
|
|
7308
|
+
this.errorStore = errorStore;
|
|
7309
|
+
this.codeCorpus = new CodeCorpus(codeStore, embedder);
|
|
7310
|
+
this.healingCorpus = new HealingCorpus(healingStore, embedder);
|
|
7311
|
+
this.errorCatalog = new ErrorCatalog(errorStore, embedder);
|
|
7312
|
+
}
|
|
7313
|
+
/* ── Factory ───────────────────────────────────────────────────────────── */
|
|
7314
|
+
/**
|
|
7315
|
+
* Create and initialize a RagManager.
|
|
7316
|
+
* Loads all corpora from disk. Safe to call if files don't exist yet.
|
|
7317
|
+
*/
|
|
7318
|
+
static async create(rootDir, config) {
|
|
7319
|
+
const ragDir = (0, import_path19.join)(rootDir, "results", ".rag");
|
|
7320
|
+
await (0, import_fs_extra16.ensureDir)(ragDir);
|
|
7321
|
+
logger21.info({ ragDir }, "Initializing RAG manager");
|
|
7322
|
+
const embedder = new TfIdfEmbedder((0, import_path19.join)(ragDir, "idf-vocab.json"));
|
|
7323
|
+
await embedder.load();
|
|
7324
|
+
const codeStore = new VectorStore(
|
|
7325
|
+
(0, import_path19.join)(ragDir, "code-corpus.json"),
|
|
7326
|
+
config.codeCorpus.maxEntries
|
|
7327
|
+
);
|
|
7328
|
+
const healingStore = new VectorStore(
|
|
7329
|
+
(0, import_path19.join)(ragDir, "healing-corpus.json"),
|
|
7330
|
+
config.healingCorpus.maxEntries
|
|
7331
|
+
);
|
|
7332
|
+
const errorStore = new VectorStore(
|
|
7333
|
+
(0, import_path19.join)(ragDir, "error-catalog.json"),
|
|
7334
|
+
config.errorCatalog.maxEntries
|
|
7335
|
+
);
|
|
7336
|
+
await Promise.all([
|
|
7337
|
+
codeStore.load(),
|
|
7338
|
+
healingStore.load(),
|
|
7339
|
+
errorStore.load()
|
|
7340
|
+
]);
|
|
7341
|
+
logger21.info(
|
|
7342
|
+
{
|
|
7343
|
+
codeCorpus: codeStore.size,
|
|
7344
|
+
healingCorpus: healingStore.size,
|
|
7345
|
+
errorCatalog: errorStore.size,
|
|
7346
|
+
idfDocuments: embedder.documentCount
|
|
7347
|
+
},
|
|
7348
|
+
"RAG corpora loaded"
|
|
7349
|
+
);
|
|
7350
|
+
return new _RagManager(embedder, codeStore, healingStore, errorStore);
|
|
7351
|
+
}
|
|
7352
|
+
/* ── Persistence ───────────────────────────────────────────────────────── */
|
|
7353
|
+
/** Persist all corpora and the embedder vocabulary to disk */
|
|
7354
|
+
async persist() {
|
|
7355
|
+
await Promise.all([
|
|
7356
|
+
this.embedder.save(),
|
|
7357
|
+
this.codeStore.persist(),
|
|
7358
|
+
this.healingStore.persist(),
|
|
7359
|
+
this.errorStore.persist()
|
|
7360
|
+
]);
|
|
7361
|
+
logger21.debug("RAG corpora persisted");
|
|
7362
|
+
}
|
|
7363
|
+
/* ── Stats ─────────────────────────────────────────────────────────────── */
|
|
7364
|
+
getStats() {
|
|
7365
|
+
return {
|
|
7366
|
+
codeCorpus: this.codeStore.size,
|
|
7367
|
+
healingCorpus: this.healingStore.size,
|
|
7368
|
+
errorCatalog: this.errorStore.size,
|
|
7369
|
+
idfVocab: this.embedder.documentCount
|
|
7370
|
+
};
|
|
7371
|
+
}
|
|
7372
|
+
};
|
|
7373
|
+
}
|
|
7374
|
+
});
|
|
7375
|
+
|
|
6513
7376
|
// src/mcp/proactive-checker.ts
|
|
6514
7377
|
function extractSelectors(code) {
|
|
6515
7378
|
const results = [];
|
|
@@ -6597,7 +7460,7 @@ async function checkSelectors(mcpSession, url, code) {
|
|
|
6597
7460
|
await mcpSession.navigate(url);
|
|
6598
7461
|
const raw = await mcpSession.snapshot();
|
|
6599
7462
|
if (!raw) {
|
|
6600
|
-
|
|
7463
|
+
logger22.warn({ url }, "Proactive check: snapshot returned null \u2014 skipping validation");
|
|
6601
7464
|
return { url, ok: true, total: selectors.length, matched: 0, missing: 0, checks: [] };
|
|
6602
7465
|
}
|
|
6603
7466
|
const parsed = parseSnapshot(raw);
|
|
@@ -6638,7 +7501,7 @@ async function checkSelectors(mcpSession, url, code) {
|
|
|
6638
7501
|
}
|
|
6639
7502
|
const matched = checks.filter((c) => c.found).length;
|
|
6640
7503
|
const missing = checks.filter((c) => !c.found).length;
|
|
6641
|
-
|
|
7504
|
+
logger22.info(
|
|
6642
7505
|
{ url, total: checks.length, matched, missing },
|
|
6643
7506
|
"Proactive selector check complete"
|
|
6644
7507
|
);
|
|
@@ -6651,14 +7514,14 @@ async function checkSelectors(mcpSession, url, code) {
|
|
|
6651
7514
|
checks
|
|
6652
7515
|
};
|
|
6653
7516
|
}
|
|
6654
|
-
var
|
|
7517
|
+
var logger22, SELECTOR_PATTERNS;
|
|
6655
7518
|
var init_proactive_checker = __esm({
|
|
6656
7519
|
"src/mcp/proactive-checker.ts"() {
|
|
6657
7520
|
"use strict";
|
|
6658
7521
|
init_cjs_shims();
|
|
6659
7522
|
init_snapshot_parser();
|
|
6660
7523
|
init_logger();
|
|
6661
|
-
|
|
7524
|
+
logger22 = createChildLogger("proactive-checker");
|
|
6662
7525
|
SELECTOR_PATTERNS = [
|
|
6663
7526
|
// getByRole('button', { name: 'Login' })
|
|
6664
7527
|
{ regex: /getByRole\(\s*['"](\w+)['"]\s*,\s*\{[^}]*name:\s*['"]([^'"]+)['"]/g, type: "getByRole" },
|
|
@@ -6675,14 +7538,14 @@ var init_proactive_checker = __esm({
|
|
|
6675
7538
|
});
|
|
6676
7539
|
|
|
6677
7540
|
// src/engine/browser-manager.ts
|
|
6678
|
-
var import_playwright,
|
|
7541
|
+
var import_playwright, logger23, BrowserManager;
|
|
6679
7542
|
var init_browser_manager = __esm({
|
|
6680
7543
|
"src/engine/browser-manager.ts"() {
|
|
6681
7544
|
"use strict";
|
|
6682
7545
|
init_cjs_shims();
|
|
6683
7546
|
import_playwright = require("playwright");
|
|
6684
7547
|
init_logger();
|
|
6685
|
-
|
|
7548
|
+
logger23 = createChildLogger("browser-manager");
|
|
6686
7549
|
BrowserManager = class {
|
|
6687
7550
|
browsers = /* @__PURE__ */ new Map();
|
|
6688
7551
|
// ─── Launch ───────────────────────────────────────────────────────────────
|
|
@@ -6692,14 +7555,14 @@ var init_browser_manager = __esm({
|
|
|
6692
7555
|
const launcher = browserName === "firefox" ? import_playwright.firefox : browserName === "webkit" ? import_playwright.webkit : import_playwright.chromium;
|
|
6693
7556
|
const browser = await launcher.launch({ headless: config.headless });
|
|
6694
7557
|
this.browsers.set(browserName, browser);
|
|
6695
|
-
|
|
7558
|
+
logger23.info({ browser: browserName, headless: config.headless }, "Browser launched");
|
|
6696
7559
|
return browser;
|
|
6697
7560
|
}
|
|
6698
7561
|
// ─── Context ──────────────────────────────────────────────────────────────
|
|
6699
7562
|
async newContext(browser, config, videosDir) {
|
|
6700
7563
|
const deviceDescriptor = config.device && import_playwright.devices[config.device] ? import_playwright.devices[config.device] : null;
|
|
6701
7564
|
if (config.device && !deviceDescriptor) {
|
|
6702
|
-
|
|
7565
|
+
logger23.warn({ device: config.device }, "Unknown Playwright device \u2014 falling back to configured viewport");
|
|
6703
7566
|
}
|
|
6704
7567
|
const contextOptions = {
|
|
6705
7568
|
// Do NOT set baseURL here — it causes Playwright to pre-navigate the page
|
|
@@ -6812,7 +7675,7 @@ var init_browser_manager = __esm({
|
|
|
6812
7675
|
close: async () => {
|
|
6813
7676
|
}
|
|
6814
7677
|
};
|
|
6815
|
-
|
|
7678
|
+
logger23.info("Created API-only request context (zero browser)");
|
|
6816
7679
|
return fakePage;
|
|
6817
7680
|
}
|
|
6818
7681
|
/** Closes all API request contexts. */
|
|
@@ -6837,11 +7700,11 @@ var init_browser_manager = __esm({
|
|
|
6837
7700
|
await context.tracing.stop();
|
|
6838
7701
|
}
|
|
6839
7702
|
} catch (err) {
|
|
6840
|
-
|
|
7703
|
+
logger23.debug({ err }, "Tracing stop failed \u2014 ignoring");
|
|
6841
7704
|
}
|
|
6842
7705
|
}
|
|
6843
7706
|
await context.close().catch((err) => {
|
|
6844
|
-
|
|
7707
|
+
logger23.debug({ err }, "Context close failed \u2014 ignoring");
|
|
6845
7708
|
});
|
|
6846
7709
|
}
|
|
6847
7710
|
/** Closes all open browsers and API contexts. Called at the end of every run. */
|
|
@@ -6850,9 +7713,9 @@ var init_browser_manager = __esm({
|
|
|
6850
7713
|
for (const [name, browser] of this.browsers) {
|
|
6851
7714
|
try {
|
|
6852
7715
|
await browser.close();
|
|
6853
|
-
|
|
7716
|
+
logger23.debug({ browser: name }, "Browser closed");
|
|
6854
7717
|
} catch (err) {
|
|
6855
|
-
|
|
7718
|
+
logger23.warn({ err, browser: name }, "Error closing browser");
|
|
6856
7719
|
}
|
|
6857
7720
|
}
|
|
6858
7721
|
this.browsers.clear();
|
|
@@ -6865,13 +7728,13 @@ var init_browser_manager = __esm({
|
|
|
6865
7728
|
});
|
|
6866
7729
|
|
|
6867
7730
|
// src/engine/healing-budget.ts
|
|
6868
|
-
var
|
|
7731
|
+
var logger24, ENVIRONMENTAL_PATTERNS, HealingBudget;
|
|
6869
7732
|
var init_healing_budget = __esm({
|
|
6870
7733
|
"src/engine/healing-budget.ts"() {
|
|
6871
7734
|
"use strict";
|
|
6872
7735
|
init_cjs_shims();
|
|
6873
7736
|
init_logger();
|
|
6874
|
-
|
|
7737
|
+
logger24 = createChildLogger("healing-budget");
|
|
6875
7738
|
ENVIRONMENTAL_PATTERNS = [
|
|
6876
7739
|
/timeout/i,
|
|
6877
7740
|
/ECONNREFUSED/i,
|
|
@@ -6894,7 +7757,7 @@ var init_healing_budget = __esm({
|
|
|
6894
7757
|
// ─── Main decision ────────────────────────────────────────────────────────
|
|
6895
7758
|
shouldHeal(failure) {
|
|
6896
7759
|
if (this.spent >= this.dailyBudget) {
|
|
6897
|
-
|
|
7760
|
+
logger24.warn(
|
|
6898
7761
|
{ spent: this.spent, budget: this.dailyBudget },
|
|
6899
7762
|
"Daily healing budget exhausted"
|
|
6900
7763
|
);
|
|
@@ -6928,7 +7791,7 @@ var init_healing_budget = __esm({
|
|
|
6928
7791
|
/** Call after each AI healing call with the estimated cost. */
|
|
6929
7792
|
recordSpend(amountUsd) {
|
|
6930
7793
|
this.spent += amountUsd;
|
|
6931
|
-
|
|
7794
|
+
logger24.debug({ spend: amountUsd, total: this.spent }, "Healing budget spend recorded");
|
|
6932
7795
|
}
|
|
6933
7796
|
/** Call when a healing attempt itself fails (AI returned bad code, etc.). */
|
|
6934
7797
|
recordHealFailure(stepId) {
|
|
@@ -6950,21 +7813,21 @@ var init_healing_budget = __esm({
|
|
|
6950
7813
|
});
|
|
6951
7814
|
|
|
6952
7815
|
// src/engine/healing-report.ts
|
|
6953
|
-
var
|
|
7816
|
+
var import_crypto8, logger25, HealingReporter;
|
|
6954
7817
|
var init_healing_report = __esm({
|
|
6955
7818
|
"src/engine/healing-report.ts"() {
|
|
6956
7819
|
"use strict";
|
|
6957
7820
|
init_cjs_shims();
|
|
6958
|
-
|
|
7821
|
+
import_crypto8 = require("crypto");
|
|
6959
7822
|
init_healing_store();
|
|
6960
7823
|
init_logger();
|
|
6961
|
-
|
|
7824
|
+
logger25 = createChildLogger("healing-report");
|
|
6962
7825
|
HealingReporter = class {
|
|
6963
7826
|
events = [];
|
|
6964
7827
|
/** Records a successful healing event (called by executor after each healed step). */
|
|
6965
7828
|
record(input) {
|
|
6966
7829
|
const event = {
|
|
6967
|
-
id: (0,
|
|
7830
|
+
id: (0, import_crypto8.randomUUID)(),
|
|
6968
7831
|
runId: input.runId,
|
|
6969
7832
|
suiteId: input.suiteId,
|
|
6970
7833
|
caseId: input.caseId,
|
|
@@ -6980,7 +7843,7 @@ var init_healing_report = __esm({
|
|
|
6980
7843
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
6981
7844
|
};
|
|
6982
7845
|
this.events.push(event);
|
|
6983
|
-
|
|
7846
|
+
logger25.info(
|
|
6984
7847
|
{ stepId: input.stepId, strategy: input.strategy, level: input.level },
|
|
6985
7848
|
"Healing event recorded"
|
|
6986
7849
|
);
|
|
@@ -7006,15 +7869,15 @@ var init_healing_report = __esm({
|
|
|
7006
7869
|
};
|
|
7007
7870
|
try {
|
|
7008
7871
|
await writeHealingReport(rootDir, report);
|
|
7009
|
-
|
|
7872
|
+
logger25.info({ runId, count: this.events.length }, "Healing report written");
|
|
7010
7873
|
} catch (err) {
|
|
7011
|
-
|
|
7874
|
+
logger25.error({ err, runId }, "Failed to write healing report");
|
|
7012
7875
|
}
|
|
7013
7876
|
for (const event of this.events) {
|
|
7014
7877
|
try {
|
|
7015
7878
|
await appendPendingEvent(rootDir, event);
|
|
7016
7879
|
} catch (err) {
|
|
7017
|
-
|
|
7880
|
+
logger25.warn({ err, eventId: event.id }, "Failed to append pending healing event");
|
|
7018
7881
|
}
|
|
7019
7882
|
}
|
|
7020
7883
|
}
|
|
@@ -7206,40 +8069,40 @@ function buildCategoryHtml(caseName, pageLoads, category) {
|
|
|
7206
8069
|
</body>
|
|
7207
8070
|
</html>`;
|
|
7208
8071
|
}
|
|
7209
|
-
var
|
|
8072
|
+
var import_path20, import_crypto9, import_fs_extra17, logger26, AllureReporter;
|
|
7210
8073
|
var init_allure_reporter = __esm({
|
|
7211
8074
|
"src/engine/allure-reporter.ts"() {
|
|
7212
8075
|
"use strict";
|
|
7213
8076
|
init_cjs_shims();
|
|
7214
|
-
|
|
7215
|
-
|
|
7216
|
-
|
|
8077
|
+
import_path20 = __toESM(require("path"));
|
|
8078
|
+
import_crypto9 = require("crypto");
|
|
8079
|
+
import_fs_extra17 = __toESM(require("fs-extra"));
|
|
7217
8080
|
init_logger();
|
|
7218
|
-
|
|
8081
|
+
logger26 = createChildLogger("allure-reporter");
|
|
7219
8082
|
AllureReporter = class {
|
|
7220
8083
|
resultsDir;
|
|
7221
8084
|
environment;
|
|
7222
8085
|
constructor(rootDir, runId, environment = "dev") {
|
|
7223
|
-
this.resultsDir =
|
|
8086
|
+
this.resultsDir = import_path20.default.join(rootDir, "results", "allure-results", runId);
|
|
7224
8087
|
this.environment = environment;
|
|
7225
8088
|
}
|
|
7226
8089
|
/** Writes one allure result JSON per test case + environment.properties. */
|
|
7227
8090
|
async writeResults(runResult) {
|
|
7228
|
-
await
|
|
8091
|
+
await import_fs_extra17.default.ensureDir(this.resultsDir);
|
|
7229
8092
|
const envProps = [
|
|
7230
8093
|
`Environment=${this.environment.toUpperCase()}`,
|
|
7231
8094
|
`Framework=Assuremind`
|
|
7232
8095
|
].join("\n");
|
|
7233
|
-
await
|
|
8096
|
+
await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, "environment.properties"), envProps, "utf-8");
|
|
7234
8097
|
let logFileName;
|
|
7235
8098
|
if (runResult.logFilePath) {
|
|
7236
8099
|
try {
|
|
7237
|
-
logFileName =
|
|
8100
|
+
logFileName = import_path20.default.basename(runResult.logFilePath);
|
|
7238
8101
|
await this.copyAttachment(runResult.logFilePath).catch(() => {
|
|
7239
8102
|
});
|
|
7240
|
-
|
|
8103
|
+
logger26.debug({ logFile: logFileName }, "Log file copied to Allure results");
|
|
7241
8104
|
} catch {
|
|
7242
|
-
|
|
8105
|
+
logger26.debug("Failed to copy log file to Allure results");
|
|
7243
8106
|
}
|
|
7244
8107
|
}
|
|
7245
8108
|
let written = 0;
|
|
@@ -7249,23 +8112,23 @@ var init_allure_reporter = __esm({
|
|
|
7249
8112
|
written++;
|
|
7250
8113
|
}
|
|
7251
8114
|
}
|
|
7252
|
-
|
|
8115
|
+
logger26.info({ dir: this.resultsDir, written }, "Allure results written");
|
|
7253
8116
|
}
|
|
7254
8117
|
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
7255
8118
|
async writeTestResult(suite, tc, logFileName) {
|
|
7256
|
-
const uuid = (0,
|
|
8119
|
+
const uuid = (0, import_crypto9.randomUUID)();
|
|
7257
8120
|
const start = new Date(tc.startedAt).getTime();
|
|
7258
8121
|
const stop2 = new Date(tc.finishedAt).getTime();
|
|
7259
8122
|
const failedStep = tc.steps.find((s) => s.status === "failed");
|
|
7260
8123
|
const testAttachments = [];
|
|
7261
8124
|
if (tc.videoPath) {
|
|
7262
|
-
const videoName =
|
|
7263
|
-
const ext =
|
|
8125
|
+
const videoName = import_path20.default.basename(tc.videoPath);
|
|
8126
|
+
const ext = import_path20.default.extname(videoName).toLowerCase();
|
|
7264
8127
|
const mime = ext === ".mp4" ? "video/mp4" : "video/webm";
|
|
7265
8128
|
testAttachments.push({ name: "Video Recording", source: videoName, type: mime });
|
|
7266
8129
|
}
|
|
7267
8130
|
if (tc.tracePath) {
|
|
7268
|
-
const traceName =
|
|
8131
|
+
const traceName = import_path20.default.basename(tc.tracePath);
|
|
7269
8132
|
testAttachments.push({ name: "Playwright Trace", source: traceName, type: "application/zip" });
|
|
7270
8133
|
}
|
|
7271
8134
|
if (logFileName) {
|
|
@@ -7279,19 +8142,19 @@ var init_allure_reporter = __esm({
|
|
|
7279
8142
|
if (hasPerf) {
|
|
7280
8143
|
const html = buildCategoryHtml(tc.caseName, pageLoads, "performance");
|
|
7281
8144
|
const file = `${uuid}-perf.html`;
|
|
7282
|
-
await
|
|
8145
|
+
await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
|
|
7283
8146
|
testAttachments.push({ name: "\u26A1 Performance Report", source: file, type: "text/html" });
|
|
7284
8147
|
}
|
|
7285
8148
|
if (hasA11y) {
|
|
7286
8149
|
const html = buildCategoryHtml(tc.caseName, pageLoads, "accessibility");
|
|
7287
8150
|
const file = `${uuid}-a11y.html`;
|
|
7288
|
-
await
|
|
8151
|
+
await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
|
|
7289
8152
|
testAttachments.push({ name: "\u267F Accessibility Report", source: file, type: "text/html" });
|
|
7290
8153
|
}
|
|
7291
8154
|
if (hasSeo) {
|
|
7292
8155
|
const html = buildCategoryHtml(tc.caseName, pageLoads, "seo");
|
|
7293
8156
|
const file = `${uuid}-seo.html`;
|
|
7294
|
-
await
|
|
8157
|
+
await import_fs_extra17.default.writeFile(import_path20.default.join(this.resultsDir, file), html, "utf-8");
|
|
7295
8158
|
testAttachments.push({ name: "\u{1F50D} SEO Report", source: file, type: "text/html" });
|
|
7296
8159
|
}
|
|
7297
8160
|
}
|
|
@@ -7319,8 +8182,8 @@ var init_allure_reporter = __esm({
|
|
|
7319
8182
|
statusDetails: { message: failedStep.error }
|
|
7320
8183
|
}
|
|
7321
8184
|
};
|
|
7322
|
-
const filePath =
|
|
7323
|
-
await
|
|
8185
|
+
const filePath = import_path20.default.join(this.resultsDir, `${uuid}-result.json`);
|
|
8186
|
+
await import_fs_extra17.default.writeJson(filePath, result, { spaces: 2 });
|
|
7324
8187
|
for (const step of tc.steps) {
|
|
7325
8188
|
if (step.screenshotPath) {
|
|
7326
8189
|
await this.copyAttachment(step.screenshotPath).catch(() => {
|
|
@@ -7341,7 +8204,7 @@ var init_allure_reporter = __esm({
|
|
|
7341
8204
|
const stepStop = caseStart + step.duration;
|
|
7342
8205
|
const attachments = [];
|
|
7343
8206
|
if (step.screenshotPath) {
|
|
7344
|
-
const name =
|
|
8207
|
+
const name = import_path20.default.basename(step.screenshotPath);
|
|
7345
8208
|
attachments.push({ name: "Screenshot", source: name, type: "image/png" });
|
|
7346
8209
|
}
|
|
7347
8210
|
return {
|
|
@@ -7356,8 +8219,8 @@ var init_allure_reporter = __esm({
|
|
|
7356
8219
|
};
|
|
7357
8220
|
}
|
|
7358
8221
|
async copyAttachment(screenshotPath) {
|
|
7359
|
-
const dest =
|
|
7360
|
-
await
|
|
8222
|
+
const dest = import_path20.default.join(this.resultsDir, import_path20.default.basename(screenshotPath));
|
|
8223
|
+
await import_fs_extra17.default.copy(screenshotPath, dest, { overwrite: true });
|
|
7361
8224
|
}
|
|
7362
8225
|
};
|
|
7363
8226
|
}
|
|
@@ -7365,15 +8228,15 @@ var init_allure_reporter = __esm({
|
|
|
7365
8228
|
|
|
7366
8229
|
// src/engine/allure-generator.ts
|
|
7367
8230
|
async function generateAllureReport(rootDir, runId) {
|
|
7368
|
-
const resultsDir =
|
|
7369
|
-
const reportDir =
|
|
7370
|
-
if (!await
|
|
7371
|
-
|
|
8231
|
+
const resultsDir = import_path21.default.join(rootDir, "results", "allure-results", runId);
|
|
8232
|
+
const reportDir = import_path21.default.join(rootDir, "results", "allure-report", runId);
|
|
8233
|
+
if (!await import_fs_extra18.default.pathExists(resultsDir)) {
|
|
8234
|
+
logger27.debug({ resultsDir }, "No allure-results dir for run \u2014 skipping report generation");
|
|
7372
8235
|
return false;
|
|
7373
8236
|
}
|
|
7374
|
-
const files = await
|
|
8237
|
+
const files = await import_fs_extra18.default.readdir(resultsDir);
|
|
7375
8238
|
if (files.length === 0) {
|
|
7376
|
-
|
|
8239
|
+
logger27.debug({ runId }, "allure-results is empty \u2014 skipping report generation");
|
|
7377
8240
|
return false;
|
|
7378
8241
|
}
|
|
7379
8242
|
try {
|
|
@@ -7382,34 +8245,34 @@ async function generateAllureReport(rootDir, runId) {
|
|
|
7382
8245
|
["generate", resultsDir, "--output", reportDir, "--clean", "--single-file"],
|
|
7383
8246
|
isWindows ? { shell: true } : {}
|
|
7384
8247
|
);
|
|
7385
|
-
|
|
8248
|
+
logger27.info({ reportDir, runId }, "Allure HTML report generated");
|
|
7386
8249
|
return true;
|
|
7387
8250
|
} catch (err) {
|
|
7388
8251
|
const msg = err instanceof Error ? err.message : String(err);
|
|
7389
8252
|
if (msg.includes("java") || msg.includes("Java") || msg.includes("JAVA")) {
|
|
7390
|
-
|
|
8253
|
+
logger27.warn(
|
|
7391
8254
|
"Allure report generation skipped \u2014 Java is not installed. Install Java (https://adoptium.net) to enable Allure HTML reports."
|
|
7392
8255
|
);
|
|
7393
8256
|
} else {
|
|
7394
|
-
|
|
8257
|
+
logger27.warn({ err: msg, runId }, "Allure report generation failed");
|
|
7395
8258
|
}
|
|
7396
8259
|
return false;
|
|
7397
8260
|
}
|
|
7398
8261
|
}
|
|
7399
|
-
var
|
|
8262
|
+
var import_path21, import_fs_extra18, import_child_process2, import_util, execFileAsync, logger27, isWindows, alluireBin;
|
|
7400
8263
|
var init_allure_generator = __esm({
|
|
7401
8264
|
"src/engine/allure-generator.ts"() {
|
|
7402
8265
|
"use strict";
|
|
7403
8266
|
init_cjs_shims();
|
|
7404
|
-
|
|
7405
|
-
|
|
8267
|
+
import_path21 = __toESM(require("path"));
|
|
8268
|
+
import_fs_extra18 = __toESM(require("fs-extra"));
|
|
7406
8269
|
import_child_process2 = require("child_process");
|
|
7407
8270
|
import_util = require("util");
|
|
7408
8271
|
init_logger();
|
|
7409
8272
|
execFileAsync = (0, import_util.promisify)(import_child_process2.execFile);
|
|
7410
|
-
|
|
8273
|
+
logger27 = createChildLogger("allure-generator");
|
|
7411
8274
|
isWindows = process.platform === "win32";
|
|
7412
|
-
alluireBin =
|
|
8275
|
+
alluireBin = import_path21.default.resolve(
|
|
7413
8276
|
__dirname,
|
|
7414
8277
|
isWindows ? "../../node_modules/allure-commandline/dist/bin/allure.bat" : "../../node_modules/.bin/allure"
|
|
7415
8278
|
);
|
|
@@ -7954,7 +8817,7 @@ function parseMultiStrategies(raw) {
|
|
|
7954
8817
|
}
|
|
7955
8818
|
return stripped.split("\n").map((s) => s.trim()).filter((s) => s.length > 0 && s.startsWith("await"));
|
|
7956
8819
|
}
|
|
7957
|
-
var
|
|
8820
|
+
var logger28, SelfHealer;
|
|
7958
8821
|
var init_self_healing = __esm({
|
|
7959
8822
|
"src/engine/self-healing.ts"() {
|
|
7960
8823
|
"use strict";
|
|
@@ -7966,11 +8829,12 @@ var init_self_healing = __esm({
|
|
|
7966
8829
|
init_logger();
|
|
7967
8830
|
init_code_runner();
|
|
7968
8831
|
init_step_type_detector();
|
|
7969
|
-
|
|
8832
|
+
logger28 = createChildLogger("self-healing");
|
|
7970
8833
|
SelfHealer = class {
|
|
7971
|
-
constructor(provider, maxLevel) {
|
|
8834
|
+
constructor(provider, maxLevel, ragManager) {
|
|
7972
8835
|
this.provider = provider;
|
|
7973
8836
|
this.maxLevel = maxLevel;
|
|
8837
|
+
this.ragManager = ragManager;
|
|
7974
8838
|
}
|
|
7975
8839
|
/**
|
|
7976
8840
|
* Attempts to heal a failed step by cycling through levels 1–maxLevel.
|
|
@@ -7981,7 +8845,7 @@ var init_self_healing = __esm({
|
|
|
7981
8845
|
*/
|
|
7982
8846
|
async heal(step, failure, previousSteps, page, variables, baseUrl) {
|
|
7983
8847
|
if (failure.failureKind === "assertion") {
|
|
7984
|
-
|
|
8848
|
+
logger28.info(
|
|
7985
8849
|
{ stepId: step.id },
|
|
7986
8850
|
"Assertion failure \u2014 skipping all healing levels (possible application bug)"
|
|
7987
8851
|
);
|
|
@@ -7990,10 +8854,10 @@ var init_self_healing = __esm({
|
|
|
7990
8854
|
const isApi = step.stepType === "api" || detectStepType(step.instruction) === "api";
|
|
7991
8855
|
for (let level = 1; level <= this.maxLevel; level++) {
|
|
7992
8856
|
if (isApi && (level === 3 || level === 4)) {
|
|
7993
|
-
|
|
8857
|
+
logger28.debug({ stepId: step.id, level }, `Skipping DOM-specific level ${level} for API step`);
|
|
7994
8858
|
continue;
|
|
7995
8859
|
}
|
|
7996
|
-
|
|
8860
|
+
logger28.debug({ stepId: step.id, level, isApi }, `Attempting heal level ${level}`);
|
|
7997
8861
|
try {
|
|
7998
8862
|
const result = await this.tryLevel(
|
|
7999
8863
|
level,
|
|
@@ -8006,14 +8870,14 @@ var init_self_healing = __esm({
|
|
|
8006
8870
|
isApi
|
|
8007
8871
|
);
|
|
8008
8872
|
if (result !== null) {
|
|
8009
|
-
|
|
8873
|
+
logger28.info({ stepId: step.id, level, strategy: result.strategy }, "Healing succeeded");
|
|
8010
8874
|
return result;
|
|
8011
8875
|
}
|
|
8012
8876
|
} catch (err) {
|
|
8013
|
-
|
|
8877
|
+
logger28.debug({ err, level, stepId: step.id }, `Level ${level} healing attempt threw`);
|
|
8014
8878
|
}
|
|
8015
8879
|
}
|
|
8016
|
-
|
|
8880
|
+
logger28.warn({ stepId: step.id, maxLevel: this.maxLevel }, "All healing levels exhausted");
|
|
8017
8881
|
return null;
|
|
8018
8882
|
}
|
|
8019
8883
|
// ─── Level dispatcher ─────────────────────────────────────────────────────
|
|
@@ -8048,7 +8912,7 @@ var init_self_healing = __esm({
|
|
|
8048
8912
|
if (this.provider.supportsVision) {
|
|
8049
8913
|
try {
|
|
8050
8914
|
const som = await captureSetOfMarks(page);
|
|
8051
|
-
|
|
8915
|
+
logger28.debug({ stepId: step.id }, "Level 2: using SoM vision healing");
|
|
8052
8916
|
const code2 = await this.provider.analyzeWithSoM(
|
|
8053
8917
|
som.screenshot,
|
|
8054
8918
|
som.elementMap,
|
|
@@ -8058,13 +8922,27 @@ var init_self_healing = __esm({
|
|
|
8058
8922
|
await executeCode(sanitized2, page);
|
|
8059
8923
|
return { code: sanitized2, level: 2, strategy: "regenerate" };
|
|
8060
8924
|
} catch (visionErr) {
|
|
8061
|
-
|
|
8925
|
+
logger28.debug({ stepId: step.id, err: visionErr }, "SoM vision failed, falling back to text regeneration");
|
|
8062
8926
|
await cleanupSoMOverlay(page);
|
|
8063
8927
|
}
|
|
8064
8928
|
}
|
|
8065
8929
|
const context = await extractPageContext(page, previousSteps, variables);
|
|
8930
|
+
let healingHint = "";
|
|
8931
|
+
if (this.ragManager) {
|
|
8932
|
+
try {
|
|
8933
|
+
const matches = this.ragManager.healingCorpus.retrieve(step.instruction, failure.error, 2);
|
|
8934
|
+
if (matches.length > 0) {
|
|
8935
|
+
healingHint = "\n\nSimilar past fixes that worked:\n" + matches.map(
|
|
8936
|
+
(m, i) => ` ${i + 1}. Error: ${m.error}
|
|
8937
|
+
Fix: ${m.healedCode}`
|
|
8938
|
+
).join("\n");
|
|
8939
|
+
logger28.debug({ stepId: step.id, matches: matches.length }, "RAG healing corpus matches found");
|
|
8940
|
+
}
|
|
8941
|
+
} catch {
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8066
8944
|
const code = await this.provider.healSelector(
|
|
8067
|
-
step.instruction,
|
|
8945
|
+
step.instruction + healingHint,
|
|
8068
8946
|
failure.failedCode,
|
|
8069
8947
|
failure.error,
|
|
8070
8948
|
context
|
|
@@ -8114,12 +8992,12 @@ var init_self_healing = __esm({
|
|
|
8114
8992
|
// Produces inspectable Playwright code (not pixel coordinates).
|
|
8115
8993
|
async level4Visual(step, page, variables, baseUrl) {
|
|
8116
8994
|
if (!this.provider.supportsVision) {
|
|
8117
|
-
|
|
8995
|
+
logger28.debug({ stepId: step.id }, "Skipping SoM visual healing \u2014 provider lacks vision");
|
|
8118
8996
|
return null;
|
|
8119
8997
|
}
|
|
8120
8998
|
try {
|
|
8121
8999
|
const som = await captureSetOfMarks(page);
|
|
8122
|
-
|
|
9000
|
+
logger28.debug(
|
|
8123
9001
|
{ stepId: step.id, elementCount: som.elements.length },
|
|
8124
9002
|
"Level 4: SoM screenshot captured"
|
|
8125
9003
|
);
|
|
@@ -8133,7 +9011,7 @@ var init_self_healing = __esm({
|
|
|
8133
9011
|
return { code: sanitized, level: 4, strategy: "visual" };
|
|
8134
9012
|
} catch (err) {
|
|
8135
9013
|
await cleanupSoMOverlay(page);
|
|
8136
|
-
|
|
9014
|
+
logger28.debug({ stepId: step.id, err }, "Level 4 SoM failed");
|
|
8137
9015
|
return null;
|
|
8138
9016
|
}
|
|
8139
9017
|
}
|
|
@@ -8158,7 +9036,7 @@ var init_self_healing = __esm({
|
|
|
8158
9036
|
}
|
|
8159
9037
|
// ─── Level 6: Manual (human in the loop) ─────────────────────────────────
|
|
8160
9038
|
level6Manual(step, failure) {
|
|
8161
|
-
|
|
9039
|
+
logger28.warn(
|
|
8162
9040
|
{ stepId: step.id, instruction: step.instruction, error: failure.error },
|
|
8163
9041
|
"Level 6: manual intervention required \u2014 marking step for human review"
|
|
8164
9042
|
);
|
|
@@ -8183,17 +9061,17 @@ async function executeStep(step, page, ctx) {
|
|
|
8183
9061
|
const mergedVars = { ...ctx.variables, ...Object.fromEntries(ctx.sharedVariables) };
|
|
8184
9062
|
const hasTokens = step.generatedCode.includes("{{");
|
|
8185
9063
|
if (hasTokens) {
|
|
8186
|
-
|
|
9064
|
+
logger29.warn(
|
|
8187
9065
|
{ stepId: step.id, variableKeys: Object.keys(mergedVars), variableCount: Object.keys(mergedVars).length },
|
|
8188
9066
|
"Interpolating variables into step code"
|
|
8189
9067
|
);
|
|
8190
9068
|
}
|
|
8191
9069
|
const code = interpolate(step.generatedCode, mergedVars, ctx.config.baseUrl);
|
|
8192
9070
|
if (hasTokens) {
|
|
8193
|
-
|
|
9071
|
+
logger29.warn({ stepId: step.id, before: step.generatedCode, after: code }, "Variable interpolation result");
|
|
8194
9072
|
}
|
|
8195
9073
|
if (!code.trim() && step.stepType !== "mock") {
|
|
8196
|
-
|
|
9074
|
+
logger29.warn({ stepId: step.id }, "Step has no generated code \u2014 skipping");
|
|
8197
9075
|
return buildResult(step, "skipped", code, Date.now() - start);
|
|
8198
9076
|
}
|
|
8199
9077
|
if (step.stepType === "mock" && step.mockUrl && step.mockResponse) {
|
|
@@ -8218,7 +9096,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8218
9096
|
const runtimeCtx = {
|
|
8219
9097
|
setVariable: (key, value) => {
|
|
8220
9098
|
ctx.sharedVariables.set(key, value);
|
|
8221
|
-
|
|
9099
|
+
logger29.info({ stepId: step.id, key, value: value.slice(0, 50) }, "Runtime variable set");
|
|
8222
9100
|
},
|
|
8223
9101
|
getVariable: (key) => ctx.sharedVariables.get(key)
|
|
8224
9102
|
};
|
|
@@ -8250,7 +9128,13 @@ async function executeStep(step, page, ctx) {
|
|
|
8250
9128
|
} catch {
|
|
8251
9129
|
}
|
|
8252
9130
|
}
|
|
8253
|
-
if (ctx.
|
|
9131
|
+
if (ctx.ragManager && code.trim()) {
|
|
9132
|
+
try {
|
|
9133
|
+
ctx.ragManager.codeCorpus.ingest(step.instruction, code, page.url());
|
|
9134
|
+
} catch {
|
|
9135
|
+
}
|
|
9136
|
+
}
|
|
9137
|
+
if (ctx.config.screenshot === "on") {
|
|
8254
9138
|
const screenshotPath = await captureScreenshot(page, ctx, step.id, "pass");
|
|
8255
9139
|
return { ...buildResult(step, "passed", code, Date.now() - start, void 0, screenshotPath), navigatedToUrl, auditUrl };
|
|
8256
9140
|
}
|
|
@@ -8258,7 +9142,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8258
9142
|
} catch (err) {
|
|
8259
9143
|
lastErr = err;
|
|
8260
9144
|
if (attempt < stepRetries) {
|
|
8261
|
-
|
|
9145
|
+
logger29.debug({ stepId: step.id, attempt, stepRetries }, "Step failed \u2014 retrying");
|
|
8262
9146
|
continue;
|
|
8263
9147
|
}
|
|
8264
9148
|
}
|
|
@@ -8266,7 +9150,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8266
9150
|
const firstErr = lastErr;
|
|
8267
9151
|
{
|
|
8268
9152
|
const errorMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
8269
|
-
|
|
9153
|
+
logger29.debug({ stepId: step.id, error: errorMsg }, "Step failed on first attempt");
|
|
8270
9154
|
const failAuditUrl = step.runAudit && step.stepType !== "api" ? (() => {
|
|
8271
9155
|
try {
|
|
8272
9156
|
return page.url() || void 0;
|
|
@@ -8280,7 +9164,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8280
9164
|
}
|
|
8281
9165
|
const failureKind = classifyError(errorMsg);
|
|
8282
9166
|
if (failureKind === "assertion") {
|
|
8283
|
-
|
|
9167
|
+
logger29.info(
|
|
8284
9168
|
{ stepId: step.id, error: errorMsg },
|
|
8285
9169
|
"Assertion failure detected \u2014 NOT healing (possible application bug)"
|
|
8286
9170
|
);
|
|
@@ -8289,7 +9173,13 @@ async function executeStep(step, page, ctx) {
|
|
|
8289
9173
|
if (!ctx.config.healing.enabled) {
|
|
8290
9174
|
return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
|
|
8291
9175
|
}
|
|
8292
|
-
|
|
9176
|
+
if (ctx.ragManager) {
|
|
9177
|
+
try {
|
|
9178
|
+
ctx.ragManager.errorCatalog.record(errorMsg, page.url());
|
|
9179
|
+
} catch {
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
logger29.debug({ stepId: step.id, failureKind }, "Infrastructure failure \u2014 attempting to heal");
|
|
8293
9183
|
const failure = {
|
|
8294
9184
|
stepId: step.id,
|
|
8295
9185
|
instruction: step.instruction,
|
|
@@ -8300,10 +9190,10 @@ async function executeStep(step, page, ctx) {
|
|
|
8300
9190
|
};
|
|
8301
9191
|
const decision = ctx.budget.shouldHeal(failure);
|
|
8302
9192
|
if (!decision.heal) {
|
|
8303
|
-
|
|
9193
|
+
logger29.info({ stepId: step.id, reason: decision.reason }, "Healing skipped");
|
|
8304
9194
|
return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
|
|
8305
9195
|
}
|
|
8306
|
-
const healer = new SelfHealer(ctx.provider, ctx.config.healing.maxLevel);
|
|
9196
|
+
const healer = new SelfHealer(ctx.provider, ctx.config.healing.maxLevel, ctx.ragManager);
|
|
8307
9197
|
try {
|
|
8308
9198
|
const healResult = await healer.heal(
|
|
8309
9199
|
step,
|
|
@@ -8317,6 +9207,21 @@ async function executeStep(step, page, ctx) {
|
|
|
8317
9207
|
const codeChanged = healResult.code.trim() !== code.trim();
|
|
8318
9208
|
if (codeChanged) {
|
|
8319
9209
|
ctx.budget.recordSpend(5e-3);
|
|
9210
|
+
if (ctx.ragManager) {
|
|
9211
|
+
try {
|
|
9212
|
+
ctx.ragManager.healingCorpus.ingest({
|
|
9213
|
+
instruction: step.instruction,
|
|
9214
|
+
error: errorMsg,
|
|
9215
|
+
failedCode: code,
|
|
9216
|
+
healedCode: healResult.code,
|
|
9217
|
+
strategy: healResult.strategy,
|
|
9218
|
+
level: healResult.level,
|
|
9219
|
+
url: page.url()
|
|
9220
|
+
});
|
|
9221
|
+
ctx.ragManager.errorCatalog.record(errorMsg, page.url(), healResult.code);
|
|
9222
|
+
} catch {
|
|
9223
|
+
}
|
|
9224
|
+
}
|
|
8320
9225
|
ctx.healingReporter.record({
|
|
8321
9226
|
runId: ctx.runId,
|
|
8322
9227
|
suiteId: ctx.suiteId,
|
|
@@ -8331,7 +9236,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8331
9236
|
pageUrl: page.url()
|
|
8332
9237
|
});
|
|
8333
9238
|
}
|
|
8334
|
-
|
|
9239
|
+
logger29.info(
|
|
8335
9240
|
{ stepId: step.id, level: healResult.level, strategy: healResult.strategy, codeChanged },
|
|
8336
9241
|
codeChanged ? "Step passed after healing (code NOT saved \u2014 awaits human review)" : "Step passed after smart retry (no code change)"
|
|
8337
9242
|
);
|
|
@@ -8347,7 +9252,7 @@ async function executeStep(step, page, ctx) {
|
|
|
8347
9252
|
}
|
|
8348
9253
|
} catch (healErr) {
|
|
8349
9254
|
const healErrMsg = healErr instanceof Error ? healErr.message : String(healErr);
|
|
8350
|
-
|
|
9255
|
+
logger29.warn({ stepId: step.id, error: healErrMsg }, "Healing threw an error");
|
|
8351
9256
|
ctx.budget.recordHealFailure(step.id);
|
|
8352
9257
|
}
|
|
8353
9258
|
return { ...buildResult(step, "failed", code, Date.now() - start, errorMsg, screenshotPath), ...failAuditUrl ? { auditUrl: failAuditUrl } : {} };
|
|
@@ -8371,13 +9276,13 @@ async function captureScreenshot(page, ctx, stepId, suffix) {
|
|
|
8371
9276
|
try {
|
|
8372
9277
|
await page.waitForLoadState("load", { timeout: 5e3 }).catch(() => {
|
|
8373
9278
|
});
|
|
8374
|
-
const screenshotsDir =
|
|
9279
|
+
const screenshotsDir = import_path22.default.join(ctx.rootDir, "results", "screenshots");
|
|
8375
9280
|
const filename = `${ctx.runId}_${ctx.caseId}_${stepId}_${suffix}.png`;
|
|
8376
|
-
const screenshotPath =
|
|
9281
|
+
const screenshotPath = import_path22.default.join(screenshotsDir, filename);
|
|
8377
9282
|
await page.screenshot({ path: screenshotPath, fullPage: true });
|
|
8378
9283
|
return screenshotPath;
|
|
8379
9284
|
} catch (err) {
|
|
8380
|
-
|
|
9285
|
+
logger29.debug({ err }, "Screenshot capture failed");
|
|
8381
9286
|
return void 0;
|
|
8382
9287
|
}
|
|
8383
9288
|
}
|
|
@@ -8389,19 +9294,19 @@ function withTimeout(promise, ms) {
|
|
|
8389
9294
|
)
|
|
8390
9295
|
]);
|
|
8391
9296
|
}
|
|
8392
|
-
var
|
|
9297
|
+
var import_path22, logger29, ASSERTION_PATTERNS, INFRA_PATTERNS;
|
|
8393
9298
|
var init_executor = __esm({
|
|
8394
9299
|
"src/engine/executor.ts"() {
|
|
8395
9300
|
"use strict";
|
|
8396
9301
|
init_cjs_shims();
|
|
8397
|
-
|
|
9302
|
+
import_path22 = __toESM(require("path"));
|
|
8398
9303
|
init_variable_interpolator();
|
|
8399
9304
|
init_code_runner();
|
|
8400
9305
|
init_self_healing();
|
|
8401
9306
|
init_step_type_detector();
|
|
8402
9307
|
init_logger();
|
|
8403
9308
|
init_code_runner();
|
|
8404
|
-
|
|
9309
|
+
logger29 = createChildLogger("executor");
|
|
8405
9310
|
ASSERTION_PATTERNS = [
|
|
8406
9311
|
/expect\(.*\)\.(toBe|toEqual|toContain|toMatch|toHaveText|toHaveValue|toBeChecked|toBeVisible|toBeHidden|toBeEnabled|toBeDisabled|toHaveCount|toHaveAttribute|toHaveClass|toHaveCSS|toHaveURL|toHaveTitle)/i,
|
|
8407
9312
|
/AssertionError/i,
|
|
@@ -8618,24 +9523,24 @@ async function loadDataRows(rootDir, dataSource) {
|
|
|
8618
9523
|
return dataSource.data ?? [];
|
|
8619
9524
|
case "json-file": {
|
|
8620
9525
|
if (!dataSource.path) throw new Error("JSON data source requires a path");
|
|
8621
|
-
const filePath =
|
|
8622
|
-
if (!await
|
|
9526
|
+
const filePath = import_path23.default.resolve(rootDir, dataSource.path);
|
|
9527
|
+
if (!await import_fs_extra19.default.pathExists(filePath)) {
|
|
8623
9528
|
throw new Error(`Data file not found: ${filePath}`);
|
|
8624
9529
|
}
|
|
8625
|
-
const raw = await
|
|
9530
|
+
const raw = await import_fs_extra19.default.readJson(filePath);
|
|
8626
9531
|
if (!Array.isArray(raw)) throw new Error("JSON data file must contain an array of objects");
|
|
8627
|
-
|
|
9532
|
+
logger30.info({ path: filePath, rows: raw.length }, "Loaded JSON data rows");
|
|
8628
9533
|
return raw.map(
|
|
8629
9534
|
(row) => Object.fromEntries(Object.entries(row).map(([k, v]) => [k, String(v)]))
|
|
8630
9535
|
);
|
|
8631
9536
|
}
|
|
8632
9537
|
case "csv-file": {
|
|
8633
9538
|
if (!dataSource.path) throw new Error("CSV data source requires a path");
|
|
8634
|
-
const filePath =
|
|
8635
|
-
if (!await
|
|
9539
|
+
const filePath = import_path23.default.resolve(rootDir, dataSource.path);
|
|
9540
|
+
if (!await import_fs_extra19.default.pathExists(filePath)) {
|
|
8636
9541
|
throw new Error(`Data file not found: ${filePath}`);
|
|
8637
9542
|
}
|
|
8638
|
-
const content = await
|
|
9543
|
+
const content = await import_fs_extra19.default.readFile(filePath, "utf8");
|
|
8639
9544
|
const lines = content.trim().split("\n").map((l) => l.trim()).filter(Boolean);
|
|
8640
9545
|
if (lines.length < 2) return [];
|
|
8641
9546
|
const headers = lines[0].split(",").map((h) => h.trim());
|
|
@@ -8647,22 +9552,22 @@ async function loadDataRows(rootDir, dataSource) {
|
|
|
8647
9552
|
});
|
|
8648
9553
|
return row;
|
|
8649
9554
|
});
|
|
8650
|
-
|
|
9555
|
+
logger30.info({ path: filePath, rows: rows.length }, "Loaded CSV data rows");
|
|
8651
9556
|
return rows;
|
|
8652
9557
|
}
|
|
8653
9558
|
default:
|
|
8654
9559
|
return [];
|
|
8655
9560
|
}
|
|
8656
9561
|
}
|
|
8657
|
-
var
|
|
9562
|
+
var import_path23, import_fs_extra19, logger30;
|
|
8658
9563
|
var init_data_loader = __esm({
|
|
8659
9564
|
"src/engine/data-loader.ts"() {
|
|
8660
9565
|
"use strict";
|
|
8661
9566
|
init_cjs_shims();
|
|
8662
|
-
|
|
8663
|
-
|
|
9567
|
+
import_path23 = __toESM(require("path"));
|
|
9568
|
+
import_fs_extra19 = __toESM(require("fs-extra"));
|
|
8664
9569
|
init_logger();
|
|
8665
|
-
|
|
9570
|
+
logger30 = createChildLogger("data-loader");
|
|
8666
9571
|
}
|
|
8667
9572
|
});
|
|
8668
9573
|
|
|
@@ -9922,16 +10827,16 @@ function makeUnixTmpDir() {
|
|
|
9922
10827
|
function makeWin32TmpDir() {
|
|
9923
10828
|
const winTmpPath = process.env.TEMP || process.env.TMP || (process.env.SystemRoot || process.env.windir) + "\\temp";
|
|
9924
10829
|
const randomNumber = Math.floor(Math.random() * 9e7 + 1e7);
|
|
9925
|
-
const tmpdir = (0,
|
|
10830
|
+
const tmpdir = (0, import_path24.join)(winTmpPath, "lighthouse." + randomNumber);
|
|
9926
10831
|
(0, import_fs.mkdirSync)(tmpdir, { recursive: true });
|
|
9927
10832
|
return tmpdir;
|
|
9928
10833
|
}
|
|
9929
|
-
var
|
|
10834
|
+
var import_path24, import_child_process3, import_fs, import_is_wsl, LauncherError, ChromePathNotSetError, InvalidUserDataDirectoryError, UnsupportedPlatformError, ChromeNotInstalledError;
|
|
9930
10835
|
var init_utils3 = __esm({
|
|
9931
10836
|
"node_modules/chrome-launcher/dist/utils.js"() {
|
|
9932
10837
|
"use strict";
|
|
9933
10838
|
init_cjs_shims();
|
|
9934
|
-
|
|
10839
|
+
import_path24 = require("path");
|
|
9935
10840
|
import_child_process3 = __toESM(require("child_process"), 1);
|
|
9936
10841
|
import_fs = require("fs");
|
|
9937
10842
|
import_is_wsl = __toESM(require_is_wsl(), 1);
|
|
@@ -10007,7 +10912,7 @@ function darwin() {
|
|
|
10007
10912
|
}
|
|
10008
10913
|
(0, import_child_process4.execSync)(`${LSREGISTER} -dump | grep -i 'google chrome\\( canary\\)\\?\\.app' | awk '{$1=""; print $0}'`).toString().split(newLineRegex).forEach((inst) => {
|
|
10009
10914
|
suffixes.forEach((suffix) => {
|
|
10010
|
-
const execPath =
|
|
10915
|
+
const execPath = import_path25.default.join(inst.substring(0, inst.indexOf(".app") + 4).trim(), suffix);
|
|
10011
10916
|
if (canAccess(execPath) && installations.indexOf(execPath) === -1) {
|
|
10012
10917
|
installations.push(execPath);
|
|
10013
10918
|
}
|
|
@@ -10047,7 +10952,7 @@ function linux() {
|
|
|
10047
10952
|
installations.push(customChromePath);
|
|
10048
10953
|
}
|
|
10049
10954
|
const desktopInstallationFolders = [
|
|
10050
|
-
|
|
10955
|
+
import_path25.default.join((0, import_os.homedir)(), ".local/share/applications/"),
|
|
10051
10956
|
"/usr/share/applications/"
|
|
10052
10957
|
];
|
|
10053
10958
|
desktopInstallationFolders.forEach((folder) => {
|
|
@@ -10095,8 +11000,8 @@ function wsl() {
|
|
|
10095
11000
|
function win32() {
|
|
10096
11001
|
const installations = [];
|
|
10097
11002
|
const suffixes = [
|
|
10098
|
-
`${
|
|
10099
|
-
`${
|
|
11003
|
+
`${import_path25.default.sep}Google${import_path25.default.sep}Chrome SxS${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`,
|
|
11004
|
+
`${import_path25.default.sep}Google${import_path25.default.sep}Chrome${import_path25.default.sep}Application${import_path25.default.sep}chrome.exe`
|
|
10100
11005
|
];
|
|
10101
11006
|
const prefixes = [
|
|
10102
11007
|
process.env.LOCALAPPDATA,
|
|
@@ -10108,7 +11013,7 @@ function win32() {
|
|
|
10108
11013
|
installations.push(customChromePath);
|
|
10109
11014
|
}
|
|
10110
11015
|
prefixes.forEach((prefix) => suffixes.forEach((suffix) => {
|
|
10111
|
-
const chromePath =
|
|
11016
|
+
const chromePath = import_path25.default.join(prefix, suffix);
|
|
10112
11017
|
if (canAccess(chromePath)) {
|
|
10113
11018
|
installations.push(chromePath);
|
|
10114
11019
|
}
|
|
@@ -10156,13 +11061,13 @@ function findChromeExecutables(folder) {
|
|
|
10156
11061
|
}
|
|
10157
11062
|
return installations;
|
|
10158
11063
|
}
|
|
10159
|
-
var import_fs2,
|
|
11064
|
+
var import_fs2, import_path25, import_os, import_child_process4, import_escape_string_regexp, newLineRegex;
|
|
10160
11065
|
var init_chrome_finder = __esm({
|
|
10161
11066
|
"node_modules/chrome-launcher/dist/chrome-finder.js"() {
|
|
10162
11067
|
"use strict";
|
|
10163
11068
|
init_cjs_shims();
|
|
10164
11069
|
import_fs2 = __toESM(require("fs"), 1);
|
|
10165
|
-
|
|
11070
|
+
import_path25 = __toESM(require("path"), 1);
|
|
10166
11071
|
import_os = require("os");
|
|
10167
11072
|
import_child_process4 = require("child_process");
|
|
10168
11073
|
import_escape_string_regexp = __toESM(require_escape_string_regexp(), 1);
|
|
@@ -10633,15 +11538,15 @@ function isSuiteAudit(suite) {
|
|
|
10633
11538
|
return suite.type === "audit" || suite.type === "performance";
|
|
10634
11539
|
}
|
|
10635
11540
|
async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
10636
|
-
const runId = (0,
|
|
11541
|
+
const runId = (0, import_crypto10.randomUUID)();
|
|
10637
11542
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10638
|
-
const testsDir =
|
|
11543
|
+
const testsDir = import_path26.default.join(rootDir, "tests");
|
|
10639
11544
|
const activeEnv = autotestConfig.environment ?? "dev";
|
|
10640
11545
|
const envUrl = autotestConfig.environmentUrls?.[activeEnv];
|
|
10641
11546
|
if (envUrl) {
|
|
10642
11547
|
autotestConfig = { ...autotestConfig, baseUrl: envUrl };
|
|
10643
11548
|
}
|
|
10644
|
-
|
|
11549
|
+
logger31.info({ runId, runConfig, environment: activeEnv, baseUrl: autotestConfig.baseUrl }, "Run started");
|
|
10645
11550
|
const runLog = await createRunLogFile(rootDir, runId);
|
|
10646
11551
|
const logToFile = (level, msg, data) => {
|
|
10647
11552
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -10655,7 +11560,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10655
11560
|
try {
|
|
10656
11561
|
const pairs = await collectWork(testsDir, runConfig);
|
|
10657
11562
|
if (pairs.length === 0) {
|
|
10658
|
-
|
|
11563
|
+
logger31.warn({ runConfig }, "No tests matched the run config");
|
|
10659
11564
|
}
|
|
10660
11565
|
const variables = await loadVariables(rootDir, runConfig.env);
|
|
10661
11566
|
const sharedVariables = /* @__PURE__ */ new Map();
|
|
@@ -10666,18 +11571,18 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10666
11571
|
const rows = await loadDataRows(rootDir, pair.testCase.dataSource);
|
|
10667
11572
|
if (rows.length > 0) {
|
|
10668
11573
|
rows.forEach((row, idx) => expandedPairs.push({ ...pair, dataRow: row, dataRowIndex: idx }));
|
|
10669
|
-
|
|
11574
|
+
logger31.info({ caseId: pair.testCase.id, rows: rows.length }, "Expanded data-driven case");
|
|
10670
11575
|
continue;
|
|
10671
11576
|
}
|
|
10672
11577
|
} catch (err) {
|
|
10673
|
-
|
|
11578
|
+
logger31.warn({ err, caseId: pair.testCase.id }, "Failed to load data rows \u2014 running case once");
|
|
10674
11579
|
}
|
|
10675
11580
|
}
|
|
10676
11581
|
expandedPairs.push(pair);
|
|
10677
11582
|
}
|
|
10678
11583
|
ws?.broadcast("run:start", { runId, totalTests: expandedPairs.length });
|
|
10679
11584
|
if (autotestConfig.mcp?.proactiveHealing && autotestConfig.mcp?.enabled) {
|
|
10680
|
-
|
|
11585
|
+
logger31.info("Proactive healing enabled \u2014 validating selectors before run");
|
|
10681
11586
|
let mcpChecker = null;
|
|
10682
11587
|
try {
|
|
10683
11588
|
mcpChecker = new McpSession({
|
|
@@ -10700,7 +11605,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10700
11605
|
suggestion: c.suggestion
|
|
10701
11606
|
}))
|
|
10702
11607
|
});
|
|
10703
|
-
|
|
11608
|
+
logger31.warn(
|
|
10704
11609
|
{ stepId: step.id, missing: result.missing },
|
|
10705
11610
|
`Proactive check: ${result.missing} selector(s) may be broken`
|
|
10706
11611
|
);
|
|
@@ -10708,12 +11613,22 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10708
11613
|
}
|
|
10709
11614
|
}
|
|
10710
11615
|
} catch (err) {
|
|
10711
|
-
|
|
11616
|
+
logger31.warn({ err }, "Proactive healing check failed \u2014 continuing with run");
|
|
10712
11617
|
} finally {
|
|
10713
11618
|
await mcpChecker?.disconnect();
|
|
10714
11619
|
}
|
|
10715
11620
|
}
|
|
10716
|
-
|
|
11621
|
+
let ragManager;
|
|
11622
|
+
if (autotestConfig.rag?.enabled) {
|
|
11623
|
+
try {
|
|
11624
|
+
ragManager = await RagManager.create(rootDir, autotestConfig.rag);
|
|
11625
|
+
logger31.info(ragManager.getStats(), "RAG memory loaded");
|
|
11626
|
+
logToFile("info", "RAG memory loaded", ragManager.getStats());
|
|
11627
|
+
} catch (err) {
|
|
11628
|
+
logger31.warn({ err }, "RAG initialization failed \u2014 continuing without memory");
|
|
11629
|
+
}
|
|
11630
|
+
}
|
|
11631
|
+
const smartRouter = createSmartRouter(rootDir, void 0, void 0, ragManager);
|
|
10717
11632
|
const provider = smartRouter.getPrimaryProvider();
|
|
10718
11633
|
const budget = new HealingBudget(autotestConfig.healing.dailyBudget);
|
|
10719
11634
|
const browsers = autotestConfig.browsers;
|
|
@@ -10722,7 +11637,7 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10722
11637
|
({ suite, testCase }) => (isSuiteApiOnly(suite) || isCaseApiOnly(testCase)) && !isSuiteAudit(suite)
|
|
10723
11638
|
);
|
|
10724
11639
|
if (allApiOnly) {
|
|
10725
|
-
|
|
11640
|
+
logger31.info("All test cases are API-only \u2014 no browser will be launched");
|
|
10726
11641
|
logToFile("info", "API-only run detected \u2014 skipping browser launch");
|
|
10727
11642
|
}
|
|
10728
11643
|
const browserInstances = /* @__PURE__ */ new Map();
|
|
@@ -10776,7 +11691,8 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10776
11691
|
browserManager,
|
|
10777
11692
|
ws,
|
|
10778
11693
|
logToFile,
|
|
10779
|
-
compositeCaseId
|
|
11694
|
+
compositeCaseId,
|
|
11695
|
+
ragManager
|
|
10780
11696
|
);
|
|
10781
11697
|
result.suiteType = suite.type;
|
|
10782
11698
|
if (dataRow !== void 0) {
|
|
@@ -10826,13 +11742,21 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10826
11742
|
generateAllureReport(rootDir, runId).catch(() => void 0);
|
|
10827
11743
|
}
|
|
10828
11744
|
if (autotestConfig.reporting.json) {
|
|
10829
|
-
const jsonPath =
|
|
10830
|
-
await
|
|
10831
|
-
await
|
|
11745
|
+
const jsonPath = import_path26.default.join(rootDir, "results", `run-${runId}.json`);
|
|
11746
|
+
await import_fs_extra20.default.ensureDir(import_path26.default.dirname(jsonPath));
|
|
11747
|
+
await import_fs_extra20.default.writeJson(jsonPath, runResult, { spaces: 2 });
|
|
10832
11748
|
}
|
|
10833
11749
|
await healingReporter.flush(rootDir, runId);
|
|
10834
11750
|
await smartRouter.getCache().persist();
|
|
10835
|
-
|
|
11751
|
+
if (ragManager) {
|
|
11752
|
+
try {
|
|
11753
|
+
await ragManager.persist();
|
|
11754
|
+
logger31.info(ragManager.getStats(), "RAG memory persisted");
|
|
11755
|
+
} catch (err) {
|
|
11756
|
+
logger31.warn({ err }, "RAG persistence failed");
|
|
11757
|
+
}
|
|
11758
|
+
}
|
|
11759
|
+
logger31.info(
|
|
10836
11760
|
{
|
|
10837
11761
|
runId,
|
|
10838
11762
|
passed: runResult.passed,
|
|
@@ -10847,22 +11771,22 @@ async function runTests(rootDir, runConfig, autotestConfig, ws) {
|
|
|
10847
11771
|
await browserManager.closeAll();
|
|
10848
11772
|
}
|
|
10849
11773
|
}
|
|
10850
|
-
async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId) {
|
|
11774
|
+
async function runCase(rootDir, runId, suite, testCase, browserName, browser, config, variables, sharedVariables, provider, budget, healingReporter, browserManager, ws, logToFile, wsCaseId, ragManager) {
|
|
10851
11775
|
const broadcastCaseId = wsCaseId ?? testCase.id;
|
|
10852
11776
|
const caseStart = (/* @__PURE__ */ new Date()).toISOString();
|
|
10853
11777
|
const stepResults = [];
|
|
10854
11778
|
const previousSteps = [];
|
|
10855
11779
|
const apiOnly = isSuiteApiOnly(suite) || isCaseApiOnly(testCase);
|
|
10856
11780
|
const caseConfig = isSuiteAudit(suite) ? { ...config, screenshot: "off", video: "off", trace: "off" } : config;
|
|
10857
|
-
const tracePath = !apiOnly && caseConfig.trace !== "off" ?
|
|
11781
|
+
const tracePath = !apiOnly && caseConfig.trace !== "off" ? import_path26.default.join(rootDir, "results", "traces", `${runId}_${testCase.id}_${browserName}.zip`) : void 0;
|
|
10858
11782
|
let context = null;
|
|
10859
11783
|
let page;
|
|
10860
11784
|
if (apiOnly) {
|
|
10861
11785
|
page = await browserManager.createApiOnlyPage(config.baseUrl);
|
|
10862
|
-
|
|
11786
|
+
logger31.debug({ caseName: testCase.name }, "API-only case \u2014 zero browser, using standalone HTTP client");
|
|
10863
11787
|
} else {
|
|
10864
11788
|
if (!browser) throw new Error("Browser required for UI test cases");
|
|
10865
|
-
context = await browserManager.newContext(browser, caseConfig,
|
|
11789
|
+
context = await browserManager.newContext(browser, caseConfig, import_path26.default.join(rootDir, "results", "videos"));
|
|
10866
11790
|
page = await browserManager.newPage(context);
|
|
10867
11791
|
await installHighlighter(page);
|
|
10868
11792
|
}
|
|
@@ -10932,7 +11856,8 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
|
|
|
10932
11856
|
config: effectiveConfig,
|
|
10933
11857
|
provider,
|
|
10934
11858
|
budget,
|
|
10935
|
-
healingReporter
|
|
11859
|
+
healingReporter,
|
|
11860
|
+
ragManager
|
|
10936
11861
|
});
|
|
10937
11862
|
ws?.broadcast("run:step", {
|
|
10938
11863
|
runId,
|
|
@@ -10996,16 +11921,16 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
|
|
|
10996
11921
|
const launch2 = chromeLauncher.launch ?? chromeLauncher.default?.launch;
|
|
10997
11922
|
const { chromium: pwChromium } = await import("playwright");
|
|
10998
11923
|
const chromePath = pwChromium.executablePath();
|
|
10999
|
-
const lhBaseDir =
|
|
11000
|
-
await
|
|
11924
|
+
const lhBaseDir = import_path26.default.join(rootDir, "results", ".lh-profiles");
|
|
11925
|
+
await import_fs_extra20.default.ensureDir(lhBaseDir);
|
|
11001
11926
|
for (let auditIdx = 0; auditIdx < navigatedSteps.length; auditIdx++) {
|
|
11002
11927
|
const { s, idx } = navigatedSteps[auditIdx];
|
|
11003
11928
|
const url = s.auditUrl ?? s.navigatedToUrl;
|
|
11004
11929
|
logToFile?.("info", ` Auditing: ${url}`);
|
|
11005
11930
|
let chromeInstance = null;
|
|
11006
|
-
const userDataDir =
|
|
11931
|
+
const userDataDir = import_path26.default.join(lhBaseDir, `audit-${Date.now()}-${auditIdx}`);
|
|
11007
11932
|
try {
|
|
11008
|
-
await
|
|
11933
|
+
await import_fs_extra20.default.ensureDir(userDataDir);
|
|
11009
11934
|
chromeInstance = await launch2({
|
|
11010
11935
|
chromePath,
|
|
11011
11936
|
chromeFlags: ["--headless", "--no-sandbox", "--disable-gpu"],
|
|
@@ -11081,7 +12006,7 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
|
|
|
11081
12006
|
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" : "--"}`);
|
|
11082
12007
|
} catch (auditErr) {
|
|
11083
12008
|
const errMsg = auditErr instanceof Error ? auditErr.message : String(auditErr);
|
|
11084
|
-
|
|
12009
|
+
logger31.warn({ url, err: errMsg }, "Lighthouse audit failed for URL");
|
|
11085
12010
|
logToFile?.("warn", ` Lighthouse failed for ${url}: ${errMsg}`);
|
|
11086
12011
|
pageLoads.push({
|
|
11087
12012
|
url,
|
|
@@ -11095,14 +12020,14 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
|
|
|
11095
12020
|
} catch {
|
|
11096
12021
|
}
|
|
11097
12022
|
try {
|
|
11098
|
-
await
|
|
12023
|
+
await import_fs_extra20.default.remove(userDataDir);
|
|
11099
12024
|
} catch {
|
|
11100
12025
|
}
|
|
11101
12026
|
}
|
|
11102
12027
|
}
|
|
11103
12028
|
} catch (lhErr) {
|
|
11104
12029
|
const errMsg = lhErr instanceof Error ? lhErr.message : String(lhErr);
|
|
11105
|
-
|
|
12030
|
+
logger31.warn({ err: errMsg }, "Lighthouse import/init failed");
|
|
11106
12031
|
logToFile?.("warn", ` Lighthouse init failed: ${errMsg}`);
|
|
11107
12032
|
const recordedIndices = new Set(pageLoads.map((p) => p.stepIndex));
|
|
11108
12033
|
for (const { s, idx } of navigatedSteps) {
|
|
@@ -11148,11 +12073,11 @@ async function runCase(rootDir, runId, suite, testCase, browserName, browser, co
|
|
|
11148
12073
|
await browserManager.closeContext(context, caseConfig, tracePath);
|
|
11149
12074
|
const videoDeleteOnPass = caseConfig.video === "retain-on-failure" || caseConfig.video === "on-first-retry";
|
|
11150
12075
|
if (videoDeleteOnPass && caseStatus === "passed" && pendingVideoPath) {
|
|
11151
|
-
await
|
|
12076
|
+
await import_fs_extra20.default.remove(pendingVideoPath).catch(() => void 0);
|
|
11152
12077
|
}
|
|
11153
12078
|
const traceDeleteOnPass = caseConfig.trace === "retain-on-failure" || caseConfig.trace === "on-first-retry";
|
|
11154
12079
|
if (traceDeleteOnPass && caseStatus === "passed" && tracePath) {
|
|
11155
|
-
await
|
|
12080
|
+
await import_fs_extra20.default.remove(tracePath).catch(() => void 0);
|
|
11156
12081
|
}
|
|
11157
12082
|
}
|
|
11158
12083
|
}
|
|
@@ -11198,7 +12123,7 @@ async function collectWork(testsDir, runConfig) {
|
|
|
11198
12123
|
}
|
|
11199
12124
|
const suiteDir = await findSuiteDirById(testsDir, suite.id);
|
|
11200
12125
|
if (!suiteDir) {
|
|
11201
|
-
|
|
12126
|
+
logger31.warn({ suiteId: suite.id }, "Suite directory not found \u2014 skipping");
|
|
11202
12127
|
continue;
|
|
11203
12128
|
}
|
|
11204
12129
|
const cases = await listCases(suiteDir);
|
|
@@ -11220,10 +12145,10 @@ async function loadVariables(rootDir, env) {
|
|
|
11220
12145
|
try {
|
|
11221
12146
|
const resolved = await resolveVariables(rootDir, env ?? "dev");
|
|
11222
12147
|
const count = Object.keys(resolved).length;
|
|
11223
|
-
|
|
12148
|
+
logger31.info({ count, env: env ?? "dev", rootDir }, "Variables loaded");
|
|
11224
12149
|
return resolved;
|
|
11225
12150
|
} catch (err) {
|
|
11226
|
-
|
|
12151
|
+
logger31.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to load variables \u2014 running without variable substitution");
|
|
11227
12152
|
return {};
|
|
11228
12153
|
}
|
|
11229
12154
|
}
|
|
@@ -11294,20 +12219,21 @@ function buildRunResult(runId, startedAt, finishedAt, suites, environment) {
|
|
|
11294
12219
|
skipped
|
|
11295
12220
|
};
|
|
11296
12221
|
}
|
|
11297
|
-
var
|
|
12222
|
+
var import_path26, import_crypto10, import_fs_extra20, logger31;
|
|
11298
12223
|
var init_runner = __esm({
|
|
11299
12224
|
"src/engine/runner.ts"() {
|
|
11300
12225
|
"use strict";
|
|
11301
12226
|
init_cjs_shims();
|
|
11302
|
-
|
|
11303
|
-
|
|
11304
|
-
|
|
12227
|
+
import_path26 = __toESM(require("path"));
|
|
12228
|
+
import_crypto10 = require("crypto");
|
|
12229
|
+
import_fs_extra20 = __toESM(require("fs-extra"));
|
|
11305
12230
|
init_suite_store();
|
|
11306
12231
|
init_case_store();
|
|
11307
12232
|
init_variable_store();
|
|
11308
12233
|
init_result_store();
|
|
11309
12234
|
init_router();
|
|
11310
12235
|
init_mcp_session();
|
|
12236
|
+
init_rag_manager();
|
|
11311
12237
|
init_proactive_checker();
|
|
11312
12238
|
init_browser_manager();
|
|
11313
12239
|
init_healing_budget();
|
|
@@ -11319,7 +12245,7 @@ var init_runner = __esm({
|
|
|
11319
12245
|
init_data_loader();
|
|
11320
12246
|
init_step_type_detector();
|
|
11321
12247
|
init_logger();
|
|
11322
|
-
|
|
12248
|
+
logger31 = createChildLogger("runner");
|
|
11323
12249
|
}
|
|
11324
12250
|
});
|
|
11325
12251
|
|
|
@@ -11353,7 +12279,7 @@ async function runRoutes(fastify) {
|
|
|
11353
12279
|
};
|
|
11354
12280
|
const wsManager = fastify.wsManager;
|
|
11355
12281
|
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
11356
|
-
|
|
12282
|
+
logger32.info({ runConfig }, "Run requested via API");
|
|
11357
12283
|
runTests(rootDir, runConfig, autotestConfig, wsManager).then((result) => {
|
|
11358
12284
|
wsManager.broadcast("run:complete", {
|
|
11359
12285
|
runId: result.runId,
|
|
@@ -11362,9 +12288,9 @@ async function runRoutes(fastify) {
|
|
|
11362
12288
|
skipped: result.skipped,
|
|
11363
12289
|
duration: result.duration
|
|
11364
12290
|
});
|
|
11365
|
-
|
|
12291
|
+
logger32.info({ runId: result.runId, status: result.status }, "API-triggered run complete");
|
|
11366
12292
|
}).catch((err) => {
|
|
11367
|
-
|
|
12293
|
+
logger32.error({ err }, "API-triggered run failed");
|
|
11368
12294
|
wsManager.broadcast("run:error", {
|
|
11369
12295
|
error: err instanceof Error ? err.message : String(err)
|
|
11370
12296
|
});
|
|
@@ -11395,7 +12321,7 @@ async function runRoutes(fastify) {
|
|
|
11395
12321
|
}
|
|
11396
12322
|
});
|
|
11397
12323
|
}
|
|
11398
|
-
var import_zod14,
|
|
12324
|
+
var import_zod14, logger32, ScreenshotMode, VideoMode, TraceMode, StartRunBody, ListRunsQuery;
|
|
11399
12325
|
var init_run2 = __esm({
|
|
11400
12326
|
"src/server/routes/run.ts"() {
|
|
11401
12327
|
"use strict";
|
|
@@ -11406,7 +12332,7 @@ var init_run2 = __esm({
|
|
|
11406
12332
|
init_result_store();
|
|
11407
12333
|
init_utils2();
|
|
11408
12334
|
init_logger();
|
|
11409
|
-
|
|
12335
|
+
logger32 = createChildLogger("routes/run");
|
|
11410
12336
|
ScreenshotMode = import_zod14.z.enum(["off", "on", "only-on-failure"]);
|
|
11411
12337
|
VideoMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
|
|
11412
12338
|
TraceMode = import_zod14.z.enum(["off", "on", "on-first-retry", "retain-on-failure"]);
|
|
@@ -11496,7 +12422,7 @@ async function parseSwaggerSpec(urlOrJson) {
|
|
|
11496
12422
|
specUrl = new URL(specUrl, url).href;
|
|
11497
12423
|
}
|
|
11498
12424
|
urls.unshift(specUrl);
|
|
11499
|
-
|
|
12425
|
+
logger33.info({ specUrl, pattern: pattern.source }, "Extracted spec URL from HTML page");
|
|
11500
12426
|
}
|
|
11501
12427
|
}
|
|
11502
12428
|
}
|
|
@@ -11508,7 +12434,7 @@ async function parseSwaggerSpec(urlOrJson) {
|
|
|
11508
12434
|
const errors = [];
|
|
11509
12435
|
for (const tryUrl of uniqueUrls) {
|
|
11510
12436
|
try {
|
|
11511
|
-
|
|
12437
|
+
logger33.debug({ tryUrl }, "Trying to fetch OpenAPI spec");
|
|
11512
12438
|
const res = await fetch(tryUrl, {
|
|
11513
12439
|
headers: { Accept: "application/json, application/yaml, */*" },
|
|
11514
12440
|
signal: AbortSignal.timeout(15e3)
|
|
@@ -11523,7 +12449,7 @@ async function parseSwaggerSpec(urlOrJson) {
|
|
|
11523
12449
|
spec = isJson ? JSON.parse(text) : import_js_yaml.default.load(text);
|
|
11524
12450
|
if (spec && (spec.openapi || spec.swagger || spec.paths)) {
|
|
11525
12451
|
fetched = true;
|
|
11526
|
-
|
|
12452
|
+
logger33.info({ tryUrl, format: isYaml ? "YAML" : "JSON" }, "Successfully fetched OpenAPI spec");
|
|
11527
12453
|
break;
|
|
11528
12454
|
}
|
|
11529
12455
|
errors.push(`${tryUrl}: Parsed but not a valid OpenAPI spec`);
|
|
@@ -11537,7 +12463,7 @@ async function parseSwaggerSpec(urlOrJson) {
|
|
|
11537
12463
|
if (parsed && typeof parsed === "object" && (parsed.openapi || parsed.swagger || parsed.paths)) {
|
|
11538
12464
|
spec = parsed;
|
|
11539
12465
|
fetched = true;
|
|
11540
|
-
|
|
12466
|
+
logger33.info({ tryUrl, format: "YAML (text/plain)" }, "Successfully fetched OpenAPI spec");
|
|
11541
12467
|
break;
|
|
11542
12468
|
}
|
|
11543
12469
|
} catch {
|
|
@@ -11626,17 +12552,17 @@ function parseSpec(spec) {
|
|
|
11626
12552
|
});
|
|
11627
12553
|
}
|
|
11628
12554
|
}
|
|
11629
|
-
|
|
12555
|
+
logger33.info({ title, endpointCount: endpoints.length }, "Parsed OpenAPI spec");
|
|
11630
12556
|
return { title, baseUrl, endpoints };
|
|
11631
12557
|
}
|
|
11632
|
-
var import_js_yaml,
|
|
12558
|
+
var import_js_yaml, logger33;
|
|
11633
12559
|
var init_swagger_parser = __esm({
|
|
11634
12560
|
"src/ai/swagger-parser.ts"() {
|
|
11635
12561
|
"use strict";
|
|
11636
12562
|
init_cjs_shims();
|
|
11637
12563
|
import_js_yaml = __toESM(require("js-yaml"));
|
|
11638
12564
|
init_logger();
|
|
11639
|
-
|
|
12565
|
+
logger33 = createChildLogger("swagger-parser");
|
|
11640
12566
|
}
|
|
11641
12567
|
});
|
|
11642
12568
|
|
|
@@ -11692,7 +12618,7 @@ async function fetchJiraIssue(issueKey) {
|
|
|
11692
12618
|
}
|
|
11693
12619
|
const authHeader = "Basic " + Buffer.from(`${config.email}:${config.apiToken}`).toString("base64");
|
|
11694
12620
|
const url = `${config.baseUrl}/rest/api/3/issue/${encodeURIComponent(issueKey)}?fields=summary,description,comment,issuetype,status,priority,labels,customfield_10020`;
|
|
11695
|
-
|
|
12621
|
+
logger34.info({ issueKey, url: url.replace(/\/rest.*/, "/...") }, "Fetching Jira issue");
|
|
11696
12622
|
const res = await fetch(url, {
|
|
11697
12623
|
method: "GET",
|
|
11698
12624
|
headers: {
|
|
@@ -11813,7 +12739,7 @@ async function storyRoutes(fastify) {
|
|
|
11813
12739
|
return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
|
|
11814
12740
|
}
|
|
11815
12741
|
try {
|
|
11816
|
-
const testsDir =
|
|
12742
|
+
const testsDir = import_path27.default.join(rootDir, "tests");
|
|
11817
12743
|
let storyText = parsed.data.story;
|
|
11818
12744
|
if (parsed.data.jiraUrl) {
|
|
11819
12745
|
const issueKey = extractIssueKey(parsed.data.jiraUrl);
|
|
@@ -11821,19 +12747,19 @@ async function storyRoutes(fastify) {
|
|
|
11821
12747
|
try {
|
|
11822
12748
|
const jiraContent = await fetchJiraIssue(issueKey);
|
|
11823
12749
|
storyText = buildEnrichedStory(jiraContent, parsed.data.story || void 0);
|
|
11824
|
-
|
|
12750
|
+
logger34.info({ issueKey }, "Enriched story with Jira content");
|
|
11825
12751
|
} catch (jiraErr) {
|
|
11826
12752
|
if (!storyText || storyText.length < 10) {
|
|
11827
12753
|
return sendError(reply, 400, "Jira fetch failed and no user story provided", jiraErr);
|
|
11828
12754
|
}
|
|
11829
|
-
|
|
12755
|
+
logger34.warn({ err: jiraErr }, "Failed to fetch Jira issue \u2014 proceeding with user story only");
|
|
11830
12756
|
}
|
|
11831
12757
|
}
|
|
11832
12758
|
}
|
|
11833
12759
|
if (!storyText || storyText.length < 10) {
|
|
11834
12760
|
return reply.status(400).send({ error: "Not enough content to generate tests. Provide a story or a valid Jira link." });
|
|
11835
12761
|
}
|
|
11836
|
-
|
|
12762
|
+
logger34.info({ suiteName: parsed.data.suiteName }, "Story generation requested");
|
|
11837
12763
|
const { suiteId, suiteName } = await generateSuiteFromStory(storyText, {
|
|
11838
12764
|
rootDir,
|
|
11839
12765
|
outputDir: testsDir,
|
|
@@ -11859,9 +12785,9 @@ async function storyRoutes(fastify) {
|
|
|
11859
12785
|
return reply.status(400).send({ error: "Invalid request", details: parsed.error.issues });
|
|
11860
12786
|
}
|
|
11861
12787
|
const { stories, concurrency } = parsed.data;
|
|
11862
|
-
const testsDir =
|
|
12788
|
+
const testsDir = import_path27.default.join(rootDir, "tests");
|
|
11863
12789
|
const wsManager = fastify.wsManager;
|
|
11864
|
-
|
|
12790
|
+
logger34.info({ total: stories.length, concurrency }, "Bulk story generation requested");
|
|
11865
12791
|
const enrichedStories = await Promise.all(
|
|
11866
12792
|
stories.map(async (item) => {
|
|
11867
12793
|
if (!item.jiraUrl) return { story: item.story, suiteName: item.suiteName };
|
|
@@ -11874,7 +12800,7 @@ async function storyRoutes(fastify) {
|
|
|
11874
12800
|
suiteName: item.suiteName ?? jiraContent.summary
|
|
11875
12801
|
};
|
|
11876
12802
|
} catch {
|
|
11877
|
-
|
|
12803
|
+
logger34.warn({ issueKey }, "Failed to fetch Jira issue for bulk item \u2014 using raw story");
|
|
11878
12804
|
return { story: item.story, suiteName: item.suiteName };
|
|
11879
12805
|
}
|
|
11880
12806
|
})
|
|
@@ -11885,7 +12811,7 @@ async function storyRoutes(fastify) {
|
|
|
11885
12811
|
wsManager,
|
|
11886
12812
|
concurrency
|
|
11887
12813
|
).catch((err) => {
|
|
11888
|
-
|
|
12814
|
+
logger34.error({ err }, "Bulk story generation error");
|
|
11889
12815
|
});
|
|
11890
12816
|
return reply.status(202).send({
|
|
11891
12817
|
message: `Processing ${stories.length} stories in the background`,
|
|
@@ -11927,7 +12853,7 @@ async function storyRoutes(fastify) {
|
|
|
11927
12853
|
}
|
|
11928
12854
|
try {
|
|
11929
12855
|
const spec = await parseSwaggerSpec(parsed.data.specUrl);
|
|
11930
|
-
|
|
12856
|
+
logger34.info({ title: spec.title, endpoints: spec.endpoints.length }, "Parsed OpenAPI spec");
|
|
11931
12857
|
let endpoints = spec.endpoints;
|
|
11932
12858
|
if (parsed.data.tags && parsed.data.tags.length > 0) {
|
|
11933
12859
|
const tagSet = new Set(parsed.data.tags.map((t) => t.toLowerCase()));
|
|
@@ -11939,7 +12865,7 @@ async function storyRoutes(fastify) {
|
|
|
11939
12865
|
const MAX_ENDPOINTS = 20;
|
|
11940
12866
|
const totalEndpoints = endpoints.length;
|
|
11941
12867
|
if (endpoints.length > MAX_ENDPOINTS) {
|
|
11942
|
-
|
|
12868
|
+
logger34.warn({ total: endpoints.length, max: MAX_ENDPOINTS }, "Capping endpoints to max");
|
|
11943
12869
|
endpoints = endpoints.slice(0, MAX_ENDPOINTS);
|
|
11944
12870
|
}
|
|
11945
12871
|
const storyLines = [
|
|
@@ -11971,7 +12897,7 @@ async function storyRoutes(fastify) {
|
|
|
11971
12897
|
}
|
|
11972
12898
|
const storyText = storyLines.join("\n");
|
|
11973
12899
|
const suiteName = parsed.data.suiteName ?? `${spec.title} API Tests`;
|
|
11974
|
-
const testsDir =
|
|
12900
|
+
const testsDir = import_path27.default.join(rootDir, "tests");
|
|
11975
12901
|
const { suiteId } = await generateSuiteFromStory(storyText, {
|
|
11976
12902
|
rootDir,
|
|
11977
12903
|
outputDir: testsDir,
|
|
@@ -11991,18 +12917,18 @@ async function storyRoutes(fastify) {
|
|
|
11991
12917
|
}
|
|
11992
12918
|
});
|
|
11993
12919
|
}
|
|
11994
|
-
var
|
|
12920
|
+
var import_path27, import_zod15, logger34, GenerateStoryBody, FetchJiraBody;
|
|
11995
12921
|
var init_story = __esm({
|
|
11996
12922
|
"src/server/routes/story.ts"() {
|
|
11997
12923
|
"use strict";
|
|
11998
12924
|
init_cjs_shims();
|
|
11999
|
-
|
|
12925
|
+
import_path27 = __toESM(require("path"));
|
|
12000
12926
|
import_zod15 = require("zod");
|
|
12001
12927
|
init_router();
|
|
12002
12928
|
init_swagger_parser();
|
|
12003
12929
|
init_utils2();
|
|
12004
12930
|
init_logger();
|
|
12005
|
-
|
|
12931
|
+
logger34 = createChildLogger("routes/story");
|
|
12006
12932
|
GenerateStoryBody = import_zod15.z.object({
|
|
12007
12933
|
story: import_zod15.z.string().default(""),
|
|
12008
12934
|
suiteName: import_zod15.z.string().min(1).optional(),
|
|
@@ -12043,14 +12969,14 @@ async function reportRoutes(fastify) {
|
|
|
12043
12969
|
fastify.delete("/report/:runId", async (req, reply) => {
|
|
12044
12970
|
try {
|
|
12045
12971
|
await deleteResult(rootDir, req.params.runId);
|
|
12046
|
-
|
|
12972
|
+
logger35.info({ runId: req.params.runId }, "Run result deleted");
|
|
12047
12973
|
return reply.status(204).send();
|
|
12048
12974
|
} catch {
|
|
12049
12975
|
return reply.status(404).send({ error: `Report for run "${req.params.runId}" not found` });
|
|
12050
12976
|
}
|
|
12051
12977
|
});
|
|
12052
12978
|
}
|
|
12053
|
-
var import_zod16,
|
|
12979
|
+
var import_zod16, logger35, ListReportsQuery;
|
|
12054
12980
|
var init_report = __esm({
|
|
12055
12981
|
"src/server/routes/report.ts"() {
|
|
12056
12982
|
"use strict";
|
|
@@ -12059,7 +12985,7 @@ var init_report = __esm({
|
|
|
12059
12985
|
init_result_store();
|
|
12060
12986
|
init_utils2();
|
|
12061
12987
|
init_logger();
|
|
12062
|
-
|
|
12988
|
+
logger35 = createChildLogger("routes/report");
|
|
12063
12989
|
ListReportsQuery = import_zod16.z.object({
|
|
12064
12990
|
limit: import_zod16.z.coerce.number().int().positive().max(100).default(20)
|
|
12065
12991
|
});
|
|
@@ -12068,26 +12994,26 @@ var init_report = __esm({
|
|
|
12068
12994
|
|
|
12069
12995
|
// src/storage/baseline-store.ts
|
|
12070
12996
|
function baselinesDir(rootDir) {
|
|
12071
|
-
return
|
|
12997
|
+
return import_path28.default.join(rootDir, "results", "baselines");
|
|
12072
12998
|
}
|
|
12073
12999
|
function baselinePath(rootDir, stepId) {
|
|
12074
|
-
return
|
|
13000
|
+
return import_path28.default.join(baselinesDir(rootDir), `${stepId}.png`);
|
|
12075
13001
|
}
|
|
12076
13002
|
async function saveBaseline(rootDir, stepId, sourcePath) {
|
|
12077
13003
|
const dir = baselinesDir(rootDir);
|
|
12078
|
-
await
|
|
12079
|
-
await
|
|
13004
|
+
await import_fs_extra21.default.ensureDir(dir);
|
|
13005
|
+
await import_fs_extra21.default.copy(sourcePath, baselinePath(rootDir, stepId), { overwrite: true });
|
|
12080
13006
|
}
|
|
12081
13007
|
async function hasBaseline(rootDir, stepId) {
|
|
12082
|
-
return
|
|
13008
|
+
return import_fs_extra21.default.pathExists(baselinePath(rootDir, stepId));
|
|
12083
13009
|
}
|
|
12084
|
-
var
|
|
13010
|
+
var import_path28, import_fs_extra21;
|
|
12085
13011
|
var init_baseline_store = __esm({
|
|
12086
13012
|
"src/storage/baseline-store.ts"() {
|
|
12087
13013
|
"use strict";
|
|
12088
13014
|
init_cjs_shims();
|
|
12089
|
-
|
|
12090
|
-
|
|
13015
|
+
import_path28 = __toESM(require("path"));
|
|
13016
|
+
import_fs_extra21 = __toESM(require("fs-extra"));
|
|
12091
13017
|
}
|
|
12092
13018
|
});
|
|
12093
13019
|
|
|
@@ -12098,26 +13024,26 @@ async function mediaRoutes(fastify) {
|
|
|
12098
13024
|
if (!["screenshots", "videos", "traces", "logs", "baselines"].includes(category)) {
|
|
12099
13025
|
return reply.status(404).send({ error: "Unknown media category" });
|
|
12100
13026
|
}
|
|
12101
|
-
const safeName =
|
|
12102
|
-
const filePath =
|
|
12103
|
-
if (!await
|
|
13027
|
+
const safeName = import_path29.default.basename(filename);
|
|
13028
|
+
const filePath = import_path29.default.join(fastify.rootDir, "results", category, safeName);
|
|
13029
|
+
if (!await import_fs_extra22.default.pathExists(filePath)) {
|
|
12104
13030
|
return reply.status(404).send({ error: `File not found: ${safeName}` });
|
|
12105
13031
|
}
|
|
12106
|
-
const ext =
|
|
13032
|
+
const ext = import_path29.default.extname(safeName).toLowerCase();
|
|
12107
13033
|
const mimeType = MIME[ext] ?? "application/octet-stream";
|
|
12108
13034
|
if (ext === ".zip") {
|
|
12109
13035
|
void reply.header("Content-Disposition", `attachment; filename="${safeName}"`);
|
|
12110
13036
|
}
|
|
12111
|
-
const stat = await
|
|
13037
|
+
const stat = await import_fs_extra22.default.stat(filePath);
|
|
12112
13038
|
void reply.header("Content-Length", stat.size);
|
|
12113
|
-
return reply.type(mimeType).send(
|
|
13039
|
+
return reply.type(mimeType).send(import_fs_extra22.default.createReadStream(filePath));
|
|
12114
13040
|
});
|
|
12115
13041
|
fastify.post("/baselines/:stepId/capture", async (req, reply) => {
|
|
12116
13042
|
try {
|
|
12117
13043
|
const body = req.body;
|
|
12118
13044
|
if (!body?.screenshotPath) return reply.status(400).send({ error: "screenshotPath required" });
|
|
12119
|
-
const fullPath =
|
|
12120
|
-
if (!await
|
|
13045
|
+
const fullPath = import_path29.default.isAbsolute(body.screenshotPath) ? body.screenshotPath : import_path29.default.join(fastify.rootDir, body.screenshotPath);
|
|
13046
|
+
if (!await import_fs_extra22.default.pathExists(fullPath)) return reply.status(404).send({ error: "Screenshot not found" });
|
|
12121
13047
|
await saveBaseline(fastify.rootDir, req.params.stepId, fullPath);
|
|
12122
13048
|
return reply.send({ saved: true });
|
|
12123
13049
|
} catch (err) {
|
|
@@ -12128,21 +13054,21 @@ async function mediaRoutes(fastify) {
|
|
|
12128
13054
|
try {
|
|
12129
13055
|
const exists = await hasBaseline(fastify.rootDir, req.params.stepId);
|
|
12130
13056
|
if (!exists) return reply.send({ hasBaseline: false });
|
|
12131
|
-
const filePath =
|
|
12132
|
-
const stat = await
|
|
13057
|
+
const filePath = import_path29.default.join(fastify.rootDir, "results", "baselines", `${req.params.stepId}.png`);
|
|
13058
|
+
const stat = await import_fs_extra22.default.stat(filePath).catch(() => null);
|
|
12133
13059
|
return reply.send({ hasBaseline: true, capturedAt: stat?.mtimeMs ?? Date.now() });
|
|
12134
13060
|
} catch (err) {
|
|
12135
13061
|
return sendError(reply, 500, "Failed to check baseline", err);
|
|
12136
13062
|
}
|
|
12137
13063
|
});
|
|
12138
13064
|
}
|
|
12139
|
-
var
|
|
13065
|
+
var import_path29, import_fs_extra22, MIME;
|
|
12140
13066
|
var init_media = __esm({
|
|
12141
13067
|
"src/server/routes/media.ts"() {
|
|
12142
13068
|
"use strict";
|
|
12143
13069
|
init_cjs_shims();
|
|
12144
|
-
|
|
12145
|
-
|
|
13070
|
+
import_path29 = __toESM(require("path"));
|
|
13071
|
+
import_fs_extra22 = __toESM(require("fs-extra"));
|
|
12146
13072
|
init_baseline_store();
|
|
12147
13073
|
init_utils2();
|
|
12148
13074
|
MIME = {
|
|
@@ -12166,13 +13092,13 @@ function toAdf(text) {
|
|
|
12166
13092
|
}));
|
|
12167
13093
|
return { type: "doc", version: 1, content: paragraphs };
|
|
12168
13094
|
}
|
|
12169
|
-
var
|
|
13095
|
+
var logger36, XrayClient;
|
|
12170
13096
|
var init_client = __esm({
|
|
12171
13097
|
"src/xray/client.ts"() {
|
|
12172
13098
|
"use strict";
|
|
12173
13099
|
init_cjs_shims();
|
|
12174
13100
|
init_logger();
|
|
12175
|
-
|
|
13101
|
+
logger36 = createChildLogger("xray-client");
|
|
12176
13102
|
XrayClient = class {
|
|
12177
13103
|
config;
|
|
12178
13104
|
authHeader;
|
|
@@ -12188,7 +13114,7 @@ var init_client = __esm({
|
|
|
12188
13114
|
// ─── Jira REST API helpers ──────────────────────────────────────────────────
|
|
12189
13115
|
async jiraRequest(method, path37, body) {
|
|
12190
13116
|
const url = `${this.config.jiraBaseUrl}/rest/api/3${path37}`;
|
|
12191
|
-
|
|
13117
|
+
logger36.debug({ method, url }, "Jira API request");
|
|
12192
13118
|
const headers = {
|
|
12193
13119
|
"Authorization": this.authHeader,
|
|
12194
13120
|
"Accept": "application/json"
|
|
@@ -12203,7 +13129,7 @@ var init_client = __esm({
|
|
|
12203
13129
|
});
|
|
12204
13130
|
if (!res.ok) {
|
|
12205
13131
|
const text = await res.text();
|
|
12206
|
-
|
|
13132
|
+
logger36.error({ status: res.status, url, body: text }, "Jira API error");
|
|
12207
13133
|
throw new Error(`Jira API ${method} ${path37} \u2192 ${res.status}: ${text}`);
|
|
12208
13134
|
}
|
|
12209
13135
|
if (res.status === 204) return void 0;
|
|
@@ -12235,7 +13161,7 @@ var init_client = __esm({
|
|
|
12235
13161
|
const token = await this.getXrayToken();
|
|
12236
13162
|
const baseUrl = token ? "https://xray.cloud.getxray.app/api/v2" : `${this.config.jiraBaseUrl}/rest/raven/2.0/api`;
|
|
12237
13163
|
const url = `${baseUrl}${path37}`;
|
|
12238
|
-
|
|
13164
|
+
logger36.debug({ method, url }, "Xray API request");
|
|
12239
13165
|
const headers = {
|
|
12240
13166
|
"Accept": "application/json"
|
|
12241
13167
|
};
|
|
@@ -12254,7 +13180,7 @@ var init_client = __esm({
|
|
|
12254
13180
|
});
|
|
12255
13181
|
if (!res.ok) {
|
|
12256
13182
|
const text = await res.text();
|
|
12257
|
-
|
|
13183
|
+
logger36.error({ status: res.status, url, body: text }, "Xray API error");
|
|
12258
13184
|
throw new Error(`Xray API ${method} ${path37} \u2192 ${res.status}: ${text}`);
|
|
12259
13185
|
}
|
|
12260
13186
|
if (res.status === 204) return void 0;
|
|
@@ -12323,7 +13249,7 @@ var init_client = __esm({
|
|
|
12323
13249
|
warnings
|
|
12324
13250
|
}
|
|
12325
13251
|
}`;
|
|
12326
|
-
|
|
13252
|
+
logger36.info({ caseName: testCase.name, projectKey: this.config.projectKey, folderPath }, "Creating Test via Xray Cloud GraphQL");
|
|
12327
13253
|
const result = await this.xrayGraphQL(mutation);
|
|
12328
13254
|
if (!result?.createTest?.test) {
|
|
12329
13255
|
throw new Error("Xray createTest returned no test data");
|
|
@@ -12332,9 +13258,9 @@ var init_client = __esm({
|
|
|
12332
13258
|
const jiraKey = test.jira?.key ?? "";
|
|
12333
13259
|
const warnings = result.createTest.warnings;
|
|
12334
13260
|
if (warnings && warnings.length > 0) {
|
|
12335
|
-
|
|
13261
|
+
logger36.warn({ warnings, jiraKey }, "Xray createTest warnings");
|
|
12336
13262
|
}
|
|
12337
|
-
|
|
13263
|
+
logger36.info({ issueId: test.issueId, jiraKey, stepCount: testCase.steps.length, folderPath }, "Xray Test created via GraphQL");
|
|
12338
13264
|
return { id: test.issueId, key: jiraKey, self: "" };
|
|
12339
13265
|
}
|
|
12340
13266
|
/** Xray Server/DC: create via Jira REST API */
|
|
@@ -12347,7 +13273,7 @@ var init_client = __esm({
|
|
|
12347
13273
|
issuetype: { name: issueType },
|
|
12348
13274
|
labels: ["assuremind", ...testCase.tags]
|
|
12349
13275
|
};
|
|
12350
|
-
|
|
13276
|
+
logger36.info({ caseName: testCase.name, projectKey: this.config.projectKey }, "Creating Xray Test via Jira REST");
|
|
12351
13277
|
const result = await this.jiraRequest("POST", "/issue", { fields });
|
|
12352
13278
|
if (testCase.steps.length > 0) {
|
|
12353
13279
|
await this.addTestStepsServerDC(result.key, testCase.steps);
|
|
@@ -12363,7 +13289,7 @@ var init_client = __esm({
|
|
|
12363
13289
|
description: toAdf(testCase.description || `Automated test case: ${testCase.name}`),
|
|
12364
13290
|
labels: ["assuremind", ...testCase.tags]
|
|
12365
13291
|
};
|
|
12366
|
-
|
|
13292
|
+
logger36.info({ issueKey: testIssue.key, caseName: testCase.name }, "Updating Xray Test");
|
|
12367
13293
|
await this.jiraRequest("PUT", `/issue/${testIssue.key}`, { fields });
|
|
12368
13294
|
if (testCase.steps.length > 0) {
|
|
12369
13295
|
await this.replaceTestSteps(testIssue, testCase.steps);
|
|
@@ -12402,7 +13328,7 @@ var init_client = __esm({
|
|
|
12402
13328
|
warnings
|
|
12403
13329
|
}
|
|
12404
13330
|
}`;
|
|
12405
|
-
|
|
13331
|
+
logger36.info({ suiteName: suite.name, projectKey: this.config.projectKey, testCount: testIssueIds?.length }, "Creating Test Set via Xray Cloud GraphQL");
|
|
12406
13332
|
const result = await this.xrayGraphQL(mutation);
|
|
12407
13333
|
if (!result?.createTestSet?.testSet) {
|
|
12408
13334
|
throw new Error("Xray createTestSet returned no data");
|
|
@@ -12411,9 +13337,9 @@ var init_client = __esm({
|
|
|
12411
13337
|
const jiraKey = testSet.jira?.key ?? "";
|
|
12412
13338
|
const warnings = result.createTestSet.warnings;
|
|
12413
13339
|
if (warnings && warnings.length > 0) {
|
|
12414
|
-
|
|
13340
|
+
logger36.warn({ warnings, jiraKey }, "Xray createTestSet warnings");
|
|
12415
13341
|
}
|
|
12416
|
-
|
|
13342
|
+
logger36.info({ issueId: testSet.issueId, jiraKey }, "Xray Test Set created via GraphQL");
|
|
12417
13343
|
return { id: testSet.issueId, key: jiraKey, self: "" };
|
|
12418
13344
|
}
|
|
12419
13345
|
/** Xray Server/DC: create via Jira REST API */
|
|
@@ -12426,7 +13352,7 @@ var init_client = __esm({
|
|
|
12426
13352
|
issuetype: { name: issueType },
|
|
12427
13353
|
labels: ["assuremind", ...suite.tags]
|
|
12428
13354
|
};
|
|
12429
|
-
|
|
13355
|
+
logger36.info({ suiteName: suite.name, projectKey: this.config.projectKey }, "Creating Xray Test Set via Jira REST");
|
|
12430
13356
|
const result = await this.jiraRequest("POST", "/issue", { fields });
|
|
12431
13357
|
return { id: result.id, key: result.key, self: result.self };
|
|
12432
13358
|
}
|
|
@@ -12439,7 +13365,7 @@ var init_client = __esm({
|
|
|
12439
13365
|
description: toAdf(suite.description || `Automated test suite: ${suite.name}`),
|
|
12440
13366
|
labels: ["assuremind", ...suite.tags]
|
|
12441
13367
|
};
|
|
12442
|
-
|
|
13368
|
+
logger36.info({ issueKey, suiteName: suite.name }, "Updating Xray Test Set");
|
|
12443
13369
|
await this.jiraRequest("PUT", `/issue/${issueKey}`, { fields });
|
|
12444
13370
|
}
|
|
12445
13371
|
// ─── Test Steps ────────────────────────────────────────────────────────────
|
|
@@ -12456,7 +13382,7 @@ var init_client = __esm({
|
|
|
12456
13382
|
if (this.isXrayCloud) {
|
|
12457
13383
|
const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
|
|
12458
13384
|
if (!xrayId) {
|
|
12459
|
-
|
|
13385
|
+
logger36.warn({ testKey: testIssue.key }, "Cannot add steps \u2014 Test not found in Xray yet");
|
|
12460
13386
|
return;
|
|
12461
13387
|
}
|
|
12462
13388
|
await this.addTestStepsCloud(xrayId, steps);
|
|
@@ -12491,16 +13417,16 @@ var init_client = __esm({
|
|
|
12491
13417
|
} catch (err) {
|
|
12492
13418
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
12493
13419
|
if (errMsg.includes("not found")) {
|
|
12494
|
-
|
|
13420
|
+
logger36.warn({ xrayIssueId }, "Xray issue not found during step add \u2014 aborting");
|
|
12495
13421
|
break;
|
|
12496
13422
|
}
|
|
12497
|
-
|
|
13423
|
+
logger36.warn({ xrayIssueId, step: action, err: errMsg }, "Failed to add test step");
|
|
12498
13424
|
}
|
|
12499
13425
|
}
|
|
12500
13426
|
if (added > 0) {
|
|
12501
|
-
|
|
13427
|
+
logger36.info({ xrayIssueId, added, total: steps.length }, "Test steps added via Xray Cloud GraphQL");
|
|
12502
13428
|
} else if (steps.length > 0) {
|
|
12503
|
-
|
|
13429
|
+
logger36.warn({ xrayIssueId }, "Failed to add any steps");
|
|
12504
13430
|
}
|
|
12505
13431
|
}
|
|
12506
13432
|
/** Xray Server/DC — use REST API /test/{key}/step */
|
|
@@ -12513,14 +13439,14 @@ var init_client = __esm({
|
|
|
12513
13439
|
}));
|
|
12514
13440
|
try {
|
|
12515
13441
|
await this.xrayRequest("PUT", `/test/${testIssueKey}/step`, xraySteps);
|
|
12516
|
-
|
|
13442
|
+
logger36.debug({ testIssueKey, stepCount: steps.length }, "Test steps added");
|
|
12517
13443
|
} catch (err) {
|
|
12518
|
-
|
|
13444
|
+
logger36.warn({ testIssueKey, err }, "Bulk step add failed, trying one-by-one");
|
|
12519
13445
|
for (const step of xraySteps) {
|
|
12520
13446
|
try {
|
|
12521
13447
|
await this.xrayRequest("POST", `/test/${testIssueKey}/step`, step);
|
|
12522
13448
|
} catch (stepErr) {
|
|
12523
|
-
|
|
13449
|
+
logger36.warn({ testIssueKey, step: step.index, err: stepErr }, "Individual step add failed");
|
|
12524
13450
|
}
|
|
12525
13451
|
}
|
|
12526
13452
|
}
|
|
@@ -12532,7 +13458,7 @@ var init_client = __esm({
|
|
|
12532
13458
|
if (this.isXrayCloud) {
|
|
12533
13459
|
const xrayId = await this.resolveXrayIssueId(testIssue.key, "Test");
|
|
12534
13460
|
if (!xrayId) {
|
|
12535
|
-
|
|
13461
|
+
logger36.warn({ testKey: testIssue.key }, "Cannot replace steps \u2014 Test not found in Xray");
|
|
12536
13462
|
return;
|
|
12537
13463
|
}
|
|
12538
13464
|
await this.addTestStepsCloud(xrayId, steps);
|
|
@@ -12544,7 +13470,7 @@ var init_client = __esm({
|
|
|
12544
13470
|
await this.xrayRequest("DELETE", `/test/${testIssue.key}/step/${step.id}`);
|
|
12545
13471
|
}
|
|
12546
13472
|
} catch {
|
|
12547
|
-
|
|
13473
|
+
logger36.debug({ testIssueKey: testIssue.key }, "No existing steps to delete (or endpoint unavailable)");
|
|
12548
13474
|
}
|
|
12549
13475
|
await this.addTestStepsServerDC(testIssue.key, steps);
|
|
12550
13476
|
}
|
|
@@ -12563,9 +13489,9 @@ var init_client = __esm({
|
|
|
12563
13489
|
await this.xrayRequest("POST", `/testset/${testSet.key}/test`, {
|
|
12564
13490
|
add: tests.map((t) => t.key)
|
|
12565
13491
|
});
|
|
12566
|
-
|
|
13492
|
+
logger36.info({ testSetKey: testSet.key, count: tests.length }, "Tests linked to Test Set");
|
|
12567
13493
|
} catch (err) {
|
|
12568
|
-
|
|
13494
|
+
logger36.warn({ testSetKey: testSet.key, err }, "Xray link API failed, trying Jira issue links");
|
|
12569
13495
|
await this.linkViaJiraIssueLinks(testSet.key, tests.map((t) => t.key));
|
|
12570
13496
|
}
|
|
12571
13497
|
}
|
|
@@ -12575,10 +13501,10 @@ var init_client = __esm({
|
|
|
12575
13501
|
* Resolves Xray internal IDs first (they differ from Jira numeric IDs).
|
|
12576
13502
|
*/
|
|
12577
13503
|
async linkTestsCloud(testSet, tests) {
|
|
12578
|
-
|
|
13504
|
+
logger36.info({ testSetKey: testSet.key, testKeys: tests.map((t) => t.key) }, "Resolving Xray IDs for linking");
|
|
12579
13505
|
const xrayTestSetId = await this.resolveXrayIssueId(testSet.key, "Test Set");
|
|
12580
13506
|
if (!xrayTestSetId) {
|
|
12581
|
-
|
|
13507
|
+
logger36.error({ testSetKey: testSet.key }, "Cannot link \u2014 Test Set not found in Xray");
|
|
12582
13508
|
return;
|
|
12583
13509
|
}
|
|
12584
13510
|
const xrayTestIds = [];
|
|
@@ -12587,11 +13513,11 @@ var init_client = __esm({
|
|
|
12587
13513
|
if (xrayTestId) {
|
|
12588
13514
|
xrayTestIds.push(xrayTestId);
|
|
12589
13515
|
} else {
|
|
12590
|
-
|
|
13516
|
+
logger36.warn({ testKey: test.key }, "Could not resolve Xray ID for test \u2014 skipping");
|
|
12591
13517
|
}
|
|
12592
13518
|
}
|
|
12593
13519
|
if (xrayTestIds.length === 0) {
|
|
12594
|
-
|
|
13520
|
+
logger36.warn({ testSetKey: testSet.key }, "No test IDs resolved \u2014 skipping link");
|
|
12595
13521
|
return;
|
|
12596
13522
|
}
|
|
12597
13523
|
try {
|
|
@@ -12607,7 +13533,7 @@ var init_client = __esm({
|
|
|
12607
13533
|
}`
|
|
12608
13534
|
);
|
|
12609
13535
|
if (result) {
|
|
12610
|
-
|
|
13536
|
+
logger36.info({
|
|
12611
13537
|
testSetKey: testSet.key,
|
|
12612
13538
|
xrayTestSetId,
|
|
12613
13539
|
count: xrayTestIds.length,
|
|
@@ -12616,7 +13542,7 @@ var init_client = __esm({
|
|
|
12616
13542
|
}, "Tests linked to Test Set via Xray Cloud");
|
|
12617
13543
|
}
|
|
12618
13544
|
} catch (err) {
|
|
12619
|
-
|
|
13545
|
+
logger36.error({ testSetKey: testSet.key, xrayTestSetId, err: err instanceof Error ? err.message : String(err) }, "linkTestsCloud failed");
|
|
12620
13546
|
}
|
|
12621
13547
|
}
|
|
12622
13548
|
sleep(ms) {
|
|
@@ -12632,7 +13558,7 @@ var init_client = __esm({
|
|
|
12632
13558
|
outwardIssue: { key: testKey }
|
|
12633
13559
|
});
|
|
12634
13560
|
} catch (linkErr) {
|
|
12635
|
-
|
|
13561
|
+
logger36.error({ testSetKey, testKey, err: linkErr }, "Failed to link test to test set");
|
|
12636
13562
|
}
|
|
12637
13563
|
}
|
|
12638
13564
|
}
|
|
@@ -12645,7 +13571,7 @@ var init_client = __esm({
|
|
|
12645
13571
|
*/
|
|
12646
13572
|
async createFolderAndAddTests(folderName, tests) {
|
|
12647
13573
|
if (!this.isXrayCloud) {
|
|
12648
|
-
|
|
13574
|
+
logger36.debug("Folder creation skipped \u2014 only supported on Xray Cloud");
|
|
12649
13575
|
return;
|
|
12650
13576
|
}
|
|
12651
13577
|
if (tests.length === 0) return;
|
|
@@ -12667,11 +13593,11 @@ var init_client = __esm({
|
|
|
12667
13593
|
}
|
|
12668
13594
|
}`
|
|
12669
13595
|
);
|
|
12670
|
-
|
|
13596
|
+
logger36.info({ folderPath, projectId }, "Test Repository folder created");
|
|
12671
13597
|
} catch (folderErr) {
|
|
12672
13598
|
const errMsg = folderErr instanceof Error ? folderErr.message : String(folderErr);
|
|
12673
13599
|
if (errMsg.includes("already exists")) {
|
|
12674
|
-
|
|
13600
|
+
logger36.info({ folderPath }, "Test Repository folder already exists \u2014 reusing");
|
|
12675
13601
|
} else {
|
|
12676
13602
|
throw folderErr;
|
|
12677
13603
|
}
|
|
@@ -12682,14 +13608,14 @@ var init_client = __esm({
|
|
|
12682
13608
|
if (xrayId) {
|
|
12683
13609
|
xrayTestIds.push(xrayId);
|
|
12684
13610
|
} else {
|
|
12685
|
-
|
|
13611
|
+
logger36.warn({ testKey: test.key }, "Could not resolve Xray ID for folder \u2014 skipping");
|
|
12686
13612
|
}
|
|
12687
13613
|
}
|
|
12688
13614
|
if (xrayTestIds.length === 0) {
|
|
12689
|
-
|
|
13615
|
+
logger36.warn({ folderPath }, "No Xray IDs resolved \u2014 cannot add tests to folder");
|
|
12690
13616
|
return;
|
|
12691
13617
|
}
|
|
12692
|
-
|
|
13618
|
+
logger36.info({ folderPath, xrayTestIds }, "Adding tests to folder with resolved Xray IDs");
|
|
12693
13619
|
try {
|
|
12694
13620
|
await this.xrayGraphQL(
|
|
12695
13621
|
`mutation {
|
|
@@ -12703,12 +13629,12 @@ var init_client = __esm({
|
|
|
12703
13629
|
}
|
|
12704
13630
|
}`
|
|
12705
13631
|
);
|
|
12706
|
-
|
|
13632
|
+
logger36.info({ folderPath, count: xrayTestIds.length }, "Tests added to folder");
|
|
12707
13633
|
} catch (addErr) {
|
|
12708
|
-
|
|
13634
|
+
logger36.warn({ folderPath, err: addErr instanceof Error ? addErr.message : String(addErr) }, "Failed to add tests to folder");
|
|
12709
13635
|
}
|
|
12710
13636
|
} catch (err) {
|
|
12711
|
-
|
|
13637
|
+
logger36.error({ folderName, err: err instanceof Error ? err.message : String(err) }, "FAILED to create folder or add tests \u2014 tests exist but are unorganized");
|
|
12712
13638
|
}
|
|
12713
13639
|
}
|
|
12714
13640
|
// ─── Xray Cloud GraphQL helper ────────────────────────────────────────────
|
|
@@ -12726,12 +13652,12 @@ var init_client = __esm({
|
|
|
12726
13652
|
});
|
|
12727
13653
|
if (!res.ok) {
|
|
12728
13654
|
const text = await res.text();
|
|
12729
|
-
|
|
13655
|
+
logger36.warn({ status: res.status, body: text }, "Xray Cloud GraphQL request failed");
|
|
12730
13656
|
throw new Error(`Xray GraphQL \u2192 ${res.status}: ${text}`);
|
|
12731
13657
|
}
|
|
12732
13658
|
const result = await res.json();
|
|
12733
13659
|
if (result.errors && result.errors.length > 0) {
|
|
12734
|
-
|
|
13660
|
+
logger36.warn({ errors: result.errors }, "Xray Cloud GraphQL returned errors");
|
|
12735
13661
|
throw new Error(`Xray GraphQL errors: ${result.errors.map((e) => e.message).join("; ")}`);
|
|
12736
13662
|
}
|
|
12737
13663
|
return result.data ?? null;
|
|
@@ -12749,7 +13675,7 @@ var init_client = __esm({
|
|
|
12749
13675
|
const delays = [0, 5e3, 1e4, 2e4];
|
|
12750
13676
|
for (let attempt = 0; attempt < delays.length; attempt++) {
|
|
12751
13677
|
if (delays[attempt] > 0) {
|
|
12752
|
-
|
|
13678
|
+
logger36.info({ jiraKey, attempt, delay: delays[attempt] }, `Waiting ${delays[attempt] / 1e3}s before retry`);
|
|
12753
13679
|
await this.sleep(delays[attempt]);
|
|
12754
13680
|
}
|
|
12755
13681
|
try {
|
|
@@ -12762,21 +13688,21 @@ var init_client = __esm({
|
|
|
12762
13688
|
}
|
|
12763
13689
|
}
|
|
12764
13690
|
}`;
|
|
12765
|
-
|
|
13691
|
+
logger36.info({ jiraKey, attempt, query: queryName }, `Querying Xray for ${type} issueId`);
|
|
12766
13692
|
const result = await this.xrayGraphQL(gql);
|
|
12767
13693
|
const queryResult = result?.[resultField];
|
|
12768
|
-
|
|
13694
|
+
logger36.info({ jiraKey, attempt, total: queryResult?.total, resultCount: queryResult?.results?.length, raw: JSON.stringify(queryResult) }, `Xray ${queryName} response`);
|
|
12769
13695
|
const items = queryResult?.results;
|
|
12770
13696
|
if (items && items.length > 0 && items[0].issueId) {
|
|
12771
|
-
|
|
13697
|
+
logger36.info({ jiraKey, xrayIssueId: items[0].issueId, attempt }, `Resolved Xray issueId for ${type}`);
|
|
12772
13698
|
return items[0].issueId;
|
|
12773
13699
|
}
|
|
12774
|
-
|
|
13700
|
+
logger36.warn({ jiraKey, attempt, total: queryResult?.total }, `Xray has not indexed ${type} yet \u2014 no results`);
|
|
12775
13701
|
} catch (err) {
|
|
12776
|
-
|
|
13702
|
+
logger36.warn({ jiraKey, attempt, err: err instanceof Error ? err.message : String(err) }, `resolveXrayIssueId attempt ${attempt} failed`);
|
|
12777
13703
|
}
|
|
12778
13704
|
}
|
|
12779
|
-
|
|
13705
|
+
logger36.error({ jiraKey }, `Could not resolve Xray issueId for ${type} after ${delays.length} attempts`);
|
|
12780
13706
|
return null;
|
|
12781
13707
|
}
|
|
12782
13708
|
// ─── Validation ────────────────────────────────────────────────────────────
|
|
@@ -12842,11 +13768,11 @@ var init_xray = __esm({
|
|
|
12842
13768
|
|
|
12843
13769
|
// src/storage/xray-store.ts
|
|
12844
13770
|
function mappingPath(rootDir) {
|
|
12845
|
-
return
|
|
13771
|
+
return import_path30.default.join(rootDir, "assuremind-data", MAPPING_FILE);
|
|
12846
13772
|
}
|
|
12847
13773
|
async function readXrayMapping(rootDir) {
|
|
12848
13774
|
const filePath = mappingPath(rootDir);
|
|
12849
|
-
if (!await
|
|
13775
|
+
if (!await import_fs_extra23.default.pathExists(filePath)) {
|
|
12850
13776
|
return {
|
|
12851
13777
|
projectKey: "",
|
|
12852
13778
|
suites: [],
|
|
@@ -12856,17 +13782,17 @@ async function readXrayMapping(rootDir) {
|
|
|
12856
13782
|
const raw = await readJson(filePath);
|
|
12857
13783
|
const result = XrayMappingFileSchema.safeParse(raw);
|
|
12858
13784
|
if (!result.success) {
|
|
12859
|
-
|
|
13785
|
+
logger37.warn({ path: filePath, errors: result.error.issues }, "Invalid xray-mapping.json \u2014 returning empty");
|
|
12860
13786
|
return { projectKey: "", suites: [], updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12861
13787
|
}
|
|
12862
13788
|
return result.data;
|
|
12863
13789
|
}
|
|
12864
13790
|
async function writeXrayMapping(rootDir, mapping) {
|
|
12865
13791
|
const filePath = mappingPath(rootDir);
|
|
12866
|
-
await
|
|
13792
|
+
await import_fs_extra23.default.ensureDir(import_path30.default.dirname(filePath));
|
|
12867
13793
|
const data = { ...mapping, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
12868
13794
|
await atomicWriteJson(filePath, data);
|
|
12869
|
-
|
|
13795
|
+
logger37.debug({ path: filePath }, "Xray mapping written");
|
|
12870
13796
|
}
|
|
12871
13797
|
function findSuiteMapping(mapping, suiteId) {
|
|
12872
13798
|
return mapping.suites.find((s) => s.suiteId === suiteId);
|
|
@@ -12898,24 +13824,24 @@ function upsertCaseMapping(suiteMapping, caseMapping) {
|
|
|
12898
13824
|
}
|
|
12899
13825
|
return { ...suiteMapping, cases };
|
|
12900
13826
|
}
|
|
12901
|
-
var
|
|
13827
|
+
var import_path30, import_fs_extra23, logger37, MAPPING_FILE;
|
|
12902
13828
|
var init_xray_store = __esm({
|
|
12903
13829
|
"src/storage/xray-store.ts"() {
|
|
12904
13830
|
"use strict";
|
|
12905
13831
|
init_cjs_shims();
|
|
12906
|
-
|
|
12907
|
-
|
|
13832
|
+
import_path30 = __toESM(require("path"));
|
|
13833
|
+
import_fs_extra23 = __toESM(require("fs-extra"));
|
|
12908
13834
|
init_xray();
|
|
12909
13835
|
init_utils();
|
|
12910
13836
|
init_logger();
|
|
12911
|
-
|
|
13837
|
+
logger37 = createChildLogger("xray-store");
|
|
12912
13838
|
MAPPING_FILE = "xray-mapping.json";
|
|
12913
13839
|
}
|
|
12914
13840
|
});
|
|
12915
13841
|
|
|
12916
13842
|
// src/xray/service.ts
|
|
12917
13843
|
function contentHash(obj) {
|
|
12918
|
-
return
|
|
13844
|
+
return import_crypto11.default.createHash("sha256").update(JSON.stringify(obj)).digest("hex").slice(0, 16);
|
|
12919
13845
|
}
|
|
12920
13846
|
function suiteHash(suite) {
|
|
12921
13847
|
return contentHash({ name: suite.name, description: suite.description, tags: suite.tags });
|
|
@@ -12929,18 +13855,18 @@ function caseHash(tc) {
|
|
|
12929
13855
|
steps: tc.steps.map((s) => ({ instruction: s.instruction, code: s.generatedCode }))
|
|
12930
13856
|
});
|
|
12931
13857
|
}
|
|
12932
|
-
var
|
|
13858
|
+
var import_crypto11, logger38, XrayService;
|
|
12933
13859
|
var init_service = __esm({
|
|
12934
13860
|
"src/xray/service.ts"() {
|
|
12935
13861
|
"use strict";
|
|
12936
13862
|
init_cjs_shims();
|
|
12937
|
-
|
|
13863
|
+
import_crypto11 = __toESM(require("crypto"));
|
|
12938
13864
|
init_client();
|
|
12939
13865
|
init_xray_store();
|
|
12940
13866
|
init_suite_store();
|
|
12941
13867
|
init_case_store();
|
|
12942
13868
|
init_logger();
|
|
12943
|
-
|
|
13869
|
+
logger38 = createChildLogger("xray-service");
|
|
12944
13870
|
XrayService = class {
|
|
12945
13871
|
client;
|
|
12946
13872
|
rootDir;
|
|
@@ -13048,7 +13974,7 @@ var init_service = __esm({
|
|
|
13048
13974
|
const results = [];
|
|
13049
13975
|
let sm = findSuiteMapping(mapping, suiteId);
|
|
13050
13976
|
if (sm) {
|
|
13051
|
-
|
|
13977
|
+
logger38.info({ suiteId }, "Suite already mapped in Xray \u2014 delegating to sync");
|
|
13052
13978
|
return this.syncSuite(suiteId);
|
|
13053
13979
|
}
|
|
13054
13980
|
const folderPath = `/${suite.name}`;
|
|
@@ -13077,7 +14003,7 @@ var init_service = __esm({
|
|
|
13077
14003
|
await writeXrayMapping(this.rootDir, mapping);
|
|
13078
14004
|
createdTestIssueIds.push(test.id);
|
|
13079
14005
|
results.push({ success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" });
|
|
13080
|
-
|
|
14006
|
+
logger38.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
|
|
13081
14007
|
} catch (err) {
|
|
13082
14008
|
results.push({
|
|
13083
14009
|
success: false,
|
|
@@ -13097,9 +14023,9 @@ var init_service = __esm({
|
|
|
13097
14023
|
if (allTestPairs.length > 0) {
|
|
13098
14024
|
try {
|
|
13099
14025
|
await this.client.createFolderAndAddTests(suite.name, allTestPairs);
|
|
13100
|
-
|
|
14026
|
+
logger38.info({ folderPath, testCount: allTestPairs.length }, "Tests organized into folder");
|
|
13101
14027
|
} catch (err) {
|
|
13102
|
-
|
|
14028
|
+
logger38.warn({ folderPath, err: err instanceof Error ? err.message : String(err) }, "Folder organization failed (non-fatal)");
|
|
13103
14029
|
}
|
|
13104
14030
|
}
|
|
13105
14031
|
if (!findSuiteMapping(mapping, suiteId)?.xrayTestSetId) {
|
|
@@ -13116,7 +14042,7 @@ var init_service = __esm({
|
|
|
13116
14042
|
mapping = upsertSuiteMapping(mapping, sm);
|
|
13117
14043
|
await writeXrayMapping(this.rootDir, mapping);
|
|
13118
14044
|
results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
|
|
13119
|
-
|
|
14045
|
+
logger38.info({ suiteId, xrayKey: testSet.key, testCount: createdTestIssueIds.length }, "Test Set created with linked tests");
|
|
13120
14046
|
} catch (err) {
|
|
13121
14047
|
results.push({
|
|
13122
14048
|
success: false,
|
|
@@ -13185,7 +14111,7 @@ var init_service = __esm({
|
|
|
13185
14111
|
{ key: caseResult.result.xrayKey, id: caseResult.result.xrayId }
|
|
13186
14112
|
]);
|
|
13187
14113
|
} catch (err) {
|
|
13188
|
-
|
|
14114
|
+
logger38.warn({ err: err instanceof Error ? err.message : String(err) }, "Folder organization failed for single case (non-fatal)");
|
|
13189
14115
|
}
|
|
13190
14116
|
}
|
|
13191
14117
|
if (caseResult.result.xrayId && sm.xrayTestSetIssueId) {
|
|
@@ -13195,7 +14121,7 @@ var init_service = __esm({
|
|
|
13195
14121
|
[{ key: caseResult.result.xrayKey ?? "", id: caseResult.result.xrayId }]
|
|
13196
14122
|
);
|
|
13197
14123
|
} catch (err) {
|
|
13198
|
-
|
|
14124
|
+
logger38.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to link test to test set");
|
|
13199
14125
|
}
|
|
13200
14126
|
}
|
|
13201
14127
|
return this.summarizeResults([caseResult.result]);
|
|
@@ -13226,7 +14152,7 @@ var init_service = __esm({
|
|
|
13226
14152
|
const updatedSm = upsertCaseMapping(sm, cm);
|
|
13227
14153
|
const updatedMapping = upsertSuiteMapping(mapping, updatedSm);
|
|
13228
14154
|
await writeXrayMapping(this.rootDir, updatedMapping);
|
|
13229
|
-
|
|
14155
|
+
logger38.info({ caseId: tc.id, xrayKey: test.key, xrayId: test.id }, "Test created");
|
|
13230
14156
|
return {
|
|
13231
14157
|
result: { success: true, caseId: tc.id, xrayKey: test.key, xrayId: test.id, action: "created" },
|
|
13232
14158
|
mapping: updatedMapping
|
|
@@ -13292,7 +14218,7 @@ var init_service = __esm({
|
|
|
13292
14218
|
mapping = upsertSuiteMapping(mapping, sm);
|
|
13293
14219
|
await writeXrayMapping(this.rootDir, mapping);
|
|
13294
14220
|
results.push({ success: true, suiteId, xrayKey: testSet.key, action: "created" });
|
|
13295
|
-
|
|
14221
|
+
logger38.info({ suiteId, xrayKey: testSet.key }, "Test Set created during sync (was missing)");
|
|
13296
14222
|
} catch (err) {
|
|
13297
14223
|
results.push({
|
|
13298
14224
|
success: false,
|
|
@@ -13388,11 +14314,11 @@ var init_service = __esm({
|
|
|
13388
14314
|
try {
|
|
13389
14315
|
await this.client.createFolderAndAddTests(suite.name, newTests);
|
|
13390
14316
|
} catch (folderErr) {
|
|
13391
|
-
|
|
14317
|
+
logger38.warn({ err: folderErr instanceof Error ? folderErr.message : String(folderErr) }, "Folder organization failed for synced tests (non-fatal)");
|
|
13392
14318
|
}
|
|
13393
14319
|
}
|
|
13394
14320
|
} catch (err) {
|
|
13395
|
-
|
|
14321
|
+
logger38.warn({ err }, "Failed to link new tests to test set");
|
|
13396
14322
|
}
|
|
13397
14323
|
}
|
|
13398
14324
|
}
|
|
@@ -13549,7 +14475,7 @@ async function xrayRoutes(fastify) {
|
|
|
13549
14475
|
const service = requireService(reply);
|
|
13550
14476
|
if (!service) return;
|
|
13551
14477
|
try {
|
|
13552
|
-
|
|
14478
|
+
logger39.info("Creating all suites and cases in Xray");
|
|
13553
14479
|
const result = await service.createAll();
|
|
13554
14480
|
return reply.send(result);
|
|
13555
14481
|
} catch (err) {
|
|
@@ -13560,7 +14486,7 @@ async function xrayRoutes(fastify) {
|
|
|
13560
14486
|
const service = requireService(reply);
|
|
13561
14487
|
if (!service) return;
|
|
13562
14488
|
try {
|
|
13563
|
-
|
|
14489
|
+
logger39.info({ suiteId: req.params.suiteId }, "Creating suite in Xray");
|
|
13564
14490
|
const result = await service.createSuite(req.params.suiteId);
|
|
13565
14491
|
return reply.send(result);
|
|
13566
14492
|
} catch (err) {
|
|
@@ -13584,7 +14510,7 @@ async function xrayRoutes(fastify) {
|
|
|
13584
14510
|
const service = requireService(reply);
|
|
13585
14511
|
if (!service) return;
|
|
13586
14512
|
try {
|
|
13587
|
-
|
|
14513
|
+
logger39.info("Syncing all suites and cases with Xray");
|
|
13588
14514
|
const result = await service.syncAll();
|
|
13589
14515
|
return reply.send(result);
|
|
13590
14516
|
} catch (err) {
|
|
@@ -13595,7 +14521,7 @@ async function xrayRoutes(fastify) {
|
|
|
13595
14521
|
const service = requireService(reply);
|
|
13596
14522
|
if (!service) return;
|
|
13597
14523
|
try {
|
|
13598
|
-
|
|
14524
|
+
logger39.info({ suiteId: req.params.suiteId }, "Syncing suite with Xray");
|
|
13599
14525
|
const result = await service.syncSuite(req.params.suiteId);
|
|
13600
14526
|
return reply.send(result);
|
|
13601
14527
|
} catch (err) {
|
|
@@ -13626,7 +14552,7 @@ async function xrayRoutes(fastify) {
|
|
|
13626
14552
|
}
|
|
13627
14553
|
});
|
|
13628
14554
|
}
|
|
13629
|
-
var
|
|
14555
|
+
var logger39;
|
|
13630
14556
|
var init_xray2 = __esm({
|
|
13631
14557
|
"src/server/routes/xray.ts"() {
|
|
13632
14558
|
"use strict";
|
|
@@ -13634,21 +14560,21 @@ var init_xray2 = __esm({
|
|
|
13634
14560
|
init_service();
|
|
13635
14561
|
init_utils2();
|
|
13636
14562
|
init_logger();
|
|
13637
|
-
|
|
14563
|
+
logger39 = createChildLogger("xray-routes");
|
|
13638
14564
|
}
|
|
13639
14565
|
});
|
|
13640
14566
|
|
|
13641
14567
|
// src/git/service.ts
|
|
13642
|
-
var import_simple_git,
|
|
14568
|
+
var import_simple_git, import_fs_extra24, import_path31, logger40, GitService;
|
|
13643
14569
|
var init_service2 = __esm({
|
|
13644
14570
|
"src/git/service.ts"() {
|
|
13645
14571
|
"use strict";
|
|
13646
14572
|
init_cjs_shims();
|
|
13647
14573
|
import_simple_git = __toESM(require("simple-git"));
|
|
13648
|
-
|
|
13649
|
-
|
|
14574
|
+
import_fs_extra24 = __toESM(require("fs-extra"));
|
|
14575
|
+
import_path31 = __toESM(require("path"));
|
|
13650
14576
|
init_logger();
|
|
13651
|
-
|
|
14577
|
+
logger40 = createChildLogger("git-service");
|
|
13652
14578
|
GitService = class {
|
|
13653
14579
|
git;
|
|
13654
14580
|
rootDir;
|
|
@@ -13710,7 +14636,7 @@ var init_service2 = __esm({
|
|
|
13710
14636
|
try {
|
|
13711
14637
|
await this.git.checkout(branch);
|
|
13712
14638
|
this.broadcastLog("Checkout branch", "done", branch);
|
|
13713
|
-
|
|
14639
|
+
logger40.info({ branch }, "Switched branch");
|
|
13714
14640
|
} catch (err) {
|
|
13715
14641
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13716
14642
|
this.broadcastLog("Checkout branch", "error", msg);
|
|
@@ -13728,7 +14654,7 @@ var init_service2 = __esm({
|
|
|
13728
14654
|
const hash = result.commit || "unknown";
|
|
13729
14655
|
this.broadcastLog("Create commit", "done", hash);
|
|
13730
14656
|
this.broadcastDone("commit");
|
|
13731
|
-
|
|
14657
|
+
logger40.info({ hash, message }, "Commit created");
|
|
13732
14658
|
return hash;
|
|
13733
14659
|
} catch (err) {
|
|
13734
14660
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -13745,7 +14671,7 @@ var init_service2 = __esm({
|
|
|
13745
14671
|
await this.git.pull(void 0, void 0, opts);
|
|
13746
14672
|
this.broadcastLog(label, "done", rebase ? "Rebase successful" : "Pull successful");
|
|
13747
14673
|
this.broadcastDone("pull");
|
|
13748
|
-
|
|
14674
|
+
logger40.info({ rebase }, "Pull completed");
|
|
13749
14675
|
} catch (err) {
|
|
13750
14676
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13751
14677
|
if (msg.includes("CONFLICT") || msg.includes("conflict") || msg.includes("could not apply")) {
|
|
@@ -13767,7 +14693,7 @@ var init_service2 = __esm({
|
|
|
13767
14693
|
await this.git.push();
|
|
13768
14694
|
this.broadcastLog("Push to origin", "done");
|
|
13769
14695
|
this.broadcastDone("push");
|
|
13770
|
-
|
|
14696
|
+
logger40.info("Push completed");
|
|
13771
14697
|
} catch (err) {
|
|
13772
14698
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13773
14699
|
this.broadcastLog("Push to origin", "error", msg);
|
|
@@ -13814,7 +14740,7 @@ var init_service2 = __esm({
|
|
|
13814
14740
|
throw pushErr;
|
|
13815
14741
|
}
|
|
13816
14742
|
this.broadcastDone("full-sync");
|
|
13817
|
-
|
|
14743
|
+
logger40.info("Full sync completed");
|
|
13818
14744
|
} catch (err) {
|
|
13819
14745
|
this.broadcastDone("full-sync");
|
|
13820
14746
|
throw err;
|
|
@@ -13846,8 +14772,8 @@ var init_service2 = __esm({
|
|
|
13846
14772
|
return status.conflicted;
|
|
13847
14773
|
}
|
|
13848
14774
|
async getFileConflictContent(filePath) {
|
|
13849
|
-
const fullPath =
|
|
13850
|
-
const content = await
|
|
14775
|
+
const fullPath = import_path31.default.join(this.rootDir, filePath);
|
|
14776
|
+
const content = await import_fs_extra24.default.readFile(fullPath, "utf-8");
|
|
13851
14777
|
let ours = "";
|
|
13852
14778
|
let theirs = "";
|
|
13853
14779
|
let section = "none";
|
|
@@ -13873,10 +14799,10 @@ var init_service2 = __esm({
|
|
|
13873
14799
|
return { ours: ours.trimEnd(), theirs: theirs.trimEnd() };
|
|
13874
14800
|
}
|
|
13875
14801
|
async resolveConflict(filePath, resolvedContent) {
|
|
13876
|
-
const fullPath =
|
|
13877
|
-
await
|
|
14802
|
+
const fullPath = import_path31.default.join(this.rootDir, filePath);
|
|
14803
|
+
await import_fs_extra24.default.writeFile(fullPath, resolvedContent, "utf-8");
|
|
13878
14804
|
await this.git.add(filePath);
|
|
13879
|
-
|
|
14805
|
+
logger40.info({ filePath }, "Conflict resolved");
|
|
13880
14806
|
}
|
|
13881
14807
|
async continueRebase() {
|
|
13882
14808
|
this.broadcastLog("Continuing rebase...", "running");
|
|
@@ -13951,10 +14877,10 @@ async function generateCommitMessage(diff) {
|
|
|
13951
14877
|
cleaned = cleaned.replace(/^["'`]+|["'`]+$/g, "");
|
|
13952
14878
|
cleaned = cleaned.replace(/^(commit message:?\s*)/i, "");
|
|
13953
14879
|
cleaned = cleaned.split("\n")[0].trim();
|
|
13954
|
-
|
|
14880
|
+
logger41.info({ messageLength: cleaned.length }, "AI commit message generated");
|
|
13955
14881
|
return cleaned || "chore: update files";
|
|
13956
14882
|
} catch (err) {
|
|
13957
|
-
|
|
14883
|
+
logger41.warn({ err: err instanceof Error ? err.message : String(err) }, "AI commit message generation failed");
|
|
13958
14884
|
return "chore: update files";
|
|
13959
14885
|
}
|
|
13960
14886
|
}
|
|
@@ -13981,21 +14907,21 @@ ${theirs}`);
|
|
|
13981
14907
|
const provider = createProvider(void 0, { temperature: 0.4 });
|
|
13982
14908
|
let result = await provider.generateCode(instruction, context);
|
|
13983
14909
|
result = result.replace(/^```[\w]*\n?/, "").replace(/\n?```$/, "");
|
|
13984
|
-
|
|
14910
|
+
logger41.info({ filePath, resultLength: result.length }, "AI conflict resolution generated");
|
|
13985
14911
|
return result;
|
|
13986
14912
|
} catch (err) {
|
|
13987
|
-
|
|
14913
|
+
logger41.warn({ filePath, err: err instanceof Error ? err.message : String(err) }, "AI conflict resolution failed");
|
|
13988
14914
|
throw err;
|
|
13989
14915
|
}
|
|
13990
14916
|
}
|
|
13991
|
-
var
|
|
14917
|
+
var logger41;
|
|
13992
14918
|
var init_ai = __esm({
|
|
13993
14919
|
"src/git/ai.ts"() {
|
|
13994
14920
|
"use strict";
|
|
13995
14921
|
init_cjs_shims();
|
|
13996
14922
|
init_router();
|
|
13997
14923
|
init_logger();
|
|
13998
|
-
|
|
14924
|
+
logger41 = createChildLogger("git-ai");
|
|
13999
14925
|
}
|
|
14000
14926
|
});
|
|
14001
14927
|
|
|
@@ -14012,7 +14938,7 @@ async function gitRoutes(fastify) {
|
|
|
14012
14938
|
_isGitRepo = true;
|
|
14013
14939
|
} catch {
|
|
14014
14940
|
_isGitRepo = false;
|
|
14015
|
-
|
|
14941
|
+
logger42.info("Project directory is not a git repository \u2014 git features disabled");
|
|
14016
14942
|
}
|
|
14017
14943
|
return _isGitRepo;
|
|
14018
14944
|
};
|
|
@@ -14188,7 +15114,7 @@ async function gitRoutes(fastify) {
|
|
|
14188
15114
|
const changedFiles = stdout.split("\n").filter(Boolean);
|
|
14189
15115
|
const { listSuiteDirs: listSuiteDirs2, readSuite: readSuite2 } = await Promise.resolve().then(() => (init_suite_store(), suite_store_exports));
|
|
14190
15116
|
const { listCases: listCases2 } = await Promise.resolve().then(() => (init_case_store(), case_store_exports));
|
|
14191
|
-
const testsDir =
|
|
15117
|
+
const testsDir = import_path32.default.join(rootDir, "tests");
|
|
14192
15118
|
const suiteDirs = await listSuiteDirs2(testsDir);
|
|
14193
15119
|
const impacted = [];
|
|
14194
15120
|
for (const suiteDir of suiteDirs) {
|
|
@@ -14198,8 +15124,8 @@ async function gitRoutes(fastify) {
|
|
|
14198
15124
|
for (const tc of cases) {
|
|
14199
15125
|
const allText = [tc.name, tc.description ?? "", ...tc.steps.map((s) => s.instruction)].join(" ").toLowerCase();
|
|
14200
15126
|
for (const file of changedFiles) {
|
|
14201
|
-
const basename =
|
|
14202
|
-
const dirname =
|
|
15127
|
+
const basename = import_path32.default.basename(file).toLowerCase();
|
|
15128
|
+
const dirname = import_path32.default.dirname(file).toLowerCase();
|
|
14203
15129
|
if (allText.includes(basename) || allText.includes(dirname)) {
|
|
14204
15130
|
impacted.push({ caseId: tc.id, caseName: tc.name, suiteId: suite.id, suiteName: suite.name, reason: `Mentions "${file}"` });
|
|
14205
15131
|
break;
|
|
@@ -14215,18 +15141,18 @@ async function gitRoutes(fastify) {
|
|
|
14215
15141
|
}
|
|
14216
15142
|
});
|
|
14217
15143
|
}
|
|
14218
|
-
var
|
|
15144
|
+
var import_path32, import_zod18, logger42, CheckoutBody, CommitBody, PullBody, FullSyncBody, ResolveConflictBody, ResolveConflictAIBody;
|
|
14219
15145
|
var init_git = __esm({
|
|
14220
15146
|
"src/server/routes/git.ts"() {
|
|
14221
15147
|
"use strict";
|
|
14222
15148
|
init_cjs_shims();
|
|
14223
|
-
|
|
15149
|
+
import_path32 = __toESM(require("path"));
|
|
14224
15150
|
import_zod18 = require("zod");
|
|
14225
15151
|
init_service2();
|
|
14226
15152
|
init_ai();
|
|
14227
15153
|
init_utils2();
|
|
14228
15154
|
init_logger();
|
|
14229
|
-
|
|
15155
|
+
logger42 = createChildLogger("git-routes");
|
|
14230
15156
|
CheckoutBody = import_zod18.z.object({ branch: import_zod18.z.string().min(1) });
|
|
14231
15157
|
CommitBody = import_zod18.z.object({ message: import_zod18.z.string().min(1) });
|
|
14232
15158
|
PullBody = import_zod18.z.object({ rebase: import_zod18.z.boolean().optional() });
|
|
@@ -14271,12 +15197,12 @@ function parseCSV(content) {
|
|
|
14271
15197
|
}
|
|
14272
15198
|
async function dataFileRoutes(fastify) {
|
|
14273
15199
|
const rootDir = fastify.rootDir;
|
|
14274
|
-
const dataDir =
|
|
14275
|
-
await
|
|
15200
|
+
const dataDir = import_path33.default.join(rootDir, "tests", "data");
|
|
15201
|
+
await import_fs_extra25.default.ensureDir(dataDir);
|
|
14276
15202
|
fastify.get("/data-files", async (_req, reply) => {
|
|
14277
15203
|
try {
|
|
14278
|
-
await
|
|
14279
|
-
const entries = await
|
|
15204
|
+
await import_fs_extra25.default.ensureDir(dataDir);
|
|
15205
|
+
const entries = await import_fs_extra25.default.readdir(dataDir, { withFileTypes: true });
|
|
14280
15206
|
const files = entries.filter((e) => e.isFile() && ALLOWED_EXTENSIONS.some((ext) => e.name.endsWith(ext))).map((e) => ({
|
|
14281
15207
|
name: e.name,
|
|
14282
15208
|
type: e.name.endsWith(".csv") ? "csv" : "json",
|
|
@@ -14289,12 +15215,12 @@ async function dataFileRoutes(fastify) {
|
|
|
14289
15215
|
});
|
|
14290
15216
|
fastify.get("/data-files/:filename", async (req, reply) => {
|
|
14291
15217
|
try {
|
|
14292
|
-
const filename =
|
|
14293
|
-
const filePath =
|
|
14294
|
-
if (!await
|
|
15218
|
+
const filename = import_path33.default.basename(req.params.filename);
|
|
15219
|
+
const filePath = import_path33.default.join(dataDir, filename);
|
|
15220
|
+
if (!await import_fs_extra25.default.pathExists(filePath)) {
|
|
14295
15221
|
return reply.status(404).send({ error: `Data file "${filename}" not found` });
|
|
14296
15222
|
}
|
|
14297
|
-
const raw = await
|
|
15223
|
+
const raw = await import_fs_extra25.default.readFile(filePath, "utf8");
|
|
14298
15224
|
let rows;
|
|
14299
15225
|
if (filename.endsWith(".csv")) {
|
|
14300
15226
|
rows = parseCSV(raw);
|
|
@@ -14315,14 +15241,14 @@ async function dataFileRoutes(fastify) {
|
|
|
14315
15241
|
if (!filename) {
|
|
14316
15242
|
return reply.status(400).send({ error: "filename is required" });
|
|
14317
15243
|
}
|
|
14318
|
-
const ext =
|
|
15244
|
+
const ext = import_path33.default.extname(filename).toLowerCase();
|
|
14319
15245
|
if (!ALLOWED_EXTENSIONS.includes(ext)) {
|
|
14320
15246
|
return reply.status(400).send({ error: `Only ${ALLOWED_EXTENSIONS.join(", ")} files are allowed` });
|
|
14321
15247
|
}
|
|
14322
|
-
const safeName =
|
|
14323
|
-
const filePath =
|
|
14324
|
-
await
|
|
14325
|
-
await
|
|
15248
|
+
const safeName = import_path33.default.basename(filename);
|
|
15249
|
+
const filePath = import_path33.default.join(dataDir, safeName);
|
|
15250
|
+
await import_fs_extra25.default.ensureDir(dataDir);
|
|
15251
|
+
await import_fs_extra25.default.writeFile(filePath, content, "utf8");
|
|
14326
15252
|
let rowCount = 0;
|
|
14327
15253
|
try {
|
|
14328
15254
|
if (ext === ".csv") {
|
|
@@ -14333,7 +15259,7 @@ async function dataFileRoutes(fastify) {
|
|
|
14333
15259
|
}
|
|
14334
15260
|
} catch {
|
|
14335
15261
|
}
|
|
14336
|
-
|
|
15262
|
+
logger43.info({ filename: safeName, rowCount }, "Data file uploaded");
|
|
14337
15263
|
return reply.status(201).send({
|
|
14338
15264
|
filename: safeName,
|
|
14339
15265
|
path: `data/${safeName}`,
|
|
@@ -14345,13 +15271,13 @@ async function dataFileRoutes(fastify) {
|
|
|
14345
15271
|
});
|
|
14346
15272
|
fastify.put("/data-files/:filename", async (req, reply) => {
|
|
14347
15273
|
try {
|
|
14348
|
-
const filename =
|
|
14349
|
-
const filePath =
|
|
15274
|
+
const filename = import_path33.default.basename(req.params.filename);
|
|
15275
|
+
const filePath = import_path33.default.join(dataDir, filename);
|
|
14350
15276
|
const body = req.body;
|
|
14351
15277
|
const content = String(body.content ?? "");
|
|
14352
|
-
await
|
|
14353
|
-
await
|
|
14354
|
-
|
|
15278
|
+
await import_fs_extra25.default.ensureDir(dataDir);
|
|
15279
|
+
await import_fs_extra25.default.writeFile(filePath, content, "utf8");
|
|
15280
|
+
logger43.info({ filename }, "Data file updated");
|
|
14355
15281
|
return reply.send({ filename, path: `data/${filename}` });
|
|
14356
15282
|
} catch (err) {
|
|
14357
15283
|
return sendError(reply, 500, "Failed to update data file", err);
|
|
@@ -14359,29 +15285,29 @@ async function dataFileRoutes(fastify) {
|
|
|
14359
15285
|
});
|
|
14360
15286
|
fastify.delete("/data-files/:filename", async (req, reply) => {
|
|
14361
15287
|
try {
|
|
14362
|
-
const filename =
|
|
14363
|
-
const filePath =
|
|
14364
|
-
if (!await
|
|
15288
|
+
const filename = import_path33.default.basename(req.params.filename);
|
|
15289
|
+
const filePath = import_path33.default.join(dataDir, filename);
|
|
15290
|
+
if (!await import_fs_extra25.default.pathExists(filePath)) {
|
|
14365
15291
|
return reply.status(404).send({ error: `Data file "${filename}" not found` });
|
|
14366
15292
|
}
|
|
14367
|
-
await
|
|
14368
|
-
|
|
15293
|
+
await import_fs_extra25.default.remove(filePath);
|
|
15294
|
+
logger43.info({ filename }, "Data file deleted");
|
|
14369
15295
|
return reply.status(204).send();
|
|
14370
15296
|
} catch (err) {
|
|
14371
15297
|
return sendError(reply, 500, "Failed to delete data file", err);
|
|
14372
15298
|
}
|
|
14373
15299
|
});
|
|
14374
15300
|
}
|
|
14375
|
-
var
|
|
15301
|
+
var import_path33, import_fs_extra25, logger43, ALLOWED_EXTENSIONS;
|
|
14376
15302
|
var init_data_files = __esm({
|
|
14377
15303
|
"src/server/routes/data-files.ts"() {
|
|
14378
15304
|
"use strict";
|
|
14379
15305
|
init_cjs_shims();
|
|
14380
|
-
|
|
14381
|
-
|
|
15306
|
+
import_path33 = __toESM(require("path"));
|
|
15307
|
+
import_fs_extra25 = __toESM(require("fs-extra"));
|
|
14382
15308
|
init_utils2();
|
|
14383
15309
|
init_logger();
|
|
14384
|
-
|
|
15310
|
+
logger43 = createChildLogger("routes/data-files");
|
|
14385
15311
|
ALLOWED_EXTENSIONS = [".json", ".csv"];
|
|
14386
15312
|
}
|
|
14387
15313
|
});
|
|
@@ -14400,7 +15326,7 @@ function matchWeight(matchedOn) {
|
|
|
14400
15326
|
}
|
|
14401
15327
|
}
|
|
14402
15328
|
async function searchRoutes(fastify) {
|
|
14403
|
-
const testsDir =
|
|
15329
|
+
const testsDir = import_path34.default.join(fastify.rootDir, "tests");
|
|
14404
15330
|
fastify.get("/search", async (req, reply) => {
|
|
14405
15331
|
const q = (req.query.q ?? "").trim();
|
|
14406
15332
|
if (q.length < 2) {
|
|
@@ -14462,12 +15388,12 @@ async function searchRoutes(fastify) {
|
|
|
14462
15388
|
}
|
|
14463
15389
|
});
|
|
14464
15390
|
}
|
|
14465
|
-
var
|
|
15391
|
+
var import_path34;
|
|
14466
15392
|
var init_search = __esm({
|
|
14467
15393
|
"src/server/routes/search.ts"() {
|
|
14468
15394
|
"use strict";
|
|
14469
15395
|
init_cjs_shims();
|
|
14470
|
-
|
|
15396
|
+
import_path34 = __toESM(require("path"));
|
|
14471
15397
|
init_suite_store();
|
|
14472
15398
|
init_case_store();
|
|
14473
15399
|
init_utils2();
|
|
@@ -14748,13 +15674,13 @@ var init_flakiness = __esm({
|
|
|
14748
15674
|
|
|
14749
15675
|
// src/storage/step-library-store.ts
|
|
14750
15676
|
function libraryPath(rootDir) {
|
|
14751
|
-
return
|
|
15677
|
+
return import_path35.default.join(rootDir, "tests", ".step-library.json");
|
|
14752
15678
|
}
|
|
14753
15679
|
async function readLibrary(rootDir) {
|
|
14754
15680
|
const filePath = libraryPath(rootDir);
|
|
14755
|
-
if (!await
|
|
15681
|
+
if (!await import_fs_extra26.default.pathExists(filePath)) return [];
|
|
14756
15682
|
try {
|
|
14757
|
-
const raw = await
|
|
15683
|
+
const raw = await import_fs_extra26.default.readJson(filePath);
|
|
14758
15684
|
if (!Array.isArray(raw)) return [];
|
|
14759
15685
|
return raw.map((item) => LibraryStepSchema.parse(item));
|
|
14760
15686
|
} catch {
|
|
@@ -14763,7 +15689,7 @@ async function readLibrary(rootDir) {
|
|
|
14763
15689
|
}
|
|
14764
15690
|
async function writeLibrary(rootDir, steps) {
|
|
14765
15691
|
const filePath = libraryPath(rootDir);
|
|
14766
|
-
await
|
|
15692
|
+
await import_fs_extra26.default.ensureDir(import_path35.default.dirname(filePath));
|
|
14767
15693
|
await atomicWriteJson(filePath, steps);
|
|
14768
15694
|
}
|
|
14769
15695
|
async function addStep(rootDir, data) {
|
|
@@ -14773,6 +15699,7 @@ async function addStep(rootDir, data) {
|
|
|
14773
15699
|
id: (0, import_uuid3.v4)(),
|
|
14774
15700
|
name: data.name,
|
|
14775
15701
|
instruction: data.instruction,
|
|
15702
|
+
generatedCode: data.generatedCode ?? "",
|
|
14776
15703
|
tags: data.tags ?? [],
|
|
14777
15704
|
createdAt: now2,
|
|
14778
15705
|
updatedAt: now2
|
|
@@ -14794,13 +15721,13 @@ async function deleteStep(rootDir, id) {
|
|
|
14794
15721
|
const filtered = steps.filter((s) => s.id !== id);
|
|
14795
15722
|
await writeLibrary(rootDir, filtered);
|
|
14796
15723
|
}
|
|
14797
|
-
var
|
|
15724
|
+
var import_path35, import_fs_extra26, import_uuid3, import_zod20, LibraryStepSchema;
|
|
14798
15725
|
var init_step_library_store = __esm({
|
|
14799
15726
|
"src/storage/step-library-store.ts"() {
|
|
14800
15727
|
"use strict";
|
|
14801
15728
|
init_cjs_shims();
|
|
14802
|
-
|
|
14803
|
-
|
|
15729
|
+
import_path35 = __toESM(require("path"));
|
|
15730
|
+
import_fs_extra26 = __toESM(require("fs-extra"));
|
|
14804
15731
|
import_uuid3 = require("uuid");
|
|
14805
15732
|
import_zod20 = require("zod");
|
|
14806
15733
|
init_utils();
|
|
@@ -14808,6 +15735,7 @@ var init_step_library_store = __esm({
|
|
|
14808
15735
|
id: import_zod20.z.string(),
|
|
14809
15736
|
name: import_zod20.z.string().min(1),
|
|
14810
15737
|
instruction: import_zod20.z.string().min(1),
|
|
15738
|
+
generatedCode: import_zod20.z.string().optional().default(""),
|
|
14811
15739
|
tags: import_zod20.z.array(import_zod20.z.string()).default([]),
|
|
14812
15740
|
createdAt: import_zod20.z.string(),
|
|
14813
15741
|
updatedAt: import_zod20.z.string()
|
|
@@ -14854,6 +15782,40 @@ async function stepLibraryRoutes(fastify) {
|
|
|
14854
15782
|
return sendError(reply, 500, "Failed to delete library step", err);
|
|
14855
15783
|
}
|
|
14856
15784
|
});
|
|
15785
|
+
fastify.post("/step-library/:id/generate", async (req, reply) => {
|
|
15786
|
+
let mcpSession;
|
|
15787
|
+
try {
|
|
15788
|
+
const steps = await readLibrary(rootDir);
|
|
15789
|
+
const step = steps.find((s) => s.id === req.params.id);
|
|
15790
|
+
if (!step) return reply.status(404).send({ error: `Library step "${req.params.id}" not found` });
|
|
15791
|
+
const config = await readConfig(rootDir);
|
|
15792
|
+
const variables = await resolveVariables(rootDir).catch(() => ({}));
|
|
15793
|
+
const useMcp = config.mcp?.enabled;
|
|
15794
|
+
if (useMcp) {
|
|
15795
|
+
mcpSession = new McpSession({
|
|
15796
|
+
headless: config.mcp?.headless ?? true,
|
|
15797
|
+
actionTimeout: config.mcp?.actionTimeout ?? 15e3,
|
|
15798
|
+
idleTimeout: config.mcp?.idleTimeout ?? 3e4
|
|
15799
|
+
});
|
|
15800
|
+
}
|
|
15801
|
+
const router = createSmartRouter(rootDir, mcpSession, useMcp ? config.mcp?.actThenScript : false);
|
|
15802
|
+
const result = await router.generate(step.instruction, {
|
|
15803
|
+
url: config.baseUrl,
|
|
15804
|
+
title: "",
|
|
15805
|
+
interactiveElements: "",
|
|
15806
|
+
htmlSnapshot: "",
|
|
15807
|
+
previousSteps: [],
|
|
15808
|
+
variables
|
|
15809
|
+
});
|
|
15810
|
+
await router.getCache().persist();
|
|
15811
|
+
const updated = await updateStep(rootDir, step.id, { generatedCode: result.code });
|
|
15812
|
+
return reply.send({ step: updated, strategy: result.strategy, cost: result.cost });
|
|
15813
|
+
} catch (err) {
|
|
15814
|
+
return sendError(reply, 500, "Failed to generate code for library step", err);
|
|
15815
|
+
} finally {
|
|
15816
|
+
mcpSession?.release();
|
|
15817
|
+
}
|
|
15818
|
+
});
|
|
14857
15819
|
}
|
|
14858
15820
|
var import_zod21, CreateBody, UpdateBody;
|
|
14859
15821
|
var init_step_library = __esm({
|
|
@@ -14862,20 +15824,701 @@ var init_step_library = __esm({
|
|
|
14862
15824
|
init_cjs_shims();
|
|
14863
15825
|
import_zod21 = require("zod");
|
|
14864
15826
|
init_step_library_store();
|
|
15827
|
+
init_config_store();
|
|
15828
|
+
init_variable_store();
|
|
15829
|
+
init_router();
|
|
15830
|
+
init_mcp_session();
|
|
14865
15831
|
init_utils2();
|
|
14866
15832
|
CreateBody = import_zod21.z.object({
|
|
14867
15833
|
name: import_zod21.z.string().min(1),
|
|
14868
15834
|
instruction: import_zod21.z.string().min(1),
|
|
15835
|
+
generatedCode: import_zod21.z.string().optional(),
|
|
14869
15836
|
tags: import_zod21.z.array(import_zod21.z.string()).optional()
|
|
14870
15837
|
});
|
|
14871
15838
|
UpdateBody = import_zod21.z.object({
|
|
14872
15839
|
name: import_zod21.z.string().min(1).optional(),
|
|
14873
15840
|
instruction: import_zod21.z.string().min(1).optional(),
|
|
15841
|
+
generatedCode: import_zod21.z.string().optional(),
|
|
14874
15842
|
tags: import_zod21.z.array(import_zod21.z.string()).optional()
|
|
14875
15843
|
});
|
|
14876
15844
|
}
|
|
14877
15845
|
});
|
|
14878
15846
|
|
|
15847
|
+
// src/engine/recorder.ts
|
|
15848
|
+
function buildCaptureScript() {
|
|
15849
|
+
return `
|
|
15850
|
+
(function() {
|
|
15851
|
+
if (window.__amListenersAttached) return;
|
|
15852
|
+
window.__amListenersAttached = true;
|
|
15853
|
+
|
|
15854
|
+
// Safety wrapper \u2014 queue if binding not ready
|
|
15855
|
+
var sendQueue = [];
|
|
15856
|
+
function safeSend(json) {
|
|
15857
|
+
if (typeof window.__amSend === 'function') {
|
|
15858
|
+
while (sendQueue.length > 0) { try { window.__amSend(sendQueue.shift()); } catch(e) {} }
|
|
15859
|
+
try { window.__amSend(json); } catch(e) { sendQueue.push(json); }
|
|
15860
|
+
} else { sendQueue.push(json); }
|
|
15861
|
+
}
|
|
15862
|
+
var retryInterval = setInterval(function() {
|
|
15863
|
+
if (typeof window.__amSend === 'function' && sendQueue.length > 0) {
|
|
15864
|
+
while (sendQueue.length > 0) { try { window.__amSend(sendQueue.shift()); } catch(e) { break; } }
|
|
15865
|
+
}
|
|
15866
|
+
if (typeof window.__amSend === 'function') clearInterval(retryInterval);
|
|
15867
|
+
}, 200);
|
|
15868
|
+
|
|
15869
|
+
// \u2500\u2500 Detect iframe context \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
15870
|
+
// When running inside an iframe, compute a selector for the iframe element
|
|
15871
|
+
// as seen from the parent frame. This lets Node.js use page.frameLocator().
|
|
15872
|
+
var _frameSelector = '';
|
|
15873
|
+
try {
|
|
15874
|
+
if (window !== window.top && window.frameElement) {
|
|
15875
|
+
var iframe = window.frameElement;
|
|
15876
|
+
if (iframe.id) { _frameSelector = '#' + iframe.id; }
|
|
15877
|
+
else if (iframe.getAttribute('name')) { _frameSelector = 'iframe[name="' + iframe.getAttribute('name') + '"]'; }
|
|
15878
|
+
else if (iframe.getAttribute('data-testid')) { _frameSelector = 'iframe[data-testid="' + iframe.getAttribute('data-testid') + '"]'; }
|
|
15879
|
+
else if (iframe.getAttribute('src')) {
|
|
15880
|
+
// Use src but strip query params for stability
|
|
15881
|
+
var src = iframe.getAttribute('src').split('?')[0];
|
|
15882
|
+
_frameSelector = 'iframe[src*="' + src + '"]';
|
|
15883
|
+
}
|
|
15884
|
+
else { _frameSelector = 'iframe'; }
|
|
15885
|
+
}
|
|
15886
|
+
} catch(e) {
|
|
15887
|
+
// Cross-origin iframe \u2014 frameElement is null, try parent postMessage approach
|
|
15888
|
+
// For same-origin iframes this works; cross-origin will use fallback
|
|
15889
|
+
_frameSelector = '';
|
|
15890
|
+
}
|
|
15891
|
+
|
|
15892
|
+
// \u2500\u2500 Collect element metadata \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
15893
|
+
function getMeta(el) {
|
|
15894
|
+
var tag = el.tagName.toLowerCase();
|
|
15895
|
+
var type = (el.getAttribute('type') || '').toLowerCase();
|
|
15896
|
+
var meta = {
|
|
15897
|
+
tag: tag,
|
|
15898
|
+
type: type,
|
|
15899
|
+
id: el.id || '',
|
|
15900
|
+
testId: el.getAttribute('data-testid') || el.getAttribute('data-test-id') || el.getAttribute('data-cy') || '',
|
|
15901
|
+
ariaLabel: el.getAttribute('aria-label') || '',
|
|
15902
|
+
ariaRole: el.getAttribute('role') || '',
|
|
15903
|
+
placeholder: el.getAttribute('placeholder') || '',
|
|
15904
|
+
labelText: getLabel(el) || '',
|
|
15905
|
+
innerText: visText(el),
|
|
15906
|
+
title: el.getAttribute('title') || '',
|
|
15907
|
+
name: el.getAttribute('name') || '',
|
|
15908
|
+
value: (el.value !== undefined ? el.value : '') || '',
|
|
15909
|
+
checked: !!el.checked,
|
|
15910
|
+
cssPath: cssPath(el),
|
|
15911
|
+
classes: Array.from(el.classList || []).join(' '),
|
|
15912
|
+
};
|
|
15913
|
+
if (_frameSelector) { meta.frameSelector = _frameSelector; }
|
|
15914
|
+
return meta;
|
|
15915
|
+
}
|
|
15916
|
+
|
|
15917
|
+
function getLabel(el) {
|
|
15918
|
+
if (!el.id) {
|
|
15919
|
+
var wrap = el.closest('label');
|
|
15920
|
+
if (wrap) {
|
|
15921
|
+
var clone = wrap.cloneNode(true);
|
|
15922
|
+
clone.querySelectorAll('input,select,textarea').forEach(function(i) { i.remove(); });
|
|
15923
|
+
var t = clone.textContent.trim();
|
|
15924
|
+
if (t) return t;
|
|
15925
|
+
}
|
|
15926
|
+
return null;
|
|
15927
|
+
}
|
|
15928
|
+
var lbl = document.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
15929
|
+
return lbl ? lbl.textContent.trim() : null;
|
|
15930
|
+
}
|
|
15931
|
+
|
|
15932
|
+
function visText(el) {
|
|
15933
|
+
var t = el.innerText || el.textContent || '';
|
|
15934
|
+
return t.trim().replace(/\\s+/g, ' ').slice(0, 80);
|
|
15935
|
+
}
|
|
15936
|
+
|
|
15937
|
+
function cssPath(el) {
|
|
15938
|
+
if (el.id) return '#' + CSS.escape(el.id);
|
|
15939
|
+
var parts = [], cur = el;
|
|
15940
|
+
for (var i = 0; i < 4 && cur && cur !== document.body; i++) {
|
|
15941
|
+
var sel = cur.tagName.toLowerCase();
|
|
15942
|
+
if (cur.id) { parts.unshift('#' + CSS.escape(cur.id)); break; }
|
|
15943
|
+
var cls = Array.from(cur.classList).filter(function(c) { return !/^(ng-|_|css-|sc-|jsx-|emotion)/.test(c); }).slice(0, 2);
|
|
15944
|
+
if (cls.length) sel += '.' + cls.map(function(c) { return CSS.escape(c); }).join('.');
|
|
15945
|
+
var p = cur.parentElement;
|
|
15946
|
+
if (p) {
|
|
15947
|
+
var sibs = Array.from(p.children).filter(function(s) { return s.tagName === cur.tagName; });
|
|
15948
|
+
if (sibs.length > 1) sel += ':nth-child(' + (sibs.indexOf(cur) + 1) + ')';
|
|
15949
|
+
}
|
|
15950
|
+
parts.unshift(sel);
|
|
15951
|
+
cur = cur.parentElement;
|
|
15952
|
+
}
|
|
15953
|
+
return parts.join(' > ');
|
|
15954
|
+
}
|
|
15955
|
+
|
|
15956
|
+
// Human-readable element description for instructions
|
|
15957
|
+
function descEl(meta) {
|
|
15958
|
+
var t = meta.tag, ty = meta.type;
|
|
15959
|
+
var nm = meta.ariaLabel || meta.labelText || meta.innerText || meta.placeholder || meta.title || meta.testId;
|
|
15960
|
+
if (t === 'a') return 'the "' + (nm || 'link') + '" link';
|
|
15961
|
+
if (t === 'button' || (t === 'input' && ty === 'submit')) return 'the "' + (nm || 'button') + '" button';
|
|
15962
|
+
if (t === 'input') {
|
|
15963
|
+
if (ty === 'checkbox') return 'the "' + (nm || 'checkbox') + '" checkbox';
|
|
15964
|
+
if (ty === 'radio') return 'the "' + (nm || 'radio button') + '" radio button';
|
|
15965
|
+
var lb = meta.labelText || meta.placeholder || nm;
|
|
15966
|
+
return 'the "' + (lb || 'input') + '" field';
|
|
15967
|
+
}
|
|
15968
|
+
if (t === 'textarea') return 'the "' + (meta.labelText || meta.placeholder || nm || 'text area') + '" field';
|
|
15969
|
+
if (t === 'select') return 'the "' + (meta.labelText || nm || 'dropdown') + '" dropdown';
|
|
15970
|
+
if (t === 'img') return 'the "' + (nm || 'image') + '" image';
|
|
15971
|
+
if (['h1','h2','h3','h4','h5','h6'].indexOf(t) >= 0) return 'the "' + (nm || 'heading') + '" heading';
|
|
15972
|
+
return 'the "' + (nm || t) + '" element';
|
|
15973
|
+
}
|
|
15974
|
+
|
|
15975
|
+
// Track the currently active input \u2014 only record its value when the user LEAVES the field
|
|
15976
|
+
var activeInput = null; // the DOM element being typed into
|
|
15977
|
+
var pendingInput = null; // the action payload to send
|
|
15978
|
+
function flushInput() {
|
|
15979
|
+
if (pendingInput && activeInput) {
|
|
15980
|
+
// Grab the FINAL value at flush time, not the value when input event fired
|
|
15981
|
+
var meta = getMeta(activeInput);
|
|
15982
|
+
var desc = descEl(meta);
|
|
15983
|
+
pendingInput.instruction = 'Enter "' + activeInput.value + '" in ' + desc;
|
|
15984
|
+
pendingInput.meta = meta;
|
|
15985
|
+
pendingInput.value = activeInput.value;
|
|
15986
|
+
safeSend(JSON.stringify(pendingInput));
|
|
15987
|
+
}
|
|
15988
|
+
pendingInput = null;
|
|
15989
|
+
activeInput = null;
|
|
15990
|
+
}
|
|
15991
|
+
|
|
15992
|
+
// Banner + styles (only in main frame \u2014 iframes don't need the banner)
|
|
15993
|
+
function ensureBanner() {
|
|
15994
|
+
if (window !== window.top) return;
|
|
15995
|
+
if (document.getElementById('__am-banner')) return;
|
|
15996
|
+
var s = document.createElement('style');
|
|
15997
|
+
s.textContent = '@keyframes __amp{0%,100%{opacity:1}50%{opacity:0.3}}.__am-ah{outline:3px solid #22c55e!important;outline-offset:2px;background:rgba(34,197,94,0.08)!important;}.__am-hh{outline:2px dashed #a78bfa!important;outline-offset:1px;}';
|
|
15998
|
+
document.head.appendChild(s);
|
|
15999
|
+
var b = document.createElement('div');
|
|
16000
|
+
b.id = '__am-banner';
|
|
16001
|
+
b.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;background:linear-gradient(90deg,#1e1e2e,#2d1b3d);color:#fff;font:13px system-ui,sans-serif;padding:6px 16px;display:flex;align-items:center;box-shadow:0 2px 8px rgba(0,0,0,0.3);gap:12px;';
|
|
16002
|
+
b.innerHTML = '<span style="display:inline-flex;align-items:center;gap:6px"><span style="width:8px;height:8px;border-radius:50%;background:#ef4444;animation:__amp 1.5s infinite"></span><b>Recording</b></span><span style="opacity:.65;font-size:12px">Shift+Click = assert | Ctrl+Shift+Click = soft assert | Ctrl+Shift+U = URL | Ctrl+Shift+T = title</span>';
|
|
16003
|
+
document.body.appendChild(b);
|
|
16004
|
+
}
|
|
16005
|
+
if (document.body) ensureBanner();
|
|
16006
|
+
else document.addEventListener('DOMContentLoaded', ensureBanner);
|
|
16007
|
+
|
|
16008
|
+
// Hover highlight
|
|
16009
|
+
var lastH = null;
|
|
16010
|
+
document.addEventListener('mouseover', function(e) {
|
|
16011
|
+
var el = e.target;
|
|
16012
|
+
if (!el || !el.tagName || !el.classList) return;
|
|
16013
|
+
if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
|
|
16014
|
+
if (lastH && lastH !== el && lastH.classList) lastH.classList.remove('__am-hh');
|
|
16015
|
+
el.classList.add('__am-hh');
|
|
16016
|
+
lastH = el;
|
|
16017
|
+
}, true);
|
|
16018
|
+
document.addEventListener('mouseout', function(e) {
|
|
16019
|
+
if (e.target && e.target.classList) e.target.classList.remove('__am-hh');
|
|
16020
|
+
}, true);
|
|
16021
|
+
|
|
16022
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
16023
|
+
// EVENT LISTENERS \u2014 send raw metadata, Node.js resolves locators
|
|
16024
|
+
// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550
|
|
16025
|
+
|
|
16026
|
+
// Click (normal = action, Shift = assert)
|
|
16027
|
+
document.addEventListener('click', function(e) {
|
|
16028
|
+
flushInput();
|
|
16029
|
+
var el = e.target;
|
|
16030
|
+
if (!el || !el.tagName) return;
|
|
16031
|
+
if (el.id === '__am-banner' || el.closest('#__am-banner')) return;
|
|
16032
|
+
|
|
16033
|
+
var meta = getMeta(el);
|
|
16034
|
+
var desc = descEl(meta);
|
|
16035
|
+
|
|
16036
|
+
// Shift+Click = ASSERTION (Ctrl+Shift+Click = SOFT assertion)
|
|
16037
|
+
if (e.shiftKey) {
|
|
16038
|
+
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
|
|
16039
|
+
var isSoft = e.ctrlKey || e.metaKey;
|
|
16040
|
+
var prefix = isSoft ? 'Soft verify ' : 'Verify ';
|
|
16041
|
+
if (el.classList) { el.classList.add('__am-ah'); setTimeout(function() { el.classList.remove('__am-ah'); }, 1200); }
|
|
16042
|
+
|
|
16043
|
+
var inst = '';
|
|
16044
|
+
if (meta.tag === 'input' || meta.tag === 'textarea') {
|
|
16045
|
+
inst = meta.value ? prefix + desc + ' has value "' + meta.value + '"' : prefix + desc + ' is visible';
|
|
16046
|
+
} else if (meta.type === 'checkbox' || meta.type === 'radio') {
|
|
16047
|
+
inst = prefix + desc + ' is ' + (meta.checked ? 'checked' : 'unchecked');
|
|
16048
|
+
} else {
|
|
16049
|
+
inst = (meta.innerText && meta.innerText.length <= 60) ? prefix + '"' + meta.innerText + '" text is visible' : prefix + desc + ' is visible';
|
|
16050
|
+
}
|
|
16051
|
+
safeSend(JSON.stringify({ action: 'assert', instruction: inst, meta: meta, url: location.href, soft: isSoft }));
|
|
16052
|
+
return;
|
|
16053
|
+
}
|
|
16054
|
+
|
|
16055
|
+
// Normal click
|
|
16056
|
+
if (meta.tag === 'input' && ['submit','button','checkbox','radio','file'].indexOf(meta.type) < 0) return;
|
|
16057
|
+
if (meta.tag === 'textarea' || meta.tag === 'option') return;
|
|
16058
|
+
|
|
16059
|
+
var action = 'click', instruction = 'Click ' + desc;
|
|
16060
|
+
if (meta.type === 'checkbox') { action = meta.checked ? 'check' : 'uncheck'; instruction = (meta.checked ? 'Check' : 'Uncheck') + ' ' + desc; }
|
|
16061
|
+
else if (meta.type === 'radio') { action = 'check'; instruction = 'Select ' + desc; }
|
|
16062
|
+
safeSend(JSON.stringify({ action: action, instruction: instruction, meta: meta, url: location.href }));
|
|
16063
|
+
}, true);
|
|
16064
|
+
|
|
16065
|
+
// Input \u2014 just mark this element as actively being typed into (NO timer, NO intermediate captures)
|
|
16066
|
+
document.addEventListener('input', function(e) {
|
|
16067
|
+
var el = e.target; if (!el || !el.tagName) return;
|
|
16068
|
+
if (el.tagName.toLowerCase() !== 'input' && el.tagName.toLowerCase() !== 'textarea') return;
|
|
16069
|
+
var ty = (el.getAttribute('type') || '').toLowerCase();
|
|
16070
|
+
if (['checkbox','radio','submit','button','file'].indexOf(ty) >= 0) return;
|
|
16071
|
+
// Just mark as active \u2014 we'll capture the FINAL value on blur/click/Enter
|
|
16072
|
+
activeInput = el;
|
|
16073
|
+
pendingInput = { action: 'fill', instruction: '', meta: null, value: '', url: location.href };
|
|
16074
|
+
}, true);
|
|
16075
|
+
|
|
16076
|
+
// Blur \u2014 user left the field \u2192 capture the FINAL value
|
|
16077
|
+
document.addEventListener('blur', function(e) {
|
|
16078
|
+
var el = e.target;
|
|
16079
|
+
if (el && el === activeInput && pendingInput) {
|
|
16080
|
+
flushInput();
|
|
16081
|
+
}
|
|
16082
|
+
}, true);
|
|
16083
|
+
|
|
16084
|
+
// Select change
|
|
16085
|
+
document.addEventListener('change', function(e) {
|
|
16086
|
+
var el = e.target; if (!el || el.tagName.toLowerCase() !== 'select') return;
|
|
16087
|
+
flushInput();
|
|
16088
|
+
var meta = getMeta(el), desc = descEl(meta);
|
|
16089
|
+
var selText = el.options[el.selectedIndex] ? el.options[el.selectedIndex].text : el.value;
|
|
16090
|
+
safeSend(JSON.stringify({ action: 'select', instruction: 'Select "' + selText + '" from ' + desc, meta: meta, value: el.value, url: location.href }));
|
|
16091
|
+
}, true);
|
|
16092
|
+
|
|
16093
|
+
// Keyboard
|
|
16094
|
+
document.addEventListener('keydown', function(e) {
|
|
16095
|
+
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 'u') {
|
|
16096
|
+
e.preventDefault(); e.stopPropagation(); flushInput();
|
|
16097
|
+
var b = document.getElementById('__am-banner');
|
|
16098
|
+
if (b) { b.style.background = 'linear-gradient(90deg,#064e3b,#065f46)'; setTimeout(function() { b.style.background = 'linear-gradient(90deg,#1e1e2e,#2d1b3d)'; }, 800); }
|
|
16099
|
+
var path = location.pathname + location.search + location.hash;
|
|
16100
|
+
safeSend(JSON.stringify({ action: 'assert', instruction: 'Verify the current URL contains "' + (path || '/') + '"', meta: null, value: location.href, url: location.href }));
|
|
16101
|
+
return;
|
|
16102
|
+
}
|
|
16103
|
+
if (e.ctrlKey && e.shiftKey && e.key.toLowerCase() === 't') {
|
|
16104
|
+
e.preventDefault(); e.stopPropagation(); flushInput();
|
|
16105
|
+
var b2 = document.getElementById('__am-banner');
|
|
16106
|
+
if (b2) { b2.style.background = 'linear-gradient(90deg,#064e3b,#065f46)'; setTimeout(function() { b2.style.background = 'linear-gradient(90deg,#1e1e2e,#2d1b3d)'; }, 800); }
|
|
16107
|
+
safeSend(JSON.stringify({ action: 'assert', instruction: 'Verify the page title is "' + document.title + '"', meta: null, value: document.title, url: location.href }));
|
|
16108
|
+
return;
|
|
16109
|
+
}
|
|
16110
|
+
if (e.key === 'Enter') {
|
|
16111
|
+
flushInput();
|
|
16112
|
+
var el = e.target;
|
|
16113
|
+
if (el && el.tagName) {
|
|
16114
|
+
var t = el.tagName.toLowerCase();
|
|
16115
|
+
if (t === 'input' || t === 'textarea') {
|
|
16116
|
+
var meta = getMeta(el), desc = descEl(meta);
|
|
16117
|
+
safeSend(JSON.stringify({ action: 'press', instruction: 'Press Enter on ' + desc, meta: meta, value: 'Enter', url: location.href }));
|
|
16118
|
+
}
|
|
16119
|
+
}
|
|
16120
|
+
} else if (e.key === 'Escape') {
|
|
16121
|
+
flushInput();
|
|
16122
|
+
safeSend(JSON.stringify({ action: 'press', instruction: 'Press Escape', meta: null, value: 'Escape', url: location.href }));
|
|
16123
|
+
} else if (e.key === 'Tab') {
|
|
16124
|
+
flushInput();
|
|
16125
|
+
}
|
|
16126
|
+
}, true);
|
|
16127
|
+
})();
|
|
16128
|
+
`;
|
|
16129
|
+
}
|
|
16130
|
+
async function resolveLocator(page, meta) {
|
|
16131
|
+
const candidates = [];
|
|
16132
|
+
const inIframe = !!meta.frameSelector;
|
|
16133
|
+
const framePrefix = inIframe ? `frameLocator('${esc(meta.frameSelector)}').` : "";
|
|
16134
|
+
const base = inIframe ? page.frameLocator(meta.frameSelector) : page;
|
|
16135
|
+
try {
|
|
16136
|
+
const role = meta.ariaRole || getImplicitRole(meta.tag, meta.type);
|
|
16137
|
+
const bestName = meta.ariaLabel || meta.labelText || meta.innerText || meta.title || "";
|
|
16138
|
+
const headingLevel = getHeadingLevel(meta.tag);
|
|
16139
|
+
if (meta.testId) {
|
|
16140
|
+
candidates.push({
|
|
16141
|
+
name: "testid",
|
|
16142
|
+
locator: base.getByTestId(meta.testId),
|
|
16143
|
+
pw: `${framePrefix}getByTestId('${esc(meta.testId)}')`
|
|
16144
|
+
});
|
|
16145
|
+
}
|
|
16146
|
+
if (role && bestName && bestName.length <= 60) {
|
|
16147
|
+
if (headingLevel) {
|
|
16148
|
+
candidates.push({
|
|
16149
|
+
name: "role+name+level",
|
|
16150
|
+
locator: base.getByRole(role, { name: bestName, exact: true, level: headingLevel }),
|
|
16151
|
+
pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}', level: ${headingLevel} })`
|
|
16152
|
+
});
|
|
16153
|
+
}
|
|
16154
|
+
candidates.push({
|
|
16155
|
+
name: "role+name-exact",
|
|
16156
|
+
locator: base.getByRole(role, { name: bestName, exact: true }),
|
|
16157
|
+
pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}' })`
|
|
16158
|
+
});
|
|
16159
|
+
candidates.push({
|
|
16160
|
+
name: "role+name-fuzzy",
|
|
16161
|
+
locator: base.getByRole(role, { name: bestName, exact: false }),
|
|
16162
|
+
pw: `${framePrefix}getByRole('${role}', { name: '${esc(bestName)}' })`
|
|
16163
|
+
});
|
|
16164
|
+
}
|
|
16165
|
+
if (meta.labelText) {
|
|
16166
|
+
candidates.push({
|
|
16167
|
+
name: "label",
|
|
16168
|
+
locator: base.getByLabel(meta.labelText, { exact: true }),
|
|
16169
|
+
pw: `${framePrefix}getByLabel('${esc(meta.labelText)}')`
|
|
16170
|
+
});
|
|
16171
|
+
candidates.push({
|
|
16172
|
+
name: "label-fuzzy",
|
|
16173
|
+
locator: base.getByLabel(meta.labelText, { exact: false }),
|
|
16174
|
+
pw: `${framePrefix}getByLabel('${esc(meta.labelText)}')`
|
|
16175
|
+
});
|
|
16176
|
+
}
|
|
16177
|
+
if (meta.placeholder) {
|
|
16178
|
+
candidates.push({
|
|
16179
|
+
name: "placeholder",
|
|
16180
|
+
locator: base.getByPlaceholder(meta.placeholder, { exact: true }),
|
|
16181
|
+
pw: `${framePrefix}getByPlaceholder('${esc(meta.placeholder)}')`
|
|
16182
|
+
});
|
|
16183
|
+
}
|
|
16184
|
+
if (meta.innerText && meta.innerText.length <= 60) {
|
|
16185
|
+
candidates.push({
|
|
16186
|
+
name: "text-exact",
|
|
16187
|
+
locator: base.getByText(meta.innerText, { exact: true }),
|
|
16188
|
+
pw: `${framePrefix}getByText('${esc(meta.innerText)}')`
|
|
16189
|
+
});
|
|
16190
|
+
candidates.push({
|
|
16191
|
+
name: "text-fuzzy",
|
|
16192
|
+
locator: base.getByText(meta.innerText, { exact: false }),
|
|
16193
|
+
pw: `${framePrefix}getByText('${esc(meta.innerText)}')`
|
|
16194
|
+
});
|
|
16195
|
+
}
|
|
16196
|
+
if (meta.cssPath) {
|
|
16197
|
+
candidates.push({
|
|
16198
|
+
name: "css",
|
|
16199
|
+
locator: base.locator(meta.cssPath),
|
|
16200
|
+
pw: `${framePrefix}locator('${esc(meta.cssPath)}')`
|
|
16201
|
+
});
|
|
16202
|
+
}
|
|
16203
|
+
for (const c of candidates) {
|
|
16204
|
+
try {
|
|
16205
|
+
const count = await c.locator.count();
|
|
16206
|
+
if (count === 1) {
|
|
16207
|
+
logger44.debug({ strategy: c.name, pw: c.pw, inIframe }, "Locator resolved");
|
|
16208
|
+
return c.pw;
|
|
16209
|
+
}
|
|
16210
|
+
} catch {
|
|
16211
|
+
}
|
|
16212
|
+
}
|
|
16213
|
+
if (meta.cssPath) return `${framePrefix}locator('${esc(meta.cssPath)}')`;
|
|
16214
|
+
if (meta.id) return `${framePrefix}locator('#${esc(meta.id)}')`;
|
|
16215
|
+
return `${framePrefix}locator('${esc(meta.tag)}')`;
|
|
16216
|
+
} catch (err) {
|
|
16217
|
+
logger44.warn({ err, meta: meta.cssPath }, "Locator resolution failed \u2014 using CSS fallback");
|
|
16218
|
+
return meta.cssPath ? `${framePrefix}locator('${esc(meta.cssPath)}')` : `${framePrefix}locator('${esc(meta.tag)}')`;
|
|
16219
|
+
}
|
|
16220
|
+
}
|
|
16221
|
+
function getImplicitRole(tag, type) {
|
|
16222
|
+
if (tag === "button" || tag === "input" && type === "submit") return "button";
|
|
16223
|
+
if (tag === "a") return "link";
|
|
16224
|
+
if (tag === "input") {
|
|
16225
|
+
if (type === "checkbox") return "checkbox";
|
|
16226
|
+
if (type === "radio") return "radio";
|
|
16227
|
+
if (["text", "email", "password", "search", "tel", "url", "number", ""].includes(type)) return "textbox";
|
|
16228
|
+
}
|
|
16229
|
+
if (tag === "textarea") return "textbox";
|
|
16230
|
+
if (tag === "select") return "combobox";
|
|
16231
|
+
if (tag === "img") return "img";
|
|
16232
|
+
if (["h1", "h2", "h3", "h4", "h5", "h6"].includes(tag)) return "heading";
|
|
16233
|
+
return null;
|
|
16234
|
+
}
|
|
16235
|
+
function getHeadingLevel(tag) {
|
|
16236
|
+
const match = tag.match(/^h([1-6])$/);
|
|
16237
|
+
return match ? parseInt(match[1], 10) : null;
|
|
16238
|
+
}
|
|
16239
|
+
function esc(s) {
|
|
16240
|
+
return s.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
|
|
16241
|
+
}
|
|
16242
|
+
var import_playwright2, logger44, TestRecorder;
|
|
16243
|
+
var init_recorder = __esm({
|
|
16244
|
+
"src/engine/recorder.ts"() {
|
|
16245
|
+
"use strict";
|
|
16246
|
+
init_cjs_shims();
|
|
16247
|
+
import_playwright2 = require("playwright");
|
|
16248
|
+
init_logger();
|
|
16249
|
+
logger44 = createChildLogger("recorder");
|
|
16250
|
+
TestRecorder = class {
|
|
16251
|
+
browser = null;
|
|
16252
|
+
context = null;
|
|
16253
|
+
page = null;
|
|
16254
|
+
actions = [];
|
|
16255
|
+
actionCounter = 0;
|
|
16256
|
+
running = false;
|
|
16257
|
+
options;
|
|
16258
|
+
_setLastUserActionTime = null;
|
|
16259
|
+
constructor(options) {
|
|
16260
|
+
this.options = options;
|
|
16261
|
+
}
|
|
16262
|
+
get isRunning() {
|
|
16263
|
+
return this.running;
|
|
16264
|
+
}
|
|
16265
|
+
get recordedActions() {
|
|
16266
|
+
return [...this.actions];
|
|
16267
|
+
}
|
|
16268
|
+
async start() {
|
|
16269
|
+
if (this.running) throw new Error("Recorder is already running");
|
|
16270
|
+
logger44.info({ url: this.options.startUrl }, "Starting recorder");
|
|
16271
|
+
this.actions = [];
|
|
16272
|
+
this.actionCounter = 0;
|
|
16273
|
+
this.running = true;
|
|
16274
|
+
try {
|
|
16275
|
+
this.browser = await import_playwright2.chromium.launch({
|
|
16276
|
+
headless: false,
|
|
16277
|
+
args: ["--start-maximized"]
|
|
16278
|
+
});
|
|
16279
|
+
this.context = await this.browser.newContext({
|
|
16280
|
+
viewport: this.options.viewport || { width: 1280, height: 720 },
|
|
16281
|
+
ignoreHTTPSErrors: true
|
|
16282
|
+
});
|
|
16283
|
+
await this.context.exposeBinding("__amSend", (_source, raw) => {
|
|
16284
|
+
void this.handleRawAction(raw);
|
|
16285
|
+
});
|
|
16286
|
+
this.page = await this.context.newPage();
|
|
16287
|
+
this.page.on("pageerror", (err) => {
|
|
16288
|
+
logger44.warn({ error: err.message }, "Page error during recording");
|
|
16289
|
+
});
|
|
16290
|
+
const captureCode = buildCaptureScript();
|
|
16291
|
+
const injectIntoFrame = async (frame) => {
|
|
16292
|
+
try {
|
|
16293
|
+
await frame.evaluate(captureCode);
|
|
16294
|
+
logger44.debug({ url: frame.url(), name: frame.name() }, "Capture script injected into frame");
|
|
16295
|
+
} catch (err) {
|
|
16296
|
+
logger44.debug({ err, url: frame.url() }, "Could not inject into frame (likely cross-origin)");
|
|
16297
|
+
}
|
|
16298
|
+
};
|
|
16299
|
+
const injectAllFrames = async (p) => {
|
|
16300
|
+
await injectIntoFrame(p.mainFrame());
|
|
16301
|
+
for (const frame of p.frames()) {
|
|
16302
|
+
if (frame !== p.mainFrame()) {
|
|
16303
|
+
await injectIntoFrame(frame);
|
|
16304
|
+
}
|
|
16305
|
+
}
|
|
16306
|
+
};
|
|
16307
|
+
this.page.on("load", () => {
|
|
16308
|
+
if (!this.running || !this.page) return;
|
|
16309
|
+
setTimeout(() => injectAllFrames(this.page), 150);
|
|
16310
|
+
});
|
|
16311
|
+
this.page.on("frameattached", (frame) => {
|
|
16312
|
+
if (!this.running) return;
|
|
16313
|
+
frame.waitForLoadState("load").then(() => {
|
|
16314
|
+
injectIntoFrame(frame);
|
|
16315
|
+
}).catch(() => {
|
|
16316
|
+
});
|
|
16317
|
+
});
|
|
16318
|
+
this.page.on("framenavigated", (frame) => {
|
|
16319
|
+
if (!this.running || !this.page) return;
|
|
16320
|
+
if (frame === this.page.mainFrame()) return;
|
|
16321
|
+
setTimeout(() => injectIntoFrame(frame), 150);
|
|
16322
|
+
});
|
|
16323
|
+
let lastUrl = "";
|
|
16324
|
+
let lastUserActionTime = 0;
|
|
16325
|
+
const REDIRECT_WINDOW_MS = 3e3;
|
|
16326
|
+
this.page.on("framenavigated", (frame) => {
|
|
16327
|
+
if (!this.running || !this.page) return;
|
|
16328
|
+
if (frame !== this.page.mainFrame()) return;
|
|
16329
|
+
const url = frame.url();
|
|
16330
|
+
if (url === lastUrl || url === "about:blank") return;
|
|
16331
|
+
lastUrl = url;
|
|
16332
|
+
const isInitialNav = this.actionCounter === 0;
|
|
16333
|
+
if (!isInitialNav && lastUserActionTime > 0) {
|
|
16334
|
+
const elapsed = Date.now() - lastUserActionTime;
|
|
16335
|
+
if (elapsed < REDIRECT_WINDOW_MS) {
|
|
16336
|
+
logger44.debug({ url, elapsed }, "Skipping redirect navigation after user action");
|
|
16337
|
+
return;
|
|
16338
|
+
}
|
|
16339
|
+
}
|
|
16340
|
+
const action = {
|
|
16341
|
+
id: `rec-${++this.actionCounter}`,
|
|
16342
|
+
order: this.actionCounter,
|
|
16343
|
+
action: "navigate",
|
|
16344
|
+
instruction: isInitialNav ? `Go to ${url}` : `Navigate to ${url}`,
|
|
16345
|
+
locator: "",
|
|
16346
|
+
url,
|
|
16347
|
+
timestamp: Date.now()
|
|
16348
|
+
};
|
|
16349
|
+
this.actions.push(action);
|
|
16350
|
+
this.options.onAction(action);
|
|
16351
|
+
logger44.debug({ action: action.instruction }, "Recorded navigation");
|
|
16352
|
+
});
|
|
16353
|
+
this._setLastUserActionTime = (t) => {
|
|
16354
|
+
lastUserActionTime = t;
|
|
16355
|
+
};
|
|
16356
|
+
this.page.on("close", () => {
|
|
16357
|
+
if (this.running) {
|
|
16358
|
+
logger44.info("Browser closed by user \u2014 stopping recorder");
|
|
16359
|
+
this.running = false;
|
|
16360
|
+
this.options.onStop?.(this.actions);
|
|
16361
|
+
}
|
|
16362
|
+
});
|
|
16363
|
+
await this.page.goto(this.options.startUrl, { waitUntil: "load", timeout: 3e4 });
|
|
16364
|
+
await injectAllFrames(this.page);
|
|
16365
|
+
logger44.info("Recorder started \u2014 browser is open for interaction");
|
|
16366
|
+
} catch (err) {
|
|
16367
|
+
logger44.error({ err }, "Failed to start recorder");
|
|
16368
|
+
await this.cleanup();
|
|
16369
|
+
throw err;
|
|
16370
|
+
}
|
|
16371
|
+
}
|
|
16372
|
+
async stop() {
|
|
16373
|
+
if (!this.running) return this.actions;
|
|
16374
|
+
logger44.info({ actionCount: this.actions.length }, "Stopping recorder");
|
|
16375
|
+
this.running = false;
|
|
16376
|
+
const result = [...this.actions];
|
|
16377
|
+
await this.cleanup();
|
|
16378
|
+
this.options.onStop?.(result);
|
|
16379
|
+
return result;
|
|
16380
|
+
}
|
|
16381
|
+
/**
|
|
16382
|
+
* Handle a raw action from the browser capture script.
|
|
16383
|
+
* Resolves the best Playwright locator using the live page's DOM + Accessibility tree.
|
|
16384
|
+
*/
|
|
16385
|
+
async handleRawAction(raw) {
|
|
16386
|
+
if (!this.running || !this.page) return;
|
|
16387
|
+
try {
|
|
16388
|
+
const parsed = JSON.parse(raw);
|
|
16389
|
+
const meta = parsed.meta;
|
|
16390
|
+
let locator = "";
|
|
16391
|
+
if (meta && this.page) {
|
|
16392
|
+
try {
|
|
16393
|
+
locator = await resolveLocator(this.page, meta);
|
|
16394
|
+
} catch (err) {
|
|
16395
|
+
logger44.warn({ err }, "Locator resolution failed \u2014 using CSS fallback");
|
|
16396
|
+
locator = meta.cssPath ? `locator('${esc(meta.cssPath)}')` : "";
|
|
16397
|
+
}
|
|
16398
|
+
}
|
|
16399
|
+
const userActions = ["click", "fill", "check", "uncheck", "select", "press"];
|
|
16400
|
+
if (userActions.includes(parsed.action) && this._setLastUserActionTime) {
|
|
16401
|
+
this._setLastUserActionTime(Date.now());
|
|
16402
|
+
}
|
|
16403
|
+
const action = {
|
|
16404
|
+
id: `rec-${++this.actionCounter}`,
|
|
16405
|
+
order: this.actionCounter,
|
|
16406
|
+
action: parsed.action,
|
|
16407
|
+
instruction: parsed.instruction,
|
|
16408
|
+
locator,
|
|
16409
|
+
value: parsed.value,
|
|
16410
|
+
url: parsed.url,
|
|
16411
|
+
timestamp: Date.now(),
|
|
16412
|
+
...parsed.soft ? { soft: true } : {},
|
|
16413
|
+
...meta?.frameSelector ? { frameSelector: meta.frameSelector } : {}
|
|
16414
|
+
};
|
|
16415
|
+
this.actions.push(action);
|
|
16416
|
+
this.options.onAction(action);
|
|
16417
|
+
logger44.debug({ action: action.instruction, locator, strategy: "resolved" }, "Recorded action");
|
|
16418
|
+
} catch (err) {
|
|
16419
|
+
logger44.warn({ err, raw }, "Failed to parse recorded action");
|
|
16420
|
+
}
|
|
16421
|
+
}
|
|
16422
|
+
async cleanup() {
|
|
16423
|
+
try {
|
|
16424
|
+
if (this.context) await this.context.close().catch(() => {
|
|
16425
|
+
});
|
|
16426
|
+
if (this.browser) await this.browser.close().catch(() => {
|
|
16427
|
+
});
|
|
16428
|
+
} catch {
|
|
16429
|
+
}
|
|
16430
|
+
this.browser = null;
|
|
16431
|
+
this.context = null;
|
|
16432
|
+
this.page = null;
|
|
16433
|
+
}
|
|
16434
|
+
};
|
|
16435
|
+
}
|
|
16436
|
+
});
|
|
16437
|
+
|
|
16438
|
+
// src/server/routes/recorder.ts
|
|
16439
|
+
async function recorderRoutes(fastify) {
|
|
16440
|
+
const rootDir = fastify.rootDir;
|
|
16441
|
+
const wsManager = fastify.wsManager;
|
|
16442
|
+
fastify.post("/recorder/start", async (req, reply) => {
|
|
16443
|
+
try {
|
|
16444
|
+
if (activeRecorder?.isRunning) {
|
|
16445
|
+
return reply.status(409).send({ error: "A recording session is already active. Stop it first." });
|
|
16446
|
+
}
|
|
16447
|
+
const body = StartBody.parse(req.body || {});
|
|
16448
|
+
const config = await readConfig(rootDir);
|
|
16449
|
+
const startUrl = body.url || config.baseUrl || "https://www.google.com";
|
|
16450
|
+
activeRecorder = new TestRecorder({
|
|
16451
|
+
startUrl,
|
|
16452
|
+
viewport: body.viewport,
|
|
16453
|
+
onAction: (action) => {
|
|
16454
|
+
wsManager.broadcast("recorder:action", action);
|
|
16455
|
+
},
|
|
16456
|
+
onStop: (actions) => {
|
|
16457
|
+
wsManager.broadcast("recorder:stopped", { actionCount: actions.length });
|
|
16458
|
+
logger45.info({ actionCount: actions.length }, "Recording stopped (browser closed)");
|
|
16459
|
+
}
|
|
16460
|
+
});
|
|
16461
|
+
await activeRecorder.start();
|
|
16462
|
+
return reply.send({
|
|
16463
|
+
status: "recording",
|
|
16464
|
+
url: startUrl,
|
|
16465
|
+
message: "Browser launched \u2014 interact with your app. Actions are streamed in real time."
|
|
16466
|
+
});
|
|
16467
|
+
} catch (err) {
|
|
16468
|
+
activeRecorder = null;
|
|
16469
|
+
return sendError(reply, 500, "Failed to start recorder", err);
|
|
16470
|
+
}
|
|
16471
|
+
});
|
|
16472
|
+
fastify.post("/recorder/stop", async (_req, reply) => {
|
|
16473
|
+
try {
|
|
16474
|
+
if (!activeRecorder?.isRunning) {
|
|
16475
|
+
return reply.status(404).send({ error: "No active recording session." });
|
|
16476
|
+
}
|
|
16477
|
+
const actions = await activeRecorder.stop();
|
|
16478
|
+
activeRecorder = null;
|
|
16479
|
+
wsManager.broadcast("recorder:stopped", { actionCount: actions.length });
|
|
16480
|
+
return reply.send({
|
|
16481
|
+
status: "stopped",
|
|
16482
|
+
actions,
|
|
16483
|
+
actionCount: actions.length
|
|
16484
|
+
});
|
|
16485
|
+
} catch (err) {
|
|
16486
|
+
activeRecorder = null;
|
|
16487
|
+
return sendError(reply, 500, "Failed to stop recorder", err);
|
|
16488
|
+
}
|
|
16489
|
+
});
|
|
16490
|
+
fastify.get("/recorder/status", async (_req, reply) => {
|
|
16491
|
+
const isRecording = activeRecorder?.isRunning ?? false;
|
|
16492
|
+
const actionCount = activeRecorder?.recordedActions.length ?? 0;
|
|
16493
|
+
return reply.send({
|
|
16494
|
+
recording: isRecording,
|
|
16495
|
+
actionCount,
|
|
16496
|
+
actions: isRecording ? activeRecorder.recordedActions : []
|
|
16497
|
+
});
|
|
16498
|
+
});
|
|
16499
|
+
}
|
|
16500
|
+
var import_zod22, logger45, StartBody, activeRecorder;
|
|
16501
|
+
var init_recorder2 = __esm({
|
|
16502
|
+
"src/server/routes/recorder.ts"() {
|
|
16503
|
+
"use strict";
|
|
16504
|
+
init_cjs_shims();
|
|
16505
|
+
import_zod22 = require("zod");
|
|
16506
|
+
init_recorder();
|
|
16507
|
+
init_config_store();
|
|
16508
|
+
init_utils2();
|
|
16509
|
+
init_logger();
|
|
16510
|
+
logger45 = createChildLogger("recorder-routes");
|
|
16511
|
+
StartBody = import_zod22.z.object({
|
|
16512
|
+
url: import_zod22.z.string().url().optional(),
|
|
16513
|
+
viewport: import_zod22.z.object({
|
|
16514
|
+
width: import_zod22.z.number().int().positive(),
|
|
16515
|
+
height: import_zod22.z.number().int().positive()
|
|
16516
|
+
}).optional()
|
|
16517
|
+
});
|
|
16518
|
+
activeRecorder = null;
|
|
16519
|
+
}
|
|
16520
|
+
});
|
|
16521
|
+
|
|
14879
16522
|
// src/server/index.ts
|
|
14880
16523
|
var server_exports = {};
|
|
14881
16524
|
__export(server_exports, {
|
|
@@ -14917,17 +16560,18 @@ async function startServer(options) {
|
|
|
14917
16560
|
await fastify.register(templatesRoutes, { prefix: "/api" });
|
|
14918
16561
|
await fastify.register(flakinessRoutes, { prefix: "/api" });
|
|
14919
16562
|
await fastify.register(stepLibraryRoutes, { prefix: "/api" });
|
|
16563
|
+
await fastify.register(recorderRoutes, { prefix: "/api" });
|
|
14920
16564
|
await registerStatic(fastify, rootDir);
|
|
14921
16565
|
await fastify.listen({ port, host: "127.0.0.1" });
|
|
14922
|
-
|
|
16566
|
+
logger46.info({ port, rootDir }, `Assuremind Studio listening on http://127.0.0.1:${port}`);
|
|
14923
16567
|
return {
|
|
14924
16568
|
async close() {
|
|
14925
16569
|
await fastify.close();
|
|
14926
|
-
|
|
16570
|
+
logger46.info("Server closed");
|
|
14927
16571
|
}
|
|
14928
16572
|
};
|
|
14929
16573
|
}
|
|
14930
|
-
var import_fastify, import_cors, import_websocket,
|
|
16574
|
+
var import_fastify, import_cors, import_websocket, logger46;
|
|
14931
16575
|
var init_server = __esm({
|
|
14932
16576
|
"src/server/index.ts"() {
|
|
14933
16577
|
"use strict";
|
|
@@ -14957,8 +16601,9 @@ var init_server = __esm({
|
|
|
14957
16601
|
init_templates();
|
|
14958
16602
|
init_flakiness();
|
|
14959
16603
|
init_step_library();
|
|
16604
|
+
init_recorder2();
|
|
14960
16605
|
init_logger();
|
|
14961
|
-
|
|
16606
|
+
logger46 = createChildLogger("server");
|
|
14962
16607
|
}
|
|
14963
16608
|
});
|
|
14964
16609
|
|
|
@@ -15261,14 +16906,14 @@ async function loadStoryText(options) {
|
|
|
15261
16906
|
return options.story;
|
|
15262
16907
|
}
|
|
15263
16908
|
if (options.storyFile) {
|
|
15264
|
-
const filePath =
|
|
15265
|
-
if (!await
|
|
16909
|
+
const filePath = import_path36.default.resolve(options.storyFile);
|
|
16910
|
+
if (!await import_fs_extra27.default.pathExists(filePath)) {
|
|
15266
16911
|
throw new ConfigError(
|
|
15267
16912
|
`Story file not found: "${filePath}"`,
|
|
15268
16913
|
"STORY_FILE_NOT_FOUND"
|
|
15269
16914
|
);
|
|
15270
16915
|
}
|
|
15271
|
-
return
|
|
16916
|
+
return import_fs_extra27.default.readFile(filePath, "utf8");
|
|
15272
16917
|
}
|
|
15273
16918
|
throw new ConfigError(
|
|
15274
16919
|
'Provide a story with --story "<text>" or --story-file <path>',
|
|
@@ -15291,7 +16936,7 @@ async function runGenerate(options) {
|
|
|
15291
16936
|
process.exit(1);
|
|
15292
16937
|
}
|
|
15293
16938
|
const rootDir = process.cwd();
|
|
15294
|
-
const outputDir = options.output ?
|
|
16939
|
+
const outputDir = options.output ? import_path36.default.resolve(options.output) : import_path36.default.join(rootDir, "tests");
|
|
15295
16940
|
printInfo(`Story: ${colors.dim(`${storyText.slice(0, 80)}${storyText.length > 80 ? "\u2026" : ""}`)}`);
|
|
15296
16941
|
if (options.suite) {
|
|
15297
16942
|
printInfo(`Suite name: ${options.suite}`);
|
|
@@ -15323,8 +16968,8 @@ async function runGenerate(options) {
|
|
|
15323
16968
|
suiteName: options.suite
|
|
15324
16969
|
});
|
|
15325
16970
|
genSpinner.succeed("Suite generated");
|
|
15326
|
-
const suiteOutputDir = options.output ??
|
|
15327
|
-
printSuccess(`Test suite created in: ${
|
|
16971
|
+
const suiteOutputDir = options.output ?? import_path36.default.join(rootDir, "tests");
|
|
16972
|
+
printSuccess(`Test suite created in: ${import_path36.default.relative(rootDir, suiteOutputDir)}`);
|
|
15328
16973
|
printInfo("Open Studio or use the Test Editor to review and generate Playwright code:");
|
|
15329
16974
|
process.stdout.write(` ${colors.accent("npx assuremind studio")}
|
|
15330
16975
|
|
|
@@ -15335,13 +16980,13 @@ async function runGenerate(options) {
|
|
|
15335
16980
|
process.exit(1);
|
|
15336
16981
|
}
|
|
15337
16982
|
}
|
|
15338
|
-
var
|
|
16983
|
+
var import_path36, import_fs_extra27;
|
|
15339
16984
|
var init_generate = __esm({
|
|
15340
16985
|
"src/cli/generate.ts"() {
|
|
15341
16986
|
"use strict";
|
|
15342
16987
|
init_cjs_shims();
|
|
15343
|
-
|
|
15344
|
-
|
|
16988
|
+
import_path36 = __toESM(require("path"));
|
|
16989
|
+
import_fs_extra27 = __toESM(require("fs-extra"));
|
|
15345
16990
|
init_ui();
|
|
15346
16991
|
init_env();
|
|
15347
16992
|
init_errors();
|
|
@@ -15355,7 +17000,7 @@ __export(validate_exports, {
|
|
|
15355
17000
|
});
|
|
15356
17001
|
async function checkConfig(rootDir, result) {
|
|
15357
17002
|
printInfo("Checking configuration\u2026");
|
|
15358
|
-
if (!await
|
|
17003
|
+
if (!await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.json")) && !await import_fs_extra28.default.pathExists(import_path37.default.join(rootDir, "autotest.config.ts"))) {
|
|
15359
17004
|
printError(' autotest.config.ts not found \u2014 run "npx assuremind init" to create it');
|
|
15360
17005
|
result.errors++;
|
|
15361
17006
|
return;
|
|
@@ -15371,7 +17016,7 @@ async function checkConfig(rootDir, result) {
|
|
|
15371
17016
|
}
|
|
15372
17017
|
async function checkEnv(result) {
|
|
15373
17018
|
printInfo("Checking environment\u2026");
|
|
15374
|
-
if (!await
|
|
17019
|
+
if (!await import_fs_extra28.default.pathExists(".env")) {
|
|
15375
17020
|
printWarn(" .env file not found \u2014 copy .env.example to .env and fill in your API key");
|
|
15376
17021
|
result.warnings++;
|
|
15377
17022
|
return;
|
|
@@ -15387,8 +17032,8 @@ async function checkEnv(result) {
|
|
|
15387
17032
|
}
|
|
15388
17033
|
async function checkTests(rootDir, result) {
|
|
15389
17034
|
printInfo("Scanning test files\u2026");
|
|
15390
|
-
const testsDir =
|
|
15391
|
-
if (!await
|
|
17035
|
+
const testsDir = import_path37.default.join(rootDir, "tests");
|
|
17036
|
+
if (!await import_fs_extra28.default.pathExists(testsDir)) {
|
|
15392
17037
|
printWarn(" tests/ directory not found \u2014 no tests to validate");
|
|
15393
17038
|
result.warnings++;
|
|
15394
17039
|
return;
|
|
@@ -15408,7 +17053,7 @@ async function checkTests(rootDir, result) {
|
|
|
15408
17053
|
try {
|
|
15409
17054
|
await readCase(casePath);
|
|
15410
17055
|
} catch {
|
|
15411
|
-
printError(` Invalid: ${
|
|
17056
|
+
printError(` Invalid: ${import_path37.default.relative(rootDir, casePath)}`);
|
|
15412
17057
|
invalidCases++;
|
|
15413
17058
|
result.errors++;
|
|
15414
17059
|
}
|
|
@@ -15436,7 +17081,7 @@ async function checkVariables(rootDir, result) {
|
|
|
15436
17081
|
try {
|
|
15437
17082
|
await readVariables(file);
|
|
15438
17083
|
} catch {
|
|
15439
|
-
printError(` Invalid: ${
|
|
17084
|
+
printError(` Invalid: ${import_path37.default.relative(rootDir, file)}`);
|
|
15440
17085
|
invalid++;
|
|
15441
17086
|
result.errors++;
|
|
15442
17087
|
}
|
|
@@ -15473,13 +17118,13 @@ async function runValidate() {
|
|
|
15473
17118
|
process.exit(1);
|
|
15474
17119
|
}
|
|
15475
17120
|
}
|
|
15476
|
-
var
|
|
17121
|
+
var import_path37, import_fs_extra28;
|
|
15477
17122
|
var init_validate = __esm({
|
|
15478
17123
|
"src/cli/validate.ts"() {
|
|
15479
17124
|
"use strict";
|
|
15480
17125
|
init_cjs_shims();
|
|
15481
|
-
|
|
15482
|
-
|
|
17126
|
+
import_path37 = __toESM(require("path"));
|
|
17127
|
+
import_fs_extra28 = __toESM(require("fs-extra"));
|
|
15483
17128
|
init_ui();
|
|
15484
17129
|
init_config_store();
|
|
15485
17130
|
init_suite_store();
|
|
@@ -15544,7 +17189,7 @@ async function checkPlaywright() {
|
|
|
15544
17189
|
}
|
|
15545
17190
|
}
|
|
15546
17191
|
async function checkEnvFile() {
|
|
15547
|
-
if (!await
|
|
17192
|
+
if (!await import_fs_extra29.default.pathExists(".env")) {
|
|
15548
17193
|
return check(
|
|
15549
17194
|
".env file",
|
|
15550
17195
|
false,
|
|
@@ -15582,7 +17227,7 @@ async function checkConfig2() {
|
|
|
15582
17227
|
}
|
|
15583
17228
|
}
|
|
15584
17229
|
async function checkPackageJson() {
|
|
15585
|
-
if (!await
|
|
17230
|
+
if (!await import_fs_extra29.default.pathExists("package.json")) {
|
|
15586
17231
|
return check(
|
|
15587
17232
|
"package.json",
|
|
15588
17233
|
false,
|
|
@@ -15590,7 +17235,7 @@ async function checkPackageJson() {
|
|
|
15590
17235
|
);
|
|
15591
17236
|
}
|
|
15592
17237
|
try {
|
|
15593
|
-
const pkg2 = await
|
|
17238
|
+
const pkg2 = await import_fs_extra29.default.readJson("package.json");
|
|
15594
17239
|
return check("package.json", true, `${pkg2.name ?? "unnamed"} v${pkg2.version ?? "?"}`);
|
|
15595
17240
|
} catch {
|
|
15596
17241
|
return check("package.json", false, "Exists but is not valid JSON");
|
|
@@ -15670,13 +17315,13 @@ async function runDoctor() {
|
|
|
15670
17315
|
process.exit(1);
|
|
15671
17316
|
}
|
|
15672
17317
|
}
|
|
15673
|
-
var import_child_process6,
|
|
17318
|
+
var import_child_process6, import_fs_extra29;
|
|
15674
17319
|
var init_doctor = __esm({
|
|
15675
17320
|
"src/cli/doctor.ts"() {
|
|
15676
17321
|
"use strict";
|
|
15677
17322
|
init_cjs_shims();
|
|
15678
17323
|
import_child_process6 = require("child_process");
|
|
15679
|
-
|
|
17324
|
+
import_fs_extra29 = __toESM(require("fs-extra"));
|
|
15680
17325
|
init_ui();
|
|
15681
17326
|
init_env();
|
|
15682
17327
|
init_config_store();
|
|
@@ -15703,7 +17348,7 @@ async function findCaseFilePath(testsDir, caseId) {
|
|
|
15703
17348
|
return null;
|
|
15704
17349
|
}
|
|
15705
17350
|
async function applyHealToFile(rootDir, event) {
|
|
15706
|
-
const testsDir =
|
|
17351
|
+
const testsDir = import_path38.default.join(rootDir, "tests");
|
|
15707
17352
|
const casePath = await findCaseFilePath(testsDir, event.caseId);
|
|
15708
17353
|
if (!casePath) {
|
|
15709
17354
|
throw new Error(
|
|
@@ -15725,7 +17370,7 @@ async function applyHealToFile(rootDir, event) {
|
|
|
15725
17370
|
testCase.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
15726
17371
|
await writeCase(casePath, testCase);
|
|
15727
17372
|
printSuccess(
|
|
15728
|
-
` Updated: ${
|
|
17373
|
+
` Updated: ${import_path38.default.relative(rootDir, casePath)} \u203A step "${testCase.steps[stepIdx].instruction}"`
|
|
15729
17374
|
);
|
|
15730
17375
|
}
|
|
15731
17376
|
function ask(rl, question) {
|
|
@@ -15867,21 +17512,21 @@ Review the changes and merge if the fixes look correct.
|
|
|
15867
17512
|
}
|
|
15868
17513
|
async function runApplyHealing(options) {
|
|
15869
17514
|
const rootDir = process.cwd();
|
|
15870
|
-
(0, import_dotenv2.config)({ path:
|
|
17515
|
+
(0, import_dotenv2.config)({ path: import_path38.default.join(rootDir, ".env") });
|
|
15871
17516
|
process.stdout.write("\n");
|
|
15872
17517
|
let events;
|
|
15873
17518
|
if (options.from) {
|
|
15874
|
-
const reportPath =
|
|
15875
|
-
if (!await
|
|
17519
|
+
const reportPath = import_path38.default.resolve(options.from);
|
|
17520
|
+
if (!await import_fs_extra30.default.pathExists(reportPath)) {
|
|
15876
17521
|
printError(`Healing report not found: ${reportPath}`);
|
|
15877
17522
|
process.exit(1);
|
|
15878
17523
|
}
|
|
15879
17524
|
try {
|
|
15880
|
-
const rawRunId =
|
|
17525
|
+
const rawRunId = import_path38.default.basename(reportPath, ".json").replace("healing-report-", "");
|
|
15881
17526
|
const report = await readHealingReport(rootDir, rawRunId);
|
|
15882
17527
|
events = report.events.filter((e) => e.status === "pending");
|
|
15883
17528
|
} catch {
|
|
15884
|
-
const raw = await
|
|
17529
|
+
const raw = await import_fs_extra30.default.readJson(reportPath);
|
|
15885
17530
|
events = Array.isArray(raw) ? raw.filter((e) => e.status === "pending") : (raw.events ?? []).filter((e) => e.status === "pending");
|
|
15886
17531
|
}
|
|
15887
17532
|
} else {
|
|
@@ -15910,13 +17555,13 @@ async function runApplyHealing(options) {
|
|
|
15910
17555
|
}
|
|
15911
17556
|
}
|
|
15912
17557
|
}
|
|
15913
|
-
var
|
|
17558
|
+
var import_path38, import_fs_extra30, import_readline, import_dotenv2;
|
|
15914
17559
|
var init_apply_healing = __esm({
|
|
15915
17560
|
"src/cli/apply-healing.ts"() {
|
|
15916
17561
|
"use strict";
|
|
15917
17562
|
init_cjs_shims();
|
|
15918
|
-
|
|
15919
|
-
|
|
17563
|
+
import_path38 = __toESM(require("path"));
|
|
17564
|
+
import_fs_extra30 = __toESM(require("fs-extra"));
|
|
15920
17565
|
import_readline = __toESM(require("readline"));
|
|
15921
17566
|
import_dotenv2 = require("dotenv");
|
|
15922
17567
|
init_healing_store();
|