ado-sync 0.1.67 → 0.1.68
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/dist/__tests__/regressions.test.js +122 -0
- package/dist/__tests__/regressions.test.js.map +1 -1
- package/dist/cli-diagnostics.d.ts +66 -0
- package/dist/cli-diagnostics.js +75 -0
- package/dist/cli-diagnostics.js.map +1 -0
- package/dist/cli.js +254 -19
- package/dist/cli.js.map +1 -1
- package/dist/config.js +120 -8
- package/dist/config.js.map +1 -1
- package/dist/extensions.d.ts +8 -0
- package/dist/extensions.js +86 -0
- package/dist/extensions.js.map +1 -0
- package/dist/sync/engine.d.ts +18 -2
- package/dist/sync/engine.js +70 -10
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/publish-results.d.ts +25 -0
- package/dist/sync/publish-results.js +81 -2
- package/dist/sync/publish-results.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -55,7 +55,9 @@ const readline = __importStar(require("readline"));
|
|
|
55
55
|
const package_json_1 = __importDefault(require("../package.json"));
|
|
56
56
|
const summarizer_1 = require("./ai/summarizer");
|
|
57
57
|
const client_1 = require("./azure/client");
|
|
58
|
+
const cli_diagnostics_1 = require("./cli-diagnostics");
|
|
58
59
|
const config_1 = require("./config");
|
|
60
|
+
const id_markers_1 = require("./id-markers");
|
|
59
61
|
const engine_1 = require("./sync/engine");
|
|
60
62
|
const generate_1 = require("./sync/generate");
|
|
61
63
|
const manifest_1 = require("./sync/manifest");
|
|
@@ -69,6 +71,17 @@ program
|
|
|
69
71
|
// Global options
|
|
70
72
|
program.option('-c, --config <path>', 'Path to config file (default: ado-sync.json)');
|
|
71
73
|
program.option('--output <format>', 'Output format: text (default) or json');
|
|
74
|
+
program.option('--pat-override <token>', 'Override auth.token for this invocation (not persisted to config)');
|
|
75
|
+
program.option('--org-override <url>', 'Override orgUrl for this invocation (not persisted to config)');
|
|
76
|
+
function loadConfigWithOverrides(configPath) {
|
|
77
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
78
|
+
const globalOpts = program.opts();
|
|
79
|
+
if (globalOpts.patOverride)
|
|
80
|
+
config.auth.token = globalOpts.patOverride;
|
|
81
|
+
if (globalOpts.orgOverride)
|
|
82
|
+
config.orgUrl = globalOpts.orgOverride;
|
|
83
|
+
return config;
|
|
84
|
+
}
|
|
72
85
|
// ─── Error formatting (L) ─────────────────────────────────────────────────────
|
|
73
86
|
function toError(err) {
|
|
74
87
|
if (err instanceof Error)
|
|
@@ -106,6 +119,12 @@ function normalizeSourceFilesForCli(sourceFiles, configDir) {
|
|
|
106
119
|
return undefined;
|
|
107
120
|
return sourceFiles.map((filePath) => path.normalize(path.resolve(configDir, filePath)));
|
|
108
121
|
}
|
|
122
|
+
function printDiagnosticItems(items) {
|
|
123
|
+
console.log(chalk_1.default.bold('Diagnostics:'));
|
|
124
|
+
for (const item of items) {
|
|
125
|
+
console.log(chalk_1.default.dim(` ${item.label}: ${item.value}`));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
109
128
|
// ─── AI summary helper ────────────────────────────────────────────────────────
|
|
110
129
|
/**
|
|
111
130
|
* Build AiSummaryOpts from parsed CLI opts, falling back to config file values.
|
|
@@ -248,6 +267,86 @@ async function runInitWizard(isYaml) {
|
|
|
248
267
|
}
|
|
249
268
|
return JSON.stringify(cfg, null, 2) + '\n';
|
|
250
269
|
}
|
|
270
|
+
// ─── config ───────────────────────────────────────────────────────────────────
|
|
271
|
+
const configCmd = program
|
|
272
|
+
.command('config')
|
|
273
|
+
.description('Configuration inspection and management');
|
|
274
|
+
configCmd
|
|
275
|
+
.command('show')
|
|
276
|
+
.description('Display the fully resolved configuration (after parent merge, env vars, and overrides)')
|
|
277
|
+
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
278
|
+
.action(async (opts) => {
|
|
279
|
+
const globalOpts = program.opts();
|
|
280
|
+
try {
|
|
281
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
282
|
+
const config = loadConfigWithOverrides(configPath);
|
|
283
|
+
if (opts.configOverride?.length)
|
|
284
|
+
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
285
|
+
const redacted = { ...config, auth: { ...config.auth, token: config.auth?.token ? '***' : undefined } };
|
|
286
|
+
process.stdout.write(JSON.stringify(redacted, null, 2) + '\n');
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
handleError(err);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
// ─── extensions ───────────────────────────────────────────────────────────────
|
|
293
|
+
const extCmd = program
|
|
294
|
+
.command('extensions')
|
|
295
|
+
.description('Manage ado-sync extensions');
|
|
296
|
+
extCmd
|
|
297
|
+
.command('list')
|
|
298
|
+
.description('List registered extensions from config')
|
|
299
|
+
.action(async () => {
|
|
300
|
+
const globalOpts = program.opts();
|
|
301
|
+
try {
|
|
302
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
303
|
+
const config = loadConfigWithOverrides(configPath);
|
|
304
|
+
const extensions = config.extensions ?? [];
|
|
305
|
+
if (extensions.length === 0) {
|
|
306
|
+
console.log(chalk_1.default.dim('No extensions configured.'));
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
for (const ext of extensions) {
|
|
310
|
+
console.log(`${chalk_1.default.bold(ext.name)} ${chalk_1.default.dim(`(${ext.type})`)} — ${ext.package}`);
|
|
311
|
+
if (ext.filePatterns?.length) {
|
|
312
|
+
console.log(chalk_1.default.dim(` patterns: ${ext.filePatterns.join(', ')}`));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (err) {
|
|
317
|
+
handleError(err);
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
extCmd
|
|
321
|
+
.command('validate')
|
|
322
|
+
.description('Validate extension compatibility and loading')
|
|
323
|
+
.action(async () => {
|
|
324
|
+
const globalOpts = program.opts();
|
|
325
|
+
try {
|
|
326
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
327
|
+
const config = loadConfigWithOverrides(configPath);
|
|
328
|
+
const configDir = path.dirname(configPath);
|
|
329
|
+
const extensions = config.extensions ?? [];
|
|
330
|
+
if (extensions.length === 0) {
|
|
331
|
+
console.log(chalk_1.default.dim('No extensions configured.'));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
const { loadExtensions, validateExtensions } = await Promise.resolve().then(() => __importStar(require('./extensions')));
|
|
335
|
+
const loaded = await loadExtensions(extensions, configDir);
|
|
336
|
+
const errors = validateExtensions(loaded, package_json_1.default.version);
|
|
337
|
+
if (errors.length) {
|
|
338
|
+
for (const err of errors)
|
|
339
|
+
console.log(chalk_1.default.red(` ✗ ${err}`));
|
|
340
|
+
process.exit(1);
|
|
341
|
+
}
|
|
342
|
+
for (const ext of loaded) {
|
|
343
|
+
console.log(chalk_1.default.green(` ✓ ${ext.config.name}`) + chalk_1.default.dim(` v${ext.manifest.version} (${ext.manifest.type})`));
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
handleError(err);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
251
350
|
// ─── push ─────────────────────────────────────────────────────────────────────
|
|
252
351
|
program
|
|
253
352
|
.command('push')
|
|
@@ -258,6 +357,7 @@ program
|
|
|
258
357
|
.option('--update-only', 'Only update linked test cases; skip unlinked specs and removed-case detection')
|
|
259
358
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
|
|
260
359
|
.option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
|
|
360
|
+
.option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
|
|
261
361
|
.option('--config-override <path=value>', 'Override a config value (repeatable, e.g. --config-override sync.tagPrefix=mytag)', collect, [])
|
|
262
362
|
.option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
|
|
263
363
|
.option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
|
|
@@ -269,7 +369,7 @@ program
|
|
|
269
369
|
const globalOpts = program.opts();
|
|
270
370
|
try {
|
|
271
371
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
272
|
-
const config = (
|
|
372
|
+
const config = loadConfigWithOverrides(configPath);
|
|
273
373
|
if (opts.configOverride?.length)
|
|
274
374
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
275
375
|
(0, engine_1.validatePushModeOptions)(opts);
|
|
@@ -290,6 +390,8 @@ program
|
|
|
290
390
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
291
391
|
if (opts.sourceFile?.length)
|
|
292
392
|
console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
|
|
393
|
+
if (opts.include?.length)
|
|
394
|
+
console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
|
|
293
395
|
if (opts.configOverride?.length)
|
|
294
396
|
console.log(chalk_1.default.dim(`Overrides: ${opts.configOverride.join(', ')}`));
|
|
295
397
|
console.log('');
|
|
@@ -298,7 +400,7 @@ program
|
|
|
298
400
|
const outputFormat = program.opts().output;
|
|
299
401
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
300
402
|
const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
|
|
301
|
-
const results = await (0, engine_1.push)(config, configDir, { dryRun: opts.dryRun, createOnly: opts.createOnly, linkOnly: opts.linkOnly, updateOnly: opts.updateOnly, tags: opts.tags, sourceFiles: opts.sourceFile, onProgress, onAiProgress, aiSummary });
|
|
403
|
+
const results = await (0, engine_1.push)(config, configDir, { dryRun: opts.dryRun, createOnly: opts.createOnly, linkOnly: opts.linkOnly, updateOnly: opts.updateOnly, tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress, onAiProgress, aiSummary });
|
|
302
404
|
if (isTTY && outputFormat !== 'json')
|
|
303
405
|
clearProgressLine();
|
|
304
406
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -314,12 +416,13 @@ program
|
|
|
314
416
|
.option('--dry-run', 'Show what would change without modifying local files')
|
|
315
417
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
|
|
316
418
|
.option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
|
|
419
|
+
.option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
|
|
317
420
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
318
421
|
.action(async (opts) => {
|
|
319
422
|
const globalOpts = program.opts();
|
|
320
423
|
try {
|
|
321
424
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
322
|
-
const config = (
|
|
425
|
+
const config = loadConfigWithOverrides(configPath);
|
|
323
426
|
if (opts.configOverride?.length)
|
|
324
427
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
325
428
|
const configDir = path.dirname(configPath);
|
|
@@ -331,11 +434,13 @@ program
|
|
|
331
434
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
332
435
|
if (opts.sourceFile?.length)
|
|
333
436
|
console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
|
|
437
|
+
if (opts.include?.length)
|
|
438
|
+
console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
|
|
334
439
|
console.log('');
|
|
335
440
|
const isTTY = process.stdout.isTTY ?? false;
|
|
336
441
|
const outputFormat = program.opts().output;
|
|
337
442
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
338
|
-
const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, sourceFiles: opts.sourceFile, onProgress });
|
|
443
|
+
const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress });
|
|
339
444
|
if (isTTY && outputFormat !== 'json')
|
|
340
445
|
clearProgressLine();
|
|
341
446
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -350,6 +455,7 @@ program
|
|
|
350
455
|
.description('Show diff between local specs and Azure DevOps without making changes')
|
|
351
456
|
.option('--tags <expression>', 'Only check scenarios matching this tag expression')
|
|
352
457
|
.option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
|
|
458
|
+
.option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
|
|
353
459
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
354
460
|
.option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
|
|
355
461
|
.option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
|
|
@@ -361,7 +467,7 @@ program
|
|
|
361
467
|
const globalOpts = program.opts();
|
|
362
468
|
try {
|
|
363
469
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
364
|
-
const config = (
|
|
470
|
+
const config = loadConfigWithOverrides(configPath);
|
|
365
471
|
if (opts.configOverride?.length)
|
|
366
472
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
367
473
|
const configDir = path.dirname(configPath);
|
|
@@ -371,13 +477,15 @@ program
|
|
|
371
477
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
372
478
|
if (opts.sourceFile?.length)
|
|
373
479
|
console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
|
|
480
|
+
if (opts.include?.length)
|
|
481
|
+
console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
|
|
374
482
|
console.log('');
|
|
375
483
|
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
376
484
|
const isTTY = process.stdout.isTTY ?? false;
|
|
377
485
|
const outputFormat = program.opts().output;
|
|
378
486
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
379
487
|
const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
|
|
380
|
-
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, onProgress, onAiProgress, aiSummary });
|
|
488
|
+
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, onProgress, onAiProgress, aiSummary });
|
|
381
489
|
if (isTTY && outputFormat !== 'json')
|
|
382
490
|
clearProgressLine();
|
|
383
491
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -416,7 +524,7 @@ program
|
|
|
416
524
|
const globalOpts = program.opts();
|
|
417
525
|
try {
|
|
418
526
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
419
|
-
const config = (
|
|
527
|
+
const config = loadConfigWithOverrides(configPath);
|
|
420
528
|
const cliPublishOverrides = [
|
|
421
529
|
...(opts.testConfiguration ? [/^\d+$/.test(opts.testConfiguration)
|
|
422
530
|
? `publishTestResults.testConfiguration.id=${opts.testConfiguration}`
|
|
@@ -476,6 +584,28 @@ program
|
|
|
476
584
|
console.log(chalk_1.default.dim(`Run ID: ${result.runId}`));
|
|
477
585
|
console.log(chalk_1.default.dim(`URL: ${result.runUrl}`));
|
|
478
586
|
}
|
|
587
|
+
if (config.toolSettings?.outputLevel === 'diagnostic' && result.diagnostics) {
|
|
588
|
+
console.log('');
|
|
589
|
+
console.log(chalk_1.default.bold('Diagnostics:'));
|
|
590
|
+
if (result.diagnostics.sources.length) {
|
|
591
|
+
console.log(chalk_1.default.dim(' Sources:'));
|
|
592
|
+
for (const source of result.diagnostics.sources) {
|
|
593
|
+
console.log(chalk_1.default.dim(` ${path.relative(process.cwd(), source.filePath)} (${source.format})`));
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (result.diagnostics.configurationId) {
|
|
597
|
+
console.log(chalk_1.default.dim(` Configuration ID: ${result.diagnostics.configurationId}`));
|
|
598
|
+
}
|
|
599
|
+
if (result.diagnostics.plannedRun) {
|
|
600
|
+
console.log(chalk_1.default.dim(` Planned run: plan ${result.diagnostics.plannedRun.planId}, suite ${result.diagnostics.plannedRun.suiteId}, ${result.diagnostics.plannedRun.pointCount} point(s)`));
|
|
601
|
+
}
|
|
602
|
+
if (result.diagnostics.attachments) {
|
|
603
|
+
console.log(chalk_1.default.dim(` Attachments: ${result.diagnostics.attachments.resultCount} result-level, ${result.diagnostics.attachments.runCount} run-level`));
|
|
604
|
+
}
|
|
605
|
+
if (result.diagnostics.analyzedFailures) {
|
|
606
|
+
console.log(chalk_1.default.dim(` AI analyses: ${result.diagnostics.analyzedFailures}`));
|
|
607
|
+
}
|
|
608
|
+
}
|
|
479
609
|
if (result.issuesSummary) {
|
|
480
610
|
const s = result.issuesSummary;
|
|
481
611
|
console.log('');
|
|
@@ -640,10 +770,19 @@ program
|
|
|
640
770
|
console.log('');
|
|
641
771
|
// 1. Load config
|
|
642
772
|
let config;
|
|
773
|
+
let validateDiagnostics;
|
|
643
774
|
try {
|
|
644
775
|
config = (0, config_1.loadConfig)(configPath);
|
|
645
776
|
if (opts.configOverride?.length)
|
|
646
777
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
778
|
+
const plans = config.testPlans ?? [config.testPlan];
|
|
779
|
+
validateDiagnostics = {
|
|
780
|
+
authType: config.auth.type,
|
|
781
|
+
localType: config.local.type,
|
|
782
|
+
syncTargetMode: config.syncTarget?.mode ?? 'suite',
|
|
783
|
+
planIds: plans.map((plan) => plan.id),
|
|
784
|
+
overrideCount: opts.configOverride?.length ?? 0,
|
|
785
|
+
};
|
|
647
786
|
console.log(chalk_1.default.green(' ✓ Config is valid'));
|
|
648
787
|
}
|
|
649
788
|
catch (err) {
|
|
@@ -659,6 +798,10 @@ program
|
|
|
659
798
|
}
|
|
660
799
|
catch (err) {
|
|
661
800
|
console.log(chalk_1.default.red(` ✗ Azure connection failed: ${formatError(err)}`));
|
|
801
|
+
if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
|
|
802
|
+
console.log('');
|
|
803
|
+
printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
|
|
804
|
+
}
|
|
662
805
|
process.exit(1);
|
|
663
806
|
}
|
|
664
807
|
// 3. Verify project
|
|
@@ -671,6 +814,10 @@ program
|
|
|
671
814
|
}
|
|
672
815
|
catch (err) {
|
|
673
816
|
console.log(chalk_1.default.red(` ✗ Project "${config.project}" not found: ${formatError(err)}`));
|
|
817
|
+
if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
|
|
818
|
+
console.log('');
|
|
819
|
+
printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
|
|
820
|
+
}
|
|
674
821
|
process.exit(1);
|
|
675
822
|
}
|
|
676
823
|
// 4. Verify test plan(s)
|
|
@@ -687,6 +834,10 @@ program
|
|
|
687
834
|
allPlansOk = false;
|
|
688
835
|
}
|
|
689
836
|
}
|
|
837
|
+
if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
|
|
838
|
+
console.log('');
|
|
839
|
+
printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
|
|
840
|
+
}
|
|
690
841
|
if (!allPlansOk)
|
|
691
842
|
process.exit(1);
|
|
692
843
|
console.log('');
|
|
@@ -698,6 +849,7 @@ program
|
|
|
698
849
|
.description('Show field-level diff between local specs and Azure DevOps')
|
|
699
850
|
.option('--tags <expression>', 'Only check scenarios matching this tag expression')
|
|
700
851
|
.option('--source-file <path>', 'Restrict the operation to a specific local file (repeatable)', collect, [])
|
|
852
|
+
.option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
|
|
701
853
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
702
854
|
.option('--format <fmt>', 'Output format: text (default) or json')
|
|
703
855
|
.option('--fail-on-drift', 'Exit with code 1 when any differences are found (useful as a CI quality gate)')
|
|
@@ -709,7 +861,7 @@ program
|
|
|
709
861
|
const globalOpts = program.opts();
|
|
710
862
|
try {
|
|
711
863
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
712
|
-
const config = (
|
|
864
|
+
const config = loadConfigWithOverrides(configPath);
|
|
713
865
|
if (opts.configOverride?.length)
|
|
714
866
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
715
867
|
const configDir = path.dirname(configPath);
|
|
@@ -719,9 +871,11 @@ program
|
|
|
719
871
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
720
872
|
if (opts.sourceFile?.length)
|
|
721
873
|
console.log(chalk_1.default.dim(`Files: ${opts.sourceFile.join(', ')}`));
|
|
874
|
+
if (opts.include?.length)
|
|
875
|
+
console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
|
|
722
876
|
console.log('');
|
|
723
877
|
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
724
|
-
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, aiSummary });
|
|
878
|
+
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, sourceFiles: opts.sourceFile, includePatterns: opts.include?.length ? opts.include : undefined, aiSummary });
|
|
725
879
|
// --format json or global --output json
|
|
726
880
|
const outputFormat = opts.format ?? program.opts().output;
|
|
727
881
|
if (outputFormat === 'json') {
|
|
@@ -805,7 +959,7 @@ program
|
|
|
805
959
|
const globalOpts = program.opts();
|
|
806
960
|
try {
|
|
807
961
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
808
|
-
const config = (
|
|
962
|
+
const config = loadConfigWithOverrides(configPath);
|
|
809
963
|
if (opts.configOverride?.length)
|
|
810
964
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
811
965
|
const configDir = path.dirname(configPath);
|
|
@@ -930,7 +1084,7 @@ program
|
|
|
930
1084
|
const globalOpts = program.opts();
|
|
931
1085
|
try {
|
|
932
1086
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
933
|
-
const config = (
|
|
1087
|
+
const config = loadConfigWithOverrides(configPath);
|
|
934
1088
|
if (opts.configOverride?.length)
|
|
935
1089
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
936
1090
|
const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
|
|
@@ -980,23 +1134,31 @@ program
|
|
|
980
1134
|
const globalOpts = program.opts();
|
|
981
1135
|
try {
|
|
982
1136
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
983
|
-
const config = (
|
|
1137
|
+
const config = loadConfigWithOverrides(configPath);
|
|
984
1138
|
if (opts.configOverride?.length)
|
|
985
1139
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
986
1140
|
const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
|
|
987
1141
|
const { acGate, getWorkItemsByQuery, getWorkItemsByAreaPath } = await Promise.resolve().then(() => __importStar(require('./azure/work-items')));
|
|
988
1142
|
const client = await AzureClient.create(config);
|
|
989
1143
|
let storyIds = [];
|
|
1144
|
+
let selectorMode = 'default-states';
|
|
1145
|
+
let selectorValue = opts.states ?? 'Active,Resolved,Closed';
|
|
990
1146
|
if (opts.query) {
|
|
991
1147
|
const stories = await getWorkItemsByQuery(client, config.project, opts.query);
|
|
992
1148
|
storyIds = stories.map((s) => s.id);
|
|
1149
|
+
selectorMode = 'query';
|
|
1150
|
+
selectorValue = opts.query;
|
|
993
1151
|
}
|
|
994
1152
|
else if (opts.areaPath) {
|
|
995
1153
|
const stories = await getWorkItemsByAreaPath(client, config.project, opts.areaPath);
|
|
996
1154
|
storyIds = stories.map((s) => s.id);
|
|
1155
|
+
selectorMode = 'area-path';
|
|
1156
|
+
selectorValue = opts.areaPath;
|
|
997
1157
|
}
|
|
998
1158
|
else if (opts.storyIds) {
|
|
999
1159
|
storyIds = opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
|
|
1160
|
+
selectorMode = 'story-ids';
|
|
1161
|
+
selectorValue = opts.storyIds;
|
|
1000
1162
|
}
|
|
1001
1163
|
else {
|
|
1002
1164
|
// Default: all active stories in project
|
|
@@ -1005,6 +1167,7 @@ program
|
|
|
1005
1167
|
const wiql = `SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '${config.project.replace(/'/g, "''")}' AND [System.WorkItemType] = 'User Story' AND [System.State] IN (${stateList}) ORDER BY [System.Id]`;
|
|
1006
1168
|
const stories = await getWorkItemsByQuery(client, config.project, wiql);
|
|
1007
1169
|
storyIds = stories.map((s) => s.id);
|
|
1170
|
+
selectorValue = states;
|
|
1008
1171
|
}
|
|
1009
1172
|
if (storyIds.length === 0) {
|
|
1010
1173
|
console.log(chalk_1.default.yellow('No stories found to validate.'));
|
|
@@ -1015,6 +1178,23 @@ program
|
|
|
1015
1178
|
console.log(chalk_1.default.dim(`Stories: ${storyIds.length} to validate`));
|
|
1016
1179
|
console.log('');
|
|
1017
1180
|
const report = await acGate(client, config.project, storyIds, config.orgUrl);
|
|
1181
|
+
if (config.toolSettings?.outputLevel === 'diagnostic') {
|
|
1182
|
+
const noAc = report.failed.filter((r) => r.outcome === 'no-ac').length;
|
|
1183
|
+
const noTc = report.failed.filter((r) => r.outcome === 'no-tc').length;
|
|
1184
|
+
printDiagnosticItems((0, cli_diagnostics_1.getAcGateDiagnosticItems)({
|
|
1185
|
+
selectorMode,
|
|
1186
|
+
selectorValue,
|
|
1187
|
+
states: selectorMode === 'default-states' ? selectorValue : undefined,
|
|
1188
|
+
failMode: opts.failOnNoAc ? 'no-ac-only' : 'all-failures',
|
|
1189
|
+
totalStories: storyIds.length,
|
|
1190
|
+
passed: report.passed.length,
|
|
1191
|
+
failed: report.failed.length,
|
|
1192
|
+
noAc,
|
|
1193
|
+
noTc,
|
|
1194
|
+
overrideCount: opts.configOverride?.length ?? 0,
|
|
1195
|
+
}));
|
|
1196
|
+
console.log('');
|
|
1197
|
+
}
|
|
1018
1198
|
const outputFormat = globalOpts.output;
|
|
1019
1199
|
if (outputFormat === 'json') {
|
|
1020
1200
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1055,6 +1235,7 @@ program
|
|
|
1055
1235
|
.command('stale')
|
|
1056
1236
|
.description('List Azure DevOps Test Cases that have no corresponding local spec')
|
|
1057
1237
|
.option('--tags <expression>', 'Only consider local specs matching this tag expression')
|
|
1238
|
+
.option('--suites', 'Report orphaned suite memberships (affected by stalenessPolicy config)')
|
|
1058
1239
|
.option('--retire', 'Automatically transition stale Test Cases to Closed state and tag ado-sync:retired')
|
|
1059
1240
|
.option('--retire-state <state>', 'Target state when retiring (default: Closed)', 'Closed')
|
|
1060
1241
|
.option('--dry-run', 'Show what --retire would do without making changes')
|
|
@@ -1063,7 +1244,7 @@ program
|
|
|
1063
1244
|
const globalOpts = program.opts();
|
|
1064
1245
|
try {
|
|
1065
1246
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1066
|
-
const config = (
|
|
1247
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1067
1248
|
if (opts.configOverride?.length)
|
|
1068
1249
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1069
1250
|
const configDir = path.dirname(configPath);
|
|
@@ -1075,6 +1256,23 @@ program
|
|
|
1075
1256
|
console.log(chalk_1.default.dim(`Retire: will transition to "${opts.retireState}"${opts.dryRun ? ' (dry-run)' : ''}`));
|
|
1076
1257
|
console.log('');
|
|
1077
1258
|
const staleCases = await (0, engine_1.detectStaleTestCases)(config, configDir, { tags: opts.tags });
|
|
1259
|
+
if (config.toolSettings?.outputLevel === 'diagnostic') {
|
|
1260
|
+
console.log(chalk_1.default.bold('Diagnostics:'));
|
|
1261
|
+
for (const item of (0, cli_diagnostics_1.getStaleDiagnosticItems)({
|
|
1262
|
+
syncTargetMode: config.syncTarget?.mode ?? 'suite',
|
|
1263
|
+
planIds: (config.testPlans?.length ? config.testPlans : [config.testPlan]).map((plan) => plan.id),
|
|
1264
|
+
markerPrefix: (0, id_markers_1.getPreferredMarkerTagPrefix)(config),
|
|
1265
|
+
ownershipTag: (0, id_markers_1.getSyncTargetOwnershipTag)(config),
|
|
1266
|
+
tagExpression: opts.tags,
|
|
1267
|
+
staleCount: staleCases.length,
|
|
1268
|
+
retireState: opts.retire ? opts.retireState : undefined,
|
|
1269
|
+
dryRun: Boolean(opts.dryRun),
|
|
1270
|
+
overrideCount: opts.configOverride?.length ?? 0,
|
|
1271
|
+
})) {
|
|
1272
|
+
console.log(chalk_1.default.dim(` ${item.label}: ${item.value}`));
|
|
1273
|
+
}
|
|
1274
|
+
console.log('');
|
|
1275
|
+
}
|
|
1078
1276
|
const outputFormat = globalOpts.output;
|
|
1079
1277
|
if (outputFormat === 'json') {
|
|
1080
1278
|
process.stdout.write(JSON.stringify(staleCases, null, 2) + '\n');
|
|
@@ -1139,7 +1337,7 @@ program
|
|
|
1139
1337
|
const globalOpts = program.opts();
|
|
1140
1338
|
try {
|
|
1141
1339
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1142
|
-
const config = (
|
|
1340
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1143
1341
|
if (opts.configOverride?.length)
|
|
1144
1342
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1145
1343
|
const configDir = path.dirname(configPath);
|
|
@@ -1149,6 +1347,23 @@ program
|
|
|
1149
1347
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
1150
1348
|
console.log('');
|
|
1151
1349
|
const report = await (0, engine_1.coverageReport)(config, configDir, { tags: opts.tags });
|
|
1350
|
+
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1351
|
+
if (config.toolSettings?.outputLevel === 'diagnostic') {
|
|
1352
|
+
printDiagnosticItems((0, cli_diagnostics_1.getCoverageDiagnosticItems)({
|
|
1353
|
+
localType: config.local.type,
|
|
1354
|
+
syncTargetMode: config.syncTarget?.mode ?? 'suite',
|
|
1355
|
+
tagExpression: opts.tags,
|
|
1356
|
+
totalLocalSpecs: report.totalLocalSpecs,
|
|
1357
|
+
linkedSpecs: report.linkedSpecs,
|
|
1358
|
+
unlinkedSpecs: report.unlinkedSpecs,
|
|
1359
|
+
storiesReferenced: report.storiesReferenced.length,
|
|
1360
|
+
storiesCovered: report.storiesCovered.length,
|
|
1361
|
+
storyPrefix: config.sync?.links?.find((link) => link.prefix === 'story')?.prefix ?? 'story',
|
|
1362
|
+
failBelow: threshold,
|
|
1363
|
+
overrideCount: opts.configOverride?.length ?? 0,
|
|
1364
|
+
}));
|
|
1365
|
+
console.log('');
|
|
1366
|
+
}
|
|
1152
1367
|
const outputFormat = globalOpts.output;
|
|
1153
1368
|
if (outputFormat === 'json') {
|
|
1154
1369
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1174,7 +1389,6 @@ program
|
|
|
1174
1389
|
console.log(chalk_1.default.dim('No @story: tags found — add @story:ID tags to specs to track story coverage.'));
|
|
1175
1390
|
}
|
|
1176
1391
|
}
|
|
1177
|
-
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1178
1392
|
if (threshold !== undefined && report.specLinkRate < threshold) {
|
|
1179
1393
|
console.error(chalk_1.default.red(`\nSpec link rate ${report.specLinkRate}% is below threshold ${threshold}%`));
|
|
1180
1394
|
process.exit(1);
|
|
@@ -1201,7 +1415,7 @@ program
|
|
|
1201
1415
|
const globalOpts = program.opts();
|
|
1202
1416
|
try {
|
|
1203
1417
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1204
|
-
const config = (
|
|
1418
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1205
1419
|
if (opts.configOverride?.length)
|
|
1206
1420
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1207
1421
|
const { trendReport, postTrendToWebhook } = await Promise.resolve().then(() => __importStar(require('./azure/test-runs')));
|
|
@@ -1215,6 +1429,24 @@ program
|
|
|
1215
1429
|
topN: parseInt(opts.top, 10),
|
|
1216
1430
|
runNameFilter: opts.runName,
|
|
1217
1431
|
});
|
|
1432
|
+
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1433
|
+
if (config.toolSettings?.outputLevel === 'diagnostic') {
|
|
1434
|
+
printDiagnosticItems((0, cli_diagnostics_1.getTrendDiagnosticItems)({
|
|
1435
|
+
days: parseInt(opts.days, 10),
|
|
1436
|
+
maxRuns: parseInt(opts.maxRuns, 10),
|
|
1437
|
+
topN: parseInt(opts.top, 10),
|
|
1438
|
+
runNameFilter: opts.runName,
|
|
1439
|
+
webhookType: opts.webhookUrl ? (opts.webhookType ?? 'slack') : undefined,
|
|
1440
|
+
failOnFlaky: Boolean(opts.failOnFlaky),
|
|
1441
|
+
failBelow: threshold,
|
|
1442
|
+
runsAnalyzed: report.runsAnalyzed,
|
|
1443
|
+
totalResults: report.totalResults,
|
|
1444
|
+
flakyCount: report.flakyTests.length,
|
|
1445
|
+
failingCount: report.topFailingTests.length,
|
|
1446
|
+
overrideCount: opts.configOverride?.length ?? 0,
|
|
1447
|
+
}));
|
|
1448
|
+
console.log('');
|
|
1449
|
+
}
|
|
1218
1450
|
const outputFormat = globalOpts.output;
|
|
1219
1451
|
if (outputFormat === 'json') {
|
|
1220
1452
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1257,7 +1489,6 @@ program
|
|
|
1257
1489
|
console.error(chalk_1.default.red(`\n${report.flakyTests.length} flaky test${report.flakyTests.length !== 1 ? 's' : ''} detected.`));
|
|
1258
1490
|
process.exit(1);
|
|
1259
1491
|
}
|
|
1260
|
-
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1261
1492
|
if (threshold !== undefined && report.overallPassRate < threshold) {
|
|
1262
1493
|
console.error(chalk_1.default.red(`\nOverall pass rate ${report.overallPassRate}% is below threshold ${threshold}%`));
|
|
1263
1494
|
process.exit(1);
|
|
@@ -1277,6 +1508,7 @@ program
|
|
|
1277
1508
|
.option('--update-only', 'Only update linked test cases during each watched push')
|
|
1278
1509
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression')
|
|
1279
1510
|
.option('--source-file <path>', 'Restrict watch and push to a specific local file (repeatable)', collect, [])
|
|
1511
|
+
.option('--include <pattern>', 'Restrict the operation to files matching a glob pattern relative to config dir (repeatable)', collect, [])
|
|
1280
1512
|
.option('--debounce <ms>', 'Debounce delay in milliseconds before running push (default: 800)', '800')
|
|
1281
1513
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
1282
1514
|
.option('--ai-provider <provider>', 'AI provider for test step generation')
|
|
@@ -1287,7 +1519,7 @@ program
|
|
|
1287
1519
|
const globalOpts = program.opts();
|
|
1288
1520
|
try {
|
|
1289
1521
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1290
|
-
const config = (
|
|
1522
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1291
1523
|
if (opts.configOverride?.length)
|
|
1292
1524
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1293
1525
|
(0, engine_1.validatePushModeOptions)(opts);
|
|
@@ -1310,6 +1542,8 @@ program
|
|
|
1310
1542
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
1311
1543
|
if (sourceFiles?.length)
|
|
1312
1544
|
console.log(chalk_1.default.dim(`Files: ${sourceFiles.join(', ')}`));
|
|
1545
|
+
if (opts.include?.length)
|
|
1546
|
+
console.log(chalk_1.default.dim(`Include: ${opts.include.join(', ')}`));
|
|
1313
1547
|
console.log('');
|
|
1314
1548
|
console.log(chalk_1.default.dim(`Watching ${configDir} for changes... (Ctrl+C to stop)`));
|
|
1315
1549
|
console.log('');
|
|
@@ -1332,6 +1566,7 @@ program
|
|
|
1332
1566
|
updateOnly: opts.updateOnly,
|
|
1333
1567
|
tags: opts.tags,
|
|
1334
1568
|
sourceFiles,
|
|
1569
|
+
includePatterns: opts.include?.length ? opts.include : undefined,
|
|
1335
1570
|
onProgress,
|
|
1336
1571
|
aiSummary,
|
|
1337
1572
|
});
|
|
@@ -1392,7 +1627,7 @@ program
|
|
|
1392
1627
|
const globalOpts = program.opts();
|
|
1393
1628
|
try {
|
|
1394
1629
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1395
|
-
const config = (
|
|
1630
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1396
1631
|
if (opts.configOverride?.length)
|
|
1397
1632
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1398
1633
|
if (!opts.hours && !opts.days) {
|