@curl-runner/cli 1.14.0 → 1.15.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.
- package/package.json +1 -1
- package/src/cli.ts +269 -0
- package/src/diff/baseline-manager.test.ts +181 -0
- package/src/diff/baseline-manager.ts +266 -0
- package/src/diff/diff-formatter.ts +316 -0
- package/src/diff/index.ts +3 -0
- package/src/diff/response-differ.test.ts +330 -0
- package/src/diff/response-differ.ts +489 -0
- package/src/types/config.ts +102 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
3
|
import { Glob } from 'bun';
|
|
4
|
+
import { BaselineManager, DiffFormatter, DiffOrchestrator } from './diff';
|
|
4
5
|
import { ProfileExecutor } from './executor/profile-executor';
|
|
5
6
|
import { RequestExecutor } from './executor/request-executor';
|
|
6
7
|
import { YamlParser } from './parser/yaml';
|
|
7
8
|
import type {
|
|
9
|
+
DiffConfig,
|
|
8
10
|
ExecutionResult,
|
|
9
11
|
ExecutionSummary,
|
|
10
12
|
GlobalConfig,
|
|
13
|
+
GlobalDiffConfig,
|
|
11
14
|
ProfileConfig,
|
|
12
15
|
RequestConfig,
|
|
13
16
|
WatchConfig,
|
|
@@ -244,6 +247,52 @@ class CurlRunnerCLI {
|
|
|
244
247
|
};
|
|
245
248
|
}
|
|
246
249
|
|
|
250
|
+
// Diff configuration
|
|
251
|
+
if (process.env.CURL_RUNNER_DIFF) {
|
|
252
|
+
envConfig.diff = {
|
|
253
|
+
...envConfig.diff,
|
|
254
|
+
enabled: process.env.CURL_RUNNER_DIFF.toLowerCase() === 'true',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (process.env.CURL_RUNNER_DIFF_SAVE) {
|
|
259
|
+
envConfig.diff = {
|
|
260
|
+
...envConfig.diff,
|
|
261
|
+
save: process.env.CURL_RUNNER_DIFF_SAVE.toLowerCase() === 'true',
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (process.env.CURL_RUNNER_DIFF_LABEL) {
|
|
266
|
+
envConfig.diff = {
|
|
267
|
+
...envConfig.diff,
|
|
268
|
+
label: process.env.CURL_RUNNER_DIFF_LABEL,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (process.env.CURL_RUNNER_DIFF_COMPARE) {
|
|
273
|
+
envConfig.diff = {
|
|
274
|
+
...envConfig.diff,
|
|
275
|
+
compareWith: process.env.CURL_RUNNER_DIFF_COMPARE,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (process.env.CURL_RUNNER_DIFF_DIR) {
|
|
280
|
+
envConfig.diff = {
|
|
281
|
+
...envConfig.diff,
|
|
282
|
+
dir: process.env.CURL_RUNNER_DIFF_DIR,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (process.env.CURL_RUNNER_DIFF_OUTPUT) {
|
|
287
|
+
const format = process.env.CURL_RUNNER_DIFF_OUTPUT.toLowerCase();
|
|
288
|
+
if (['terminal', 'json', 'markdown'].includes(format)) {
|
|
289
|
+
envConfig.diff = {
|
|
290
|
+
...envConfig.diff,
|
|
291
|
+
outputFormat: format as 'terminal' | 'json' | 'markdown',
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
247
296
|
return envConfig;
|
|
248
297
|
}
|
|
249
298
|
|
|
@@ -269,6 +318,12 @@ class CurlRunnerCLI {
|
|
|
269
318
|
return;
|
|
270
319
|
}
|
|
271
320
|
|
|
321
|
+
// Handle diff subcommand: curl-runner diff <label1> <label2> [file]
|
|
322
|
+
if (args[0] === 'diff' && args.length >= 3) {
|
|
323
|
+
await this.executeDiffSubcommand(args.slice(1), options);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
272
327
|
// Load configuration from environment variables, config file, then CLI options
|
|
273
328
|
const envConfig = this.loadEnvironmentVariables();
|
|
274
329
|
const configFile = await this.loadConfigFile();
|
|
@@ -416,6 +471,46 @@ class CurlRunnerCLI {
|
|
|
416
471
|
};
|
|
417
472
|
}
|
|
418
473
|
|
|
474
|
+
// Apply diff options
|
|
475
|
+
if (options.diff !== undefined) {
|
|
476
|
+
globalConfig.diff = {
|
|
477
|
+
...globalConfig.diff,
|
|
478
|
+
enabled: options.diff as boolean,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (options.diffSave !== undefined) {
|
|
482
|
+
globalConfig.diff = {
|
|
483
|
+
...globalConfig.diff,
|
|
484
|
+
enabled: true,
|
|
485
|
+
save: options.diffSave as boolean,
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
if (options.diffLabel !== undefined) {
|
|
489
|
+
globalConfig.diff = {
|
|
490
|
+
...globalConfig.diff,
|
|
491
|
+
label: options.diffLabel as string,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
if (options.diffCompare !== undefined) {
|
|
495
|
+
globalConfig.diff = {
|
|
496
|
+
...globalConfig.diff,
|
|
497
|
+
enabled: true,
|
|
498
|
+
compareWith: options.diffCompare as string,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (options.diffDir !== undefined) {
|
|
502
|
+
globalConfig.diff = {
|
|
503
|
+
...globalConfig.diff,
|
|
504
|
+
dir: options.diffDir as string,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
if (options.diffOutput !== undefined) {
|
|
508
|
+
globalConfig.diff = {
|
|
509
|
+
...globalConfig.diff,
|
|
510
|
+
outputFormat: options.diffOutput as 'terminal' | 'json' | 'markdown',
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
419
514
|
if (allRequests.length === 0) {
|
|
420
515
|
this.logger.logError('No requests found in YAML files');
|
|
421
516
|
process.exit(1);
|
|
@@ -593,9 +688,138 @@ class CurlRunnerCLI {
|
|
|
593
688
|
summary = await executor.execute(allRequests);
|
|
594
689
|
}
|
|
595
690
|
|
|
691
|
+
// Handle diff mode
|
|
692
|
+
if (globalConfig.diff?.enabled || globalConfig.diff?.save || globalConfig.diff?.compareWith) {
|
|
693
|
+
await this.handleDiffMode(yamlFiles[0], summary.results, globalConfig.diff);
|
|
694
|
+
}
|
|
695
|
+
|
|
596
696
|
return summary;
|
|
597
697
|
}
|
|
598
698
|
|
|
699
|
+
private async handleDiffMode(
|
|
700
|
+
yamlPath: string,
|
|
701
|
+
results: ExecutionResult[],
|
|
702
|
+
diffConfig: GlobalDiffConfig,
|
|
703
|
+
): Promise<void> {
|
|
704
|
+
const orchestrator = new DiffOrchestrator(diffConfig);
|
|
705
|
+
const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
|
|
706
|
+
const config: DiffConfig = BaselineManager.mergeConfig(diffConfig, true) || {};
|
|
707
|
+
|
|
708
|
+
const currentLabel = diffConfig.label || 'current';
|
|
709
|
+
const compareLabel = diffConfig.compareWith;
|
|
710
|
+
|
|
711
|
+
// Save baseline if requested
|
|
712
|
+
if (diffConfig.save) {
|
|
713
|
+
await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
|
|
714
|
+
this.logger.logInfo(`Baseline saved as '${currentLabel}'`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// Compare with baseline if requested
|
|
718
|
+
if (compareLabel) {
|
|
719
|
+
const diffSummary = await orchestrator.compareWithBaseline(
|
|
720
|
+
yamlPath,
|
|
721
|
+
results,
|
|
722
|
+
currentLabel,
|
|
723
|
+
compareLabel,
|
|
724
|
+
config,
|
|
725
|
+
);
|
|
726
|
+
|
|
727
|
+
// Check if baseline exists
|
|
728
|
+
if (diffSummary.newBaselines === diffSummary.totalRequests) {
|
|
729
|
+
this.logger.logWarning(
|
|
730
|
+
`No baseline '${compareLabel}' found. Saving current run as baseline.`,
|
|
731
|
+
);
|
|
732
|
+
await orchestrator.saveBaseline(yamlPath, compareLabel, results, config);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const output = formatter.formatSummary(diffSummary, compareLabel, currentLabel);
|
|
737
|
+
console.log(output);
|
|
738
|
+
|
|
739
|
+
// Save current as baseline if configured
|
|
740
|
+
if (diffConfig.save) {
|
|
741
|
+
await orchestrator.saveBaseline(yamlPath, currentLabel, results, config);
|
|
742
|
+
}
|
|
743
|
+
} else if (diffConfig.enabled && !diffConfig.save) {
|
|
744
|
+
// Auto-detect: list available baselines or save first baseline
|
|
745
|
+
const labels = await orchestrator.listLabels(yamlPath);
|
|
746
|
+
|
|
747
|
+
if (labels.length === 0) {
|
|
748
|
+
// No baselines exist - save current as default baseline
|
|
749
|
+
await orchestrator.saveBaseline(yamlPath, 'baseline', results, config);
|
|
750
|
+
this.logger.logInfo(`No baselines found. Saved current run as 'baseline'.`);
|
|
751
|
+
} else if (labels.length === 1) {
|
|
752
|
+
// One baseline exists - compare against it
|
|
753
|
+
const diffSummary = await orchestrator.compareWithBaseline(
|
|
754
|
+
yamlPath,
|
|
755
|
+
results,
|
|
756
|
+
currentLabel,
|
|
757
|
+
labels[0],
|
|
758
|
+
config,
|
|
759
|
+
);
|
|
760
|
+
const output = formatter.formatSummary(diffSummary, labels[0], currentLabel);
|
|
761
|
+
console.log(output);
|
|
762
|
+
} else {
|
|
763
|
+
// Multiple baselines - list them
|
|
764
|
+
this.logger.logInfo(`Available baselines: ${labels.join(', ')}`);
|
|
765
|
+
this.logger.logInfo(`Use --diff-compare <label> to compare against a specific baseline.`);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Executes the diff subcommand to compare two stored baselines.
|
|
772
|
+
* Usage: curl-runner diff <label1> <label2> [file.yaml]
|
|
773
|
+
*/
|
|
774
|
+
private async executeDiffSubcommand(
|
|
775
|
+
args: string[],
|
|
776
|
+
options: Record<string, unknown>,
|
|
777
|
+
): Promise<void> {
|
|
778
|
+
const label1 = args[0];
|
|
779
|
+
const label2 = args[1];
|
|
780
|
+
let yamlFile = args[2];
|
|
781
|
+
|
|
782
|
+
// Find YAML file if not specified
|
|
783
|
+
if (!yamlFile) {
|
|
784
|
+
const yamlFiles = await this.findYamlFiles([], options);
|
|
785
|
+
if (yamlFiles.length === 0) {
|
|
786
|
+
this.logger.logError(
|
|
787
|
+
'No YAML files found. Specify a file: curl-runner diff <label1> <label2> <file.yaml>',
|
|
788
|
+
);
|
|
789
|
+
process.exit(1);
|
|
790
|
+
}
|
|
791
|
+
if (yamlFiles.length > 1) {
|
|
792
|
+
this.logger.logError('Multiple YAML files found. Specify which file to use.');
|
|
793
|
+
process.exit(1);
|
|
794
|
+
}
|
|
795
|
+
yamlFile = yamlFiles[0];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const diffConfig: GlobalDiffConfig = {
|
|
799
|
+
dir: (options.diffDir as string) || '__baselines__',
|
|
800
|
+
outputFormat: (options.diffOutput as 'terminal' | 'json' | 'markdown') || 'terminal',
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
const orchestrator = new DiffOrchestrator(diffConfig);
|
|
804
|
+
const formatter = new DiffFormatter(diffConfig.outputFormat || 'terminal');
|
|
805
|
+
const config: DiffConfig = { exclude: [], match: {} };
|
|
806
|
+
|
|
807
|
+
try {
|
|
808
|
+
const diffSummary = await orchestrator.compareTwoBaselines(yamlFile, label1, label2, config);
|
|
809
|
+
const output = formatter.formatSummary(diffSummary, label1, label2);
|
|
810
|
+
console.log(output);
|
|
811
|
+
|
|
812
|
+
// Exit with code 1 if differences found
|
|
813
|
+
if (diffSummary.changed > 0) {
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
process.exit(0);
|
|
817
|
+
} catch (error) {
|
|
818
|
+
this.logger.logError(error instanceof Error ? error.message : String(error));
|
|
819
|
+
process.exit(1);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
599
823
|
private parseArguments(args: string[]): { files: string[]; options: Record<string, unknown> } {
|
|
600
824
|
const options: Record<string, unknown> = {};
|
|
601
825
|
const files: string[] = [];
|
|
@@ -637,6 +861,10 @@ class CurlRunnerCLI {
|
|
|
637
861
|
options.snapshotUpdate = 'failing';
|
|
638
862
|
} else if (key === 'ci-snapshot') {
|
|
639
863
|
options.snapshotCi = true;
|
|
864
|
+
} else if (key === 'diff') {
|
|
865
|
+
options.diff = true;
|
|
866
|
+
} else if (key === 'diff-save') {
|
|
867
|
+
options.diffSave = true;
|
|
640
868
|
} else if (nextArg && !nextArg.startsWith('--')) {
|
|
641
869
|
if (key === 'continue-on-error') {
|
|
642
870
|
options.continueOnError = nextArg === 'true';
|
|
@@ -680,6 +908,16 @@ class CurlRunnerCLI {
|
|
|
680
908
|
options.profileExport = nextArg;
|
|
681
909
|
} else if (key === 'snapshot-dir') {
|
|
682
910
|
options.snapshotDir = nextArg;
|
|
911
|
+
} else if (key === 'diff-label') {
|
|
912
|
+
options.diffLabel = nextArg;
|
|
913
|
+
} else if (key === 'diff-compare') {
|
|
914
|
+
options.diffCompare = nextArg;
|
|
915
|
+
} else if (key === 'diff-dir') {
|
|
916
|
+
options.diffDir = nextArg;
|
|
917
|
+
} else if (key === 'diff-output') {
|
|
918
|
+
if (['terminal', 'json', 'markdown'].includes(nextArg)) {
|
|
919
|
+
options.diffOutput = nextArg;
|
|
920
|
+
}
|
|
683
921
|
} else {
|
|
684
922
|
options[key] = nextArg;
|
|
685
923
|
}
|
|
@@ -715,6 +953,9 @@ class CurlRunnerCLI {
|
|
|
715
953
|
case 'u':
|
|
716
954
|
options.snapshotUpdate = 'all';
|
|
717
955
|
break;
|
|
956
|
+
case 'd':
|
|
957
|
+
options.diff = true;
|
|
958
|
+
break;
|
|
718
959
|
case 'o': {
|
|
719
960
|
// Handle -o flag for output file
|
|
720
961
|
const outputArg = args[i + 1];
|
|
@@ -854,6 +1095,7 @@ class CurlRunnerCLI {
|
|
|
854
1095
|
ci: { ...base.ci, ...override.ci },
|
|
855
1096
|
watch: { ...base.watch, ...override.watch },
|
|
856
1097
|
snapshot: { ...base.snapshot, ...override.snapshot },
|
|
1098
|
+
diff: { ...base.diff, ...override.diff },
|
|
857
1099
|
};
|
|
858
1100
|
}
|
|
859
1101
|
|
|
@@ -960,6 +1202,18 @@ ${this.logger.color('SNAPSHOT OPTIONS:', 'yellow')}
|
|
|
960
1202
|
--snapshot-dir <dir> Custom snapshot directory (default: __snapshots__)
|
|
961
1203
|
--ci-snapshot Fail if snapshot is missing (CI mode)
|
|
962
1204
|
|
|
1205
|
+
${this.logger.color('DIFF OPTIONS:', 'yellow')}
|
|
1206
|
+
-d, --diff Enable response diffing (compare with baseline)
|
|
1207
|
+
--diff-save Save current run as baseline
|
|
1208
|
+
--diff-label <name> Label for current run (e.g., 'staging', 'v1.0')
|
|
1209
|
+
--diff-compare <label> Compare against this baseline label
|
|
1210
|
+
--diff-dir <dir> Baseline storage directory (default: __baselines__)
|
|
1211
|
+
--diff-output <format> Output format (terminal|json|markdown)
|
|
1212
|
+
|
|
1213
|
+
${this.logger.color('DIFF SUBCOMMAND:', 'yellow')}
|
|
1214
|
+
curl-runner diff <label1> <label2> [file.yaml]
|
|
1215
|
+
Compare two stored baselines without making requests
|
|
1216
|
+
|
|
963
1217
|
${this.logger.color('EXAMPLES:', 'yellow')}
|
|
964
1218
|
# Run all YAML files in current directory
|
|
965
1219
|
curl-runner
|
|
@@ -1027,6 +1281,21 @@ ${this.logger.color('EXAMPLES:', 'yellow')}
|
|
|
1027
1281
|
# CI mode - fail if snapshot missing
|
|
1028
1282
|
curl-runner api.yaml --snapshot --ci-snapshot
|
|
1029
1283
|
|
|
1284
|
+
# Response diffing - save baseline for staging
|
|
1285
|
+
curl-runner api.yaml --diff-save --diff-label staging
|
|
1286
|
+
|
|
1287
|
+
# Compare current run against staging baseline
|
|
1288
|
+
curl-runner api.yaml --diff --diff-compare staging
|
|
1289
|
+
|
|
1290
|
+
# Compare staging vs production baselines (offline)
|
|
1291
|
+
curl-runner diff staging production api.yaml
|
|
1292
|
+
|
|
1293
|
+
# Auto-diff: creates baseline on first run, compares on subsequent runs
|
|
1294
|
+
curl-runner api.yaml --diff
|
|
1295
|
+
|
|
1296
|
+
# Diff with JSON output for CI
|
|
1297
|
+
curl-runner api.yaml --diff --diff-compare staging --diff-output json
|
|
1298
|
+
|
|
1030
1299
|
${this.logger.color('YAML STRUCTURE:', 'yellow')}
|
|
1031
1300
|
Single request:
|
|
1032
1301
|
request:
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ExecutionResult } from '../types/config';
|
|
3
|
+
import { BaselineManager } from './baseline-manager';
|
|
4
|
+
|
|
5
|
+
describe('BaselineManager', () => {
|
|
6
|
+
describe('getBaselinePath', () => {
|
|
7
|
+
test('should generate correct baseline path', () => {
|
|
8
|
+
const manager = new BaselineManager({});
|
|
9
|
+
expect(manager.getBaselinePath('tests/api.yaml', 'staging')).toBe(
|
|
10
|
+
'tests/__baselines__/api.staging.baseline.json',
|
|
11
|
+
);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test('should use custom directory', () => {
|
|
15
|
+
const manager = new BaselineManager({ dir: '.baselines' });
|
|
16
|
+
expect(manager.getBaselinePath('api.yaml', 'prod')).toBe('.baselines/api.prod.baseline.json');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should handle nested paths', () => {
|
|
20
|
+
const manager = new BaselineManager({});
|
|
21
|
+
expect(manager.getBaselinePath('tests/integration/users.yaml', 'staging')).toBe(
|
|
22
|
+
'tests/integration/__baselines__/users.staging.baseline.json',
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should handle labels with special characters', () => {
|
|
27
|
+
const manager = new BaselineManager({});
|
|
28
|
+
expect(manager.getBaselinePath('api.yaml', 'v1.0.0')).toBe(
|
|
29
|
+
'__baselines__/api.v1.0.0.baseline.json',
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('getBaselineDir', () => {
|
|
35
|
+
test('should return correct directory', () => {
|
|
36
|
+
const manager = new BaselineManager({});
|
|
37
|
+
expect(manager.getBaselineDir('tests/api.yaml')).toBe('tests/__baselines__');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('should use custom directory', () => {
|
|
41
|
+
const manager = new BaselineManager({ dir: 'snapshots' });
|
|
42
|
+
expect(manager.getBaselineDir('api.yaml')).toBe('snapshots');
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('createBaseline', () => {
|
|
47
|
+
const mockResult: ExecutionResult = {
|
|
48
|
+
request: { url: 'https://api.example.com', name: 'Get Users' },
|
|
49
|
+
success: true,
|
|
50
|
+
status: 200,
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'X-Request-Id': 'abc123',
|
|
54
|
+
},
|
|
55
|
+
body: {
|
|
56
|
+
id: 1,
|
|
57
|
+
name: 'Test',
|
|
58
|
+
timestamp: '2024-01-01',
|
|
59
|
+
},
|
|
60
|
+
metrics: {
|
|
61
|
+
duration: 150,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
test('should create baseline with all fields', () => {
|
|
66
|
+
const manager = new BaselineManager({});
|
|
67
|
+
const baseline = manager.createBaseline(mockResult, {});
|
|
68
|
+
|
|
69
|
+
expect(baseline.status).toBe(200);
|
|
70
|
+
expect(baseline.body).toEqual(mockResult.body);
|
|
71
|
+
expect(baseline.headers).toBeDefined();
|
|
72
|
+
expect(baseline.hash).toBeDefined();
|
|
73
|
+
expect(baseline.capturedAt).toBeDefined();
|
|
74
|
+
expect(baseline.timing).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('should include timing when configured', () => {
|
|
78
|
+
const manager = new BaselineManager({});
|
|
79
|
+
const baseline = manager.createBaseline(mockResult, { includeTimings: true });
|
|
80
|
+
|
|
81
|
+
expect(baseline.timing).toBe(150);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('should normalize headers (lowercase, sorted)', () => {
|
|
85
|
+
const manager = new BaselineManager({});
|
|
86
|
+
const baseline = manager.createBaseline(mockResult, {});
|
|
87
|
+
|
|
88
|
+
expect(baseline.headers).toEqual({
|
|
89
|
+
'content-type': 'application/json',
|
|
90
|
+
'x-request-id': 'abc123',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('hash', () => {
|
|
96
|
+
test('should generate consistent hashes', () => {
|
|
97
|
+
const manager = new BaselineManager({});
|
|
98
|
+
const content = { id: 1, name: 'test' };
|
|
99
|
+
|
|
100
|
+
const hash1 = manager.hash(content);
|
|
101
|
+
const hash2 = manager.hash(content);
|
|
102
|
+
|
|
103
|
+
expect(hash1).toBe(hash2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('should generate different hashes for different content', () => {
|
|
107
|
+
const manager = new BaselineManager({});
|
|
108
|
+
|
|
109
|
+
const hash1 = manager.hash({ id: 1 });
|
|
110
|
+
const hash2 = manager.hash({ id: 2 });
|
|
111
|
+
|
|
112
|
+
expect(hash1).not.toBe(hash2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('should produce 8 character hash', () => {
|
|
116
|
+
const manager = new BaselineManager({});
|
|
117
|
+
const hash = manager.hash({ data: 'test' });
|
|
118
|
+
|
|
119
|
+
expect(hash).toHaveLength(8);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
describe('mergeConfig', () => {
|
|
124
|
+
test('should return null if not enabled', () => {
|
|
125
|
+
const config = BaselineManager.mergeConfig({}, undefined);
|
|
126
|
+
expect(config).toBeNull();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('should return null if explicitly disabled', () => {
|
|
130
|
+
const config = BaselineManager.mergeConfig({}, { enabled: false });
|
|
131
|
+
expect(config).toBeNull();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('should handle boolean true', () => {
|
|
135
|
+
const config = BaselineManager.mergeConfig({}, true);
|
|
136
|
+
expect(config).toEqual({
|
|
137
|
+
enabled: true,
|
|
138
|
+
exclude: [],
|
|
139
|
+
match: {},
|
|
140
|
+
includeTimings: false,
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('should merge global and request excludes', () => {
|
|
145
|
+
const config = BaselineManager.mergeConfig(
|
|
146
|
+
{ exclude: ['*.timestamp'] },
|
|
147
|
+
{ enabled: true, exclude: ['body.id'] },
|
|
148
|
+
);
|
|
149
|
+
expect(config?.exclude).toEqual(['*.timestamp', 'body.id']);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('should merge match rules', () => {
|
|
153
|
+
const config = BaselineManager.mergeConfig(
|
|
154
|
+
{ match: { 'body.id': '*' } },
|
|
155
|
+
{ enabled: true, match: { 'body.token': 'regex:^[a-z]+$' } },
|
|
156
|
+
);
|
|
157
|
+
expect(config?.match).toEqual({
|
|
158
|
+
'body.id': '*',
|
|
159
|
+
'body.token': 'regex:^[a-z]+$',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test('should use global enabled', () => {
|
|
164
|
+
const config = BaselineManager.mergeConfig({ enabled: true }, undefined);
|
|
165
|
+
expect(config?.enabled).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('should inherit includeTimings from global', () => {
|
|
169
|
+
const config = BaselineManager.mergeConfig({ enabled: true, includeTimings: true }, true);
|
|
170
|
+
expect(config?.includeTimings).toBe(true);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test('should allow request to override includeTimings', () => {
|
|
174
|
+
const config = BaselineManager.mergeConfig(
|
|
175
|
+
{ enabled: true, includeTimings: false },
|
|
176
|
+
{ enabled: true, includeTimings: true },
|
|
177
|
+
);
|
|
178
|
+
expect(config?.includeTimings).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
});
|