@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;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,CA0O1B"}
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"}
@@ -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
- const allCandidates = [...strongCandidates, ...contentCandidates.slice(0, perFlowLimit)];
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
- '- Only map a test when its path clearly matches the flow scenario. Generic subsystem similarity is not enough.',
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 every flow you map to tests, read CANDIDATE_TEST_CONTENT and list up to 5 specific test scenarios NOT yet covered by those tests. Write each as a short imperative statement (e.g. "Search messages with date filter"). Only include missingScenarios you can clearly identify; return [] if unsure.',
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
- const allCandidates = [...strongCandidates, ...contentCandidates.slice(0, perFlowLimit)];
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
- '- Only map a test when its path clearly matches the flow scenario. Generic subsystem similarity is not enough.',
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 every flow you map to tests, read CANDIDATE_TEST_CONTENT and list up to 5 specific test scenarios NOT yet covered by those tests. Write each as a short imperative statement (e.g. "Search messages with date filter"). Only include missingScenarios you can clearly identify; return [] if unsure.',
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.11",
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",