api-tests-coverage 1.0.0

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.
Files changed (288) hide show
  1. package/README.md +703 -0
  2. package/config.yaml.example +227 -0
  3. package/dist/action/src/index.d.ts +2 -0
  4. package/dist/action/src/index.d.ts.map +1 -0
  5. package/dist/action/src/index.js +349 -0
  6. package/dist/action/src/prComment.d.ts +34 -0
  7. package/dist/action/src/prComment.d.ts.map +1 -0
  8. package/dist/action/src/prComment.js +146 -0
  9. package/dist/src/ast/astAnalysisOrchestrator.d.ts +36 -0
  10. package/dist/src/ast/astAnalysisOrchestrator.d.ts.map +1 -0
  11. package/dist/src/ast/astAnalysisOrchestrator.js +123 -0
  12. package/dist/src/ast/astTypes.d.ts +105 -0
  13. package/dist/src/ast/astTypes.d.ts.map +1 -0
  14. package/dist/src/ast/astTypes.js +9 -0
  15. package/dist/src/ast/languageAnalyzer.d.ts +46 -0
  16. package/dist/src/ast/languageAnalyzer.d.ts.map +1 -0
  17. package/dist/src/ast/languageAnalyzer.js +9 -0
  18. package/dist/src/ast/languageCapabilities.d.ts +24 -0
  19. package/dist/src/ast/languageCapabilities.d.ts.map +1 -0
  20. package/dist/src/ast/languageCapabilities.js +92 -0
  21. package/dist/src/ast/parseFile.d.ts +16 -0
  22. package/dist/src/ast/parseFile.d.ts.map +1 -0
  23. package/dist/src/ast/parseFile.js +65 -0
  24. package/dist/src/ast/parserRegistry.d.ts +39 -0
  25. package/dist/src/ast/parserRegistry.d.ts.map +1 -0
  26. package/dist/src/ast/parserRegistry.js +66 -0
  27. package/dist/src/buildSummary.d.ts +26 -0
  28. package/dist/src/buildSummary.d.ts.map +1 -0
  29. package/dist/src/buildSummary.js +193 -0
  30. package/dist/src/businessCoverage.d.ts +68 -0
  31. package/dist/src/businessCoverage.d.ts.map +1 -0
  32. package/dist/src/businessCoverage.js +290 -0
  33. package/dist/src/compatibilityCoverage.d.ts +83 -0
  34. package/dist/src/compatibilityCoverage.d.ts.map +1 -0
  35. package/dist/src/compatibilityCoverage.js +501 -0
  36. package/dist/src/config/defaultConfig.d.ts +9 -0
  37. package/dist/src/config/defaultConfig.d.ts.map +1 -0
  38. package/dist/src/config/defaultConfig.js +97 -0
  39. package/dist/src/config/index.d.ts +12 -0
  40. package/dist/src/config/index.d.ts.map +1 -0
  41. package/dist/src/config/index.js +37 -0
  42. package/dist/src/config/loadConfig.d.ts +29 -0
  43. package/dist/src/config/loadConfig.d.ts.map +1 -0
  44. package/dist/src/config/loadConfig.js +135 -0
  45. package/dist/src/config/mergeConfig.d.ts +15 -0
  46. package/dist/src/config/mergeConfig.d.ts.map +1 -0
  47. package/dist/src/config/mergeConfig.js +57 -0
  48. package/dist/src/config/schema.d.ts +15 -0
  49. package/dist/src/config/schema.d.ts.map +1 -0
  50. package/dist/src/config/schema.js +30 -0
  51. package/dist/src/config/types.d.ts +175 -0
  52. package/dist/src/config/types.d.ts.map +1 -0
  53. package/dist/src/config/types.js +9 -0
  54. package/dist/src/config/validateConfig.d.ts +22 -0
  55. package/dist/src/config/validateConfig.d.ts.map +1 -0
  56. package/dist/src/config/validateConfig.js +171 -0
  57. package/dist/src/config.d.ts +168 -0
  58. package/dist/src/config.d.ts.map +1 -0
  59. package/dist/src/config.js +204 -0
  60. package/dist/src/coverage/deep-analysis/callGraph.d.ts +67 -0
  61. package/dist/src/coverage/deep-analysis/callGraph.d.ts.map +1 -0
  62. package/dist/src/coverage/deep-analysis/callGraph.js +275 -0
  63. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts +23 -0
  64. package/dist/src/coverage/deep-analysis/deepEndpointResolver.d.ts.map +1 -0
  65. package/dist/src/coverage/deep-analysis/deepEndpointResolver.js +394 -0
  66. package/dist/src/coverage/deep-analysis/index.d.ts +17 -0
  67. package/dist/src/coverage/deep-analysis/index.d.ts.map +1 -0
  68. package/dist/src/coverage/deep-analysis/index.js +63 -0
  69. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts +60 -0
  70. package/dist/src/coverage/deep-analysis/resolveAssertions.d.ts.map +1 -0
  71. package/dist/src/coverage/deep-analysis/resolveAssertions.js +121 -0
  72. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts +36 -0
  73. package/dist/src/coverage/deep-analysis/resolveConstants.d.ts.map +1 -0
  74. package/dist/src/coverage/deep-analysis/resolveConstants.js +92 -0
  75. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts +55 -0
  76. package/dist/src/coverage/deep-analysis/resolveEnums.d.ts.map +1 -0
  77. package/dist/src/coverage/deep-analysis/resolveEnums.js +152 -0
  78. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts +70 -0
  79. package/dist/src/coverage/deep-analysis/resolveMethodChains.d.ts.map +1 -0
  80. package/dist/src/coverage/deep-analysis/resolveMethodChains.js +152 -0
  81. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts +80 -0
  82. package/dist/src/coverage/deep-analysis/resolvePaths.d.ts.map +1 -0
  83. package/dist/src/coverage/deep-analysis/resolvePaths.js +216 -0
  84. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts +71 -0
  85. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.d.ts.map +1 -0
  86. package/dist/src/coverage/deep-analysis/resolveRequestWrappers.js +226 -0
  87. package/dist/src/coverage/deep-analysis/symbolTable.d.ts +58 -0
  88. package/dist/src/coverage/deep-analysis/symbolTable.d.ts.map +1 -0
  89. package/dist/src/coverage/deep-analysis/symbolTable.js +230 -0
  90. package/dist/src/coverage/deep-analysis/types.d.ts +122 -0
  91. package/dist/src/coverage/deep-analysis/types.d.ts.map +1 -0
  92. package/dist/src/coverage/deep-analysis/types.js +21 -0
  93. package/dist/src/discovery/fileClassifier.d.ts +50 -0
  94. package/dist/src/discovery/fileClassifier.d.ts.map +1 -0
  95. package/dist/src/discovery/fileClassifier.js +238 -0
  96. package/dist/src/discovery/projectDiscovery.d.ts +66 -0
  97. package/dist/src/discovery/projectDiscovery.d.ts.map +1 -0
  98. package/dist/src/discovery/projectDiscovery.js +287 -0
  99. package/dist/src/endpointCoverage.d.ts +70 -0
  100. package/dist/src/endpointCoverage.d.ts.map +1 -0
  101. package/dist/src/endpointCoverage.js +381 -0
  102. package/dist/src/errorCoverage.d.ts +93 -0
  103. package/dist/src/errorCoverage.d.ts.map +1 -0
  104. package/dist/src/errorCoverage.js +698 -0
  105. package/dist/src/index.d.ts +3 -0
  106. package/dist/src/index.d.ts.map +1 -0
  107. package/dist/src/index.js +1441 -0
  108. package/dist/src/inference/businessRuleInference.d.ts +63 -0
  109. package/dist/src/inference/businessRuleInference.d.ts.map +1 -0
  110. package/dist/src/inference/businessRuleInference.js +268 -0
  111. package/dist/src/inference/integrationFlowInference.d.ts +56 -0
  112. package/dist/src/inference/integrationFlowInference.d.ts.map +1 -0
  113. package/dist/src/inference/integrationFlowInference.js +266 -0
  114. package/dist/src/integrationCoverage.d.ts +72 -0
  115. package/dist/src/integrationCoverage.d.ts.map +1 -0
  116. package/dist/src/integrationCoverage.js +317 -0
  117. package/dist/src/intelligence/index.d.ts +20 -0
  118. package/dist/src/intelligence/index.d.ts.map +1 -0
  119. package/dist/src/intelligence/index.js +105 -0
  120. package/dist/src/intelligence/linkageEngine.d.ts +20 -0
  121. package/dist/src/intelligence/linkageEngine.d.ts.map +1 -0
  122. package/dist/src/intelligence/linkageEngine.js +522 -0
  123. package/dist/src/intelligence/markdownReporter.d.ts +12 -0
  124. package/dist/src/intelligence/markdownReporter.d.ts.map +1 -0
  125. package/dist/src/intelligence/markdownReporter.js +265 -0
  126. package/dist/src/intelligence/riskScoring.d.ts +53 -0
  127. package/dist/src/intelligence/riskScoring.d.ts.map +1 -0
  128. package/dist/src/intelligence/riskScoring.js +181 -0
  129. package/dist/src/intelligence/types.d.ts +121 -0
  130. package/dist/src/intelligence/types.d.ts.map +1 -0
  131. package/dist/src/intelligence/types.js +8 -0
  132. package/dist/src/languageDetection.d.ts +100 -0
  133. package/dist/src/languageDetection.d.ts.map +1 -0
  134. package/dist/src/languageDetection.js +349 -0
  135. package/dist/src/languages/java/index.d.ts +16 -0
  136. package/dist/src/languages/java/index.d.ts.map +1 -0
  137. package/dist/src/languages/java/index.js +103 -0
  138. package/dist/src/languages/java/parser.d.ts +7 -0
  139. package/dist/src/languages/java/parser.d.ts.map +1 -0
  140. package/dist/src/languages/java/parser.js +50 -0
  141. package/dist/src/languages/java/semanticBuilder.d.ts +21 -0
  142. package/dist/src/languages/java/semanticBuilder.d.ts.map +1 -0
  143. package/dist/src/languages/java/semanticBuilder.js +358 -0
  144. package/dist/src/languages/javascript/annotationExtractor.d.ts +20 -0
  145. package/dist/src/languages/javascript/annotationExtractor.d.ts.map +1 -0
  146. package/dist/src/languages/javascript/annotationExtractor.js +94 -0
  147. package/dist/src/languages/javascript/assertionResolver.d.ts +18 -0
  148. package/dist/src/languages/javascript/assertionResolver.d.ts.map +1 -0
  149. package/dist/src/languages/javascript/assertionResolver.js +150 -0
  150. package/dist/src/languages/javascript/callResolver.d.ts +23 -0
  151. package/dist/src/languages/javascript/callResolver.d.ts.map +1 -0
  152. package/dist/src/languages/javascript/callResolver.js +236 -0
  153. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts +23 -0
  154. package/dist/src/languages/javascript/httpInteractionExtractor.d.ts.map +1 -0
  155. package/dist/src/languages/javascript/httpInteractionExtractor.js +205 -0
  156. package/dist/src/languages/javascript/index.d.ts +20 -0
  157. package/dist/src/languages/javascript/index.d.ts.map +1 -0
  158. package/dist/src/languages/javascript/index.js +136 -0
  159. package/dist/src/languages/javascript/parser.d.ts +14 -0
  160. package/dist/src/languages/javascript/parser.d.ts.map +1 -0
  161. package/dist/src/languages/javascript/parser.js +38 -0
  162. package/dist/src/languages/javascript/symbolResolver.d.ts +31 -0
  163. package/dist/src/languages/javascript/symbolResolver.d.ts.map +1 -0
  164. package/dist/src/languages/javascript/symbolResolver.js +183 -0
  165. package/dist/src/languages/kotlin/index.d.ts +16 -0
  166. package/dist/src/languages/kotlin/index.d.ts.map +1 -0
  167. package/dist/src/languages/kotlin/index.js +151 -0
  168. package/dist/src/languages/kotlin/parser.d.ts +11 -0
  169. package/dist/src/languages/kotlin/parser.d.ts.map +1 -0
  170. package/dist/src/languages/kotlin/parser.js +74 -0
  171. package/dist/src/languages/python/index.d.ts +15 -0
  172. package/dist/src/languages/python/index.d.ts.map +1 -0
  173. package/dist/src/languages/python/index.js +293 -0
  174. package/dist/src/languages/ruby/index.d.ts +15 -0
  175. package/dist/src/languages/ruby/index.d.ts.map +1 -0
  176. package/dist/src/languages/ruby/index.js +274 -0
  177. package/dist/src/languages/shared/treeSitterUtils.d.ts +43 -0
  178. package/dist/src/languages/shared/treeSitterUtils.d.ts.map +1 -0
  179. package/dist/src/languages/shared/treeSitterUtils.js +100 -0
  180. package/dist/src/languages/typescript/index.d.ts +14 -0
  181. package/dist/src/languages/typescript/index.d.ts.map +1 -0
  182. package/dist/src/languages/typescript/index.js +25 -0
  183. package/dist/src/lib/index.d.ts +228 -0
  184. package/dist/src/lib/index.d.ts.map +1 -0
  185. package/dist/src/lib/index.js +486 -0
  186. package/dist/src/mcp/client/index.d.ts +37 -0
  187. package/dist/src/mcp/client/index.d.ts.map +1 -0
  188. package/dist/src/mcp/client/index.js +235 -0
  189. package/dist/src/mcp/config.d.ts +50 -0
  190. package/dist/src/mcp/config.d.ts.map +1 -0
  191. package/dist/src/mcp/config.js +125 -0
  192. package/dist/src/mcp/events.d.ts +24 -0
  193. package/dist/src/mcp/events.d.ts.map +1 -0
  194. package/dist/src/mcp/events.js +48 -0
  195. package/dist/src/mcp/fallback/index.d.ts +50 -0
  196. package/dist/src/mcp/fallback/index.d.ts.map +1 -0
  197. package/dist/src/mcp/fallback/index.js +216 -0
  198. package/dist/src/mcp/index.d.ts +67 -0
  199. package/dist/src/mcp/index.d.ts.map +1 -0
  200. package/dist/src/mcp/index.js +212 -0
  201. package/dist/src/mcp/normalizer.d.ts +21 -0
  202. package/dist/src/mcp/normalizer.d.ts.map +1 -0
  203. package/dist/src/mcp/normalizer.js +99 -0
  204. package/dist/src/mcp/prompts/index.d.ts +86 -0
  205. package/dist/src/mcp/prompts/index.d.ts.map +1 -0
  206. package/dist/src/mcp/prompts/index.js +304 -0
  207. package/dist/src/mcp/templates/index.d.ts +35 -0
  208. package/dist/src/mcp/templates/index.d.ts.map +1 -0
  209. package/dist/src/mcp/templates/index.js +143 -0
  210. package/dist/src/mcp/testing/mock-server/index.d.ts +47 -0
  211. package/dist/src/mcp/testing/mock-server/index.d.ts.map +1 -0
  212. package/dist/src/mcp/testing/mock-server/index.js +157 -0
  213. package/dist/src/mcp/types.d.ts +127 -0
  214. package/dist/src/mcp/types.d.ts.map +1 -0
  215. package/dist/src/mcp/types.js +8 -0
  216. package/dist/src/observability.d.ts +138 -0
  217. package/dist/src/observability.d.ts.map +1 -0
  218. package/dist/src/observability.js +519 -0
  219. package/dist/src/parameterCoverage.d.ts +75 -0
  220. package/dist/src/parameterCoverage.d.ts.map +1 -0
  221. package/dist/src/parameterCoverage.js +629 -0
  222. package/dist/src/perfResilienceCoverage.d.ts +155 -0
  223. package/dist/src/perfResilienceCoverage.d.ts.map +1 -0
  224. package/dist/src/perfResilienceCoverage.js +670 -0
  225. package/dist/src/pluginLoader.d.ts +51 -0
  226. package/dist/src/pluginLoader.d.ts.map +1 -0
  227. package/dist/src/pluginLoader.js +72 -0
  228. package/dist/src/publishing.d.ts +63 -0
  229. package/dist/src/publishing.d.ts.map +1 -0
  230. package/dist/src/publishing.js +379 -0
  231. package/dist/src/qualityGate.d.ts +58 -0
  232. package/dist/src/qualityGate.d.ts.map +1 -0
  233. package/dist/src/qualityGate.js +118 -0
  234. package/dist/src/reporting.d.ts +41 -0
  235. package/dist/src/reporting.d.ts.map +1 -0
  236. package/dist/src/reporting.js +278 -0
  237. package/dist/src/screenshots.d.ts +71 -0
  238. package/dist/src/screenshots.d.ts.map +1 -0
  239. package/dist/src/screenshots.js +141 -0
  240. package/dist/src/security/gate/index.d.ts +11 -0
  241. package/dist/src/security/gate/index.d.ts.map +1 -0
  242. package/dist/src/security/gate/index.js +65 -0
  243. package/dist/src/security/index.d.ts +30 -0
  244. package/dist/src/security/index.d.ts.map +1 -0
  245. package/dist/src/security/index.js +342 -0
  246. package/dist/src/security/normalizers/semgrep.d.ts +10 -0
  247. package/dist/src/security/normalizers/semgrep.d.ts.map +1 -0
  248. package/dist/src/security/normalizers/semgrep.js +104 -0
  249. package/dist/src/security/normalizers/trivy.d.ts +10 -0
  250. package/dist/src/security/normalizers/trivy.d.ts.map +1 -0
  251. package/dist/src/security/normalizers/trivy.js +78 -0
  252. package/dist/src/security/normalizers/zap.d.ts +10 -0
  253. package/dist/src/security/normalizers/zap.d.ts.map +1 -0
  254. package/dist/src/security/normalizers/zap.js +104 -0
  255. package/dist/src/security/scanners/semgrep.d.ts +6 -0
  256. package/dist/src/security/scanners/semgrep.d.ts.map +1 -0
  257. package/dist/src/security/scanners/semgrep.js +125 -0
  258. package/dist/src/security/scanners/trivy.d.ts +6 -0
  259. package/dist/src/security/scanners/trivy.d.ts.map +1 -0
  260. package/dist/src/security/scanners/trivy.js +115 -0
  261. package/dist/src/security/scanners/zap.d.ts +6 -0
  262. package/dist/src/security/scanners/zap.d.ts.map +1 -0
  263. package/dist/src/security/scanners/zap.js +135 -0
  264. package/dist/src/security/types.d.ts +146 -0
  265. package/dist/src/security/types.d.ts.map +1 -0
  266. package/dist/src/security/types.js +6 -0
  267. package/dist/src/securityCoverage.d.ts +116 -0
  268. package/dist/src/securityCoverage.d.ts.map +1 -0
  269. package/dist/src/securityCoverage.js +725 -0
  270. package/dist/src/summary/buildSummary.d.ts +28 -0
  271. package/dist/src/summary/buildSummary.d.ts.map +1 -0
  272. package/dist/src/summary/buildSummary.js +257 -0
  273. package/dist/src/summary/evaluateMetrics.d.ts +31 -0
  274. package/dist/src/summary/evaluateMetrics.d.ts.map +1 -0
  275. package/dist/src/summary/evaluateMetrics.js +118 -0
  276. package/dist/src/summary/index.d.ts +10 -0
  277. package/dist/src/summary/index.d.ts.map +1 -0
  278. package/dist/src/summary/index.js +22 -0
  279. package/dist/src/summary/markdownRenderer.d.ts +139 -0
  280. package/dist/src/summary/markdownRenderer.d.ts.map +1 -0
  281. package/dist/src/summary/markdownRenderer.js +459 -0
  282. package/dist/src/summary/prSummary.d.ts +24 -0
  283. package/dist/src/summary/prSummary.d.ts.map +1 -0
  284. package/dist/src/summary/prSummary.js +233 -0
  285. package/dist/src/summary/summaryTypes.d.ts +35 -0
  286. package/dist/src/summary/summaryTypes.d.ts.map +1 -0
  287. package/dist/src/summary/summaryTypes.js +27 -0
  288. package/package.json +84 -0
