e2e-ai 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +322 -21
- package/agents/feature-analyzer-agent.md +77 -0
- package/agents/init-agent.md +29 -21
- package/agents/playwright-generator-agent.md +0 -4
- package/agents/qa-testcase-agent.md +0 -4
- package/agents/refactor-agent.md +0 -4
- package/agents/scenario-agent.md +0 -4
- package/agents/scenario-planner-agent.md +64 -0
- package/agents/self-healing-agent.md +0 -4
- package/agents/transcript-agent.md +0 -4
- package/dist/cli-6c0wsk32.js +165 -0
- package/dist/cli-98db6h2q.js +101 -0
- package/dist/cli-cqabyzv3.js +64 -0
- package/dist/cli-fgp618yt.js +13610 -0
- package/dist/cli-kx32qnf3.js +67 -0
- package/dist/cli.js +3900 -146
- package/dist/config/schema.js +1 -1
- package/dist/index.js +2 -2
- package/dist/mcp.js +72 -9
- package/package.json +2 -1
- package/scripts/codegen-env.mjs +74 -42
- package/scripts/voice/merger.mjs +44 -13
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
agent: scenario-planner-agent
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# System Prompt
|
|
6
|
+
|
|
7
|
+
You are a QA scenario planner. You receive a QA map (features, workflows, components) and generate test scenarios for each workflow. Scenarios cover happy paths, edge cases, validation, error handling, and permission checks.
|
|
8
|
+
|
|
9
|
+
Your output completes the QA map by adding the scenarios array, producing a full QAMapV2Payload ready for use.
|
|
10
|
+
|
|
11
|
+
## Input Schema
|
|
12
|
+
|
|
13
|
+
You receive a JSON object with:
|
|
14
|
+
- `features`: array - Feature definitions from feature-analyzer-agent
|
|
15
|
+
- `workflows`: array - Workflow definitions with steps
|
|
16
|
+
- `components`: array - Component definitions
|
|
17
|
+
|
|
18
|
+
## Output Schema
|
|
19
|
+
|
|
20
|
+
Respond with JSON only (no markdown fences, no extra text). Return the complete payload including the original features/workflows/components plus a new `scenarios` array:
|
|
21
|
+
|
|
22
|
+
```json
|
|
23
|
+
{
|
|
24
|
+
"features": [...],
|
|
25
|
+
"workflows": [...],
|
|
26
|
+
"components": [...],
|
|
27
|
+
"scenarios": [
|
|
28
|
+
{
|
|
29
|
+
"id": "sc:<workflow-id>:<index>",
|
|
30
|
+
"workflowId": "wf:<kebab>",
|
|
31
|
+
"featureId": "feat:<kebab>",
|
|
32
|
+
"name": "Descriptive scenario name",
|
|
33
|
+
"description": "What this scenario verifies",
|
|
34
|
+
"category": "happy-path|permission|validation|error|edge-case|precondition",
|
|
35
|
+
"preconditions": ["User is authenticated", "Data exists"],
|
|
36
|
+
"steps": [
|
|
37
|
+
{
|
|
38
|
+
"order": 1,
|
|
39
|
+
"action": "What the user does",
|
|
40
|
+
"expectedResult": "What should happen"
|
|
41
|
+
}
|
|
42
|
+
],
|
|
43
|
+
"expectedOutcome": "Final expected state",
|
|
44
|
+
"componentIds": ["comp:<kebab>"],
|
|
45
|
+
"workflowStepIds": ["step:<workflow>:<index>"],
|
|
46
|
+
"priority": "critical|high|medium|low"
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Rules
|
|
53
|
+
|
|
54
|
+
1. Generate at least one happy-path scenario per workflow
|
|
55
|
+
2. For CRUD workflows: test create, read, update, delete + validation failures
|
|
56
|
+
3. For multi-step workflows: test complete flow + abandonment at each step
|
|
57
|
+
4. For forms: test validation (empty fields, invalid input, boundary values)
|
|
58
|
+
5. For workflows with conditionalBranches: generate one scenario per branch
|
|
59
|
+
6. Priority mapping: happy-path critical flows = critical, validation = high, edge-cases = medium, precondition checks = low
|
|
60
|
+
7. Each scenario should have 2-8 steps, each with a verifiable expectedResult
|
|
61
|
+
8. Scenario names should be descriptive: "[Feature]: [what is being tested]"
|
|
62
|
+
9. Link scenarios to workflow steps via workflowStepIds
|
|
63
|
+
10. Aim for 3-8 scenarios per workflow depending on complexity
|
|
64
|
+
11. Output valid JSON only, no markdown code fences or surrounding text
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPackageRoot,
|
|
3
|
+
getProjectRoot
|
|
4
|
+
} from "./cli-cqabyzv3.js";
|
|
5
|
+
|
|
6
|
+
// src/agents/loadAgent.ts
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
function loadAgent(agentName, config) {
|
|
10
|
+
const localPath = join(getProjectRoot(), ".e2e-ai", "agents", `${agentName}.md`);
|
|
11
|
+
const packagePath = join(getPackageRoot(), "agents", `${agentName}.md`);
|
|
12
|
+
const filePath = existsSync(localPath) ? localPath : packagePath;
|
|
13
|
+
let content;
|
|
14
|
+
try {
|
|
15
|
+
content = readFileSync(filePath, "utf-8");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`Agent file not found: ${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
20
|
+
const agentConfig = extractConfig(frontmatter);
|
|
21
|
+
let systemPrompt = body;
|
|
22
|
+
if (config) {
|
|
23
|
+
const contextPath = join(getProjectRoot(), ".e2e-ai", "context.md");
|
|
24
|
+
if (existsSync(contextPath)) {
|
|
25
|
+
const projectContext = readFileSync(contextPath, "utf-8").trim();
|
|
26
|
+
if (projectContext) {
|
|
27
|
+
systemPrompt = `${body}
|
|
28
|
+
|
|
29
|
+
## Project Context
|
|
30
|
+
|
|
31
|
+
${projectContext}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (config.llm.agentModels[agentName]) {
|
|
35
|
+
agentConfig.model = config.llm.agentModels[agentName];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const sections = parseSections(body);
|
|
39
|
+
return {
|
|
40
|
+
name: frontmatter.agent ?? agentName,
|
|
41
|
+
systemPrompt,
|
|
42
|
+
inputSchema: sections["Input Schema"],
|
|
43
|
+
outputSchema: sections["Output Schema"],
|
|
44
|
+
rules: sections["Rules"],
|
|
45
|
+
example: sections["Example"],
|
|
46
|
+
config: agentConfig
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseFrontmatter(content) {
|
|
50
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
51
|
+
if (!match)
|
|
52
|
+
return { frontmatter: {}, body: content };
|
|
53
|
+
const frontmatter = {};
|
|
54
|
+
for (const line of match[1].split(`
|
|
55
|
+
`)) {
|
|
56
|
+
const colonIdx = line.indexOf(":");
|
|
57
|
+
if (colonIdx === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = line.slice(0, colonIdx).trim();
|
|
60
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
61
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
62
|
+
value = value.slice(1, -1);
|
|
63
|
+
if (value === "true")
|
|
64
|
+
value = true;
|
|
65
|
+
if (value === "false")
|
|
66
|
+
value = false;
|
|
67
|
+
if (!isNaN(Number(value)) && value !== "")
|
|
68
|
+
value = Number(value);
|
|
69
|
+
frontmatter[key] = value;
|
|
70
|
+
}
|
|
71
|
+
return { frontmatter, body: match[2] };
|
|
72
|
+
}
|
|
73
|
+
function extractConfig(frontmatter) {
|
|
74
|
+
return {
|
|
75
|
+
model: frontmatter.model,
|
|
76
|
+
maxTokens: frontmatter.max_tokens ?? 4096,
|
|
77
|
+
temperature: frontmatter.temperature ?? 0.2
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parseSections(body) {
|
|
81
|
+
const sections = {};
|
|
82
|
+
const headingRegex = /^##\s+(.+)$/gm;
|
|
83
|
+
const headings = [];
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = headingRegex.exec(body)) !== null) {
|
|
86
|
+
headings.push({ title: match[1].trim(), index: match.index });
|
|
87
|
+
}
|
|
88
|
+
const systemMatch = body.match(/^#\s+System Prompt\n([\s\S]*?)(?=\n##\s|$)/m);
|
|
89
|
+
if (systemMatch) {
|
|
90
|
+
sections["System Prompt"] = systemMatch[1].trim();
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0;i < headings.length; i++) {
|
|
93
|
+
const start = headings[i].index + body.slice(headings[i].index).indexOf(`
|
|
94
|
+
`) + 1;
|
|
95
|
+
const end = i + 1 < headings.length ? headings[i + 1].index : body.length;
|
|
96
|
+
sections[headings[i].title] = body.slice(start, end).trim();
|
|
97
|
+
}
|
|
98
|
+
return sections;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/scan.ts
|
|
102
|
+
import { readdirSync, existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
103
|
+
import { join as join2, relative } from "node:path";
|
|
104
|
+
async function scanCodebase(root) {
|
|
105
|
+
const scan = {
|
|
106
|
+
testFiles: [],
|
|
107
|
+
configFiles: [],
|
|
108
|
+
fixtureFiles: [],
|
|
109
|
+
featureFiles: [],
|
|
110
|
+
tsconfigPaths: {},
|
|
111
|
+
playwrightConfig: null,
|
|
112
|
+
sampleTestContent: null
|
|
113
|
+
};
|
|
114
|
+
function walk(dir, depth = 0) {
|
|
115
|
+
if (depth > 5)
|
|
116
|
+
return [];
|
|
117
|
+
const files = [];
|
|
118
|
+
try {
|
|
119
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
120
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
121
|
+
continue;
|
|
122
|
+
const full = join2(dir, entry.name);
|
|
123
|
+
if (entry.isDirectory()) {
|
|
124
|
+
files.push(...walk(full, depth + 1));
|
|
125
|
+
} else {
|
|
126
|
+
files.push(full);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
} catch {}
|
|
130
|
+
return files;
|
|
131
|
+
}
|
|
132
|
+
const allFiles = walk(root);
|
|
133
|
+
for (const file of allFiles) {
|
|
134
|
+
const rel = relative(root, file);
|
|
135
|
+
if (rel.endsWith(".test.ts") || rel.endsWith(".spec.ts")) {
|
|
136
|
+
scan.testFiles.push(rel);
|
|
137
|
+
if (!scan.sampleTestContent && scan.testFiles.length <= 3) {
|
|
138
|
+
try {
|
|
139
|
+
scan.sampleTestContent = readFileSync2(file, "utf-8").slice(0, 3000);
|
|
140
|
+
} catch {}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (rel.endsWith(".feature.ts"))
|
|
144
|
+
scan.featureFiles.push(rel);
|
|
145
|
+
if (rel.includes("fixture") && rel.endsWith(".ts"))
|
|
146
|
+
scan.fixtureFiles.push(rel);
|
|
147
|
+
if (rel === "playwright.config.ts" || rel === "playwright.config.js")
|
|
148
|
+
scan.playwrightConfig = rel;
|
|
149
|
+
if (rel === "tsconfig.json" || rel.endsWith("/tsconfig.json")) {
|
|
150
|
+
try {
|
|
151
|
+
const tsconfig = JSON.parse(readFileSync2(file, "utf-8"));
|
|
152
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
153
|
+
scan.tsconfigPaths = { ...scan.tsconfigPaths, ...tsconfig.compilerOptions.paths };
|
|
154
|
+
}
|
|
155
|
+
} catch {}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
for (const name of ["playwright.config.ts", "vitest.config.ts", "jest.config.ts", "tsconfig.json", "package.json"]) {
|
|
159
|
+
if (existsSync2(join2(root, name)))
|
|
160
|
+
scan.configFiles.push(name);
|
|
161
|
+
}
|
|
162
|
+
return scan;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { loadAgent, scanCodebase };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getPackageRoot,
|
|
3
|
+
getProjectRoot
|
|
4
|
+
} from "./cli-kx32qnf3.js";
|
|
5
|
+
|
|
6
|
+
// src/agents/loadAgent.ts
|
|
7
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
function loadAgent(agentName, config) {
|
|
10
|
+
const localPath = join(getProjectRoot(), ".e2e-ai", "agents", `${agentName}.md`);
|
|
11
|
+
const packagePath = join(getPackageRoot(), "agents", `${agentName}.md`);
|
|
12
|
+
const filePath = existsSync(localPath) ? localPath : packagePath;
|
|
13
|
+
let content;
|
|
14
|
+
try {
|
|
15
|
+
content = readFileSync(filePath, "utf-8");
|
|
16
|
+
} catch {
|
|
17
|
+
throw new Error(`Agent file not found: ${filePath}`);
|
|
18
|
+
}
|
|
19
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
20
|
+
const agentConfig = extractConfig(frontmatter);
|
|
21
|
+
let systemPrompt = body;
|
|
22
|
+
if (config) {
|
|
23
|
+
const contextPath = join(getProjectRoot(), ".e2e-ai", "context.md");
|
|
24
|
+
if (existsSync(contextPath)) {
|
|
25
|
+
const projectContext = readFileSync(contextPath, "utf-8").trim();
|
|
26
|
+
if (projectContext) {
|
|
27
|
+
systemPrompt = `${body}
|
|
28
|
+
|
|
29
|
+
## Project Context
|
|
30
|
+
|
|
31
|
+
${projectContext}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (config.llm.agentModels[agentName]) {
|
|
35
|
+
agentConfig.model = config.llm.agentModels[agentName];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const sections = parseSections(body);
|
|
39
|
+
return {
|
|
40
|
+
name: frontmatter.agent ?? agentName,
|
|
41
|
+
systemPrompt,
|
|
42
|
+
inputSchema: sections["Input Schema"],
|
|
43
|
+
outputSchema: sections["Output Schema"],
|
|
44
|
+
rules: sections["Rules"],
|
|
45
|
+
example: sections["Example"],
|
|
46
|
+
config: agentConfig
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function parseFrontmatter(content) {
|
|
50
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
51
|
+
if (!match)
|
|
52
|
+
return { frontmatter: {}, body: content };
|
|
53
|
+
const frontmatter = {};
|
|
54
|
+
for (const line of match[1].split(`
|
|
55
|
+
`)) {
|
|
56
|
+
const colonIdx = line.indexOf(":");
|
|
57
|
+
if (colonIdx === -1)
|
|
58
|
+
continue;
|
|
59
|
+
const key = line.slice(0, colonIdx).trim();
|
|
60
|
+
let value = line.slice(colonIdx + 1).trim();
|
|
61
|
+
if (value.startsWith('"') && value.endsWith('"'))
|
|
62
|
+
value = value.slice(1, -1);
|
|
63
|
+
if (value === "true")
|
|
64
|
+
value = true;
|
|
65
|
+
if (value === "false")
|
|
66
|
+
value = false;
|
|
67
|
+
if (!isNaN(Number(value)) && value !== "")
|
|
68
|
+
value = Number(value);
|
|
69
|
+
frontmatter[key] = value;
|
|
70
|
+
}
|
|
71
|
+
return { frontmatter, body: match[2] };
|
|
72
|
+
}
|
|
73
|
+
function extractConfig(frontmatter) {
|
|
74
|
+
return {
|
|
75
|
+
model: frontmatter.model,
|
|
76
|
+
maxTokens: frontmatter.max_tokens ?? 4096,
|
|
77
|
+
temperature: frontmatter.temperature ?? 0.2
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function parseSections(body) {
|
|
81
|
+
const sections = {};
|
|
82
|
+
const headingRegex = /^##\s+(.+)$/gm;
|
|
83
|
+
const headings = [];
|
|
84
|
+
let match;
|
|
85
|
+
while ((match = headingRegex.exec(body)) !== null) {
|
|
86
|
+
headings.push({ title: match[1].trim(), index: match.index });
|
|
87
|
+
}
|
|
88
|
+
const systemMatch = body.match(/^#\s+System Prompt\n([\s\S]*?)(?=\n##\s|$)/m);
|
|
89
|
+
if (systemMatch) {
|
|
90
|
+
sections["System Prompt"] = systemMatch[1].trim();
|
|
91
|
+
}
|
|
92
|
+
for (let i = 0;i < headings.length; i++) {
|
|
93
|
+
const start = headings[i].index + body.slice(headings[i].index).indexOf(`
|
|
94
|
+
`) + 1;
|
|
95
|
+
const end = i + 1 < headings.length ? headings[i + 1].index : body.length;
|
|
96
|
+
sections[headings[i].title] = body.slice(start, end).trim();
|
|
97
|
+
}
|
|
98
|
+
return sections;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export { loadAgent };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import {
|
|
2
|
+
E2eAiConfigSchema
|
|
3
|
+
} from "./cli-fgp618yt.js";
|
|
4
|
+
|
|
5
|
+
// src/config/loader.ts
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { dirname, join, resolve } from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
var CONFIG_FILENAMES = ["e2e-ai.config.ts", "e2e-ai.config.js", "e2e-ai.config.mjs"];
|
|
10
|
+
var cachedConfig = null;
|
|
11
|
+
var cachedProjectRoot = null;
|
|
12
|
+
function findConfigDir(startDir) {
|
|
13
|
+
let dir = resolve(startDir);
|
|
14
|
+
const root = dirname(dir) === dir ? dir : undefined;
|
|
15
|
+
while (true) {
|
|
16
|
+
for (const name of CONFIG_FILENAMES) {
|
|
17
|
+
if (existsSync(join(dir, name))) {
|
|
18
|
+
return dir;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(dir);
|
|
22
|
+
if (parent === dir || dir === root)
|
|
23
|
+
return null;
|
|
24
|
+
dir = parent;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getProjectRoot() {
|
|
28
|
+
if (cachedProjectRoot)
|
|
29
|
+
return cachedProjectRoot;
|
|
30
|
+
const found = findConfigDir(process.cwd());
|
|
31
|
+
cachedProjectRoot = found ?? process.cwd();
|
|
32
|
+
return cachedProjectRoot;
|
|
33
|
+
}
|
|
34
|
+
function getPackageRoot() {
|
|
35
|
+
let dir = import.meta.dirname;
|
|
36
|
+
while (!existsSync(join(dir, "package.json"))) {
|
|
37
|
+
const parent = dirname(dir);
|
|
38
|
+
if (parent === dir)
|
|
39
|
+
return dir;
|
|
40
|
+
dir = parent;
|
|
41
|
+
}
|
|
42
|
+
return dir;
|
|
43
|
+
}
|
|
44
|
+
async function loadConfig() {
|
|
45
|
+
if (cachedConfig)
|
|
46
|
+
return cachedConfig;
|
|
47
|
+
const projectRoot = getProjectRoot();
|
|
48
|
+
let userConfig = {};
|
|
49
|
+
for (const name of CONFIG_FILENAMES) {
|
|
50
|
+
const configPath = join(projectRoot, name);
|
|
51
|
+
if (existsSync(configPath)) {
|
|
52
|
+
try {
|
|
53
|
+
const fileUrl = pathToFileURL(configPath).href;
|
|
54
|
+
const mod = await import(fileUrl);
|
|
55
|
+
userConfig = mod.default ?? mod;
|
|
56
|
+
break;
|
|
57
|
+
} catch {}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
cachedConfig = E2eAiConfigSchema.parse(userConfig);
|
|
61
|
+
return cachedConfig;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { getProjectRoot, getPackageRoot, loadConfig };
|