@yasserkhanorg/e2e-agents 0.5.10 → 0.5.12
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ai_mapping.d.ts","sourceRoot":"","sources":["../../src/agent/ai_mapping.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,YAAY,CAAC;AA4BvD,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;
|
|
1
|
+
{"version":3,"file":"ai_mapping.d.ts","sourceRoot":"","sources":["../../src/agent/ai_mapping.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,aAAa,CAAC;AACvD,OAAO,KAAK,EAAC,YAAY,EAAE,QAAQ,EAAC,MAAM,YAAY,CAAC;AA4BvD,MAAM,WAAW,eAAe;IAC5B,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACtB;AAuTD,wBAAsB,iBAAiB,CACnC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,qBAAqB,EAC7B,KAAK,EAAE,UAAU,EAAE,EACnB,KAAK,EAAE,QAAQ,EAAE,GAClB,OAAO,CAAC,eAAe,CAAC,CA4O1B"}
|
package/dist/agent/ai_mapping.js
CHANGED
|
@@ -139,14 +139,36 @@ function isStrongCandidateMatch(flow, matchedKeywords) {
|
|
|
139
139
|
const keywords = flowKeywords(flow);
|
|
140
140
|
return keywords.length === 1 && matchedKeywords[0].length >= MIN_SINGLE_KEYWORD_LENGTH;
|
|
141
141
|
}
|
|
142
|
+
// Extract test/describe/it title strings from file content for semantic matching.
|
|
143
|
+
function extractTestTitles(content) {
|
|
144
|
+
const titles = [];
|
|
145
|
+
const pattern = /(?:^|\s)(?:test|it|describe)\s*\(\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)/gm;
|
|
146
|
+
let match;
|
|
147
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
148
|
+
const title = match[1] ?? match[2] ?? match[3];
|
|
149
|
+
if (title) {
|
|
150
|
+
titles.push(title);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return titles.join(' ');
|
|
154
|
+
}
|
|
155
|
+
function matchedFlowKeywordsInTitles(flow, testContent) {
|
|
156
|
+
const haystack = extractTestTitles(testContent).toLowerCase();
|
|
157
|
+
if (!haystack) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return flowKeywords(flow).filter((keyword) => keyword && haystack.includes(keyword.toLowerCase()));
|
|
161
|
+
}
|
|
142
162
|
function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
143
163
|
const selected = new Set();
|
|
144
164
|
const byFlow = new Map();
|
|
145
165
|
const evidence = [];
|
|
146
166
|
const warnings = [];
|
|
147
167
|
const normalizedTests = tests.map((test) => (0, utils_js_1.normalizePath)(test.path)).filter(Boolean);
|
|
168
|
+
const testByNormalizedPath = new Map(tests.map((t) => [(0, utils_js_1.normalizePath)(t.path), t]));
|
|
148
169
|
const perFlowLimit = Math.max(2, Math.min(6, Math.floor(maxCandidateTests / Math.max(1, flows.length))));
|
|
149
170
|
for (const flow of flows) {
|
|
171
|
+
// Pass 1: path-keyword matching
|
|
150
172
|
const scored = [];
|
|
151
173
|
for (const testPath of normalizedTests) {
|
|
152
174
|
const matchedKeywords = matchedFlowKeywords(flow, testPath);
|
|
@@ -163,7 +185,37 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
163
185
|
.filter((candidate) => isStrongCandidateMatch(flow, candidate.matchedKeywords))
|
|
164
186
|
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
165
187
|
.slice(0, perFlowLimit);
|
|
166
|
-
|
|
188
|
+
// Pass 2: content-title matching
|
|
189
|
+
// For flows without enough path-matched candidates, also search test/describe/it
|
|
190
|
+
// title strings for flow keywords. This surfaces semantically related tests even
|
|
191
|
+
// when the file name does not match the flow (e.g. a search.spec.ts with a test
|
|
192
|
+
// titled "search for message in channel" covers search_messages).
|
|
193
|
+
const contentCandidates = [];
|
|
194
|
+
if (strongCandidates.length < perFlowLimit) {
|
|
195
|
+
const alreadyByPath = new Set(strongCandidates.map((c) => c.path));
|
|
196
|
+
for (const testPath of normalizedTests) {
|
|
197
|
+
if (alreadyByPath.has(testPath)) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const testFile = testByNormalizedPath.get(testPath);
|
|
201
|
+
if (!testFile?.content) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const titleKeywords = matchedFlowKeywordsInTitles(flow, testFile.content);
|
|
205
|
+
if (!isStrongCandidateMatch(flow, titleKeywords)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// Score content matches lower than path matches so path candidates rank higher.
|
|
209
|
+
contentCandidates.push({
|
|
210
|
+
path: testPath,
|
|
211
|
+
score: titleKeywords.length,
|
|
212
|
+
matchedKeywords: titleKeywords,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
contentCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
216
|
+
}
|
|
217
|
+
const allCandidates = [...strongCandidates, ...contentCandidates.slice(0, perFlowLimit)];
|
|
218
|
+
if (allCandidates.length === 0) {
|
|
167
219
|
// Exact-name fallback: if the flow ID has no effective keywords (all tokens are
|
|
168
220
|
// low-signal, e.g. view_user_group_modal), look for a test whose path contains
|
|
169
221
|
// the exact flow ID as a directory name or filename without extension.
|
|
@@ -181,9 +233,9 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
181
233
|
}
|
|
182
234
|
continue;
|
|
183
235
|
}
|
|
184
|
-
byFlow.set(flow.id, new Set(
|
|
185
|
-
evidence.push({ flowId: flow.id, candidates:
|
|
186
|
-
for (const candidate of
|
|
236
|
+
byFlow.set(flow.id, new Set(allCandidates.map((candidate) => candidate.path)));
|
|
237
|
+
evidence.push({ flowId: flow.id, candidates: allCandidates });
|
|
238
|
+
for (const candidate of allCandidates) {
|
|
187
239
|
selected.add(candidate.path);
|
|
188
240
|
}
|
|
189
241
|
}
|
|
@@ -324,8 +376,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
324
376
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
325
377
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
326
378
|
'- If unsure for a flow, return tests: [].',
|
|
327
|
-
'- For
|
|
328
|
-
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
379
|
+
'- For EVERY flow (whether or not tests were found), return missingScenarios with 3-5 key user-facing test scenarios that must be covered. Write each as a short imperative statement starting with a verb (e.g. "Search for a message by keyword and verify results appear"). For mapped flows, focus on what the existing tests do NOT cover; for unmapped flows, describe the core scenarios a new test should include.',
|
|
329
380
|
'',
|
|
330
381
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
331
382
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -392,6 +443,16 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
392
443
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
393
444
|
continue;
|
|
394
445
|
}
|
|
446
|
+
// Capture scenario suggestions for ALL flows up-front — before any early returns —
|
|
447
|
+
// so unmapped flows (tests: []) still get their suggested scenarios in the gap report.
|
|
448
|
+
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
449
|
+
const scenarios = entry.missingScenarios
|
|
450
|
+
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
451
|
+
.slice(0, 5);
|
|
452
|
+
if (scenarios.length > 0) {
|
|
453
|
+
scenarioGaps.set(entry.flowId, scenarios);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
395
456
|
const flow = prioritizedFlowsById.get(entry.flowId);
|
|
396
457
|
const confidence = typeof entry.confidence === 'number' ? entry.confidence : undefined;
|
|
397
458
|
const allowedTestsForFlow = candidateSelection.byFlow.get(entry.flowId);
|
|
@@ -410,15 +471,6 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
410
471
|
for (const testPath of valid) {
|
|
411
472
|
matchedTests.add(testPath);
|
|
412
473
|
}
|
|
413
|
-
// Store missing scenarios identified by the AI for this flow.
|
|
414
|
-
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
415
|
-
const scenarios = entry.missingScenarios
|
|
416
|
-
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
417
|
-
.slice(0, 5);
|
|
418
|
-
if (scenarios.length > 0) {
|
|
419
|
-
scenarioGaps.set(entry.flowId, scenarios);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
474
|
}
|
|
423
475
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
424
476
|
// for a file or directory whose name exactly matches the flow ID. This handles flows
|
|
@@ -136,14 +136,36 @@ function isStrongCandidateMatch(flow, matchedKeywords) {
|
|
|
136
136
|
const keywords = flowKeywords(flow);
|
|
137
137
|
return keywords.length === 1 && matchedKeywords[0].length >= MIN_SINGLE_KEYWORD_LENGTH;
|
|
138
138
|
}
|
|
139
|
+
// Extract test/describe/it title strings from file content for semantic matching.
|
|
140
|
+
function extractTestTitles(content) {
|
|
141
|
+
const titles = [];
|
|
142
|
+
const pattern = /(?:^|\s)(?:test|it|describe)\s*\(\s*(?:'([^']*)'|"([^"]*)"|`([^`]*)`)/gm;
|
|
143
|
+
let match;
|
|
144
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
145
|
+
const title = match[1] ?? match[2] ?? match[3];
|
|
146
|
+
if (title) {
|
|
147
|
+
titles.push(title);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return titles.join(' ');
|
|
151
|
+
}
|
|
152
|
+
function matchedFlowKeywordsInTitles(flow, testContent) {
|
|
153
|
+
const haystack = extractTestTitles(testContent).toLowerCase();
|
|
154
|
+
if (!haystack) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return flowKeywords(flow).filter((keyword) => keyword && haystack.includes(keyword.toLowerCase()));
|
|
158
|
+
}
|
|
139
159
|
function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
140
160
|
const selected = new Set();
|
|
141
161
|
const byFlow = new Map();
|
|
142
162
|
const evidence = [];
|
|
143
163
|
const warnings = [];
|
|
144
164
|
const normalizedTests = tests.map((test) => normalizePath(test.path)).filter(Boolean);
|
|
165
|
+
const testByNormalizedPath = new Map(tests.map((t) => [normalizePath(t.path), t]));
|
|
145
166
|
const perFlowLimit = Math.max(2, Math.min(6, Math.floor(maxCandidateTests / Math.max(1, flows.length))));
|
|
146
167
|
for (const flow of flows) {
|
|
168
|
+
// Pass 1: path-keyword matching
|
|
147
169
|
const scored = [];
|
|
148
170
|
for (const testPath of normalizedTests) {
|
|
149
171
|
const matchedKeywords = matchedFlowKeywords(flow, testPath);
|
|
@@ -160,7 +182,37 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
160
182
|
.filter((candidate) => isStrongCandidateMatch(flow, candidate.matchedKeywords))
|
|
161
183
|
.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path))
|
|
162
184
|
.slice(0, perFlowLimit);
|
|
163
|
-
|
|
185
|
+
// Pass 2: content-title matching
|
|
186
|
+
// For flows without enough path-matched candidates, also search test/describe/it
|
|
187
|
+
// title strings for flow keywords. This surfaces semantically related tests even
|
|
188
|
+
// when the file name does not match the flow (e.g. a search.spec.ts with a test
|
|
189
|
+
// titled "search for message in channel" covers search_messages).
|
|
190
|
+
const contentCandidates = [];
|
|
191
|
+
if (strongCandidates.length < perFlowLimit) {
|
|
192
|
+
const alreadyByPath = new Set(strongCandidates.map((c) => c.path));
|
|
193
|
+
for (const testPath of normalizedTests) {
|
|
194
|
+
if (alreadyByPath.has(testPath)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const testFile = testByNormalizedPath.get(testPath);
|
|
198
|
+
if (!testFile?.content) {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const titleKeywords = matchedFlowKeywordsInTitles(flow, testFile.content);
|
|
202
|
+
if (!isStrongCandidateMatch(flow, titleKeywords)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Score content matches lower than path matches so path candidates rank higher.
|
|
206
|
+
contentCandidates.push({
|
|
207
|
+
path: testPath,
|
|
208
|
+
score: titleKeywords.length,
|
|
209
|
+
matchedKeywords: titleKeywords,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
contentCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
213
|
+
}
|
|
214
|
+
const allCandidates = [...strongCandidates, ...contentCandidates.slice(0, perFlowLimit)];
|
|
215
|
+
if (allCandidates.length === 0) {
|
|
164
216
|
// Exact-name fallback: if the flow ID has no effective keywords (all tokens are
|
|
165
217
|
// low-signal, e.g. view_user_group_modal), look for a test whose path contains
|
|
166
218
|
// the exact flow ID as a directory name or filename without extension.
|
|
@@ -178,9 +230,9 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
178
230
|
}
|
|
179
231
|
continue;
|
|
180
232
|
}
|
|
181
|
-
byFlow.set(flow.id, new Set(
|
|
182
|
-
evidence.push({ flowId: flow.id, candidates:
|
|
183
|
-
for (const candidate of
|
|
233
|
+
byFlow.set(flow.id, new Set(allCandidates.map((candidate) => candidate.path)));
|
|
234
|
+
evidence.push({ flowId: flow.id, candidates: allCandidates });
|
|
235
|
+
for (const candidate of allCandidates) {
|
|
184
236
|
selected.add(candidate.path);
|
|
185
237
|
}
|
|
186
238
|
}
|
|
@@ -321,8 +373,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
321
373
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
322
374
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
323
375
|
'- If unsure for a flow, return tests: [].',
|
|
324
|
-
'- For
|
|
325
|
-
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
376
|
+
'- For EVERY flow (whether or not tests were found), return missingScenarios with 3-5 key user-facing test scenarios that must be covered. Write each as a short imperative statement starting with a verb (e.g. "Search for a message by keyword and verify results appear"). For mapped flows, focus on what the existing tests do NOT cover; for unmapped flows, describe the core scenarios a new test should include.',
|
|
326
377
|
'',
|
|
327
378
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
328
379
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -389,6 +440,16 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
389
440
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
390
441
|
continue;
|
|
391
442
|
}
|
|
443
|
+
// Capture scenario suggestions for ALL flows up-front — before any early returns —
|
|
444
|
+
// so unmapped flows (tests: []) still get their suggested scenarios in the gap report.
|
|
445
|
+
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
446
|
+
const scenarios = entry.missingScenarios
|
|
447
|
+
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
448
|
+
.slice(0, 5);
|
|
449
|
+
if (scenarios.length > 0) {
|
|
450
|
+
scenarioGaps.set(entry.flowId, scenarios);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
392
453
|
const flow = prioritizedFlowsById.get(entry.flowId);
|
|
393
454
|
const confidence = typeof entry.confidence === 'number' ? entry.confidence : undefined;
|
|
394
455
|
const allowedTestsForFlow = candidateSelection.byFlow.get(entry.flowId);
|
|
@@ -407,15 +468,6 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
407
468
|
for (const testPath of valid) {
|
|
408
469
|
matchedTests.add(testPath);
|
|
409
470
|
}
|
|
410
|
-
// Store missing scenarios identified by the AI for this flow.
|
|
411
|
-
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
412
|
-
const scenarios = entry.missingScenarios
|
|
413
|
-
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
414
|
-
.slice(0, 5);
|
|
415
|
-
if (scenarios.length > 0) {
|
|
416
|
-
scenarioGaps.set(entry.flowId, scenarios);
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
471
|
}
|
|
420
472
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
421
473
|
// for a file or directory whose name exactly matches the flow ID. This handles flows
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yasserkhanorg/e2e-agents",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
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",
|