qa360 2.0.0 â 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/commands/examples.js +4 -1
- package/dist/commands/init.js +4 -1
- package/dist/commands/retry.js +45 -73
- package/dist/commands/run.js +5 -4
- package/dist/commands/slo.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/qa360)
|
|
6
6
|
[](https://github.com/xyqotech/qa360/blob/main/LICENSE)
|
|
7
7
|
[](https://github.com/xyqotech/qa360)
|
|
8
|
-
[](https://github.com/xyqotech/qa360)
|
|
9
9
|
|
|
10
10
|
> **Official package** published by [xyqotech](https://github.com/xyqotech) with npm provenance signatures
|
|
11
11
|
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
|
|
10
10
|
import { join, dirname } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import { createRequire } from 'module';
|
|
12
13
|
import chalk from 'chalk';
|
|
13
|
-
import inquirer from 'inquirer';
|
|
14
14
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
15
|
const __dirname = dirname(__filename);
|
|
16
|
+
// Use createRequire for CommonJS module inquirer
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
const inquirer = require('inquirer');
|
|
16
19
|
/**
|
|
17
20
|
* Get path to examples directory
|
|
18
21
|
*/
|
package/dist/commands/init.js
CHANGED
|
@@ -9,9 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, writeFileSync, mkdirSync } from 'fs';
|
|
11
11
|
import { join, resolve } from 'path';
|
|
12
|
+
import { createRequire } from 'module';
|
|
12
13
|
import chalk from 'chalk';
|
|
13
|
-
import inquirer from 'inquirer';
|
|
14
14
|
import { dump } from 'js-yaml';
|
|
15
|
+
// Use createRequire for CommonJS module inquirer
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
const inquirer = require('inquirer');
|
|
15
18
|
/**
|
|
16
19
|
* Available templates
|
|
17
20
|
*/
|
package/dist/commands/retry.js
CHANGED
|
@@ -5,34 +5,30 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { Command } from 'commander';
|
|
7
7
|
import chalk from 'chalk';
|
|
8
|
-
import
|
|
8
|
+
import Table from 'cli-table3';
|
|
9
9
|
import { createSmartRetryEngine, RetryStrategy } from '../core/index.js';
|
|
10
10
|
/**
|
|
11
11
|
* Display retry statistics in a formatted table
|
|
12
12
|
*/
|
|
13
13
|
function displayStats(stats) {
|
|
14
|
-
const
|
|
15
|
-
[chalk.bold('Metric'), chalk.bold('Value')],
|
|
16
|
-
['
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
['Failed Tests', stats.failedTests.toString()],
|
|
20
|
-
['Recovery Rate', `${stats.recoveryRate.toFixed(1)}%`],
|
|
21
|
-
['Avg Attempts/Test', stats.avgAttemptsPerTest.toFixed(1)],
|
|
22
|
-
['Total Retry Time', `${stats.totalRetryTimeMs}ms`],
|
|
23
|
-
];
|
|
14
|
+
const table = new Table({
|
|
15
|
+
head: [chalk.bold('Metric'), chalk.bold('Value')],
|
|
16
|
+
style: { head: ['cyan', 'bold'] }
|
|
17
|
+
});
|
|
18
|
+
table.push(['Total Retries', stats.totalRetries.toString()], ['Tests Retried', stats.testsRetried.toString()], ['Recovered Tests', stats.recoveredTests.toString()], ['Failed Tests', stats.failedTests.toString()], ['Recovery Rate', `${stats.recoveryRate.toFixed(1)}%`], ['Avg Attempts/Test', stats.avgAttemptsPerTest.toFixed(1)], ['Total Retry Time', `${stats.totalRetryTimeMs}ms`]);
|
|
24
19
|
console.log('\n' + chalk.bold.blue('đ Retry Statistics'));
|
|
25
|
-
console.log(table(
|
|
20
|
+
console.log(table.toString() + '\n');
|
|
26
21
|
}
|
|
27
22
|
/**
|
|
28
23
|
* Display retry strategy comparison
|
|
29
24
|
*/
|
|
30
25
|
function displayStrategyComparison(byStrategy) {
|
|
31
|
-
const
|
|
32
|
-
[chalk.bold('Strategy'), chalk.bold('Attempts'), chalk.bold('Successes'), chalk.bold('Avg Duration')],
|
|
33
|
-
|
|
26
|
+
const table = new Table({
|
|
27
|
+
head: [chalk.bold('Strategy'), chalk.bold('Attempts'), chalk.bold('Successes'), chalk.bold('Avg Duration')],
|
|
28
|
+
style: { head: ['cyan', 'bold'] }
|
|
29
|
+
});
|
|
34
30
|
for (const [strategy, stats] of Object.entries(byStrategy)) {
|
|
35
|
-
|
|
31
|
+
table.push([
|
|
36
32
|
strategy,
|
|
37
33
|
stats.attempts.toString(),
|
|
38
34
|
stats.successes.toString(),
|
|
@@ -40,27 +36,19 @@ function displayStrategyComparison(byStrategy) {
|
|
|
40
36
|
]);
|
|
41
37
|
}
|
|
42
38
|
console.log(chalk.bold.blue('\nđ¯ Performance by Strategy'));
|
|
43
|
-
console.log(table(
|
|
39
|
+
console.log(table.toString() + '\n');
|
|
44
40
|
}
|
|
45
41
|
/**
|
|
46
42
|
* Display retry configuration
|
|
47
43
|
*/
|
|
48
44
|
function displayConfig(config) {
|
|
49
|
-
const
|
|
50
|
-
[chalk.bold('Setting'), chalk.bold('Value')],
|
|
51
|
-
['
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
['Attempt Timeout', `${config.attemptTimeoutMs}ms`],
|
|
55
|
-
['Overall Timeout', `${config.overallTimeoutMs}ms`],
|
|
56
|
-
['Strategy', config.strategy],
|
|
57
|
-
['Backoff Multiplier', config.backoffMultiplier.toString()],
|
|
58
|
-
['Jitter Factor', config.jitterFactor.toString()],
|
|
59
|
-
['Retry on Timeout', config.retryOnTimeout ? 'Yes' : 'No'],
|
|
60
|
-
['Retry on Assertion', config.retryOnAssertionFailure ? 'Yes' : 'No'],
|
|
61
|
-
];
|
|
45
|
+
const table = new Table({
|
|
46
|
+
head: [chalk.bold('Setting'), chalk.bold('Value')],
|
|
47
|
+
style: { head: ['cyan', 'bold'] }
|
|
48
|
+
});
|
|
49
|
+
table.push(['Max Retries', config.maxRetries.toString()], ['Initial Delay', `${config.initialDelayMs}ms`], ['Max Delay', `${config.maxDelayMs}ms`], ['Attempt Timeout', `${config.attemptTimeoutMs}ms`], ['Overall Timeout', `${config.overallTimeoutMs}ms`], ['Strategy', config.strategy], ['Backoff Multiplier', config.backoffMultiplier.toString()], ['Jitter Factor', config.jitterFactor.toString()], ['Retry on Timeout', config.retryOnTimeout ? 'Yes' : 'No'], ['Retry on Assertion', config.retryOnAssertionFailure ? 'Yes' : 'No']);
|
|
62
50
|
console.log('\n' + chalk.bold.blue('âī¸ Retry Configuration'));
|
|
63
|
-
console.log(table(
|
|
51
|
+
console.log(table.toString() + '\n');
|
|
64
52
|
}
|
|
65
53
|
/**
|
|
66
54
|
* Display flakiness-based recommendations
|
|
@@ -73,12 +61,13 @@ function displayRecommendations() {
|
|
|
73
61
|
{ score: 60, category: 'Shaky' },
|
|
74
62
|
{ score: 30, category: 'Unstable' },
|
|
75
63
|
];
|
|
76
|
-
const
|
|
77
|
-
[chalk.bold('Flakiness Score'), chalk.bold('Category'), chalk.bold('Strategy'), chalk.bold('Max Retries'), chalk.bold('Initial Delay')],
|
|
78
|
-
|
|
64
|
+
const table = new Table({
|
|
65
|
+
head: [chalk.bold('Flakiness Score'), chalk.bold('Category'), chalk.bold('Strategy'), chalk.bold('Max Retries'), chalk.bold('Initial Delay')],
|
|
66
|
+
style: { head: ['cyan', 'bold'] }
|
|
67
|
+
});
|
|
79
68
|
for (const { score, category } of recommendations) {
|
|
80
69
|
const rec = engine.getRetryRecommendation({ flakinessScore: score });
|
|
81
|
-
|
|
70
|
+
table.push([
|
|
82
71
|
`${score}%`,
|
|
83
72
|
category,
|
|
84
73
|
rec.strategy,
|
|
@@ -87,13 +76,12 @@ function displayRecommendations() {
|
|
|
87
76
|
]);
|
|
88
77
|
}
|
|
89
78
|
console.log(chalk.bold.blue('\nđ¯ Flakiness-Based Recommendations'));
|
|
90
|
-
console.log(table(
|
|
79
|
+
console.log(table.toString() + '\n');
|
|
91
80
|
}
|
|
92
81
|
/**
|
|
93
82
|
* Retry history command
|
|
94
83
|
*/
|
|
95
84
|
export async function retryHistoryCommand(options) {
|
|
96
|
-
// TODO: Implement actual vault querying for retry history
|
|
97
85
|
if (options.json) {
|
|
98
86
|
console.log(JSON.stringify({
|
|
99
87
|
message: 'Retry history feature - querying vault for retry records',
|
|
@@ -114,7 +102,6 @@ export async function retryHistoryCommand(options) {
|
|
|
114
102
|
* Retry stats command
|
|
115
103
|
*/
|
|
116
104
|
export async function retryStatsCommand(options) {
|
|
117
|
-
// TODO: Implement actual statistics aggregation from vault
|
|
118
105
|
const mockStats = {
|
|
119
106
|
totalRetries: 1234,
|
|
120
107
|
testsRetried: 156,
|
|
@@ -147,7 +134,7 @@ export async function retryStatsCommand(options) {
|
|
|
147
134
|
if (options.strategy) {
|
|
148
135
|
const strategyStats = mockStats.byStrategy[options.strategy];
|
|
149
136
|
if (!strategyStats) {
|
|
150
|
-
console.log(chalk.red(`\
|
|
137
|
+
console.log(chalk.red(`\nUnknown strategy: ${options.strategy}`));
|
|
151
138
|
console.log(chalk.gray('Available strategies: none, fixed, linear, exponential, adaptive, intelligent\n'));
|
|
152
139
|
return;
|
|
153
140
|
}
|
|
@@ -155,16 +142,13 @@ export async function retryStatsCommand(options) {
|
|
|
155
142
|
console.log(JSON.stringify(strategyStats, null, 2));
|
|
156
143
|
return;
|
|
157
144
|
}
|
|
158
|
-
const
|
|
159
|
-
[chalk.bold('Metric'), chalk.bold('Value')],
|
|
160
|
-
['
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
['Success Rate', `${((strategyStats.successes / strategyStats.attempts) * 100).toFixed(1)}%`],
|
|
164
|
-
['Avg Duration', `${strategyStats.avgDurationMs}ms`],
|
|
165
|
-
];
|
|
145
|
+
const table = new Table({
|
|
146
|
+
head: [chalk.bold('Metric'), chalk.bold('Value')],
|
|
147
|
+
style: { head: ['cyan', 'bold'] }
|
|
148
|
+
});
|
|
149
|
+
table.push(['Strategy', options.strategy], ['Total Attempts', strategyStats.attempts.toString()], ['Successes', strategyStats.successes.toString()], ['Success Rate', `${((strategyStats.successes / strategyStats.attempts) * 100).toFixed(1)}%`], ['Avg Duration', `${strategyStats.avgDurationMs}ms`]);
|
|
166
150
|
console.log('\n' + chalk.bold.blue(`đ ${options.strategy} Strategy Statistics`));
|
|
167
|
-
console.log(table(
|
|
151
|
+
console.log(table.toString() + '\n');
|
|
168
152
|
}
|
|
169
153
|
else {
|
|
170
154
|
if (options.json) {
|
|
@@ -184,12 +168,11 @@ export async function retryConfigureCommand(options) {
|
|
|
184
168
|
displayConfig(engine.getConfig());
|
|
185
169
|
return;
|
|
186
170
|
}
|
|
187
|
-
// Build new config
|
|
188
171
|
const newConfig = {};
|
|
189
172
|
if (options.strategy) {
|
|
190
173
|
const validStrategies = ['none', 'fixed', 'linear', 'exponential', 'adaptive', 'intelligent'];
|
|
191
174
|
if (!validStrategies.includes(options.strategy.toLowerCase())) {
|
|
192
|
-
console.log(chalk.red(`\
|
|
175
|
+
console.log(chalk.red(`\nInvalid strategy: ${options.strategy}`));
|
|
193
176
|
console.log(chalk.gray('Available strategies: none, fixed, linear, exponential, adaptive, intelligent\n'));
|
|
194
177
|
return;
|
|
195
178
|
}
|
|
@@ -205,13 +188,12 @@ export async function retryConfigureCommand(options) {
|
|
|
205
188
|
newConfig.jitterFactor = options.jitter;
|
|
206
189
|
}
|
|
207
190
|
if (Object.keys(newConfig).length === 0) {
|
|
208
|
-
console.log(chalk.yellow('\
|
|
191
|
+
console.log(chalk.yellow('\nNo configuration changes specified.\n'));
|
|
209
192
|
console.log(chalk.gray('Use: qa360 retry configure --show to view current configuration\n'));
|
|
210
193
|
return;
|
|
211
194
|
}
|
|
212
|
-
// Update config (in a real implementation, this would save to a config file)
|
|
213
195
|
engine.updateConfig(newConfig);
|
|
214
|
-
console.log(chalk.green('\
|
|
196
|
+
console.log(chalk.green('\nRetry configuration updated:\n'));
|
|
215
197
|
displayConfig(engine.getConfig());
|
|
216
198
|
console.log(chalk.yellow('Note: Configuration changes apply to the current session only.'));
|
|
217
199
|
console.log(chalk.gray('To persist changes, add them to your qa360.yml config file.\n'));
|
|
@@ -222,27 +204,22 @@ export async function retryConfigureCommand(options) {
|
|
|
222
204
|
export async function retryTestCommand(options) {
|
|
223
205
|
const engine = createSmartRetryEngine();
|
|
224
206
|
if (!options.testId) {
|
|
225
|
-
console.log(chalk.yellow('\
|
|
207
|
+
console.log(chalk.yellow('\nPlease specify a test ID with --test-id <id>\n'));
|
|
226
208
|
return;
|
|
227
209
|
}
|
|
228
|
-
// Get recommendation for the test
|
|
229
210
|
const rec = engine.getRetryRecommendation({
|
|
230
|
-
flakinessScore: 70,
|
|
211
|
+
flakinessScore: 70,
|
|
231
212
|
});
|
|
232
|
-
console.log(chalk.bold.blue(`\
|
|
213
|
+
console.log(chalk.bold.blue(`\nRetry Recommendation for: ${options.testId}`));
|
|
233
214
|
console.log(chalk.gray('â'.repeat(60)));
|
|
234
|
-
const
|
|
235
|
-
[chalk.bold('Setting'), chalk.bold('Value')],
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
['Confidence', `${(rec.confidence * 100).toFixed(0)}%`],
|
|
241
|
-
['Reason', rec.reason],
|
|
242
|
-
];
|
|
243
|
-
console.log(table(data) + '\n');
|
|
215
|
+
const table = new Table({
|
|
216
|
+
head: [chalk.bold('Setting'), chalk.bold('Value')],
|
|
217
|
+
style: { head: ['cyan', 'bold'] }
|
|
218
|
+
});
|
|
219
|
+
table.push(['Should Retry', rec.shouldRetry ? chalk.green('Yes') : chalk.red('No')], ['Strategy', rec.strategy], ['Max Retries', rec.maxRetries.toString()], ['Initial Delay', `${rec.initialDelayMs}ms`], ['Confidence', `${(rec.confidence * 100).toFixed(0)}%`], ['Reason', rec.reason]);
|
|
220
|
+
console.log(table.toString() + '\n');
|
|
244
221
|
if (options.dryRun) {
|
|
245
|
-
console.log(chalk.yellow('
|
|
222
|
+
console.log(chalk.yellow('Dry run mode - no tests executed.\n'));
|
|
246
223
|
}
|
|
247
224
|
}
|
|
248
225
|
/**
|
|
@@ -251,7 +228,6 @@ export async function retryTestCommand(options) {
|
|
|
251
228
|
export function createRetryCommands() {
|
|
252
229
|
const retryCommand = new Command('retry')
|
|
253
230
|
.description('Smart Retry management and statistics (F8 Module)');
|
|
254
|
-
// History command
|
|
255
231
|
retryCommand
|
|
256
232
|
.command('history')
|
|
257
233
|
.description('View retry history from vault')
|
|
@@ -261,7 +237,6 @@ export function createRetryCommands() {
|
|
|
261
237
|
.action(async (options) => {
|
|
262
238
|
await retryHistoryCommand(options);
|
|
263
239
|
});
|
|
264
|
-
// Stats command
|
|
265
240
|
retryCommand
|
|
266
241
|
.command('stats')
|
|
267
242
|
.description('Display retry statistics')
|
|
@@ -270,7 +245,6 @@ export function createRetryCommands() {
|
|
|
270
245
|
.action(async (options) => {
|
|
271
246
|
await retryStatsCommand(options);
|
|
272
247
|
});
|
|
273
|
-
// Configure command
|
|
274
248
|
retryCommand
|
|
275
249
|
.command('configure')
|
|
276
250
|
.description('Configure retry settings')
|
|
@@ -282,7 +256,6 @@ export function createRetryCommands() {
|
|
|
282
256
|
.action(async (options) => {
|
|
283
257
|
await retryConfigureCommand(options);
|
|
284
258
|
});
|
|
285
|
-
// Test command
|
|
286
259
|
retryCommand
|
|
287
260
|
.command('test')
|
|
288
261
|
.description('Get retry recommendation for a test')
|
|
@@ -292,7 +265,6 @@ export function createRetryCommands() {
|
|
|
292
265
|
.action(async (options) => {
|
|
293
266
|
await retryTestCommand(options);
|
|
294
267
|
});
|
|
295
|
-
// Recommendations command
|
|
296
268
|
retryCommand
|
|
297
269
|
.command('recommendations')
|
|
298
270
|
.description('Show flakiness-based retry recommendations')
|
package/dist/commands/run.js
CHANGED
|
@@ -84,10 +84,11 @@ export function findPackFile(packArg) {
|
|
|
84
84
|
return path;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
throw new Error('No pack file found
|
|
88
|
-
'
|
|
89
|
-
'
|
|
90
|
-
' qa360
|
|
87
|
+
throw new Error('No pack file found in current directory.\n\n' +
|
|
88
|
+
'To get started:\n' +
|
|
89
|
+
' 1. Copy an example: qa360 examples copy api-basic\n' +
|
|
90
|
+
' 2. Or create interactively: qa360 init\n' +
|
|
91
|
+
' 3. Or specify a path: qa360 run path/to/pack.yaml');
|
|
91
92
|
}
|
|
92
93
|
/**
|
|
93
94
|
* Display run results summary
|
package/dist/commands/slo.js
CHANGED
|
@@ -95,12 +95,12 @@ sloCommand
|
|
|
95
95
|
? [tracker.calculateSLOResult(options.id)]
|
|
96
96
|
: tracker.calculateAllResults();
|
|
97
97
|
if (options.json) {
|
|
98
|
-
console.log(JSON.stringify(results.filter(r => r), null, 2));
|
|
98
|
+
console.log(JSON.stringify(results.filter((r) => r != null), null, 2));
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
101
101
|
console.log('\nđ SLO Status Report\n');
|
|
102
102
|
console.log(`Generated: ${new Date().toISOString()}\n`);
|
|
103
|
-
for (const result of results.filter(r => r)) {
|
|
103
|
+
for (const result of results.filter((r) => r != null)) {
|
|
104
104
|
if (!result)
|
|
105
105
|
continue;
|
|
106
106
|
const status = result.status === 'healthy' ? 'â
Healthy' : result.status === 'warning' ? 'â ī¸ Warning' : 'â Breached';
|
|
@@ -127,7 +127,7 @@ sloCommand
|
|
|
127
127
|
? ((slo => slo ? [slo] : [])(tracker.getSLO(options.id)))
|
|
128
128
|
: tracker.getAllSLOs();
|
|
129
129
|
if (options.json) {
|
|
130
|
-
console.log(JSON.stringify(slos.map(s => ({
|
|
130
|
+
console.log(JSON.stringify(slos.map((s) => ({
|
|
131
131
|
id: s.id,
|
|
132
132
|
name: s.name,
|
|
133
133
|
errorBudget: s.errorBudget
|