@yasserkhanorg/e2e-agents 0.5.9 → 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;AA2BvD,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;AAiOD,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,CAoN1B"}
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"}
@@ -12,7 +12,7 @@ const PRIORITY_RANK = {
12
12
  P1: 1,
13
13
  P2: 2,
14
14
  };
15
- const MIN_SINGLE_KEYWORD_LENGTH = 8;
15
+ const MIN_SINGLE_KEYWORD_LENGTH = 6;
16
16
  const LOW_SIGNAL_FLOW_KEYWORDS = new Set([
17
17
  'app',
18
18
  'apps',
@@ -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
  }
@@ -194,7 +246,34 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
194
246
  warnings,
195
247
  };
196
248
  }
197
- function buildCoverage(flows, mapped) {
249
+ function readCandidateTestContents(testsRoot, testPaths) {
250
+ const result = [];
251
+ const maxCharsPerFile = 6000;
252
+ const maxTotalChars = 24000;
253
+ let totalChars = 0;
254
+ for (const testPath of testPaths.slice(0, 6)) {
255
+ if (totalChars >= maxTotalChars) {
256
+ break;
257
+ }
258
+ const candidates = (0, path_1.isAbsolute)(testPath) ? [testPath] : [(0, path_1.join)(testsRoot, testPath)];
259
+ for (const fullPath of candidates) {
260
+ if (!(0, fs_1.existsSync)(fullPath)) {
261
+ continue;
262
+ }
263
+ const content = (0, fs_1.readFileSync)(fullPath, 'utf-8').trim();
264
+ if (!content) {
265
+ continue;
266
+ }
267
+ const remaining = Math.max(0, maxTotalChars - totalChars);
268
+ const clipped = content.slice(0, Math.min(maxCharsPerFile, remaining));
269
+ result.push({ path: testPath, content: clipped });
270
+ totalChars += clipped.length;
271
+ break;
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ function buildCoverage(flows, mapped, scenarioGaps) {
198
277
  return flows.map((flow) => ({
199
278
  flowId: flow.id,
200
279
  flowName: flow.name,
@@ -202,6 +281,7 @@ function buildCoverage(flows, mapped) {
202
281
  coveredBy: mapped.get(flow.id) || [],
203
282
  score: (mapped.get(flow.id) || []).length,
204
283
  source: 'ai',
284
+ missingScenarios: scenarioGaps.get(flow.id) || [],
205
285
  }));
206
286
  }
207
287
  function providerFor(config) {
@@ -255,6 +335,12 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
255
335
  if (contextFiles.length === 0) {
256
336
  warnings.push('AI mapping context files were not found; continuing without optional markdown context.');
257
337
  }
338
+ // Read candidate test file contents so the AI can reason about what scenarios
339
+ // are already covered and which ones are still missing.
340
+ const candidateTestContents = readCandidateTestContents(testsRoot, candidateTests);
341
+ const testContentBlock = candidateTestContents.length > 0
342
+ ? candidateTestContents.map((entry) => `### Test: ${entry.path}\n\`\`\`typescript\n${entry.content}\n\`\`\``).join('\n\n')
343
+ : 'No candidate test file contents could be read.';
258
344
  let provider;
259
345
  try {
260
346
  provider = config.provider === 'auto'
@@ -280,7 +366,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
280
366
  'Only use tests from CANDIDATE_TESTS. Never invent paths.',
281
367
  'Prefer no mapping over a broad or generic mapping.',
282
368
  'Return strict JSON only with this shape:',
283
- '{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0}]}',
369
+ '{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0,"missingScenarios":["scenario description"]}]}',
284
370
  '',
285
371
  'Rules:',
286
372
  '- Keep at most 5 tests per flow.',
@@ -290,6 +376,8 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
290
376
  '- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
291
377
  '- If the candidate path overlap is weak or ambiguous, return tests: [].',
292
378
  '- 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.',
293
381
  '',
294
382
  `FLOWS (${prioritizedFlows.length}):`,
295
383
  JSON.stringify(prioritizedFlows.map((flow) => ({
@@ -307,6 +395,9 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
307
395
  `FLOW_CANDIDATE_SIGNALS (${candidateSelection.evidence.length}):`,
308
396
  JSON.stringify(candidateSelection.evidence, null, 2),
309
397
  '',
398
+ `CANDIDATE_TEST_CONTENT (${candidateTestContents.length} file(s)):`,
399
+ testContentBlock,
400
+ '',
310
401
  contextBlock,
311
402
  ].join('\n');
312
403
  let parsed = null;
@@ -347,6 +438,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
347
438
  const allowedFlowIds = new Set(prioritizedFlows.map((flow) => flow.id));
348
439
  const prioritizedFlowsById = new Map(prioritizedFlows.map((flow) => [flow.id, flow]));
349
440
  const mapped = new Map();
441
+ const scenarioGaps = new Map();
350
442
  const matchedTests = new Set();
351
443
  for (const entry of parsed.mappings) {
352
444
  if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
@@ -370,6 +462,15 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
370
462
  for (const testPath of valid) {
371
463
  matchedTests.add(testPath);
372
464
  }
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
+ }
373
474
  }
374
475
  // Post-AI exact-name fallback: for any flow still uncovered, search all test paths
375
476
  // for a file or directory whose name exactly matches the flow ID. This handles flows
@@ -389,7 +490,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
389
490
  matchedTests.add(exactMatch);
390
491
  }
391
492
  }
392
- const coverage = buildCoverage(flows, mapped);
493
+ const coverage = buildCoverage(flows, mapped, scenarioGaps);
393
494
  if (mapped.size === 0) {
394
495
  warnings.push(`AI mapping returned no valid test mappings (${provider.name}).`);
395
496
  }
@@ -40,6 +40,8 @@ export interface FlowImpact {
40
40
  priorityFloor?: FlowPriority;
41
41
  subsystemRiskBoost?: number;
42
42
  subsystemRiskRules?: string[];
43
+ existingTests?: string[];
44
+ missingScenarios?: string[];
43
45
  }
44
46
  export interface ImpactAnalysisResult {
45
47
  files: FileAnalysis[];
@@ -1 +1 @@
1
- {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/agent/analysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAE,YAAY,EAAa,MAAM,aAAa,CAAC;AACvE,OAAO,EAAqE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAC,MAAM,YAAY,CAAC;AAY9H,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,aAAa,CAAC,EAAE;QACZ,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,YAAY,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACL;AAED,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE;QACX,MAAM,EAAE,KAAK,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,OAAO,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AA6HD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,oBAAoB,CA+IhH;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCnH"}
1
+ {"version":3,"file":"analysis.d.ts","sourceRoot":"","sources":["../../src/agent/analysis.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,WAAW,EAAE,YAAY,EAAa,MAAM,aAAa,CAAC;AACvE,OAAO,EAAqE,KAAK,WAAW,EAAE,KAAK,OAAO,EAAC,MAAM,YAAY,CAAC;AAY9H,MAAM,MAAM,YAAY,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C,MAAM,WAAW,YAAY;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;IACjB,eAAe,EAAE,OAAO,CAAC;IACzB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAAC;IAC5B,QAAQ,EAAE,YAAY,EAAE,CAAC;IACzB,KAAK,EAAE,OAAO,EAAE,CAAC;IACjB,aAAa,CAAC,EAAE;QACZ,KAAK,EAAE,MAAM,EAAE,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;QACnB,aAAa,CAAC,EAAE,YAAY,CAAC;QAC7B,OAAO,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACL;AAED,MAAM,WAAW,UAAU;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC;IAClB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,aAAa,CAAC,EAAE,YAAY,CAAC;IAC7B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,aAAa,EAAE;QACX,MAAM,EAAE,KAAK,CAAC;QACd,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,OAAO,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;QACrB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACxB,CAAC;CACL;AA6HD,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,WAAW,GAAG,oBAAoB,CA+IhH;AAED,wBAAgB,cAAc,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAE5D;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,SAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAsCnH"}
@@ -18,6 +18,8 @@ export interface GapDetail {
18
18
  priority: string;
19
19
  reasons: string[];
20
20
  files: string[];
21
+ existingTests?: string[];
22
+ missingScenarios?: string[];
21
23
  }
22
24
  export interface CoveredFlowSummary {
23
25
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/agent/plan.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/agent/plan.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,aAAa,CAAC;AAE9C,MAAM,MAAM,iBAAiB,GAAG,OAAO,GAAG,UAAU,GAAG,MAAM,CAAC;AAC9D,MAAM,MAAM,QAAQ,GAAG,SAAS,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAEtE,MAAM,WAAW,gBAAgB;IAC7B,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,OAAO,EAAE,YAAY,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC5B,MAAM,EAAE,QAAQ,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,UAAU;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,YAAY,EAAE,kBAAkB,EAAE,CAAC;IACnC,MAAM,EAAE,gBAAgB,CAAC;IACzB,QAAQ,EAAE,eAAe,CAAC;IAC1B,WAAW,EAAE;QACT,IAAI,EAAE,YAAY,CAAC,iBAAiB,CAAC,CAAC;QACtC,cAAc,EAAE,QAAQ,EAAE,CAAC;QAC3B,aAAa,EAAE,OAAO,CAAC;QACvB,UAAU,EAAE,OAAO,CAAC;QACpB,OAAO,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,QAAQ,CAAC,EAAE;QACP,KAAK,CAAC,EAAE;YACJ,wBAAwB,EAAE,KAAK,CAAC;gBAC5B,IAAI,EAAE,MAAM,CAAC;gBACb,SAAS,EAAE,MAAM,CAAC;gBAClB,WAAW,CAAC,EAAE,MAAM,CAAC;gBACrB,YAAY,CAAC,EAAE,MAAM,CAAC;gBACtB,KAAK,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;gBACjC,SAAS,CAAC,EAAE,MAAM,CAAC;gBACnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;gBAClB,UAAU,CAAC,EAAE,OAAO,CAAC;gBACrB,eAAe,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,kBAAkB,CAAC;gBACzD,aAAa,CAAC,EAAE,MAAM,CAAC;aAC1B,CAAC,CAAC;YACH,2BAA2B,EAAE,MAAM,EAAE,CAAC;YACtC,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;SAC5B,CAAC;QACF,YAAY,CAAC,EAAE;YACX,MAAM,EAAE,KAAK,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;gBAAC,OAAO,CAAC,EAAE,MAAM,CAAA;aAAC,CAAC,CAAC;YAClF,QAAQ,EAAE,KAAK,CAAC;gBAAC,IAAI,EAAE,MAAM,CAAC;gBAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;gBAAC,OAAO,CAAC,EAAE,MAAM,CAAA;aAAC,CAAC,CAAC;SACvF,CAAC;QACF,WAAW,CAAC,EAAE;YACV,SAAS,EAAE,MAAM,CAAC;YAClB,MAAM,EAAE,MAAM,CAAC;YACf,iBAAiB,EAAE,MAAM,CAAC;SAC7B,CAAC;KACL,CAAC;IACF,WAAW,CAAC,EAAE;QACV,iCAAiC,EAAE,OAAO,CAAC;QAC3C,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;QAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;QAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,OAAO,EAAE;QACL,YAAY,EAAE,MAAM,CAAC;QACrB,aAAa,EAAE,MAAM,CAAC;QACtB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,kBAAkB,EAAE,MAAM,CAAC;QAC3B,QAAQ,EAAE,MAAM,CAAC;KACpB,CAAC;CACL;AAmPD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,UAAU,GAAG,UAAU,CAKnE;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,UAAU,EAAE,cAAc,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,UAAU,CAgEhH;AAED,wBAAgB,sBAAsB,CAClC,IAAI,EAAE,UAAU,EAChB,OAAO,EAAE;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAC,GACtF,UAAU,CAuBZ;AAED,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,MAAM,CAMzE;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,UAAU,GAAG,MAAM,CA2BhE;AAED,MAAM,WAAW,eAAe;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,QAAQ,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,YAAY,CAAC,iBAAiB,CAAC,CAAC;IACjD,qBAAqB,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,WAAW,kBAAkB;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACnC,QAAQ,EAAE,MAAM,CAAC,iBAAiB,EAAE,MAAM,CAAC,CAAC;IAC5C,uBAAuB,EAAE,MAAM,CAAC;IAChC,YAAY,EAAE,MAAM,CAAC;CACxB;AAqBD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,WAAW,EAAE,MAAM,CAAA;CAAC,CAkE9G;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,SAAiC,GAAG,MAAM,CAMvH"}
@@ -265,6 +265,8 @@ function buildPlanFromImpactReport(impact, policyOverride) {
265
265
  priority: flow.priority,
266
266
  reasons: (flow.reasons || []).slice(0, 5),
267
267
  files: (flow.files || []).slice(0, 6),
268
+ existingTests: flow.existingTests && flow.existingTests.length > 0 ? flow.existingTests.slice(0, 3) : undefined,
269
+ missingScenarios: flow.missingScenarios && flow.missingScenarios.length > 0 ? flow.missingScenarios.slice(0, 5) : undefined,
268
270
  }));
269
271
  const coveredFlowIds = new Set(impact.gaps.map((g) => g.id));
270
272
  const coveredFlows = impact.coverage
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AA+R7C,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,OAAO,CAAC;CAClB;AAYD,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAuTzF;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAoUtF"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/agent/runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,aAAa,CAAC;AA+S7C,MAAM,WAAW,UAAU;IACvB,KAAK,EAAE,OAAO,CAAC;CAClB;AAYD,wBAAsB,SAAS,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAwTzF;AAED,wBAAsB,MAAM,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAqUtF"}
@@ -34,13 +34,29 @@ function ensureAppRoot(path) {
34
34
  throw new Error(`App path does not exist: ${path}`);
35
35
  }
36
36
  }
37
- function computeGaps(flows, coverageMap) {
38
- return flows.filter((flow) => {
37
+ function computeGaps(flows, coverageMap, coverage) {
38
+ const coverageByFlowId = new Map((coverage || []).map((c) => [c.flowId, c]));
39
+ return flows
40
+ .filter((flow) => {
39
41
  if (flow.priority !== 'P0' && flow.priority !== 'P1') {
40
42
  return false;
41
43
  }
42
44
  const coveredBy = coverageMap.get(flow.id) || [];
43
- return coveredBy.length === 0;
45
+ if (coveredBy.length === 0) {
46
+ return true; // no tests at all
47
+ }
48
+ // Also flag as a gap if tests exist but the AI identified missing scenarios.
49
+ const flowCoverage = coverageByFlowId.get(flow.id);
50
+ return (flowCoverage?.missingScenarios || []).length > 0;
51
+ })
52
+ .map((flow) => {
53
+ const coveredBy = coverageMap.get(flow.id) || [];
54
+ const flowCoverage = coverageByFlowId.get(flow.id);
55
+ return {
56
+ ...flow,
57
+ existingTests: coveredBy.length > 0 ? coveredBy : undefined,
58
+ missingScenarios: flowCoverage?.missingScenarios?.length ? flowCoverage.missingScenarios : undefined,
59
+ };
44
60
  });
45
61
  }
46
62
  function normalizeChangedFiles(appRoot, files) {
@@ -460,7 +476,8 @@ async function runImpact(_config, _options) {
460
476
  for (const entry of coverage) {
461
477
  coverageMap.set(entry.flowId, entry.coveredBy);
462
478
  }
463
- gaps = computeGaps(flows, coverageMap);
479
+ // Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
480
+ gaps = computeGaps(flows, coverageMap, coverage);
464
481
  }
465
482
  if (Date.now() <= deadline) {
466
483
  testSuggestions = (0, gap_suggestions_js_1.buildGapTestSuggestions)(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
@@ -750,7 +767,8 @@ async function runGap(_config, _options) {
750
767
  for (const entry of coverage) {
751
768
  coverageMap.set(entry.flowId, entry.coveredBy);
752
769
  }
753
- gaps = computeGaps(flows, coverageMap);
770
+ // Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
771
+ gaps = computeGaps(flows, coverageMap, coverage);
754
772
  }
755
773
  if (Date.now() <= deadline) {
756
774
  testSuggestions = (0, gap_suggestions_js_1.buildGapTestSuggestions)(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
@@ -11,6 +11,7 @@ export interface FlowCoverage {
11
11
  score: number;
12
12
  expectedTests?: string[];
13
13
  source?: 'catalog' | 'traceability' | 'heuristic' | 'ai';
14
+ missingScenarios?: string[];
14
15
  }
15
16
  export declare function discoverTests(appRoot: string, patterns: string[]): TestFile[];
16
17
  export declare function mapTestsToFlows(flows: FlowImpact[], tests: TestFile[]): FlowCoverage[];
@@ -1 +1 @@
1
- {"version":3,"file":"tests.d.ts","sourceRoot":"","sources":["../../src/agent/tests.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAG9C,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,WAAW,GAAG,IAAI,CAAC;CAC5D;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAkB7E;AAUD,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,YAAY,EAAE,CAgCtF;AAuBD,wBAAgB,sBAAsB,CAClC,KAAK,EAAE,UAAU,EAAE,EACnB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,YAAY,EAAE,CA8BhB"}
1
+ {"version":3,"file":"tests.d.ts","sourceRoot":"","sources":["../../src/agent/tests.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,eAAe,CAAC;AAG9C,MAAM,WAAW,QAAQ;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,MAAM,WAAW,YAAY;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;IACzB,MAAM,CAAC,EAAE,SAAS,GAAG,cAAc,GAAG,WAAW,GAAG,IAAI,CAAC;IACzD,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC/B;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,QAAQ,EAAE,CAkB7E;AAUD,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,YAAY,EAAE,CAgCtF;AAuBD,wBAAgB,sBAAsB,CAClC,KAAK,EAAE,UAAU,EAAE,EACnB,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,GACnC,YAAY,EAAE,CA8BhB"}
@@ -9,7 +9,7 @@ const PRIORITY_RANK = {
9
9
  P1: 1,
10
10
  P2: 2,
11
11
  };
12
- const MIN_SINGLE_KEYWORD_LENGTH = 8;
12
+ const MIN_SINGLE_KEYWORD_LENGTH = 6;
13
13
  const LOW_SIGNAL_FLOW_KEYWORDS = new Set([
14
14
  'app',
15
15
  'apps',
@@ -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
  }
@@ -191,7 +243,34 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
191
243
  warnings,
192
244
  };
193
245
  }
194
- function buildCoverage(flows, mapped) {
246
+ function readCandidateTestContents(testsRoot, testPaths) {
247
+ const result = [];
248
+ const maxCharsPerFile = 6000;
249
+ const maxTotalChars = 24000;
250
+ let totalChars = 0;
251
+ for (const testPath of testPaths.slice(0, 6)) {
252
+ if (totalChars >= maxTotalChars) {
253
+ break;
254
+ }
255
+ const candidates = isAbsolute(testPath) ? [testPath] : [join(testsRoot, testPath)];
256
+ for (const fullPath of candidates) {
257
+ if (!existsSync(fullPath)) {
258
+ continue;
259
+ }
260
+ const content = readFileSync(fullPath, 'utf-8').trim();
261
+ if (!content) {
262
+ continue;
263
+ }
264
+ const remaining = Math.max(0, maxTotalChars - totalChars);
265
+ const clipped = content.slice(0, Math.min(maxCharsPerFile, remaining));
266
+ result.push({ path: testPath, content: clipped });
267
+ totalChars += clipped.length;
268
+ break;
269
+ }
270
+ }
271
+ return result;
272
+ }
273
+ function buildCoverage(flows, mapped, scenarioGaps) {
195
274
  return flows.map((flow) => ({
196
275
  flowId: flow.id,
197
276
  flowName: flow.name,
@@ -199,6 +278,7 @@ function buildCoverage(flows, mapped) {
199
278
  coveredBy: mapped.get(flow.id) || [],
200
279
  score: (mapped.get(flow.id) || []).length,
201
280
  source: 'ai',
281
+ missingScenarios: scenarioGaps.get(flow.id) || [],
202
282
  }));
203
283
  }
204
284
  function providerFor(config) {
@@ -252,6 +332,12 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
252
332
  if (contextFiles.length === 0) {
253
333
  warnings.push('AI mapping context files were not found; continuing without optional markdown context.');
254
334
  }
335
+ // Read candidate test file contents so the AI can reason about what scenarios
336
+ // are already covered and which ones are still missing.
337
+ const candidateTestContents = readCandidateTestContents(testsRoot, candidateTests);
338
+ const testContentBlock = candidateTestContents.length > 0
339
+ ? candidateTestContents.map((entry) => `### Test: ${entry.path}\n\`\`\`typescript\n${entry.content}\n\`\`\``).join('\n\n')
340
+ : 'No candidate test file contents could be read.';
255
341
  let provider;
256
342
  try {
257
343
  provider = config.provider === 'auto'
@@ -277,7 +363,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
277
363
  'Only use tests from CANDIDATE_TESTS. Never invent paths.',
278
364
  'Prefer no mapping over a broad or generic mapping.',
279
365
  'Return strict JSON only with this shape:',
280
- '{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0}]}',
366
+ '{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0,"missingScenarios":["scenario description"]}]}',
281
367
  '',
282
368
  'Rules:',
283
369
  '- Keep at most 5 tests per flow.',
@@ -287,6 +373,8 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
287
373
  '- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
288
374
  '- If the candidate path overlap is weak or ambiguous, return tests: [].',
289
375
  '- 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.',
290
378
  '',
291
379
  `FLOWS (${prioritizedFlows.length}):`,
292
380
  JSON.stringify(prioritizedFlows.map((flow) => ({
@@ -304,6 +392,9 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
304
392
  `FLOW_CANDIDATE_SIGNALS (${candidateSelection.evidence.length}):`,
305
393
  JSON.stringify(candidateSelection.evidence, null, 2),
306
394
  '',
395
+ `CANDIDATE_TEST_CONTENT (${candidateTestContents.length} file(s)):`,
396
+ testContentBlock,
397
+ '',
307
398
  contextBlock,
308
399
  ].join('\n');
309
400
  let parsed = null;
@@ -344,6 +435,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
344
435
  const allowedFlowIds = new Set(prioritizedFlows.map((flow) => flow.id));
345
436
  const prioritizedFlowsById = new Map(prioritizedFlows.map((flow) => [flow.id, flow]));
346
437
  const mapped = new Map();
438
+ const scenarioGaps = new Map();
347
439
  const matchedTests = new Set();
348
440
  for (const entry of parsed.mappings) {
349
441
  if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
@@ -367,6 +459,15 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
367
459
  for (const testPath of valid) {
368
460
  matchedTests.add(testPath);
369
461
  }
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
+ }
370
471
  }
371
472
  // Post-AI exact-name fallback: for any flow still uncovered, search all test paths
372
473
  // for a file or directory whose name exactly matches the flow ID. This handles flows
@@ -386,7 +487,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
386
487
  matchedTests.add(exactMatch);
387
488
  }
388
489
  }
389
- const coverage = buildCoverage(flows, mapped);
490
+ const coverage = buildCoverage(flows, mapped, scenarioGaps);
390
491
  if (mapped.size === 0) {
391
492
  warnings.push(`AI mapping returned no valid test mappings (${provider.name}).`);
392
493
  }
@@ -256,6 +256,8 @@ export function buildPlanFromImpactReport(impact, policyOverride) {
256
256
  priority: flow.priority,
257
257
  reasons: (flow.reasons || []).slice(0, 5),
258
258
  files: (flow.files || []).slice(0, 6),
259
+ existingTests: flow.existingTests && flow.existingTests.length > 0 ? flow.existingTests.slice(0, 3) : undefined,
260
+ missingScenarios: flow.missingScenarios && flow.missingScenarios.length > 0 ? flow.missingScenarios.slice(0, 5) : undefined,
259
261
  }));
260
262
  const coveredFlowIds = new Set(impact.gaps.map((g) => g.id));
261
263
  const coveredFlows = impact.coverage
@@ -30,13 +30,29 @@ function ensureAppRoot(path) {
30
30
  throw new Error(`App path does not exist: ${path}`);
31
31
  }
32
32
  }
33
- function computeGaps(flows, coverageMap) {
34
- return flows.filter((flow) => {
33
+ function computeGaps(flows, coverageMap, coverage) {
34
+ const coverageByFlowId = new Map((coverage || []).map((c) => [c.flowId, c]));
35
+ return flows
36
+ .filter((flow) => {
35
37
  if (flow.priority !== 'P0' && flow.priority !== 'P1') {
36
38
  return false;
37
39
  }
38
40
  const coveredBy = coverageMap.get(flow.id) || [];
39
- return coveredBy.length === 0;
41
+ if (coveredBy.length === 0) {
42
+ return true; // no tests at all
43
+ }
44
+ // Also flag as a gap if tests exist but the AI identified missing scenarios.
45
+ const flowCoverage = coverageByFlowId.get(flow.id);
46
+ return (flowCoverage?.missingScenarios || []).length > 0;
47
+ })
48
+ .map((flow) => {
49
+ const coveredBy = coverageMap.get(flow.id) || [];
50
+ const flowCoverage = coverageByFlowId.get(flow.id);
51
+ return {
52
+ ...flow,
53
+ existingTests: coveredBy.length > 0 ? coveredBy : undefined,
54
+ missingScenarios: flowCoverage?.missingScenarios?.length ? flowCoverage.missingScenarios : undefined,
55
+ };
40
56
  });
41
57
  }
42
58
  function normalizeChangedFiles(appRoot, files) {
@@ -456,7 +472,8 @@ export async function runImpact(_config, _options) {
456
472
  for (const entry of coverage) {
457
473
  coverageMap.set(entry.flowId, entry.coveredBy);
458
474
  }
459
- gaps = computeGaps(flows, coverageMap);
475
+ // Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
476
+ gaps = computeGaps(flows, coverageMap, coverage);
460
477
  }
461
478
  if (Date.now() <= deadline) {
462
479
  testSuggestions = buildGapTestSuggestions(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
@@ -746,7 +763,8 @@ export async function runGap(_config, _options) {
746
763
  for (const entry of coverage) {
747
764
  coverageMap.set(entry.flowId, entry.coveredBy);
748
765
  }
749
- gaps = computeGaps(flows, coverageMap);
766
+ // Pass the full coverage array so partial gaps (tests exist but missing scenarios) are included.
767
+ gaps = computeGaps(flows, coverageMap, coverage);
750
768
  }
751
769
  if (Date.now() <= deadline) {
752
770
  testSuggestions = buildGapTestSuggestions(testsRoot, gaps, frameworkDetection.framework, testPatterns.patterns);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "0.5.9",
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",