ado-sync 0.1.47 → 0.1.49
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/.next-steps.md +179 -0
- package/README.md +13 -0
- package/dist/ai/summarizer.js +6 -3
- package/dist/ai/summarizer.js.map +1 -1
- package/dist/azure/test-cases.js +28 -10
- package/dist/azure/test-cases.js.map +1 -1
- package/dist/azure/work-items.d.ts +28 -0
- package/dist/azure/work-items.js +96 -0
- package/dist/azure/work-items.js.map +1 -1
- package/dist/cli.js +362 -25
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +0 -5
- package/dist/config.js +17 -3
- package/dist/config.js.map +1 -1
- package/dist/issues/ado-bugs.d.ts +23 -0
- package/dist/issues/ado-bugs.js +59 -0
- package/dist/issues/ado-bugs.js.map +1 -0
- package/dist/issues/create-issues.d.ts +32 -0
- package/dist/issues/create-issues.js +236 -0
- package/dist/issues/create-issues.js.map +1 -0
- package/dist/issues/github.d.ts +22 -0
- package/dist/issues/github.js +95 -0
- package/dist/issues/github.js.map +1 -0
- package/dist/mcp-server.d.ts +3 -0
- package/dist/mcp-server.js +154 -20
- package/dist/mcp-server.js.map +1 -1
- package/dist/parsers/gherkin.js +2 -1
- package/dist/parsers/gherkin.js.map +1 -1
- package/dist/sync/engine.d.ts +41 -0
- package/dist/sync/engine.js +176 -10
- package/dist/sync/engine.js.map +1 -1
- package/dist/sync/manifest.d.ts +69 -0
- package/dist/sync/manifest.js +197 -0
- package/dist/sync/manifest.js.map +1 -0
- package/dist/sync/publish-results.d.ts +8 -1
- package/dist/sync/publish-results.js +167 -5
- package/dist/sync/publish-results.js.map +1 -1
- package/dist/sync/writeback.js +41 -20
- package/dist/sync/writeback.js.map +1 -1
- package/dist/types.d.ts +53 -0
- package/docs/mcp-server.md +131 -29
- package/docs/publish-test-results.md +136 -2
- package/docs/vscode-extension.md +139 -0
- package/llms.txt +28 -3
- package/package.json +2 -2
- package/.cucumber/.ado-sync-state.json +0 -282
- package/.cucumber/ado-sync.yaml +0 -21
- package/.cucumber/features/cart.feature +0 -62
- package/.cucumber/features/checkout.feature +0 -100
- package/.cucumber/features/homepage.feature +0 -7
- package/.cucumber/features/inventory.feature +0 -59
- package/.cucumber/features/login.feature +0 -74
package/dist/cli.js
CHANGED
|
@@ -37,15 +37,14 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
38
38
|
};
|
|
39
39
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
-
// Suppress the url.parse() deprecation warning emitted by azure-devops-node-api
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
};
|
|
40
|
+
// Suppress the url.parse() deprecation warning (DEP0169) emitted by azure-devops-node-api
|
|
41
|
+
// internals. Cannot be fixed on our side; use the warning event listener instead of
|
|
42
|
+
// monkey-patching process.emit.
|
|
43
|
+
process.on('warning', (warning) => {
|
|
44
|
+
if (warning.code === 'DEP0169')
|
|
45
|
+
return;
|
|
46
|
+
process.stderr.write(`[node:warning] ${warning.name}: ${warning.message}\n`);
|
|
47
|
+
});
|
|
49
48
|
require("dotenv/config");
|
|
50
49
|
const chalk_1 = __importDefault(require("chalk"));
|
|
51
50
|
const commander_1 = require("commander");
|
|
@@ -58,6 +57,7 @@ const client_1 = require("./azure/client");
|
|
|
58
57
|
const config_1 = require("./config");
|
|
59
58
|
const engine_1 = require("./sync/engine");
|
|
60
59
|
const generate_1 = require("./sync/generate");
|
|
60
|
+
const manifest_1 = require("./sync/manifest");
|
|
61
61
|
const publish_results_1 = require("./sync/publish-results");
|
|
62
62
|
// ─── CLI definition ───────────────────────────────────────────────────────────
|
|
63
63
|
const program = new commander_1.Command();
|
|
@@ -69,24 +69,31 @@ program
|
|
|
69
69
|
program.option('-c, --config <path>', 'Path to config file (default: ado-sync.json)');
|
|
70
70
|
program.option('--output <format>', 'Output format: text (default) or json');
|
|
71
71
|
// ─── Error formatting (L) ─────────────────────────────────────────────────────
|
|
72
|
+
function toError(err) {
|
|
73
|
+
if (err instanceof Error)
|
|
74
|
+
return err;
|
|
75
|
+
return { message: String(err) };
|
|
76
|
+
}
|
|
72
77
|
function formatError(err) {
|
|
73
|
-
const
|
|
78
|
+
const e = toError(err);
|
|
79
|
+
const status = e.statusCode ?? e.status ?? 0;
|
|
74
80
|
if (status === 401)
|
|
75
81
|
return 'Authentication failed. Check your token (auth.token) and permissions.';
|
|
76
82
|
if (status === 403)
|
|
77
83
|
return 'Access denied. Your token lacks the required Azure DevOps scopes.';
|
|
78
84
|
if (status === 404)
|
|
79
|
-
return `Resource not found. Check your orgUrl, project, or test plan ID.\n ${
|
|
85
|
+
return `Resource not found. Check your orgUrl, project, or test plan ID.\n ${e.message}`;
|
|
80
86
|
if (status === 429)
|
|
81
87
|
return 'Azure DevOps rate limit hit. Reduce concurrent operations or retry later.';
|
|
82
88
|
if (status === 503)
|
|
83
89
|
return 'Azure DevOps is temporarily unavailable (503). Retry later.';
|
|
84
|
-
return
|
|
90
|
+
return e.message;
|
|
85
91
|
}
|
|
86
92
|
function handleError(err) {
|
|
87
|
-
|
|
93
|
+
const e = toError(err);
|
|
94
|
+
console.error(chalk_1.default.red(formatError(e)));
|
|
88
95
|
if (process.env.DEBUG)
|
|
89
|
-
console.error(chalk_1.default.dim(
|
|
96
|
+
console.error(chalk_1.default.dim(e.stack ?? ''));
|
|
90
97
|
process.exit(1);
|
|
91
98
|
}
|
|
92
99
|
/** Collect repeatable option values into an array. */
|
|
@@ -140,7 +147,8 @@ function buildAiOpts(opts, config, configDir) {
|
|
|
140
147
|
contextContent = fs.readFileSync(absContextPath, 'utf8');
|
|
141
148
|
}
|
|
142
149
|
catch (err) {
|
|
143
|
-
|
|
150
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
151
|
+
process.stderr.write(` [ai-summary] Warning: could not read contextFile "${absContextPath}": ${msg}\n`);
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
return {
|
|
@@ -337,13 +345,19 @@ program
|
|
|
337
345
|
// ─── publish-test-results ─────────────────────────────────────────────────────
|
|
338
346
|
program
|
|
339
347
|
.command('publish-test-results')
|
|
340
|
-
.description('Publish test results from result files (TRX, JUnit, Cucumber JSON) to Azure DevOps')
|
|
348
|
+
.description('Publish test results from result files (TRX, JUnit, Cucumber JSON, CTRF) to Azure DevOps')
|
|
341
349
|
.option('--testResult <path>', 'Path to a test result file (repeatable)', collect, [])
|
|
342
|
-
.option('--testResultFormat <format>', 'Result file format: trx, nunitXml, junit, cucumberJson, playwrightJson')
|
|
350
|
+
.option('--testResultFormat <format>', 'Result file format: trx, nunitXml, junit, cucumberJson, playwrightJson, ctrfJson')
|
|
343
351
|
.option('--attachmentsFolder <path>', 'Folder with screenshots/videos/logs to attach to test results')
|
|
344
352
|
.option('--runName <name>', 'Name for the test run in Azure DevOps')
|
|
345
353
|
.option('--buildId <id>', 'Build ID to associate with the test run')
|
|
346
354
|
.option('--dry-run', 'Parse results and show summary without publishing')
|
|
355
|
+
.option('--create-issues-on-failure', 'File GitHub Issues or ADO Bugs for each failed test')
|
|
356
|
+
.option('--issue-provider <provider>', 'Issue provider: github (default) or ado')
|
|
357
|
+
.option('--github-repo <owner/repo>', 'GitHub repository to file issues in (e.g. "myorg/myrepo")')
|
|
358
|
+
.option('--github-token <token>', 'GitHub token ($ENV_VAR reference supported)')
|
|
359
|
+
.option('--bug-threshold <percent>', 'Failure % above which one env-failure issue is filed instead of per-test (default: 20)')
|
|
360
|
+
.option('--max-issues <n>', 'Hard cap on issues filed per run (default: 50)')
|
|
347
361
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
348
362
|
.action(async (opts) => {
|
|
349
363
|
const globalOpts = program.opts();
|
|
@@ -365,6 +379,14 @@ program
|
|
|
365
379
|
attachmentsFolder: opts.attachmentsFolder,
|
|
366
380
|
runName: opts.runName,
|
|
367
381
|
buildId: opts.buildId ? parseInt(opts.buildId) : undefined,
|
|
382
|
+
createIssuesOnFailure: opts.createIssuesOnFailure,
|
|
383
|
+
issueOverrides: {
|
|
384
|
+
...(opts.issueProvider && { provider: opts.issueProvider }),
|
|
385
|
+
...(opts.githubRepo && { repo: opts.githubRepo }),
|
|
386
|
+
...(opts.githubToken && { token: opts.githubToken }),
|
|
387
|
+
...(opts.bugThreshold && { threshold: parseInt(opts.bugThreshold) }),
|
|
388
|
+
...(opts.maxIssues && { maxIssues: parseInt(opts.maxIssues) }),
|
|
389
|
+
},
|
|
368
390
|
});
|
|
369
391
|
console.log(chalk_1.default.green(`Total results: ${result.totalResults}`));
|
|
370
392
|
console.log(` ${chalk_1.default.green(`${result.passed} passed`)} ${chalk_1.default.red(`${result.failed} failed`)} ${chalk_1.default.dim(`${result.other} other`)}`);
|
|
@@ -372,6 +394,21 @@ program
|
|
|
372
394
|
console.log(chalk_1.default.dim(`Run ID: ${result.runId}`));
|
|
373
395
|
console.log(chalk_1.default.dim(`URL: ${result.runUrl}`));
|
|
374
396
|
}
|
|
397
|
+
if (result.issuesSummary) {
|
|
398
|
+
const s = result.issuesSummary;
|
|
399
|
+
console.log('');
|
|
400
|
+
console.log(chalk_1.default.bold('Issues filed:'));
|
|
401
|
+
console.log(chalk_1.default.dim(` Mode: ${s.mode} | Total failed: ${s.totalFailed} | Suppressed: ${s.suppressed}`));
|
|
402
|
+
for (const issue of s.issued) {
|
|
403
|
+
const prefix = issue.action === 'created' ? chalk_1.default.green('+')
|
|
404
|
+
: issue.action === 'skipped' ? chalk_1.default.dim('=')
|
|
405
|
+
: chalk_1.default.yellow('~');
|
|
406
|
+
const detail = issue.url ? chalk_1.default.dim(` → ${issue.url}`)
|
|
407
|
+
: issue.reason ? chalk_1.default.dim(` (${issue.reason})`)
|
|
408
|
+
: '';
|
|
409
|
+
console.log(` ${prefix} ${issue.title}${detail}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
375
412
|
}
|
|
376
413
|
catch (err) {
|
|
377
414
|
handleError(err);
|
|
@@ -509,7 +546,8 @@ program
|
|
|
509
546
|
console.log(chalk_1.default.green(' ✓ Config is valid'));
|
|
510
547
|
}
|
|
511
548
|
catch (err) {
|
|
512
|
-
|
|
549
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
550
|
+
console.log(chalk_1.default.red(` ✗ Config error: ${msg}`));
|
|
513
551
|
process.exit(1);
|
|
514
552
|
}
|
|
515
553
|
// 2. Connect to Azure
|
|
@@ -559,6 +597,8 @@ program
|
|
|
559
597
|
.description('Show field-level diff between local specs and Azure DevOps')
|
|
560
598
|
.option('--tags <expression>', 'Only check scenarios matching this tag expression')
|
|
561
599
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
600
|
+
.option('--format <fmt>', 'Output format: text (default) or json')
|
|
601
|
+
.option('--fail-on-drift', 'Exit with code 1 when any differences are found (useful as a CI quality gate)')
|
|
562
602
|
.option('--ai-provider <provider>', 'AI provider for test step generation')
|
|
563
603
|
.option('--ai-model <model>', 'AI model path or name')
|
|
564
604
|
.option('--ai-url <url>', 'Base URL for AI endpoint')
|
|
@@ -578,9 +618,22 @@ program
|
|
|
578
618
|
console.log('');
|
|
579
619
|
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
580
620
|
const results = await (0, engine_1.status)(config, configDir, { tags: opts.tags, aiSummary });
|
|
581
|
-
|
|
621
|
+
// --format json or global --output json
|
|
622
|
+
const outputFormat = opts.format ?? program.opts().output;
|
|
582
623
|
if (outputFormat === 'json') {
|
|
583
|
-
|
|
624
|
+
const diffs = results
|
|
625
|
+
.filter((r) => r.action === 'updated' || r.action === 'conflict')
|
|
626
|
+
.map((r) => ({
|
|
627
|
+
action: r.action,
|
|
628
|
+
azureId: r.azureId,
|
|
629
|
+
title: r.title,
|
|
630
|
+
filePath: r.filePath ? path.relative(process.cwd(), r.filePath) : '',
|
|
631
|
+
changedFields: r.changedFields ?? [],
|
|
632
|
+
diffDetail: r.diffDetail ?? [],
|
|
633
|
+
}));
|
|
634
|
+
process.stdout.write(JSON.stringify({ total: results.length, diffs }, null, 2) + '\n');
|
|
635
|
+
if (opts.failOnDrift && diffs.length > 0)
|
|
636
|
+
process.exit(1);
|
|
584
637
|
return;
|
|
585
638
|
}
|
|
586
639
|
let anyDiff = false;
|
|
@@ -595,12 +648,23 @@ program
|
|
|
595
648
|
if (r.changedFields?.length) {
|
|
596
649
|
console.log(chalk_1.default.dim(` changed fields: ${r.changedFields.join(', ')}`));
|
|
597
650
|
}
|
|
651
|
+
if (r.diffDetail?.length) {
|
|
652
|
+
for (const d of r.diffDetail) {
|
|
653
|
+
const local = String(d.local).slice(0, 80).replace(/\n/g, '↵');
|
|
654
|
+
const remote = String(d.remote).slice(0, 80).replace(/\n/g, '↵');
|
|
655
|
+
console.log(chalk_1.default.dim(` ${d.field}:`));
|
|
656
|
+
console.log(chalk_1.default.green(` local: ${local}`));
|
|
657
|
+
console.log(chalk_1.default.red(` remote: ${remote}`));
|
|
658
|
+
}
|
|
659
|
+
}
|
|
598
660
|
else if (r.detail) {
|
|
599
661
|
console.log(chalk_1.default.dim(` ${r.detail}`));
|
|
600
662
|
}
|
|
601
663
|
}
|
|
602
664
|
if (!anyDiff)
|
|
603
665
|
console.log(chalk_1.default.dim('No differences found.'));
|
|
666
|
+
if (opts.failOnDrift && anyDiff)
|
|
667
|
+
process.exit(1);
|
|
604
668
|
}
|
|
605
669
|
catch (err) {
|
|
606
670
|
handleError(err);
|
|
@@ -609,12 +673,13 @@ program
|
|
|
609
673
|
// ─── generate ────────────────────────────────────────────────────────────────
|
|
610
674
|
program
|
|
611
675
|
.command('generate')
|
|
612
|
-
.description('Generate local spec files from Azure DevOps User Stories')
|
|
676
|
+
.description('Generate local spec files (or AI workflow manifests) from Azure DevOps User Stories')
|
|
613
677
|
.option('--story-ids <ids>', 'Comma-separated ADO work item IDs (e.g. 1234,5678)')
|
|
614
678
|
.option('--query <wiql>', 'WIQL query string to select stories')
|
|
615
679
|
.option('--area-path <path>', 'Filter by area path')
|
|
616
680
|
.option('--format <fmt>', 'Output format: gherkin or markdown (default: markdown)')
|
|
617
681
|
.option('--output-folder <dir>', 'Where to write files (default: config pull.targetFolder or .)')
|
|
682
|
+
.option('--manifest', 'Generate .ai-workflow-manifest-{id}.json instead of spec files')
|
|
618
683
|
.option('--force', 'Overwrite existing files')
|
|
619
684
|
.option('--dry-run', 'Show what would be created without writing files')
|
|
620
685
|
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
@@ -626,15 +691,55 @@ program
|
|
|
626
691
|
if (opts.configOverride?.length)
|
|
627
692
|
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
628
693
|
const configDir = path.dirname(configPath);
|
|
694
|
+
const outputFormat = globalOpts.output;
|
|
695
|
+
const storyIds = opts.storyIds
|
|
696
|
+
? opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
697
|
+
: undefined;
|
|
698
|
+
if (opts.manifest) {
|
|
699
|
+
// ── Manifest mode ──────────────────────────────────────────────────────
|
|
700
|
+
if (!storyIds?.length) {
|
|
701
|
+
console.error(chalk_1.default.red('--story-ids is required with --manifest'));
|
|
702
|
+
process.exit(1);
|
|
703
|
+
}
|
|
704
|
+
console.log(chalk_1.default.bold('ado-sync generate --manifest'));
|
|
705
|
+
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
706
|
+
if (opts.dryRun)
|
|
707
|
+
console.log(chalk_1.default.yellow('Dry run — no files will be written'));
|
|
708
|
+
console.log('');
|
|
709
|
+
const results = await (0, manifest_1.generateManifests)(config, configDir, {
|
|
710
|
+
storyIds,
|
|
711
|
+
outputFolder: opts.outputFolder,
|
|
712
|
+
format: opts.format,
|
|
713
|
+
force: opts.force ?? false,
|
|
714
|
+
dryRun: opts.dryRun ?? false,
|
|
715
|
+
});
|
|
716
|
+
if (outputFormat === 'json') {
|
|
717
|
+
process.stdout.write(JSON.stringify(results, null, 2) + '\n');
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
for (const r of results) {
|
|
721
|
+
const symbol = r.action === 'created' ? chalk_1.default.green('+') : chalk_1.default.dim('=');
|
|
722
|
+
const relPath = path.relative(process.cwd(), r.filePath);
|
|
723
|
+
console.log(`${symbol} [#${r.storyId}] ${relPath} — ${r.title}`);
|
|
724
|
+
if (r.manifest && r.action === 'created') {
|
|
725
|
+
console.log(chalk_1.default.dim(` ${r.manifest.context.acceptanceCriteria.length} AC items · ${r.manifest.context.suggestedTags.join(' ')} · ${r.manifest.context.relatedTestCases.length} linked TCs`));
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
console.log('');
|
|
729
|
+
const created = results.filter((r) => r.action === 'created').length;
|
|
730
|
+
const skipped = results.filter((r) => r.action === 'skipped').length;
|
|
731
|
+
console.log([
|
|
732
|
+
created && chalk_1.default.green(`${created} manifest${created !== 1 ? 's' : ''} created`),
|
|
733
|
+
skipped && chalk_1.default.dim(`${skipped} skipped`),
|
|
734
|
+
].filter(Boolean).join(chalk_1.default.dim(' ')) || chalk_1.default.dim('Nothing to generate.'));
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
// ── Spec file mode ─────────────────────────────────────────────────────
|
|
629
738
|
console.log(chalk_1.default.bold('ado-sync generate'));
|
|
630
739
|
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
631
740
|
if (opts.dryRun)
|
|
632
741
|
console.log(chalk_1.default.yellow('Dry run — no files will be written'));
|
|
633
742
|
console.log('');
|
|
634
|
-
const storyIds = opts.storyIds
|
|
635
|
-
? opts.storyIds.split(',').map((s) => parseInt(s.trim(), 10)).filter(Boolean)
|
|
636
|
-
: undefined;
|
|
637
|
-
const outputFormat = program.opts().output;
|
|
638
743
|
const results = await (0, generate_1.generateSpecs)(config, configDir, {
|
|
639
744
|
storyIds,
|
|
640
745
|
query: opts.query,
|
|
@@ -665,6 +770,238 @@ program
|
|
|
665
770
|
handleError(err);
|
|
666
771
|
}
|
|
667
772
|
});
|
|
773
|
+
// ─── story-context ────────────────────────────────────────────────────────────
|
|
774
|
+
program
|
|
775
|
+
.command('story-context')
|
|
776
|
+
.description('Show planner-agent context for an ADO User Story (AC items, suggested tags, linked TCs)')
|
|
777
|
+
.requiredOption('--story-id <id>', 'ADO work item ID')
|
|
778
|
+
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
779
|
+
.action(async (opts) => {
|
|
780
|
+
const globalOpts = program.opts();
|
|
781
|
+
try {
|
|
782
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
783
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
784
|
+
if (opts.configOverride?.length)
|
|
785
|
+
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
786
|
+
const { AzureClient } = await Promise.resolve().then(() => __importStar(require('./azure/client')));
|
|
787
|
+
const { getStoryContext } = await Promise.resolve().then(() => __importStar(require('./azure/work-items')));
|
|
788
|
+
const client = await AzureClient.create(config);
|
|
789
|
+
const ctx = await getStoryContext(client, config.project, parseInt(opts.storyId, 10), config.orgUrl);
|
|
790
|
+
if (globalOpts.output === 'json') {
|
|
791
|
+
process.stdout.write(JSON.stringify(ctx, null, 2) + '\n');
|
|
792
|
+
return;
|
|
793
|
+
}
|
|
794
|
+
console.log(chalk_1.default.bold(`Story #${ctx.storyId}: ${ctx.title}`));
|
|
795
|
+
console.log(chalk_1.default.dim(`State: ${ctx.state ?? 'unknown'} | ${ctx.url}`));
|
|
796
|
+
console.log('');
|
|
797
|
+
if (ctx.acItems.length) {
|
|
798
|
+
console.log(chalk_1.default.bold('Acceptance Criteria:'));
|
|
799
|
+
ctx.acItems.forEach((item, i) => console.log(` ${chalk_1.default.dim(`${i + 1}.`)} ${item}`));
|
|
800
|
+
console.log('');
|
|
801
|
+
}
|
|
802
|
+
if (ctx.suggestedTags.length) {
|
|
803
|
+
console.log(`${chalk_1.default.bold('Suggested tags:')} ${ctx.suggestedTags.join(' ')}`);
|
|
804
|
+
}
|
|
805
|
+
if (ctx.suggestedActors.length) {
|
|
806
|
+
console.log(`${chalk_1.default.bold('Actors:')} ${ctx.suggestedActors.join(', ')}`);
|
|
807
|
+
}
|
|
808
|
+
if (ctx.relatedTestCases.length) {
|
|
809
|
+
console.log(`${chalk_1.default.bold('Linked TCs:')} ${ctx.relatedTestCases.map((id) => `#${id}`).join(', ')}`);
|
|
810
|
+
}
|
|
811
|
+
else {
|
|
812
|
+
console.log(chalk_1.default.dim('No linked test cases yet.'));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
catch (err) {
|
|
816
|
+
handleError(err);
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
// ─── stale ───────────────────────────────────────────────────────────────────
|
|
820
|
+
program
|
|
821
|
+
.command('stale')
|
|
822
|
+
.description('List Azure DevOps Test Cases that have no corresponding local spec')
|
|
823
|
+
.option('--tags <expression>', 'Only consider local specs matching this tag expression')
|
|
824
|
+
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
825
|
+
.action(async (opts) => {
|
|
826
|
+
const globalOpts = program.opts();
|
|
827
|
+
try {
|
|
828
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
829
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
830
|
+
if (opts.configOverride?.length)
|
|
831
|
+
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
832
|
+
const configDir = path.dirname(configPath);
|
|
833
|
+
console.log(chalk_1.default.bold('ado-sync stale'));
|
|
834
|
+
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
835
|
+
if (opts.tags)
|
|
836
|
+
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
837
|
+
console.log('');
|
|
838
|
+
const staleCases = await (0, engine_1.detectStaleTestCases)(config, configDir, { tags: opts.tags });
|
|
839
|
+
const outputFormat = globalOpts.output;
|
|
840
|
+
if (outputFormat === 'json') {
|
|
841
|
+
process.stdout.write(JSON.stringify(staleCases, null, 2) + '\n');
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (staleCases.length === 0) {
|
|
845
|
+
console.log(chalk_1.default.green('No stale test cases found.'));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
for (const tc of staleCases) {
|
|
849
|
+
const tagsStr = tc.tags.length ? chalk_1.default.dim(` [${tc.tags.join(', ')}]`) : '';
|
|
850
|
+
console.log(`${chalk_1.default.yellow('?')} ${chalk_1.default.dim(`[#${tc.id}]`)} ${tc.title}${tagsStr}`);
|
|
851
|
+
}
|
|
852
|
+
console.log('');
|
|
853
|
+
console.log(chalk_1.default.yellow(`${staleCases.length} stale test case${staleCases.length !== 1 ? 's' : ''} — not referenced by any local spec.`));
|
|
854
|
+
console.log(chalk_1.default.dim('Run `ado-sync push` to tag them as ado-sync:removed, or delete them manually.'));
|
|
855
|
+
}
|
|
856
|
+
catch (err) {
|
|
857
|
+
handleError(err);
|
|
858
|
+
}
|
|
859
|
+
});
|
|
860
|
+
// ─── coverage ────────────────────────────────────────────────────────────────
|
|
861
|
+
program
|
|
862
|
+
.command('coverage')
|
|
863
|
+
.description('Show spec link rate and story coverage for local specs vs Azure DevOps')
|
|
864
|
+
.option('--tags <expression>', 'Only consider local specs matching this tag expression')
|
|
865
|
+
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
866
|
+
.option('--fail-below <percent>', 'Exit with code 1 when spec link rate is below this threshold (0–100)')
|
|
867
|
+
.action(async (opts) => {
|
|
868
|
+
const globalOpts = program.opts();
|
|
869
|
+
try {
|
|
870
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
871
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
872
|
+
if (opts.configOverride?.length)
|
|
873
|
+
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
874
|
+
const configDir = path.dirname(configPath);
|
|
875
|
+
console.log(chalk_1.default.bold('ado-sync coverage'));
|
|
876
|
+
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
877
|
+
if (opts.tags)
|
|
878
|
+
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
879
|
+
console.log('');
|
|
880
|
+
const report = await (0, engine_1.coverageReport)(config, configDir, { tags: opts.tags });
|
|
881
|
+
const outputFormat = globalOpts.output;
|
|
882
|
+
if (outputFormat === 'json') {
|
|
883
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
const linkColor = report.specLinkRate >= 80 ? chalk_1.default.green : report.specLinkRate >= 50 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
887
|
+
console.log(chalk_1.default.bold('Spec link rate'));
|
|
888
|
+
console.log(` ${linkColor(`${report.specLinkRate}%`)} ${chalk_1.default.dim(`(${report.linkedSpecs} of ${report.totalLocalSpecs} specs linked to Azure Test Cases)`)}`);
|
|
889
|
+
if (report.unlinkedSpecs > 0) {
|
|
890
|
+
console.log(` ${chalk_1.default.dim(`${report.unlinkedSpecs} unlinked — run \`ado-sync push\` to create Azure Test Cases for them`)}`);
|
|
891
|
+
}
|
|
892
|
+
if (report.storiesReferenced.length > 0) {
|
|
893
|
+
console.log('');
|
|
894
|
+
const storyColor = report.storyCoverageRate >= 80 ? chalk_1.default.green : report.storyCoverageRate >= 50 ? chalk_1.default.yellow : chalk_1.default.red;
|
|
895
|
+
console.log(chalk_1.default.bold('Story coverage'));
|
|
896
|
+
console.log(` ${storyColor(`${report.storyCoverageRate}%`)} ${chalk_1.default.dim(`(${report.storiesCovered.length} of ${report.storiesReferenced.length} referenced stories have linked specs)`)}`);
|
|
897
|
+
if (report.storiesUncovered.length > 0) {
|
|
898
|
+
console.log(` ${chalk_1.default.dim('Uncovered stories: ')}${report.storiesUncovered.map((id) => `#${id}`).join(', ')}`);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
else {
|
|
902
|
+
console.log('');
|
|
903
|
+
console.log(chalk_1.default.dim('No @story: tags found — add @story:ID tags to specs to track story coverage.'));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const threshold = opts.failBelow ? parseInt(opts.failBelow, 10) : undefined;
|
|
907
|
+
if (threshold !== undefined && report.specLinkRate < threshold) {
|
|
908
|
+
console.error(chalk_1.default.red(`\nSpec link rate ${report.specLinkRate}% is below threshold ${threshold}%`));
|
|
909
|
+
process.exit(1);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
catch (err) {
|
|
913
|
+
handleError(err);
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
// ─── watch ────────────────────────────────────────────────────────────────────
|
|
917
|
+
program
|
|
918
|
+
.command('watch')
|
|
919
|
+
.description('Watch local spec files for changes and auto-push on save')
|
|
920
|
+
.option('--dry-run', 'Show what would change without making any modifications')
|
|
921
|
+
.option('--tags <expression>', 'Only sync scenarios matching this tag expression')
|
|
922
|
+
.option('--debounce <ms>', 'Debounce delay in milliseconds before running push (default: 800)', '800')
|
|
923
|
+
.option('--config-override <path=value>', 'Override a config value (repeatable)', collect, [])
|
|
924
|
+
.option('--ai-provider <provider>', 'AI provider for test step generation')
|
|
925
|
+
.option('--ai-model <model>', 'AI model path or name')
|
|
926
|
+
.option('--ai-url <url>', 'Base URL for AI endpoint')
|
|
927
|
+
.option('--ai-key <key>', 'API key for AI provider')
|
|
928
|
+
.action(async (opts) => {
|
|
929
|
+
const globalOpts = program.opts();
|
|
930
|
+
try {
|
|
931
|
+
const configPath = (0, config_1.resolveConfigPath)(globalOpts.config);
|
|
932
|
+
const config = (0, config_1.loadConfig)(configPath);
|
|
933
|
+
if (opts.configOverride?.length)
|
|
934
|
+
(0, config_1.applyOverrides)(config, opts.configOverride);
|
|
935
|
+
const configDir = path.dirname(configPath);
|
|
936
|
+
const debounceMs = parseInt(opts.debounce ?? '800', 10);
|
|
937
|
+
console.log(chalk_1.default.bold('ado-sync watch'));
|
|
938
|
+
console.log(chalk_1.default.dim(`Config: ${configPath}`));
|
|
939
|
+
console.log(chalk_1.default.dim(`Project: ${config.project}`));
|
|
940
|
+
console.log(chalk_1.default.dim(`Plan: ${config.testPlan.id}`));
|
|
941
|
+
if (opts.dryRun)
|
|
942
|
+
console.log(chalk_1.default.yellow('Dry run — no changes will be made'));
|
|
943
|
+
if (opts.tags)
|
|
944
|
+
console.log(chalk_1.default.dim(`Tags: ${opts.tags}`));
|
|
945
|
+
console.log('');
|
|
946
|
+
console.log(chalk_1.default.dim(`Watching ${configDir} for changes... (Ctrl+C to stop)`));
|
|
947
|
+
console.log('');
|
|
948
|
+
const aiSummary = buildAiOpts(opts, config, configDir);
|
|
949
|
+
let debounceTimer;
|
|
950
|
+
let running = false;
|
|
951
|
+
const runPush = async () => {
|
|
952
|
+
if (running)
|
|
953
|
+
return;
|
|
954
|
+
running = true;
|
|
955
|
+
const ts = new Date().toLocaleTimeString();
|
|
956
|
+
console.log(chalk_1.default.dim(`[${ts}] Change detected — running push...`));
|
|
957
|
+
try {
|
|
958
|
+
const isTTY = process.stdout.isTTY ?? false;
|
|
959
|
+
const onProgress = createProgressCallback(isTTY);
|
|
960
|
+
const results = await (0, engine_1.push)(config, configDir, {
|
|
961
|
+
dryRun: opts.dryRun,
|
|
962
|
+
tags: opts.tags,
|
|
963
|
+
onProgress,
|
|
964
|
+
aiSummary,
|
|
965
|
+
});
|
|
966
|
+
if (isTTY)
|
|
967
|
+
clearProgressLine();
|
|
968
|
+
printResults(results, config.toolSettings?.outputLevel);
|
|
969
|
+
}
|
|
970
|
+
catch (err) {
|
|
971
|
+
console.error(chalk_1.default.red(`Push failed: ${err instanceof Error ? err.message : String(err)}`));
|
|
972
|
+
}
|
|
973
|
+
finally {
|
|
974
|
+
running = false;
|
|
975
|
+
}
|
|
976
|
+
};
|
|
977
|
+
const onChange = () => {
|
|
978
|
+
if (debounceTimer)
|
|
979
|
+
clearTimeout(debounceTimer);
|
|
980
|
+
debounceTimer = setTimeout(runPush, debounceMs);
|
|
981
|
+
};
|
|
982
|
+
// Watch the config directory recursively for changes to spec files
|
|
983
|
+
const watcher = fs.watch(configDir, { recursive: true }, (eventType, filename) => {
|
|
984
|
+
if (!filename)
|
|
985
|
+
return;
|
|
986
|
+
// Only react to spec file changes, not the cache or config file itself
|
|
987
|
+
const skipPatterns = ['.ado-sync-cache.json', 'ado-sync.json', 'ado-sync.yml', 'node_modules'];
|
|
988
|
+
if (skipPatterns.some((p) => filename.includes(p)))
|
|
989
|
+
return;
|
|
990
|
+
onChange();
|
|
991
|
+
});
|
|
992
|
+
// Keep process alive
|
|
993
|
+
process.on('SIGINT', () => {
|
|
994
|
+
watcher.close();
|
|
995
|
+
console.log('\n' + chalk_1.default.dim('Watch mode stopped.'));
|
|
996
|
+
process.exit(0);
|
|
997
|
+
});
|
|
998
|
+
// Trigger an initial push on start
|
|
999
|
+
await runPush();
|
|
1000
|
+
}
|
|
1001
|
+
catch (err) {
|
|
1002
|
+
handleError(err);
|
|
1003
|
+
}
|
|
1004
|
+
});
|
|
668
1005
|
// ─── Run ─────────────────────────────────────────────────────────────────────
|
|
669
1006
|
program.parse(process.argv);
|
|
670
1007
|
//# sourceMappingURL=cli.js.map
|