@@ -0,0 +1,698 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ERROR_CATEGORIES = void 0;
40
+ exports.statusCodeToCategories = statusCodeToCategories;
41
+ exports.parseErrorScenarios = parseErrorScenarios;
42
+ exports.segmentMentionsEndpoint = segmentMentionsEndpoint;
43
+ exports.matchesCategoryHeuristic = matchesCategoryHeuristic;
44
+ exports.segmentCoversScenario = segmentCoversScenario;
45
+ exports.analyzeErrorCoverage = analyzeErrorCoverage;
46
+ exports.buildErrorCoverageReport = buildErrorCoverageReport;
47
+ exports.generateErrorReports = generateErrorReports;
48
+ const swagger_parser_1 = __importDefault(require("@apidevtools/swagger-parser"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const fast_glob_1 = __importDefault(require("fast-glob"));
52
+ const astAnalysisOrchestrator_1 = require("./ast/astAnalysisOrchestrator");
53
+ exports.ERROR_CATEGORIES = [
54
+ 'missing-parameter',
55
+ 'invalid-value',
56
+ 'unauthorized',
57
+ 'forbidden',
58
+ 'not-found',
59
+ 'conflict',
60
+ 'server-error',
61
+ ];
62
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
63
+ const HTTP_METHODS = ['get', 'post', 'put', 'patch', 'delete', 'head', 'options', 'trace'];
64
+ function escapeRegex(s) {
65
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
66
+ }
67
+ /**
68
+ * Map a HTTP status code (and optional description) to one or more error
69
+ * categories that are relevant for heuristic matching.
70
+ */
71
+ function statusCodeToCategories(statusCode, description) {
72
+ const descLower = description.toLowerCase();
73
+ const categories = [];
74
+ switch (statusCode) {
75
+ case 400:
76
+ // Distinguish by description when possible; otherwise include both
77
+ if (descLower.includes('missing') || descLower.includes('required')) {
78
+ categories.push('missing-parameter');
79
+ }
80
+ else if (descLower.includes('invalid') ||
81
+ descLower.includes('format') ||
82
+ descLower.includes('type') ||
83
+ descLower.includes('value')) {
84
+ categories.push('invalid-value');
85
+ }
86
+ else {
87
+ categories.push('missing-parameter', 'invalid-value');
88
+ }
89
+ break;
90
+ case 401:
91
+ categories.push('unauthorized');
92
+ break;
93
+ case 403:
94
+ categories.push('forbidden');
95
+ break;
96
+ case 404:
97
+ categories.push('not-found');
98
+ break;
99
+ case 409:
100
+ categories.push('conflict');
101
+ break;
102
+ case 422:
103
+ categories.push('invalid-value');
104
+ break;
105
+ case 500:
106
+ case 502:
107
+ case 503:
108
+ case 504:
109
+ categories.push('server-error');
110
+ break;
111
+ default:
112
+ if (statusCode >= 400 && statusCode < 500) {
113
+ categories.push('invalid-value');
114
+ }
115
+ else if (statusCode >= 500) {
116
+ categories.push('server-error');
117
+ }
118
+ }
119
+ return categories;
120
+ }
121
+ // ─── Spec parsing ─────────────────────────────────────────────────────────────
122
+ /**
123
+ * Parse an OpenAPI 3 spec and return all non-2xx (error) response scenarios.
124
+ */
125
+ async function parseErrorScenarios(specPath) {
126
+ var _a;
127
+ const api = (await swagger_parser_1.default.dereference(specPath));
128
+ const scenarios = [];
129
+ if (!api.paths)
130
+ return scenarios;
131
+ for (const [apiPath, pathItem] of Object.entries(api.paths)) {
132
+ if (!pathItem)
133
+ continue;
134
+ for (const method of HTTP_METHODS) {
135
+ const operation = pathItem[method];
136
+ if (!operation)
137
+ continue;
138
+ if (!operation.responses)
139
+ continue;
140
+ const endpoint = `${method.toUpperCase()} ${apiPath}`;
141
+ for (const [statusCodeStr, responseObj] of Object.entries(operation.responses)) {
142
+ const statusCode = parseInt(statusCodeStr, 10);
143
+ // Only 4xx and 5xx responses
144
+ if (isNaN(statusCode) || statusCode < 400)
145
+ continue;
146
+ const response = responseObj;
147
+ const description = (_a = response.description) !== null && _a !== void 0 ? _a : '';
148
+ const categories = statusCodeToCategories(statusCode, description);
149
+ scenarios.push({
150
+ id: `${endpoint}:${statusCode}`,
151
+ endpoint,
152
+ method: method.toUpperCase(),
153
+ path: apiPath,
154
+ statusCode,
155
+ description,
156
+ categories,
157
+ });
158
+ }
159
+ }
160
+ }
161
+ return scenarios;
162
+ }
163
+ /**
164
+ * Split a test file into individual test blocks (test / it declarations).
165
+ */
166
+ function extractTestSegments(fileContents) {
167
+ const segments = [];
168
+ const declPattern = /\b(?:test|it)\s*\(\s*(['"`])([\s\S]*?)\1/g;
169
+ const positions = [];
170
+ let m;
171
+ while ((m = declPattern.exec(fileContents)) !== null) {
172
+ positions.push({ start: m.index, desc: m[2] });
173
+ }
174
+ for (let i = 0; i < positions.length; i++) {
175
+ const start = positions[i].start;
176
+ const end = i + 1 < positions.length ? positions[i + 1].start : fileContents.length;
177
+ segments.push({ description: positions[i].desc, content: fileContents.slice(start, end) });
178
+ }
179
+ return segments;
180
+ }
181
+ // ─── Endpoint matching ────────────────────────────────────────────────────────
182
+ /**
183
+ * Return true if the test segment appears to exercise the endpoint associated
184
+ * with the given error scenario.
185
+ *
186
+ * Detection strategy (in order):
187
+ * 1. Description contains "METHOD /pathBase" (e.g. "POST /users")
188
+ * 2. Description contains the path base
189
+ * 3. Content contains the path base AND the HTTP method as a string literal
190
+ */
191
+ function segmentMentionsEndpoint(segment, scenario) {
192
+ const contentLower = segment.content.toLowerCase();
193
+ const descLower = segment.description.toLowerCase();
194
+ const pathBase = scenario.path.split('{')[0].toLowerCase(); // e.g. "/users/" or "/users"
195
+ const methodLower = scenario.method.toLowerCase();
196
+ // 1. Description contains "METHOD /pathBase" phrase
197
+ if (descLower.includes(`${methodLower} ${pathBase}`))
198
+ return true;
199
+ // 2. Description contains the pathBase AND method keyword
200
+ if (descLower.includes(pathBase) && descLower.includes(methodLower))
201
+ return true;
202
+ // 3. Content contains pathBase (as a URL string) AND method (as a string literal)
203
+ const pathInContent = contentLower.includes(pathBase);
204
+ const methodInContent = contentLower.includes(`'${methodLower}'`) ||
205
+ contentLower.includes(`"${methodLower}"`);
206
+ if (pathInContent && methodInContent)
207
+ return true;
208
+ return false;
209
+ }
210
+ // ─── Category heuristics ──────────────────────────────────────────────────────
211
+ /**
212
+ * Return true when a segment's description or content hints that it is
213
+ * exercising the given error category.
214
+ */
215
+ function matchesCategoryHeuristic(segment, category) {
216
+ const descLower = segment.description.toLowerCase();
217
+ const contentLower = segment.content.toLowerCase();
218
+ switch (category) {
219
+ case 'missing-parameter':
220
+ // Description keywords
221
+ if (descLower.includes('missing') ||
222
+ descLower.includes('without') ||
223
+ descLower.includes('omit') ||
224
+ descLower.includes('no body') ||
225
+ descLower.includes('required field'))
226
+ return true;
227
+ // Code: delete obj.field or field = undefined
228
+ if (/delete\s+\w+\.\w+/.test(segment.content))
229
+ return true;
230
+ if (/\w+\s*=\s*undefined/.test(segment.content))
231
+ return true;
232
+ return false;
233
+ case 'invalid-value':
234
+ // Description keywords
235
+ if (descLower.includes('invalid') ||
236
+ descLower.includes('wrong type') ||
237
+ descLower.includes('bad value') ||
238
+ descLower.includes('malformed') ||
239
+ descLower.includes('incorrect format') ||
240
+ descLower.includes('out of range') ||
241
+ descLower.includes('exceeds'))
242
+ return true;
243
+ // Code: field: null or field: undefined
244
+ if (/['"]?\w+['"]?\s*:\s*(null|undefined)\b/.test(segment.content))
245
+ return true;
246
+ return false;
247
+ case 'unauthorized':
248
+ // Description keywords
249
+ if (descLower.includes('unauthorized') ||
250
+ descLower.includes('unauthenticated') ||
251
+ descLower.includes('no token') ||
252
+ descLower.includes('missing token') ||
253
+ descLower.includes('invalid token') ||
254
+ descLower.includes('no auth') ||
255
+ descLower.includes('missing auth') ||
256
+ descLower.includes('without auth') ||
257
+ descLower.includes('without token') ||
258
+ descLower.includes('missing api key') ||
259
+ descLower.includes('invalid api key') ||
260
+ descLower.includes('no api key'))
261
+ return true;
262
+ // Code: Authorization header with empty or bare "Bearer " token
263
+ if (/authorization\s*:\s*['"]\s*['"]/.test(contentLower))
264
+ return true;
265
+ if (/authorization\s*:\s*['"]bearer\s+['"]/.test(contentLower))
266
+ return true;
267
+ return false;
268
+ case 'forbidden':
269
+ // Description keywords
270
+ if (descLower.includes('forbidden') ||
271
+ descLower.includes('not allowed') ||
272
+ descLower.includes('insufficient permission') ||
273
+ descLower.includes('access denied') ||
274
+ descLower.includes('no permission'))
275
+ return true;
276
+ return false;
277
+ case 'not-found':
278
+ // Description keywords
279
+ if (descLower.includes('not found') ||
280
+ descLower.includes('nonexistent') ||
281
+ descLower.includes('non-existent') ||
282
+ descLower.includes('unknown id') ||
283
+ descLower.includes('does not exist') ||
284
+ descLower.includes('missing resource'))
285
+ return true;
286
+ // Code: very large / obviously fake numeric IDs (6+ digits)
287
+ if (/\/\d{6,}/.test(segment.content))
288
+ return true;
289
+ if (/['"`]\d{6,}['"`]/.test(segment.content))
290
+ return true;
291
+ // Nil UUID
292
+ if (/00000000-0000-0000-0000-000000000000/.test(segment.content))
293
+ return true;
294
+ return false;
295
+ case 'conflict':
296
+ if (descLower.includes('conflict') ||
297
+ descLower.includes('duplicate') ||
298
+ descLower.includes('already exists') ||
299
+ descLower.includes('already registered') ||
300
+ descLower.includes('already in use') ||
301
+ descLower.includes('taken'))
302
+ return true;
303
+ return false;
304
+ case 'server-error':
305
+ if (descLower.includes('server error') ||
306
+ descLower.includes('internal error') ||
307
+ descLower.includes('service unavailable') ||
308
+ descLower.includes('unexpected error'))
309
+ return true;
310
+ return false;
311
+ }
312
+ }
313
+ // ─── Coverage determination ───────────────────────────────────────────────────
314
+ /**
315
+ * Return true if the segment contains a direct status-code assertion
316
+ * matching the scenario's status code.
317
+ *
318
+ * Recognised patterns:
319
+ * .toBe(400) .toEqual(400) status === 400 statusCode: 400
320
+ * expect(…).toBe(400)
321
+ */
322
+ function hasStatusCodeAssertion(segment, statusCode) {
323
+ const patterns = [
324
+ new RegExp(`\\.toBe\\(\\s*${statusCode}\\s*\\)`),
325
+ new RegExp(`\\.toEqual\\(\\s*${statusCode}\\s*\\)`),
326
+ new RegExp(`status\\s*[=!]=+\\s*${statusCode}\\b`),
327
+ new RegExp(`statusCode\\s*[=!]=+\\s*${statusCode}\\b`),
328
+ new RegExp(`statusCode\\s*:\\s*${statusCode}\\b`),
329
+ ];
330
+ return patterns.some((p) => p.test(segment.content));
331
+ }
332
+ /**
333
+ * Return true if the segment contains an assertion on an error message body
334
+ * that is consistent with the given category.
335
+ *
336
+ * Examples detected:
337
+ * expect(res.body).toHaveProperty('error')
338
+ * expect(res.body.message).toContain('not found')
339
+ * expect(thrown.message).toMatch(/unauthorized/i)
340
+ */
341
+ function hasErrorMessageAssertion(segment, category) {
342
+ const contentLower = segment.content.toLowerCase();
343
+ // Generic: any error-body assertion
344
+ const hasBodyErrorProp = /tohaveproperty\s*\(\s*['"]error['"]/.test(contentLower) ||
345
+ /\.body\.error/.test(contentLower) ||
346
+ /\.body\.message/.test(contentLower);
347
+ if (!hasBodyErrorProp)
348
+ return false;
349
+ // Narrow by category keyword inside the assertion
350
+ const categoryKeywords = {
351
+ 'missing-parameter': ['required', 'missing', 'must be provided'],
352
+ 'invalid-value': ['invalid', 'format', 'type', 'must be', 'out of range'],
353
+ unauthorized: ['unauthorized', 'unauthenticated', 'token', 'api key'],
354
+ forbidden: ['forbidden', 'permission', 'access denied'],
355
+ 'not-found': ['not found', 'does not exist'],
356
+ conflict: ['conflict', 'duplicate', 'already', 'in use'],
357
+ 'server-error': ['internal', 'server error', 'unexpected'],
358
+ };
359
+ const keywords = categoryKeywords[category];
360
+ // If any keyword appears anywhere in the test content → match
361
+ return keywords.some((kw) => contentLower.includes(kw));
362
+ }
363
+ /**
364
+ * Decide whether a single test segment covers the given error scenario.
365
+ */
366
+ function segmentCoversScenario(segment, scenario) {
367
+ // 1. Direct status code assertion
368
+ if (hasStatusCodeAssertion(segment, scenario.statusCode))
369
+ return true;
370
+ // 2. Category heuristics from description
371
+ for (const cat of scenario.categories) {
372
+ if (matchesCategoryHeuristic(segment, cat))
373
+ return true;
374
+ }
375
+ // 3. Indirect error-body assertion with category keywords
376
+ for (const cat of scenario.categories) {
377
+ if (hasErrorMessageAssertion(segment, cat))
378
+ return true;
379
+ }
380
+ return false;
381
+ }
382
+ // ─── AST augmentation ─────────────────────────────────────────────────────────
383
+ /** Detect language from file extension for AST analysis. */
384
+ function detectLanguageForAst(filePath) {
385
+ const ext = path.extname(filePath).toLowerCase();
386
+ switch (ext) {
387
+ case '.ts':
388
+ case '.tsx': return 'typescript';
389
+ case '.js':
390
+ case '.jsx': return 'javascript';
391
+ case '.java': return 'java';
392
+ case '.kt':
393
+ case '.kts': return 'kotlin';
394
+ case '.py': return 'python';
395
+ case '.rb': return 'ruby';
396
+ case '.feature': return 'cucumber';
397
+ default: return 'auto';
398
+ }
399
+ }
400
+ /** Normalize a path for loose matching against OpenAPI templates. */
401
+ function normalizeErrPath(p) {
402
+ return p.split('?')[0].replace(/\/$/, '').toLowerCase();
403
+ }
404
+ /**
405
+ * Return true when the interaction's path loosely matches the scenario path.
406
+ */
407
+ function errorPathMatches(interaction, scenarioPath) {
408
+ var _a;
409
+ const iPath = normalizeErrPath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
410
+ const aPath = normalizeErrPath(scenarioPath);
411
+ if (iPath === aPath)
412
+ return true;
413
+ const templateBase = aPath.split('{')[0].replace(/\/$/, '');
414
+ return !!(templateBase && iPath.startsWith(templateBase));
415
+ }
416
+ /**
417
+ * Map parameterScenarios strings to error categories.
418
+ */
419
+ function scenariosToErrorCategories(scenarios) {
420
+ const s = scenarios.map((x) => x.toLowerCase());
421
+ const cats = [];
422
+ if (s.some((x) => x.includes('missing') || x.includes('required')))
423
+ cats.push('missing-parameter');
424
+ if (s.some((x) => x.includes('invalid') || x.includes('bad') || x.includes('malformed')))
425
+ cats.push('invalid-value');
426
+ if (s.some((x) => x.includes('unauth') || x.includes('401')))
427
+ cats.push('unauthorized');
428
+ if (s.some((x) => x.includes('forbidden') || x.includes('403')))
429
+ cats.push('forbidden');
430
+ if (s.some((x) => x.includes('not-found') || x.includes('404')))
431
+ cats.push('not-found');
432
+ if (s.some((x) => x.includes('conflict') || x.includes('409')))
433
+ cats.push('conflict');
434
+ if (s.some((x) => x.includes('server') || x.includes('5xx') || x.includes('500')))
435
+ cats.push('server-error');
436
+ return cats;
437
+ }
438
+ /**
439
+ * Build AST interaction map from test files for error coverage augmentation.
440
+ */
441
+ function buildErrorAstMap(testFiles, astOptions) {
442
+ var _a;
443
+ (0, astAnalysisOrchestrator_1.registerAllAnalyzers)();
444
+ const context = (0, astAnalysisOrchestrator_1.buildAnalysisContext)(astOptions.astConfig, astOptions.deepConfig);
445
+ const map = new Map();
446
+ for (const filePath of testFiles) {
447
+ let content;
448
+ try {
449
+ content = fs.readFileSync(filePath, 'utf-8');
450
+ }
451
+ catch {
452
+ continue;
453
+ }
454
+ const lang = detectLanguageForAst(filePath);
455
+ const interactions = (0, astAnalysisOrchestrator_1.analyzeFile)(content, filePath, lang, context);
456
+ for (const interaction of interactions) {
457
+ const key = normalizeErrPath((_a = interaction.normalizedPath) !== null && _a !== void 0 ? _a : interaction.path);
458
+ if (!map.has(key))
459
+ map.set(key, []);
460
+ map.get(key).push(interaction);
461
+ }
462
+ }
463
+ return map;
464
+ }
465
+ /**
466
+ * Check if AST interactions at the scenario's endpoint cover this error scenario.
467
+ * Returns truthy with metadata when an AST-derived signal is found.
468
+ */
469
+ function checkAstErrorCoverage(matchingInteractions, scenario) {
470
+ let bestInteraction;
471
+ for (const interaction of matchingInteractions) {
472
+ // A status-code assertion indicates the test checks an HTTP status at this
473
+ // endpoint — a strong signal that the scenario is exercised
474
+ if (interaction.assertionType === 'status-code' ||
475
+ interaction.assertionType === 'fluent-chain') {
476
+ bestInteraction = interaction;
477
+ }
478
+ // parameterScenarios → error category matching
479
+ if (interaction.parameterScenarios && interaction.parameterScenarios.length > 0) {
480
+ const cats = scenariosToErrorCategories(interaction.parameterScenarios);
481
+ if (cats.some((c) => scenario.categories.includes(c))) {
482
+ bestInteraction = interaction;
483
+ }
484
+ }
485
+ }
486
+ if (!bestInteraction)
487
+ return { covered: false };
488
+ return {
489
+ covered: true,
490
+ astMetadata: {
491
+ sourceLanguage: bestInteraction.sourceLanguage,
492
+ resolutionType: bestInteraction.resolutionType,
493
+ confidence: bestInteraction.confidence,
494
+ },
495
+ };
496
+ }
497
+ // ─── Analysis ─────────────────────────────────────────────────────────────────
498
+ /**
499
+ * Scan test files matching testGlob and determine which error scenarios are
500
+ * covered. Optionally augments text-scan results with AST-derived semantic
501
+ * signals when `astOptions` is provided.
502
+ */
503
+ async function analyzeErrorCoverage(scenarios, testGlob, astOptions) {
504
+ const testFiles = await (0, fast_glob_1.default)(testGlob, { onlyFiles: true });
505
+ // Pre-read and segment all test files
506
+ const allSegments = [];
507
+ for (const filePath of testFiles) {
508
+ const contents = fs.readFileSync(filePath, 'utf-8');
509
+ allSegments.push(...extractTestSegments(contents));
510
+ }
511
+ // Build AST interaction map when AST options provided
512
+ const astMap = astOptions
513
+ ? buildErrorAstMap(testFiles, astOptions)
514
+ : null;
515
+ return scenarios.map((scenario) => {
516
+ var _a;
517
+ // ── Text-scan pass ──────────────────────────────────────────────────────
518
+ const matchedTests = [];
519
+ for (const segment of allSegments) {
520
+ if (segmentMentionsEndpoint(segment, scenario) && segmentCoversScenario(segment, scenario)) {
521
+ matchedTests.push(segment.description);
522
+ }
523
+ }
524
+ let covered = matchedTests.length > 0;
525
+ let astMetadata;
526
+ // ── AST augmentation pass ───────────────────────────────────────────────
527
+ if (astMap !== null && !covered) {
528
+ const matchingInteractions = [];
529
+ for (const [, interactions] of astMap) {
530
+ for (const interaction of interactions) {
531
+ if (errorPathMatches(interaction, scenario.path) &&
532
+ interaction.method.toUpperCase() === scenario.method.toUpperCase()) {
533
+ matchingInteractions.push(interaction);
534
+ }
535
+ }
536
+ }
537
+ if (matchingInteractions.length > 0) {
538
+ const astResult = checkAstErrorCoverage(matchingInteractions, scenario);
539
+ if (astResult.covered) {
540
+ covered = true;
541
+ astMetadata = astResult.astMetadata;
542
+ }
543
+ }
544
+ }
545
+ else if (covered && astMap !== null) {
546
+ // Already covered by text-scan — still attach AST metadata if available
547
+ const matchingInteractions = [];
548
+ for (const [, interactions] of astMap) {
549
+ for (const interaction of interactions) {
550
+ if (errorPathMatches(interaction, scenario.path) &&
551
+ interaction.method.toUpperCase() === scenario.method.toUpperCase()) {
552
+ matchingInteractions.push(interaction);
553
+ }
554
+ }
555
+ }
556
+ if (matchingInteractions.length > 0) {
557
+ const best = (_a = matchingInteractions.find((i) => i.confidence === 'high' || i.confidence === 'medium')) !== null && _a !== void 0 ? _a : matchingInteractions[0];
558
+ astMetadata = {
559
+ sourceLanguage: best.sourceLanguage,
560
+ resolutionType: best.resolutionType,
561
+ confidence: best.confidence,
562
+ };
563
+ }
564
+ }
565
+ return { scenario, covered, matchedTests, astMetadata };
566
+ });
567
+ }
568
+ // ─── Report building ──────────────────────────────────────────────────────────
569
+ /**
570
+ * Aggregate error coverage results into a summary report.
571
+ */
572
+ function buildErrorCoverageReport(coverages) {
573
+ const total = coverages.length;
574
+ const covered = coverages.filter((c) => c.covered).length;
575
+ const percentage = total === 0 ? 0 : Math.round((covered / total) * 10000) / 100;
576
+ // Per-category tallies
577
+ const categorySummary = exports.ERROR_CATEGORIES.reduce((acc, cat) => {
578
+ acc[cat] = { total: 0, covered: 0 };
579
+ return acc;
580
+ }, {});
581
+ for (const c of coverages) {
582
+ for (const cat of c.scenario.categories) {
583
+ categorySummary[cat].total += 1;
584
+ if (c.covered)
585
+ categorySummary[cat].covered += 1;
586
+ }
587
+ }
588
+ return { total, covered, percentage, scenarios: coverages, categorySummary };
589
+ }
590
+ // ─── Report generation ────────────────────────────────────────────────────────
591
+ /**
592
+ * Write JSON and HTML error-coverage reports to reportsDir.
593
+ */
594
+ function generateErrorReports(report, reportsDir) {
595
+ if (!fs.existsSync(reportsDir)) {
596
+ fs.mkdirSync(reportsDir, { recursive: true });
597
+ }
598
+ // ── JSON ─────────────────────────────────────────────────────────────────
599
+ const jsonPath = path.join(reportsDir, 'error-coverage.json');
600
+ const jsonReport = {
601
+ total: report.total,
602
+ covered: report.covered,
603
+ percentage: report.percentage,
604
+ categorySummary: report.categorySummary,
605
+ scenarios: report.scenarios.map(({ scenario, covered, matchedTests }) => ({
606
+ id: scenario.id,
607
+ endpoint: scenario.endpoint,
608
+ statusCode: scenario.statusCode,
609
+ description: scenario.description,
610
+ categories: scenario.categories,
611
+ covered,
612
+ matchedTests,
613
+ })),
614
+ };
615
+ fs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), 'utf-8');
616
+ // ── HTML ─────────────────────────────────────────────────────────────────
617
+ const htmlPath = path.join(reportsDir, 'error-coverage.html');
618
+ const catHeaders = exports.ERROR_CATEGORIES.map((c) => `<th>${c}</th>`).join('\n ');
619
+ const rows = report.scenarios
620
+ .map(({ scenario, covered, matchedTests }) => {
621
+ const rowClass = covered ? 'covered' : 'uncovered';
622
+ const statusCell = covered ? '✅ Covered' : '❌ Not covered';
623
+ const catCells = exports.ERROR_CATEGORIES.map((cat) => {
624
+ if (!scenario.categories.includes(cat))
625
+ return '<td class="na">—</td>';
626
+ return covered ? '<td class="yes">✅</td>' : '<td class="no">❌</td>';
627
+ }).join('\n ');
628
+ const testsHint = matchedTests.length > 0 ? matchedTests.map((t) => `• ${t}`).join('<br>') : '—';
629
+ return ` <tr class="${rowClass}" title="${testsHint}">
630
+ <td>${scenario.endpoint}</td>
631
+ <td>${scenario.statusCode}</td>
632
+ <td>${scenario.description}</td>
633
+ <td>${statusCell}</td>
634
+ ${catCells}
635
+ </tr>`;
636
+ })
637
+ .join('\n');
638
+ const catSummaryRows = exports.ERROR_CATEGORIES.map((cat) => {
639
+ const s = report.categorySummary[cat];
640
+ const pct = s.total === 0 ? '—' : `${Math.round((s.covered / s.total) * 100)}%`;
641
+ return ` <tr>
642
+ <td>${cat}</td>
643
+ <td>${s.covered}/${s.total}</td>
644
+ <td>${pct}</td>
645
+ </tr>`;
646
+ }).join('\n');
647
+ const html = `<!DOCTYPE html>
648
+ <html lang="en">
649
+ <head>
650
+ <meta charset="UTF-8">
651
+ <title>Error Coverage Report</title>
652
+ <style>
653
+ body { font-family: sans-serif; padding: 2rem; }
654
+ h1, h2 { margin-bottom: 0.5rem; }
655
+ .summary { margin-bottom: 1.5rem; font-size: 1.1rem; }
656
+ table { border-collapse: collapse; width: 100%; margin-bottom: 2rem; }
657
+ th, td { border: 1px solid #ccc; padding: 0.5rem 1rem; text-align: left; }
658
+ th { background: #f0f0f0; }
659
+ tr.covered { background: #e6ffe6; }
660
+ tr.uncovered { background: #ffe6e6; }
661
+ td.yes { color: #2a7a2a; font-weight: bold; }
662
+ td.no { color: #aa2222; font-weight: bold; }
663
+ td.na { color: #999; }
664
+ </style>
665
+ </head>
666
+ <body>
667
+ <h1>Error Coverage Report</h1>
668
+ <div class="summary">
669
+ Covered: <strong>${report.covered}/${report.total}</strong> error scenarios
670
+ (<strong>${report.percentage}%</strong>)
671
+ </div>
672
+ <table>
673
+ <thead>
674
+ <tr>
675
+ <th>Endpoint</th>
676
+ <th>Status</th>
677
+ <th>Description</th>
678
+ <th>Coverage</th>
679
+ ${catHeaders}
680
+ </tr>
681
+ </thead>
682
+ <tbody>
683
+ ${rows}
684
+ </tbody>
685
+ </table>
686
+ <h2>Category Summary</h2>
687
+ <table>
688
+ <thead>
689
+ <tr><th>Category</th><th>Covered / Total</th><th>%</th></tr>
690
+ </thead>
691
+ <tbody>
692
+ ${catSummaryRows}
693
+ </tbody>
694
+ </table>
695
+ </body>
696
+ </html>`;
697
+ fs.writeFileSync(htmlPath, html, 'utf-8');
698
+ }