@elliemae/encw-leak-runner 1.0.2

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 (261) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/.stylelintignore +4 -0
  3. package/CHANGELOG.md +51 -0
  4. package/README.md +309 -0
  5. package/babel.config.cjs +2 -0
  6. package/bin/leak-runner.ts +9 -0
  7. package/dist/.tsbuildinfo +1 -0
  8. package/dist/bin/leak-runner.js +792 -0
  9. package/dist/cjs/analysis/thresholdEvaluator.js +46 -0
  10. package/dist/cjs/browser/iframeHeapProfiler.js +46 -0
  11. package/dist/cjs/cli/command.js +16 -0
  12. package/dist/cjs/cli/commands/listCommand.js +47 -0
  13. package/dist/cjs/cli/commands/runCommand.js +111 -0
  14. package/dist/cjs/cli/index.js +42 -0
  15. package/dist/cjs/config/missingRequiredParamError.js +34 -0
  16. package/dist/cjs/config/requiredEnvParams.js +57 -0
  17. package/dist/cjs/config/runnerConfigLoader.js +73 -0
  18. package/dist/cjs/config/runnerConfigSchema.js +40 -0
  19. package/dist/cjs/config/sources/cliOverrideConfigSource.js +44 -0
  20. package/dist/cjs/config/sources/configSource.js +35 -0
  21. package/dist/cjs/config/sources/envVarConfigSource.js +41 -0
  22. package/dist/cjs/config/sources/fileConfigSource.js +62 -0
  23. package/dist/cjs/index.js +52 -0
  24. package/dist/cjs/package.json +7 -0
  25. package/dist/cjs/registry/scenarioRegistry.js +51 -0
  26. package/dist/cjs/reporting/consoleReporter.js +60 -0
  27. package/dist/cjs/reporting/junitReporter.js +75 -0
  28. package/dist/cjs/reporting/reporter.js +16 -0
  29. package/dist/cjs/runner/aiEnhancementStep.js +39 -0
  30. package/dist/cjs/runner/batchRunner.js +76 -0
  31. package/dist/cjs/runner/scenarioRunner.js +165 -0
  32. package/dist/cjs/scenarios/index.js +29 -0
  33. package/dist/cjs/scenarios/one-admin/export-navigation.scenario.js +50 -0
  34. package/dist/cjs/scenarios/one-admin/index.js +27 -0
  35. package/dist/cjs/scenarios/one-admin/page-models/ExportPageModel.js +43 -0
  36. package/dist/cjs/scenarios/one-admin/page-models/SelectSettingsPageModel.js +47 -0
  37. package/dist/cjs/scenarios/one-admin/page-models/index.js +26 -0
  38. package/dist/cjs/types/config.js +27 -0
  39. package/dist/cjs/types/results.js +16 -0
  40. package/dist/cjs/types/scenario.js +16 -0
  41. package/dist/esm/analysis/thresholdEvaluator.js +26 -0
  42. package/dist/esm/browser/iframeHeapProfiler.js +26 -0
  43. package/dist/esm/cli/command.js +0 -0
  44. package/dist/esm/cli/commands/listCommand.js +27 -0
  45. package/dist/esm/cli/commands/runCommand.js +93 -0
  46. package/dist/esm/cli/index.js +22 -0
  47. package/dist/esm/config/missingRequiredParamError.js +14 -0
  48. package/dist/esm/config/requiredEnvParams.js +37 -0
  49. package/dist/esm/config/runnerConfigLoader.js +53 -0
  50. package/dist/esm/config/runnerConfigSchema.js +20 -0
  51. package/dist/esm/config/sources/cliOverrideConfigSource.js +24 -0
  52. package/dist/esm/config/sources/configSource.js +15 -0
  53. package/dist/esm/config/sources/envVarConfigSource.js +21 -0
  54. package/dist/esm/config/sources/fileConfigSource.js +34 -0
  55. package/dist/esm/index.js +35 -0
  56. package/dist/esm/package.json +7 -0
  57. package/dist/esm/registry/scenarioRegistry.js +31 -0
  58. package/dist/esm/reporting/consoleReporter.js +40 -0
  59. package/dist/esm/reporting/junitReporter.js +45 -0
  60. package/dist/esm/reporting/reporter.js +0 -0
  61. package/dist/esm/runner/aiEnhancementStep.js +22 -0
  62. package/dist/esm/runner/batchRunner.js +56 -0
  63. package/dist/esm/runner/scenarioRunner.js +137 -0
  64. package/dist/esm/scenarios/index.js +9 -0
  65. package/dist/esm/scenarios/one-admin/export-navigation.scenario.js +33 -0
  66. package/dist/esm/scenarios/one-admin/index.js +7 -0
  67. package/dist/esm/scenarios/one-admin/page-models/ExportPageModel.js +23 -0
  68. package/dist/esm/scenarios/one-admin/page-models/SelectSettingsPageModel.js +27 -0
  69. package/dist/esm/scenarios/one-admin/page-models/index.js +6 -0
  70. package/dist/esm/types/config.js +7 -0
  71. package/dist/esm/types/results.js +0 -0
  72. package/dist/esm/types/scenario.js +0 -0
  73. package/dist/types/bin/leak-runner.d.ts +2 -0
  74. package/dist/types/lib/analysis/tests/thresholdEvaluator.test.d.ts +1 -0
  75. package/dist/types/lib/analysis/thresholdEvaluator.d.ts +6 -0
  76. package/dist/types/lib/browser/iframeHeapProfiler.d.ts +9 -0
  77. package/dist/types/lib/browser/tests/iframeHeapProfiler.test.d.ts +1 -0
  78. package/dist/types/lib/cli/command.d.ts +17 -0
  79. package/dist/types/lib/cli/commands/listCommand.d.ts +5 -0
  80. package/dist/types/lib/cli/commands/runCommand.d.ts +7 -0
  81. package/dist/types/lib/cli/index.d.ts +4 -0
  82. package/dist/types/lib/config/missingRequiredParamError.d.ts +4 -0
  83. package/dist/types/lib/config/requiredEnvParams.d.ts +16 -0
  84. package/dist/types/lib/config/runnerConfigLoader.d.ts +13 -0
  85. package/dist/types/lib/config/runnerConfigSchema.d.ts +78 -0
  86. package/dist/types/lib/config/sources/cliOverrideConfigSource.d.ts +14 -0
  87. package/dist/types/lib/config/sources/configSource.d.ts +14 -0
  88. package/dist/types/lib/config/sources/envVarConfigSource.d.ts +7 -0
  89. package/dist/types/lib/config/sources/fileConfigSource.d.ts +9 -0
  90. package/dist/types/lib/config/tests/cliOverrideConfigSource.test.d.ts +1 -0
  91. package/dist/types/lib/config/tests/envVarConfigSource.test.d.ts +1 -0
  92. package/dist/types/lib/config/tests/fileConfigSource.test.d.ts +1 -0
  93. package/dist/types/lib/config/tests/requiredEnvParams.test.d.ts +1 -0
  94. package/dist/types/lib/config/tests/runnerConfigLoader.test.d.ts +1 -0
  95. package/dist/types/lib/index.d.ts +18 -0
  96. package/dist/types/lib/registry/scenarioRegistry.d.ts +18 -0
  97. package/dist/types/lib/registry/tests/scenarioRegistry.test.d.ts +1 -0
  98. package/dist/types/lib/reporting/consoleReporter.d.ts +5 -0
  99. package/dist/types/lib/reporting/junitReporter.d.ts +5 -0
  100. package/dist/types/lib/reporting/reporter.d.ts +4 -0
  101. package/dist/types/lib/reporting/tests/consoleReporter.test.d.ts +1 -0
  102. package/dist/types/lib/reporting/tests/junitReporter.test.d.ts +1 -0
  103. package/dist/types/lib/runner/aiEnhancementStep.d.ts +15 -0
  104. package/dist/types/lib/runner/batchRunner.d.ts +14 -0
  105. package/dist/types/lib/runner/scenarioRunner.d.ts +15 -0
  106. package/dist/types/lib/runner/tests/aiEnhancementStep.test.d.ts +1 -0
  107. package/dist/types/lib/runner/tests/batchRunner.test.d.ts +1 -0
  108. package/dist/types/lib/runner/tests/scenarioRunner.test.d.ts +1 -0
  109. package/dist/types/lib/scenarios/index.d.ts +2 -0
  110. package/dist/types/lib/scenarios/one-admin/export-navigation.scenario.d.ts +2 -0
  111. package/dist/types/lib/scenarios/one-admin/index.d.ts +2 -0
  112. package/dist/types/lib/scenarios/one-admin/page-models/ExportPageModel.d.ts +8 -0
  113. package/dist/types/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.d.ts +10 -0
  114. package/dist/types/lib/scenarios/one-admin/page-models/index.d.ts +2 -0
  115. package/dist/types/lib/types/config.d.ts +26 -0
  116. package/dist/types/lib/types/results.d.ts +19 -0
  117. package/dist/types/lib/types/scenario.d.ts +17 -0
  118. package/jest.config.cjs +9 -0
  119. package/leak-runner.config.json +13 -0
  120. package/leak-runner.schema.json +27 -0
  121. package/lib/analysis/tests/thresholdEvaluator.test.ts +125 -0
  122. package/lib/analysis/thresholdEvaluator.ts +36 -0
  123. package/lib/browser/iframeHeapProfiler.ts +30 -0
  124. package/lib/browser/tests/iframeHeapProfiler.test.ts +71 -0
  125. package/lib/cli/command.ts +19 -0
  126. package/lib/cli/commands/listCommand.ts +36 -0
  127. package/lib/cli/commands/runCommand.ts +126 -0
  128. package/lib/cli/index.ts +25 -0
  129. package/lib/config/missingRequiredParamError.ts +10 -0
  130. package/lib/config/requiredEnvParams.ts +50 -0
  131. package/lib/config/runnerConfigLoader.ts +84 -0
  132. package/lib/config/runnerConfigSchema.ts +27 -0
  133. package/lib/config/sources/cliOverrideConfigSource.ts +30 -0
  134. package/lib/config/sources/configSource.ts +27 -0
  135. package/lib/config/sources/envVarConfigSource.ts +23 -0
  136. package/lib/config/sources/fileConfigSource.ts +39 -0
  137. package/lib/config/tests/cliOverrideConfigSource.test.ts +25 -0
  138. package/lib/config/tests/envVarConfigSource.test.ts +57 -0
  139. package/lib/config/tests/fileConfigSource.test.ts +49 -0
  140. package/lib/config/tests/requiredEnvParams.test.ts +113 -0
  141. package/lib/config/tests/runnerConfigLoader.test.ts +59 -0
  142. package/lib/index.ts +37 -0
  143. package/lib/registry/scenarioRegistry.ts +48 -0
  144. package/lib/registry/tests/scenarioRegistry.test.ts +96 -0
  145. package/lib/reporting/consoleReporter.ts +48 -0
  146. package/lib/reporting/junitReporter.ts +62 -0
  147. package/lib/reporting/reporter.ts +5 -0
  148. package/lib/reporting/tests/consoleReporter.test.ts +82 -0
  149. package/lib/reporting/tests/junitReporter.test.ts +103 -0
  150. package/lib/runner/aiEnhancementStep.ts +39 -0
  151. package/lib/runner/batchRunner.ts +71 -0
  152. package/lib/runner/scenarioRunner.ts +189 -0
  153. package/lib/runner/tests/aiEnhancementStep.test.ts +174 -0
  154. package/lib/runner/tests/batchRunner.test.ts +133 -0
  155. package/lib/runner/tests/scenarioRunner.test.ts +162 -0
  156. package/lib/scenarios/index.ts +8 -0
  157. package/lib/scenarios/one-admin/export-navigation.scenario.ts +38 -0
  158. package/lib/scenarios/one-admin/index.ts +6 -0
  159. package/lib/scenarios/one-admin/page-models/ExportPageModel.ts +26 -0
  160. package/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.ts +30 -0
  161. package/lib/scenarios/one-admin/page-models/index.ts +2 -0
  162. package/lib/types/config.ts +34 -0
  163. package/lib/types/results.ts +22 -0
  164. package/lib/types/scenario.ts +18 -0
  165. package/package.json +46 -0
  166. package/reports/analysis/index.html +116 -0
  167. package/reports/analysis/thresholdEvaluator.ts.html +193 -0
  168. package/reports/base.css +224 -0
  169. package/reports/block-navigation.js +87 -0
  170. package/reports/browser/iframeHeapProfiler.ts.html +175 -0
  171. package/reports/browser/index.html +116 -0
  172. package/reports/cli/commands/index.html +131 -0
  173. package/reports/cli/commands/listCommand.ts.html +193 -0
  174. package/reports/cli/commands/runCommand.ts.html +463 -0
  175. package/reports/cli/index.html +116 -0
  176. package/reports/cli/index.ts.html +160 -0
  177. package/reports/config/index.html +161 -0
  178. package/reports/config/missingRequiredParamError.ts.html +115 -0
  179. package/reports/config/requiredEnvParams.ts.html +235 -0
  180. package/reports/config/runnerConfigLoader.ts.html +337 -0
  181. package/reports/config/runnerConfigSchema.ts.html +166 -0
  182. package/reports/config/sources/cliOverrideConfigSource.ts.html +175 -0
  183. package/reports/config/sources/configSource.ts.html +166 -0
  184. package/reports/config/sources/envVarConfigSource.ts.html +154 -0
  185. package/reports/config/sources/fileConfigSource.ts.html +202 -0
  186. package/reports/config/sources/index.html +161 -0
  187. package/reports/favicon.png +0 -0
  188. package/reports/index.html +296 -0
  189. package/reports/lcov-report/analysis/index.html +116 -0
  190. package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +193 -0
  191. package/reports/lcov-report/base.css +224 -0
  192. package/reports/lcov-report/block-navigation.js +87 -0
  193. package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +175 -0
  194. package/reports/lcov-report/browser/index.html +116 -0
  195. package/reports/lcov-report/cli/commands/index.html +131 -0
  196. package/reports/lcov-report/cli/commands/listCommand.ts.html +193 -0
  197. package/reports/lcov-report/cli/commands/runCommand.ts.html +463 -0
  198. package/reports/lcov-report/cli/index.html +116 -0
  199. package/reports/lcov-report/cli/index.ts.html +160 -0
  200. package/reports/lcov-report/config/index.html +161 -0
  201. package/reports/lcov-report/config/missingRequiredParamError.ts.html +115 -0
  202. package/reports/lcov-report/config/requiredEnvParams.ts.html +235 -0
  203. package/reports/lcov-report/config/runnerConfigLoader.ts.html +337 -0
  204. package/reports/lcov-report/config/runnerConfigSchema.ts.html +166 -0
  205. package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +175 -0
  206. package/reports/lcov-report/config/sources/configSource.ts.html +166 -0
  207. package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +154 -0
  208. package/reports/lcov-report/config/sources/fileConfigSource.ts.html +202 -0
  209. package/reports/lcov-report/config/sources/index.html +161 -0
  210. package/reports/lcov-report/favicon.png +0 -0
  211. package/reports/lcov-report/index.html +296 -0
  212. package/reports/lcov-report/prettify.css +1 -0
  213. package/reports/lcov-report/prettify.js +2 -0
  214. package/reports/lcov-report/registry/index.html +116 -0
  215. package/reports/lcov-report/registry/scenarioRegistry.ts.html +229 -0
  216. package/reports/lcov-report/reporting/consoleReporter.ts.html +229 -0
  217. package/reports/lcov-report/reporting/index.html +131 -0
  218. package/reports/lcov-report/reporting/junitReporter.ts.html +271 -0
  219. package/reports/lcov-report/runner/aiEnhancementStep.ts.html +202 -0
  220. package/reports/lcov-report/runner/batchRunner.ts.html +298 -0
  221. package/reports/lcov-report/runner/index.html +146 -0
  222. package/reports/lcov-report/runner/scenarioRunner.ts.html +652 -0
  223. package/reports/lcov-report/scenarios/index.html +116 -0
  224. package/reports/lcov-report/scenarios/index.ts.html +109 -0
  225. package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
  226. package/reports/lcov-report/scenarios/one-admin/index.html +131 -0
  227. package/reports/lcov-report/scenarios/one-admin/index.ts.html +103 -0
  228. package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
  229. package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
  230. package/reports/lcov-report/scenarios/one-admin/page-models/index.html +131 -0
  231. package/reports/lcov-report/sort-arrow-sprite.png +0 -0
  232. package/reports/lcov-report/sorter.js +210 -0
  233. package/reports/lcov-report/types/config.ts.html +187 -0
  234. package/reports/lcov-report/types/index.html +116 -0
  235. package/reports/lcov.info +883 -0
  236. package/reports/prettify.css +1 -0
  237. package/reports/prettify.js +2 -0
  238. package/reports/registry/index.html +116 -0
  239. package/reports/registry/scenarioRegistry.ts.html +229 -0
  240. package/reports/reporting/consoleReporter.ts.html +229 -0
  241. package/reports/reporting/index.html +131 -0
  242. package/reports/reporting/junitReporter.ts.html +271 -0
  243. package/reports/runner/aiEnhancementStep.ts.html +202 -0
  244. package/reports/runner/batchRunner.ts.html +298 -0
  245. package/reports/runner/index.html +146 -0
  246. package/reports/runner/scenarioRunner.ts.html +652 -0
  247. package/reports/scenarios/index.html +116 -0
  248. package/reports/scenarios/index.ts.html +109 -0
  249. package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
  250. package/reports/scenarios/one-admin/index.html +131 -0
  251. package/reports/scenarios/one-admin/index.ts.html +103 -0
  252. package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
  253. package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
  254. package/reports/scenarios/one-admin/page-models/index.html +131 -0
  255. package/reports/sort-arrow-sprite.png +0 -0
  256. package/reports/sorter.js +210 -0
  257. package/reports/types/config.ts.html +187 -0
  258. package/reports/types/index.html +116 -0
  259. package/stylelint.config.cjs +2 -0
  260. package/test-report.xml +100 -0
  261. package/tsconfig.json +12 -0
