api-tests-coverage 1.0.19 → 1.0.20

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/src/index.js CHANGED
@@ -1403,7 +1403,7 @@ program
1403
1403
  .option('--port <port>', 'Port for the dashboard server (requires --dashboard)', parseInt)
1404
1404
  .option('--open', 'Open the dashboard in your browser automatically (requires --dashboard)')
1405
1405
  .action(async (options) => {
1406
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
1406
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w;
1407
1407
  const { metricsPort, serviceName } = setupObservability();
1408
1408
  const logger = (0, observability_1.getLogger)();
1409
1409
  const configPath = program.opts()['config'];
@@ -1456,7 +1456,7 @@ program
1456
1456
  }
1457
1457
  // ── 3. Integration flow inference ──────────────────────────────────────
1458
1458
  if (doInferFlows) {
1459
- inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings);
1459
+ inferredFlowsResult = (0, integrationFlowInference_1.inferIntegrationFlows)(artifacts.testFiles, warnings, artifacts.serviceFiles);
1460
1460
  const flowsPath = (0, integrationFlowInference_1.writeInferredIntegrationFlows)(inferredFlowsResult, reportsDir);
1461
1461
  console.log(`\nIntegration Flow Inference`);
1462
1462
  console.log(` Multi-step flows detected in tests: ${inferredFlowsResult.flows.length}`);
@@ -1514,6 +1514,13 @@ program
1514
1514
  while ((m = TEST_DECL_RE.exec(content)) !== null) {
1515
1515
  descriptions.push(m[2].toLowerCase());
1516
1516
  }
1517
+ // Generic: extract test function/method names for Python (def test_xxx),
1518
+ // Ruby (def test_xxx), and similar frameworks where test names are method names.
1519
+ const GENERIC_TEST_FN_RE = /\bdef\s+((?:test|should|spec)_\w+)\s*\(/g;
1520
+ GENERIC_TEST_FN_RE.lastIndex = 0;
1521
+ while ((m = GENERIC_TEST_FN_RE.exec(content)) !== null) {
1522
+ descriptions.push(m[1].replace(/_/g, ' ').toLowerCase());
1523
+ }
1517
1524
  }
1518
1525
  return [{ file: tf, contentLower, descriptions, isJavaLike }];
1519
1526
  });
@@ -1910,6 +1917,137 @@ program
1910
1917
  allCoverageResults.push(flowResult);
1911
1918
  console.log(` ${syntheticReport.complete}/${syntheticReport.total} multi-step flows detected and covered (100%)`);
1912
1919
  }
