@yasserkhanorg/e2e-agents 0.4.0 → 0.5.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/dist/cli.js +30 -1
- package/dist/esm/cli.js +30 -1
- package/dist/esm/index.js +2 -0
- package/dist/esm/pipeline/orchestrator.js +25 -5
- package/dist/esm/pipeline/stage4_heal.js +145 -0
- package/dist/esm/prompts/heal.js +69 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/pipeline/orchestrator.d.ts +6 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +25 -5
- package/dist/pipeline/stage4_heal.d.ts +56 -0
- package/dist/pipeline/stage4_heal.d.ts.map +1 -0
- package/dist/pipeline/stage4_heal.js +151 -0
- package/dist/prompts/heal.d.ts +19 -0
- package/dist/prompts/heal.d.ts.map +1 -0
- package/dist/prompts/heal.js +73 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -72,7 +72,7 @@ function printUsage() {
|
|
|
72
72
|
' e2e-ai-agents feedback --path <app-root> --feedback-input <json>',
|
|
73
73
|
' e2e-ai-agents traceability-capture --path <app-root> --traceability-report <json>',
|
|
74
74
|
' e2e-ai-agents traceability-ingest --path <app-root> --traceability-input <json>',
|
|
75
|
-
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>]',
|
|
75
|
+
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>] [--heal] [--heal-report <json>]',
|
|
76
76
|
' e2e-ai-agents llm-health',
|
|
77
77
|
'',
|
|
78
78
|
'Options:',
|
|
@@ -493,6 +493,15 @@ function parseArgs(argv) {
|
|
|
493
493
|
i += 1;
|
|
494
494
|
continue;
|
|
495
495
|
}
|
|
496
|
+
if (arg === '--heal') {
|
|
497
|
+
parsed.analyzeHeal = true;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
if (arg === '--heal-report' && next) {
|
|
501
|
+
parsed.analyzeHealReport = next;
|
|
502
|
+
i += 1;
|
|
503
|
+
continue;
|
|
504
|
+
}
|
|
496
505
|
}
|
|
497
506
|
return parsed;
|
|
498
507
|
}
|
|
@@ -528,6 +537,9 @@ async function main() {
|
|
|
528
537
|
if (args.analyzeGenerate) {
|
|
529
538
|
analyzeStages.push('generation');
|
|
530
539
|
}
|
|
540
|
+
if (args.analyzeHeal || args.analyzeHealReport) {
|
|
541
|
+
analyzeStages.push('heal');
|
|
542
|
+
}
|
|
531
543
|
const result = await (0, orchestrator_js_1.runPipeline)({
|
|
532
544
|
appPath: config.path,
|
|
533
545
|
testsRoot,
|
|
@@ -541,6 +553,17 @@ async function main() {
|
|
|
541
553
|
dryRun: args.dryRun,
|
|
542
554
|
}
|
|
543
555
|
: undefined,
|
|
556
|
+
heal: (args.analyzeHeal || args.analyzeHealReport)
|
|
557
|
+
? {
|
|
558
|
+
mcp: args.pipelineMcp ?? true,
|
|
559
|
+
mcpAllowFallback: args.pipelineMcpAllowFallback ?? false,
|
|
560
|
+
mcpOnly: args.pipelineMcpOnly ?? false,
|
|
561
|
+
mcpCommandTimeoutMs: args.pipelineMcpTimeoutMs,
|
|
562
|
+
mcpRetries: args.pipelineMcpRetries ?? 1,
|
|
563
|
+
dryRun: args.dryRun,
|
|
564
|
+
}
|
|
565
|
+
: undefined,
|
|
566
|
+
playwrightReportPath: args.analyzeHealReport,
|
|
544
567
|
});
|
|
545
568
|
// eslint-disable-next-line no-console
|
|
546
569
|
console.log(`Analyze report: ${result.reportPath}`);
|
|
@@ -563,6 +586,12 @@ async function main() {
|
|
|
563
586
|
console.log(` ${g.mode}: ${g.specPath}`);
|
|
564
587
|
}
|
|
565
588
|
}
|
|
589
|
+
if (result.healResult) {
|
|
590
|
+
const healed = result.healResult.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
591
|
+
const healFailed = result.healResult.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
592
|
+
// eslint-disable-next-line no-console
|
|
593
|
+
console.log(`Analyze heal targets: ${result.healResult.targets.length} (healed=${healed}, failed=${healFailed})`);
|
|
594
|
+
}
|
|
566
595
|
if (result.warnings.length > 0) {
|
|
567
596
|
// eslint-disable-next-line no-console
|
|
568
597
|
console.log(`Analyze warnings: ${result.warnings.join(' | ')}`);
|
package/dist/esm/cli.js
CHANGED
|
@@ -70,7 +70,7 @@ function printUsage() {
|
|
|
70
70
|
' e2e-ai-agents feedback --path <app-root> --feedback-input <json>',
|
|
71
71
|
' e2e-ai-agents traceability-capture --path <app-root> --traceability-report <json>',
|
|
72
72
|
' e2e-ai-agents traceability-ingest --path <app-root> --traceability-input <json>',
|
|
73
|
-
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>]',
|
|
73
|
+
' e2e-ai-agents analyze --path <app-root> [--tests-root <path>] [--since <ref>] [--generate] [--generate-output <dir>] [--heal] [--heal-report <json>]',
|
|
74
74
|
' e2e-ai-agents llm-health',
|
|
75
75
|
'',
|
|
76
76
|
'Options:',
|
|
@@ -491,6 +491,15 @@ function parseArgs(argv) {
|
|
|
491
491
|
i += 1;
|
|
492
492
|
continue;
|
|
493
493
|
}
|
|
494
|
+
if (arg === '--heal') {
|
|
495
|
+
parsed.analyzeHeal = true;
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
if (arg === '--heal-report' && next) {
|
|
499
|
+
parsed.analyzeHealReport = next;
|
|
500
|
+
i += 1;
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
494
503
|
}
|
|
495
504
|
return parsed;
|
|
496
505
|
}
|
|
@@ -526,6 +535,9 @@ async function main() {
|
|
|
526
535
|
if (args.analyzeGenerate) {
|
|
527
536
|
analyzeStages.push('generation');
|
|
528
537
|
}
|
|
538
|
+
if (args.analyzeHeal || args.analyzeHealReport) {
|
|
539
|
+
analyzeStages.push('heal');
|
|
540
|
+
}
|
|
529
541
|
const result = await runPipeline({
|
|
530
542
|
appPath: config.path,
|
|
531
543
|
testsRoot,
|
|
@@ -539,6 +551,17 @@ async function main() {
|
|
|
539
551
|
dryRun: args.dryRun,
|
|
540
552
|
}
|
|
541
553
|
: undefined,
|
|
554
|
+
heal: (args.analyzeHeal || args.analyzeHealReport)
|
|
555
|
+
? {
|
|
556
|
+
mcp: args.pipelineMcp ?? true,
|
|
557
|
+
mcpAllowFallback: args.pipelineMcpAllowFallback ?? false,
|
|
558
|
+
mcpOnly: args.pipelineMcpOnly ?? false,
|
|
559
|
+
mcpCommandTimeoutMs: args.pipelineMcpTimeoutMs,
|
|
560
|
+
mcpRetries: args.pipelineMcpRetries ?? 1,
|
|
561
|
+
dryRun: args.dryRun,
|
|
562
|
+
}
|
|
563
|
+
: undefined,
|
|
564
|
+
playwrightReportPath: args.analyzeHealReport,
|
|
542
565
|
});
|
|
543
566
|
// eslint-disable-next-line no-console
|
|
544
567
|
console.log(`Analyze report: ${result.reportPath}`);
|
|
@@ -561,6 +584,12 @@ async function main() {
|
|
|
561
584
|
console.log(` ${g.mode}: ${g.specPath}`);
|
|
562
585
|
}
|
|
563
586
|
}
|
|
587
|
+
if (result.healResult) {
|
|
588
|
+
const healed = result.healResult.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
589
|
+
const healFailed = result.healResult.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
590
|
+
// eslint-disable-next-line no-console
|
|
591
|
+
console.log(`Analyze heal targets: ${result.healResult.targets.length} (healed=${healed}, failed=${healFailed})`);
|
|
592
|
+
}
|
|
564
593
|
if (result.warnings.length > 0) {
|
|
565
594
|
// eslint-disable-next-line no-console
|
|
566
595
|
console.log(`Analyze warnings: ${result.warnings.join(' | ')}`);
|
package/dist/esm/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export { captureTraceabilityInput } from './agent/traceability_capture.js';
|
|
|
18
18
|
export { runPipeline } from './pipeline/orchestrator.js';
|
|
19
19
|
export { runGenerationStage } from './pipeline/stage3_generation.js';
|
|
20
20
|
export { buildGenerationPrompt, parseGenerationResponse, detectHallucinatedMethods } from './prompts/generation.js';
|
|
21
|
+
export { runHealStage, healFromReport, resolveHealTargets, renderHealMarkdown } from './pipeline/stage4_heal.js';
|
|
22
|
+
export { buildHealPrompt, buildQualityFixPrompt } from './prompts/heal.js';
|
|
21
23
|
// Knowledge modules
|
|
22
24
|
export { loadRouteFamilyManifest, bindFilesToFamilies } from './knowledge/route_families.js';
|
|
23
25
|
export { buildApiSurface, loadOrBuildApiSurface } from './knowledge/api_surface.js';
|
|
@@ -7,6 +7,7 @@ import { preprocess } from './stage0_preprocess.js';
|
|
|
7
7
|
import { runImpactStage } from './stage1_impact.js';
|
|
8
8
|
import { runCoverageStage } from './stage2_coverage.js';
|
|
9
9
|
import { runGenerationStage } from './stage3_generation.js';
|
|
10
|
+
import { runHealStage, resolveHealTargets, renderHealMarkdown } from './stage4_heal.js';
|
|
10
11
|
import { buildSummary } from '../validation/output_schema.js';
|
|
11
12
|
import { computeCannotDetermineRatio } from '../validation/guardrails.js';
|
|
12
13
|
function createRunId() {
|
|
@@ -31,6 +32,7 @@ export async function runPipeline(config) {
|
|
|
31
32
|
const allWarnings = [];
|
|
32
33
|
const stages = config.stages || ['preprocess', 'impact', 'coverage'];
|
|
33
34
|
let generatedSpecs;
|
|
35
|
+
let healResult;
|
|
34
36
|
// Step 1: Get changed files
|
|
35
37
|
const gitResult = getChangedFiles(config.appPath, config.gitSince, {
|
|
36
38
|
includeUncommitted: config.gitIncludeUncommitted,
|
|
@@ -87,6 +89,20 @@ export async function runPipeline(config) {
|
|
|
87
89
|
generatedSpecs = generationResult.generated;
|
|
88
90
|
allWarnings.push(...generationResult.warnings);
|
|
89
91
|
}
|
|
92
|
+
// Step 6: Heal stage — MCP-backed playwright-test-healer for failing/flaky specs
|
|
93
|
+
if (stages.includes('heal')) {
|
|
94
|
+
const healTargets = resolveHealTargets(config.testsRoot, {
|
|
95
|
+
playwrightReportPath: config.playwrightReportPath,
|
|
96
|
+
generatedSpecs,
|
|
97
|
+
}, decisions);
|
|
98
|
+
if (healTargets.length > 0) {
|
|
99
|
+
healResult = await runHealStage(config.testsRoot, healTargets, config.heal || { mcp: true });
|
|
100
|
+
allWarnings.push(...healResult.warnings);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
allWarnings.push('Heal stage: no targets found (no failing specs in report, no generated specs).');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
90
106
|
// Build report
|
|
91
107
|
const report = {
|
|
92
108
|
runId,
|
|
@@ -101,10 +117,10 @@ export async function runPipeline(config) {
|
|
|
101
117
|
generationAgent: stages.includes('generation') ? (config.generation?.provider || 'auto') : undefined,
|
|
102
118
|
},
|
|
103
119
|
};
|
|
104
|
-
const reportPath = writeReport(config.testsRoot, report);
|
|
105
|
-
return { report, reportPath, warnings: allWarnings, generated: generatedSpecs };
|
|
120
|
+
const reportPath = writeReport(config.testsRoot, report, healResult);
|
|
121
|
+
return { report, reportPath, warnings: allWarnings, generated: generatedSpecs, healResult };
|
|
106
122
|
}
|
|
107
|
-
function writeReport(testsRoot, report) {
|
|
123
|
+
function writeReport(testsRoot, report, healResult) {
|
|
108
124
|
const outputDir = join(testsRoot, '.e2e-ai-agents');
|
|
109
125
|
if (!existsSync(outputDir)) {
|
|
110
126
|
mkdirSync(outputDir, { recursive: true });
|
|
@@ -112,10 +128,10 @@ function writeReport(testsRoot, report) {
|
|
|
112
128
|
const jsonPath = join(outputDir, 'pipeline-report.json');
|
|
113
129
|
writeFileSync(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
114
130
|
const mdPath = join(outputDir, 'pipeline-report.md');
|
|
115
|
-
writeFileSync(mdPath, renderMarkdown(report), 'utf-8');
|
|
131
|
+
writeFileSync(mdPath, renderMarkdown(report, healResult), 'utf-8');
|
|
116
132
|
return jsonPath;
|
|
117
133
|
}
|
|
118
|
-
function renderMarkdown(report) {
|
|
134
|
+
function renderMarkdown(report, healResult) {
|
|
119
135
|
const lines = [
|
|
120
136
|
`# Impact Analysis Pipeline Report`,
|
|
121
137
|
'',
|
|
@@ -188,6 +204,10 @@ function renderMarkdown(report) {
|
|
|
188
204
|
lines.push('');
|
|
189
205
|
}
|
|
190
206
|
}
|
|
207
|
+
if (healResult) {
|
|
208
|
+
lines.push('');
|
|
209
|
+
lines.push(renderHealMarkdown(healResult));
|
|
210
|
+
}
|
|
191
211
|
if (report.warnings.length > 0) {
|
|
192
212
|
lines.push('## Warnings', '');
|
|
193
213
|
for (const w of report.warnings) {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
import { existsSync } from 'fs';
|
|
4
|
+
import { join, resolve } from 'path';
|
|
5
|
+
import { runTargetedSpecHeal } from '../agent/pipeline.js';
|
|
6
|
+
import { extractPlaywrightUnstableSpecs } from '../agent/playwright_report.js';
|
|
7
|
+
/**
|
|
8
|
+
* Resolve heal targets from one or more sources, in priority order:
|
|
9
|
+
* 1. Playwright JSON report (CI failures/flakes)
|
|
10
|
+
* 2. Stage 3 generated specs (newly written files that need runtime validation)
|
|
11
|
+
* 3. Explicit target list
|
|
12
|
+
*/
|
|
13
|
+
export function resolveHealTargets(testsRoot, options, decisions) {
|
|
14
|
+
const targets = [];
|
|
15
|
+
const seen = new Set();
|
|
16
|
+
const addTarget = (specPath, status, reason) => {
|
|
17
|
+
// Normalize to absolute path so relative (from Playwright report) and absolute
|
|
18
|
+
// (from generated specs) deduplicate correctly
|
|
19
|
+
let normalized = specPath.replace(/\\/g, '/');
|
|
20
|
+
if (!specPath.startsWith('/') && !(/^[A-Za-z]:[\\/]/).test(specPath)) {
|
|
21
|
+
normalized = join(testsRoot, specPath).replace(/\\/g, '/');
|
|
22
|
+
}
|
|
23
|
+
if (seen.has(normalized)) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
seen.add(normalized);
|
|
27
|
+
// Try to match to a FlowDecision for context
|
|
28
|
+
const decision = findDecisionForSpec(normalized, decisions, testsRoot);
|
|
29
|
+
targets.push({ specPath: normalized, status, decision, reason });
|
|
30
|
+
};
|
|
31
|
+
// Source 1: Playwright JSON report
|
|
32
|
+
if (options.playwrightReportPath) {
|
|
33
|
+
const reportPath = resolve(options.playwrightReportPath);
|
|
34
|
+
if (existsSync(reportPath)) {
|
|
35
|
+
const unstable = extractPlaywrightUnstableSpecs(reportPath, [testsRoot]);
|
|
36
|
+
for (const spec of unstable) {
|
|
37
|
+
addTarget(spec.specPath, spec.status, `Playwright report: failingTests=${spec.failingTests}, flakyTests=${spec.flakyTests}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Source 2: Stage 3 generated specs (heal immediately after generation)
|
|
42
|
+
if (options.generatedSpecs) {
|
|
43
|
+
for (const gen of options.generatedSpecs) {
|
|
44
|
+
if (gen.written) {
|
|
45
|
+
addTarget(gen.specPath, 'failed', `Newly generated spec — needs runtime validation`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Source 3: Explicit targets
|
|
50
|
+
if (options.explicitTargets) {
|
|
51
|
+
for (const t of options.explicitTargets) {
|
|
52
|
+
addTarget(t.specPath, t.status, t.reason);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return targets;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Find the FlowDecision most relevant to a given spec path by matching
|
|
59
|
+
* targetSpec / newSpecPath / specPath suffix against decisions.
|
|
60
|
+
*/
|
|
61
|
+
function findDecisionForSpec(specPath, decisions, testsRoot) {
|
|
62
|
+
const normalizedRoot = testsRoot.replace(/\\/g, '/');
|
|
63
|
+
const relative = specPath.startsWith(normalizedRoot)
|
|
64
|
+
? specPath.slice(normalizedRoot.length).replace(/^\//, '')
|
|
65
|
+
: specPath;
|
|
66
|
+
return decisions.find((d) => {
|
|
67
|
+
const target = (d.targetSpec || d.newSpecPath || '').replace(/\\/g, '/');
|
|
68
|
+
return target && (target === relative || target === specPath || relative.endsWith(target) || target.endsWith(relative));
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function runHealStage(testsRoot, targets, config) {
|
|
72
|
+
const warnings = [];
|
|
73
|
+
if (targets.length === 0) {
|
|
74
|
+
return {
|
|
75
|
+
targets,
|
|
76
|
+
summary: {
|
|
77
|
+
runner: 'package-native',
|
|
78
|
+
results: [],
|
|
79
|
+
warnings: ['No heal targets provided.'],
|
|
80
|
+
},
|
|
81
|
+
warnings,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const healTargets = targets.map((t) => ({
|
|
85
|
+
specPath: t.specPath,
|
|
86
|
+
status: t.status,
|
|
87
|
+
reason: t.reason,
|
|
88
|
+
}));
|
|
89
|
+
const pipelineConfig = {
|
|
90
|
+
enabled: true,
|
|
91
|
+
scenarios: 1,
|
|
92
|
+
outputDir: config.outputDir || 'specs/functional/ai-assisted',
|
|
93
|
+
heal: true,
|
|
94
|
+
dryRun: config.dryRun,
|
|
95
|
+
mcp: config.mcp ?? true,
|
|
96
|
+
mcpAllowFallback: config.mcpAllowFallback ?? false,
|
|
97
|
+
mcpOnly: config.mcpOnly ?? false,
|
|
98
|
+
mcpCommandTimeoutMs: config.mcpCommandTimeoutMs,
|
|
99
|
+
mcpRetries: config.mcpRetries ?? 1,
|
|
100
|
+
};
|
|
101
|
+
const summary = runTargetedSpecHeal(testsRoot, healTargets, pipelineConfig);
|
|
102
|
+
warnings.push(...summary.warnings);
|
|
103
|
+
return { targets, summary, warnings };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Convenience: extract heal targets from a complete pipeline report + optional
|
|
107
|
+
* Playwright run results, then run the heal stage.
|
|
108
|
+
*/
|
|
109
|
+
export async function healFromReport(testsRoot, report, options) {
|
|
110
|
+
const targets = resolveHealTargets(testsRoot, {
|
|
111
|
+
playwrightReportPath: options.playwrightReportPath,
|
|
112
|
+
generatedSpecs: options.generatedSpecs,
|
|
113
|
+
}, report.decisions);
|
|
114
|
+
return runHealStage(testsRoot, targets, options.healConfig || { mcp: true });
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Write a heal summary section to the pipeline report markdown.
|
|
118
|
+
*/
|
|
119
|
+
export function renderHealMarkdown(result) {
|
|
120
|
+
const lines = ['## Heal Results', ''];
|
|
121
|
+
const healedCount = result.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
122
|
+
const failedCount = result.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
123
|
+
const skippedCount = result.summary.results.filter((r) => r.healStatus === 'skipped').length;
|
|
124
|
+
lines.push(`| Metric | Value |`);
|
|
125
|
+
lines.push(`|--------|-------|`);
|
|
126
|
+
lines.push(`| Targets | ${result.targets.length} |`);
|
|
127
|
+
lines.push(`| Healed | ${healedCount} |`);
|
|
128
|
+
lines.push(`| Failed | ${failedCount} |`);
|
|
129
|
+
lines.push(`| Skipped | ${skippedCount} |`);
|
|
130
|
+
lines.push('');
|
|
131
|
+
for (const r of result.summary.results) {
|
|
132
|
+
const icon = r.healStatus === 'success' ? '✅' : r.healStatus === 'failed' ? '❌' : '⏭';
|
|
133
|
+
lines.push(`- ${icon} \`${r.flowId}\` — heal: ${r.healStatus || 'n/a'}`);
|
|
134
|
+
if (r.error) {
|
|
135
|
+
lines.push(` - Error: ${r.error}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (result.warnings.length > 0) {
|
|
139
|
+
lines.push('', '### Heal Warnings', '');
|
|
140
|
+
for (const w of result.warnings) {
|
|
141
|
+
lines.push(`- ${w}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return lines.join('\n');
|
|
145
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
2
|
+
// See LICENSE.txt for license information.
|
|
3
|
+
/**
|
|
4
|
+
* Builds a route-family-aware heal prompt for the playwright-test-healer agent.
|
|
5
|
+
* Enriches the base healer constraints with flow context so the agent understands
|
|
6
|
+
* what the test is supposed to verify, reducing hallucination during selector repair.
|
|
7
|
+
*/
|
|
8
|
+
export function buildHealPrompt(ctx) {
|
|
9
|
+
const flowBlock = ctx.decision
|
|
10
|
+
? [
|
|
11
|
+
'',
|
|
12
|
+
'FLOW CONTEXT (use to understand test intent — do not change test objectives):',
|
|
13
|
+
` Flow: ${ctx.decision.flowName}`,
|
|
14
|
+
` Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
|
|
15
|
+
` Route: ${ctx.decision.specificRoute || '(family-level)'}`,
|
|
16
|
+
` User Actions: ${ctx.decision.userActions.join('; ') || 'not specified'}`,
|
|
17
|
+
` Evidence: ${ctx.decision.evidence}`,
|
|
18
|
+
].join('\n')
|
|
19
|
+
: '';
|
|
20
|
+
const statusNote = ctx.status === 'flaky'
|
|
21
|
+
? 'This test is FLAKY (passes sometimes, fails other times). Look for race conditions, missing waits, or order-dependent state.'
|
|
22
|
+
: 'This test is FAILING consistently. The selector, URL, or API call is likely broken.';
|
|
23
|
+
const failureBlock = ctx.failureDetail
|
|
24
|
+
? `\nFailure detail:\n${ctx.failureDetail}`
|
|
25
|
+
: '';
|
|
26
|
+
return [
|
|
27
|
+
'Heal this specific Playwright test file and keep edits minimal.',
|
|
28
|
+
'',
|
|
29
|
+
`Target test file: ${ctx.specPath}`,
|
|
30
|
+
`Status: ${ctx.status.toUpperCase()} — ${statusNote}`,
|
|
31
|
+
failureBlock,
|
|
32
|
+
flowBlock,
|
|
33
|
+
'',
|
|
34
|
+
'Healing constraints (must follow):',
|
|
35
|
+
'- Import ONLY from "@mattermost/playwright-lib". Do not use "@playwright/test" directly.',
|
|
36
|
+
'- Do not use test.describe or test.only.',
|
|
37
|
+
'- Keep a single tag string matching the route family (e.g. "@channels", "@scheduled_posts").',
|
|
38
|
+
'- Use only existing Mattermost Playwright fixture and page-object APIs.',
|
|
39
|
+
'- Do NOT invent new pw.* clients or page object methods that do not exist.',
|
|
40
|
+
'- Avoid brittle class selectors (.backstage-navbar, .admin-console__wrapper, .left-panel, .panel-card).',
|
|
41
|
+
'- Prefer stable assertions using URL patterns, data-testid attributes, ARIA roles, and page-object methods.',
|
|
42
|
+
'- For flaky tests: add explicit waits (waitFor, expect().toBeVisible()) before interactions.',
|
|
43
|
+
'- Keep the test intent and scenario unchanged — only fix what is broken.',
|
|
44
|
+
'- If behavior is genuinely broken server-side, mark test.fixme with a clear comment explaining why.',
|
|
45
|
+
'',
|
|
46
|
+
'Run and fix this test until it compiles and passes, or mark test.fixme when the behavior is truly broken.',
|
|
47
|
+
].filter((line) => line !== null).join('\n');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds a minimal quality-fix prompt for spec files that fail content validation
|
|
51
|
+
* (e.g. contain test.describe, test.only, wrong imports).
|
|
52
|
+
*/
|
|
53
|
+
export function buildQualityFixPrompt(specPath, qualityIssues) {
|
|
54
|
+
return [
|
|
55
|
+
'Fix quality issues in this Playwright spec file. Make minimal edits only.',
|
|
56
|
+
'',
|
|
57
|
+
`Target file: ${specPath}`,
|
|
58
|
+
'',
|
|
59
|
+
'Issues to fix:',
|
|
60
|
+
...qualityIssues.map((issue) => ` - ${issue}`),
|
|
61
|
+
'',
|
|
62
|
+
'Rules:',
|
|
63
|
+
'- Import only from "@mattermost/playwright-lib".',
|
|
64
|
+
'- Remove test.describe wrappers (flatten to top-level test() calls).',
|
|
65
|
+
'- Remove test.only calls.',
|
|
66
|
+
'- Ensure each test has exactly one tag string.',
|
|
67
|
+
'- Do not change test logic — only fix structural quality issues.',
|
|
68
|
+
].join('\n');
|
|
69
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -35,6 +35,10 @@ export { runGenerationStage } from './pipeline/stage3_generation.js';
|
|
|
35
35
|
export type { GenerationConfig, GenerationResult, GeneratedSpec } from './pipeline/stage3_generation.js';
|
|
36
36
|
export { buildGenerationPrompt, parseGenerationResponse, detectHallucinatedMethods } from './prompts/generation.js';
|
|
37
37
|
export type { GenerationPromptContext, GenerationAgentResponse } from './prompts/generation.js';
|
|
38
|
+
export { runHealStage, healFromReport, resolveHealTargets, renderHealMarkdown } from './pipeline/stage4_heal.js';
|
|
39
|
+
export type { HealConfig, HealTarget, HealResult } from './pipeline/stage4_heal.js';
|
|
40
|
+
export { buildHealPrompt, buildQualityFixPrompt } from './prompts/heal.js';
|
|
41
|
+
export type { HealPromptContext } from './prompts/heal.js';
|
|
38
42
|
export { loadRouteFamilyManifest, bindFilesToFamilies } from './knowledge/route_families.js';
|
|
39
43
|
export type { RouteFamily, RouteFeature, RouteFamilyManifest, FileBinding } from './knowledge/route_families.js';
|
|
40
44
|
export { buildApiSurface, loadOrBuildApiSurface } from './knowledge/api_surface.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAGH,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,GACf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,gBAAgB,EAAE,0BAA0B,EAAC,MAAM,yBAAyB,CAAC;AAGrF,OAAO,EAAC,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAC,kBAAkB,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAChF,YAAY,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAC,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAC,MAAM,UAAU,CAAC;AACjI,YAAY,EACR,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,4BAA4B,EAC5B,6BAA6B,GAChC,MAAM,UAAU,CAAC;AAClB,OAAO,EAAC,0BAA0B,EAAE,eAAe,EAAC,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAC,2BAA2B,EAAE,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACzF,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAC1D,YAAY,EAAC,6BAA6B,EAAE,4BAA4B,EAAC,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAC,uBAAuB,EAAC,MAAM,gCAAgC,CAAC;AACvE,YAAY,EAAC,yBAAyB,EAAE,wBAAwB,EAAE,uBAAuB,EAAC,MAAM,gCAAgC,CAAC;AACjI,OAAO,EAAC,wBAAwB,EAAC,MAAM,iCAAiC,CAAC;AACzE,YAAY,EAAC,0BAA0B,EAAE,yBAAyB,EAAC,MAAM,iCAAiC,CAAC;AAG3G,OAAO,EAAC,WAAW,EAAC,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAC/E,YAAY,EAAC,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,UAAU,EAAE,cAAc,EAAC,MAAM,+BAA+B,CAAC;AACrI,OAAO,EAAC,kBAAkB,EAAC,MAAM,iCAAiC,CAAC;AACnE,YAAY,EAAC,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAC,MAAM,iCAAiC,CAAC;AACvG,OAAO,EAAC,qBAAqB,EAAE,uBAAuB,EAAE,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AAClH,YAAY,EAAC,uBAAuB,EAAE,uBAAuB,EAAC,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;GAWG;AAGH,YAAY,EACR,WAAW,EACX,eAAe,EACf,UAAU,EACV,WAAW,EACX,UAAU,EACV,oBAAoB,EACpB,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,EACZ,YAAY,GACf,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EAAC,gBAAgB,EAAE,0BAA0B,EAAC,MAAM,yBAAyB,CAAC;AAGrF,OAAO,EAAC,iBAAiB,EAAE,mBAAmB,EAAC,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,sBAAsB,CAAC;AACtE,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAGpD,OAAO,EAAC,kBAAkB,EAAE,qBAAqB,EAAC,MAAM,uBAAuB,CAAC;AAChF,YAAY,EAAC,YAAY,EAAC,MAAM,uBAAuB,CAAC;AAGxD,OAAO,EAAC,aAAa,EAAE,QAAQ,EAAE,cAAc,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAC,MAAM,UAAU,CAAC;AACjI,YAAY,EACR,eAAe,EACf,aAAa,EACb,oBAAoB,EACpB,4BAA4B,EAC5B,6BAA6B,GAChC,MAAM,UAAU,CAAC;AAClB,OAAO,EAAC,0BAA0B,EAAE,eAAe,EAAC,MAAM,qBAAqB,CAAC;AAChF,YAAY,EAAC,2BAA2B,EAAE,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AACzF,OAAO,EAAC,sBAAsB,EAAC,MAAM,oBAAoB,CAAC;AAC1D,YAAY,EAAC,6BAA6B,EAAE,4BAA4B,EAAC,MAAM,oBAAoB,CAAC;AACpG,OAAO,EAAC,uBAAuB,EAAC,MAAM,gCAAgC,CAAC;AACvE,YAAY,EAAC,yBAAyB,EAAE,wBAAwB,EAAE,uBAAuB,EAAC,MAAM,gCAAgC,CAAC;AACjI,OAAO,EAAC,wBAAwB,EAAC,MAAM,iCAAiC,CAAC;AACzE,YAAY,EAAC,0BAA0B,EAAE,yBAAyB,EAAC,MAAM,iCAAiC,CAAC;AAG3G,OAAO,EAAC,WAAW,EAAC,MAAM,4BAA4B,CAAC;AACvD,YAAY,EAAC,cAAc,EAAE,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAC/E,YAAY,EAAC,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,UAAU,EAAE,cAAc,EAAC,MAAM,+BAA+B,CAAC;AACrI,OAAO,EAAC,kBAAkB,EAAC,MAAM,iCAAiC,CAAC;AACnE,YAAY,EAAC,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAC,MAAM,iCAAiC,CAAC;AACvG,OAAO,EAAC,qBAAqB,EAAE,uBAAuB,EAAE,yBAAyB,EAAC,MAAM,yBAAyB,CAAC;AAClH,YAAY,EAAC,uBAAuB,EAAE,uBAAuB,EAAC,MAAM,yBAAyB,CAAC;AAC9F,OAAO,EAAC,YAAY,EAAE,cAAc,EAAE,kBAAkB,EAAE,kBAAkB,EAAC,MAAM,2BAA2B,CAAC;AAC/G,YAAY,EAAC,UAAU,EAAE,UAAU,EAAE,UAAU,EAAC,MAAM,2BAA2B,CAAC;AAClF,OAAO,EAAC,eAAe,EAAE,qBAAqB,EAAC,MAAM,mBAAmB,CAAC;AACzE,YAAY,EAAC,iBAAiB,EAAC,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAC,uBAAuB,EAAE,mBAAmB,EAAC,MAAM,+BAA+B,CAAC;AAC3F,YAAY,EAAC,WAAW,EAAE,YAAY,EAAE,mBAAmB,EAAE,WAAW,EAAC,MAAM,+BAA+B,CAAC;AAC/G,OAAO,EAAC,eAAe,EAAE,qBAAqB,EAAC,MAAM,4BAA4B,CAAC;AAClF,YAAY,EAAC,iBAAiB,EAAE,iBAAiB,EAAC,MAAM,4BAA4B,CAAC;AACrF,OAAO,EAAC,cAAc,EAAE,iBAAiB,EAAC,MAAM,2BAA2B,CAAC;AAC5E,YAAY,EAAC,SAAS,EAAE,SAAS,EAAC,MAAM,2BAA2B,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
3
|
// See LICENSE.txt for license information.
|
|
4
4
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
-
exports.getSpecsForFamily = exports.buildSpecIndex = exports.loadOrBuildApiSurface = exports.buildApiSurface = exports.bindFilesToFamilies = exports.loadRouteFamilyManifest = exports.detectHallucinatedMethods = exports.parseGenerationResponse = exports.buildGenerationPrompt = exports.runGenerationStage = exports.runPipeline = exports.captureTraceabilityInput = exports.ingestTraceabilityInput = exports.finalizeGeneratedTests = exports.readCalibration = exports.appendFeedbackAndRecompute = exports.captureTraceability = exports.ingestTraceability = exports.handoffGeneratedTests = exports.recommendTests = exports.findGaps = exports.analyzeImpact = exports.validateProviderSetup = exports.LLMProviderFactory = exports.CustomProvider = exports.checkOpenAISetup = exports.OpenAIProvider = exports.checkOllamaSetup = exports.OllamaProvider = exports.checkAnthropicSetup = exports.AnthropicProvider = exports.UnsupportedCapabilityError = exports.LLMProviderError = void 0;
|
|
5
|
+
exports.getSpecsForFamily = exports.buildSpecIndex = exports.loadOrBuildApiSurface = exports.buildApiSurface = exports.bindFilesToFamilies = exports.loadRouteFamilyManifest = exports.buildQualityFixPrompt = exports.buildHealPrompt = exports.renderHealMarkdown = exports.resolveHealTargets = exports.healFromReport = exports.runHealStage = exports.detectHallucinatedMethods = exports.parseGenerationResponse = exports.buildGenerationPrompt = exports.runGenerationStage = exports.runPipeline = exports.captureTraceabilityInput = exports.ingestTraceabilityInput = exports.finalizeGeneratedTests = exports.readCalibration = exports.appendFeedbackAndRecompute = exports.captureTraceability = exports.ingestTraceability = exports.handoffGeneratedTests = exports.recommendTests = exports.findGaps = exports.analyzeImpact = exports.validateProviderSetup = exports.LLMProviderFactory = exports.CustomProvider = exports.checkOpenAISetup = exports.OpenAIProvider = exports.checkOllamaSetup = exports.OllamaProvider = exports.checkAnthropicSetup = exports.AnthropicProvider = exports.UnsupportedCapabilityError = exports.LLMProviderError = void 0;
|
|
6
6
|
var provider_interface_js_1 = require("./provider_interface.js");
|
|
7
7
|
Object.defineProperty(exports, "LLMProviderError", { enumerable: true, get: function () { return provider_interface_js_1.LLMProviderError; } });
|
|
8
8
|
Object.defineProperty(exports, "UnsupportedCapabilityError", { enumerable: true, get: function () { return provider_interface_js_1.UnsupportedCapabilityError; } });
|
|
@@ -48,6 +48,14 @@ var generation_js_1 = require("./prompts/generation.js");
|
|
|
48
48
|
Object.defineProperty(exports, "buildGenerationPrompt", { enumerable: true, get: function () { return generation_js_1.buildGenerationPrompt; } });
|
|
49
49
|
Object.defineProperty(exports, "parseGenerationResponse", { enumerable: true, get: function () { return generation_js_1.parseGenerationResponse; } });
|
|
50
50
|
Object.defineProperty(exports, "detectHallucinatedMethods", { enumerable: true, get: function () { return generation_js_1.detectHallucinatedMethods; } });
|
|
51
|
+
var stage4_heal_js_1 = require("./pipeline/stage4_heal.js");
|
|
52
|
+
Object.defineProperty(exports, "runHealStage", { enumerable: true, get: function () { return stage4_heal_js_1.runHealStage; } });
|
|
53
|
+
Object.defineProperty(exports, "healFromReport", { enumerable: true, get: function () { return stage4_heal_js_1.healFromReport; } });
|
|
54
|
+
Object.defineProperty(exports, "resolveHealTargets", { enumerable: true, get: function () { return stage4_heal_js_1.resolveHealTargets; } });
|
|
55
|
+
Object.defineProperty(exports, "renderHealMarkdown", { enumerable: true, get: function () { return stage4_heal_js_1.renderHealMarkdown; } });
|
|
56
|
+
var heal_js_1 = require("./prompts/heal.js");
|
|
57
|
+
Object.defineProperty(exports, "buildHealPrompt", { enumerable: true, get: function () { return heal_js_1.buildHealPrompt; } });
|
|
58
|
+
Object.defineProperty(exports, "buildQualityFixPrompt", { enumerable: true, get: function () { return heal_js_1.buildQualityFixPrompt; } });
|
|
51
59
|
// Knowledge modules
|
|
52
60
|
var route_families_js_1 = require("./knowledge/route_families.js");
|
|
53
61
|
Object.defineProperty(exports, "loadRouteFamilyManifest", { enumerable: true, get: function () { return route_families_js_1.loadRouteFamilyManifest; } });
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ImpactConfig } from './stage1_impact.js';
|
|
2
2
|
import { type CoverageConfig } from './stage2_coverage.js';
|
|
3
3
|
import { type GenerationConfig, type GeneratedSpec } from './stage3_generation.js';
|
|
4
|
+
import { type HealConfig, type HealResult } from './stage4_heal.js';
|
|
4
5
|
import { type FlowDecisionReport } from '../validation/output_schema.js';
|
|
5
6
|
import type { RouteFamilyConfig } from '../knowledge/route_families.js';
|
|
6
7
|
import type { ApiSurfaceConfig } from '../knowledge/api_surface.js';
|
|
@@ -14,13 +15,17 @@ export interface PipelineConfig {
|
|
|
14
15
|
impact?: ImpactConfig;
|
|
15
16
|
coverage?: CoverageConfig;
|
|
16
17
|
generation?: GenerationConfig;
|
|
17
|
-
|
|
18
|
+
heal?: HealConfig;
|
|
19
|
+
/** Path to a Playwright JSON report for heal-from-report mode */
|
|
20
|
+
playwrightReportPath?: string;
|
|
21
|
+
stages?: Array<'preprocess' | 'impact' | 'coverage' | 'generation' | 'heal'>;
|
|
18
22
|
}
|
|
19
23
|
export interface PipelineResult {
|
|
20
24
|
report: FlowDecisionReport;
|
|
21
25
|
reportPath: string;
|
|
22
26
|
warnings: string[];
|
|
23
27
|
generated?: GeneratedSpec[];
|
|
28
|
+
healResult?: HealResult;
|
|
24
29
|
}
|
|
25
30
|
export declare function runPipeline(config: PipelineConfig): Promise<PipelineResult>;
|
|
26
31
|
//# sourceMappingURL=orchestrator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/pipeline/orchestrator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAmB,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAe,KAAK,kBAAkB,EAAoB,MAAM,gCAAgC,CAAC;AAExG,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/pipeline/orchestrator.ts"],"names":[],"mappings":"AAOA,OAAO,EAAiB,KAAK,YAAY,EAAC,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAmB,KAAK,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAC3E,OAAO,EAAqB,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAC,MAAM,wBAAwB,CAAC;AACrG,OAAO,EAAuD,KAAK,UAAU,EAAE,KAAK,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACxH,OAAO,EAAe,KAAK,kBAAkB,EAAoB,MAAM,gCAAgC,CAAC;AAExG,OAAO,KAAK,EAAC,iBAAiB,EAAC,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,6BAA6B,CAAC;AAElE,MAAM,WAAW,cAAc;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,iEAAiE;IACjE,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,MAAM,CAAC,EAAE,KAAK,CAAC,YAAY,GAAG,QAAQ,GAAG,UAAU,GAAG,YAAY,GAAG,MAAM,CAAC,CAAC;CAChF;AAED,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,kBAAkB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B;AAoBD,wBAAsB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC,CAgIjF"}
|
|
@@ -10,6 +10,7 @@ const stage0_preprocess_js_1 = require("./stage0_preprocess.js");
|
|
|
10
10
|
const stage1_impact_js_1 = require("./stage1_impact.js");
|
|
11
11
|
const stage2_coverage_js_1 = require("./stage2_coverage.js");
|
|
12
12
|
const stage3_generation_js_1 = require("./stage3_generation.js");
|
|
13
|
+
const stage4_heal_js_1 = require("./stage4_heal.js");
|
|
13
14
|
const output_schema_js_1 = require("../validation/output_schema.js");
|
|
14
15
|
const guardrails_js_1 = require("../validation/guardrails.js");
|
|
15
16
|
function createRunId() {
|
|
@@ -34,6 +35,7 @@ async function runPipeline(config) {
|
|
|
34
35
|
const allWarnings = [];
|
|
35
36
|
const stages = config.stages || ['preprocess', 'impact', 'coverage'];
|
|
36
37
|
let generatedSpecs;
|
|
38
|
+
let healResult;
|
|
37
39
|
// Step 1: Get changed files
|
|
38
40
|
const gitResult = (0, git_js_1.getChangedFiles)(config.appPath, config.gitSince, {
|
|
39
41
|
includeUncommitted: config.gitIncludeUncommitted,
|
|
@@ -90,6 +92,20 @@ async function runPipeline(config) {
|
|
|
90
92
|
generatedSpecs = generationResult.generated;
|
|
91
93
|
allWarnings.push(...generationResult.warnings);
|
|
92
94
|
}
|
|
95
|
+
// Step 6: Heal stage — MCP-backed playwright-test-healer for failing/flaky specs
|
|
96
|
+
if (stages.includes('heal')) {
|
|
97
|
+
const healTargets = (0, stage4_heal_js_1.resolveHealTargets)(config.testsRoot, {
|
|
98
|
+
playwrightReportPath: config.playwrightReportPath,
|
|
99
|
+
generatedSpecs,
|
|
100
|
+
}, decisions);
|
|
101
|
+
if (healTargets.length > 0) {
|
|
102
|
+
healResult = await (0, stage4_heal_js_1.runHealStage)(config.testsRoot, healTargets, config.heal || { mcp: true });
|
|
103
|
+
allWarnings.push(...healResult.warnings);
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
allWarnings.push('Heal stage: no targets found (no failing specs in report, no generated specs).');
|
|
107
|
+
}
|
|
108
|
+
}
|
|
93
109
|
// Build report
|
|
94
110
|
const report = {
|
|
95
111
|
runId,
|
|
@@ -104,10 +120,10 @@ async function runPipeline(config) {
|
|
|
104
120
|
generationAgent: stages.includes('generation') ? (config.generation?.provider || 'auto') : undefined,
|
|
105
121
|
},
|
|
106
122
|
};
|
|
107
|
-
const reportPath = writeReport(config.testsRoot, report);
|
|
108
|
-
return { report, reportPath, warnings: allWarnings, generated: generatedSpecs };
|
|
123
|
+
const reportPath = writeReport(config.testsRoot, report, healResult);
|
|
124
|
+
return { report, reportPath, warnings: allWarnings, generated: generatedSpecs, healResult };
|
|
109
125
|
}
|
|
110
|
-
function writeReport(testsRoot, report) {
|
|
126
|
+
function writeReport(testsRoot, report, healResult) {
|
|
111
127
|
const outputDir = (0, path_1.join)(testsRoot, '.e2e-ai-agents');
|
|
112
128
|
if (!(0, fs_1.existsSync)(outputDir)) {
|
|
113
129
|
(0, fs_1.mkdirSync)(outputDir, { recursive: true });
|
|
@@ -115,10 +131,10 @@ function writeReport(testsRoot, report) {
|
|
|
115
131
|
const jsonPath = (0, path_1.join)(outputDir, 'pipeline-report.json');
|
|
116
132
|
(0, fs_1.writeFileSync)(jsonPath, JSON.stringify(report, null, 2), 'utf-8');
|
|
117
133
|
const mdPath = (0, path_1.join)(outputDir, 'pipeline-report.md');
|
|
118
|
-
(0, fs_1.writeFileSync)(mdPath, renderMarkdown(report), 'utf-8');
|
|
134
|
+
(0, fs_1.writeFileSync)(mdPath, renderMarkdown(report, healResult), 'utf-8');
|
|
119
135
|
return jsonPath;
|
|
120
136
|
}
|
|
121
|
-
function renderMarkdown(report) {
|
|
137
|
+
function renderMarkdown(report, healResult) {
|
|
122
138
|
const lines = [
|
|
123
139
|
`# Impact Analysis Pipeline Report`,
|
|
124
140
|
'',
|
|
@@ -191,6 +207,10 @@ function renderMarkdown(report) {
|
|
|
191
207
|
lines.push('');
|
|
192
208
|
}
|
|
193
209
|
}
|
|
210
|
+
if (healResult) {
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push((0, stage4_heal_js_1.renderHealMarkdown)(healResult));
|
|
213
|
+
}
|
|
194
214
|
if (report.warnings.length > 0) {
|
|
195
215
|
lines.push('## Warnings', '');
|
|
196
216
|
for (const w of report.warnings) {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { PipelineSummary } from '../agent/pipeline.js';
|
|
2
|
+
import type { FlowDecision, FlowDecisionReport } from '../validation/output_schema.js';
|
|
3
|
+
import type { GeneratedSpec } from './stage3_generation.js';
|
|
4
|
+
export interface HealConfig {
|
|
5
|
+
/** Enable MCP-backed heal via playwright-test-healer agent */
|
|
6
|
+
mcp?: boolean;
|
|
7
|
+
mcpAllowFallback?: boolean;
|
|
8
|
+
mcpOnly?: boolean;
|
|
9
|
+
mcpCommandTimeoutMs?: number;
|
|
10
|
+
mcpRetries?: number;
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
/** Output directory for healed/re-generated specs */
|
|
13
|
+
outputDir?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface HealTarget {
|
|
16
|
+
specPath: string;
|
|
17
|
+
status: 'failed' | 'flaky';
|
|
18
|
+
/** Matching FlowDecision for context-enriched prompts */
|
|
19
|
+
decision?: FlowDecision;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HealResult {
|
|
23
|
+
targets: HealTarget[];
|
|
24
|
+
summary: PipelineSummary;
|
|
25
|
+
warnings: string[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve heal targets from one or more sources, in priority order:
|
|
29
|
+
* 1. Playwright JSON report (CI failures/flakes)
|
|
30
|
+
* 2. Stage 3 generated specs (newly written files that need runtime validation)
|
|
31
|
+
* 3. Explicit target list
|
|
32
|
+
*/
|
|
33
|
+
export declare function resolveHealTargets(testsRoot: string, options: {
|
|
34
|
+
playwrightReportPath?: string;
|
|
35
|
+
generatedSpecs?: GeneratedSpec[];
|
|
36
|
+
explicitTargets?: Array<{
|
|
37
|
+
specPath: string;
|
|
38
|
+
status: 'failed' | 'flaky';
|
|
39
|
+
reason?: string;
|
|
40
|
+
}>;
|
|
41
|
+
}, decisions: FlowDecision[]): HealTarget[];
|
|
42
|
+
export declare function runHealStage(testsRoot: string, targets: HealTarget[], config: HealConfig): Promise<HealResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Convenience: extract heal targets from a complete pipeline report + optional
|
|
45
|
+
* Playwright run results, then run the heal stage.
|
|
46
|
+
*/
|
|
47
|
+
export declare function healFromReport(testsRoot: string, report: FlowDecisionReport, options: {
|
|
48
|
+
playwrightReportPath?: string;
|
|
49
|
+
generatedSpecs?: GeneratedSpec[];
|
|
50
|
+
healConfig?: HealConfig;
|
|
51
|
+
}): Promise<HealResult>;
|
|
52
|
+
/**
|
|
53
|
+
* Write a heal summary section to the pipeline report markdown.
|
|
54
|
+
*/
|
|
55
|
+
export declare function renderHealMarkdown(result: HealResult): string;
|
|
56
|
+
//# sourceMappingURL=stage4_heal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stage4_heal.d.ts","sourceRoot":"","sources":["../../src/pipeline/stage4_heal.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAiB,eAAe,EAAC,MAAM,sBAAsB,CAAC;AAE1E,OAAO,KAAK,EAAC,YAAY,EAAE,kBAAkB,EAAC,MAAM,gCAAgC,CAAC;AACrF,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,wBAAwB,CAAC;AAE1D,MAAM,WAAW,UAAU;IACvB,8DAA8D;IAC9D,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,qDAAqD;IACrD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,UAAU;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,UAAU,EAAE,CAAC;IACtB,OAAO,EAAE,eAAe,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE;IACL,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC,eAAe,CAAC,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAC,CAAC,CAAC;CAC5F,EACD,SAAS,EAAE,YAAY,EAAE,GAC1B,UAAU,EAAE,CAqDd;AAsBD,wBAAsB,YAAY,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,UAAU,EAAE,EACrB,MAAM,EAAE,UAAU,GACnB,OAAO,CAAC,UAAU,CAAC,CAsCrB;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAChC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,kBAAkB,EAC1B,OAAO,EAAE;IACL,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,cAAc,CAAC,EAAE,aAAa,EAAE,CAAC;IACjC,UAAU,CAAC,EAAE,UAAU,CAAC;CAC3B,GACF,OAAO,CAAC,UAAU,CAAC,CAWrB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,UAAU,GAAG,MAAM,CA8B7D"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
|
+
// See LICENSE.txt for license information.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.resolveHealTargets = resolveHealTargets;
|
|
6
|
+
exports.runHealStage = runHealStage;
|
|
7
|
+
exports.healFromReport = healFromReport;
|
|
8
|
+
exports.renderHealMarkdown = renderHealMarkdown;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const pipeline_js_1 = require("../agent/pipeline.js");
|
|
12
|
+
const playwright_report_js_1 = require("../agent/playwright_report.js");
|
|
13
|
+
/**
|
|
14
|
+
* Resolve heal targets from one or more sources, in priority order:
|
|
15
|
+
* 1. Playwright JSON report (CI failures/flakes)
|
|
16
|
+
* 2. Stage 3 generated specs (newly written files that need runtime validation)
|
|
17
|
+
* 3. Explicit target list
|
|
18
|
+
*/
|
|
19
|
+
function resolveHealTargets(testsRoot, options, decisions) {
|
|
20
|
+
const targets = [];
|
|
21
|
+
const seen = new Set();
|
|
22
|
+
const addTarget = (specPath, status, reason) => {
|
|
23
|
+
// Normalize to absolute path so relative (from Playwright report) and absolute
|
|
24
|
+
// (from generated specs) deduplicate correctly
|
|
25
|
+
let normalized = specPath.replace(/\\/g, '/');
|
|
26
|
+
if (!specPath.startsWith('/') && !(/^[A-Za-z]:[\\/]/).test(specPath)) {
|
|
27
|
+
normalized = (0, path_1.join)(testsRoot, specPath).replace(/\\/g, '/');
|
|
28
|
+
}
|
|
29
|
+
if (seen.has(normalized)) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
seen.add(normalized);
|
|
33
|
+
// Try to match to a FlowDecision for context
|
|
34
|
+
const decision = findDecisionForSpec(normalized, decisions, testsRoot);
|
|
35
|
+
targets.push({ specPath: normalized, status, decision, reason });
|
|
36
|
+
};
|
|
37
|
+
// Source 1: Playwright JSON report
|
|
38
|
+
if (options.playwrightReportPath) {
|
|
39
|
+
const reportPath = (0, path_1.resolve)(options.playwrightReportPath);
|
|
40
|
+
if ((0, fs_1.existsSync)(reportPath)) {
|
|
41
|
+
const unstable = (0, playwright_report_js_1.extractPlaywrightUnstableSpecs)(reportPath, [testsRoot]);
|
|
42
|
+
for (const spec of unstable) {
|
|
43
|
+
addTarget(spec.specPath, spec.status, `Playwright report: failingTests=${spec.failingTests}, flakyTests=${spec.flakyTests}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Source 2: Stage 3 generated specs (heal immediately after generation)
|
|
48
|
+
if (options.generatedSpecs) {
|
|
49
|
+
for (const gen of options.generatedSpecs) {
|
|
50
|
+
if (gen.written) {
|
|
51
|
+
addTarget(gen.specPath, 'failed', `Newly generated spec — needs runtime validation`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Source 3: Explicit targets
|
|
56
|
+
if (options.explicitTargets) {
|
|
57
|
+
for (const t of options.explicitTargets) {
|
|
58
|
+
addTarget(t.specPath, t.status, t.reason);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return targets;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Find the FlowDecision most relevant to a given spec path by matching
|
|
65
|
+
* targetSpec / newSpecPath / specPath suffix against decisions.
|
|
66
|
+
*/
|
|
67
|
+
function findDecisionForSpec(specPath, decisions, testsRoot) {
|
|
68
|
+
const normalizedRoot = testsRoot.replace(/\\/g, '/');
|
|
69
|
+
const relative = specPath.startsWith(normalizedRoot)
|
|
70
|
+
? specPath.slice(normalizedRoot.length).replace(/^\//, '')
|
|
71
|
+
: specPath;
|
|
72
|
+
return decisions.find((d) => {
|
|
73
|
+
const target = (d.targetSpec || d.newSpecPath || '').replace(/\\/g, '/');
|
|
74
|
+
return target && (target === relative || target === specPath || relative.endsWith(target) || target.endsWith(relative));
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
async function runHealStage(testsRoot, targets, config) {
|
|
78
|
+
const warnings = [];
|
|
79
|
+
if (targets.length === 0) {
|
|
80
|
+
return {
|
|
81
|
+
targets,
|
|
82
|
+
summary: {
|
|
83
|
+
runner: 'package-native',
|
|
84
|
+
results: [],
|
|
85
|
+
warnings: ['No heal targets provided.'],
|
|
86
|
+
},
|
|
87
|
+
warnings,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const healTargets = targets.map((t) => ({
|
|
91
|
+
specPath: t.specPath,
|
|
92
|
+
status: t.status,
|
|
93
|
+
reason: t.reason,
|
|
94
|
+
}));
|
|
95
|
+
const pipelineConfig = {
|
|
96
|
+
enabled: true,
|
|
97
|
+
scenarios: 1,
|
|
98
|
+
outputDir: config.outputDir || 'specs/functional/ai-assisted',
|
|
99
|
+
heal: true,
|
|
100
|
+
dryRun: config.dryRun,
|
|
101
|
+
mcp: config.mcp ?? true,
|
|
102
|
+
mcpAllowFallback: config.mcpAllowFallback ?? false,
|
|
103
|
+
mcpOnly: config.mcpOnly ?? false,
|
|
104
|
+
mcpCommandTimeoutMs: config.mcpCommandTimeoutMs,
|
|
105
|
+
mcpRetries: config.mcpRetries ?? 1,
|
|
106
|
+
};
|
|
107
|
+
const summary = (0, pipeline_js_1.runTargetedSpecHeal)(testsRoot, healTargets, pipelineConfig);
|
|
108
|
+
warnings.push(...summary.warnings);
|
|
109
|
+
return { targets, summary, warnings };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Convenience: extract heal targets from a complete pipeline report + optional
|
|
113
|
+
* Playwright run results, then run the heal stage.
|
|
114
|
+
*/
|
|
115
|
+
async function healFromReport(testsRoot, report, options) {
|
|
116
|
+
const targets = resolveHealTargets(testsRoot, {
|
|
117
|
+
playwrightReportPath: options.playwrightReportPath,
|
|
118
|
+
generatedSpecs: options.generatedSpecs,
|
|
119
|
+
}, report.decisions);
|
|
120
|
+
return runHealStage(testsRoot, targets, options.healConfig || { mcp: true });
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Write a heal summary section to the pipeline report markdown.
|
|
124
|
+
*/
|
|
125
|
+
function renderHealMarkdown(result) {
|
|
126
|
+
const lines = ['## Heal Results', ''];
|
|
127
|
+
const healedCount = result.summary.results.filter((r) => r.healStatus === 'success').length;
|
|
128
|
+
const failedCount = result.summary.results.filter((r) => r.healStatus === 'failed').length;
|
|
129
|
+
const skippedCount = result.summary.results.filter((r) => r.healStatus === 'skipped').length;
|
|
130
|
+
lines.push(`| Metric | Value |`);
|
|
131
|
+
lines.push(`|--------|-------|`);
|
|
132
|
+
lines.push(`| Targets | ${result.targets.length} |`);
|
|
133
|
+
lines.push(`| Healed | ${healedCount} |`);
|
|
134
|
+
lines.push(`| Failed | ${failedCount} |`);
|
|
135
|
+
lines.push(`| Skipped | ${skippedCount} |`);
|
|
136
|
+
lines.push('');
|
|
137
|
+
for (const r of result.summary.results) {
|
|
138
|
+
const icon = r.healStatus === 'success' ? '✅' : r.healStatus === 'failed' ? '❌' : '⏭';
|
|
139
|
+
lines.push(`- ${icon} \`${r.flowId}\` — heal: ${r.healStatus || 'n/a'}`);
|
|
140
|
+
if (r.error) {
|
|
141
|
+
lines.push(` - Error: ${r.error}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (result.warnings.length > 0) {
|
|
145
|
+
lines.push('', '### Heal Warnings', '');
|
|
146
|
+
for (const w of result.warnings) {
|
|
147
|
+
lines.push(`- ${w}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return lines.join('\n');
|
|
151
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { FlowDecision } from '../validation/output_schema.js';
|
|
2
|
+
export interface HealPromptContext {
|
|
3
|
+
specPath: string;
|
|
4
|
+
status: 'failed' | 'flaky';
|
|
5
|
+
decision?: FlowDecision;
|
|
6
|
+
failureDetail?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Builds a route-family-aware heal prompt for the playwright-test-healer agent.
|
|
10
|
+
* Enriches the base healer constraints with flow context so the agent understands
|
|
11
|
+
* what the test is supposed to verify, reducing hallucination during selector repair.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildHealPrompt(ctx: HealPromptContext): string;
|
|
14
|
+
/**
|
|
15
|
+
* Builds a minimal quality-fix prompt for spec files that fail content validation
|
|
16
|
+
* (e.g. contain test.describe, test.only, wrong imports).
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildQualityFixPrompt(specPath: string, qualityIssues: string[]): string;
|
|
19
|
+
//# sourceMappingURL=heal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"heal.d.ts","sourceRoot":"","sources":["../../src/prompts/heal.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gCAAgC,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,QAAQ,GAAG,OAAO,CAAC;IAC3B,QAAQ,CAAC,EAAE,YAAY,CAAC;IACxB,aAAa,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CA2C9D;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,GAAG,MAAM,CAgBvF"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
3
|
+
// See LICENSE.txt for license information.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.buildHealPrompt = buildHealPrompt;
|
|
6
|
+
exports.buildQualityFixPrompt = buildQualityFixPrompt;
|
|
7
|
+
/**
|
|
8
|
+
* Builds a route-family-aware heal prompt for the playwright-test-healer agent.
|
|
9
|
+
* Enriches the base healer constraints with flow context so the agent understands
|
|
10
|
+
* what the test is supposed to verify, reducing hallucination during selector repair.
|
|
11
|
+
*/
|
|
12
|
+
function buildHealPrompt(ctx) {
|
|
13
|
+
const flowBlock = ctx.decision
|
|
14
|
+
? [
|
|
15
|
+
'',
|
|
16
|
+
'FLOW CONTEXT (use to understand test intent — do not change test objectives):',
|
|
17
|
+
` Flow: ${ctx.decision.flowName}`,
|
|
18
|
+
` Route Family: ${ctx.decision.routeFamily}${ctx.decision.featureId ? ` / ${ctx.decision.featureId}` : ''}`,
|
|
19
|
+
` Route: ${ctx.decision.specificRoute || '(family-level)'}`,
|
|
20
|
+
` User Actions: ${ctx.decision.userActions.join('; ') || 'not specified'}`,
|
|
21
|
+
` Evidence: ${ctx.decision.evidence}`,
|
|
22
|
+
].join('\n')
|
|
23
|
+
: '';
|
|
24
|
+
const statusNote = ctx.status === 'flaky'
|
|
25
|
+
? 'This test is FLAKY (passes sometimes, fails other times). Look for race conditions, missing waits, or order-dependent state.'
|
|
26
|
+
: 'This test is FAILING consistently. The selector, URL, or API call is likely broken.';
|
|
27
|
+
const failureBlock = ctx.failureDetail
|
|
28
|
+
? `\nFailure detail:\n${ctx.failureDetail}`
|
|
29
|
+
: '';
|
|
30
|
+
return [
|
|
31
|
+
'Heal this specific Playwright test file and keep edits minimal.',
|
|
32
|
+
'',
|
|
33
|
+
`Target test file: ${ctx.specPath}`,
|
|
34
|
+
`Status: ${ctx.status.toUpperCase()} — ${statusNote}`,
|
|
35
|
+
failureBlock,
|
|
36
|
+
flowBlock,
|
|
37
|
+
'',
|
|
38
|
+
'Healing constraints (must follow):',
|
|
39
|
+
'- Import ONLY from "@mattermost/playwright-lib". Do not use "@playwright/test" directly.',
|
|
40
|
+
'- Do not use test.describe or test.only.',
|
|
41
|
+
'- Keep a single tag string matching the route family (e.g. "@channels", "@scheduled_posts").',
|
|
42
|
+
'- Use only existing Mattermost Playwright fixture and page-object APIs.',
|
|
43
|
+
'- Do NOT invent new pw.* clients or page object methods that do not exist.',
|
|
44
|
+
'- Avoid brittle class selectors (.backstage-navbar, .admin-console__wrapper, .left-panel, .panel-card).',
|
|
45
|
+
'- Prefer stable assertions using URL patterns, data-testid attributes, ARIA roles, and page-object methods.',
|
|
46
|
+
'- For flaky tests: add explicit waits (waitFor, expect().toBeVisible()) before interactions.',
|
|
47
|
+
'- Keep the test intent and scenario unchanged — only fix what is broken.',
|
|
48
|
+
'- If behavior is genuinely broken server-side, mark test.fixme with a clear comment explaining why.',
|
|
49
|
+
'',
|
|
50
|
+
'Run and fix this test until it compiles and passes, or mark test.fixme when the behavior is truly broken.',
|
|
51
|
+
].filter((line) => line !== null).join('\n');
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Builds a minimal quality-fix prompt for spec files that fail content validation
|
|
55
|
+
* (e.g. contain test.describe, test.only, wrong imports).
|
|
56
|
+
*/
|
|
57
|
+
function buildQualityFixPrompt(specPath, qualityIssues) {
|
|
58
|
+
return [
|
|
59
|
+
'Fix quality issues in this Playwright spec file. Make minimal edits only.',
|
|
60
|
+
'',
|
|
61
|
+
`Target file: ${specPath}`,
|
|
62
|
+
'',
|
|
63
|
+
'Issues to fix:',
|
|
64
|
+
...qualityIssues.map((issue) => ` - ${issue}`),
|
|
65
|
+
'',
|
|
66
|
+
'Rules:',
|
|
67
|
+
'- Import only from "@mattermost/playwright-lib".',
|
|
68
|
+
'- Remove test.describe wrappers (flatten to top-level test() calls).',
|
|
69
|
+
'- Remove test.only calls.',
|
|
70
|
+
'- Ensure each test has exactly one tag string.',
|
|
71
|
+
'- Do not change test logic — only fix structural quality issues.',
|
|
72
|
+
].join('\n');
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yasserkhanorg/e2e-agents",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Pluggable LLM provider library for AI-powered test automation. Use Claude, Ollama, or your own LLM. Integrate with Playwright, Jest, or any test framework. MCP server for test agents, cost tracking, and hybrid provider mode.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|