claude-autopm 1.18.0 → 1.20.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/README.md +159 -0
- package/autopm/.claude/agents/core/mcp-manager.md +1 -1
- package/autopm/.claude/commands/pm/context.md +11 -0
- package/autopm/.claude/commands/pm/epic-decompose.md +25 -2
- package/autopm/.claude/commands/pm/epic-oneshot.md +13 -0
- package/autopm/.claude/commands/pm/epic-start.md +19 -0
- package/autopm/.claude/commands/pm/epic-sync-modular.md +10 -10
- package/autopm/.claude/commands/pm/epic-sync.md +14 -14
- package/autopm/.claude/commands/pm/issue-start.md +50 -5
- package/autopm/.claude/commands/pm/issue-sync.md +15 -15
- package/autopm/.claude/commands/pm/what-next.md +11 -0
- package/autopm/.claude/mcp/MCP-REGISTRY.md +1 -1
- package/autopm/.claude/scripts/azure/active-work.js +2 -2
- package/autopm/.claude/scripts/azure/blocked.js +13 -13
- package/autopm/.claude/scripts/azure/daily.js +1 -1
- package/autopm/.claude/scripts/azure/dashboard.js +1 -1
- package/autopm/.claude/scripts/azure/feature-list.js +2 -2
- package/autopm/.claude/scripts/azure/feature-status.js +1 -1
- package/autopm/.claude/scripts/azure/next-task.js +1 -1
- package/autopm/.claude/scripts/azure/search.js +1 -1
- package/autopm/.claude/scripts/azure/setup.js +15 -15
- package/autopm/.claude/scripts/azure/sprint-report.js +2 -2
- package/autopm/.claude/scripts/azure/sync.js +1 -1
- package/autopm/.claude/scripts/azure/us-list.js +1 -1
- package/autopm/.claude/scripts/azure/us-status.js +1 -1
- package/autopm/.claude/scripts/azure/validate.js +13 -13
- package/autopm/.claude/scripts/lib/frontmatter-utils.sh +42 -7
- package/autopm/.claude/scripts/lib/logging-utils.sh +20 -16
- package/autopm/.claude/scripts/lib/validation-utils.sh +1 -1
- package/autopm/.claude/scripts/pm/context.js +338 -0
- package/autopm/.claude/scripts/pm/issue-sync/format-comment.sh +3 -3
- package/autopm/.claude/scripts/pm/lib/README.md +85 -0
- package/autopm/.claude/scripts/pm/lib/logger.js +78 -0
- package/autopm/.claude/scripts/pm/next.js +25 -1
- package/autopm/.claude/scripts/pm/what-next.js +660 -0
- package/bin/autopm.js +25 -0
- package/package.json +1 -1
- package/lib/agentExecutor.js.deprecated +0 -101
- package/lib/azure/cache.js +0 -80
- package/lib/azure/client.js +0 -77
- package/lib/azure/formatter.js +0 -177
- package/lib/commandHelpers.js +0 -177
- package/lib/context/manager.js +0 -290
- package/lib/documentation/manager.js +0 -528
- package/lib/github/workflow-manager.js +0 -546
- package/lib/helpers/azure-batch-api.js +0 -133
- package/lib/helpers/azure-cache-manager.js +0 -287
- package/lib/helpers/azure-parallel-processor.js +0 -158
- package/lib/helpers/azure-work-item-create.js +0 -278
- package/lib/helpers/gh-issue-create.js +0 -250
- package/lib/helpers/interactive-prompt.js +0 -336
- package/lib/helpers/output-manager.js +0 -335
- package/lib/helpers/progress-indicator.js +0 -258
- package/lib/performance/benchmarker.js +0 -429
- package/lib/pm/epic-decomposer.js +0 -273
- package/lib/pm/epic-syncer.js +0 -221
- package/lib/prdMetadata.js +0 -270
- package/lib/providers/azure/index.js +0 -234
- package/lib/providers/factory.js +0 -87
- package/lib/providers/github/index.js +0 -204
- package/lib/providers/interface.js +0 -73
- package/lib/python/scaffold-manager.js +0 -576
- package/lib/react/scaffold-manager.js +0 -745
- package/lib/regression/analyzer.js +0 -578
- package/lib/release/manager.js +0 -324
- package/lib/tailwind/manager.js +0 -486
- package/lib/traefik/manager.js +0 -484
- package/lib/utils/colors.js +0 -126
- package/lib/utils/config.js +0 -317
- package/lib/utils/filesystem.js +0 -316
- package/lib/utils/logger.js +0 -135
- package/lib/utils/prompts.js +0 -294
- package/lib/utils/shell.js +0 -237
- package/lib/validators/email-validator.js +0 -337
- package/lib/workflow/manager.js +0 -449
|
@@ -1,578 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Regression Test Analyzer
|
|
3
|
-
* Centralized regression testing and analysis functionality
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const fs = require('fs').promises;
|
|
7
|
-
const path = require('path');
|
|
8
|
-
const { exec } = require('child_process');
|
|
9
|
-
const { promisify } = require('util');
|
|
10
|
-
const execAsync = promisify(exec);
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Configuration
|
|
14
|
-
*/
|
|
15
|
-
const CONFIG = {
|
|
16
|
-
directories: {
|
|
17
|
-
regression: '.claude/regression'
|
|
18
|
-
},
|
|
19
|
-
defaults: {
|
|
20
|
-
coverageThreshold: 80,
|
|
21
|
-
slowTestThreshold: 1000, // ms
|
|
22
|
-
maxHistorySize: 100
|
|
23
|
-
},
|
|
24
|
-
filePatterns: {
|
|
25
|
-
baseline: 'baseline.json',
|
|
26
|
-
history: 'history.json',
|
|
27
|
-
report: 'report-{timestamp}.json'
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
class RegressionAnalyzer {
|
|
32
|
-
constructor(projectRoot = process.cwd()) {
|
|
33
|
-
this.projectRoot = projectRoot;
|
|
34
|
-
this.regressionDir = path.join(projectRoot, CONFIG.directories.regression);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Runs the test suite
|
|
39
|
-
*/
|
|
40
|
-
async runTests() {
|
|
41
|
-
const packagePath = path.join(this.projectRoot, 'package.json');
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
// Check if package.json exists
|
|
45
|
-
const packageJson = JSON.parse(await fs.readFile(packagePath, 'utf8'));
|
|
46
|
-
|
|
47
|
-
// Try to run npm test
|
|
48
|
-
if (packageJson.scripts && packageJson.scripts.test) {
|
|
49
|
-
const startTime = Date.now();
|
|
50
|
-
|
|
51
|
-
try {
|
|
52
|
-
const { stdout, stderr } = await execAsync('npm test', {
|
|
53
|
-
cwd: this.projectRoot,
|
|
54
|
-
env: { ...process.env, CI: 'true' }
|
|
55
|
-
});
|
|
56
|
-
const duration = Date.now() - startTime;
|
|
57
|
-
|
|
58
|
-
return this.parseTestResults(stdout, duration, true);
|
|
59
|
-
} catch (error) {
|
|
60
|
-
const duration = Date.now() - startTime;
|
|
61
|
-
return this.parseTestResults(error.stdout || error.message, duration, false);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
// No package.json or no test script
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Fallback: look for test files
|
|
69
|
-
const testFiles = await this.findTestFiles();
|
|
70
|
-
const results = {
|
|
71
|
-
success: true,
|
|
72
|
-
tests: { total: testFiles.length, passed: 0, failed: 0 },
|
|
73
|
-
timestamp: new Date().toISOString(),
|
|
74
|
-
duration: 0
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const startTime = Date.now();
|
|
78
|
-
for (const testFile of testFiles) {
|
|
79
|
-
try {
|
|
80
|
-
await execAsync(`node ${testFile}`, { cwd: this.projectRoot });
|
|
81
|
-
results.tests.passed++;
|
|
82
|
-
} catch (error) {
|
|
83
|
-
results.tests.failed++;
|
|
84
|
-
results.success = false;
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
results.duration = Date.now() - startTime;
|
|
88
|
-
|
|
89
|
-
return results;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Parses test results from output
|
|
94
|
-
*/
|
|
95
|
-
parseTestResults(output, duration, success) {
|
|
96
|
-
const results = {
|
|
97
|
-
success,
|
|
98
|
-
duration,
|
|
99
|
-
output: output.substring(0, 1000), // Limit output size
|
|
100
|
-
timestamp: new Date().toISOString()
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
// Try to parse different test output formats
|
|
104
|
-
const patterns = [
|
|
105
|
-
/(\d+)\s+(pass|passed|passing)/i,
|
|
106
|
-
/(\d+)\s+(fail|failed|failing)/i,
|
|
107
|
-
/(\d+)\s+tests?/i,
|
|
108
|
-
/✓\s+(\d+)/,
|
|
109
|
-
/✗\s+(\d+)/
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
const passMatch = output.match(patterns[0]);
|
|
113
|
-
const failMatch = output.match(patterns[1]);
|
|
114
|
-
const totalMatch = output.match(patterns[2]);
|
|
115
|
-
|
|
116
|
-
if (passMatch || failMatch || totalMatch) {
|
|
117
|
-
results.tests = {
|
|
118
|
-
total: parseInt(totalMatch?.[1] || 0) ||
|
|
119
|
-
(parseInt(passMatch?.[1] || 0) + parseInt(failMatch?.[1] || 0)),
|
|
120
|
-
passed: parseInt(passMatch?.[1] || 0),
|
|
121
|
-
failed: parseInt(failMatch?.[1] || 0)
|
|
122
|
-
};
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return results;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Finds test files in the project
|
|
130
|
-
*/
|
|
131
|
-
async findTestFiles() {
|
|
132
|
-
const testFiles = [];
|
|
133
|
-
const testDirs = ['test', 'tests', '__tests__', 'spec'];
|
|
134
|
-
|
|
135
|
-
for (const dir of testDirs) {
|
|
136
|
-
const testDir = path.join(this.projectRoot, dir);
|
|
137
|
-
|
|
138
|
-
try {
|
|
139
|
-
await this.findTestFilesInDir(testDir, testFiles);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
// Directory doesn't exist
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return testFiles;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Recursively finds test files in a directory
|
|
150
|
-
*/
|
|
151
|
-
async findTestFilesInDir(dir, testFiles) {
|
|
152
|
-
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
153
|
-
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
const fullPath = path.join(dir, entry.name);
|
|
156
|
-
|
|
157
|
-
if (entry.isDirectory() && !entry.name.startsWith('.')) {
|
|
158
|
-
await this.findTestFilesInDir(fullPath, testFiles);
|
|
159
|
-
} else if (entry.isFile()) {
|
|
160
|
-
if (entry.name.match(/\.(test|spec)\.(js|ts|jsx|tsx)$/)) {
|
|
161
|
-
testFiles.push(fullPath);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* Gets coverage information
|
|
169
|
-
*/
|
|
170
|
-
async getCoverage() {
|
|
171
|
-
// Try to get coverage from common locations
|
|
172
|
-
const coveragePaths = [
|
|
173
|
-
path.join(this.projectRoot, 'coverage', 'coverage-summary.json'),
|
|
174
|
-
path.join(this.projectRoot, '.nyc_output', 'coverage-summary.json'),
|
|
175
|
-
path.join(this.projectRoot, 'coverage', 'coverage-final.json'),
|
|
176
|
-
path.join(this.projectRoot, 'coverage.json')
|
|
177
|
-
];
|
|
178
|
-
|
|
179
|
-
for (const coveragePath of coveragePaths) {
|
|
180
|
-
try {
|
|
181
|
-
const content = await fs.readFile(coveragePath, 'utf8');
|
|
182
|
-
const coverage = JSON.parse(content);
|
|
183
|
-
|
|
184
|
-
// Extract coverage metrics
|
|
185
|
-
if (coverage.total) {
|
|
186
|
-
return {
|
|
187
|
-
lines: Math.round(coverage.total.lines?.pct || 0),
|
|
188
|
-
branches: Math.round(coverage.total.branches?.pct || 0),
|
|
189
|
-
functions: Math.round(coverage.total.functions?.pct || 0),
|
|
190
|
-
statements: Math.round(coverage.total.statements?.pct || 0)
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Try aggregate format
|
|
195
|
-
if (coverage.aggregate) {
|
|
196
|
-
return {
|
|
197
|
-
lines: Math.round(coverage.aggregate.line?.pct || 0),
|
|
198
|
-
branches: Math.round(coverage.aggregate.branch?.pct || 0),
|
|
199
|
-
functions: Math.round(coverage.aggregate.function?.pct || 0),
|
|
200
|
-
statements: Math.round(coverage.aggregate.statement?.pct || 0)
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
} catch (error) {
|
|
204
|
-
// Coverage file not found or invalid
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// Fallback: simulated coverage for testing
|
|
209
|
-
return {
|
|
210
|
-
lines: 75 + Math.floor(Math.random() * 20),
|
|
211
|
-
branches: 70 + Math.floor(Math.random() * 20),
|
|
212
|
-
functions: 80 + Math.floor(Math.random() * 15),
|
|
213
|
-
statements: 75 + Math.floor(Math.random() * 20)
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Captures baseline for regression testing
|
|
219
|
-
*/
|
|
220
|
-
async captureBaseline() {
|
|
221
|
-
// Run tests to get baseline
|
|
222
|
-
const testResults = await this.runTests();
|
|
223
|
-
|
|
224
|
-
// Get coverage if available
|
|
225
|
-
const coverage = await this.getCoverage();
|
|
226
|
-
|
|
227
|
-
const baseline = {
|
|
228
|
-
timestamp: new Date().toISOString(),
|
|
229
|
-
tests: testResults.tests || { total: 0, passed: 0, failed: 0 },
|
|
230
|
-
coverage: coverage,
|
|
231
|
-
performance: {
|
|
232
|
-
duration: testResults.duration || 0
|
|
233
|
-
},
|
|
234
|
-
environment: {
|
|
235
|
-
node: process.version,
|
|
236
|
-
platform: process.platform,
|
|
237
|
-
arch: process.arch
|
|
238
|
-
}
|
|
239
|
-
};
|
|
240
|
-
|
|
241
|
-
// Save baseline
|
|
242
|
-
await fs.mkdir(this.regressionDir, { recursive: true });
|
|
243
|
-
const baselinePath = path.join(this.regressionDir, CONFIG.filePatterns.baseline);
|
|
244
|
-
await fs.writeFile(baselinePath, JSON.stringify(baseline, null, 2));
|
|
245
|
-
|
|
246
|
-
return { baseline, path: baselinePath };
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
/**
|
|
250
|
-
* Loads baseline
|
|
251
|
-
*/
|
|
252
|
-
async loadBaseline() {
|
|
253
|
-
const baselinePath = path.join(this.regressionDir, CONFIG.filePatterns.baseline);
|
|
254
|
-
|
|
255
|
-
try {
|
|
256
|
-
const content = await fs.readFile(baselinePath, 'utf8');
|
|
257
|
-
return JSON.parse(content);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
return null;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Compares current state with baseline
|
|
265
|
-
*/
|
|
266
|
-
async compareWithBaseline() {
|
|
267
|
-
const baseline = await this.loadBaseline();
|
|
268
|
-
|
|
269
|
-
if (!baseline) {
|
|
270
|
-
throw new Error('No baseline found. Run "regression:suite baseline" first.');
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Run current tests
|
|
274
|
-
const currentResults = await this.runTests();
|
|
275
|
-
const currentCoverage = await this.getCoverage();
|
|
276
|
-
|
|
277
|
-
const comparison = {
|
|
278
|
-
baseline: {
|
|
279
|
-
timestamp: baseline.timestamp,
|
|
280
|
-
tests: baseline.tests,
|
|
281
|
-
coverage: baseline.coverage,
|
|
282
|
-
performance: baseline.performance
|
|
283
|
-
},
|
|
284
|
-
current: {
|
|
285
|
-
timestamp: new Date().toISOString(),
|
|
286
|
-
tests: currentResults.tests || { total: 0, passed: 0, failed: 0 },
|
|
287
|
-
coverage: currentCoverage,
|
|
288
|
-
performance: { duration: currentResults.duration || 0 }
|
|
289
|
-
},
|
|
290
|
-
regressions: [],
|
|
291
|
-
improvements: []
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
// Compare tests
|
|
295
|
-
if (baseline.tests && currentResults.tests) {
|
|
296
|
-
const testDiff = currentResults.tests.passed - baseline.tests.passed;
|
|
297
|
-
if (testDiff < 0) {
|
|
298
|
-
comparison.regressions.push({
|
|
299
|
-
type: 'tests',
|
|
300
|
-
message: `${Math.abs(testDiff)} fewer tests passing`
|
|
301
|
-
});
|
|
302
|
-
} else if (testDiff > 0) {
|
|
303
|
-
comparison.improvements.push({
|
|
304
|
-
type: 'tests',
|
|
305
|
-
message: `${testDiff} more tests passing`
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Compare coverage
|
|
311
|
-
if (baseline.coverage && currentCoverage) {
|
|
312
|
-
const coverageDiff = currentCoverage.lines - baseline.coverage.lines;
|
|
313
|
-
if (coverageDiff < -5) {
|
|
314
|
-
comparison.regressions.push({
|
|
315
|
-
type: 'coverage',
|
|
316
|
-
message: `${Math.abs(coverageDiff)}% coverage decrease`
|
|
317
|
-
});
|
|
318
|
-
} else if (coverageDiff > 5) {
|
|
319
|
-
comparison.improvements.push({
|
|
320
|
-
type: 'coverage',
|
|
321
|
-
message: `${coverageDiff}% coverage increase`
|
|
322
|
-
});
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Compare performance
|
|
327
|
-
if (baseline.performance && currentResults.duration) {
|
|
328
|
-
const perfChange = ((currentResults.duration - baseline.performance.duration) /
|
|
329
|
-
baseline.performance.duration) * 100;
|
|
330
|
-
if (perfChange > 20) {
|
|
331
|
-
comparison.regressions.push({
|
|
332
|
-
type: 'performance',
|
|
333
|
-
message: `${Math.round(perfChange)}% slower`
|
|
334
|
-
});
|
|
335
|
-
} else if (perfChange < -10) {
|
|
336
|
-
comparison.improvements.push({
|
|
337
|
-
type: 'performance',
|
|
338
|
-
message: `${Math.abs(Math.round(perfChange))}% faster`
|
|
339
|
-
});
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return comparison;
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Analyzes test history for patterns
|
|
348
|
-
*/
|
|
349
|
-
async analyzePatterns(options = {}) {
|
|
350
|
-
const historyPath = path.join(this.regressionDir, CONFIG.filePatterns.history);
|
|
351
|
-
|
|
352
|
-
// Load history
|
|
353
|
-
let history = [];
|
|
354
|
-
try {
|
|
355
|
-
const content = await fs.readFile(historyPath, 'utf8');
|
|
356
|
-
history = JSON.parse(content);
|
|
357
|
-
} catch (error) {
|
|
358
|
-
// No history yet
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const analysis = {
|
|
362
|
-
flaky: [],
|
|
363
|
-
slow: [],
|
|
364
|
-
trends: {}
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
if (options.flaky) {
|
|
368
|
-
// Analyze for flaky tests
|
|
369
|
-
const testRuns = {};
|
|
370
|
-
for (const run of history) {
|
|
371
|
-
if (run.test) {
|
|
372
|
-
if (!testRuns[run.test]) {
|
|
373
|
-
testRuns[run.test] = [];
|
|
374
|
-
}
|
|
375
|
-
testRuns[run.test].push(run.passed);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
for (const [test, results] of Object.entries(testRuns)) {
|
|
380
|
-
if (results.length < 3) continue;
|
|
381
|
-
|
|
382
|
-
const passCount = results.filter(r => r).length;
|
|
383
|
-
const failCount = results.filter(r => !r).length;
|
|
384
|
-
|
|
385
|
-
if (passCount > 0 && failCount > 0) {
|
|
386
|
-
const passRate = (passCount / results.length) * 100;
|
|
387
|
-
if (passRate < 95 && passRate > 5) {
|
|
388
|
-
analysis.flaky.push({
|
|
389
|
-
test,
|
|
390
|
-
passRate: Math.round(passRate),
|
|
391
|
-
runs: results.length
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (options.slow) {
|
|
399
|
-
// Identify slow tests (simulated)
|
|
400
|
-
const threshold = options.slowThreshold || CONFIG.defaults.slowTestThreshold;
|
|
401
|
-
analysis.slow = [
|
|
402
|
-
{ name: 'integration test', duration: 2500 },
|
|
403
|
-
{ name: 'database test', duration: 1800 }
|
|
404
|
-
].filter(t => t.duration > threshold);
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return analysis;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Adds entry to history
|
|
412
|
-
*/
|
|
413
|
-
async addToHistory(entry) {
|
|
414
|
-
const historyPath = path.join(this.regressionDir, CONFIG.filePatterns.history);
|
|
415
|
-
|
|
416
|
-
let history = [];
|
|
417
|
-
try {
|
|
418
|
-
const content = await fs.readFile(historyPath, 'utf8');
|
|
419
|
-
history = JSON.parse(content);
|
|
420
|
-
} catch (error) {
|
|
421
|
-
// No history yet
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
history.push(entry);
|
|
425
|
-
|
|
426
|
-
// Limit history size
|
|
427
|
-
if (history.length > CONFIG.defaults.maxHistorySize) {
|
|
428
|
-
history = history.slice(-CONFIG.defaults.maxHistorySize);
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
await fs.mkdir(this.regressionDir, { recursive: true });
|
|
432
|
-
await fs.writeFile(historyPath, JSON.stringify(history, null, 2));
|
|
433
|
-
|
|
434
|
-
return history;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
/**
|
|
438
|
-
* Gets test trends from history
|
|
439
|
-
*/
|
|
440
|
-
async getTrends(limit = 10) {
|
|
441
|
-
const historyPath = path.join(this.regressionDir, CONFIG.filePatterns.history);
|
|
442
|
-
|
|
443
|
-
let history = [];
|
|
444
|
-
try {
|
|
445
|
-
const content = await fs.readFile(historyPath, 'utf8');
|
|
446
|
-
history = JSON.parse(content);
|
|
447
|
-
} catch (error) {
|
|
448
|
-
return { history: [], statistics: {} };
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
const recentRuns = history.slice(-limit);
|
|
452
|
-
|
|
453
|
-
// Calculate statistics
|
|
454
|
-
const statistics = {
|
|
455
|
-
totalRuns: history.length,
|
|
456
|
-
avgSuccessRate: 0,
|
|
457
|
-
avgDuration: 0,
|
|
458
|
-
avgCoverage: 0
|
|
459
|
-
};
|
|
460
|
-
|
|
461
|
-
let successCount = 0;
|
|
462
|
-
let durationCount = 0;
|
|
463
|
-
let coverageCount = 0;
|
|
464
|
-
|
|
465
|
-
for (const run of history) {
|
|
466
|
-
if (run.tests && run.tests.total > 0) {
|
|
467
|
-
statistics.avgSuccessRate += (run.tests.passed / run.tests.total);
|
|
468
|
-
successCount++;
|
|
469
|
-
}
|
|
470
|
-
if (run.duration) {
|
|
471
|
-
statistics.avgDuration += run.duration;
|
|
472
|
-
durationCount++;
|
|
473
|
-
}
|
|
474
|
-
if (run.coverage && run.coverage.lines) {
|
|
475
|
-
statistics.avgCoverage += run.coverage.lines;
|
|
476
|
-
coverageCount++;
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (successCount > 0) {
|
|
481
|
-
statistics.avgSuccessRate = Math.round((statistics.avgSuccessRate / successCount) * 100);
|
|
482
|
-
}
|
|
483
|
-
if (durationCount > 0) {
|
|
484
|
-
statistics.avgDuration = Math.round(statistics.avgDuration / durationCount);
|
|
485
|
-
}
|
|
486
|
-
if (coverageCount > 0) {
|
|
487
|
-
statistics.avgCoverage = Math.round(statistics.avgCoverage / coverageCount);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return { history: recentRuns, statistics };
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Generates comprehensive report
|
|
495
|
-
*/
|
|
496
|
-
async generateReport() {
|
|
497
|
-
// Gather all data
|
|
498
|
-
const testResults = await this.runTests();
|
|
499
|
-
const coverage = await this.getCoverage();
|
|
500
|
-
const baseline = await this.loadBaseline();
|
|
501
|
-
const { history, statistics } = await this.getTrends();
|
|
502
|
-
|
|
503
|
-
// Create report
|
|
504
|
-
const report = {
|
|
505
|
-
timestamp: new Date().toISOString(),
|
|
506
|
-
summary: {
|
|
507
|
-
tests: testResults.tests || { total: 0, passed: 0, failed: 0 },
|
|
508
|
-
coverage: coverage,
|
|
509
|
-
duration: testResults.duration,
|
|
510
|
-
success: testResults.success
|
|
511
|
-
},
|
|
512
|
-
baseline: baseline ? {
|
|
513
|
-
timestamp: baseline.timestamp,
|
|
514
|
-
comparison: await this.compareWithBaseline()
|
|
515
|
-
} : null,
|
|
516
|
-
trends: statistics,
|
|
517
|
-
environment: {
|
|
518
|
-
node: process.version,
|
|
519
|
-
platform: process.platform,
|
|
520
|
-
arch: process.arch,
|
|
521
|
-
cwd: this.projectRoot
|
|
522
|
-
}
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
// Save report
|
|
526
|
-
await fs.mkdir(this.regressionDir, { recursive: true });
|
|
527
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
528
|
-
const reportPath = path.join(
|
|
529
|
-
this.regressionDir,
|
|
530
|
-
CONFIG.filePatterns.report.replace('{timestamp}', timestamp)
|
|
531
|
-
);
|
|
532
|
-
|
|
533
|
-
await fs.writeFile(reportPath, JSON.stringify(report, null, 2));
|
|
534
|
-
|
|
535
|
-
// Add to history
|
|
536
|
-
await this.addToHistory({
|
|
537
|
-
timestamp: report.timestamp,
|
|
538
|
-
tests: report.summary.tests,
|
|
539
|
-
coverage: report.summary.coverage,
|
|
540
|
-
duration: report.summary.duration
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
return { report, path: reportPath };
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
/**
|
|
547
|
-
* Checks coverage against threshold
|
|
548
|
-
*/
|
|
549
|
-
checkCoverageThreshold(coverage, threshold) {
|
|
550
|
-
const failures = [];
|
|
551
|
-
|
|
552
|
-
if (coverage.lines < threshold) {
|
|
553
|
-
failures.push({
|
|
554
|
-
metric: 'lines',
|
|
555
|
-
actual: coverage.lines,
|
|
556
|
-
threshold
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
if (coverage.branches < threshold) {
|
|
560
|
-
failures.push({
|
|
561
|
-
metric: 'branches',
|
|
562
|
-
actual: coverage.branches,
|
|
563
|
-
threshold
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
if (coverage.functions < threshold) {
|
|
567
|
-
failures.push({
|
|
568
|
-
metric: 'functions',
|
|
569
|
-
actual: coverage.functions,
|
|
570
|
-
threshold
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return failures;
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
module.exports = RegressionAnalyzer;
|