1920
+ // ── 4f-perf. Performance & resilience coverage ────────────────────────────
1921
+ // When load-test files (JMeter/k6/Gatling) are available, use them.
1922
+ // When none are found, infer performance/resilience evidence from test content.
1923
+ {
1924
+ // ─ Keywords ──────────────────────────────────────────────────────────────
1925
+ const PERF_KEYWORDS = [
1926
+ 'load', 'performance', 'stress', 'throughput', 'latency', 'benchmark',
1927
+ 'concurrent', 'response time', 'response_time', 'timing', 'timed',
1928
+ 'slow', 'fast', 'speed', 'millisecond', 'ms ', ' ms',
1929
+ ];
1930
+ const RESILIENCE_KEYWORD_MAP = {
1931
+ 'timeout': ['timeout', 'timed out', 'connection timeout', 'request timeout', 'read timeout'],
1932
+ 'retry': ['retry', 'retries', 'retried', 'attempt', 'backoff', 'back-off'],
1933
+ 'circuit-breaker': ['circuit breaker', 'circuit_breaker', 'circuitbreaker', 'open circuit'],
1934
+ 'fallback': ['fallback', 'fall back', 'fall-back', 'default response', 'degraded'],
1935
+ 'rate-limiting': ['rate limit', 'rate_limit', 'ratelimit', '429', 'too many requests', 'throttl'],
1936
+ 'bulkhead': ['bulkhead', 'semaphore', 'queue full', 'concurrency limit'],
1937
+ };
1938
+ if (artifacts.performanceFiles.length > 0) {
1939
+ // ─ Explicit: JMeter / k6 / Gatling files found ─────────────────────
1940
+ try {
1941
+ console.log(`\nAnalyzing performance coverage (${artifacts.performanceFiles.length} load-test file(s))...`);
1942
+ const endpointItems = (_s = (_r = (_q = allCoverageResults.find((r) => r.type === 'endpoint')) === null || _q === void 0 ? void 0 : _q.details) === null || _r === void 0 ? void 0 : _r.items) !== null && _s !== void 0 ? _s : [];
1943
+ const endpoints = endpointItems.map((item) => {
1944
+ var _a, _b;
1945
+ const parts = item.id.split(' ');
1946
+ return { id: item.id, method: (_a = parts[0]) !== null && _a !== void 0 ? _a : 'GET', path: (_b = parts[1]) !== null && _b !== void 0 ? _b : item.id };
1947
+ });
1948
+ const metricsMap = (0, perfResilienceCoverage_1.parseLoadTestResults)(artifacts.performanceFiles);
1949
+ const perfThresholds = { responseMs: 500, errorRate: 0.05 };
1950
+ const perfCoverages = (0, perfResilienceCoverage_1.analyzePerformanceCoverage)(endpoints, metricsMap, perfThresholds);
1951
+ const scenarios = (0, perfResilienceCoverage_1.buildResilienceScenarios)(endpoints);
1952
+ const resilienceCoverages = await (0, perfResilienceCoverage_1.analyzeResilienceCoverage)(scenarios, testsGlob);
1953
+ const report = (0, perfResilienceCoverage_1.buildPerfResilienceReport)(perfCoverages, resilienceCoverages);
1954
+ const perfResult = {
1955
+ type: 'performance',
1956
+ totalItems: report.totalEndpoints,
1957
+ coveredItems: report.endpointsWithLoadData,
1958
+ coveragePercent: report.performanceCoveragePercent,
1959
+ details: report,
1960
+ };
1961
+ const resilienceResult = {
1962
+ type: 'resilience',
1963
+ totalItems: report.totalResilienceScenarios,
1964
+ coveredItems: report.coveredResilienceScenarios,
1965
+ coveragePercent: report.resilienceCoveragePercent,
1966
+ details: report,
1967
+ };
1968
+ allCoverageResults.push(perfResult, resilienceResult);
1969
+ console.log(` ${report.endpointsWithLoadData}/${report.totalEndpoints} endpoints have load-test data (${report.performanceCoveragePercent}%)`);
1970
+ console.log(` ${report.coveredResilienceScenarios}/${report.totalResilienceScenarios} resilience scenarios covered (${report.resilienceCoveragePercent}%)`);
1971
+ }
1972
+ catch (perfErr) {
1973
+ warnings.push(`Performance coverage failed: ${perfErr instanceof Error ? perfErr.message : String(perfErr)}`);
1974
+ }
1975
+ }
1976
+ else {
1977
+ // ─ Inferred: no load-test files found — scan test content for signals ─
1978
+ console.log(`\nPerformance & resilience coverage (inferred — no JMeter/k6/Gatling files found)...`);
1979
+ // Build a flat list of all known endpoints from earlier coverage results
1980
+ const knownEndpoints = ((_v = (_u = (_t = allCoverageResults.find((r) => r.type === 'endpoint')) === null || _t === void 0 ? void 0 : _t.details) === null || _u === void 0 ? void 0 : _u.items) !== null && _v !== void 0 ? _v : []).map((item) => item.id);
1981
+ const allTestContent = testEntries.map((e) => e.contentLower);
1982
+ const combinedTestContent = allTestContent.join('\n');
1983
+ // ─ Performance inference ────────────────────────────────────────────
1984
+ const hasPerfSignal = PERF_KEYWORDS.some((kw) => combinedTestContent.includes(kw));
1985
+ const perfItems = knownEndpoints.map((endpointId) => {
1986
+ var _a, _b;
1987
+ const parts = endpointId.split(' ');
1988
+ const epPath = ((_a = parts[1]) !== null && _a !== void 0 ? _a : endpointId).toLowerCase();
1989
+ const epLeaf = (_b = epPath.split('/').filter(Boolean).pop()) !== null && _b !== void 0 ? _b : epPath;
1990
+ const hasEvidenceInTests = testEntries.some(({ contentLower }) => {
1991
+ const mentionsEndpoint = epLeaf.length > 2
1992
+ ? contentLower.includes(epLeaf)
1993
+ : contentLower.includes(epPath);
1994
+ const hasPerfKw = PERF_KEYWORDS.some((kw) => contentLower.includes(kw));
1995
+ return mentionsEndpoint && hasPerfKw;
1996
+ });
1997
+ return { id: endpointId, hasEvidence: hasEvidenceInTests };
1998
+ });
1999
+ const perfCoveredCount = perfItems.filter((i) => i.hasEvidence).length;
2000
+ const perfTotal = Math.max(perfItems.length, 1); // avoid 0-denominator
2001
+ const perfPct = knownEndpoints.length === 0
2002
+ ? (hasPerfSignal ? 30 : 0) // no routes but some signal
2003
+ : Math.round((perfCoveredCount / perfItems.length) * 100);
2004
+ const perfResult = {
2005
+ type: 'performance',
2006
+ totalItems: perfItems.length || 1,
2007
+ coveredItems: knownEndpoints.length === 0 && hasPerfSignal ? 0 : perfCoveredCount,
2008
+ coveragePercent: perfPct,
2009
+ details: {
2010
+ inferred: true,
2011
+ note: 'No JMeter, k6, or Gatling files found. Coverage inferred from test content keywords.',
2012
+ totalEndpoints: perfItems.length,
2013
+ endpointsWithEvidence: perfCoveredCount,
2014
+ performanceCoveragePercent: perfPct,
2015
+ items: perfItems,
2016
+ },
2017
+ };
2018
+ allCoverageResults.push(perfResult);
2019
+ if (knownEndpoints.length > 0) {
2020
+ console.log(` ${perfCoveredCount}/${perfItems.length} endpoints have performance test evidence (${perfPct}%)`);
2021
+ }
2022
+ else {
2023
+ console.log(` Performance signal in tests: ${hasPerfSignal ? 'yes' : 'none'} (0% — no load-test files)`);
2024
+ }
2025
+ console.log(` [NOTE] Add JMeter .jtl/.csv, k6 .json, or Gatling simulation.log files for accurate load-test metrics.`);
2026
+ // ─ Resilience inference ──────────────────────────────────────────────
2027
+ const resilienceItems = Object.entries(RESILIENCE_KEYWORD_MAP).map(([category, keywords]) => {
2028
+ const covered = testEntries.some(({ contentLower }) => keywords.some((kw) => contentLower.includes(kw)));
2029
+ return { id: `resilience:${category}`, category, covered };
2030
+ });
2031
+ const resCoveredCount = resilienceItems.filter((i) => i.covered).length;
2032
+ const resPct = Math.round((resCoveredCount / resilienceItems.length) * 100);
2033
+ const resilienceResult = {
2034
+ type: 'resilience',
2035
+ totalItems: resilienceItems.length,
2036
+ coveredItems: resCoveredCount,
2037
+ coveragePercent: resPct,
2038
+ details: {
2039
+ inferred: true,
2040
+ note: 'Resilience coverage inferred from test content. No load-test files found.',
2041
+ totalResilienceScenarios: resilienceItems.length,
2042
+ coveredResilienceScenarios: resCoveredCount,
2043
+ resilienceCoveragePercent: resPct,
2044
+ items: resilienceItems,
2045
+ },
2046
+ };
2047
+ allCoverageResults.push(resilienceResult);
2048
+ console.log(` ${resCoveredCount}/${resilienceItems.length} resilience categories have test evidence (${resPct}%)`);
2049
+ }
2050
+ }
1913
2051
  if (allCoverageResults.length > 0) {
1914
2052
  const observabilityInfo = (0, observability_1.buildObservabilityInfo)(metricsPort);
1915
2053
  (0, reporting_1.generateMultiFormatReports)(allCoverageResults, ['json'], reportsDir, {}, observabilityInfo);
@@ -2019,7 +2157,7 @@ program
2019
2157
  if (options['dashboard']) {
2020
2158
  (0, serveDashboard_1.serveDashboard)({
2021
2159
  reportsDir: reportsDir,
2022
- port: (_q = options['port']) !== null && _q !== void 0 ? _q : 4000,
2160
+ port: (_w = options['port']) !== null && _w !== void 0 ? _w : 4000,
2023
2161
  open: Boolean(options['open']),
2024
2162
  });
2025
2163
  // Keep the process alive — the HTTP server holds the event loop open
@@ -1 +1 @@
1
- {"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA2ID;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA4C3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,GAAE,MAAM,EAAO,GACtB,2BAA2B,CAuB7B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,2BAA2B,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR;AAID,eAAO,MAAM,kBAAkB,aAM7B,CAAC;AAEH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwCnE"}
1
+ {"version":3,"file":"businessRuleInference.d.ts","sourceRoot":"","sources":["../../../src/inference/businessRuleInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AACjD,MAAM,MAAM,QAAQ,GAChB,YAAY,GACZ,eAAe,GACf,gBAAgB,GAChB,YAAY,CAAC;AAEjB,MAAM,WAAW,oBAAoB;IACnC,0DAA0D;IAC1D,EAAE,EAAE,MAAM,CAAC;IACX,gCAAgC;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,IAAI,EAAE,QAAQ,CAAC;IACf,+CAA+C;IAC/C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,8BAA8B;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,+BAA+B;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,gBAAgB,EAAE,MAAM,EAAE,CAAC;CAC5B;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,uCAAuC;IACvC,aAAa,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,EAAE,OAAO,CAAC;IAClB,uCAAuC;IACvC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAwPD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,oBAAoB,EAAE,CA8C3E;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,MAAM,EAAE,EACtB,QAAQ,GAAE,MAAM,EAAO,GACtB,2BAA2B,CAuB7B;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,2BAA2B,EACnC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR;AAID,eAAO,MAAM,kBAAkB,aAM7B,CAAC;AAEH;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE,CAwCnE"}
@@ -138,12 +138,104 @@ const INFERENCE_PATTERNS = [
138
138
  behaviorTemplate: (m) => `${m[1]} must not exceed ${m[2]}`,
139
139
  conditionTemplate: (m) => `${m[1]} > ${m[2]}`,
140
140
  },
141
+ // ── Flask / Python-specific patterns ──────────────────────────────────────
142
+ // Broader Python raise: any raise X() or raise X.y() not already ending in Exception/Error
143
+ // (Ordered after the existing raise patterns so deduplification by source location handles overlap)
144
+ {
145
+ type: 'business_logic',
146
+ pattern: /\braise\s+([\w]+(?:\.[\w]+)*)\s*(?:\(([^)]{0,60})\))?/,
147
+ nameTemplate: (m) => { var _a; return toSnakeCase((_a = m[1].split('.').pop()) !== null && _a !== void 0 ? _a : m[1]); },
148
+ behaviorTemplate: (m) => `Operation rejected: ${m[1]}`,
149
+ conditionTemplate: (m) => { var _a; return `raise ${m[1]}(${((_a = m[2]) !== null && _a !== void 0 ? _a : '').trim()})`; },
150
+ },
151
+ // Python `if not X:` null guard — idiomatic Python nil check
152
+ {
153
+ type: 'validation',
154
+ pattern: /if\s+not\s+([\w.]+)\s*(?::|and|or)/,
155
+ nameTemplate: (m) => `require_${toSnakeCase(m[1].replace(/\./g, '_'))}`,
156
+ behaviorTemplate: (m) => `${m[1]} must exist`,
157
+ conditionTemplate: (m) => `if not ${m[1]}`,
158
+ },
159
+ // Flask/Django auth decorators: @jwt_required, @login_required, etc.
160
+ {
161
+ type: 'authorization',
162
+ pattern: /@(jwt_required|login_required|require_auth|requires_auth|permission_required|auth_required|authenticated_user|require_permissions?)\b/i,
163
+ nameTemplate: (m) => `require_${toSnakeCase(m[1])}`,
164
+ behaviorTemplate: (m) => `Request requires authentication (${m[1]})`,
165
+ conditionTemplate: (m) => `@${m[1]}`,
166
+ },
167
+ // Flask `@use_kwargs` / `@validate_arguments` input validation
168
+ {
169
+ type: 'validation',
170
+ pattern: /@(use_kwargs|validate_arguments?|expects_json|validate_body)\s*\(\s*([\w]+)/i,
171
+ nameTemplate: (m) => { var _a; return `validate_input_${toSnakeCase((_a = m[2]) !== null && _a !== void 0 ? _a : 'schema')}`; },
172
+ behaviorTemplate: (m) => { var _a; return `Input validated against ${(_a = m[2]) !== null && _a !== void 0 ? _a : 'schema'}`; },
173
+ conditionTemplate: (m) => { var _a; return `@${m[1]}(${(_a = m[2]) !== null && _a !== void 0 ? _a : ''})`; },
174
+ },
175
+ // Python ownership / attribute comparison guard
176
+ {
177
+ type: 'authorization',
178
+ pattern: /if\s+[\w.]+\s*!=\s*[\w.]+\.(?:id|user_id|author_id|owner_id|profile\.id)/,
179
+ nameTemplate: () => 'ownership_check',
180
+ behaviorTemplate: () => 'Resource must belong to the requesting user',
181
+ conditionTemplate: (m) => m[0].trim(),
182
+ },
183
+ // ── Generic JS/TS/frontend patterns ───────────────────────────────────────
184
+ // Generic JS/TS `if (!expression)` null/falsy guard
185
+ // Matches meaningful presence checks like !JWT.get(), !User.current, !token
186
+ // Skips pure normalisation calls like .trim(), .length, .toLowerCase(), etc.
187
+ // via the skipIfGroup1Matches post-match filter.
188
+ {
189
+ type: 'validation',
190
+ pattern: /if\s*\(\s*!\s*([\w][\w.[\]]*(?:\.\w+\(\s*\))?)\s*\)/,
191
+ skipIfGroup1Matches: /\.(trim|length|split|join|toLowerCase|toUpperCase|toString|valueOf|slice|substr|substring|replace|indexOf|includes|startsWith|endsWith)\(\s*\)$/,
192
+ nameTemplate: (m) => `require_${toSnakeCase(m[1].replace(/[^a-zA-Z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, ''))}`,
193
+ behaviorTemplate: (m) => `${m[1]} must be present/truthy`,
194
+ conditionTemplate: (m) => `!${m[1]}`,
195
+ },
196
+ // HTTP status comparison guard (frontend interceptors): rejection.status === 401
197
+ {
198
+ type: 'validation',
199
+ pattern: /\.status\s*={1,3}\s*(4\d\d|5\d\d)/,
200
+ nameTemplate: (m) => `http_status_${m[1]}_check`,
201
+ behaviorTemplate: (m) => `Handle HTTP ${m[1]} error response`,
202
+ conditionTemplate: (m) => `status == ${m[1]}`,
203
+ },
204
+ // Ownership / identity comparison guard (username === author.username)
205
+ {
206
+ type: 'authorization',
207
+ pattern: /if\s*\(?\s*[\w.]+\.(?:username|userId|user_id|email|id)\s*!==?\s*[\w.]+\.(?:username|userId|user_id|email|id|author\.username)/,
208
+ nameTemplate: () => 'ownership_identity_check',
209
+ behaviorTemplate: () => 'Resource belongs to a specific user identity',
210
+ conditionTemplate: (m) => m[0].trim().replace(/^if\s*\(?/, '').replace(/\)?\s*$/, ''),
211
+ },
212
+ // Generic early-return guard: if (cond) return/throw on same or adjacent line
213
+ {
214
+ type: 'business_logic',
215
+ pattern: /if\s*\([^)]{3,80}\)\s*(?:\{[^}]{0,40}\})?\s*(?:return|throw|raise)\b/,
216
+ nameTemplate: () => 'guard_condition',
217
+ behaviorTemplate: () => 'Early return or error on condition',
218
+ conditionTemplate: (m) => {
219
+ const cond = m[0].match(/if\s*\(([^)]{3,80})\)/);
220
+ return cond ? cond[1].trim() : m[0].trim();
221
+ },
222
+ },
141
223
  ];
