@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.
- package/.eslintrc.cjs +10 -0
- package/.stylelintignore +4 -0
- package/CHANGELOG.md +51 -0
- package/README.md +309 -0
- package/babel.config.cjs +2 -0
- package/bin/leak-runner.ts +9 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/bin/leak-runner.js +792 -0
- package/dist/cjs/analysis/thresholdEvaluator.js +46 -0
- package/dist/cjs/browser/iframeHeapProfiler.js +46 -0
- package/dist/cjs/cli/command.js +16 -0
- package/dist/cjs/cli/commands/listCommand.js +47 -0
- package/dist/cjs/cli/commands/runCommand.js +111 -0
- package/dist/cjs/cli/index.js +42 -0
- package/dist/cjs/config/missingRequiredParamError.js +34 -0
- package/dist/cjs/config/requiredEnvParams.js +57 -0
- package/dist/cjs/config/runnerConfigLoader.js +73 -0
- package/dist/cjs/config/runnerConfigSchema.js +40 -0
- package/dist/cjs/config/sources/cliOverrideConfigSource.js +44 -0
- package/dist/cjs/config/sources/configSource.js +35 -0
- package/dist/cjs/config/sources/envVarConfigSource.js +41 -0
- package/dist/cjs/config/sources/fileConfigSource.js +62 -0
- package/dist/cjs/index.js +52 -0
- package/dist/cjs/package.json +7 -0
- package/dist/cjs/registry/scenarioRegistry.js +51 -0
- package/dist/cjs/reporting/consoleReporter.js +60 -0
- package/dist/cjs/reporting/junitReporter.js +75 -0
- package/dist/cjs/reporting/reporter.js +16 -0
- package/dist/cjs/runner/aiEnhancementStep.js +39 -0
- package/dist/cjs/runner/batchRunner.js +76 -0
- package/dist/cjs/runner/scenarioRunner.js +165 -0
- package/dist/cjs/scenarios/index.js +29 -0
- package/dist/cjs/scenarios/one-admin/export-navigation.scenario.js +50 -0
- package/dist/cjs/scenarios/one-admin/index.js +27 -0
- package/dist/cjs/scenarios/one-admin/page-models/ExportPageModel.js +43 -0
- package/dist/cjs/scenarios/one-admin/page-models/SelectSettingsPageModel.js +47 -0
- package/dist/cjs/scenarios/one-admin/page-models/index.js +26 -0
- package/dist/cjs/types/config.js +27 -0
- package/dist/cjs/types/results.js +16 -0
- package/dist/cjs/types/scenario.js +16 -0
- package/dist/esm/analysis/thresholdEvaluator.js +26 -0
- package/dist/esm/browser/iframeHeapProfiler.js +26 -0
- package/dist/esm/cli/command.js +0 -0
- package/dist/esm/cli/commands/listCommand.js +27 -0
- package/dist/esm/cli/commands/runCommand.js +93 -0
- package/dist/esm/cli/index.js +22 -0
- package/dist/esm/config/missingRequiredParamError.js +14 -0
- package/dist/esm/config/requiredEnvParams.js +37 -0
- package/dist/esm/config/runnerConfigLoader.js +53 -0
- package/dist/esm/config/runnerConfigSchema.js +20 -0
- package/dist/esm/config/sources/cliOverrideConfigSource.js +24 -0
- package/dist/esm/config/sources/configSource.js +15 -0
- package/dist/esm/config/sources/envVarConfigSource.js +21 -0
- package/dist/esm/config/sources/fileConfigSource.js +34 -0
- package/dist/esm/index.js +35 -0
- package/dist/esm/package.json +7 -0
- package/dist/esm/registry/scenarioRegistry.js +31 -0
- package/dist/esm/reporting/consoleReporter.js +40 -0
- package/dist/esm/reporting/junitReporter.js +45 -0
- package/dist/esm/reporting/reporter.js +0 -0
- package/dist/esm/runner/aiEnhancementStep.js +22 -0
- package/dist/esm/runner/batchRunner.js +56 -0
- package/dist/esm/runner/scenarioRunner.js +137 -0
- package/dist/esm/scenarios/index.js +9 -0
- package/dist/esm/scenarios/one-admin/export-navigation.scenario.js +33 -0
- package/dist/esm/scenarios/one-admin/index.js +7 -0
- package/dist/esm/scenarios/one-admin/page-models/ExportPageModel.js +23 -0
- package/dist/esm/scenarios/one-admin/page-models/SelectSettingsPageModel.js +27 -0
- package/dist/esm/scenarios/one-admin/page-models/index.js +6 -0
- package/dist/esm/types/config.js +7 -0
- package/dist/esm/types/results.js +0 -0
- package/dist/esm/types/scenario.js +0 -0
- package/dist/types/bin/leak-runner.d.ts +2 -0
- package/dist/types/lib/analysis/tests/thresholdEvaluator.test.d.ts +1 -0
- package/dist/types/lib/analysis/thresholdEvaluator.d.ts +6 -0
- package/dist/types/lib/browser/iframeHeapProfiler.d.ts +9 -0
- package/dist/types/lib/browser/tests/iframeHeapProfiler.test.d.ts +1 -0
- package/dist/types/lib/cli/command.d.ts +17 -0
- package/dist/types/lib/cli/commands/listCommand.d.ts +5 -0
- package/dist/types/lib/cli/commands/runCommand.d.ts +7 -0
- package/dist/types/lib/cli/index.d.ts +4 -0
- package/dist/types/lib/config/missingRequiredParamError.d.ts +4 -0
- package/dist/types/lib/config/requiredEnvParams.d.ts +16 -0
- package/dist/types/lib/config/runnerConfigLoader.d.ts +13 -0
- package/dist/types/lib/config/runnerConfigSchema.d.ts +78 -0
- package/dist/types/lib/config/sources/cliOverrideConfigSource.d.ts +14 -0
- package/dist/types/lib/config/sources/configSource.d.ts +14 -0
- package/dist/types/lib/config/sources/envVarConfigSource.d.ts +7 -0
- package/dist/types/lib/config/sources/fileConfigSource.d.ts +9 -0
- package/dist/types/lib/config/tests/cliOverrideConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/envVarConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/fileConfigSource.test.d.ts +1 -0
- package/dist/types/lib/config/tests/requiredEnvParams.test.d.ts +1 -0
- package/dist/types/lib/config/tests/runnerConfigLoader.test.d.ts +1 -0
- package/dist/types/lib/index.d.ts +18 -0
- package/dist/types/lib/registry/scenarioRegistry.d.ts +18 -0
- package/dist/types/lib/registry/tests/scenarioRegistry.test.d.ts +1 -0
- package/dist/types/lib/reporting/consoleReporter.d.ts +5 -0
- package/dist/types/lib/reporting/junitReporter.d.ts +5 -0
- package/dist/types/lib/reporting/reporter.d.ts +4 -0
- package/dist/types/lib/reporting/tests/consoleReporter.test.d.ts +1 -0
- package/dist/types/lib/reporting/tests/junitReporter.test.d.ts +1 -0
- package/dist/types/lib/runner/aiEnhancementStep.d.ts +15 -0
- package/dist/types/lib/runner/batchRunner.d.ts +14 -0
- package/dist/types/lib/runner/scenarioRunner.d.ts +15 -0
- package/dist/types/lib/runner/tests/aiEnhancementStep.test.d.ts +1 -0
- package/dist/types/lib/runner/tests/batchRunner.test.d.ts +1 -0
- package/dist/types/lib/runner/tests/scenarioRunner.test.d.ts +1 -0
- package/dist/types/lib/scenarios/index.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/export-navigation.scenario.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/index.d.ts +2 -0
- package/dist/types/lib/scenarios/one-admin/page-models/ExportPageModel.d.ts +8 -0
- package/dist/types/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.d.ts +10 -0
- package/dist/types/lib/scenarios/one-admin/page-models/index.d.ts +2 -0
- package/dist/types/lib/types/config.d.ts +26 -0
- package/dist/types/lib/types/results.d.ts +19 -0
- package/dist/types/lib/types/scenario.d.ts +17 -0
- package/jest.config.cjs +9 -0
- package/leak-runner.config.json +13 -0
- package/leak-runner.schema.json +27 -0
- package/lib/analysis/tests/thresholdEvaluator.test.ts +125 -0
- package/lib/analysis/thresholdEvaluator.ts +36 -0
- package/lib/browser/iframeHeapProfiler.ts +30 -0
- package/lib/browser/tests/iframeHeapProfiler.test.ts +71 -0
- package/lib/cli/command.ts +19 -0
- package/lib/cli/commands/listCommand.ts +36 -0
- package/lib/cli/commands/runCommand.ts +126 -0
- package/lib/cli/index.ts +25 -0
- package/lib/config/missingRequiredParamError.ts +10 -0
- package/lib/config/requiredEnvParams.ts +50 -0
- package/lib/config/runnerConfigLoader.ts +84 -0
- package/lib/config/runnerConfigSchema.ts +27 -0
- package/lib/config/sources/cliOverrideConfigSource.ts +30 -0
- package/lib/config/sources/configSource.ts +27 -0
- package/lib/config/sources/envVarConfigSource.ts +23 -0
- package/lib/config/sources/fileConfigSource.ts +39 -0
- package/lib/config/tests/cliOverrideConfigSource.test.ts +25 -0
- package/lib/config/tests/envVarConfigSource.test.ts +57 -0
- package/lib/config/tests/fileConfigSource.test.ts +49 -0
- package/lib/config/tests/requiredEnvParams.test.ts +113 -0
- package/lib/config/tests/runnerConfigLoader.test.ts +59 -0
- package/lib/index.ts +37 -0
- package/lib/registry/scenarioRegistry.ts +48 -0
- package/lib/registry/tests/scenarioRegistry.test.ts +96 -0
- package/lib/reporting/consoleReporter.ts +48 -0
- package/lib/reporting/junitReporter.ts +62 -0
- package/lib/reporting/reporter.ts +5 -0
- package/lib/reporting/tests/consoleReporter.test.ts +82 -0
- package/lib/reporting/tests/junitReporter.test.ts +103 -0
- package/lib/runner/aiEnhancementStep.ts +39 -0
- package/lib/runner/batchRunner.ts +71 -0
- package/lib/runner/scenarioRunner.ts +189 -0
- package/lib/runner/tests/aiEnhancementStep.test.ts +174 -0
- package/lib/runner/tests/batchRunner.test.ts +133 -0
- package/lib/runner/tests/scenarioRunner.test.ts +162 -0
- package/lib/scenarios/index.ts +8 -0
- package/lib/scenarios/one-admin/export-navigation.scenario.ts +38 -0
- package/lib/scenarios/one-admin/index.ts +6 -0
- package/lib/scenarios/one-admin/page-models/ExportPageModel.ts +26 -0
- package/lib/scenarios/one-admin/page-models/SelectSettingsPageModel.ts +30 -0
- package/lib/scenarios/one-admin/page-models/index.ts +2 -0
- package/lib/types/config.ts +34 -0
- package/lib/types/results.ts +22 -0
- package/lib/types/scenario.ts +18 -0
- package/package.json +46 -0
- package/reports/analysis/index.html +116 -0
- package/reports/analysis/thresholdEvaluator.ts.html +193 -0
- package/reports/base.css +224 -0
- package/reports/block-navigation.js +87 -0
- package/reports/browser/iframeHeapProfiler.ts.html +175 -0
- package/reports/browser/index.html +116 -0
- package/reports/cli/commands/index.html +131 -0
- package/reports/cli/commands/listCommand.ts.html +193 -0
- package/reports/cli/commands/runCommand.ts.html +463 -0
- package/reports/cli/index.html +116 -0
- package/reports/cli/index.ts.html +160 -0
- package/reports/config/index.html +161 -0
- package/reports/config/missingRequiredParamError.ts.html +115 -0
- package/reports/config/requiredEnvParams.ts.html +235 -0
- package/reports/config/runnerConfigLoader.ts.html +337 -0
- package/reports/config/runnerConfigSchema.ts.html +166 -0
- package/reports/config/sources/cliOverrideConfigSource.ts.html +175 -0
- package/reports/config/sources/configSource.ts.html +166 -0
- package/reports/config/sources/envVarConfigSource.ts.html +154 -0
- package/reports/config/sources/fileConfigSource.ts.html +202 -0
- package/reports/config/sources/index.html +161 -0
- package/reports/favicon.png +0 -0
- package/reports/index.html +296 -0
- package/reports/lcov-report/analysis/index.html +116 -0
- package/reports/lcov-report/analysis/thresholdEvaluator.ts.html +193 -0
- package/reports/lcov-report/base.css +224 -0
- package/reports/lcov-report/block-navigation.js +87 -0
- package/reports/lcov-report/browser/iframeHeapProfiler.ts.html +175 -0
- package/reports/lcov-report/browser/index.html +116 -0
- package/reports/lcov-report/cli/commands/index.html +131 -0
- package/reports/lcov-report/cli/commands/listCommand.ts.html +193 -0
- package/reports/lcov-report/cli/commands/runCommand.ts.html +463 -0
- package/reports/lcov-report/cli/index.html +116 -0
- package/reports/lcov-report/cli/index.ts.html +160 -0
- package/reports/lcov-report/config/index.html +161 -0
- package/reports/lcov-report/config/missingRequiredParamError.ts.html +115 -0
- package/reports/lcov-report/config/requiredEnvParams.ts.html +235 -0
- package/reports/lcov-report/config/runnerConfigLoader.ts.html +337 -0
- package/reports/lcov-report/config/runnerConfigSchema.ts.html +166 -0
- package/reports/lcov-report/config/sources/cliOverrideConfigSource.ts.html +175 -0
- package/reports/lcov-report/config/sources/configSource.ts.html +166 -0
- package/reports/lcov-report/config/sources/envVarConfigSource.ts.html +154 -0
- package/reports/lcov-report/config/sources/fileConfigSource.ts.html +202 -0
- package/reports/lcov-report/config/sources/index.html +161 -0
- package/reports/lcov-report/favicon.png +0 -0
- package/reports/lcov-report/index.html +296 -0
- package/reports/lcov-report/prettify.css +1 -0
- package/reports/lcov-report/prettify.js +2 -0
- package/reports/lcov-report/registry/index.html +116 -0
- package/reports/lcov-report/registry/scenarioRegistry.ts.html +229 -0
- package/reports/lcov-report/reporting/consoleReporter.ts.html +229 -0
- package/reports/lcov-report/reporting/index.html +131 -0
- package/reports/lcov-report/reporting/junitReporter.ts.html +271 -0
- package/reports/lcov-report/runner/aiEnhancementStep.ts.html +202 -0
- package/reports/lcov-report/runner/batchRunner.ts.html +298 -0
- package/reports/lcov-report/runner/index.html +146 -0
- package/reports/lcov-report/runner/scenarioRunner.ts.html +652 -0
- package/reports/lcov-report/scenarios/index.html +116 -0
- package/reports/lcov-report/scenarios/index.ts.html +109 -0
- package/reports/lcov-report/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
- package/reports/lcov-report/scenarios/one-admin/index.html +131 -0
- package/reports/lcov-report/scenarios/one-admin/index.ts.html +103 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
- package/reports/lcov-report/scenarios/one-admin/page-models/index.html +131 -0
- package/reports/lcov-report/sort-arrow-sprite.png +0 -0
- package/reports/lcov-report/sorter.js +210 -0
- package/reports/lcov-report/types/config.ts.html +187 -0
- package/reports/lcov-report/types/index.html +116 -0
- package/reports/lcov.info +883 -0
- package/reports/prettify.css +1 -0
- package/reports/prettify.js +2 -0
- package/reports/registry/index.html +116 -0
- package/reports/registry/scenarioRegistry.ts.html +229 -0
- package/reports/reporting/consoleReporter.ts.html +229 -0
- package/reports/reporting/index.html +131 -0
- package/reports/reporting/junitReporter.ts.html +271 -0
- package/reports/runner/aiEnhancementStep.ts.html +202 -0
- package/reports/runner/batchRunner.ts.html +298 -0
- package/reports/runner/index.html +146 -0
- package/reports/runner/scenarioRunner.ts.html +652 -0
- package/reports/scenarios/index.html +116 -0
- package/reports/scenarios/index.ts.html +109 -0
- package/reports/scenarios/one-admin/export-navigation.scenario.ts.html +199 -0
- package/reports/scenarios/one-admin/index.html +131 -0
- package/reports/scenarios/one-admin/index.ts.html +103 -0
- package/reports/scenarios/one-admin/page-models/ExportPageModel.ts.html +163 -0
- package/reports/scenarios/one-admin/page-models/SelectSettingsPageModel.ts.html +175 -0
- package/reports/scenarios/one-admin/page-models/index.html +131 -0
- package/reports/sort-arrow-sprite.png +0 -0
- package/reports/sorter.js +210 -0
- package/reports/types/config.ts.html +187 -0
- package/reports/types/index.html +116 -0
- package/stylelint.config.cjs +2 -0
- package/test-report.xml +100 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { HeapMemoryProfiler } from '@elliemae/smoked-suite';
|
|
2
|
+
import type { Page, Frame } from '@playwright/test';
|
|
3
|
+
import { IframeHeapProfiler } from '../iframeHeapProfiler.js';
|
|
4
|
+
|
|
5
|
+
// `getCDPTarget` is protected on the real classes; tests need structural access.
|
|
6
|
+
type ProfilerInternals = { getCDPTarget(): Page | Frame };
|
|
7
|
+
|
|
8
|
+
function exposeInternals(profiler: HeapMemoryProfiler): ProfilerInternals {
|
|
9
|
+
return profiler as unknown as ProfilerInternals;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function makeMockPage(
|
|
13
|
+
url = 'https://q3.elliemae.io/admin',
|
|
14
|
+
browserName = 'chromium',
|
|
15
|
+
): Page {
|
|
16
|
+
return {
|
|
17
|
+
url: () => url,
|
|
18
|
+
context: () => ({
|
|
19
|
+
browser: () => ({ browserType: () => ({ name: () => browserName }) }),
|
|
20
|
+
newCDPSession: jest.fn().mockResolvedValue({
|
|
21
|
+
send: jest.fn().mockResolvedValue(undefined),
|
|
22
|
+
on: jest.fn(),
|
|
23
|
+
detach: jest.fn().mockResolvedValue(undefined),
|
|
24
|
+
}),
|
|
25
|
+
}),
|
|
26
|
+
} as unknown as Page;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeMockFrame(url = 'https://q3.elliemae.io'): Frame {
|
|
30
|
+
return { url: () => url } as unknown as Frame;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('IframeHeapProfiler', () => {
|
|
34
|
+
it('is an instance of HeapMemoryProfiler', () => {
|
|
35
|
+
const page = makeMockPage();
|
|
36
|
+
const frame = makeMockFrame();
|
|
37
|
+
const profiler = new IframeHeapProfiler(page, frame, '/tmp/test');
|
|
38
|
+
expect(profiler).toBeInstanceOf(HeapMemoryProfiler);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('returns the page as CDP target when the iframe is same-origin', () => {
|
|
42
|
+
const page = makeMockPage('https://q3.elliemae.io/admin');
|
|
43
|
+
const frame = makeMockFrame('https://q3.elliemae.io/admin/microapp');
|
|
44
|
+
const profiler = new IframeHeapProfiler(page, frame, '/tmp/test');
|
|
45
|
+
const target = exposeInternals(profiler).getCDPTarget();
|
|
46
|
+
expect(target).toBe(page);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('returns the frame as CDP target when the iframe is cross-origin (OOPIF)', () => {
|
|
50
|
+
const page = makeMockPage('https://q3.elliemae.io/admin');
|
|
51
|
+
const frame = makeMockFrame('https://other.example.com/microapp');
|
|
52
|
+
const profiler = new IframeHeapProfiler(page, frame, '/tmp/test');
|
|
53
|
+
const target = exposeInternals(profiler).getCDPTarget();
|
|
54
|
+
expect(target).toBe(frame);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('treats an about:blank frame as same-origin and returns the page', () => {
|
|
58
|
+
const page = makeMockPage('https://q3.elliemae.io/admin');
|
|
59
|
+
const frame = makeMockFrame('about:blank');
|
|
60
|
+
const profiler = new IframeHeapProfiler(page, frame, '/tmp/test');
|
|
61
|
+
const target = exposeInternals(profiler).getCDPTarget();
|
|
62
|
+
expect(target).toBe(page);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('base HeapMemoryProfiler returns page as CDP target', () => {
|
|
66
|
+
const page = makeMockPage();
|
|
67
|
+
const profiler = new HeapMemoryProfiler(page, '/tmp/test');
|
|
68
|
+
const target = exposeInternals(profiler).getCDPTarget();
|
|
69
|
+
expect(target).toBe(page);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { ScenarioRegistry } from '../registry/scenarioRegistry.js';
|
|
3
|
+
import type { RequiredEnvParamsResolver } from '../config/requiredEnvParams.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dependencies a CliCommand needs from the surrounding wiring.
|
|
7
|
+
*
|
|
8
|
+
* `RunnerConfigLoader` is intentionally NOT here — each command invocation
|
|
9
|
+
* builds its own loader so it can layer in command-line flags (which are only
|
|
10
|
+
* known per-invocation).
|
|
11
|
+
*/
|
|
12
|
+
export interface CliDependencies {
|
|
13
|
+
readonly registry: ScenarioRegistry;
|
|
14
|
+
readonly envParams: RequiredEnvParamsResolver;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface CliCommand {
|
|
18
|
+
register(program: Command, deps: CliDependencies): void;
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { CliCommand, CliDependencies } from '../command.js';
|
|
3
|
+
|
|
4
|
+
export class ListCommand implements CliCommand {
|
|
5
|
+
register(program: Command, deps: CliDependencies): void {
|
|
6
|
+
program
|
|
7
|
+
.command('list')
|
|
8
|
+
.description('List all registered scenarios')
|
|
9
|
+
.option('--tag <tag>', 'Filter by tag')
|
|
10
|
+
.action((options: { tag?: string }) => {
|
|
11
|
+
const entries = options.tag
|
|
12
|
+
? deps.registry.filterByTag(options.tag)
|
|
13
|
+
: deps.registry.list();
|
|
14
|
+
|
|
15
|
+
if (entries.length === 0) {
|
|
16
|
+
process.stdout.write('No scenarios found.\n');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const keyWidth = Math.max(...entries.map((e) => e.key.length), 32);
|
|
21
|
+
process.stdout.write(`\nREGISTERED SCENARIOS (${entries.length})\n\n`);
|
|
22
|
+
|
|
23
|
+
entries.forEach(({ key, scenario }) => {
|
|
24
|
+
const tags = scenario.tags?.length
|
|
25
|
+
? `[${scenario.tags.join(', ')}]`
|
|
26
|
+
: '';
|
|
27
|
+
process.stdout.write(
|
|
28
|
+
` ${key.padEnd(keyWidth)} ${tags.padEnd(20)} ${
|
|
29
|
+
scenario.description
|
|
30
|
+
}\n`,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
process.stdout.write('\n');
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { CliCommand, CliDependencies } from '../command.js';
|
|
3
|
+
import type { RunSummary } from '../../types/results.js';
|
|
4
|
+
import type { RunnerConfig, AiConfig } from '../../types/config.js';
|
|
5
|
+
import { BatchRunner } from '../../runner/batchRunner.js';
|
|
6
|
+
import { ConsoleReporter } from '../../reporting/consoleReporter.js';
|
|
7
|
+
import { JunitReporter } from '../../reporting/junitReporter.js';
|
|
8
|
+
import {
|
|
9
|
+
CliOverrideConfigSource,
|
|
10
|
+
type CliRunnerOverrides,
|
|
11
|
+
} from '../../config/sources/cliOverrideConfigSource.js';
|
|
12
|
+
import { EnvVarConfigSource } from '../../config/sources/envVarConfigSource.js';
|
|
13
|
+
import { FileConfigSource } from '../../config/sources/fileConfigSource.js';
|
|
14
|
+
import { RunnerConfigLoader } from '../../config/runnerConfigLoader.js';
|
|
15
|
+
import { MissingRequiredParamError } from '../../config/requiredEnvParams.js';
|
|
16
|
+
|
|
17
|
+
interface RunOptions extends CliRunnerOverrides {
|
|
18
|
+
all?: boolean;
|
|
19
|
+
tag?: string;
|
|
20
|
+
baseUrl?: string;
|
|
21
|
+
instanceId?: string;
|
|
22
|
+
userId?: string;
|
|
23
|
+
configFile?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class RunCommand implements CliCommand {
|
|
27
|
+
register(program: Command, deps: CliDependencies): void {
|
|
28
|
+
program
|
|
29
|
+
.command('run [key]')
|
|
30
|
+
.description(
|
|
31
|
+
'Run a scenario by `<microapp>/<id>`, or use --all / --tag to run multiple',
|
|
32
|
+
)
|
|
33
|
+
.option('--all', 'Run all registered scenarios')
|
|
34
|
+
.option('--tag <tag>', 'Run all scenarios matching a tag')
|
|
35
|
+
.option('--base-url <url>', 'Override base URL')
|
|
36
|
+
.option('--instance-id <id>', 'Override instance ID')
|
|
37
|
+
.option('--user-id <id>', 'Override user ID')
|
|
38
|
+
.option('--output-dir <dir>', 'Override output directory')
|
|
39
|
+
.option('--top-n <n>', 'Override top-N for heap comparison', (v) =>
|
|
40
|
+
parseInt(v, 10),
|
|
41
|
+
)
|
|
42
|
+
.option(
|
|
43
|
+
'--headless <bool>',
|
|
44
|
+
'Override headless mode',
|
|
45
|
+
(v) => v !== 'false',
|
|
46
|
+
)
|
|
47
|
+
.option('--config-file <path>', 'Path to leak-runner.config.json')
|
|
48
|
+
.action(async (key: string | undefined, options: RunOptions) => {
|
|
49
|
+
try {
|
|
50
|
+
const config = this.buildConfig(options, deps);
|
|
51
|
+
const runner = new BatchRunner(config, deps.registry);
|
|
52
|
+
const summary = await this.selectSummary(runner, key, options);
|
|
53
|
+
new ConsoleReporter().write(summary);
|
|
54
|
+
await new JunitReporter().write(summary, config.runner.outputDir);
|
|
55
|
+
process.exit(summary.failCount > 0 ? 1 : 0);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof MissingRequiredParamError) {
|
|
58
|
+
process.stderr.write(`Configuration error: ${err.message}\n`);
|
|
59
|
+
process.exit(2);
|
|
60
|
+
}
|
|
61
|
+
process.stderr.write(`Fatal: ${(err as Error).message}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private buildConfig(
|
|
68
|
+
options: RunOptions,
|
|
69
|
+
deps: CliDependencies,
|
|
70
|
+
): RunnerConfig {
|
|
71
|
+
const filePath =
|
|
72
|
+
options.configFile ?? `${process.cwd()}/leak-runner.config.json`;
|
|
73
|
+
|
|
74
|
+
const loader = new RunnerConfigLoader([
|
|
75
|
+
new FileConfigSource(filePath),
|
|
76
|
+
new EnvVarConfigSource(),
|
|
77
|
+
new CliOverrideConfigSource({
|
|
78
|
+
headless: options.headless,
|
|
79
|
+
outputDir: options.outputDir,
|
|
80
|
+
topN: options.topN,
|
|
81
|
+
}),
|
|
82
|
+
]);
|
|
83
|
+
const resolved = loader.resolveOptions();
|
|
84
|
+
|
|
85
|
+
const env = deps.envParams.resolve({
|
|
86
|
+
baseUrl: options.baseUrl,
|
|
87
|
+
instanceId: options.instanceId,
|
|
88
|
+
userId: options.userId,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
let ai: AiConfig | undefined;
|
|
92
|
+
if (resolved.aiEnabled) {
|
|
93
|
+
ai = {
|
|
94
|
+
model: resolved.aiModel,
|
|
95
|
+
temperature: resolved.aiTemperature,
|
|
96
|
+
apiKey: process.env.GENICE_API_KEY ?? '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { env, runner: resolved.runner, ai };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private async selectSummary(
|
|
104
|
+
runner: BatchRunner,
|
|
105
|
+
key: string | undefined,
|
|
106
|
+
options: RunOptions,
|
|
107
|
+
): Promise<RunSummary> {
|
|
108
|
+
if (options.all) return runner.runAll();
|
|
109
|
+
if (options.tag) {
|
|
110
|
+
const summary = await runner.runByTag(options.tag);
|
|
111
|
+
if (summary.results.length === 0) {
|
|
112
|
+
process.stderr.write(
|
|
113
|
+
`Warning: no scenarios matched tag "${options.tag}"\n`,
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
return summary;
|
|
118
|
+
}
|
|
119
|
+
if (key) return runner.runByName(key);
|
|
120
|
+
process.stderr.write(
|
|
121
|
+
'Error: specify a scenario key (e.g. one-admin/export-navigation), --all, or --tag <tag>\n',
|
|
122
|
+
);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
throw new Error('unreachable');
|
|
125
|
+
}
|
|
126
|
+
}
|
package/lib/cli/index.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import type { CliCommand, CliDependencies } from './command.js';
|
|
3
|
+
import { RunCommand } from './commands/runCommand.js';
|
|
4
|
+
import { ListCommand } from './commands/listCommand.js';
|
|
5
|
+
import { RequiredEnvParamsResolver } from '../config/requiredEnvParams.js';
|
|
6
|
+
import { scenarioRegistry as populatedRegistry } from '../scenarios/index.js';
|
|
7
|
+
|
|
8
|
+
export function buildProgram(deps: CliDependencies = defaultDeps()): Command {
|
|
9
|
+
const program = new Command();
|
|
10
|
+
program
|
|
11
|
+
.name('leak-runner')
|
|
12
|
+
.description('Microapp memory leak runner for Encompass Web')
|
|
13
|
+
.version('1.0.0');
|
|
14
|
+
|
|
15
|
+
const commands: readonly CliCommand[] = [new RunCommand(), new ListCommand()];
|
|
16
|
+
commands.forEach((cmd) => cmd.register(program, deps));
|
|
17
|
+
return program;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function defaultDeps(): CliDependencies {
|
|
21
|
+
return {
|
|
22
|
+
registry: populatedRegistry,
|
|
23
|
+
envParams: new RequiredEnvParamsResolver(),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export class MissingRequiredParamError extends Error {
|
|
2
|
+
constructor(public readonly missing: readonly string[]) {
|
|
3
|
+
super(
|
|
4
|
+
`Missing required parameter(s): ${missing.join(', ')}.\n` +
|
|
5
|
+
`Provide via CLI flag (--base-url / --instance-id / --user-id) ` +
|
|
6
|
+
`or env var (BASE_URL / ENCW_INSTANCE_ID / ENCW_USER_ID / ENCW_PASSWORD).`,
|
|
7
|
+
);
|
|
8
|
+
this.name = 'MissingRequiredParamError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { EnvironmentParams } from '../types/config.js';
|
|
2
|
+
import { MissingRequiredParamError } from './missingRequiredParamError.js';
|
|
3
|
+
|
|
4
|
+
export interface CliEnvOpts {
|
|
5
|
+
readonly baseUrl?: string;
|
|
6
|
+
readonly instanceId?: string;
|
|
7
|
+
readonly userId?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export { MissingRequiredParamError };
|
|
11
|
+
|
|
12
|
+
export class RequiredEnvParamsResolver {
|
|
13
|
+
resolve(cliOpts: CliEnvOpts): EnvironmentParams {
|
|
14
|
+
const baseUrl = this.resolveBaseUrl(cliOpts);
|
|
15
|
+
const instanceId = this.resolveInstanceId(cliOpts);
|
|
16
|
+
const userId = this.resolveUserId(cliOpts);
|
|
17
|
+
const password = this.resolvePassword();
|
|
18
|
+
|
|
19
|
+
const params = { baseUrl, instanceId, userId, password };
|
|
20
|
+
const missing = this.collectMissing(params);
|
|
21
|
+
if (missing.length > 0) throw new MissingRequiredParamError(missing);
|
|
22
|
+
|
|
23
|
+
return params;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
private resolveBaseUrl(cliOpts: CliEnvOpts): string {
|
|
27
|
+
return cliOpts.baseUrl || process.env.BASE_URL || '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private resolveInstanceId(cliOpts: CliEnvOpts): string {
|
|
31
|
+
return cliOpts.instanceId || process.env.ENCW_INSTANCE_ID || '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private resolveUserId(cliOpts: CliEnvOpts): string {
|
|
35
|
+
return cliOpts.userId || process.env.ENCW_USER_ID || '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private resolvePassword(): string {
|
|
39
|
+
return process.env.ENCW_PASSWORD || '';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
private collectMissing(params: EnvironmentParams): string[] {
|
|
43
|
+
const missing: string[] = [];
|
|
44
|
+
if (!params.baseUrl) missing.push('baseUrl');
|
|
45
|
+
if (!params.instanceId) missing.push('instanceId');
|
|
46
|
+
if (!params.userId) missing.push('userId');
|
|
47
|
+
if (!params.password) missing.push('password (ENCW_PASSWORD)');
|
|
48
|
+
return missing;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { RunnerOptions } from '../types/config.js';
|
|
2
|
+
import type { RawRunnerConfigFile } from './runnerConfigSchema.js';
|
|
3
|
+
import type { ConfigSource } from './sources/configSource.js';
|
|
4
|
+
import { BUILT_IN_DEFAULTS } from './sources/configSource.js';
|
|
5
|
+
|
|
6
|
+
export interface ResolvedRunnerConfigPartial {
|
|
7
|
+
readonly runner: RunnerOptions;
|
|
8
|
+
readonly aiEnabled: boolean;
|
|
9
|
+
readonly aiModel: string;
|
|
10
|
+
readonly aiTemperature: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type MutableRunner = { headless: boolean; outputDir: string; topN: number };
|
|
14
|
+
|
|
15
|
+
type AiAccumulator = {
|
|
16
|
+
aiEnabled: boolean;
|
|
17
|
+
aiModel: string;
|
|
18
|
+
aiTemperature: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function applyRunnerPayload(
|
|
22
|
+
acc: MutableRunner,
|
|
23
|
+
payload: NonNullable<RawRunnerConfigFile['runner']>,
|
|
24
|
+
): MutableRunner {
|
|
25
|
+
return {
|
|
26
|
+
headless: payload.headless ?? acc.headless,
|
|
27
|
+
outputDir: payload.outputDir ?? acc.outputDir,
|
|
28
|
+
topN: payload.topN ?? acc.topN,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyAiPayload(
|
|
33
|
+
acc: AiAccumulator,
|
|
34
|
+
payload: NonNullable<RawRunnerConfigFile['ai']>,
|
|
35
|
+
): AiAccumulator {
|
|
36
|
+
return {
|
|
37
|
+
aiEnabled: payload.enabled ?? acc.aiEnabled,
|
|
38
|
+
aiModel: payload.model ?? acc.aiModel,
|
|
39
|
+
aiTemperature: payload.temperature ?? acc.aiTemperature,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function applySource(
|
|
44
|
+
acc: { runner: MutableRunner; ai: AiAccumulator },
|
|
45
|
+
source: ConfigSource,
|
|
46
|
+
): { runner: MutableRunner; ai: AiAccumulator } {
|
|
47
|
+
const payload = source.load();
|
|
48
|
+
return {
|
|
49
|
+
runner: payload.runner
|
|
50
|
+
? applyRunnerPayload(acc.runner, payload.runner)
|
|
51
|
+
: acc.runner,
|
|
52
|
+
ai: payload.ai ? applyAiPayload(acc.ai, payload.ai) : acc.ai,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class RunnerConfigLoader {
|
|
57
|
+
constructor(private readonly sources: readonly ConfigSource[]) {}
|
|
58
|
+
|
|
59
|
+
resolveOptions(): ResolvedRunnerConfigPartial {
|
|
60
|
+
const ordered = [...this.sources].sort((a, b) => a.priority - b.priority);
|
|
61
|
+
|
|
62
|
+
const initial = {
|
|
63
|
+
runner: {
|
|
64
|
+
headless: BUILT_IN_DEFAULTS.runner.headless ?? true,
|
|
65
|
+
outputDir: BUILT_IN_DEFAULTS.runner.outputDir ?? './leak-reports/',
|
|
66
|
+
topN: BUILT_IN_DEFAULTS.runner.topN ?? 5,
|
|
67
|
+
},
|
|
68
|
+
ai: {
|
|
69
|
+
aiEnabled: BUILT_IN_DEFAULTS.ai.enabled ?? false,
|
|
70
|
+
aiModel: BUILT_IN_DEFAULTS.ai.model ?? 'Claude3.7',
|
|
71
|
+
aiTemperature: BUILT_IN_DEFAULTS.ai.temperature ?? 0.3,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const resolved = ordered.reduce(applySource, initial);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
runner: resolved.runner,
|
|
79
|
+
aiEnabled: resolved.ai.aiEnabled,
|
|
80
|
+
aiModel: resolved.ai.aiModel,
|
|
81
|
+
aiTemperature: resolved.ai.aiTemperature,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const runnerOptionsSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
headless: z.boolean(),
|
|
6
|
+
outputDir: z.string().min(1),
|
|
7
|
+
topN: z.number().int().min(1),
|
|
8
|
+
})
|
|
9
|
+
.partial();
|
|
10
|
+
|
|
11
|
+
export const aiConfigFileSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
enabled: z.boolean(),
|
|
14
|
+
model: z.string().min(1),
|
|
15
|
+
temperature: z.number().min(0).max(2),
|
|
16
|
+
})
|
|
17
|
+
.partial();
|
|
18
|
+
|
|
19
|
+
export const runnerConfigFileSchema = z
|
|
20
|
+
.object({
|
|
21
|
+
runner: runnerOptionsSchema,
|
|
22
|
+
ai: aiConfigFileSchema,
|
|
23
|
+
})
|
|
24
|
+
.partial()
|
|
25
|
+
.strict();
|
|
26
|
+
|
|
27
|
+
export type RawRunnerConfigFile = z.infer<typeof runnerConfigFileSchema>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { RawRunnerConfigFile } from '../runnerConfigSchema.js';
|
|
2
|
+
import type { ConfigSource } from './configSource.js';
|
|
3
|
+
|
|
4
|
+
export interface CliRunnerOverrides {
|
|
5
|
+
headless?: boolean;
|
|
6
|
+
outputDir?: string;
|
|
7
|
+
topN?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class CliOverrideConfigSource implements ConfigSource {
|
|
11
|
+
readonly priority = 3;
|
|
12
|
+
|
|
13
|
+
readonly name = 'cli';
|
|
14
|
+
|
|
15
|
+
constructor(private readonly overrides: CliRunnerOverrides) {}
|
|
16
|
+
|
|
17
|
+
load(): Partial<RawRunnerConfigFile> {
|
|
18
|
+
const runner: NonNullable<RawRunnerConfigFile['runner']> = {};
|
|
19
|
+
if (this.overrides.headless !== undefined) {
|
|
20
|
+
runner.headless = this.overrides.headless;
|
|
21
|
+
}
|
|
22
|
+
if (this.overrides.outputDir !== undefined) {
|
|
23
|
+
runner.outputDir = this.overrides.outputDir;
|
|
24
|
+
}
|
|
25
|
+
if (this.overrides.topN !== undefined) {
|
|
26
|
+
runner.topN = this.overrides.topN;
|
|
27
|
+
}
|
|
28
|
+
return Object.keys(runner).length === 0 ? {} : { runner };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RawRunnerConfigFile } from '../runnerConfigSchema.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Strategy interface — each source produces a partial config payload.
|
|
5
|
+
* Higher `priority` overrides lower when merged by `RunnerConfigLoader`.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConfigSource {
|
|
8
|
+
readonly priority: number;
|
|
9
|
+
readonly name: string;
|
|
10
|
+
load(): Partial<RawRunnerConfigFile>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const BUILT_IN_DEFAULTS: Required<{
|
|
14
|
+
runner: NonNullable<RawRunnerConfigFile['runner']>;
|
|
15
|
+
ai: NonNullable<RawRunnerConfigFile['ai']>;
|
|
16
|
+
}> = {
|
|
17
|
+
runner: {
|
|
18
|
+
headless: true,
|
|
19
|
+
outputDir: './leak-reports/',
|
|
20
|
+
topN: 5,
|
|
21
|
+
},
|
|
22
|
+
ai: {
|
|
23
|
+
enabled: false,
|
|
24
|
+
model: 'Claude3.7',
|
|
25
|
+
temperature: 0.3,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { RawRunnerConfigFile } from '../runnerConfigSchema.js';
|
|
2
|
+
import type { ConfigSource } from './configSource.js';
|
|
3
|
+
|
|
4
|
+
export class EnvVarConfigSource implements ConfigSource {
|
|
5
|
+
readonly priority = 2;
|
|
6
|
+
|
|
7
|
+
readonly name = 'env';
|
|
8
|
+
|
|
9
|
+
load(): Partial<RawRunnerConfigFile> {
|
|
10
|
+
const runner: NonNullable<RawRunnerConfigFile['runner']> = {};
|
|
11
|
+
if (process.env.HEADLESS !== undefined) {
|
|
12
|
+
runner.headless = process.env.HEADLESS !== 'false';
|
|
13
|
+
}
|
|
14
|
+
if (process.env.LEAK_OUTPUT_DIR) {
|
|
15
|
+
runner.outputDir = process.env.LEAK_OUTPUT_DIR;
|
|
16
|
+
}
|
|
17
|
+
if (process.env.LEAK_TOP_N) {
|
|
18
|
+
const parsed = parseInt(process.env.LEAK_TOP_N, 10);
|
|
19
|
+
if (Number.isFinite(parsed) && parsed > 0) runner.topN = parsed;
|
|
20
|
+
}
|
|
21
|
+
return Object.keys(runner).length === 0 ? {} : { runner };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import {
|
|
3
|
+
runnerConfigFileSchema,
|
|
4
|
+
type RawRunnerConfigFile,
|
|
5
|
+
} from '../runnerConfigSchema.js';
|
|
6
|
+
import type { ConfigSource } from './configSource.js';
|
|
7
|
+
|
|
8
|
+
export class FileConfigSource implements ConfigSource {
|
|
9
|
+
readonly priority = 1;
|
|
10
|
+
|
|
11
|
+
readonly name = 'file';
|
|
12
|
+
|
|
13
|
+
constructor(private readonly filePath: string) {}
|
|
14
|
+
|
|
15
|
+
load(): Partial<RawRunnerConfigFile> {
|
|
16
|
+
if (!fs.existsSync(this.filePath)) return {};
|
|
17
|
+
|
|
18
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
19
|
+
let parsed: unknown;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`Failed to parse config file ${this.filePath}: ${
|
|
25
|
+
(err as Error).message
|
|
26
|
+
}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = runnerConfigFileSchema.safeParse(parsed);
|
|
31
|
+
if (!result.success) {
|
|
32
|
+
const issues = result.error.issues
|
|
33
|
+
.map((i) => ` - ${i.path.join('.')}: ${i.message}`)
|
|
34
|
+
.join('\n');
|
|
35
|
+
throw new Error(`Invalid config file ${this.filePath}:\n${issues}`);
|
|
36
|
+
}
|
|
37
|
+
return result.data;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { CliOverrideConfigSource } from '../sources/cliOverrideConfigSource.js';
|
|
2
|
+
|
|
3
|
+
describe('CliOverrideConfigSource', () => {
|
|
4
|
+
it('returns empty when no overrides are supplied', () => {
|
|
5
|
+
expect(new CliOverrideConfigSource({}).load()).toEqual({});
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it('passes through headless when supplied', () => {
|
|
9
|
+
expect(new CliOverrideConfigSource({ headless: false }).load()).toEqual({
|
|
10
|
+
runner: { headless: false },
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('passes through outputDir and topN when supplied', () => {
|
|
15
|
+
expect(
|
|
16
|
+
new CliOverrideConfigSource({ outputDir: './out/', topN: 8 }).load(),
|
|
17
|
+
).toEqual({
|
|
18
|
+
runner: { outputDir: './out/', topN: 8 },
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('exposes priority 3 (highest tier)', () => {
|
|
23
|
+
expect(new CliOverrideConfigSource({}).priority).toBe(3);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { EnvVarConfigSource } from '../sources/envVarConfigSource.js';
|
|
2
|
+
|
|
3
|
+
describe('EnvVarConfigSource', () => {
|
|
4
|
+
const ORIGINAL_ENV = process.env;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
process.env = { ...ORIGINAL_ENV };
|
|
8
|
+
delete process.env.HEADLESS;
|
|
9
|
+
delete process.env.LEAK_OUTPUT_DIR;
|
|
10
|
+
delete process.env.LEAK_TOP_N;
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
afterAll(() => {
|
|
14
|
+
process.env = ORIGINAL_ENV;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('returns empty when no relevant env vars are set', () => {
|
|
18
|
+
expect(new EnvVarConfigSource().load()).toEqual({});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses HEADLESS=false as headless: false', () => {
|
|
22
|
+
process.env.HEADLESS = 'false';
|
|
23
|
+
expect(new EnvVarConfigSource().load()).toEqual({
|
|
24
|
+
runner: { headless: false },
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('parses HEADLESS=true as headless: true', () => {
|
|
29
|
+
process.env.HEADLESS = 'true';
|
|
30
|
+
expect(new EnvVarConfigSource().load()).toEqual({
|
|
31
|
+
runner: { headless: true },
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('reads LEAK_OUTPUT_DIR into runner.outputDir', () => {
|
|
36
|
+
process.env.LEAK_OUTPUT_DIR = './tmp/out/';
|
|
37
|
+
expect(new EnvVarConfigSource().load()).toEqual({
|
|
38
|
+
runner: { outputDir: './tmp/out/' },
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('parses LEAK_TOP_N as an integer', () => {
|
|
43
|
+
process.env.LEAK_TOP_N = '7';
|
|
44
|
+
expect(new EnvVarConfigSource().load()).toEqual({
|
|
45
|
+
runner: { topN: 7 },
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('ignores LEAK_TOP_N when it is not a positive integer', () => {
|
|
50
|
+
process.env.LEAK_TOP_N = 'banana';
|
|
51
|
+
expect(new EnvVarConfigSource().load()).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('exposes priority 2 (above file)', () => {
|
|
55
|
+
expect(new EnvVarConfigSource().priority).toBe(2);
|
|
56
|
+
});
|
|
57
|
+
});
|