@yasserkhanorg/e2e-agents 0.5.11 → 0.5.13
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;AAyWD,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,6 +139,21 @@ 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
|
+
// Stop-words excluded from content-fallback keyword matching.
|
|
143
|
+
const CONTENT_FALLBACK_STOP_WORDS = new Set(['and', 'for', 'the', 'to', 'of', 'on', 'at', 'with', 'in', 'a', 'an']);
|
|
144
|
+
// Returns raw (unfiltered) tokens for flows where flowKeywords() returns nothing.
|
|
145
|
+
// Used exclusively for content-title matching when all standard keywords are low-signal.
|
|
146
|
+
// Empty array is returned when the flow already has effective path keywords.
|
|
147
|
+
function contentFallbackKeywords(flow) {
|
|
148
|
+
if (flowKeywords(flow).length > 0) {
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
return (0, utils_js_1.uniqueTokens)([
|
|
152
|
+
...(0, utils_js_1.tokenize)(flow.id || ''),
|
|
153
|
+
...(0, utils_js_1.tokenize)(flow.name || ''),
|
|
154
|
+
...(flow.keywords || []),
|
|
155
|
+
]).filter((k) => k.length >= 3 && !CONTENT_FALLBACK_STOP_WORDS.has(k));
|
|
156
|
+
}
|
|
142
157
|
// Extract test/describe/it title strings from file content for semantic matching.
|
|
143
158
|
function extractTestTitles(content) {
|
|
144
159
|
const titles = [];
|
|
@@ -214,7 +229,39 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
214
229
|
}
|
|
215
230
|
contentCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
216
231
|
}
|
|
217
|
-
|
|
232
|
+
// Pass 2b: comprehensive fallback for all-low-signal flows.
|
|
233
|
+
// When flowKeywords() is empty (all tokens are low-signal), flowKeywords-based
|
|
234
|
+
// matching in both Pass 1 and Pass 2 yields nothing. As a last resort, search
|
|
235
|
+
// test titles using the raw unfiltered tokens from the flow ID/name, but require
|
|
236
|
+
// ALL tokens to match simultaneously — they are individually weak signals so the
|
|
237
|
+
// full conjunction is needed to establish behavioral coverage evidence.
|
|
238
|
+
const fallbackCandidates = [];
|
|
239
|
+
if (strongCandidates.length === 0 && contentCandidates.length === 0) {
|
|
240
|
+
const fallbackKws = contentFallbackKeywords(flow);
|
|
241
|
+
if (fallbackKws.length > 0) {
|
|
242
|
+
for (const testPath of normalizedTests) {
|
|
243
|
+
const testFile = testByNormalizedPath.get(testPath);
|
|
244
|
+
if (!testFile?.content) {
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const haystack = extractTestTitles(testFile.content).toLowerCase();
|
|
248
|
+
if (!haystack) {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
const matched = fallbackKws.filter((k) => haystack.includes(k));
|
|
252
|
+
if (matched.length < fallbackKws.length) {
|
|
253
|
+
continue; // all tokens must appear in at least one test title
|
|
254
|
+
}
|
|
255
|
+
fallbackCandidates.push({ path: testPath, score: matched.length, matchedKeywords: matched });
|
|
256
|
+
}
|
|
257
|
+
fallbackCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const allCandidates = [
|
|
261
|
+
...strongCandidates,
|
|
262
|
+
...contentCandidates.slice(0, perFlowLimit),
|
|
263
|
+
...fallbackCandidates.slice(0, perFlowLimit),
|
|
264
|
+
];
|
|
218
265
|
if (allCandidates.length === 0) {
|
|
219
266
|
// Exact-name fallback: if the flow ID has no effective keywords (all tokens are
|
|
220
267
|
// low-signal, e.g. view_user_group_modal), look for a test whose path contains
|
|
@@ -371,13 +418,12 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
371
418
|
'Rules:',
|
|
372
419
|
'- Keep at most 5 tests per flow.',
|
|
373
420
|
'- Use exact flowId values from FLOWS.',
|
|
374
|
-
'-
|
|
421
|
+
'- Map a test when you have clear evidence it covers the flow — from the file path OR from test titles in the content. Behavioral coverage via test titles is sufficient even when the filename does not exactly match the flow (e.g. search_user_post_spec.js covers search_messages if its titles assert searching for messages). Generic subsystem similarity without behavioral evidence is not enough.',
|
|
375
422
|
'- A flow may only map to tests listed under FLOW_CANDIDATE_SIGNALS for that flow.',
|
|
376
423
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
377
424
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
378
425
|
'- If unsure for a flow, return tests: [].',
|
|
379
|
-
'- For
|
|
380
|
-
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
426
|
+
'- 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.',
|
|
381
427
|
'',
|
|
382
428
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
383
429
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -444,6 +490,16 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
444
490
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
445
491
|
continue;
|
|
446
492
|
}
|
|
493
|
+
// Capture scenario suggestions for ALL flows up-front — before any early returns —
|
|
494
|
+
// so unmapped flows (tests: []) still get their suggested scenarios in the gap report.
|
|
495
|
+
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
496
|
+
const scenarios = entry.missingScenarios
|
|
497
|
+
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
498
|
+
.slice(0, 5);
|
|
499
|
+
if (scenarios.length > 0) {
|
|
500
|
+
scenarioGaps.set(entry.flowId, scenarios);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
447
503
|
const flow = prioritizedFlowsById.get(entry.flowId);
|
|
448
504
|
const confidence = typeof entry.confidence === 'number' ? entry.confidence : undefined;
|
|
449
505
|
const allowedTestsForFlow = candidateSelection.byFlow.get(entry.flowId);
|
|
@@ -462,15 +518,6 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
462
518
|
for (const testPath of valid) {
|
|
463
519
|
matchedTests.add(testPath);
|
|
464
520
|
}
|
|
465
|
-
// Store missing scenarios identified by the AI for this flow.
|
|
466
|
-
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
467
|
-
const scenarios = entry.missingScenarios
|
|
468
|
-
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
469
|
-
.slice(0, 5);
|
|
470
|
-
if (scenarios.length > 0) {
|
|
471
|
-
scenarioGaps.set(entry.flowId, scenarios);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
521
|
}
|
|
475
522
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
476
523
|
// for a file or directory whose name exactly matches the flow ID. This handles flows
|
|
@@ -136,6 +136,21 @@ 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
|
+
// Stop-words excluded from content-fallback keyword matching.
|
|
140
|
+
const CONTENT_FALLBACK_STOP_WORDS = new Set(['and', 'for', 'the', 'to', 'of', 'on', 'at', 'with', 'in', 'a', 'an']);
|
|
141
|
+
// Returns raw (unfiltered) tokens for flows where flowKeywords() returns nothing.
|
|
142
|
+
// Used exclusively for content-title matching when all standard keywords are low-signal.
|
|
143
|
+
// Empty array is returned when the flow already has effective path keywords.
|
|
144
|
+
function contentFallbackKeywords(flow) {
|
|
145
|
+
if (flowKeywords(flow).length > 0) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
return uniqueTokens([
|
|
149
|
+
...tokenize(flow.id || ''),
|
|
150
|
+
...tokenize(flow.name || ''),
|
|
151
|
+
...(flow.keywords || []),
|
|
152
|
+
]).filter((k) => k.length >= 3 && !CONTENT_FALLBACK_STOP_WORDS.has(k));
|
|
153
|
+
}
|
|
139
154
|
// Extract test/describe/it title strings from file content for semantic matching.
|
|
140
155
|
function extractTestTitles(content) {
|
|
141
156
|
const titles = [];
|
|
@@ -211,7 +226,39 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
211
226
|
}
|
|
212
227
|
contentCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
213
228
|
}
|
|
214
|
-
|
|
229
|
+
// Pass 2b: comprehensive fallback for all-low-signal flows.
|
|
230
|
+
// When flowKeywords() is empty (all tokens are low-signal), flowKeywords-based
|
|
231
|
+
// matching in both Pass 1 and Pass 2 yields nothing. As a last resort, search
|
|
232
|
+
// test titles using the raw unfiltered tokens from the flow ID/name, but require
|
|
233
|
+
// ALL tokens to match simultaneously — they are individually weak signals so the
|
|
234
|
+
// full conjunction is needed to establish behavioral coverage evidence.
|
|
235
|
+
const fallbackCandidates = [];
|
|
236
|
+
if (strongCandidates.length === 0 && contentCandidates.length === 0) {
|
|
237
|
+
const fallbackKws = contentFallbackKeywords(flow);
|
|
238
|
+
if (fallbackKws.length > 0) {
|
|
239
|
+
for (const testPath of normalizedTests) {
|
|
240
|
+
const testFile = testByNormalizedPath.get(testPath);
|
|
241
|
+
if (!testFile?.content) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const haystack = extractTestTitles(testFile.content).toLowerCase();
|
|
245
|
+
if (!haystack) {
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const matched = fallbackKws.filter((k) => haystack.includes(k));
|
|
249
|
+
if (matched.length < fallbackKws.length) {
|
|
250
|
+
continue; // all tokens must appear in at least one test title
|
|
251
|
+
}
|
|
252
|
+
fallbackCandidates.push({ path: testPath, score: matched.length, matchedKeywords: matched });
|
|
253
|
+
}
|
|
254
|
+
fallbackCandidates.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const allCandidates = [
|
|
258
|
+
...strongCandidates,
|
|
259
|
+
...contentCandidates.slice(0, perFlowLimit),
|
|
260
|
+
...fallbackCandidates.slice(0, perFlowLimit),
|
|
261
|
+
];
|
|
215
262
|
if (allCandidates.length === 0) {
|
|
216
263
|
// Exact-name fallback: if the flow ID has no effective keywords (all tokens are
|
|
217
264
|
// low-signal, e.g. view_user_group_modal), look for a test whose path contains
|
|
@@ -368,13 +415,12 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
368
415
|
'Rules:',
|
|
369
416
|
'- Keep at most 5 tests per flow.',
|
|
370
417
|
'- Use exact flowId values from FLOWS.',
|
|
371
|
-
'-
|
|
418
|
+
'- Map a test when you have clear evidence it covers the flow — from the file path OR from test titles in the content. Behavioral coverage via test titles is sufficient even when the filename does not exactly match the flow (e.g. search_user_post_spec.js covers search_messages if its titles assert searching for messages). Generic subsystem similarity without behavioral evidence is not enough.',
|
|
372
419
|
'- A flow may only map to tests listed under FLOW_CANDIDATE_SIGNALS for that flow.',
|
|
373
420
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
374
421
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
375
422
|
'- If unsure for a flow, return tests: [].',
|
|
376
|
-
'- For
|
|
377
|
-
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
423
|
+
'- 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.',
|
|
378
424
|
'',
|
|
379
425
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
380
426
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -441,6 +487,16 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
441
487
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
442
488
|
continue;
|
|
443
489
|
}
|
|
490
|
+
// Capture scenario suggestions for ALL flows up-front — before any early returns —
|
|
491
|
+
// so unmapped flows (tests: []) still get their suggested scenarios in the gap report.
|
|
492
|
+
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
493
|
+
const scenarios = entry.missingScenarios
|
|
494
|
+
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
495
|
+
.slice(0, 5);
|
|
496
|
+
if (scenarios.length > 0) {
|
|
497
|
+
scenarioGaps.set(entry.flowId, scenarios);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
444
500
|
const flow = prioritizedFlowsById.get(entry.flowId);
|
|
445
501
|
const confidence = typeof entry.confidence === 'number' ? entry.confidence : undefined;
|
|
446
502
|
const allowedTestsForFlow = candidateSelection.byFlow.get(entry.flowId);
|
|
@@ -459,15 +515,6 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
459
515
|
for (const testPath of valid) {
|
|
460
516
|
matchedTests.add(testPath);
|
|
461
517
|
}
|
|
462
|
-
// Store missing scenarios identified by the AI for this flow.
|
|
463
|
-
if (Array.isArray(entry.missingScenarios) && entry.missingScenarios.length > 0) {
|
|
464
|
-
const scenarios = entry.missingScenarios
|
|
465
|
-
.filter((s) => typeof s === 'string' && s.trim().length > 0)
|
|
466
|
-
.slice(0, 5);
|
|
467
|
-
if (scenarios.length > 0) {
|
|
468
|
-
scenarioGaps.set(entry.flowId, scenarios);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
518
|
}
|
|
472
519
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
473
520
|
// 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.13",
|
|
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",
|