142
224
  // ─── Endpoint heuristics ──────────────────────────────────────────────────────
143
225
  /** Attempt to associate a rule with the nearest HTTP route/endpoint annotation. */
144
226
  function guessEndpoint(lines, ruleLineIdx) {
145
227
  // Search up to 40 lines above for common routing patterns
146
228
  const lookupLines = lines.slice(Math.max(0, ruleLineIdx - 40), ruleLineIdx);
229
+ // Flask: @blueprint.route('/path', methods=...) / @app.route('/path', ...)
230
+ for (let i = lookupLines.length - 1; i >= 0; i--) {
231
+ const fm = lookupLines[i].match(/@\w+\.route\s*\(\s*['"]([^'"]+)['"]/);
232
+ if (fm) {
233
+ // Try to find method from same line or near
234
+ const methodsMatch = lookupLines[i].match(/methods\s*=\s*[\[(]['"]?([\w]+)['"]?/);
235
+ const method = methodsMatch ? methodsMatch[1].toUpperCase() : 'GET';
236
+ return `${method} ${fm[1]}`;
237
+ }
238
+ }
147
239
  // Spring: @GetMapping("/path") / @PostMapping / @RequestMapping
148
240
  for (let i = lookupLines.length - 1; i >= 0; i--) {
149
241
  const m = lookupLines[i].match(/@(Get|Post|Put|Patch|Delete|Request)Mapping\s*\(\s*["']([^"']+)["']/i);
@@ -191,6 +283,9 @@ function inferRulesFromFile(filePath) {
191
283
  const match = line.match(ip.pattern);
192
284
  if (!match)
193
285
  continue;
286
+ // Post-match filter: skip false positives based on first captured group
287
+ if (ip.skipIfGroup1Matches && match[1] && ip.skipIfGroup1Matches.test(match[1]))
288
+ continue;
194
289
  const condition = ip.conditionTemplate(match);
195
290
  const sourceLocation = `${filePath}:${lineIdx + 1}`;
196
291
  const dedupKey = `${filePath}:${lineIdx}:${condition}`;
@@ -44,10 +44,20 @@ export interface IntegrationFlowInferenceResult {
44
44
  * Infer integration flows from a single test file.
45
45
  */
46
46
  export declare function inferFlowsFromFile(filePath: string): InferredIntegrationFlow[];
47
+ /**
48
+ * Infer integration flows from service source files (not test files).
49
+ *
50
+ * Used as a fallback when no test files are present (e.g. frontend-only projects).
51
+ * Only produces flows for source files that contain 2+ HTTP calls in the same function.
52
+ */
53
+ export declare function inferFlowsFromSourceFiles(serviceFiles: string[]): InferredIntegrationFlow[];
47
54
  /**
48
55
  * Run flow inference across all provided test files.
56
+ *
57
+ * When `testFiles` is empty and `serviceFiles` is provided, flows are inferred
58
+ * from the service source code instead and tagged accordingly.
49
59
  */
50
- export declare function inferIntegrationFlows(testFiles: string[], warnings?: string[]): IntegrationFlowInferenceResult;
60
+ export declare function inferIntegrationFlows(testFiles: string[], warnings?: string[], serviceFiles?: string[]): IntegrationFlowInferenceResult;
51
61
  /**
52
62
  * Write inferred integration flows to the reports directory.
53
63
  * Returns the path of the written file.
@@ -1 +1 @@
1
- {"version":3,"file":"integrationFlowInference.d.ts","sourceRoot":"","sources":["../../../src/inference/integrationFlowInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,+BAA+B;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA4KD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAW9E;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,GAAE,MAAM,EAAO,GACtB,8BAA8B,CA2BhC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,8BAA8B,EACtC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR"}
1
+ {"version":3,"file":"integrationFlowInference.d.ts","sourceRoot":"","sources":["../../../src/inference/integrationFlowInference.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAOH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,UAAU,CAAC;AAEjD,MAAM,WAAW,QAAQ;IACvB,kBAAkB;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,yCAAyC;IACzC,IAAI,EAAE,MAAM,CAAC;IACb,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,wBAAwB;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,UAAU,CAAC;IACxB,+BAA+B;IAC/B,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,iDAAiD;IACjD,eAAe,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B;IAC7C,KAAK,EAAE,uBAAuB,EAAE,CAAC;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAqLD;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,uBAAuB,EAAE,CAW9E;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,YAAY,EAAE,MAAM,EAAE,GAAG,uBAAuB,EAAE,CAgB3F;AAED;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,MAAM,EAAE,EACnB,QAAQ,GAAE,MAAM,EAAO,EACvB,YAAY,CAAC,EAAE,MAAM,EAAE,GACtB,8BAA8B,CAoChC;AAED;;;GAGG;AACH,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,8BAA8B,EACtC,UAAU,EAAE,MAAM,GACjB,MAAM,CAYR"}
@@ -48,6 +48,7 @@ var __importStar = (this && this.__importStar) || (function () {
48
48
  })();
49
49
  Object.defineProperty(exports, "__esModule", { value: true });
50
50
  exports.inferFlowsFromFile = inferFlowsFromFile;
51
+ exports.inferFlowsFromSourceFiles = inferFlowsFromSourceFiles;
51
52
  exports.inferIntegrationFlows = inferIntegrationFlows;
52
53
  exports.writeInferredIntegrationFlows = writeInferredIntegrationFlows;
53
54
  const fs = __importStar(require("fs"));
@@ -63,6 +64,11 @@ const HTTP_CALL_PATTERNS = [
63
64
  { pattern: /\b(get|post|put|patch|delete)\s+['"`]([^'"`\s]+)['"`]/i, methodGroup: 1, pathGroup: 2 },
64
65
  // fetch('/path', { method: 'POST' })
65
66
  { pattern: /fetch\s*\(\s*['"`]([^'"`]+)['"`]\s*,\s*\{[^}]*method\s*:\s*['"`](GET|POST|PUT|PATCH|DELETE|HEAD)['"`]/i, methodGroup: 2, pathGroup: 1 },
67
+ // WebTest TestApp: testapp.post_json(url_for('endpoint'), data)
68
+ // Also handles: testapp.get(url_for('endpoint')), testapp.delete_json(...)
69
+ { pattern: /(?:testapp|self\.testapp)\.(get_json|post_json|put_json|patch_json|delete_json|get|post|put|patch|delete)\s*\(\s*url_for\s*\(\s*['"]([^'"]+)['"]/i, methodGroup: 1, pathGroup: 2 },
70
+ // WebTest without url_for: testapp.get('/path')
71
+ { pattern: /(?:testapp|self\.testapp)\.(get_json|post_json|put_json|patch_json|delete_json|get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]/i, methodGroup: 1, pathGroup: 2 },
66
72
  ];
67
73
  /** Patterns for test function / scenario boundaries */
68
74
  const TEST_FUNCTION_PATTERNS = [
@@ -85,6 +91,8 @@ function extractHttpCallsFromLines(lines) {
85
91
  const m = line.match(p.pattern);
86
92
  if (m) {
87
93
  const method = m[p.methodGroup].toUpperCase();
94
+ // Normalize WebTest _json suffix: POST_JSON → POST, GET_JSON → GET
95
+ const normalizedMethod = method.replace(/_JSON$/, '');
88
96
  const rawPath = m[p.pathGroup];
89
97
  // Skip unlikely paths (full URLs with domain, non-path values)
90
98
  if (rawPath.startsWith('http') && !rawPath.includes('/api'))
@@ -92,7 +100,7 @@ function extractHttpCallsFromLines(lines) {
92
100
  const cleanPath = extractPathFromUrl(rawPath);
93
101
  if (!cleanPath)
94
102
  continue;
95
- calls.push({ method, path: cleanPath, lineIdx: i });
103
+ calls.push({ method: normalizedMethod, path: cleanPath, lineIdx: i });
96
104
  break; // only first pattern match per line
97
105
  }
98
106
  }
@@ -102,6 +110,9 @@ function extractHttpCallsFromLines(lines) {
102
110
  function extractPathFromUrl(raw) {
103
111
  if (raw.startsWith('/'))
104
112
  return raw;
113
+ // url_for endpoint name: 'blueprint.function' — treat as pseudo-path
114
+ if (/^\w+\.\w+$/.test(raw))
115
+ return `/${raw.replace('.', '/')}`;
105
116
  try {
106
117
  const u = new URL(raw);
107
118
  return u.pathname || undefined;
@@ -203,10 +214,46 @@ function inferFlowsFromFile(filePath) {
203
214
  const calls = extractHttpCallsFromLines(lines);
204
215
  return groupCallsIntoFlows(calls, filePath, lines);
205
216
  }
217
+ /**
218
+ * Infer integration flows from service source files (not test files).
219
+ *
220
+ * Used as a fallback when no test files are present (e.g. frontend-only projects).
221
+ * Only produces flows for source files that contain 2+ HTTP calls in the same function.
222
+ */
223
+ function inferFlowsFromSourceFiles(serviceFiles) {
224
+ const allFlows = [];
225
+ for (const fp of serviceFiles) {
226
+ let content;
227
+ try {
228
+ content = fs.readFileSync(fp, 'utf-8');
229
+ }
230
+ catch {
231
+ continue;
232
+ }
233
+ const lines = content.split('\n');
234
+ const calls = extractHttpCallsFromLines(lines);
235
+ if (calls.length < 2)
236
+ continue;
237
+ const flows = groupCallsIntoFlows(calls, fp, lines);
238
+ allFlows.push(...flows);
239
+ }
240
+ return allFlows;
241
+ }
206
242
  /**
207
243
  * Run flow inference across all provided test files.
244
+ *
245
+ * When `testFiles` is empty and `serviceFiles` is provided, flows are inferred
246
+ * from the service source code instead and tagged accordingly.
208
247
  */
209
- function inferIntegrationFlows(testFiles, warnings = []) {
248
+ function inferIntegrationFlows(testFiles, warnings = [], serviceFiles) {
249
+ if (testFiles.length === 0 && serviceFiles && serviceFiles.length > 0) {
250
+ warnings.push('No test files found; integration flows inferred from service source code.');
251
+ const flows = inferFlowsFromSourceFiles(serviceFiles);
252
+ if (flows.length === 0) {
253
+ warnings.push('No multi-step HTTP call sequences detected in service source files; integration flow inference produced no results.');
254
+ }
255
+ return { flows, filesAnalyzed: serviceFiles.length, inferred: true, warnings };
256
+ }
210
257
  if (testFiles.length === 0) {
211
258
  warnings.push('No test files provided; integration flow inference skipped.');
212
259
  return { flows: [], filesAnalyzed: 0, inferred: true, warnings };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-tests-coverage",
3
- "version": "1.0.19",
3
+ "version": "1.0.20",
4
4
  "description": "CLI and library to measure how thoroughly your test suite exercises your API surface area",
5
5
  "main": "dist/src/lib/index.js",
6
6
  "types": "dist/src/lib/index.d.ts",