@yasserkhanorg/e2e-agents 0.3.3 → 0.3.4

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.
@@ -42,6 +42,12 @@ export function writeReport(appRoot, config, data) {
42
42
  const markdownLines = [];
43
43
  markdownLines.push(`# ${data.mode === 'impact' ? 'Impact Analysis' : 'Gap Analysis'} Report`);
44
44
  markdownLines.push('');
45
+ if (data.runMetadata) {
46
+ markdownLines.push(`Run ID: ${data.runMetadata.runId}`);
47
+ markdownLines.push(`Run window: ${data.runMetadata.startedAt} -> ${data.runMetadata.completedAt}`);
48
+ markdownLines.push(`Run duration (ms): ${data.runMetadata.durationMs}`);
49
+ markdownLines.push(`Since ref: ${data.runMetadata.sinceRef}`);
50
+ }
45
51
  markdownLines.push(`Framework: ${data.framework}`);
46
52
  markdownLines.push(`Test Patterns: ${data.testPatterns.join(', ') || 'None'}`);
47
53
  if (data.flowCatalog) {
@@ -100,6 +106,9 @@ export function writeReport(appRoot, config, data) {
100
106
  if (result.error) {
101
107
  markdownLines.push(` Error: ${result.error}`);
102
108
  }
109
+ if (result.failureCategory || result.failureCode) {
110
+ markdownLines.push(` Failure taxonomy: category=${result.failureCategory || 'unknown'} code=${result.failureCode || 'unknown'}`);
111
+ }
103
112
  }
104
113
  if (data.pipeline.warnings.length > 0) {
105
114
  markdownLines.push('Pipeline warnings:');
@@ -114,11 +114,14 @@ function buildRecommendedTestsFromCoverage(flows, coverage) {
114
114
  const flow = flowMap.get(entry.flowId);
115
115
  const flagSummary = formatFlags(flow?.flags || []);
116
116
  for (const test of entry.coveredBy) {
117
- if (!testNotes.has(test)) {
118
- testNotes.set(test, new Set());
117
+ const normalizedTest = normalizePath(test)
118
+ .replace(/^\.\//, '')
119
+ .replace(/^e2e-tests\/playwright\//, '');
120
+ if (!testNotes.has(normalizedTest)) {
121
+ testNotes.set(normalizedTest, new Set());
119
122
  }
120
123
  if (flagSummary !== 'none') {
121
- testNotes.get(test)?.add(flagSummary);
124
+ testNotes.get(normalizedTest)?.add(flagSummary);
122
125
  }
123
126
  }
124
127
  }
@@ -190,12 +193,24 @@ function classifyImpactModelConfidence(flowMapping, testMapping, dependencyGraph
190
193
  }
191
194
  return 'low';
192
195
  }
196
+ function createRunId(mode) {
197
+ const ciRunId = process.env.GITHUB_RUN_ID;
198
+ const entropy = Math.random().toString(36).slice(2, 8);
199
+ const ts = Date.now().toString(36);
200
+ if (ciRunId) {
201
+ return `${mode}-gh-${ciRunId}-${ts}-${entropy}`;
202
+ }
203
+ return `${mode}-local-${ts}-${entropy}`;
204
+ }
193
205
  export async function runImpact(_config, _options) {
194
206
  ensureAppRoot(_config.path);
195
207
  if (_config.testsRoot) {
196
208
  ensureAppRoot(_config.testsRoot);
197
209
  }
198
210
  const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
211
+ const runStartedAt = new Date().toISOString();
212
+ const runStartedTs = Date.now();
213
+ const runId = createRunId('impact');
199
214
  const warnings = [];
200
215
  const testsRoot = _config.testsRoot || _config.path;
201
216
  const frameworkDetection = detectFramework(testsRoot, _config.framework);
@@ -268,7 +283,7 @@ export async function runImpact(_config, _options) {
268
283
  coverageMap.set(entry.flowId, entry.coveredBy);
269
284
  }
270
285
  gaps = computeGaps(flows, coverageMap);
271
- recommendedTests = buildRecommendedTestsWithFlags(flows, testsByFlow);
286
+ recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
272
287
  }
273
288
  else {
274
289
  const traceability = mapTraceabilityToFlows(testsRoot, _config.impact.traceability, flows);
@@ -320,6 +335,15 @@ export async function runImpact(_config, _options) {
320
335
  const reportRoot = testsRoot;
321
336
  const report = writeReport(reportRoot, _config, {
322
337
  mode: 'impact',
338
+ runMetadata: {
339
+ runId,
340
+ startedAt: runStartedAt,
341
+ completedAt: new Date().toISOString(),
342
+ durationMs: Date.now() - runStartedTs,
343
+ sinceRef: _config.git.since,
344
+ appPath: _config.path,
345
+ testsRoot,
346
+ },
323
347
  changedFiles,
324
348
  flows: sortFlows(flows),
325
349
  coverage,
@@ -366,6 +390,9 @@ export async function runGap(_config, _options) {
366
390
  ensureAppRoot(_config.testsRoot);
367
391
  }
368
392
  const deadline = Date.now() + _config.timeLimitMinutes * 60 * 1000;
393
+ const runStartedAt = new Date().toISOString();
394
+ const runStartedTs = Date.now();
395
+ const runId = createRunId('gap');
369
396
  const warnings = [];
370
397
  const testsRoot = _config.testsRoot || _config.path;
371
398
  const frameworkDetection = detectFramework(testsRoot, _config.framework);
@@ -450,7 +477,7 @@ export async function runGap(_config, _options) {
450
477
  coverageMap.set(entry.flowId, entry.coveredBy);
451
478
  }
452
479
  gaps = computeGaps(flows, coverageMap);
453
- recommendedTests = buildRecommendedTestsWithFlags(flows, testsByFlow);
480
+ recommendedTests = buildRecommendedTestsFromCoverage(flows, coverage);
454
481
  }
455
482
  else {
456
483
  const traceability = mapTraceabilityToFlows(testsRoot, _config.impact.traceability, flows);
@@ -502,6 +529,15 @@ export async function runGap(_config, _options) {
502
529
  const reportRoot = testsRoot;
503
530
  const report = writeReport(reportRoot, _config, {
504
531
  mode: 'gap',
532
+ runMetadata: {
533
+ runId,
534
+ startedAt: runStartedAt,
535
+ completedAt: new Date().toISOString(),
536
+ durationMs: Date.now() - runStartedTs,
537
+ sinceRef: _config.git.since,
538
+ appPath: _config.path,
539
+ testsRoot,
540
+ },
505
541
  changedFiles,
506
542
  flows: sortFlows(flows),
507
543
  coverage,
package/dist/esm/api.js CHANGED
@@ -4,7 +4,7 @@ import { existsSync, readFileSync } from 'fs';
4
4
  import { join } from 'path';
5
5
  import { resolveConfig } from './agent/config.js';
6
6
  import { runGap, runImpact } from './agent/runner.js';
7
- import { attachDeveloperActions, buildPlanFromImpactReport, renderCiSummaryMarkdown, writeCiSummary, writePlanReport, } from './agent/plan.js';
7
+ import { appendPlanMetrics, attachDeveloperActions, buildPlanFromImpactReport, renderCiSummaryMarkdown, writeCiSummary, writePlanReport, } from './agent/plan.js';
8
8
  import { applyOperationalInsights } from './agent/operational_insights.js';
9
9
  import { finalizeGeneratedTests } from './agent/handoff.js';
10
10
  import { ingestTraceabilityInput, } from './agent/traceability_ingest.js';
@@ -62,6 +62,7 @@ export async function recommendTests(options = {}) {
62
62
  const planPath = writePlanReport(reportRoot, plan);
63
63
  const ciSummaryMarkdown = renderCiSummaryMarkdown(plan);
64
64
  const ciSummaryPath = writeCiSummary(reportRoot, ciSummaryMarkdown);
65
+ appendPlanMetrics(reportRoot, plan);
65
66
  return {
66
67
  report,
67
68
  reportPath: impactPath,
package/dist/esm/cli.js CHANGED
@@ -7,7 +7,7 @@ import { resolveConfig } from './agent/config.js';
7
7
  import { AnthropicProvider } from './anthropic_provider.js';
8
8
  import { LLMProviderError } from './provider_interface.js';
9
9
  import { runGap, runImpact } from './agent/runner.js';
10
- import { attachDeveloperActions, buildPlanFromImpactReport, renderCiSummaryMarkdown, writeCiSummary, writePlanReport } from './agent/plan.js';
10
+ import { appendPlanMetrics, attachDeveloperActions, buildPlanFromImpactReport, renderCiSummaryMarkdown, writeCiSummary, writePlanReport, } from './agent/plan.js';
11
11
  import { applyOperationalInsights } from './agent/operational_insights.js';
12
12
  import { appendFeedbackAndRecompute } from './agent/feedback.js';
13
13
  import { finalizeGeneratedTests } from './agent/handoff.js';
@@ -87,6 +87,7 @@ function printUsage() {
87
87
  ' --pipeline-base-url Base URL for Playwright runs',
88
88
  ' --pipeline-browser Browser: chrome|chromium|firefox|webkit',
89
89
  ' --pipeline-headless Run in headless mode',
90
+ ' --pipeline-headed Run in headed mode',
90
91
  ' --pipeline-project Playwright project name',
91
92
  ' --pipeline-parallel Enable parallel mode in generator',
92
93
  ' --pipeline-dry-run Do not execute pipeline (report only)',
@@ -101,6 +102,8 @@ function printUsage() {
101
102
  ' --policy-safe-merge-confidence <n> Confidence needed for safe-to-merge',
102
103
  ' --policy-force-full-on-warnings <n> Escalate to full at warning count',
103
104
  ' --policy-risky-patterns <globs> Comma-separated risky file globs',
105
+ ' --policy-enforcement-mode <mode> advisory | warn | block',
106
+ ' --policy-block-actions <actions> Comma-separated CI actions to block/warn',
104
107
  ' --ci-comment-path <path> Write CI markdown summary',
105
108
  ' --github-output <path> Write GitHub Actions outputs',
106
109
  ' --fail-on-must-add-tests Exit non-zero on must-add-tests decision',
@@ -244,6 +247,10 @@ function parseArgs(argv) {
244
247
  parsed.pipelineHeadless = true;
245
248
  continue;
246
249
  }
250
+ if (arg === '--pipeline-headed') {
251
+ parsed.pipelineHeadless = false;
252
+ continue;
253
+ }
247
254
  if (arg === '--pipeline-project' && next) {
248
255
  parsed.pipelineProject = next;
249
256
  i += 1;
@@ -320,6 +327,21 @@ function parseArgs(argv) {
320
327
  i += 1;
321
328
  continue;
322
329
  }
330
+ if (arg === '--policy-enforcement-mode' && next) {
331
+ if (next === 'advisory' || next === 'warn' || next === 'block') {
332
+ parsed.policyEnforcementMode = next;
333
+ }
334
+ i += 1;
335
+ continue;
336
+ }
337
+ if (arg === '--policy-block-actions' && next) {
338
+ parsed.policyBlockActions = next
339
+ .split(',')
340
+ .map((value) => value.trim())
341
+ .filter((value) => (value === 'run-now' || value === 'must-add-tests' || value === 'safe-to-merge'));
342
+ i += 1;
343
+ continue;
344
+ }
323
345
  if (arg === '--ci-comment-path' && next) {
324
346
  parsed.ciCommentPath = next;
325
347
  i += 1;
@@ -794,12 +816,16 @@ async function main() {
794
816
  policy: args.policyMinConfidence !== undefined ||
795
817
  args.policySafeMergeConfidence !== undefined ||
796
818
  args.policyWarningsThreshold !== undefined ||
797
- (args.policyRiskyPatterns && args.policyRiskyPatterns.length > 0)
819
+ (args.policyRiskyPatterns && args.policyRiskyPatterns.length > 0) ||
820
+ args.policyEnforcementMode !== undefined ||
821
+ (args.policyBlockActions && args.policyBlockActions.length > 0)
798
822
  ? {
799
823
  minConfidenceForTargeted: args.policyMinConfidence,
800
824
  safeMergeMinConfidence: args.policySafeMergeConfidence,
801
825
  forceFullOnWarningsAtOrAbove: args.policyWarningsThreshold,
802
826
  riskyFilePatterns: args.policyRiskyPatterns,
827
+ enforcementMode: args.policyEnforcementMode,
828
+ blockOnActions: args.policyBlockActions,
803
829
  }
804
830
  : undefined,
805
831
  });
@@ -828,24 +854,33 @@ async function main() {
828
854
  const planPath = writePlanReport(reportRoot, plan);
829
855
  const summaryMarkdown = renderCiSummaryMarkdown(plan);
830
856
  const summaryPath = writeCiSummary(reportRoot, summaryMarkdown, args.ciCommentPath);
857
+ const metrics = appendPlanMetrics(reportRoot, plan);
831
858
  const ghaOutput = args.githubOutputPath || process.env.GITHUB_OUTPUT;
832
859
  if (ghaOutput) {
833
860
  appendFileSync(ghaOutput, `run_set=${plan.runSet}\n`);
834
861
  appendFileSync(ghaOutput, `action=${plan.decision.action}\n`);
835
862
  appendFileSync(ghaOutput, `confidence=${plan.confidence}\n`);
863
+ appendFileSync(ghaOutput, `enforcement_mode=${plan.enforcement.mode}\n`);
864
+ appendFileSync(ghaOutput, `enforcement_should_fail=${plan.enforcement.shouldFail}\n`);
836
865
  appendFileSync(ghaOutput, `recommended_tests_count=${plan.recommendedTests.length}\n`);
837
866
  appendFileSync(ghaOutput, `required_new_tests_count=${plan.requiredNewTests.length}\n`);
838
867
  appendFileSync(ghaOutput, `plan_path=${planPath}\n`);
839
868
  appendFileSync(ghaOutput, `summary_path=${summaryPath}\n`);
869
+ appendFileSync(ghaOutput, `metrics_events_path=${metrics.eventsPath}\n`);
870
+ appendFileSync(ghaOutput, `metrics_summary_path=${metrics.summaryPath}\n`);
840
871
  }
841
872
  // eslint-disable-next-line no-console
842
873
  console.log(`Suggested run set: ${plan.runSet} (confidence ${plan.confidence})`);
843
874
  // eslint-disable-next-line no-console
844
875
  console.log(`Decision: ${plan.decision.action} - ${plan.decision.summary}`);
845
876
  // eslint-disable-next-line no-console
877
+ console.log(`Enforcement: ${plan.enforcement.mode} (shouldFail=${plan.enforcement.shouldFail})`);
878
+ // eslint-disable-next-line no-console
846
879
  console.log(`Plan data: ${planPath}`);
847
880
  // eslint-disable-next-line no-console
848
881
  console.log(`CI summary: ${summaryPath}`);
882
+ // eslint-disable-next-line no-console
883
+ console.log(`Plan metrics: ${metrics.summaryPath}`);
849
884
  if (plan.nextActions) {
850
885
  // eslint-disable-next-line no-console
851
886
  console.log(`Next action (run existing): ${plan.nextActions.runRecommendedTests || plan.nextActions.runSmokeSuite}`);
@@ -854,7 +889,8 @@ async function main() {
854
889
  // eslint-disable-next-line no-console
855
890
  console.log(`Next action (heal): ${plan.nextActions.healGeneratedTests}`);
856
891
  }
857
- if (args.failOnMustAddTests && plan.decision.action === 'must-add-tests') {
892
+ const failOnLegacyFlag = args.failOnMustAddTests && plan.decision.action === 'must-add-tests';
893
+ if (failOnLegacyFlag || plan.enforcement.shouldFail) {
858
894
  process.exit(2);
859
895
  }
860
896
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yasserkhanorg/e2e-agents",
3
- "version": "0.3.3",
3
+ "version": "0.3.4",
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",
@@ -18,6 +18,28 @@
18
18
  "mode": {
19
19
  "const": "impact"
20
20
  },
21
+ "runMetadata": {
22
+ "type": "object",
23
+ "required": [
24
+ "runId",
25
+ "startedAt",
26
+ "completedAt",
27
+ "durationMs",
28
+ "sinceRef",
29
+ "appPath",
30
+ "testsRoot"
31
+ ],
32
+ "properties": {
33
+ "runId": {"type": "string"},
34
+ "startedAt": {"type": "string", "format": "date-time"},
35
+ "completedAt": {"type": "string", "format": "date-time"},
36
+ "durationMs": {"type": "number"},
37
+ "sinceRef": {"type": "string"},
38
+ "appPath": {"type": "string"},
39
+ "testsRoot": {"type": "string"}
40
+ },
41
+ "additionalProperties": false
42
+ },
21
43
  "changedFiles": {
22
44
  "type": "array",
23
45
  "items": {
@@ -370,6 +392,21 @@
370
392
  },
371
393
  "error": {
372
394
  "type": "string"
395
+ },
396
+ "failureCategory": {
397
+ "enum": [
398
+ "config",
399
+ "environment",
400
+ "generation",
401
+ "validation",
402
+ "runtime",
403
+ "quality",
404
+ "path-safety",
405
+ "unknown"
406
+ ]
407
+ },
408
+ "failureCode": {
409
+ "type": "string"
373
410
  }
374
411
  },
375
412
  "additionalProperties": false
@@ -5,6 +5,7 @@
5
5
  "type": "object",
6
6
  "required": [
7
7
  "schemaVersion",
8
+ "runId",
8
9
  "generatedAt",
9
10
  "source",
10
11
  "runSet",
@@ -14,12 +15,19 @@
14
15
  "requiredNewTests",
15
16
  "policy",
16
17
  "decision",
18
+ "enforcement",
17
19
  "metrics"
18
20
  ],
19
21
  "properties": {
20
22
  "schemaVersion": {
21
23
  "const": "1.0.0"
22
24
  },
25
+ "runId": {
26
+ "type": "string"
27
+ },
28
+ "sourceRunId": {
29
+ "type": "string"
30
+ },
23
31
  "generatedAt": {
24
32
  "type": "string",
25
33
  "format": "date-time"
@@ -108,6 +116,15 @@
108
116
  "items": {
109
117
  "type": "string"
110
118
  }
119
+ },
120
+ "enforcementMode": {
121
+ "enum": ["advisory", "warn", "block"]
122
+ },
123
+ "blockOnActions": {
124
+ "type": "array",
125
+ "items": {
126
+ "enum": ["run-now", "must-add-tests", "safe-to-merge"]
127
+ }
111
128
  }
112
129
  },
113
130
  "additionalProperties": false
@@ -139,6 +156,37 @@
139
156
  },
140
157
  "additionalProperties": false
141
158
  },
159
+ "enforcement": {
160
+ "type": "object",
161
+ "required": [
162
+ "mode",
163
+ "blockOnActions",
164
+ "matchedAction",
165
+ "shouldFail",
166
+ "summary"
167
+ ],
168
+ "properties": {
169
+ "mode": {
170
+ "enum": ["advisory", "warn", "block"]
171
+ },
172
+ "blockOnActions": {
173
+ "type": "array",
174
+ "items": {
175
+ "enum": ["run-now", "must-add-tests", "safe-to-merge"]
176
+ }
177
+ },
178
+ "matchedAction": {
179
+ "type": "boolean"
180
+ },
181
+ "shouldFail": {
182
+ "type": "boolean"
183
+ },
184
+ "summary": {
185
+ "type": "string"
186
+ }
187
+ },
188
+ "additionalProperties": false
189
+ },
142
190
  "insights": {
143
191
  "type": "object",
144
192
  "properties": {