@@ -0,0 +1,49 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { FileConfigSource } from '../sources/fileConfigSource.js';
5
+
6
+ function makeTempFile(content: string): string {
7
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'leak-runner-cfg-'));
8
+ const file = path.join(dir, 'leak-runner.config.json');
9
+ fs.writeFileSync(file, content);
10
+ return file;
11
+ }
12
+
13
+ describe('FileConfigSource', () => {
14
+ it('returns an empty payload when the file does not exist', () => {
15
+ const src = new FileConfigSource('/path/that/does/not/exist.json');
16
+ expect(src.load()).toEqual({});
17
+ });
18
+
19
+ it('parses and validates a well-formed config file', () => {
20
+ const file = makeTempFile(
21
+ JSON.stringify({
22
+ runner: { headless: false, outputDir: './out/', topN: 3 },
23
+ ai: { enabled: true, model: 'Claude3.7', temperature: 0.2 },
24
+ }),
25
+ );
26
+ const src = new FileConfigSource(file);
27
+ expect(src.load()).toEqual({
28
+ runner: { headless: false, outputDir: './out/', topN: 3 },
29
+ ai: { enabled: true, model: 'Claude3.7', temperature: 0.2 },
30
+ });
31
+ });
32
+
33
+ it('throws a descriptive error when the JSON is invalid', () => {
34
+ const file = makeTempFile('{not-valid-json');
35
+ const src = new FileConfigSource(file);
36
+ expect(() => src.load()).toThrow(/Failed to parse config file/);
37
+ });
38
+
39
+ it('throws a descriptive error when the schema is violated', () => {
40
+ const file = makeTempFile(JSON.stringify({ runner: { topN: 'banana' } }));
41
+ const src = new FileConfigSource(file);
42
+ expect(() => src.load()).toThrow(/Invalid config file/);
43
+ });
44
+
45
+ it('exposes priority 1 (lowest tier above defaults)', () => {
46
+ const src = new FileConfigSource('/anything');
47
+ expect(src.priority).toBe(1);
48
+ });
49
+ });
@@ -0,0 +1,113 @@
1
+ import { RequiredEnvParamsResolver } from '../requiredEnvParams.js';
2
+ import { MissingRequiredParamError } from '../missingRequiredParamError.js';
3
+
4
+ describe('RequiredEnvParamsResolver', () => {
5
+ const ORIGINAL_ENV = process.env;
6
+
7
+ beforeEach(() => {
8
+ process.env = { ...ORIGINAL_ENV };
9
+ delete process.env.BASE_URL;
10
+ delete process.env.ENCW_INSTANCE_ID;
11
+ delete process.env.ENCW_USER_ID;
12
+ delete process.env.ENCW_PASSWORD;
13
+ });
14
+
15
+ afterAll(() => {
16
+ process.env = ORIGINAL_ENV;
17
+ });
18
+
19
+ it('throws MissingRequiredParamError listing every missing field when nothing is provided', () => {
20
+ const resolver = new RequiredEnvParamsResolver();
21
+ let caught: unknown;
22
+ try {
23
+ resolver.resolve({});
24
+ } catch (e) {
25
+ caught = e;
26
+ }
27
+ expect(caught).toBeInstanceOf(MissingRequiredParamError);
28
+ const err = caught as MissingRequiredParamError;
29
+ expect(err.missing).toEqual([
30
+ 'baseUrl',
31
+ 'instanceId',
32
+ 'userId',
33
+ 'password (ENCW_PASSWORD)',
34
+ ]);
35
+ expect(err.message).toContain('--base-url');
36
+ expect(err.message).toContain('ENCW_PASSWORD');
37
+ });
38
+
39
+ it('reads from env vars when CLI options are absent', () => {
40
+ process.env.BASE_URL = 'https://q3.elliemae.io';
41
+ process.env.ENCW_INSTANCE_ID = 'BE1';
42
+ process.env.ENCW_USER_ID = 'admin';
43
+ process.env.ENCW_PASSWORD = 'pw';
44
+
45
+ const resolver = new RequiredEnvParamsResolver();
46
+ expect(resolver.resolve({})).toEqual({
47
+ baseUrl: 'https://q3.elliemae.io',
48
+ instanceId: 'BE1',
49
+ userId: 'admin',
50
+ password: 'pw',
51
+ });
52
+ });
53
+
54
+ it('CLI options override env vars for non-secret fields', () => {
55
+ process.env.BASE_URL = 'https://q3.elliemae.io';
56
+ process.env.ENCW_INSTANCE_ID = 'BE1';
57
+ process.env.ENCW_USER_ID = 'admin';
58
+ process.env.ENCW_PASSWORD = 'pw';
59
+
60
+ const resolver = new RequiredEnvParamsResolver();
61
+ expect(
62
+ resolver.resolve({
63
+ baseUrl: 'https://q4.elliemae.io',
64
+ instanceId: 'BE2',
65
+ userId: 'user2',
66
+ }),
67
+ ).toEqual({
68
+ baseUrl: 'https://q4.elliemae.io',
69
+ instanceId: 'BE2',
70
+ userId: 'user2',
71
+ password: 'pw',
72
+ });
73
+ });
74
+
75
+ it('password is read only from process.env (no CLI flag for secrets)', () => {
76
+ process.env.BASE_URL = 'https://q3.elliemae.io';
77
+ process.env.ENCW_INSTANCE_ID = 'BE1';
78
+ process.env.ENCW_USER_ID = 'admin';
79
+ // ENCW_PASSWORD intentionally unset
80
+
81
+ const resolver = new RequiredEnvParamsResolver();
82
+ expect(() => resolver.resolve({})).toThrow(MissingRequiredParamError);
83
+ });
84
+
85
+ it('reports only the missing fields when some are supplied', () => {
86
+ process.env.BASE_URL = 'https://q3.elliemae.io';
87
+ // ENCW_INSTANCE_ID, ENCW_USER_ID, ENCW_PASSWORD missing
88
+
89
+ const resolver = new RequiredEnvParamsResolver();
90
+ let caught: unknown;
91
+ try {
92
+ resolver.resolve({});
93
+ } catch (e) {
94
+ caught = e;
95
+ }
96
+ const err = caught as MissingRequiredParamError;
97
+ expect(err.missing).toEqual([
98
+ 'instanceId',
99
+ 'userId',
100
+ 'password (ENCW_PASSWORD)',
101
+ ]);
102
+ });
103
+
104
+ it('treats empty-string env vars as missing', () => {
105
+ process.env.BASE_URL = '';
106
+ process.env.ENCW_INSTANCE_ID = '';
107
+ process.env.ENCW_USER_ID = '';
108
+ process.env.ENCW_PASSWORD = '';
109
+
110
+ const resolver = new RequiredEnvParamsResolver();
111
+ expect(() => resolver.resolve({})).toThrow(/Missing required parameter/);
112
+ });
113
+ });
@@ -0,0 +1,59 @@
1
+ import { RunnerConfigLoader } from '../runnerConfigLoader.js';
2
+ import type { ConfigSource } from '../sources/configSource.js';
3
+ import type { RawRunnerConfigFile } from '../runnerConfigSchema.js';
4
+
5
+ function fakeSource(
6
+ priority: number,
7
+ name: string,
8
+ payload: Partial<RawRunnerConfigFile>,
9
+ ): ConfigSource {
10
+ return { priority, name, load: () => payload };
11
+ }
12
+
13
+ describe('RunnerConfigLoader', () => {
14
+ it('produces built-in defaults when no source contributes', () => {
15
+ const loader = new RunnerConfigLoader([]);
16
+ const resolved = loader.resolveOptions();
17
+ expect(resolved.runner).toEqual({
18
+ headless: true,
19
+ outputDir: './leak-reports/',
20
+ topN: 5,
21
+ });
22
+ expect(resolved.aiEnabled).toBe(false);
23
+ });
24
+
25
+ it('higher-priority sources override lower-priority sources', () => {
26
+ const file = fakeSource(1, 'file', { runner: { topN: 1, headless: true } });
27
+ const env = fakeSource(2, 'env', { runner: { topN: 2 } });
28
+ const cli = fakeSource(3, 'cli', { runner: { topN: 9 } });
29
+ const loader = new RunnerConfigLoader([file, env, cli]);
30
+ expect(loader.resolveOptions().runner.topN).toBe(9);
31
+ });
32
+
33
+ it('merges shallow keys without dropping unrelated fields', () => {
34
+ const file = fakeSource(1, 'file', {
35
+ runner: { headless: false, outputDir: './out/' },
36
+ });
37
+ const cli = fakeSource(3, 'cli', { runner: { topN: 8 } });
38
+ const loader = new RunnerConfigLoader([file, cli]);
39
+ expect(loader.resolveOptions().runner).toEqual({
40
+ headless: false,
41
+ outputDir: './out/',
42
+ topN: 8,
43
+ });
44
+ });
45
+
46
+ it('exposes ai defaults and reports aiEnabled=false when not set', () => {
47
+ const loader = new RunnerConfigLoader([]);
48
+ const resolved = loader.resolveOptions();
49
+ expect(resolved.aiEnabled).toBe(false);
50
+ expect(resolved.aiModel).toBe('Claude3.7');
51
+ expect(resolved.aiTemperature).toBe(0.3);
52
+ });
53
+
54
+ it('reports aiEnabled=true when a source enables it', () => {
55
+ const file = fakeSource(1, 'file', { ai: { enabled: true } });
56
+ const loader = new RunnerConfigLoader([file]);
57
+ expect(loader.resolveOptions().aiEnabled).toBe(true);
58
+ });
59
+ });
package/lib/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ // === Tier 1 — Scenario authoring (the most-used surface) ===
2
+ export type { MicroappLeakScenario } from './types/scenario.js';
3
+ export type {
4
+ EnvironmentParams,
5
+ RunnerOptions,
6
+ AiConfig,
7
+ RunnerConfig,
8
+ ResolvedThresholds,
9
+ } from './types/config.js';
10
+ export { DEFAULT_THRESHOLDS } from './types/config.js';
11
+ export type {
12
+ ScenarioGroup,
13
+ ScenarioEntry,
14
+ } from './registry/scenarioRegistry.js';
15
+ export type {
16
+ ThresholdResult,
17
+ ScenarioResult,
18
+ RunSummary,
19
+ } from './types/results.js';
20
+
21
+ // === Tier 2 — Embedding (UNSTABLE — no backwards-compat guarantee yet) ===
22
+ export { ScenarioRegistry } from './registry/scenarioRegistry.js';
23
+ export { BatchRunner } from './runner/batchRunner.js';
24
+ export { ScenarioRunner } from './runner/scenarioRunner.js';
25
+ export { IframeHeapProfiler } from './browser/iframeHeapProfiler.js';
26
+ export { ThresholdEvaluator } from './analysis/thresholdEvaluator.js';
27
+ export { ConsoleReporter } from './reporting/consoleReporter.js';
28
+ export { JunitReporter } from './reporting/junitReporter.js';
29
+ export { RunnerConfigLoader } from './config/runnerConfigLoader.js';
30
+ export { FileConfigSource } from './config/sources/fileConfigSource.js';
31
+ export { EnvVarConfigSource } from './config/sources/envVarConfigSource.js';
32
+ export { CliOverrideConfigSource } from './config/sources/cliOverrideConfigSource.js';
33
+ export {
34
+ RequiredEnvParamsResolver,
35
+ MissingRequiredParamError,
36
+ } from './config/requiredEnvParams.js';
37
+ export { buildProgram, defaultDeps } from './cli/index.js';
@@ -0,0 +1,48 @@
1
+ import type { MicroappLeakScenario } from '../types/scenario.js';
2
+
3
+ export interface ScenarioGroup {
4
+ readonly microapp: string;
5
+ readonly scenarios: readonly MicroappLeakScenario[];
6
+ }
7
+
8
+ export interface ScenarioEntry {
9
+ readonly key: string;
10
+ readonly scenario: MicroappLeakScenario;
11
+ }
12
+
13
+ export class ScenarioRegistry {
14
+ private readonly entries = new Map<string, MicroappLeakScenario>();
15
+
16
+ register(group: ScenarioGroup): this {
17
+ group.scenarios.forEach((scenario) => {
18
+ const key = `${group.microapp}/${scenario.id}`;
19
+ if (this.entries.has(key)) {
20
+ throw new Error(`Duplicate scenario: ${key}`);
21
+ }
22
+ this.entries.set(key, scenario);
23
+ });
24
+ return this;
25
+ }
26
+
27
+ get(key: string): MicroappLeakScenario | undefined {
28
+ return this.entries.get(key);
29
+ }
30
+
31
+ has(key: string): boolean {
32
+ return this.entries.has(key);
33
+ }
34
+
35
+ size(): number {
36
+ return this.entries.size;
37
+ }
38
+
39
+ list(): readonly ScenarioEntry[] {
40
+ return Array.from(this.entries.entries())
41
+ .map(([key, scenario]) => ({ key, scenario }))
42
+ .sort((a, b) => a.key.localeCompare(b.key));
43
+ }
44
+
45
+ filterByTag(tag: string): readonly ScenarioEntry[] {
46
+ return this.list().filter((entry) => entry.scenario.tags?.includes(tag));
47
+ }
48
+ }
@@ -0,0 +1,96 @@
1
+ import type { MicroappLeakScenario } from '../../types/scenario.js';
2
+ import { ScenarioRegistry } from '../scenarioRegistry.js';
3
+
4
+ function makeScenario(
5
+ id: string,
6
+ overrides: Partial<MicroappLeakScenario> = {},
7
+ ): MicroappLeakScenario {
8
+ return {
9
+ id,
10
+ name: id.replace(/-/g, ' '),
11
+ description: `${id} description`,
12
+ microappSelector: `iframe#${id}`,
13
+ url: () => `/${id}`,
14
+ action: () => Promise.resolve(),
15
+ ...overrides,
16
+ };
17
+ }
18
+
19
+ describe('ScenarioRegistry', () => {
20
+ it('starts empty', () => {
21
+ expect(new ScenarioRegistry().size()).toBe(0);
22
+ });
23
+
24
+ it('builds keys as <microapp>/<scenario.id> when registering a group', () => {
25
+ const reg = new ScenarioRegistry().register({
26
+ microapp: 'one-admin',
27
+ scenarios: [makeScenario('export-navigation')],
28
+ });
29
+ expect(reg.size()).toBe(1);
30
+ expect(reg.has('one-admin/export-navigation')).toBe(true);
31
+ });
32
+
33
+ it('register() returns `this` to support chaining', () => {
34
+ const reg = new ScenarioRegistry();
35
+ const out = reg.register({ microapp: 'one-admin', scenarios: [] });
36
+ expect(out).toBe(reg);
37
+ });
38
+
39
+ it('throws when registering a duplicate key', () => {
40
+ const reg = new ScenarioRegistry().register({
41
+ microapp: 'one-admin',
42
+ scenarios: [makeScenario('export-navigation')],
43
+ });
44
+ expect(() =>
45
+ reg.register({
46
+ microapp: 'one-admin',
47
+ scenarios: [makeScenario('export-navigation')],
48
+ }),
49
+ ).toThrow('Duplicate scenario: one-admin/export-navigation');
50
+ });
51
+
52
+ it('get() returns the registered scenario', () => {
53
+ const scenario = makeScenario('export-navigation');
54
+ const reg = new ScenarioRegistry().register({
55
+ microapp: 'one-admin',
56
+ scenarios: [scenario],
57
+ });
58
+ expect(reg.get('one-admin/export-navigation')).toBe(scenario);
59
+ });
60
+
61
+ it('get() returns undefined for an unknown key', () => {
62
+ const reg = new ScenarioRegistry();
63
+ expect(reg.get('unknown/scenario')).toBeUndefined();
64
+ });
65
+
66
+ it('list() returns entries sorted by key', () => {
67
+ const reg = new ScenarioRegistry()
68
+ .register({ microapp: 'beta', scenarios: [makeScenario('two')] })
69
+ .register({ microapp: 'alpha', scenarios: [makeScenario('one')] });
70
+
71
+ const keys = reg.list().map((entry) => entry.key);
72
+ expect(keys).toEqual(['alpha/one', 'beta/two']);
73
+ });
74
+
75
+ it('filterByTag() returns only scenarios that include the tag', () => {
76
+ const reg = new ScenarioRegistry().register({
77
+ microapp: 'one-admin',
78
+ scenarios: [
79
+ makeScenario('a', { tags: ['critical'] }),
80
+ makeScenario('b', { tags: ['optional'] }),
81
+ makeScenario('c', { tags: ['critical', 'smoke'] }),
82
+ ],
83
+ });
84
+
85
+ const keys = reg.filterByTag('critical').map((entry) => entry.key);
86
+ expect(keys).toEqual(['one-admin/a', 'one-admin/c']);
87
+ });
88
+
89
+ it('filterByTag() returns an empty array when no scenarios match', () => {
90
+ const reg = new ScenarioRegistry().register({
91
+ microapp: 'one-admin',
92
+ scenarios: [makeScenario('a', { tags: ['critical'] })],
93
+ });
94
+ expect(reg.filterByTag('missing')).toEqual([]);
95
+ });
96
+ });
@@ -0,0 +1,48 @@
1
+ import type { Reporter } from './reporter.js';
2
+ import type { RunSummary, ScenarioResult } from '../types/results.js';
3
+
4
+ const GREEN = '\x1b[32m';
5
+ const RED = '\x1b[31m';
6
+ const BOLD = '\x1b[1m';
7
+ const RESET = '\x1b[0m';
8
+
9
+ function formatDuration(ms: number): string {
10
+ return `${(ms / 1000).toFixed(1)}s`;
11
+ }
12
+
13
+ function printLine(text: string): void {
14
+ process.stdout.write(`${text}\n`);
15
+ }
16
+
17
+ function printScenario(result: ScenarioResult): void {
18
+ const status = result.passed
19
+ ? `${GREEN}PASSED${RESET}`
20
+ : `${RED}FAILED${RESET}`;
21
+ const duration = formatDuration(result.durationMs);
22
+ printLine(` ${status} ${BOLD}${result.name}${RESET} (${duration})`);
23
+ if (!result.passed && result.thresholdResult.reason) {
24
+ printLine(` ${RED}→ ${result.thresholdResult.reason}${RESET}`);
25
+ }
26
+ if (!result.passed && result.error) {
27
+ printLine(` ${RED}→ Error: ${result.error.message}${RESET}`);
28
+ }
29
+ }
30
+
31
+ export class ConsoleReporter implements Reporter {
32
+ write(summary: RunSummary): void {
33
+ printLine('');
34
+ printLine(`${BOLD}MEMORY LEAK RUN RESULTS${RESET}`);
35
+ printLine('─'.repeat(50));
36
+
37
+ summary.results.forEach(printScenario);
38
+
39
+ printLine('─'.repeat(50));
40
+ const total = summary.results.length;
41
+ const duration = formatDuration(summary.totalDurationMs);
42
+ printLine(
43
+ `${summary.passCount} passed, ${summary.failCount} failed ` +
44
+ `of ${total} scenarios (${duration})`,
45
+ );
46
+ printLine('');
47
+ }
48
+ }
@@ -0,0 +1,62 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import type { Reporter } from './reporter.js';
4
+ import type { RunSummary, ScenarioResult } from '../types/results.js';
5
+
6
+ function escapeXml(str: string): string {
7
+ return str
8
+ .replace(/&/g, '&amp;')
9
+ .replace(/</g, '&lt;')
10
+ .replace(/>/g, '&gt;')
11
+ .replace(/"/g, '&quot;')
12
+ .replace(/'/g, '&apos;');
13
+ }
14
+
15
+ function formatSeconds(ms: number): string {
16
+ return (ms / 1000).toFixed(1);
17
+ }
18
+
19
+ function renderTestCase(result: ScenarioResult): string {
20
+ const timeAttr = `time="${formatSeconds(result.durationMs)}"`;
21
+ const open = ` <testcase name="${escapeXml(
22
+ result.name,
23
+ )}" classname="memory-leak" ${timeAttr}>`;
24
+
25
+ if (result.passed) {
26
+ return `${open}\n </testcase>`;
27
+ }
28
+
29
+ const message =
30
+ result.thresholdResult.reason ?? result.error?.message ?? 'Unknown failure';
31
+ const body = result.thresholdResult.reason
32
+ ? `See leak-reports/${result.name}.md for full analysis`
33
+ : result.error?.stack ?? '';
34
+
35
+ return (
36
+ `${open}\n` +
37
+ ` <failure message="${escapeXml(message)}">\n` +
38
+ ` ${escapeXml(body)}\n` +
39
+ ` </failure>\n` +
40
+ ` </testcase>`
41
+ );
42
+ }
43
+
44
+ export class JunitReporter implements Reporter {
45
+ write(summary: RunSummary, outputDir: string): Promise<void> {
46
+ const totalTime = formatSeconds(summary.totalDurationMs);
47
+ const testCases = summary.results.map(renderTestCase).join('\n');
48
+
49
+ const xml = [
50
+ '<?xml version="1.0" encoding="UTF-8"?>',
51
+ `<testsuites name="encw-leak-runner" time="${totalTime}">`,
52
+ ` <testsuite name="microapp-memory-leaks" tests="${summary.results.length}" failures="${summary.failCount}" time="${totalTime}">`,
53
+ testCases,
54
+ ' </testsuite>',
55
+ '</testsuites>',
56
+ ].join('\n');
57
+
58
+ fs.mkdirSync(outputDir, { recursive: true });
59
+ fs.writeFileSync(path.join(outputDir, 'test-results.xml'), xml, 'utf8');
60
+ return Promise.resolve();
61
+ }
62
+ }
@@ -0,0 +1,5 @@
1
+ import type { RunSummary } from '../types/results.js';
2
+
3
+ export interface Reporter {
4
+ write(summary: RunSummary, outputDir: string): void | Promise<void>;
5
+ }
@@ -0,0 +1,82 @@
1
+ import { ConsoleReporter } from '../consoleReporter.js';
2
+ import type { RunSummary } from '../../types/results.js';
3
+
4
+ function makeSummary(overrides: Partial<RunSummary> = {}): RunSummary {
5
+ return {
6
+ results: [],
7
+ totalDurationMs: 5000,
8
+ passCount: 0,
9
+ failCount: 0,
10
+ ...overrides,
11
+ };
12
+ }
13
+
14
+ describe('ConsoleReporter', () => {
15
+ let reporter: ConsoleReporter;
16
+ type StdSpy = jest.SpyInstance<boolean, [string | Uint8Array, ...unknown[]]>;
17
+ let stdoutSpy: StdSpy;
18
+
19
+ beforeEach(() => {
20
+ reporter = new ConsoleReporter();
21
+ stdoutSpy = jest
22
+ .spyOn(process.stdout, 'write')
23
+ .mockImplementation(() => true) as StdSpy;
24
+ });
25
+
26
+ afterEach(() => {
27
+ stdoutSpy.mockRestore();
28
+ });
29
+
30
+ it('prints PASSED for a passing scenario', () => {
31
+ const summary = makeSummary({
32
+ passCount: 1,
33
+ results: [
34
+ {
35
+ name: 'loan-pipeline',
36
+ passed: true,
37
+ thresholdResult: { passed: true, reason: null },
38
+ report: null,
39
+ durationMs: 2000,
40
+ error: null,
41
+ },
42
+ ],
43
+ });
44
+
45
+ reporter.write(summary);
46
+ const output = stdoutSpy.mock.calls.map((c) => String(c[0])).join('');
47
+ expect(output).toContain('loan-pipeline');
48
+ expect(output).toContain('PASSED');
49
+ });
50
+
51
+ it('prints FAILED with reason for a failing scenario', () => {
52
+ const summary = makeSummary({
53
+ failCount: 1,
54
+ results: [
55
+ {
56
+ name: 'document-center',
57
+ passed: false,
58
+ thresholdResult: {
59
+ passed: false,
60
+ reason: 'Retained size delta 18.4 MB exceeds threshold 10.0 MB',
61
+ },
62
+ report: null,
63
+ durationMs: 3000,
64
+ error: null,
65
+ },
66
+ ],
67
+ });
68
+
69
+ reporter.write(summary);
70
+ const output = stdoutSpy.mock.calls.map((c) => String(c[0])).join('');
71
+ expect(output).toContain('document-center');
72
+ expect(output).toContain('FAILED');
73
+ expect(output).toContain('18.4 MB');
74
+ });
75
+
76
+ it('prints overall summary line with pass and fail counts', () => {
77
+ const summary = makeSummary({ passCount: 2, failCount: 1 });
78
+ reporter.write(summary);
79
+ const output = stdoutSpy.mock.calls.map((c) => String(c[0])).join('');
80
+ expect(output).toMatch(/2 passed.*1 failed/i);
81
+ });
82
+ });