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
package/dist/config/schema.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,10 +2,10 @@ import {
|
|
|
2
2
|
getPackageRoot,
|
|
3
3
|
getProjectRoot,
|
|
4
4
|
loadConfig
|
|
5
|
-
} from "./cli-
|
|
5
|
+
} from "./cli-kx32qnf3.js";
|
|
6
6
|
import {
|
|
7
7
|
defineConfig
|
|
8
|
-
} from "./cli-
|
|
8
|
+
} from "./cli-fgp618yt.js";
|
|
9
9
|
import"./cli-wckvcay0.js";
|
|
10
10
|
export {
|
|
11
11
|
loadConfig,
|
package/dist/mcp.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
loadAgent
|
|
4
|
-
|
|
5
|
-
} from "./cli-g7cc13w2.js";
|
|
3
|
+
loadAgent
|
|
4
|
+
} from "./cli-98db6h2q.js";
|
|
6
5
|
import {
|
|
7
6
|
getPackageRoot
|
|
8
|
-
} from "./cli-
|
|
7
|
+
} from "./cli-kx32qnf3.js";
|
|
9
8
|
import {
|
|
10
9
|
$ZodObject,
|
|
11
10
|
$ZodType,
|
|
@@ -38,7 +37,7 @@ import {
|
|
|
38
37
|
toJSONSchema,
|
|
39
38
|
union,
|
|
40
39
|
unknown
|
|
41
|
-
} from "./cli-
|
|
40
|
+
} from "./cli-fgp618yt.js";
|
|
42
41
|
import {
|
|
43
42
|
__commonJS,
|
|
44
43
|
__toESM
|
|
@@ -14857,8 +14856,72 @@ class StdioServerTransport {
|
|
|
14857
14856
|
}
|
|
14858
14857
|
|
|
14859
14858
|
// src/mcp.ts
|
|
14860
|
-
import { readFileSync } from "node:fs";
|
|
14861
|
-
import { join } from "node:path";
|
|
14859
|
+
import { readFileSync as readFileSync2 } from "node:fs";
|
|
14860
|
+
import { join as join2 } from "node:path";
|
|
14861
|
+
|
|
14862
|
+
// src/utils/scan.ts
|
|
14863
|
+
import { readdirSync, existsSync, readFileSync } from "node:fs";
|
|
14864
|
+
import { join, relative } from "node:path";
|
|
14865
|
+
async function scanCodebase(root) {
|
|
14866
|
+
const scan = {
|
|
14867
|
+
testFiles: [],
|
|
14868
|
+
configFiles: [],
|
|
14869
|
+
fixtureFiles: [],
|
|
14870
|
+
featureFiles: [],
|
|
14871
|
+
tsconfigPaths: {},
|
|
14872
|
+
playwrightConfig: null,
|
|
14873
|
+
sampleTestContent: null
|
|
14874
|
+
};
|
|
14875
|
+
function walk(dir, depth = 0) {
|
|
14876
|
+
if (depth > 5)
|
|
14877
|
+
return [];
|
|
14878
|
+
const files = [];
|
|
14879
|
+
try {
|
|
14880
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
14881
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist")
|
|
14882
|
+
continue;
|
|
14883
|
+
const full = join(dir, entry.name);
|
|
14884
|
+
if (entry.isDirectory()) {
|
|
14885
|
+
files.push(...walk(full, depth + 1));
|
|
14886
|
+
} else {
|
|
14887
|
+
files.push(full);
|
|
14888
|
+
}
|
|
14889
|
+
}
|
|
14890
|
+
} catch {}
|
|
14891
|
+
return files;
|
|
14892
|
+
}
|
|
14893
|
+
const allFiles = walk(root);
|
|
14894
|
+
for (const file of allFiles) {
|
|
14895
|
+
const rel = relative(root, file);
|
|
14896
|
+
if (rel.endsWith(".test.ts") || rel.endsWith(".spec.ts")) {
|
|
14897
|
+
scan.testFiles.push(rel);
|
|
14898
|
+
if (!scan.sampleTestContent && scan.testFiles.length <= 3) {
|
|
14899
|
+
try {
|
|
14900
|
+
scan.sampleTestContent = readFileSync(file, "utf-8").slice(0, 3000);
|
|
14901
|
+
} catch {}
|
|
14902
|
+
}
|
|
14903
|
+
}
|
|
14904
|
+
if (rel.endsWith(".feature.ts"))
|
|
14905
|
+
scan.featureFiles.push(rel);
|
|
14906
|
+
if (rel.includes("fixture") && rel.endsWith(".ts"))
|
|
14907
|
+
scan.fixtureFiles.push(rel);
|
|
14908
|
+
if (rel === "playwright.config.ts" || rel === "playwright.config.js")
|
|
14909
|
+
scan.playwrightConfig = rel;
|
|
14910
|
+
if (rel === "tsconfig.json" || rel.endsWith("/tsconfig.json")) {
|
|
14911
|
+
try {
|
|
14912
|
+
const tsconfig = JSON.parse(readFileSync(file, "utf-8"));
|
|
14913
|
+
if (tsconfig.compilerOptions?.paths) {
|
|
14914
|
+
scan.tsconfigPaths = { ...scan.tsconfigPaths, ...tsconfig.compilerOptions.paths };
|
|
14915
|
+
}
|
|
14916
|
+
} catch {}
|
|
14917
|
+
}
|
|
14918
|
+
}
|
|
14919
|
+
for (const name of ["playwright.config.ts", "vitest.config.ts", "jest.config.ts", "tsconfig.json", "package.json"]) {
|
|
14920
|
+
if (existsSync(join(root, name)))
|
|
14921
|
+
scan.configFiles.push(name);
|
|
14922
|
+
}
|
|
14923
|
+
return scan;
|
|
14924
|
+
}
|
|
14862
14925
|
|
|
14863
14926
|
// src/utils/validateContext.ts
|
|
14864
14927
|
var REQUIRED_SECTIONS = [
|
|
@@ -14955,8 +15018,8 @@ server.registerTool("e2e_ai_get_example", {
|
|
|
14955
15018
|
inputSchema: exports_external.object({})
|
|
14956
15019
|
}, async () => {
|
|
14957
15020
|
try {
|
|
14958
|
-
const examplePath =
|
|
14959
|
-
const content =
|
|
15021
|
+
const examplePath = join2(getPackageRoot(), "templates", "e2e-ai.context.example.md");
|
|
15022
|
+
const content = readFileSync2(examplePath, "utf-8");
|
|
14960
15023
|
return {
|
|
14961
15024
|
content: [{ type: "text", text: content }]
|
|
14962
15025
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "e2e-ai",
|
|
3
3
|
"description": "AI-powered test automation pipeline — record, transcribe, generate, heal and ship Playwright tests from a single CLI",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.4.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
35
35
|
"commander": "^13.1.0",
|
|
36
36
|
"dotenv": "^17.2.0",
|
|
37
|
+
"glob": "^13.0.6",
|
|
37
38
|
"ora": "^9.3.0",
|
|
38
39
|
"picocolors": "^1.1.1",
|
|
39
40
|
"yaml": "^2.7.1",
|
package/scripts/codegen-env.mjs
CHANGED
|
@@ -129,8 +129,54 @@ console.error(`Voice recording: ${voiceEnabled ? 'ENABLED' : 'disabled (--no-voi
|
|
|
129
129
|
console.error(`Trace replay: ${traceEnabled ? 'ENABLED' : 'disabled (--no-trace)'}`);
|
|
130
130
|
console.error('(When you close the Playwright Inspector, the file is written there.)\n');
|
|
131
131
|
|
|
132
|
+
// --- Session timing (always track, used for action timestamps) ---
|
|
133
|
+
const sessionStartTime = Date.now();
|
|
134
|
+
|
|
135
|
+
// --- Action timestamp tracking via file polling ---
|
|
136
|
+
const actionPattern = /^\s*(await\s+page\.|await\s+expect\()/;
|
|
137
|
+
let prevActionCount = 0;
|
|
138
|
+
const actionElapsedSec = []; // elapsed seconds for each action, in order
|
|
139
|
+
let pollInterval = null;
|
|
140
|
+
|
|
141
|
+
function startActionPolling() {
|
|
142
|
+
pollInterval = setInterval(() => {
|
|
143
|
+
if (!existsSync(outputPath)) return;
|
|
144
|
+
try {
|
|
145
|
+
const content = readFileSync(outputPath, 'utf-8');
|
|
146
|
+
const actionLines = content.split('\n').filter((l) => actionPattern.test(l));
|
|
147
|
+
const newCount = actionLines.length;
|
|
148
|
+
if (newCount > prevActionCount) {
|
|
149
|
+
const elapsed = (Date.now() - sessionStartTime) / 1000;
|
|
150
|
+
for (let i = prevActionCount; i < newCount; i++) {
|
|
151
|
+
actionElapsedSec.push(elapsed);
|
|
152
|
+
}
|
|
153
|
+
prevActionCount = newCount;
|
|
154
|
+
}
|
|
155
|
+
} catch {
|
|
156
|
+
// File might be mid-write — ignore
|
|
157
|
+
}
|
|
158
|
+
}, 500);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Inject // @t:<seconds>s comments above each action line in the codegen file. */
|
|
162
|
+
function injectActionTimestamps(filePath) {
|
|
163
|
+
if (actionElapsedSec.length === 0) return;
|
|
164
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
165
|
+
const lines = content.split('\n');
|
|
166
|
+
const result = [];
|
|
167
|
+
let idx = 0;
|
|
168
|
+
for (const line of lines) {
|
|
169
|
+
if (actionPattern.test(line) && idx < actionElapsedSec.length) {
|
|
170
|
+
const indent = line.match(/^(\s*)/)[1];
|
|
171
|
+
result.push(`${indent}// @t:${actionElapsedSec[idx].toFixed(1)}s`);
|
|
172
|
+
idx++;
|
|
173
|
+
}
|
|
174
|
+
result.push(line);
|
|
175
|
+
}
|
|
176
|
+
writeFileSync(filePath, result.join('\n'));
|
|
177
|
+
}
|
|
178
|
+
|
|
132
179
|
// --- Voice setup ---
|
|
133
|
-
let sessionStartTime = null;
|
|
134
180
|
let recording = false;
|
|
135
181
|
let currentRecProcess = null;
|
|
136
182
|
let segmentIndex = 0;
|
|
@@ -147,8 +193,6 @@ if (voiceEnabled) {
|
|
|
147
193
|
mkdirSync(recordingsDir, { recursive: true });
|
|
148
194
|
}
|
|
149
195
|
|
|
150
|
-
sessionStartTime = Date.now();
|
|
151
|
-
|
|
152
196
|
const segPath = resolve(recordingsDir, `seg-${String(segmentIndex).padStart(3, '0')}.wav`);
|
|
153
197
|
segmentPaths.push(segPath);
|
|
154
198
|
const rec = startRecording(segPath);
|
|
@@ -195,6 +239,9 @@ const child = spawn('npx', codegenArgs, {
|
|
|
195
239
|
shell: true,
|
|
196
240
|
});
|
|
197
241
|
|
|
242
|
+
// Start polling the output file for new actions to capture timestamps
|
|
243
|
+
startActionPolling();
|
|
244
|
+
|
|
198
245
|
// --- Keyboard listener for pause/resume ---
|
|
199
246
|
if (voiceEnabled && process.stdin.isTTY) {
|
|
200
247
|
process.stdin.setRawMode(true);
|
|
@@ -238,16 +285,20 @@ function cleanupTerminal() {
|
|
|
238
285
|
|
|
239
286
|
child.on('exit', async (code) => {
|
|
240
287
|
cleanupTerminal();
|
|
288
|
+
if (pollInterval) clearInterval(pollInterval);
|
|
241
289
|
|
|
242
290
|
if (code === 0) {
|
|
243
291
|
console.error(`\nSaved: ${outputRelative}`);
|
|
244
292
|
}
|
|
245
293
|
|
|
246
|
-
// ---
|
|
247
|
-
if (
|
|
248
|
-
|
|
249
|
-
|
|
294
|
+
// --- Inject action timestamps into codegen output ---
|
|
295
|
+
if (existsSync(outputPath) && actionElapsedSec.length > 0) {
|
|
296
|
+
injectActionTimestamps(outputPath);
|
|
297
|
+
console.error(`Injected ${actionElapsedSec.length} action timestamp(s) into: ${outputRelative}`);
|
|
298
|
+
}
|
|
250
299
|
|
|
300
|
+
// --- Voice post-processing: merge WAV segments only (transcription deferred to transcribe command) ---
|
|
301
|
+
if (voiceEnabled && segmentPaths.length > 0) {
|
|
251
302
|
try {
|
|
252
303
|
if (recording && currentRecProcess) {
|
|
253
304
|
const { stopRecording } = await import(resolve(__dirname, 'voice', 'recorder.mjs'));
|
|
@@ -260,45 +311,26 @@ child.on('exit', async (code) => {
|
|
|
260
311
|
|
|
261
312
|
if (existingSegments.length === 0) {
|
|
262
313
|
console.error('No audio segments recorded.');
|
|
263
|
-
process.exit(code ?? 0);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const mergedWavPath = resolve(recordingsDir, `voice-${timestamp}.wav`);
|
|
268
|
-
|
|
269
|
-
if (existingSegments.length === 1) {
|
|
270
|
-
renameSync(existingSegments[0], mergedWavPath);
|
|
271
314
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
315
|
+
const mergedWavPath = resolve(recordingsDir, `voice-${timestamp}.wav`);
|
|
316
|
+
|
|
317
|
+
if (existingSegments.length === 1) {
|
|
318
|
+
renameSync(existingSegments[0], mergedWavPath);
|
|
319
|
+
} else {
|
|
320
|
+
console.error(`Merging ${existingSegments.length} audio segments...`);
|
|
321
|
+
const args = ['--combine', 'concatenate', ...existingSegments, mergedWavPath];
|
|
322
|
+
execSync(`sox ${args.map((a) => `"${a}"`).join(' ')}`, { stdio: 'ignore' });
|
|
323
|
+
|
|
324
|
+
for (const seg of existingSegments) {
|
|
325
|
+
try { unlinkSync(seg); } catch {}
|
|
326
|
+
}
|
|
278
327
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
console.error(`Audio saved: ${relative(root, mergedWavPath)}`);
|
|
282
328
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
writeFileSync(transcriptPath, JSON.stringify(segments, null, 2));
|
|
288
|
-
console.error(`Transcript saved: ${relative(root, transcriptPath)}`);
|
|
289
|
-
|
|
290
|
-
if (segments.length > 0 && existsSync(outputPath)) {
|
|
291
|
-
const { merge } = await import(resolve(__dirname, 'voice', 'merger.mjs'));
|
|
292
|
-
const codegenContent = readFileSync(outputPath, 'utf-8');
|
|
293
|
-
const annotated = merge(codegenContent, segments, durationSec);
|
|
294
|
-
writeFileSync(outputPath, annotated);
|
|
295
|
-
console.error(`Merged ${segments.length} voice segment(s) into: ${outputRelative}`);
|
|
329
|
+
console.error(`\nVoice recording summary:`);
|
|
330
|
+
console.error(` Audio: ${relative(root, mergedWavPath)}`);
|
|
331
|
+
console.error(` Codegen: ${outputRelative}`);
|
|
332
|
+
console.error(` (Run 'transcribe' to process voice → merge into codegen)`);
|
|
296
333
|
}
|
|
297
|
-
|
|
298
|
-
console.error('\nVoice recording summary:');
|
|
299
|
-
console.error(` Audio: ${relative(root, mergedWavPath)}`);
|
|
300
|
-
console.error(` Transcript: ${relative(root, transcriptPath)}`);
|
|
301
|
-
console.error(` Codegen: ${outputRelative}`);
|
|
302
334
|
} catch (err) {
|
|
303
335
|
console.error(`\nVoice processing error: ${err.message}`);
|
|
304
336
|
}
|
package/scripts/voice/merger.mjs
CHANGED
|
@@ -9,16 +9,42 @@ function formatTime(sec) {
|
|
|
9
9
|
return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Extract real action timestamps from // @t:<seconds>s comments injected during codegen.
|
|
14
|
+
* Returns an array of { lineIndex, elapsed } or null if no timestamps found.
|
|
15
|
+
*
|
|
16
|
+
* @param {string[]} lines
|
|
17
|
+
* @param {number[]} actionIndices - line indices of action lines
|
|
18
|
+
* @returns {number[] | null} elapsed seconds per action, or null
|
|
19
|
+
*/
|
|
20
|
+
function extractEmbeddedTimestamps(lines, actionIndices) {
|
|
21
|
+
const tsPattern = /^\s*\/\/\s*@t:([\d.]+)s\s*$/;
|
|
22
|
+
const timestamps = [];
|
|
23
|
+
|
|
24
|
+
for (const actionIdx of actionIndices) {
|
|
25
|
+
// Look at the line immediately before the action for a @t: comment
|
|
26
|
+
if (actionIdx > 0 && tsPattern.test(lines[actionIdx - 1])) {
|
|
27
|
+
const match = lines[actionIdx - 1].match(tsPattern);
|
|
28
|
+
timestamps.push(parseFloat(match[1]));
|
|
29
|
+
} else {
|
|
30
|
+
// Missing timestamp for this action — can't use embedded timestamps
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return timestamps;
|
|
36
|
+
}
|
|
37
|
+
|
|
12
38
|
/**
|
|
13
39
|
* Merge codegen output with voice transcript segments.
|
|
14
40
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
41
|
+
* If the codegen contains // @t:<seconds>s comments (injected during recording),
|
|
42
|
+
* those real timestamps are used for alignment. Otherwise, action timestamps are
|
|
43
|
+
* distributed linearly across the session duration (fallback).
|
|
18
44
|
*
|
|
19
45
|
* @param {string} codegenContent - The original codegen .ts file content
|
|
20
46
|
* @param {Array<{ start: number, end: number, text: string }>} segments - Whisper transcript segments
|
|
21
|
-
* @param {number} durationSec - Total session duration in seconds
|
|
47
|
+
* @param {number} durationSec - Total session duration in seconds (used only for linear fallback)
|
|
22
48
|
* @returns {string} Annotated codegen content
|
|
23
49
|
*/
|
|
24
50
|
export function merge(codegenContent, segments, durationSec) {
|
|
@@ -26,8 +52,9 @@ export function merge(codegenContent, segments, durationSec) {
|
|
|
26
52
|
|
|
27
53
|
const lines = codegenContent.split('\n');
|
|
28
54
|
const actionPattern = /^\s*(await\s+page\.|await\s+expect\()/;
|
|
55
|
+
const tsCommentPattern = /^\s*\/\/\s*@t:[\d.]+s\s*$/;
|
|
29
56
|
|
|
30
|
-
// Find indices of action lines
|
|
57
|
+
// Find indices of action lines (skip @t: comment lines)
|
|
31
58
|
const actionIndices = [];
|
|
32
59
|
for (let i = 0; i < lines.length; i++) {
|
|
33
60
|
if (actionPattern.test(lines[i])) {
|
|
@@ -37,14 +64,16 @@ export function merge(codegenContent, segments, durationSec) {
|
|
|
37
64
|
|
|
38
65
|
if (actionIndices.length === 0) return codegenContent;
|
|
39
66
|
|
|
40
|
-
//
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
67
|
+
// Try to use embedded timestamps, fall back to linear distribution
|
|
68
|
+
const embeddedTs = extractEmbeddedTimestamps(lines, actionIndices);
|
|
69
|
+
const actionTimestamps = embeddedTs
|
|
70
|
+
? embeddedTs
|
|
71
|
+
: actionIndices.map((_, idx) => {
|
|
72
|
+
if (actionIndices.length === 1) return durationSec / 2;
|
|
73
|
+
return (idx / (actionIndices.length - 1)) * durationSec;
|
|
74
|
+
});
|
|
45
75
|
|
|
46
76
|
// For each segment, find the nearest action by timestamp
|
|
47
|
-
// Map: actionIndex → list of segments to insert before it
|
|
48
77
|
/** @type {Map<number, Array<{ start: number, end: number, text: string }>>} */
|
|
49
78
|
const insertions = new Map();
|
|
50
79
|
|
|
@@ -68,12 +97,14 @@ export function merge(codegenContent, segments, durationSec) {
|
|
|
68
97
|
insertions.get(actionLineIdx).push(seg);
|
|
69
98
|
}
|
|
70
99
|
|
|
71
|
-
// Build result: insert
|
|
100
|
+
// Build result: strip old @t: comments and insert voice comments before action lines
|
|
72
101
|
const result = [];
|
|
73
102
|
for (let i = 0; i < lines.length; i++) {
|
|
103
|
+
// Skip @t: timestamp comments — they're consumed, not preserved
|
|
104
|
+
if (tsCommentPattern.test(lines[i])) continue;
|
|
105
|
+
|
|
74
106
|
const segs = insertions.get(i);
|
|
75
107
|
if (segs) {
|
|
76
|
-
// Detect indentation of the action line
|
|
77
108
|
const indent = lines[i].match(/^(\s*)/)[1];
|
|
78
109
|
for (const seg of segs) {
|
|
79
110
|
result.push(
|