@zohodesk/unit-testing-framework 0.0.30-experimental → 0.0.31-experimental
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/build/src/runner/__tests__/jest-runner.test.js +115 -0
- package/build/src/runner/jest-runner.js +128 -13
- package/build/src/utils/__tests__/pipeline-summary-directory.test.js +248 -0
- package/build/src/utils/__tests__/pipeline-summary.test.js +473 -0
- package/build/src/utils/pipeline-summary.js +359 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _jestRunner = _interopRequireDefault(require("../jest-runner.js"));
|
|
4
|
+
var _pipelineSummary = require("../../utils/pipeline-summary.js");
|
|
5
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
6
|
+
var _path = _interopRequireDefault(require("path"));
|
|
7
|
+
var _url = require("url");
|
|
8
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
const _dirname = _path.default.dirname((0, _url.fileURLToPath)(import.meta.url));
|
|
10
|
+
const TEST_OUTPUT_DIR = _path.default.join(_dirname, '../../__temp__');
|
|
11
|
+
describe('createJestRunner with Pipeline Summary', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = _path.default.join(TEST_OUTPUT_DIR, 'jest-runner-test');
|
|
15
|
+
if (!_fs.default.existsSync(tempDir)) {
|
|
16
|
+
_fs.default.mkdirSync(tempDir, {
|
|
17
|
+
recursive: true
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
if (_fs.default.existsSync(tempDir)) {
|
|
23
|
+
_fs.default.rmSync(tempDir, {
|
|
24
|
+
recursive: true,
|
|
25
|
+
force: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
test('should return object with testResults, summary, and success flag', async () => {
|
|
30
|
+
const summaryPath = _path.default.join(tempDir, 'summary.json');
|
|
31
|
+
const result = await (0, _jestRunner.default)({
|
|
32
|
+
projectRoot: process.cwd(),
|
|
33
|
+
summaryPath,
|
|
34
|
+
testFiles: 'src/utils/__tests__/pipeline-summary.test.js'
|
|
35
|
+
});
|
|
36
|
+
expect(result).toHaveProperty('testResults');
|
|
37
|
+
expect(result).toHaveProperty('summary');
|
|
38
|
+
expect(result).toHaveProperty('success');
|
|
39
|
+
expect(typeof result.success).toBe('boolean');
|
|
40
|
+
});
|
|
41
|
+
test('should create summary file at specified path', async () => {
|
|
42
|
+
const summaryPath = _path.default.join(tempDir, 'test-summary.json');
|
|
43
|
+
await (0, _jestRunner.default)({
|
|
44
|
+
projectRoot: process.cwd(),
|
|
45
|
+
summaryPath,
|
|
46
|
+
testFiles: 'src/utils/__tests__/pipeline-summary.test.js'
|
|
47
|
+
});
|
|
48
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
49
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
50
|
+
expect(content.status).toMatch(/^(SUCCESS|FAILURE)$/);
|
|
51
|
+
});
|
|
52
|
+
test('should generate SUCCESS summary when tests pass', async () => {
|
|
53
|
+
const summaryPath = _path.default.join(tempDir, 'success-summary.json');
|
|
54
|
+
const result = await (0, _jestRunner.default)({
|
|
55
|
+
projectRoot: process.cwd(),
|
|
56
|
+
summaryPath,
|
|
57
|
+
testFiles: 'src/utils/__tests__/pipeline-summary.test.js'
|
|
58
|
+
});
|
|
59
|
+
if (result.success) {
|
|
60
|
+
expect(result.summary.status).toBe('SUCCESS');
|
|
61
|
+
expect(result.summary.message).toContain('passed');
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
test('should include web_url in summary when provided', async () => {
|
|
65
|
+
const summaryPath = _path.default.join(tempDir, 'url-summary.json');
|
|
66
|
+
const webUrl = 'https://ci.example.com/build/123';
|
|
67
|
+
const result = await (0, _jestRunner.default)({
|
|
68
|
+
projectRoot: process.cwd(),
|
|
69
|
+
summaryPath,
|
|
70
|
+
webUrl,
|
|
71
|
+
testFiles: 'src/utils/__tests__/pipeline-summary.test.js'
|
|
72
|
+
});
|
|
73
|
+
expect(result.summary.web_url).toBe(webUrl);
|
|
74
|
+
});
|
|
75
|
+
test('should handle missing test files gracefully', async () => {
|
|
76
|
+
const summaryPath = _path.default.join(tempDir, 'error-summary.json');
|
|
77
|
+
const result = await (0, _jestRunner.default)({
|
|
78
|
+
projectRoot: process.cwd(),
|
|
79
|
+
summaryPath,
|
|
80
|
+
testFiles: 'nonexistent-file.test.js'
|
|
81
|
+
});
|
|
82
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
83
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
84
|
+
expect(content.status).toBe('FAILURE');
|
|
85
|
+
});
|
|
86
|
+
test('should use default summary path if not provided', async () => {
|
|
87
|
+
const result = await (0, _jestRunner.default)({
|
|
88
|
+
projectRoot: process.cwd(),
|
|
89
|
+
testFiles: 'src/utils/__tests__/pipeline-summary.test.js'
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Should create reports directory and test-summary.json
|
|
93
|
+
expect(result.summary).toBeDefined();
|
|
94
|
+
expect(result.summary.status).toMatch(/^(SUCCESS|FAILURE)$/);
|
|
95
|
+
});
|
|
96
|
+
test('summary should have valid error structure on failure', async () => {
|
|
97
|
+
const summaryPath = _path.default.join(tempDir, 'error-check-summary.json');
|
|
98
|
+
const result = await (0, _jestRunner.default)({
|
|
99
|
+
projectRoot: process.cwd(),
|
|
100
|
+
summaryPath,
|
|
101
|
+
testFiles: 'nonexistent.test.js'
|
|
102
|
+
});
|
|
103
|
+
if (!result.success) {
|
|
104
|
+
const {
|
|
105
|
+
summary
|
|
106
|
+
} = result;
|
|
107
|
+
expect(summary.status).toBe('FAILURE');
|
|
108
|
+
expect(summary.error).toBeDefined();
|
|
109
|
+
expect(summary.error.code).toBeDefined();
|
|
110
|
+
expect(summary.error.category).toMatch(/^(code|environment|flaky|tool)$/);
|
|
111
|
+
expect(summary.error.message).toBeDefined();
|
|
112
|
+
expect(Array.isArray(summary.error.errors)).toBe(true);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -8,6 +8,7 @@ var _fs = _interopRequireDefault(require("fs"));
|
|
|
8
8
|
var _path = _interopRequireDefault(require("path"));
|
|
9
9
|
var _runnerBase = require("./runner-base.js");
|
|
10
10
|
var _configLoader = require("../config/config-loader.js");
|
|
11
|
+
var _pipelineSummary = require("../utils/pipeline-summary.js");
|
|
11
12
|
var _logger = require("../utils/logger.js");
|
|
12
13
|
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
13
14
|
function resolveTestFiles(testFiles, projectRoot) {
|
|
@@ -32,21 +33,135 @@ async function createJestRunner(options = {}) {
|
|
|
32
33
|
testFiles,
|
|
33
34
|
testPathPattern,
|
|
34
35
|
configPath,
|
|
35
|
-
watch = false
|
|
36
|
+
watch = false,
|
|
37
|
+
summaryPath = _path.default.join(projectRoot, 'test-slices', 'unit-test', 'unit_reports', 'ut-test-summary.json'),
|
|
38
|
+
webUrl = process.env.CI_BUILD_URL || ''
|
|
36
39
|
} = options;
|
|
37
40
|
|
|
38
|
-
// ──
|
|
39
|
-
|
|
41
|
+
// ── 0. Setup pipeline summary (mandatory) ──────────────────────
|
|
42
|
+
_logger.Logger.info(`Summary path: ${summaryPath}`);
|
|
43
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('JestRunner', {
|
|
44
|
+
outputPath: summaryPath,
|
|
45
|
+
webUrl
|
|
46
|
+
});
|
|
47
|
+
try {
|
|
48
|
+
// ── 1. Resolve config (auto-discover / configPath / default) ──
|
|
49
|
+
const config = await (0, _configLoader.resolveConfig)(projectRoot, configPath);
|
|
40
50
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
51
|
+
// ── 2. Resolve test file pattern ──────────────────────────────
|
|
52
|
+
const rawPattern = testFiles || testPathPattern;
|
|
53
|
+
const pattern = rawPattern && rawPattern.includes(',') ? resolveTestFiles(rawPattern, projectRoot) : rawPattern;
|
|
44
54
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
// ── 3. Build argv & run Jest ──────────────────────────────────
|
|
56
|
+
const argv = (0, _runnerBase.buildArgv)(config, {
|
|
57
|
+
testMatch: pattern,
|
|
58
|
+
watch,
|
|
59
|
+
projectRoot
|
|
60
|
+
});
|
|
61
|
+
const testResults = await (0, _runnerBase.executeJest)(argv, projectRoot);
|
|
62
|
+
|
|
63
|
+
// ── 4. Process results and generate summary ───────────────────
|
|
64
|
+
if (!testResults) {
|
|
65
|
+
const response = summary.toolError({
|
|
66
|
+
toolName: 'jest',
|
|
67
|
+
error: 'No test results returned',
|
|
68
|
+
exitCode: 1
|
|
69
|
+
});
|
|
70
|
+
const saved = summary.saveToFile(response);
|
|
71
|
+
if (!saved) {
|
|
72
|
+
_logger.Logger.warn('⚠ Warning: Error summary file could not be saved');
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
testResults: null,
|
|
76
|
+
summary: response,
|
|
77
|
+
success: false
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (testResults.numFailedTests > 0) {
|
|
81
|
+
const response = summary.testFailure({
|
|
82
|
+
testResults
|
|
83
|
+
});
|
|
84
|
+
summary.saveToFile(response);
|
|
85
|
+
return {
|
|
86
|
+
testResults,
|
|
87
|
+
summary: response,
|
|
88
|
+
success: false
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (testResults.numTotalTests === 0) {
|
|
92
|
+
const response = summary.failure({
|
|
93
|
+
message: 'No tests found',
|
|
94
|
+
errorCode: 'NO_TESTS_FOUND',
|
|
95
|
+
errorCategory: 'code',
|
|
96
|
+
errorMessage: 'No test files matched the pattern'
|
|
97
|
+
});
|
|
98
|
+
summary.saveToFile(response);
|
|
99
|
+
return {
|
|
100
|
+
testResults,
|
|
101
|
+
summary: response,
|
|
102
|
+
success: false
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// All tests passed
|
|
107
|
+
const response = summary.success({
|
|
108
|
+
message: `All ${testResults.numTotalTests} tests passed (${testResults.numPassedTests} passed, ${testResults.numSkippedTests} skipped)`
|
|
109
|
+
});
|
|
110
|
+
const saved = summary.saveToFile(response);
|
|
111
|
+
if (!saved) {
|
|
112
|
+
_logger.Logger.warn('⚠ Warning: Summary file could not be saved');
|
|
113
|
+
} else {
|
|
114
|
+
_logger.Logger.success(`✓ Test Summary saved to: ${summaryPath}`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
testResults,
|
|
118
|
+
summary: response,
|
|
119
|
+
success: true
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
// ── 5. Error handling ────────────────────────────────────────
|
|
123
|
+
_logger.Logger.error(`✗ Jest execution error: ${error.message}`);
|
|
124
|
+
let response;
|
|
125
|
+
if (error.message.includes('heap') || error.message.includes('memory') || error.message.includes('ENOMEM')) {
|
|
126
|
+
response = summary.environmentError({
|
|
127
|
+
issue: 'Out of memory during test execution',
|
|
128
|
+
requirement: 'Sufficient heap memory',
|
|
129
|
+
suggestion: 'Increase Node.js heap size: NODE_OPTIONS=--max-old-space-size=4096'
|
|
130
|
+
});
|
|
131
|
+
} else if (error.message.includes('MODULE_NOT_FOUND') || error.message.includes('Cannot find')) {
|
|
132
|
+
response = summary.environmentError({
|
|
133
|
+
issue: 'Missing dependency',
|
|
134
|
+
suggestion: 'Run: npm install'
|
|
135
|
+
});
|
|
136
|
+
} else if (error.code === 'ENOENT') {
|
|
137
|
+
response = summary.failure({
|
|
138
|
+
message: 'Test file or configuration not found',
|
|
139
|
+
errorCode: 'FILE_NOT_FOUND',
|
|
140
|
+
errorCategory: 'environment',
|
|
141
|
+
errorMessage: `${error.path || 'Unknown file'} not found`
|
|
142
|
+
});
|
|
143
|
+
} else if (error.message.includes('ECONNREFUSED')) {
|
|
144
|
+
response = summary.environmentError({
|
|
145
|
+
issue: 'Connection refused to external service',
|
|
146
|
+
suggestion: 'Check if required services (database, cache) are running'
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
response = summary.toolError({
|
|
150
|
+
toolName: 'jest',
|
|
151
|
+
error,
|
|
152
|
+
exitCode: error.code || 1
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
const saved = summary.saveToFile(response);
|
|
156
|
+
if (saved) {
|
|
157
|
+
_logger.Logger.error(`✓ Error Summary saved to: ${summaryPath}`);
|
|
158
|
+
} else {
|
|
159
|
+
_logger.Logger.warn('⚠ Warning: Error summary file could not be saved');
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
testResults: null,
|
|
163
|
+
summary: response,
|
|
164
|
+
success: false
|
|
165
|
+
};
|
|
166
|
+
}
|
|
52
167
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
4
|
+
var _path = _interopRequireDefault(require("path"));
|
|
5
|
+
var _url = require("url");
|
|
6
|
+
var _pipelineSummary = require("../pipeline-summary.js");
|
|
7
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
const _dirname = _path.default.dirname((0, _url.fileURLToPath)(import.meta.url));
|
|
9
|
+
const TEST_ROOT = _path.default.join(_dirname, '../../__temp__/dir-tests');
|
|
10
|
+
describe('Pipeline Summary - Directory Creation Edge Cases', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
// Cleanup
|
|
13
|
+
if (_fs.default.existsSync(TEST_ROOT)) {
|
|
14
|
+
_fs.default.rmSync(TEST_ROOT, {
|
|
15
|
+
recursive: true,
|
|
16
|
+
force: true
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
describe('Single level directory creation', () => {
|
|
21
|
+
test('should create single-level directory', () => {
|
|
22
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'reports', 'summary.json');
|
|
23
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
24
|
+
outputPath: summaryPath
|
|
25
|
+
});
|
|
26
|
+
const response = summary.success({
|
|
27
|
+
message: 'Test passed'
|
|
28
|
+
});
|
|
29
|
+
const saved = summary.saveToFile(response);
|
|
30
|
+
expect(saved).toBe(true);
|
|
31
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
32
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
33
|
+
expect(content.status).toBe('SUCCESS');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe('Multi-level directory creation', () => {
|
|
37
|
+
test('should create deeply nested directories', () => {
|
|
38
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'a', 'b', 'c', 'd', 'e', 'summary.json');
|
|
39
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
40
|
+
outputPath: summaryPath
|
|
41
|
+
});
|
|
42
|
+
const response = summary.failure({
|
|
43
|
+
message: 'Test failed',
|
|
44
|
+
errorCode: 'TEST_ERROR',
|
|
45
|
+
errorCategory: 'code',
|
|
46
|
+
errorMessage: 'Assertion failed'
|
|
47
|
+
});
|
|
48
|
+
const saved = summary.saveToFile(response);
|
|
49
|
+
expect(saved).toBe(true);
|
|
50
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
51
|
+
|
|
52
|
+
// Verify all parent directories were created
|
|
53
|
+
const allDirs = [_path.default.join(TEST_ROOT, 'a'), _path.default.join(TEST_ROOT, 'a', 'b'), _path.default.join(TEST_ROOT, 'a', 'b', 'c'), _path.default.join(TEST_ROOT, 'a', 'b', 'c', 'd'), _path.default.join(TEST_ROOT, 'a', 'b', 'c', 'd', 'e')];
|
|
54
|
+
allDirs.forEach(dir => {
|
|
55
|
+
expect(_fs.default.existsSync(dir)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
describe('Directory already exists', () => {
|
|
60
|
+
test('should handle when directory already exists', () => {
|
|
61
|
+
const dir = _path.default.join(TEST_ROOT, 'existing');
|
|
62
|
+
_fs.default.mkdirSync(dir, {
|
|
63
|
+
recursive: true
|
|
64
|
+
});
|
|
65
|
+
const summaryPath = _path.default.join(dir, 'summary.json');
|
|
66
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
67
|
+
outputPath: summaryPath
|
|
68
|
+
});
|
|
69
|
+
const response = summary.success();
|
|
70
|
+
const saved = summary.saveToFile(response);
|
|
71
|
+
expect(saved).toBe(true);
|
|
72
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
describe('File overwrite', () => {
|
|
76
|
+
test('should overwrite existing summary file', () => {
|
|
77
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'reports', 'summary.json');
|
|
78
|
+
_fs.default.mkdirSync(_path.default.dirname(summaryPath), {
|
|
79
|
+
recursive: true
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Write first response
|
|
83
|
+
_fs.default.writeFileSync(summaryPath, JSON.stringify({
|
|
84
|
+
version: 1
|
|
85
|
+
}), 'utf-8');
|
|
86
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
87
|
+
outputPath: summaryPath
|
|
88
|
+
});
|
|
89
|
+
const response = summary.success({
|
|
90
|
+
message: 'New summary'
|
|
91
|
+
});
|
|
92
|
+
const saved = summary.saveToFile(response);
|
|
93
|
+
expect(saved).toBe(true);
|
|
94
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
95
|
+
expect(content.status).toBe('SUCCESS');
|
|
96
|
+
expect(content.message).toBe('New summary');
|
|
97
|
+
expect(content.version).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
describe('Invalid path handling', () => {
|
|
101
|
+
test('should handle null output path', () => {
|
|
102
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
103
|
+
outputPath: null
|
|
104
|
+
});
|
|
105
|
+
const response = summary.success();
|
|
106
|
+
const saved = summary.saveToFile(response);
|
|
107
|
+
expect(saved).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
test('should handle empty output path', () => {
|
|
110
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
111
|
+
outputPath: ''
|
|
112
|
+
});
|
|
113
|
+
const response = summary.success();
|
|
114
|
+
const saved = summary.saveToFile(response);
|
|
115
|
+
expect(saved).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
test('should handle override path', () => {
|
|
118
|
+
const primaryPath = _path.default.join(TEST_ROOT, 'primary.json');
|
|
119
|
+
const overridePath = _path.default.join(TEST_ROOT, 'custom', 'override.json');
|
|
120
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
121
|
+
outputPath: primaryPath
|
|
122
|
+
});
|
|
123
|
+
const response = summary.success();
|
|
124
|
+
const saved = summary.saveToFile(response, overridePath);
|
|
125
|
+
expect(saved).toBe(true);
|
|
126
|
+
expect(_fs.default.existsSync(overridePath)).toBe(true);
|
|
127
|
+
expect(_fs.default.existsSync(primaryPath)).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
describe('Complex paths with special characters', () => {
|
|
131
|
+
test('should handle paths with hyphens and underscores', () => {
|
|
132
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'test-reports_v2', 'unit-tests_final', 'summary-result.json');
|
|
133
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
134
|
+
outputPath: summaryPath
|
|
135
|
+
});
|
|
136
|
+
const response = summary.success();
|
|
137
|
+
const saved = summary.saveToFile(response);
|
|
138
|
+
expect(saved).toBe(true);
|
|
139
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
test('should handle paths with dots and numbers', () => {
|
|
142
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'v2.1.0', 'reports-2024.01.15', 'summary.json');
|
|
143
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
144
|
+
outputPath: summaryPath
|
|
145
|
+
});
|
|
146
|
+
const response = summary.success();
|
|
147
|
+
const saved = summary.saveToFile(response);
|
|
148
|
+
expect(saved).toBe(true);
|
|
149
|
+
expect(_fs.default.existsSync(summaryPath)).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
describe('File content verification', () => {
|
|
153
|
+
test('should write valid JSON with proper formatting', () => {
|
|
154
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'reports', 'summary.json');
|
|
155
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
156
|
+
outputPath: summaryPath
|
|
157
|
+
});
|
|
158
|
+
const response = summary.success({
|
|
159
|
+
message: 'All tests passed'
|
|
160
|
+
});
|
|
161
|
+
summary.saveToFile(response);
|
|
162
|
+
const content = _fs.default.readFileSync(summaryPath, 'utf-8');
|
|
163
|
+
|
|
164
|
+
// Verify it's valid JSON
|
|
165
|
+
expect(() => JSON.parse(content)).not.toThrow();
|
|
166
|
+
|
|
167
|
+
// Verify formatting (should have indentation)
|
|
168
|
+
expect(content).toContain('\n ');
|
|
169
|
+
|
|
170
|
+
// Verify content
|
|
171
|
+
const parsed = JSON.parse(content);
|
|
172
|
+
expect(parsed.status).toBe('SUCCESS');
|
|
173
|
+
expect(parsed.message).toBe('All tests passed');
|
|
174
|
+
});
|
|
175
|
+
test('should handle complex response objects', () => {
|
|
176
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'reports', 'summary.json');
|
|
177
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
178
|
+
outputPath: summaryPath
|
|
179
|
+
});
|
|
180
|
+
const response = summary.testFailure({
|
|
181
|
+
testResults: {
|
|
182
|
+
numTotalTests: 100,
|
|
183
|
+
numFailedTests: 5,
|
|
184
|
+
numPassedTests: 95,
|
|
185
|
+
numSkippedTests: 0,
|
|
186
|
+
testResults: [{
|
|
187
|
+
name: 'test.js',
|
|
188
|
+
numFailingTests: 1,
|
|
189
|
+
assertionResults: [{
|
|
190
|
+
title: 'should work',
|
|
191
|
+
fullName: 'Suite › should work',
|
|
192
|
+
status: 'failed',
|
|
193
|
+
failureMessages: ['Expected true to be false']
|
|
194
|
+
}]
|
|
195
|
+
}]
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
const saved = summary.saveToFile(response);
|
|
199
|
+
expect(saved).toBe(true);
|
|
200
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
201
|
+
expect(content.error.errors).toHaveLength(1);
|
|
202
|
+
expect(content.result[0].failedTests).toBe(5);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
describe('Concurrent operations', () => {
|
|
206
|
+
test('should handle sequential saves to different files', () => {
|
|
207
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
208
|
+
outputPath: _path.default.join(TEST_ROOT, 'file1.json')
|
|
209
|
+
});
|
|
210
|
+
const response1 = summary.success({
|
|
211
|
+
message: 'First'
|
|
212
|
+
});
|
|
213
|
+
const response2 = summary.failure({
|
|
214
|
+
message: 'Second'
|
|
215
|
+
});
|
|
216
|
+
const saved1 = summary.saveToFile(response1);
|
|
217
|
+
const saved2 = summary.saveToFile(response2, _path.default.join(TEST_ROOT, 'file2.json'));
|
|
218
|
+
expect(saved1).toBe(true);
|
|
219
|
+
expect(saved2).toBe(true);
|
|
220
|
+
expect(_fs.default.existsSync(_path.default.join(TEST_ROOT, 'file1.json'))).toBe(true);
|
|
221
|
+
expect(_fs.default.existsSync(_path.default.join(TEST_ROOT, 'file2.json'))).toBe(true);
|
|
222
|
+
});
|
|
223
|
+
test('should handle multiple saves to same location', () => {
|
|
224
|
+
const summaryPath = _path.default.join(TEST_ROOT, 'reports', 'summary.json');
|
|
225
|
+
const summary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
226
|
+
outputPath: summaryPath
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// First save
|
|
230
|
+
const response1 = summary.success({
|
|
231
|
+
message: 'First attempt'
|
|
232
|
+
});
|
|
233
|
+
const saved1 = summary.saveToFile(response1);
|
|
234
|
+
expect(saved1).toBe(true);
|
|
235
|
+
|
|
236
|
+
// Second save (overwrite)
|
|
237
|
+
const response2 = summary.success({
|
|
238
|
+
message: 'Second attempt'
|
|
239
|
+
});
|
|
240
|
+
const saved2 = summary.saveToFile(response2);
|
|
241
|
+
expect(saved2).toBe(true);
|
|
242
|
+
|
|
243
|
+
// Verify only latest content exists
|
|
244
|
+
const content = JSON.parse(_fs.default.readFileSync(summaryPath, 'utf-8'));
|
|
245
|
+
expect(content.message).toBe('Second attempt');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
4
|
+
var _path = _interopRequireDefault(require("path"));
|
|
5
|
+
var _url = require("url");
|
|
6
|
+
var _pipelineSummary = require("../pipeline-summary.js");
|
|
7
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
const _dirname = _path.default.dirname((0, _url.fileURLToPath)(import.meta.url));
|
|
9
|
+
const TEST_OUTPUT_DIR = _path.default.join(_dirname, '../../__temp__');
|
|
10
|
+
describe('PipelineSummaryGenerator', () => {
|
|
11
|
+
let summary;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
summary = (0, _pipelineSummary.createPipelineSummary)('TestStage', {
|
|
14
|
+
outputPath: _path.default.join(TEST_OUTPUT_DIR, 'test-summary.json'),
|
|
15
|
+
webUrl: 'https://example.com/report'
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// Create temp directory
|
|
19
|
+
if (!_fs.default.existsSync(TEST_OUTPUT_DIR)) {
|
|
20
|
+
_fs.default.mkdirSync(TEST_OUTPUT_DIR, {
|
|
21
|
+
recursive: true
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Clean up
|
|
27
|
+
if (_fs.default.existsSync(TEST_OUTPUT_DIR)) {
|
|
28
|
+
_fs.default.rmSync(TEST_OUTPUT_DIR, {
|
|
29
|
+
recursive: true,
|
|
30
|
+
force: true
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
36
|
+
// SUCCESS RESPONSE TESTS
|
|
37
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
describe('success()', () => {
|
|
40
|
+
test('should create success response with defaults', () => {
|
|
41
|
+
const response = summary.success();
|
|
42
|
+
expect(response.status).toBe('SUCCESS');
|
|
43
|
+
expect(response.message).toBe('TestStage completed successfully');
|
|
44
|
+
expect(response.web_url).toBe('https://example.com/report');
|
|
45
|
+
});
|
|
46
|
+
test('should create success response with custom message', () => {
|
|
47
|
+
const response = summary.success({
|
|
48
|
+
message: 'All tests passed'
|
|
49
|
+
});
|
|
50
|
+
expect(response.status).toBe('SUCCESS');
|
|
51
|
+
expect(response.message).toBe('All tests passed');
|
|
52
|
+
});
|
|
53
|
+
test('should create success response with custom web_url', () => {
|
|
54
|
+
const response = summary.success({
|
|
55
|
+
webUrl: 'https://example.com/custom-report'
|
|
56
|
+
});
|
|
57
|
+
expect(response.web_url).toBe('https://example.com/custom-report');
|
|
58
|
+
});
|
|
59
|
+
test('should not contain error field in success response', () => {
|
|
60
|
+
const response = summary.success();
|
|
61
|
+
expect(response).not.toHaveProperty('error');
|
|
62
|
+
expect(response).not.toHaveProperty('result');
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
67
|
+
// FAILURE RESPONSE TESTS
|
|
68
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
69
|
+
|
|
70
|
+
describe('failure()', () => {
|
|
71
|
+
test('should create failure response with defaults', () => {
|
|
72
|
+
const response = summary.failure();
|
|
73
|
+
expect(response.status).toBe('FAILURE');
|
|
74
|
+
expect(response.message).toBe('TestStage failed');
|
|
75
|
+
expect(response.error.code).toBe('UNKNOWN_ERROR');
|
|
76
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.TOOL);
|
|
77
|
+
expect(Array.isArray(response.error.errors)).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
test('should create failure response with custom error details', () => {
|
|
80
|
+
const response = summary.failure({
|
|
81
|
+
message: 'Database connection failed',
|
|
82
|
+
errorCode: 'DB_CONN_ERROR',
|
|
83
|
+
errorCategory: _pipelineSummary.ERROR_CATEGORIES.ENVIRONMENT,
|
|
84
|
+
errorMessage: 'Cannot connect to database',
|
|
85
|
+
errors: ['Connection timeout', 'Host unreachable']
|
|
86
|
+
});
|
|
87
|
+
expect(response.status).toBe('FAILURE');
|
|
88
|
+
expect(response.message).toBe('Database connection failed');
|
|
89
|
+
expect(response.error.code).toBe('DB_CONN_ERROR');
|
|
90
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.ENVIRONMENT);
|
|
91
|
+
expect(response.error.message).toBe('Cannot connect to database');
|
|
92
|
+
expect(response.error.errors).toHaveLength(2);
|
|
93
|
+
});
|
|
94
|
+
test('should accept single error and convert to array', () => {
|
|
95
|
+
const response = summary.failure({
|
|
96
|
+
errors: 'Single error message'
|
|
97
|
+
});
|
|
98
|
+
expect(Array.isArray(response.error.errors)).toBe(true);
|
|
99
|
+
expect(response.error.errors[0]).toBe('Single error message');
|
|
100
|
+
});
|
|
101
|
+
test('should include result field if provided', () => {
|
|
102
|
+
const response = summary.failure({
|
|
103
|
+
result: [{
|
|
104
|
+
key: 'value'
|
|
105
|
+
}]
|
|
106
|
+
});
|
|
107
|
+
expect(response.result).toEqual([{
|
|
108
|
+
key: 'value'
|
|
109
|
+
}]);
|
|
110
|
+
});
|
|
111
|
+
test('should not include empty result field', () => {
|
|
112
|
+
const response = summary.failure({
|
|
113
|
+
result: []
|
|
114
|
+
});
|
|
115
|
+
expect(response).not.toHaveProperty('result');
|
|
116
|
+
});
|
|
117
|
+
test('should validate and warn for invalid error category', () => {
|
|
118
|
+
const response = summary.failure({
|
|
119
|
+
errorCategory: 'invalid_category'
|
|
120
|
+
});
|
|
121
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.TOOL);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
126
|
+
// TEST FAILURE TESTS
|
|
127
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
describe('testFailure()', () => {
|
|
130
|
+
test('should handle test failure without results object', () => {
|
|
131
|
+
const response = summary.testFailure({
|
|
132
|
+
message: 'Tests failed'
|
|
133
|
+
});
|
|
134
|
+
expect(response.status).toBe('FAILURE');
|
|
135
|
+
expect(response.error.code).toBe('TEST_FAILURE');
|
|
136
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.CODE);
|
|
137
|
+
});
|
|
138
|
+
test('should extract failed test information from jest results', () => {
|
|
139
|
+
const testResults = {
|
|
140
|
+
numTotalTests: 10,
|
|
141
|
+
numFailedTests: 2,
|
|
142
|
+
numPassedTests: 8,
|
|
143
|
+
numSkippedTests: 0,
|
|
144
|
+
testResults: [{
|
|
145
|
+
name: 'test-file-1.js',
|
|
146
|
+
numFailingTests: 1,
|
|
147
|
+
assertionResults: [{
|
|
148
|
+
title: 'Test 1',
|
|
149
|
+
fullName: 'Suite › Test 1',
|
|
150
|
+
status: 'failed',
|
|
151
|
+
failureMessages: ['Expected true to be false']
|
|
152
|
+
}]
|
|
153
|
+
}, {
|
|
154
|
+
name: 'test-file-2.js',
|
|
155
|
+
numFailingTests: 1,
|
|
156
|
+
assertionResults: [{
|
|
157
|
+
title: 'Test 2',
|
|
158
|
+
fullName: 'Suite › Test 2',
|
|
159
|
+
status: 'failed',
|
|
160
|
+
failureMessages: ['Timeout after 5000ms']
|
|
161
|
+
}]
|
|
162
|
+
}]
|
|
163
|
+
};
|
|
164
|
+
const response = summary.testFailure({
|
|
165
|
+
testResults
|
|
166
|
+
});
|
|
167
|
+
expect(response.error.errors).toHaveLength(2);
|
|
168
|
+
expect(response.error.errors[0].test).toBe('Suite › Test 1');
|
|
169
|
+
expect(response.error.errors[0].file).toBe('test-file-1.js');
|
|
170
|
+
expect(response.result[0].failedTests).toBe(2);
|
|
171
|
+
});
|
|
172
|
+
test('should include test statistics in result', () => {
|
|
173
|
+
const testResults = {
|
|
174
|
+
numTotalTests: 50,
|
|
175
|
+
numFailedTests: 5,
|
|
176
|
+
numPassedTests: 45,
|
|
177
|
+
numSkippedTests: 0,
|
|
178
|
+
testResults: []
|
|
179
|
+
};
|
|
180
|
+
const response = summary.testFailure({
|
|
181
|
+
testResults
|
|
182
|
+
});
|
|
183
|
+
expect(response.result[0]).toEqual({
|
|
184
|
+
totalTests: 50,
|
|
185
|
+
failedTests: 5,
|
|
186
|
+
passedTests: 45,
|
|
187
|
+
skippedTests: 0
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
193
|
+
// TOOL ERROR TESTS
|
|
194
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
describe('toolError()', () => {
|
|
197
|
+
test('should handle error object', () => {
|
|
198
|
+
const error = new Error('Module not found');
|
|
199
|
+
error.stack = 'Error: Module not found\n at line 10';
|
|
200
|
+
const response = summary.toolError({
|
|
201
|
+
toolName: 'jest',
|
|
202
|
+
error,
|
|
203
|
+
exitCode: 1
|
|
204
|
+
});
|
|
205
|
+
expect(response.status).toBe('FAILURE');
|
|
206
|
+
expect(response.error.code).toBe('JEST_EXECUTION_ERROR');
|
|
207
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.TOOL);
|
|
208
|
+
expect(response.error.errors[0].tool).toBe('jest');
|
|
209
|
+
expect(response.error.errors[0].stack).toBeDefined();
|
|
210
|
+
});
|
|
211
|
+
test('should handle string error message', () => {
|
|
212
|
+
const response = summary.toolError({
|
|
213
|
+
toolName: 'npm',
|
|
214
|
+
error: 'npm ERR! code ERESOLVE',
|
|
215
|
+
exitCode: 1
|
|
216
|
+
});
|
|
217
|
+
expect(response.error.errors[0].message).toBe('npm ERR! code ERESOLVE');
|
|
218
|
+
});
|
|
219
|
+
test('should handle missing error', () => {
|
|
220
|
+
const response = summary.toolError({
|
|
221
|
+
toolName: 'webpack',
|
|
222
|
+
exitCode: 127
|
|
223
|
+
});
|
|
224
|
+
expect(response.error.errors[0].message).toBeFalsy();
|
|
225
|
+
expect(response.error.errors[0].tool).toBe('webpack');
|
|
226
|
+
});
|
|
227
|
+
test('should format tool name in error code', () => {
|
|
228
|
+
const response = summary.toolError({
|
|
229
|
+
toolName: 'eslint',
|
|
230
|
+
error: 'Linting failed'
|
|
231
|
+
});
|
|
232
|
+
expect(response.error.code).toBe('ESLINT_EXECUTION_ERROR');
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
237
|
+
// ENVIRONMENT ERROR TESTS
|
|
238
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
describe('environmentError()', () => {
|
|
241
|
+
test('should create environment error response', () => {
|
|
242
|
+
const response = summary.environmentError({
|
|
243
|
+
issue: 'Node.js version too old',
|
|
244
|
+
requirement: 'Node.js >= 16',
|
|
245
|
+
suggestion: 'Update Node.js'
|
|
246
|
+
});
|
|
247
|
+
expect(response.status).toBe('FAILURE');
|
|
248
|
+
expect(response.error.code).toBe('ENV_SETUP_ERROR');
|
|
249
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.ENVIRONMENT);
|
|
250
|
+
expect(response.error.errors[0].issue).toBe('Node.js version too old');
|
|
251
|
+
expect(response.error.errors[0].requirement).toBe('Node.js >= 16');
|
|
252
|
+
expect(response.error.errors[0].suggestion).toBe('Update Node.js');
|
|
253
|
+
});
|
|
254
|
+
test('should handle partial environment error details', () => {
|
|
255
|
+
const response = summary.environmentError({
|
|
256
|
+
issue: 'Missing DATABASE_URL'
|
|
257
|
+
});
|
|
258
|
+
expect(response.error.errors[0]).toEqual({
|
|
259
|
+
issue: 'Missing DATABASE_URL'
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
test('should use default issue message', () => {
|
|
263
|
+
const response = summary.environmentError();
|
|
264
|
+
expect(response.error.errors[0].issue).toBe('Environment configuration issue');
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
269
|
+
// FLAKY TEST TESTS
|
|
270
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
describe('flakyTestFailure()', () => {
|
|
273
|
+
test('should create flaky test failure response', () => {
|
|
274
|
+
const response = summary.flakyTestFailure({
|
|
275
|
+
testNames: ['Test A', 'Test B'],
|
|
276
|
+
attemptNumber: 1,
|
|
277
|
+
maxAttempts: 3
|
|
278
|
+
});
|
|
279
|
+
expect(response.status).toBe('FAILURE');
|
|
280
|
+
expect(response.error.code).toBe('FLAKY_TEST_FAILURE');
|
|
281
|
+
expect(response.error.category).toBe(_pipelineSummary.ERROR_CATEGORIES.FLAKY);
|
|
282
|
+
expect(response.error.errors).toHaveLength(2);
|
|
283
|
+
expect(response.result[0].attemptNumber).toBe(1);
|
|
284
|
+
expect(response.result[0].maxAttempts).toBe(3);
|
|
285
|
+
});
|
|
286
|
+
test('should include attempt information in message', () => {
|
|
287
|
+
const response = summary.flakyTestFailure({
|
|
288
|
+
testNames: ['Flaky Test'],
|
|
289
|
+
attemptNumber: 2,
|
|
290
|
+
maxAttempts: 3
|
|
291
|
+
});
|
|
292
|
+
expect(response.message).toContain('attempt 2/3');
|
|
293
|
+
});
|
|
294
|
+
test('should handle custom message override', () => {
|
|
295
|
+
const response = summary.flakyTestFailure({
|
|
296
|
+
testNames: ['Test'],
|
|
297
|
+
message: 'Custom flaky message'
|
|
298
|
+
});
|
|
299
|
+
expect(response.message).toBe('Custom flaky message');
|
|
300
|
+
});
|
|
301
|
+
test('should use default attempt values', () => {
|
|
302
|
+
const response = summary.flakyTestFailure({
|
|
303
|
+
testNames: ['Test']
|
|
304
|
+
});
|
|
305
|
+
expect(response.result[0].attemptNumber).toBe(1);
|
|
306
|
+
expect(response.result[0].maxAttempts).toBe(3);
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
311
|
+
// FILE SAVE TESTS
|
|
312
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
313
|
+
|
|
314
|
+
describe('saveToFile()', () => {
|
|
315
|
+
test('should save response to file', () => {
|
|
316
|
+
const response = summary.success();
|
|
317
|
+
const saved = summary.saveToFile(response);
|
|
318
|
+
expect(saved).toBe(true);
|
|
319
|
+
expect(_fs.default.existsSync(summary.outputPath)).toBe(true);
|
|
320
|
+
const content = _fs.default.readFileSync(summary.outputPath, 'utf-8');
|
|
321
|
+
const parsed = JSON.parse(content);
|
|
322
|
+
expect(parsed.status).toBe('SUCCESS');
|
|
323
|
+
});
|
|
324
|
+
test('should create parent directories if not exist', () => {
|
|
325
|
+
const customPath = _path.default.join(TEST_OUTPUT_DIR, 'nested/deep/summary.json');
|
|
326
|
+
const customSummary = (0, _pipelineSummary.createPipelineSummary)('Test', {
|
|
327
|
+
outputPath: customPath
|
|
328
|
+
});
|
|
329
|
+
const response = customSummary.success();
|
|
330
|
+
const saved = customSummary.saveToFile(response);
|
|
331
|
+
expect(saved).toBe(true);
|
|
332
|
+
expect(_fs.default.existsSync(customPath)).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
test('should handle missing output path', () => {
|
|
335
|
+
const noPathSummary = (0, _pipelineSummary.createPipelineSummary)('Test');
|
|
336
|
+
const response = noPathSummary.success();
|
|
337
|
+
const saved = noPathSummary.saveToFile(response);
|
|
338
|
+
expect(saved).toBe(false);
|
|
339
|
+
});
|
|
340
|
+
test('should format JSON with indentation', () => {
|
|
341
|
+
const response = summary.success();
|
|
342
|
+
summary.saveToFile(response);
|
|
343
|
+
const content = _fs.default.readFileSync(summary.outputPath, 'utf-8');
|
|
344
|
+
expect(content).toContain('\n');
|
|
345
|
+
expect(content).toContain(' ');
|
|
346
|
+
});
|
|
347
|
+
test('should override output path', () => {
|
|
348
|
+
const customPath = _path.default.join(TEST_OUTPUT_DIR, 'custom.json');
|
|
349
|
+
const response = summary.success();
|
|
350
|
+
summary.saveToFile(response, customPath);
|
|
351
|
+
expect(_fs.default.existsSync(customPath)).toBe(true);
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
356
|
+
// SAVE AND RETURN TESTS
|
|
357
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
describe('saveAndReturn()', () => {
|
|
360
|
+
test('should return response and save status', () => {
|
|
361
|
+
const response = summary.success();
|
|
362
|
+
const {
|
|
363
|
+
response: retResponse,
|
|
364
|
+
saved
|
|
365
|
+
} = summary.saveAndReturn(response);
|
|
366
|
+
expect(retResponse).toEqual(response);
|
|
367
|
+
expect(saved).toBe(true);
|
|
368
|
+
expect(_fs.default.existsSync(summary.outputPath)).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
test('should indicate save failure', () => {
|
|
371
|
+
const noPathSummary = (0, _pipelineSummary.createPipelineSummary)('Test');
|
|
372
|
+
const response = noPathSummary.success();
|
|
373
|
+
const {
|
|
374
|
+
saved
|
|
375
|
+
} = noPathSummary.saveAndReturn(response);
|
|
376
|
+
expect(saved).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
381
|
+
// EDGE CASES
|
|
382
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
describe('edge cases', () => {
|
|
385
|
+
test('should handle very long error messages', () => {
|
|
386
|
+
const longMessage = 'x'.repeat(10000);
|
|
387
|
+
const response = summary.failure({
|
|
388
|
+
errorMessage: longMessage
|
|
389
|
+
});
|
|
390
|
+
expect(response.error.message).toBe(longMessage);
|
|
391
|
+
});
|
|
392
|
+
test('should handle special characters in messages', () => {
|
|
393
|
+
const response = summary.failure({
|
|
394
|
+
message: 'Failed: "test" & <script>alert(1)</script>'
|
|
395
|
+
});
|
|
396
|
+
expect(response.message).toContain('<script>');
|
|
397
|
+
});
|
|
398
|
+
test('should handle null/undefined in optional fields', () => {
|
|
399
|
+
const response = summary.failure({
|
|
400
|
+
errorMessage: null,
|
|
401
|
+
errors: null
|
|
402
|
+
});
|
|
403
|
+
expect(response.error.message).toBeNull();
|
|
404
|
+
expect(Array.isArray(response.error.errors)).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
test('should handle errors with circular references', () => {
|
|
407
|
+
const circularObj = {
|
|
408
|
+
name: 'test'
|
|
409
|
+
};
|
|
410
|
+
circularObj.self = circularObj;
|
|
411
|
+
|
|
412
|
+
// Should not throw
|
|
413
|
+
const response = summary.failure({
|
|
414
|
+
errors: [circularObj]
|
|
415
|
+
});
|
|
416
|
+
expect(response.error.errors).toBeDefined();
|
|
417
|
+
});
|
|
418
|
+
test('should handle unicode characters', () => {
|
|
419
|
+
const response = summary.failure({
|
|
420
|
+
errorMessage: '测试失败 🔥 エラーが発生しました'
|
|
421
|
+
});
|
|
422
|
+
expect(response.error.message).toContain('测试失败');
|
|
423
|
+
expect(response.error.message).toContain('🔥');
|
|
424
|
+
});
|
|
425
|
+
test('should handle multiple consecutive saves', () => {
|
|
426
|
+
const response1 = summary.success({
|
|
427
|
+
message: 'First'
|
|
428
|
+
});
|
|
429
|
+
const response2 = summary.failure({
|
|
430
|
+
message: 'Second'
|
|
431
|
+
});
|
|
432
|
+
summary.saveToFile(response1);
|
|
433
|
+
summary.saveToFile(response2);
|
|
434
|
+
const content = _fs.default.readFileSync(summary.outputPath, 'utf-8');
|
|
435
|
+
const parsed = JSON.parse(content);
|
|
436
|
+
expect(parsed.message).toBe('Second');
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
441
|
+
// ERROR CATEGORIES EXPORT
|
|
442
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
describe('ERROR_CATEGORIES', () => {
|
|
445
|
+
test('should export valid error categories', () => {
|
|
446
|
+
expect(_pipelineSummary.ERROR_CATEGORIES.CODE).toBe('code');
|
|
447
|
+
expect(_pipelineSummary.ERROR_CATEGORIES.ENVIRONMENT).toBe('environment');
|
|
448
|
+
expect(_pipelineSummary.ERROR_CATEGORIES.FLAKY).toBe('flaky');
|
|
449
|
+
expect(_pipelineSummary.ERROR_CATEGORIES.TOOL).toBe('tool');
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
454
|
+
// FACTORY FUNCTION TESTS
|
|
455
|
+
// ──────────────────────────────────────────────────────────────────────
|
|
456
|
+
|
|
457
|
+
describe('createPipelineSummary()', () => {
|
|
458
|
+
test('should create instance with stage name', () => {
|
|
459
|
+
const instance = (0, _pipelineSummary.createPipelineSummary)('MyStage');
|
|
460
|
+
expect(instance).toBeInstanceOf(_pipelineSummary.PipelineSummaryGenerator);
|
|
461
|
+
const response = instance.success();
|
|
462
|
+
expect(response.message).toContain('MyStage');
|
|
463
|
+
});
|
|
464
|
+
test('should merge options correctly', () => {
|
|
465
|
+
const instance = (0, _pipelineSummary.createPipelineSummary)('Build', {
|
|
466
|
+
outputPath: '/path/to/file.json',
|
|
467
|
+
webUrl: 'https://example.com'
|
|
468
|
+
});
|
|
469
|
+
const response = instance.success();
|
|
470
|
+
expect(response.web_url).toBe('https://example.com');
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
});
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.PipelineSummaryGenerator = exports.ERROR_CATEGORIES = void 0;
|
|
7
|
+
exports.createPipelineSummary = createPipelineSummary;
|
|
8
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
9
|
+
var _path = _interopRequireDefault(require("path"));
|
|
10
|
+
var _logger = require("./logger.js");
|
|
11
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
12
|
+
/**
|
|
13
|
+
* Error Categories
|
|
14
|
+
* - 'code': Issues in test/application code
|
|
15
|
+
* - 'environment': Environment setup, dependencies, configuration issues
|
|
16
|
+
* - 'flaky': Intermittent test failures
|
|
17
|
+
* - 'tool': Tool execution failures (jest, npm, etc.)
|
|
18
|
+
*/
|
|
19
|
+
const ERROR_CATEGORIES = exports.ERROR_CATEGORIES = {
|
|
20
|
+
CODE: 'code',
|
|
21
|
+
ENVIRONMENT: 'environment',
|
|
22
|
+
FLAKY: 'flaky',
|
|
23
|
+
TOOL: 'tool'
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Pipeline Summary Generator
|
|
28
|
+
* Creates standardized JSON responses for pipeline stages
|
|
29
|
+
*/
|
|
30
|
+
class PipelineSummaryGenerator {
|
|
31
|
+
constructor(options = {}) {
|
|
32
|
+
this.stageName = options.stageName || 'unknown';
|
|
33
|
+
this.outputPath = options.outputPath || null;
|
|
34
|
+
this.webUrl = options.webUrl || null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create a SUCCESS response
|
|
39
|
+
* @param {Object} params - Response parameters
|
|
40
|
+
* @param {string} params.message - Custom message
|
|
41
|
+
* @param {string} params.webUrl - Optional URL override
|
|
42
|
+
* @returns {Object} Success response object
|
|
43
|
+
*/
|
|
44
|
+
success(params = {}) {
|
|
45
|
+
const response = {
|
|
46
|
+
status: 'SUCCESS',
|
|
47
|
+
message: params.message || `${this.stageName} completed successfully`,
|
|
48
|
+
web_url: params.webUrl || this.webUrl || ''
|
|
49
|
+
};
|
|
50
|
+
this._logResponse(response);
|
|
51
|
+
return response;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a FAILURE response with error details
|
|
56
|
+
* @param {Object} params - Failure parameters
|
|
57
|
+
* @param {string} params.message - Custom message for the stage
|
|
58
|
+
* @param {string} params.errorCode - Custom error code
|
|
59
|
+
* @param {string} params.errorCategory - One of: 'code', 'environment', 'flaky', 'tool'
|
|
60
|
+
* @param {string} params.errorMessage - Detailed error message
|
|
61
|
+
* @param {Array} params.errors - List of detailed errors
|
|
62
|
+
* @param {Array} params.result - Optional result data
|
|
63
|
+
* @returns {Object} Failure response object
|
|
64
|
+
*/
|
|
65
|
+
failure(params = {}) {
|
|
66
|
+
const {
|
|
67
|
+
message = `${this.stageName} failed`,
|
|
68
|
+
errorCode = 'UNKNOWN_ERROR',
|
|
69
|
+
errorCategory = ERROR_CATEGORIES.TOOL,
|
|
70
|
+
errorMessage = 'An unexpected error occurred',
|
|
71
|
+
errors = [],
|
|
72
|
+
result = []
|
|
73
|
+
} = params;
|
|
74
|
+
if (!this._isValidErrorCategory(errorCategory)) {
|
|
75
|
+
_logger.Logger.warn(`Invalid error category: ${errorCategory}. Using 'tool' as default.`);
|
|
76
|
+
}
|
|
77
|
+
const response = {
|
|
78
|
+
status: 'FAILURE',
|
|
79
|
+
message,
|
|
80
|
+
web_url: this.webUrl || '',
|
|
81
|
+
error: {
|
|
82
|
+
code: errorCode,
|
|
83
|
+
category: errorCategory,
|
|
84
|
+
message: errorMessage,
|
|
85
|
+
errors: Array.isArray(errors) ? errors : [errors]
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
if (Array.isArray(result) && result.length > 0) {
|
|
89
|
+
response.result = result;
|
|
90
|
+
}
|
|
91
|
+
this._logResponse(response);
|
|
92
|
+
return response;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Handle test failures
|
|
97
|
+
* @param {Object} params - Test failure parameters
|
|
98
|
+
* @param {Object} params.testResults - Jest test results object
|
|
99
|
+
* @param {string} params.message - Custom message
|
|
100
|
+
* @returns {Object} Failure response object
|
|
101
|
+
*/
|
|
102
|
+
testFailure(params = {}) {
|
|
103
|
+
const {
|
|
104
|
+
testResults,
|
|
105
|
+
message
|
|
106
|
+
} = params;
|
|
107
|
+
if (!testResults) {
|
|
108
|
+
return this.failure({
|
|
109
|
+
errorCode: 'TEST_FAILURE',
|
|
110
|
+
errorCategory: ERROR_CATEGORIES.CODE,
|
|
111
|
+
errorMessage: message || 'Tests failed'
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
const errors = [];
|
|
115
|
+
const failedTests = testResults.testResults || [];
|
|
116
|
+
for (const testFile of failedTests) {
|
|
117
|
+
if (testFile.numFailingTests > 0) {
|
|
118
|
+
for (const assertion of testFile.assertionResults || []) {
|
|
119
|
+
if (assertion.status === 'failed') {
|
|
120
|
+
errors.push({
|
|
121
|
+
test: assertion.fullName || assertion.title,
|
|
122
|
+
file: testFile.name,
|
|
123
|
+
message: assertion.failureMessages?.[0] || 'Test assertion failed'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return this.failure({
|
|
130
|
+
message: message || `${testResults.numFailedTests} test(s) failed out of ${testResults.numTotalTests}`,
|
|
131
|
+
errorCode: 'TEST_FAILURE',
|
|
132
|
+
errorCategory: ERROR_CATEGORIES.CODE,
|
|
133
|
+
errorMessage: `${testResults.numFailedTests} test assertion(s) failed`,
|
|
134
|
+
errors: errors.length > 0 ? errors : ['Test failures detected'],
|
|
135
|
+
result: [{
|
|
136
|
+
totalTests: testResults.numTotalTests,
|
|
137
|
+
failedTests: testResults.numFailedTests,
|
|
138
|
+
passedTests: testResults.numPassedTests,
|
|
139
|
+
skippedTests: testResults.numSkippedTests
|
|
140
|
+
}]
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Handle tool execution errors
|
|
146
|
+
* @param {Object} params - Tool error parameters
|
|
147
|
+
* @param {string} params.toolName - Name of the tool (jest, npm, etc.)
|
|
148
|
+
* @param {Error|string} params.error - Error object or message
|
|
149
|
+
* @param {string} params.exitCode - Exit code from tool
|
|
150
|
+
* @returns {Object} Failure response object
|
|
151
|
+
*/
|
|
152
|
+
toolError(params = {}) {
|
|
153
|
+
const {
|
|
154
|
+
toolName = 'unknown',
|
|
155
|
+
error,
|
|
156
|
+
exitCode
|
|
157
|
+
} = params;
|
|
158
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
159
|
+
const errorStack = error instanceof Error ? error.stack : null;
|
|
160
|
+
const errors = [{
|
|
161
|
+
tool: toolName,
|
|
162
|
+
message: errorMessage,
|
|
163
|
+
exitCode: exitCode || 'unknown'
|
|
164
|
+
}];
|
|
165
|
+
if (errorStack) {
|
|
166
|
+
errors[0].stack = errorStack;
|
|
167
|
+
}
|
|
168
|
+
return this.failure({
|
|
169
|
+
message: `${toolName} execution failed`,
|
|
170
|
+
errorCode: `${toolName.toUpperCase()}_EXECUTION_ERROR`,
|
|
171
|
+
errorCategory: ERROR_CATEGORIES.TOOL,
|
|
172
|
+
errorMessage: `Failed to execute ${toolName}: ${errorMessage}`,
|
|
173
|
+
errors
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Handle environment errors
|
|
179
|
+
* @param {Object} params - Environment error parameters
|
|
180
|
+
* @param {string} params.issue - Description of the environment issue
|
|
181
|
+
* @param {string} params.requirement - What was required
|
|
182
|
+
* @param {string} params.suggestion - Suggested fix
|
|
183
|
+
* @returns {Object} Failure response object
|
|
184
|
+
*/
|
|
185
|
+
environmentError(params = {}) {
|
|
186
|
+
const {
|
|
187
|
+
issue = 'Environment configuration issue',
|
|
188
|
+
requirement = null,
|
|
189
|
+
suggestion = null
|
|
190
|
+
} = params;
|
|
191
|
+
const errors = [{
|
|
192
|
+
issue,
|
|
193
|
+
...(requirement && {
|
|
194
|
+
requirement
|
|
195
|
+
}),
|
|
196
|
+
...(suggestion && {
|
|
197
|
+
suggestion
|
|
198
|
+
})
|
|
199
|
+
}];
|
|
200
|
+
return this.failure({
|
|
201
|
+
message: 'Environment setup failed',
|
|
202
|
+
errorCode: 'ENV_SETUP_ERROR',
|
|
203
|
+
errorCategory: ERROR_CATEGORIES.ENVIRONMENT,
|
|
204
|
+
errorMessage: issue,
|
|
205
|
+
errors
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Handle flaky test failures
|
|
211
|
+
* @param {Object} params - Flaky test parameters
|
|
212
|
+
* @param {Array} params.testNames - Names of flaky tests
|
|
213
|
+
* @param {number} params.attemptNumber - Current attempt number
|
|
214
|
+
* @param {number} params.maxAttempts - Max retry attempts
|
|
215
|
+
* @returns {Object} Failure response object
|
|
216
|
+
*/
|
|
217
|
+
flakyTestFailure(params = {}) {
|
|
218
|
+
const {
|
|
219
|
+
testNames = [],
|
|
220
|
+
attemptNumber = 1,
|
|
221
|
+
maxAttempts = 3,
|
|
222
|
+
message
|
|
223
|
+
} = params;
|
|
224
|
+
return this.failure({
|
|
225
|
+
message: message || `Flaky test failure (attempt ${attemptNumber}/${maxAttempts})`,
|
|
226
|
+
errorCode: 'FLAKY_TEST_FAILURE',
|
|
227
|
+
errorCategory: ERROR_CATEGORIES.FLAKY,
|
|
228
|
+
errorMessage: `${testNames.length} test(s) failed intermittently`,
|
|
229
|
+
errors: testNames.map(name => ({
|
|
230
|
+
test: name,
|
|
231
|
+
type: 'flaky',
|
|
232
|
+
attemptNumber
|
|
233
|
+
})),
|
|
234
|
+
result: [{
|
|
235
|
+
attemptNumber,
|
|
236
|
+
maxAttempts,
|
|
237
|
+
flakyTestCount: testNames.length
|
|
238
|
+
}]
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Save response to JSON file
|
|
244
|
+
* @param {Object} response - Response object to save
|
|
245
|
+
* @param {string} outputPath - Optional path override
|
|
246
|
+
* @returns {boolean} Success status
|
|
247
|
+
*/
|
|
248
|
+
saveToFile(response, outputPath = null) {
|
|
249
|
+
const filePath = outputPath || this.outputPath;
|
|
250
|
+
if (!filePath) {
|
|
251
|
+
_logger.Logger.warn('No output path specified for pipeline summary');
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const dir = _path.default.dirname(filePath);
|
|
256
|
+
|
|
257
|
+
// ── Create directory if it doesn't exist ──────────────────────
|
|
258
|
+
if (!_fs.default.existsSync(dir)) {
|
|
259
|
+
try {
|
|
260
|
+
_fs.default.mkdirSync(dir, {
|
|
261
|
+
recursive: true
|
|
262
|
+
});
|
|
263
|
+
_logger.Logger.info(`Created directory: ${dir}`);
|
|
264
|
+
} catch (mkdirErr) {
|
|
265
|
+
if (mkdirErr.code === 'EACCES') {
|
|
266
|
+
_logger.Logger.error(`Permission denied creating directory: ${dir}. ` + `Check folder permissions.`);
|
|
267
|
+
} else if (mkdirErr.code === 'EROFS') {
|
|
268
|
+
_logger.Logger.error(`Read-only file system: ${dir}. ` + `Cannot create directory on read-only filesystem.`);
|
|
269
|
+
} else {
|
|
270
|
+
_logger.Logger.error(`Failed to create directory ${dir}: ${mkdirErr.message}`);
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── Verify directory is writable ─────────────────────────────
|
|
277
|
+
try {
|
|
278
|
+
_fs.default.accessSync(dir, _fs.default.constants.W_OK);
|
|
279
|
+
} catch (accessErr) {
|
|
280
|
+
_logger.Logger.error(`Directory not writable: ${dir}. ` + `Check folder permissions.`);
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── Write the summary file ───────────────────────────────────
|
|
285
|
+
const jsonContent = JSON.stringify(response, null, 2);
|
|
286
|
+
_fs.default.writeFileSync(filePath, jsonContent, 'utf-8');
|
|
287
|
+
|
|
288
|
+
// ── Verify file was written ──────────────────────────────────
|
|
289
|
+
if (!_fs.default.existsSync(filePath)) {
|
|
290
|
+
_logger.Logger.error(`File was not created: ${filePath}. ` + `Unknown file system error.`);
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
_logger.Logger.info(`✓ Pipeline summary saved to: ${filePath}`);
|
|
294
|
+
return true;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
// ── Handle unexpected errors ─────────────────────────────────
|
|
297
|
+
if (err.code === 'ENOSPC') {
|
|
298
|
+
_logger.Logger.error(`No space left on device. ` + `Cannot save pipeline summary to: ${outputPath || this.outputPath}`);
|
|
299
|
+
} else if (err.code === 'ENOTDIR') {
|
|
300
|
+
_logger.Logger.error(`Invalid path: ${outputPath || this.outputPath}. ` + `A parent component is not a directory.`);
|
|
301
|
+
} else {
|
|
302
|
+
_logger.Logger.error(`Failed to save pipeline summary: ${err.message}. ` + `Path: ${outputPath || this.outputPath}`);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Save response and return both
|
|
310
|
+
* @param {Object} response - Response object
|
|
311
|
+
* @param {string} outputPath - Optional path override
|
|
312
|
+
* @returns {Object} { response, saved: boolean }
|
|
313
|
+
*/
|
|
314
|
+
saveAndReturn(response, outputPath = null) {
|
|
315
|
+
const saved = this.saveToFile(response, outputPath);
|
|
316
|
+
return {
|
|
317
|
+
response,
|
|
318
|
+
saved
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Validate error category
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
_isValidErrorCategory(category) {
|
|
327
|
+
return Object.values(ERROR_CATEGORIES).includes(category);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Log response for debugging
|
|
332
|
+
* @private
|
|
333
|
+
*/
|
|
334
|
+
_logResponse(response) {
|
|
335
|
+
if (response.status === 'SUCCESS') {
|
|
336
|
+
_logger.Logger.info(`✓ Stage '${this.stageName}' completed: ${response.message}`);
|
|
337
|
+
} else {
|
|
338
|
+
_logger.Logger.error(`✗ Stage '${this.stageName}' failed: ${response.error.category} - ${response.error.code}`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Factory function for quick usage
|
|
345
|
+
* @param {string} stageName - Name of the pipeline stage
|
|
346
|
+
* @param {Object} options - Additional options
|
|
347
|
+
* @returns {PipelineSummaryGenerator}
|
|
348
|
+
*/
|
|
349
|
+
exports.PipelineSummaryGenerator = PipelineSummaryGenerator;
|
|
350
|
+
function createPipelineSummary(stageName, options = {}) {
|
|
351
|
+
return new PipelineSummaryGenerator({
|
|
352
|
+
stageName,
|
|
353
|
+
...options
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Export error categories for external use
|
|
359
|
+
*/
|