ado-sync 0.1.65 → 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/README.md +15 -15
- package/dist/__tests__/regressions.test.js +1133 -1
- package/dist/__tests__/regressions.test.js.map +1 -1
- package/dist/ai/summarizer.d.ts +2 -1
- package/dist/ai/summarizer.js +6 -1
- package/dist/ai/summarizer.js.map +1 -1
- package/dist/azure/test-cases.d.ts +11 -1
- package/dist/azure/test-cases.js +286 -43
- package/dist/azure/test-cases.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 +335 -23
- package/dist/cli.js.map +1 -1
- package/dist/config.js +194 -9
- 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/id-markers.d.ts +1 -0
- package/dist/id-markers.js +13 -0
- package/dist/id-markers.js.map +1 -1
- package/dist/sync/cache.d.ts +2 -0
- package/dist/sync/cache.js.map +1 -1
- package/dist/sync/engine.d.ts +29 -2
- package/dist/sync/engine.js +270 -41
- 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 +98 -2
- package/llms.txt +11 -11
- package/package.json +9 -1
- package/docs/advanced.md +0 -989
- package/docs/agent-setup.md +0 -204
- package/docs/capability-roadmap.md +0 -280
- package/docs/cli.md +0 -614
- package/docs/configuration.md +0 -322
- package/docs/examples/csharp-mstest-local-llm.yaml +0 -35
- package/docs/examples/csharp-mstest.yaml +0 -21
- package/docs/examples/csharp-nunit.yaml +0 -21
- package/docs/examples/csharp-specflow.yaml +0 -16
- package/docs/examples/cypress.yaml +0 -21
- package/docs/examples/detox-react-native.yaml +0 -21
- package/docs/examples/espresso-android.yaml +0 -21
- package/docs/examples/flutter-dart.yaml +0 -21
- package/docs/examples/java-junit.yaml +0 -21
- package/docs/examples/java-testng.yaml +0 -21
- package/docs/examples/js-jasmine-wdio.yaml +0 -21
- package/docs/examples/js-jest.yaml +0 -21
- package/docs/examples/playwright-js.yaml +0 -21
- package/docs/examples/playwright-ts.yaml +0 -21
- package/docs/examples/puppeteer.yaml +0 -21
- package/docs/examples/python-pytest.yaml +0 -21
- package/docs/examples/robot-framework.yaml +0 -19
- package/docs/examples/testcafe.yaml +0 -21
- package/docs/examples/xcuitest-ios.yaml +0 -21
- package/docs/mcp-server.md +0 -312
- package/docs/publish-test-results.md +0 -947
- package/docs/spec-formats.md +0 -1357
- package/docs/troubleshooting.md +0 -101
- package/docs/vscode-extension.md +0 -139
- package/docs/work-item-links.md +0 -115
- package/docs/workflows.md +0 -457
- package/mkdocs.yml +0 -40
- package/requirements-docs.txt +0 -4
- package/scripts/build_site.sh +0 -6
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)
|
|
@@ -101,6 +114,17 @@ function handleError(err) {
|
|
|
101
114
|
function collect(value, previous) {
|
|
102
115
|
return [...previous, value];
|
|
103
116
|
}
|
|
117
|
+
function normalizeSourceFilesForCli(sourceFiles, configDir) {
|
|
118
|
+
if (!sourceFiles?.length)
|
|
119
|
+
return undefined;
|
|
120
|
+
return sourceFiles.map((filePath) => path.normalize(path.resolve(configDir, filePath)));
|
|
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
|
+
}
|
|
104
128
|
// ─── AI summary helper ────────────────────────────────────────────────────────
|
|
105
129
|
/**
|
|
106
130
|
* Build AiSummaryOpts from parsed CLI opts, falling back to config file values.
|
|
@@ -243,12 +267,97 @@ async function runInitWizard(isYaml) {
|
|
|
243
267
|
}
|
|
244
268
|
return JSON.stringify(cfg, null, 2) + '\n';
|
|
245
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
|
+
});
|
|
246
350
|
// ─── push ─────────────────────────────────────────────────────────────────────
|
|
247
351
|
program
|
|
248
352
|
.command('push')
|
|
249
353
|
.description('Push local test specs to Azure DevOps (create or update test cases)')
|
|
250
354
|
.option('--dry-run', 'Show what would change without making any modifications')
|
|
355
|
+
.option('--create-only', 'Only create test cases for unlinked local specs; skip linked items and removed-case detection')
|
|
356
|
+
.option('--link-only', 'Only restore local IDs by linking unlinked specs to uniquely matching existing test cases in the current scope')
|
|
357
|
+
.option('--update-only', 'Only update linked test cases; skip unlinked specs and removed-case detection')
|
|
251
358
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
|
|
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, [])
|
|
252
361
|
.option('--config-override <path=value>', 'Override a config value (repeatable, e.g. --config-override sync.tagPrefix=mytag)', collect, [])
|
|
253
362
|
.option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
|
|
254
363
|
.option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
|
|
@@ -260,9 +369,10 @@ program
|
|
|
260
369
|
const globalOpts = program.opts();
|
|
261
370
|
try {
|
|
262
371
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
263
|
-
const config = (
|
|
372
|
+
const config = loadConfigWithOverrides(configPath);
|
|
264
373
|
if (opts.configOverride?.length)
|
|
265
374
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
375
|
+
(0, engine_1.validatePushModeOptions)(opts);
|
|
266
376
|
const configDir = path.dirname(configPath);
|
|
267
377
|
console.log(chalk_1.default.bold('ado-sync push'));
|
|
268
378
|
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
@@ -270,8 +380,18 @@ program
|
|
|
270
380
|
console.log(chalk_1.default.dim(`Plan: ${config.testPlan.id}`));
|
|
271
381
|
if (opts.dryRun)
|
|
272
382
|
console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
|
|
383
|
+
if (opts.createOnly)
|
|
384
|
+
console.log(chalk_1.default.dim('Mode: create-only'));
|
|
385
|
+
if (opts.linkOnly)
|
|
386
|
+
console.log(chalk_1.default.dim('Mode: link-only'));
|
|
387
|
+
if (opts.updateOnly)
|
|
388
|
+
console.log(chalk_1.default.dim('Mode: update-only'));
|
|
273
389
|
if (opts.tags)
|
|
274
390
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
391
|
+
if (opts.sourceFile?.length)
|
|
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(', ')}`));
|
|
275
395
|
if (opts.configOverride?.length)
|
|
276
396
|
console.log(chalk_1.default.dim(`Overrides: ${opts.configOverride.join(', ')}`));
|
|
277
397
|
console.log('');
|
|
@@ -280,7 +400,7 @@ program
|
|
|
280
400
|
const outputFormat = program.opts().output;
|
|
281
401
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
282
402
|
const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
|
|
283
|
-
const results = await (0, engine_1.push)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, 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 });
|
|
284
404
|
if (isTTY && outputFormat !== 'json')
|
|
285
405
|
clearProgressLine();
|
|
286
406
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -295,12 +415,14 @@ program
|
|
|
295
415
|
.description('Pull updates from Azure DevOps into local spec files')
|
|
296
416
|
.option('--dry-run', 'Show what would change without modifying local files')
|
|
297
417
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression (e.g. "@smoke and not @wip")')
|
|
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, [])
|
|
298
420
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
299
421
|
.action(async (opts) => {
|
|
300
422
|
const globalOpts = program.opts();
|
|
301
423
|
try {
|
|
302
424
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
303
|
-
const config = (
|
|
425
|
+
const config = loadConfigWithOverrides(configPath);
|
|
304
426
|
if (opts.configOverride?.length)
|
|
305
427
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
306
428
|
const configDir = path.dirname(configPath);
|
|
@@ -310,11 +432,15 @@ program
|
|
|
310
432
|
console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
|
|
311
433
|
if (opts.tags)
|
|
312
434
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
435
|
+
if (opts.sourceFile?.length)
|
|
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(', ')}`));
|
|
313
439
|
console.log('');
|
|
314
440
|
const isTTY = process.stdout.isTTY ?? false;
|
|
315
441
|
const outputFormat = program.opts().output;
|
|
316
442
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
317
|
-
const results = await (0, engine_1.pull)(config, configDir, { dryRun: opts.dryRun, tags: opts.tags, 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 });
|
|
318
444
|
if (isTTY && outputFormat !== 'json')
|
|
319
445
|
clearProgressLine();
|
|
320
446
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -328,6 +454,8 @@ program
|
|
|
328
454
|
.command('status')
|
|
329
455
|
.description('Show diff between local specs and Azure DevOps without making changes')
|
|
330
456
|
.option('--tags <expression>', 'Only check scenarios matching this tag expression')
|
|
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, [])
|
|
331
459
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
332
460
|
.option('--ai-provider <provider>', 'AI provider: local (node-llama-cpp), heuristic, ollama, docker, openai, anthropic, huggingface, bedrock, azureai, none (disable)')
|
|
333
461
|
.option('--ai-model <model>', 'local: GGUF path; ollama/docker: model tag; openai/anthropic/huggingface/bedrock/azureai: model name or id')
|
|
@@ -339,7 +467,7 @@ program
|
|
|
339
467
|
const globalOpts = program.opts();
|
|
340
468
|
try {
|
|
341
469
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
342
|
-
const config = (
|
|
470
|
+
const config = loadConfigWithOverrides(configPath);
|
|
343
471
|
if (opts.configOverride?.length)
|
|
344
472
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
345
473
|
const configDir = path.dirname(configPath);
|
|
@@ -347,13 +475,17 @@ program
|
|
|
347
475
|
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
348
476
|
if (opts.tags)
|
|
349
477
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
478
|
+
if (opts.sourceFile?.length)
|
|
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(', ')}`));
|
|
350
482
|
console.log('');
|
|
351
483
|
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
352
484
|
const isTTY = process.stdout.isTTY ?? false;
|
|
353
485
|
const outputFormat = program.opts().output;
|
|
354
486
|
const onProgress = outputFormat === 'json' ? undefined : createProgressCallback(isTTY);
|
|
355
487
|
const onAiProgress = outputFormat === 'json' ? undefined : createAiProgressCallback(isTTY);
|
|
356
|
-
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, 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 });
|
|
357
489
|
if (isTTY && outputFormat !== 'json')
|
|
358
490
|
clearProgressLine();
|
|
359
491
|
printResults(results, config.toolSettings?.outputLevel, outputFormat);
|
|
@@ -392,7 +524,7 @@ program
|
|
|
392
524
|
const globalOpts = program.opts();
|
|
393
525
|
try {
|
|
394
526
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
395
|
-
const config = (
|
|
527
|
+
const config = loadConfigWithOverrides(configPath);
|
|
396
528
|
const cliPublishOverrides = [
|
|
397
529
|
...(opts.testConfiguration ? [/^\d+$/.test(opts.testConfiguration)
|
|
398
530
|
? `publishTestResults.testConfiguration.id=${opts.testConfiguration}`
|
|
@@ -452,6 +584,28 @@ program
|
|
|
452
584
|
console.log(chalk_1.default.dim(`Run ID: ${result.runId}`));
|
|
453
585
|
console.log(chalk_1.default.dim(`URL: ${result.runUrl}`));
|
|
454
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
|
+
}
|
|
455
609
|
if (result.issuesSummary) {
|
|
456
610
|
const s = result.issuesSummary;
|
|
457
611
|
console.log('');
|
|
@@ -476,6 +630,7 @@ program
|
|
|
476
630
|
const ACTION_SYMBOL = {
|
|
477
631
|
created: chalk_1.default.green('+'),
|
|
478
632
|
updated: chalk_1.default.blue('~'),
|
|
633
|
+
linked: chalk_1.default.green('↔'),
|
|
479
634
|
pulled: chalk_1.default.cyan('↓'),
|
|
480
635
|
skipped: chalk_1.default.dim('='),
|
|
481
636
|
conflict: chalk_1.default.yellow('!'),
|
|
@@ -536,8 +691,10 @@ function printResults(results, outputLevel, outputFormat) {
|
|
|
536
691
|
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
537
692
|
return;
|
|
538
693
|
}
|
|
539
|
-
const counts = { created: 0, updated: 0, pulled: 0, skipped: 0, conflict: 0, removed: 0, error: 0 };
|
|
694
|
+
const counts = { created: 0, updated: 0, linked: 0, pulled: 0, skipped: 0, conflict: 0, removed: 0, error: 0 };
|
|
540
695
|
const quiet = outputLevel === 'quiet';
|
|
696
|
+
const verbose = outputLevel === 'verbose';
|
|
697
|
+
const diagnostic = outputLevel === 'diagnostic';
|
|
541
698
|
for (const r of results) {
|
|
542
699
|
counts[r.action]++;
|
|
543
700
|
// In quiet mode, only print actionable results (skip the '= skipped' lines)
|
|
@@ -555,6 +712,9 @@ function printResults(results, outputLevel, outputFormat) {
|
|
|
555
712
|
case 'updated':
|
|
556
713
|
console.log(`${chalk_1.default.blue('~')} ${filePart} ${r.title}${idStr}`);
|
|
557
714
|
break;
|
|
715
|
+
case 'linked':
|
|
716
|
+
console.log(`${chalk_1.default.green('↔')} ${filePart} ${r.title}${idStr}${detailStr}`);
|
|
717
|
+
break;
|
|
558
718
|
case 'pulled':
|
|
559
719
|
console.log(`${chalk_1.default.cyan('↓')} ${filePart} ${r.title}${idStr}${detailStr}`);
|
|
560
720
|
break;
|
|
@@ -571,11 +731,24 @@ function printResults(results, outputLevel, outputFormat) {
|
|
|
571
731
|
console.log(`${chalk_1.default.red('✗')} ${filePart} ${r.title}${idStr}${chalk_1.default.red(detailStr)}`);
|
|
572
732
|
break;
|
|
573
733
|
}
|
|
734
|
+
if (r.targetSuitePath && (r.action === 'created' || r.action === 'updated' || r.action === 'conflict' || verbose || diagnostic)) {
|
|
735
|
+
console.log(chalk_1.default.dim(` target suite: ${r.targetSuitePath}`));
|
|
736
|
+
}
|
|
737
|
+
if (r.previousSuitePath && (r.action === 'updated' || r.action === 'conflict' || diagnostic)) {
|
|
738
|
+
console.log(chalk_1.default.dim(` previous suite: ${r.previousSuitePath}`));
|
|
739
|
+
}
|
|
740
|
+
if (diagnostic && r.changedFields?.length) {
|
|
741
|
+
console.log(chalk_1.default.dim(` changed fields: ${r.changedFields.join(', ')}`));
|
|
742
|
+
}
|
|
743
|
+
if (diagnostic && r.detail && r.action !== 'linked' && r.action !== 'pulled' && r.action !== 'error') {
|
|
744
|
+
console.log(chalk_1.default.dim(` detail: ${r.detail}`));
|
|
745
|
+
}
|
|
574
746
|
}
|
|
575
747
|
console.log('');
|
|
576
748
|
const summary = [
|
|
577
749
|
counts.created && chalk_1.default.green(`${counts.created} created`),
|
|
578
750
|
counts.updated && chalk_1.default.blue(`${counts.updated} updated`),
|
|
751
|
+
counts.linked && chalk_1.default.green(`${counts.linked} linked`),
|
|
579
752
|
counts.pulled && chalk_1.default.cyan(`${counts.pulled} pulled`),
|
|
580
753
|
counts.skipped && chalk_1.default.dim(`${counts.skipped} skipped`),
|
|
581
754
|
counts.conflict && chalk_1.default.yellow(`${counts.conflict} conflicts`),
|
|
@@ -597,10 +770,19 @@ program
|
|
|
597
770
|
console.log('');
|
|
598
771
|
// 1. Load config
|
|
599
772
|
let config;
|
|
773
|
+
let validateDiagnostics;
|
|
600
774
|
try {
|
|
601
775
|
config = (0, config_1.loadConfig)(configPath);
|
|
602
776
|
if (opts.configOverride?.length)
|
|
603
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
|
+
};
|
|
604
786
|
console.log(chalk_1.default.green(' ✓ Config is valid'));
|
|
605
787
|
}
|
|
606
788
|
catch (err) {
|
|
@@ -616,6 +798,10 @@ program
|
|
|
616
798
|
}
|
|
617
799
|
catch (err) {
|
|
618
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
|
+
}
|
|
619
805
|
process.exit(1);
|
|
620
806
|
}
|
|
621
807
|
// 3. Verify project
|
|
@@ -628,6 +814,10 @@ program
|
|
|
628
814
|
}
|
|
629
815
|
catch (err) {
|
|
630
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
|
+
}
|
|
631
821
|
process.exit(1);
|
|
632
822
|
}
|
|
633
823
|
// 4. Verify test plan(s)
|
|
@@ -644,6 +834,10 @@ program
|
|
|
644
834
|
allPlansOk = false;
|
|
645
835
|
}
|
|
646
836
|
}
|
|
837
|
+
if (config.toolSettings?.outputLevel === 'diagnostic' && validateDiagnostics) {
|
|
838
|
+
console.log('');
|
|
839
|
+
printDiagnosticItems((0, cli_diagnostics_1.getValidateDiagnosticItems)(validateDiagnostics));
|
|
840
|
+
}
|
|
647
841
|
if (!allPlansOk)
|
|
648
842
|
process.exit(1);
|
|
649
843
|
console.log('');
|
|
@@ -654,6 +848,8 @@ program
|
|
|
654
848
|
.command('diff')
|
|
655
849
|
.description('Show field-level diff between local specs and Azure DevOps')
|
|
656
850
|
.option('--tags <expression>', 'Only check scenarios matching this tag expression')
|
|
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, [])
|
|
657
853
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
658
854
|
.option('--format <fmt>', 'Output format: text (default) or json')
|
|
659
855
|
.option('--fail-on-drift', 'Exit with code 1 when any differences are found (useful as a CI quality gate)')
|
|
@@ -665,7 +861,7 @@ program
|
|
|
665
861
|
const globalOpts = program.opts();
|
|
666
862
|
try {
|
|
667
863
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
668
|
-
const config = (
|
|
864
|
+
const config = loadConfigWithOverrides(configPath);
|
|
669
865
|
if (opts.configOverride?.length)
|
|
670
866
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
671
867
|
const configDir = path.dirname(configPath);
|
|
@@ -673,19 +869,25 @@ program
|
|
|
673
869
|
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
674
870
|
if (opts.tags)
|
|
675
871
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
872
|
+
if (opts.sourceFile?.length)
|
|
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(', ')}`));
|
|
676
876
|
console.log('');
|
|
677
877
|
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
678
|
-
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, 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 });
|
|
679
879
|
// --format json or global --output json
|
|
680
880
|
const outputFormat = opts.format ?? program.opts().output;
|
|
681
881
|
if (outputFormat === 'json') {
|
|
682
882
|
const diffs = results
|
|
683
|
-
.filter((r) => r.action === 'updated' || r.action === 'conflict')
|
|
883
|
+
.filter((r) => r.action === 'created' || r.action === 'updated' || r.action === 'conflict')
|
|
684
884
|
.map((r) => ({
|
|
685
885
|
action: r.action,
|
|
686
886
|
azureId: r.azureId,
|
|
687
887
|
title: r.title,
|
|
688
888
|
filePath: r.filePath ? path.relative(process.cwd(), r.filePath) : '',
|
|
889
|
+
targetSuitePath: r.targetSuitePath,
|
|
890
|
+
previousSuitePath: r.previousSuitePath,
|
|
689
891
|
changedFields: r.changedFields ?? [],
|
|
690
892
|
diffDetail: r.diffDetail ?? [],
|
|
691
893
|
}));
|
|
@@ -696,13 +898,19 @@ program
|
|
|
696
898
|
}
|
|
697
899
|
let anyDiff = false;
|
|
698
900
|
for (const r of results) {
|
|
699
|
-
if (r.action !== 'updated' && r.action !== 'conflict')
|
|
901
|
+
if (r.action !== 'created' && r.action !== 'updated' && r.action !== 'conflict')
|
|
700
902
|
continue;
|
|
701
903
|
anyDiff = true;
|
|
702
904
|
const idStr = r.azureId ? chalk_1.default.dim(` [#${r.azureId}]`) : '';
|
|
703
905
|
const filePart = r.filePath ? chalk_1.default.dim(path.relative(process.cwd(), r.filePath) + ':') : '';
|
|
704
|
-
const symbol = r.action === 'conflict' ? chalk_1.default.yellow('!') : chalk_1.default.blue('~');
|
|
906
|
+
const symbol = r.action === 'conflict' ? chalk_1.default.yellow('!') : r.action === 'created' ? chalk_1.default.green('+') : chalk_1.default.blue('~');
|
|
705
907
|
console.log(`${symbol} ${filePart} ${r.title}${idStr}`);
|
|
908
|
+
if (r.targetSuitePath) {
|
|
909
|
+
console.log(chalk_1.default.dim(` target suite: ${r.targetSuitePath}`));
|
|
910
|
+
}
|
|
911
|
+
if (r.previousSuitePath) {
|
|
912
|
+
console.log(chalk_1.default.dim(` previous suite: ${r.previousSuitePath}`));
|
|
913
|
+
}
|
|
706
914
|
if (r.changedFields?.length) {
|
|
707
915
|
console.log(chalk_1.default.dim(` changed fields: ${r.changedFields.join(', ')}`));
|
|
708
916
|
}
|
|
@@ -751,7 +959,7 @@ program
|
|
|
751
959
|
const globalOpts = program.opts();
|
|
752
960
|
try {
|
|
753
961
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
754
|
-
const config = (
|
|
962
|
+
const config = loadConfigWithOverrides(configPath);
|
|
755
963
|
if (opts.configOverride?.length)
|
|
756
964
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
757
965
|
const configDir = path.dirname(configPath);
|
|
@@ -876,7 +1084,7 @@ program
|
|
|
876
1084
|
const globalOpts = program.opts();
|
|
877
1085
|
try {
|
|
878
1086
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
879
|
-
const config = (
|
|
1087
|
+
const config = loadConfigWithOverrides(configPath);
|
|
880
1088
|
if (opts.configOverride?.length)
|
|
881
1089
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
882
1090
|
const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
|
|
@@ -926,23 +1134,31 @@ program
|
|
|
926
1134
|
const globalOpts = program.opts();
|
|
927
1135
|
try {
|
|
928
1136
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
929
|
-
const config = (
|
|
1137
|
+
const config = loadConfigWithOverrides(configPath);
|
|
930
1138
|
if (opts.configOverride?.length)
|
|
931
1139
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
932
1140
|
const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
|
|
933
1141
|
const { acGate, getWorkItemsByQuery, getWorkItemsByAreaPath } = await Promise.resolve().then(() => __importStar(require('./azure/work-items')));
|
|
934
1142
|
const client = await AzureClient.create(config);
|
|
935
1143
|
let storyIds = [];
|
|
1144
|
+
let selectorMode = 'default-states';
|
|
1145
|
+
let selectorValue = opts.states ?? 'Active,Resolved,Closed';
|
|
936
1146
|
if (opts.query) {
|
|
937
1147
|
const stories = await getWorkItemsByQuery(client, config.project, opts.query);
|
|
938
1148
|
storyIds = stories.map((s) => s.id);
|
|
1149
|
+
selectorMode = 'query';
|
|
1150
|
+
selectorValue = opts.query;
|
|
939
1151
|
}
|
|
940
1152
|
else if (opts.areaPath) {
|
|
941
1153
|
const stories = await getWorkItemsByAreaPath(client, config.project, opts.areaPath);
|
|
942
1154
|
storyIds = stories.map((s) => s.id);
|
|
1155
|
+
selectorMode = 'area-path';
|
|
1156
|
+
selectorValue = opts.areaPath;
|
|
943
1157
|
}
|
|
944
1158
|
else if (opts.storyIds) {
|
|
945
1159
|
storyIds = opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean);
|
|
1160
|
+
selectorMode = 'story-ids';
|
|
1161
|
+
selectorValue = opts.storyIds;
|
|
946
1162
|
}
|
|
947
1163
|
else {
|
|
948
1164
|
// Default: all active stories in project
|
|
@@ -951,6 +1167,7 @@ program
|
|
|
951
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]`;
|
|
952
1168
|
const stories = await getWorkItemsByQuery(client, config.project, wiql);
|
|
953
1169
|
storyIds = stories.map((s) => s.id);
|
|
1170
|
+
selectorValue = states;
|
|
954
1171
|
}
|
|
955
1172
|
if (storyIds.length === 0) {
|
|
956
1173
|
console.log(chalk_1.default.yellow('No stories found to validate.'));
|
|
@@ -961,6 +1178,23 @@ program
|
|
|
961
1178
|
console.log(chalk_1.default.dim(`Stories: ${storyIds.length} to validate`));
|
|
962
1179
|
console.log('');
|
|
963
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
|
+
}
|
|
964
1198
|
const outputFormat = globalOpts.output;
|
|
965
1199
|
if (outputFormat === 'json') {
|
|
966
1200
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1001,6 +1235,7 @@ program
|
|
|
1001
1235
|
.command('stale')
|
|
1002
1236
|
.description('List Azure DevOps Test Cases that have no corresponding local spec')
|
|
1003
1237
|
.option('--tags <expression>', 'Only consider local specs matching this tag expression')
|
|
1238
|
+
.option('--suites', 'Report orphaned suite memberships (affected by stalenessPolicy config)')
|
|
1004
1239
|
.option('--retire', 'Automatically transition stale Test Cases to Closed state and tag ado-sync:retired')
|
|
1005
1240
|
.option('--retire-state <state>', 'Target state when retiring (default: Closed)', 'Closed')
|
|
1006
1241
|
.option('--dry-run', 'Show what --retire would do without making changes')
|
|
@@ -1009,7 +1244,7 @@ program
|
|
|
1009
1244
|
const globalOpts = program.opts();
|
|
1010
1245
|
try {
|
|
1011
1246
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1012
|
-
const config = (
|
|
1247
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1013
1248
|
if (opts.configOverride?.length)
|
|
1014
1249
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1015
1250
|
const configDir = path.dirname(configPath);
|
|
@@ -1021,6 +1256,23 @@ program
|
|
|
1021
1256
|
console.log(chalk_1.default.dim(`Retire: will transition to "${opts.retireState}"${opts.dryRun ? ' (dry-run)' : ''}`));
|
|
1022
1257
|
console.log('');
|
|
1023
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
|
+
}
|
|
1024
1276
|
const outputFormat = globalOpts.output;
|
|
1025
1277
|
if (outputFormat === 'json') {
|
|
1026
1278
|
process.stdout.write(JSON.stringify(staleCases, null, 2) + '\n');
|
|
@@ -1085,7 +1337,7 @@ program
|
|
|
1085
1337
|
const globalOpts = program.opts();
|
|
1086
1338
|
try {
|
|
1087
1339
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1088
|
-
const config = (
|
|
1340
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1089
1341
|
if (opts.configOverride?.length)
|
|
1090
1342
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1091
1343
|
const configDir = path.dirname(configPath);
|
|
@@ -1095,6 +1347,23 @@ program
|
|
|
1095
1347
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
1096
1348
|
console.log('');
|
|
1097
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
|
+
}
|
|
1098
1367
|
const outputFormat = globalOpts.output;
|
|
1099
1368
|
if (outputFormat === 'json') {
|
|
1100
1369
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1120,7 +1389,6 @@ program
|
|
|
1120
1389
|
console.log(chalk_1.default.dim('No @story: tags found — add @story:ID tags to specs to track story coverage.'));
|
|
1121
1390
|
}
|
|
1122
1391
|
}
|
|
1123
|
-
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1124
1392
|
if (threshold !== undefined && report.specLinkRate < threshold) {
|
|
1125
1393
|
console.error(chalk_1.default.red(`\nSpec link rate ${report.specLinkRate}% is below threshold ${threshold}%`));
|
|
1126
1394
|
process.exit(1);
|
|
@@ -1147,7 +1415,7 @@ program
|
|
|
1147
1415
|
const globalOpts = program.opts();
|
|
1148
1416
|
try {
|
|
1149
1417
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1150
|
-
const config = (
|
|
1418
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1151
1419
|
if (opts.configOverride?.length)
|
|
1152
1420
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1153
1421
|
const { trendReport, postTrendToWebhook } = await Promise.resolve().then(() => __importStar(require('./azure/test-runs')));
|
|
@@ -1161,6 +1429,24 @@ program
|
|
|
1161
1429
|
topN: parseInt(opts.top, 10),
|
|
1162
1430
|
runNameFilter: opts.runName,
|
|
1163
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
|
+
}
|
|
1164
1450
|
const outputFormat = globalOpts.output;
|
|
1165
1451
|
if (outputFormat === 'json') {
|
|
1166
1452
|
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
@@ -1203,7 +1489,6 @@ program
|
|
|
1203
1489
|
console.error(chalk_1.default.red(`\n${report.flakyTests.length} flaky test${report.flakyTests.length !== 1 ? 's' : ''} detected.`));
|
|
1204
1490
|
process.exit(1);
|
|
1205
1491
|
}
|
|
1206
|
-
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
1207
1492
|
if (threshold !== undefined && report.overallPassRate < threshold) {
|
|
1208
1493
|
console.error(chalk_1.default.red(`\nOverall pass rate ${report.overallPassRate}% is below threshold ${threshold}%`));
|
|
1209
1494
|
process.exit(1);
|
|
@@ -1218,7 +1503,12 @@ program
|
|
|
1218
1503
|
.command('watch')
|
|
1219
1504
|
.description('Watch local spec files for changes and auto-push on save')
|
|
1220
1505
|
.option('--dry-run', 'Show what would change without making any modifications')
|
|
1506
|
+
.option('--create-only', 'Only create test cases for unlinked local specs during each watched push')
|
|
1507
|
+
.option('--link-only', 'Only restore local IDs during each watched push')
|
|
1508
|
+
.option('--update-only', 'Only update linked test cases during each watched push')
|
|
1221
1509
|
.option('--tags <expression>', 'Only sync scenarios matching this tag expression')
|
|
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, [])
|
|
1222
1512
|
.option('--debounce <ms>', 'Debounce delay in milliseconds before running push (default: 800)', '800')
|
|
1223
1513
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
1224
1514
|
.option('--ai-provider <provider>', 'AI provider for test step generation')
|
|
@@ -1229,10 +1519,12 @@ program
|
|
|
1229
1519
|
const globalOpts = program.opts();
|
|
1230
1520
|
try {
|
|
1231
1521
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1232
|
-
const config = (
|
|
1522
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1233
1523
|
if (opts.configOverride?.length)
|
|
1234
1524
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1525
|
+
(0, engine_1.validatePushModeOptions)(opts);
|
|
1235
1526
|
const configDir = path.dirname(configPath);
|
|
1527
|
+
const sourceFiles = normalizeSourceFilesForCli(opts.sourceFile, configDir);
|
|
1236
1528
|
const debounceMs = parseInt(opts.debounce ?? '800', 10);
|
|
1237
1529
|
console.log(chalk_1.default.bold('ado-sync watch'));
|
|
1238
1530
|
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
@@ -1240,8 +1532,18 @@ program
|
|
|
1240
1532
|
console.log(chalk_1.default.dim(`Plan: ${config.testPlan.id}`));
|
|
1241
1533
|
if (opts.dryRun)
|
|
1242
1534
|
console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
|
|
1535
|
+
if (opts.createOnly)
|
|
1536
|
+
console.log(chalk_1.default.dim('Mode: create-only'));
|
|
1537
|
+
if (opts.linkOnly)
|
|
1538
|
+
console.log(chalk_1.default.dim('Mode: link-only'));
|
|
1539
|
+
if (opts.updateOnly)
|
|
1540
|
+
console.log(chalk_1.default.dim('Mode: update-only'));
|
|
1243
1541
|
if (opts.tags)
|
|
1244
1542
|
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
1543
|
+
if (sourceFiles?.length)
|
|
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(', ')}`));
|
|
1245
1547
|
console.log('');
|
|
1246
1548
|
console.log(chalk_1.default.dim(`Watching ${configDir} for changes... (Ctrl+C to stop)`));
|
|
1247
1549
|
console.log('');
|
|
@@ -1259,7 +1561,12 @@ program
|
|
|
1259
1561
|
const onProgress = createProgressCallback(isTTY);
|
|
1260
1562
|
const results = await (0, engine_1.push)(config, configDir, {
|
|
1261
1563
|
dryRun: opts.dryRun,
|
|
1564
|
+
createOnly: opts.createOnly,
|
|
1565
|
+
linkOnly: opts.linkOnly,
|
|
1566
|
+
updateOnly: opts.updateOnly,
|
|
1262
1567
|
tags: opts.tags,
|
|
1568
|
+
sourceFiles,
|
|
1569
|
+
includePatterns: opts.include?.length ? opts.include : undefined,
|
|
1263
1570
|
onProgress,
|
|
1264
1571
|
aiSummary,
|
|
1265
1572
|
});
|
|
@@ -1287,6 +1594,11 @@ program
|
|
|
1287
1594
|
const skipPatterns = ['.ado-sync-cache.json', 'ado-sync.json', 'ado-sync.yml', 'node_modules'];
|
|
1288
1595
|
if (skipPatterns.some((p) => filename.includes(p)))
|
|
1289
1596
|
return;
|
|
1597
|
+
if (sourceFiles?.length) {
|
|
1598
|
+
const changedFilePath = path.normalize(path.resolve(configDir, filename));
|
|
1599
|
+
if (!sourceFiles.includes(changedFilePath))
|
|
1600
|
+
return;
|
|
1601
|
+
}
|
|
1290
1602
|
onChange();
|
|
1291
1603
|
});
|
|
1292
1604
|
// Keep process alive
|
|
@@ -1315,7 +1627,7 @@ program
|
|
|
1315
1627
|
const globalOpts = program.opts();
|
|
1316
1628
|
try {
|
|
1317
1629
|
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
1318
|
-
const config = (
|
|
1630
|
+
const config = loadConfigWithOverrides(configPath);
|
|
1319
1631
|
if (opts.configOverride?.length)
|
|
1320
1632
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
1321
1633
|
if (!opts.hours && !opts.days) {
|