@yasserkhanorg/e2e-agents 0.5.8 → 0.5.10
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.
- package/dist/agent/ai_mapping.d.ts.map +1 -1
- package/dist/agent/ai_mapping.js +53 -4
- package/dist/agent/analysis.d.ts +2 -0
- package/dist/agent/analysis.d.ts.map +1 -1
- package/dist/agent/plan.d.ts +2 -0
- package/dist/agent/plan.d.ts.map +1 -1
- package/dist/agent/plan.js +9 -0
- package/dist/agent/runner.d.ts.map +1 -1
- package/dist/agent/runner.js +23 -5
- package/dist/agent/tests.d.ts +1 -0
- package/dist/agent/tests.d.ts.map +1 -1
- package/dist/esm/agent/ai_mapping.js +53 -4
- package/dist/esm/agent/plan.js +9 -0
- package/dist/esm/agent/runner.js +23 -5
- package/package.json +1 -1
|
@@ -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;
|
|
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"}
|
package/dist/agent/ai_mapping.js
CHANGED
|
@@ -12,7 +12,7 @@ const PRIORITY_RANK = {
|
|
|
12
12
|
P1: 1,
|
|
13
13
|
P2: 2,
|
|
14
14
|
};
|
|
15
|
-
const MIN_SINGLE_KEYWORD_LENGTH =
|
|
15
|
+
const MIN_SINGLE_KEYWORD_LENGTH = 6;
|
|
16
16
|
const LOW_SIGNAL_FLOW_KEYWORDS = new Set([
|
|
17
17
|
'app',
|
|
18
18
|
'apps',
|
|
@@ -194,7 +194,34 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
194
194
|
warnings,
|
|
195
195
|
};
|
|
196
196
|
}
|
|
197
|
-
function
|
|
197
|
+
function readCandidateTestContents(testsRoot, testPaths) {
|
|
198
|
+
const result = [];
|
|
199
|
+
const maxCharsPerFile = 6000;
|
|
200
|
+
const maxTotalChars = 24000;
|
|
201
|
+
let totalChars = 0;
|
|
202
|
+
for (const testPath of testPaths.slice(0, 6)) {
|
|
203
|
+
if (totalChars >= maxTotalChars) {
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
const candidates = (0, path_1.isAbsolute)(testPath) ? [testPath] : [(0, path_1.join)(testsRoot, testPath)];
|
|
207
|
+
for (const fullPath of candidates) {
|
|
208
|
+
if (!(0, fs_1.existsSync)(fullPath)) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const content = (0, fs_1.readFileSync)(fullPath, 'utf-8').trim();
|
|
212
|
+
if (!content) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const remaining = Math.max(0, maxTotalChars - totalChars);
|
|
216
|
+
const clipped = content.slice(0, Math.min(maxCharsPerFile, remaining));
|
|
217
|
+
result.push({ path: testPath, content: clipped });
|
|
218
|
+
totalChars += clipped.length;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
function buildCoverage(flows, mapped, scenarioGaps) {
|
|
198
225
|
return flows.map((flow) => ({
|
|
199
226
|
flowId: flow.id,
|
|
200
227
|
flowName: flow.name,
|
|
@@ -202,6 +229,7 @@ function buildCoverage(flows, mapped) {
|
|
|
202
229
|
coveredBy: mapped.get(flow.id) || [],
|
|
203
230
|
score: (mapped.get(flow.id) || []).length,
|
|
204
231
|
source: 'ai',
|
|
232
|
+
missingScenarios: scenarioGaps.get(flow.id) || [],
|
|
205
233
|
}));
|
|
206
234
|
}
|
|
207
235
|
function providerFor(config) {
|
|
@@ -255,6 +283,12 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
255
283
|
if (contextFiles.length === 0) {
|
|
256
284
|
warnings.push('AI mapping context files were not found; continuing without optional markdown context.');
|
|
257
285
|
}
|
|
286
|
+
// Read candidate test file contents so the AI can reason about what scenarios
|
|
287
|
+
// are already covered and which ones are still missing.
|
|
288
|
+
const candidateTestContents = readCandidateTestContents(testsRoot, candidateTests);
|
|
289
|
+
const testContentBlock = candidateTestContents.length > 0
|
|
290
|
+
? candidateTestContents.map((entry) => `### Test: ${entry.path}\n\`\`\`typescript\n${entry.content}\n\`\`\``).join('\n\n')
|
|
291
|
+
: 'No candidate test file contents could be read.';
|
|
258
292
|
let provider;
|
|
259
293
|
try {
|
|
260
294
|
provider = config.provider === 'auto'
|
|
@@ -280,7 +314,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
280
314
|
'Only use tests from CANDIDATE_TESTS. Never invent paths.',
|
|
281
315
|
'Prefer no mapping over a broad or generic mapping.',
|
|
282
316
|
'Return strict JSON only with this shape:',
|
|
283
|
-
'{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0}]}',
|
|
317
|
+
'{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0,"missingScenarios":["scenario description"]}]}',
|
|
284
318
|
'',
|
|
285
319
|
'Rules:',
|
|
286
320
|
'- Keep at most 5 tests per flow.',
|
|
@@ -290,6 +324,8 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
290
324
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
291
325
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
292
326
|
'- If unsure for a flow, return tests: [].',
|
|
327
|
+
'- 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.',
|
|
328
|
+
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
293
329
|
'',
|
|
294
330
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
295
331
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -307,6 +343,9 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
307
343
|
`FLOW_CANDIDATE_SIGNALS (${candidateSelection.evidence.length}):`,
|
|
308
344
|
JSON.stringify(candidateSelection.evidence, null, 2),
|
|
309
345
|
'',
|
|
346
|
+
`CANDIDATE_TEST_CONTENT (${candidateTestContents.length} file(s)):`,
|
|
347
|
+
testContentBlock,
|
|
348
|
+
'',
|
|
310
349
|
contextBlock,
|
|
311
350
|
].join('\n');
|
|
312
351
|
let parsed = null;
|
|
@@ -347,6 +386,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
347
386
|
const allowedFlowIds = new Set(prioritizedFlows.map((flow) => flow.id));
|
|
348
387
|
const prioritizedFlowsById = new Map(prioritizedFlows.map((flow) => [flow.id, flow]));
|
|
349
388
|
const mapped = new Map();
|
|
389
|
+
const scenarioGaps = new Map();
|
|
350
390
|
const matchedTests = new Set();
|
|
351
391
|
for (const entry of parsed.mappings) {
|
|
352
392
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
@@ -370,6 +410,15 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
370
410
|
for (const testPath of valid) {
|
|
371
411
|
matchedTests.add(testPath);
|
|
372
412
|
}
|
|
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
|
+
}
|
|
373
422
|
}
|
|
374
423
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
375
424
|
// for a file or directory whose name exactly matches the flow ID. This handles flows
|
|
@@ -389,7 +438,7 @@ async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests) {
|
|
|
389
438
|
matchedTests.add(exactMatch);
|
|
390
439
|
}
|
|
391
440
|
}
|
|
392
|
-
const coverage = buildCoverage(flows, mapped);
|
|
441
|
+
const coverage = buildCoverage(flows, mapped, scenarioGaps);
|
|
393
442
|
if (mapped.size === 0) {
|
|
394
443
|
warnings.push(`AI mapping returned no valid test mappings (${provider.name}).`);
|
|
395
444
|
}
|
package/dist/agent/analysis.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/agent/plan.d.ts
CHANGED
package/dist/agent/plan.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
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;
|
|
1
|
+
{"version":3,"file":"plan.d.ts","sourceRoot":"","sources":["../../src/agent/plan.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/plan.js
CHANGED
|
@@ -169,6 +169,13 @@ function buildDecision(runSet, confidence, impact, policy) {
|
|
|
169
169
|
summary: `Detected ${impact.gaps.length} uncovered P0/P1 flow(s). Add or update tests before merge.`,
|
|
170
170
|
};
|
|
171
171
|
}
|
|
172
|
+
if (impact.changedFiles.length === 0 && impact.flows.length === 0) {
|
|
173
|
+
return {
|
|
174
|
+
action: 'safe-to-merge',
|
|
175
|
+
title: 'Safe to merge',
|
|
176
|
+
summary: 'No app file changes detected — no E2E coverage required for this change set.',
|
|
177
|
+
};
|
|
178
|
+
}
|
|
172
179
|
if (runSet === 'smoke' && confidence >= policy.safeMergeMinConfidence && impact.warnings.length === 0) {
|
|
173
180
|
return {
|
|
174
181
|
action: 'safe-to-merge',
|
|
@@ -258,6 +265,8 @@ function buildPlanFromImpactReport(impact, policyOverride) {
|
|
|
258
265
|
priority: flow.priority,
|
|
259
266
|
reasons: (flow.reasons || []).slice(0, 5),
|
|
260
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,
|
|
261
270
|
}));
|
|
262
271
|
const coveredFlowIds = new Set(impact.gaps.map((g) => g.id));
|
|
263
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+
|
|
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"}
|
package/dist/agent/runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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);
|
package/dist/agent/tests.d.ts
CHANGED
|
@@ -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;
|
|
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 =
|
|
12
|
+
const MIN_SINGLE_KEYWORD_LENGTH = 6;
|
|
13
13
|
const LOW_SIGNAL_FLOW_KEYWORDS = new Set([
|
|
14
14
|
'app',
|
|
15
15
|
'apps',
|
|
@@ -191,7 +191,34 @@ function selectCandidateTests(flows, tests, maxCandidateTests) {
|
|
|
191
191
|
warnings,
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
|
-
function
|
|
194
|
+
function readCandidateTestContents(testsRoot, testPaths) {
|
|
195
|
+
const result = [];
|
|
196
|
+
const maxCharsPerFile = 6000;
|
|
197
|
+
const maxTotalChars = 24000;
|
|
198
|
+
let totalChars = 0;
|
|
199
|
+
for (const testPath of testPaths.slice(0, 6)) {
|
|
200
|
+
if (totalChars >= maxTotalChars) {
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
const candidates = isAbsolute(testPath) ? [testPath] : [join(testsRoot, testPath)];
|
|
204
|
+
for (const fullPath of candidates) {
|
|
205
|
+
if (!existsSync(fullPath)) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const content = readFileSync(fullPath, 'utf-8').trim();
|
|
209
|
+
if (!content) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
const remaining = Math.max(0, maxTotalChars - totalChars);
|
|
213
|
+
const clipped = content.slice(0, Math.min(maxCharsPerFile, remaining));
|
|
214
|
+
result.push({ path: testPath, content: clipped });
|
|
215
|
+
totalChars += clipped.length;
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
function buildCoverage(flows, mapped, scenarioGaps) {
|
|
195
222
|
return flows.map((flow) => ({
|
|
196
223
|
flowId: flow.id,
|
|
197
224
|
flowName: flow.name,
|
|
@@ -199,6 +226,7 @@ function buildCoverage(flows, mapped) {
|
|
|
199
226
|
coveredBy: mapped.get(flow.id) || [],
|
|
200
227
|
score: (mapped.get(flow.id) || []).length,
|
|
201
228
|
source: 'ai',
|
|
229
|
+
missingScenarios: scenarioGaps.get(flow.id) || [],
|
|
202
230
|
}));
|
|
203
231
|
}
|
|
204
232
|
function providerFor(config) {
|
|
@@ -252,6 +280,12 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
252
280
|
if (contextFiles.length === 0) {
|
|
253
281
|
warnings.push('AI mapping context files were not found; continuing without optional markdown context.');
|
|
254
282
|
}
|
|
283
|
+
// Read candidate test file contents so the AI can reason about what scenarios
|
|
284
|
+
// are already covered and which ones are still missing.
|
|
285
|
+
const candidateTestContents = readCandidateTestContents(testsRoot, candidateTests);
|
|
286
|
+
const testContentBlock = candidateTestContents.length > 0
|
|
287
|
+
? candidateTestContents.map((entry) => `### Test: ${entry.path}\n\`\`\`typescript\n${entry.content}\n\`\`\``).join('\n\n')
|
|
288
|
+
: 'No candidate test file contents could be read.';
|
|
255
289
|
let provider;
|
|
256
290
|
try {
|
|
257
291
|
provider = config.provider === 'auto'
|
|
@@ -277,7 +311,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
277
311
|
'Only use tests from CANDIDATE_TESTS. Never invent paths.',
|
|
278
312
|
'Prefer no mapping over a broad or generic mapping.',
|
|
279
313
|
'Return strict JSON only with this shape:',
|
|
280
|
-
'{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0}]}',
|
|
314
|
+
'{"mappings":[{"flowId":"<flow id>","tests":["specs/..."],"reason":"short reason","confidence":0.0,"missingScenarios":["scenario description"]}]}',
|
|
281
315
|
'',
|
|
282
316
|
'Rules:',
|
|
283
317
|
'- Keep at most 5 tests per flow.',
|
|
@@ -287,6 +321,8 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
287
321
|
'- Treat single-keyword or broad subsystem overlap as insufficient evidence.',
|
|
288
322
|
'- If the candidate path overlap is weak or ambiguous, return tests: [].',
|
|
289
323
|
'- If unsure for a flow, return tests: [].',
|
|
324
|
+
'- 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.',
|
|
325
|
+
'- If tests: [], set missingScenarios: [] as well — do not invent scenarios for unmapped flows.',
|
|
290
326
|
'',
|
|
291
327
|
`FLOWS (${prioritizedFlows.length}):`,
|
|
292
328
|
JSON.stringify(prioritizedFlows.map((flow) => ({
|
|
@@ -304,6 +340,9 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
304
340
|
`FLOW_CANDIDATE_SIGNALS (${candidateSelection.evidence.length}):`,
|
|
305
341
|
JSON.stringify(candidateSelection.evidence, null, 2),
|
|
306
342
|
'',
|
|
343
|
+
`CANDIDATE_TEST_CONTENT (${candidateTestContents.length} file(s)):`,
|
|
344
|
+
testContentBlock,
|
|
345
|
+
'',
|
|
307
346
|
contextBlock,
|
|
308
347
|
].join('\n');
|
|
309
348
|
let parsed = null;
|
|
@@ -344,6 +383,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
344
383
|
const allowedFlowIds = new Set(prioritizedFlows.map((flow) => flow.id));
|
|
345
384
|
const prioritizedFlowsById = new Map(prioritizedFlows.map((flow) => [flow.id, flow]));
|
|
346
385
|
const mapped = new Map();
|
|
386
|
+
const scenarioGaps = new Map();
|
|
347
387
|
const matchedTests = new Set();
|
|
348
388
|
for (const entry of parsed.mappings) {
|
|
349
389
|
if (!entry || !allowedFlowIds.has(entry.flowId) || !Array.isArray(entry.tests)) {
|
|
@@ -367,6 +407,15 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
367
407
|
for (const testPath of valid) {
|
|
368
408
|
matchedTests.add(testPath);
|
|
369
409
|
}
|
|
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
|
+
}
|
|
370
419
|
}
|
|
371
420
|
// Post-AI exact-name fallback: for any flow still uncovered, search all test paths
|
|
372
421
|
// for a file or directory whose name exactly matches the flow ID. This handles flows
|
|
@@ -386,7 +435,7 @@ export async function mapAITestsToFlows(appRoot, testsRoot, config, flows, tests
|
|
|
386
435
|
matchedTests.add(exactMatch);
|
|
387
436
|
}
|
|
388
437
|
}
|
|
389
|
-
const coverage = buildCoverage(flows, mapped);
|
|
438
|
+
const coverage = buildCoverage(flows, mapped, scenarioGaps);
|
|
390
439
|
if (mapped.size === 0) {
|
|
391
440
|
warnings.push(`AI mapping returned no valid test mappings (${provider.name}).`);
|
|
392
441
|
}
|
package/dist/esm/agent/plan.js
CHANGED
|
@@ -160,6 +160,13 @@ function buildDecision(runSet, confidence, impact, policy) {
|
|
|
160
160
|
summary: `Detected ${impact.gaps.length} uncovered P0/P1 flow(s). Add or update tests before merge.`,
|
|
161
161
|
};
|
|
162
162
|
}
|
|
163
|
+
if (impact.changedFiles.length === 0 && impact.flows.length === 0) {
|
|
164
|
+
return {
|
|
165
|
+
action: 'safe-to-merge',
|
|
166
|
+
title: 'Safe to merge',
|
|
167
|
+
summary: 'No app file changes detected — no E2E coverage required for this change set.',
|
|
168
|
+
};
|
|
169
|
+
}
|
|
163
170
|
if (runSet === 'smoke' && confidence >= policy.safeMergeMinConfidence && impact.warnings.length === 0) {
|
|
164
171
|
return {
|
|
165
172
|
action: 'safe-to-merge',
|
|
@@ -249,6 +256,8 @@ export function buildPlanFromImpactReport(impact, policyOverride) {
|
|
|
249
256
|
priority: flow.priority,
|
|
250
257
|
reasons: (flow.reasons || []).slice(0, 5),
|
|
251
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,
|
|
252
261
|
}));
|
|
253
262
|
const coveredFlowIds = new Set(impact.gaps.map((g) => g.id));
|
|
254
263
|
const coveredFlows = impact.coverage
|
package/dist/esm/agent/runner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
3
|
+
"version": "0.5.10",
|
|
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",
|