@yasserkhanorg/e2e-agents 0.5.10 → 0.5.11

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;AA8PD,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;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"}
@@ -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
- if (strongCandidates.length === 0) {
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(strongCandidates.map((candidate) => candidate.path)));
185
- evidence.push({ flowId: flow.id, candidates: strongCandidates });
186
- for (const candidate of strongCandidates) {
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
  }
@@ -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
- if (strongCandidates.length === 0) {
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(strongCandidates.map((candidate) => candidate.path)));
182
- evidence.push({ flowId: flow.id, candidates: strongCandidates });
183
- for (const candidate of strongCandidates) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "0.5.10",
3
+ "version": "0.5.11",
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",