ado-sync 0.1.65 → 0.1.67
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 +1011 -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.js +85 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.js +74 -1
- package/dist/config.js.map +1 -1
- 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 +12 -1
- package/dist/sync/engine.js +210 -41
- package/dist/sync/engine.js.map +1 -1
- package/dist/types.d.ts +52 -2
- package/llms.txt +11 -11
- package/package.json +8 -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/sync/engine.js
CHANGED
|
@@ -41,6 +41,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
42
|
exports.failOnParseErrors = failOnParseErrors;
|
|
43
43
|
exports.buildPushDiff = buildPushDiff;
|
|
44
|
+
exports.validatePushModeOptions = validatePushModeOptions;
|
|
44
45
|
exports.push = push;
|
|
45
46
|
exports.pull = pull;
|
|
46
47
|
exports.status = status;
|
|
@@ -313,6 +314,36 @@ async function parseLocalFiles(filePaths, config, tagsFilter) {
|
|
|
313
314
|
}
|
|
314
315
|
return { tests: results, failures };
|
|
315
316
|
}
|
|
317
|
+
function validatePushModeOptions(opts) {
|
|
318
|
+
const enabledModes = [
|
|
319
|
+
opts.createOnly && '--create-only',
|
|
320
|
+
opts.linkOnly && '--link-only',
|
|
321
|
+
opts.updateOnly && '--update-only',
|
|
322
|
+
].filter(Boolean);
|
|
323
|
+
if (enabledModes.length > 1) {
|
|
324
|
+
throw new Error(`Only one push mode can be used at a time: ${enabledModes.join(', ')}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function filterDiscoveredFiles(filePaths, sourceFiles) {
|
|
328
|
+
if (!sourceFiles?.length)
|
|
329
|
+
return filePaths;
|
|
330
|
+
const requested = new Set(sourceFiles.map((filePath) => path.normalize(path.resolve(filePath))));
|
|
331
|
+
return filePaths.filter((filePath) => requested.has(path.normalize(filePath)));
|
|
332
|
+
}
|
|
333
|
+
function formatSuitePreview(pathKey) {
|
|
334
|
+
if (!pathKey)
|
|
335
|
+
return undefined;
|
|
336
|
+
return pathKey.split('/').join(' / ');
|
|
337
|
+
}
|
|
338
|
+
function mapRemoteTestsByTitle(testCases) {
|
|
339
|
+
const byTitle = new Map();
|
|
340
|
+
for (const testCase of testCases) {
|
|
341
|
+
const matches = byTitle.get(testCase.title) ?? [];
|
|
342
|
+
matches.push(testCase);
|
|
343
|
+
byTitle.set(testCase.title, matches);
|
|
344
|
+
}
|
|
345
|
+
return byTitle;
|
|
346
|
+
}
|
|
316
347
|
// ─── Multi-plan helpers ───────────────────────────────────────────────────────
|
|
317
348
|
/**
|
|
318
349
|
* Build an effective config for a single plan entry in testPlans[] mode.
|
|
@@ -330,6 +361,7 @@ function configForPlanEntry(base, entry) {
|
|
|
330
361
|
testPlan: {
|
|
331
362
|
id: entry.id,
|
|
332
363
|
suiteId: entry.suiteId ?? base.testPlan.suiteId,
|
|
364
|
+
hierarchy: entry.hierarchy ?? base.testPlan.hierarchy,
|
|
333
365
|
suiteMapping: entry.suiteMapping ?? base.testPlan.suiteMapping,
|
|
334
366
|
suiteRouting: entry.suiteRouting ?? base.testPlan.suiteRouting,
|
|
335
367
|
},
|
|
@@ -375,6 +407,7 @@ async function resolveTargetSuiteFromRouting(client, config, test, suiteCache) {
|
|
|
375
407
|
}
|
|
376
408
|
// ─── Push ─────────────────────────────────────────────────────────────────────
|
|
377
409
|
async function push(config, configDir, opts = {}) {
|
|
410
|
+
validatePushModeOptions(opts);
|
|
378
411
|
// Multi-plan: run AI summarisation across all plans first so progress totals
|
|
379
412
|
// are correct, then delegate each plan's sync loop (with AI already applied).
|
|
380
413
|
if (config.testPlans?.length) {
|
|
@@ -383,7 +416,7 @@ async function push(config, configDir, opts = {}) {
|
|
|
383
416
|
const parseFailures = [];
|
|
384
417
|
for (const entry of config.testPlans) {
|
|
385
418
|
const entryConfig = configForPlanEntry(config, entry);
|
|
386
|
-
const files = await discoverFiles(entryConfig.local.include, entryConfig.local.exclude, configDir);
|
|
419
|
+
const files = filterDiscoveredFiles(await discoverFiles(entryConfig.local.include, entryConfig.local.exclude, configDir), opts.sourceFiles);
|
|
387
420
|
const parsed = await parseLocalFiles(files, entryConfig, opts.tags);
|
|
388
421
|
parseFailures.push(...parsed.failures);
|
|
389
422
|
const tests = parsed.tests;
|
|
@@ -430,7 +463,7 @@ async function pushSingle(config, configDir, opts) {
|
|
|
430
463
|
let resolvedTests = opts._preloadedTests ?? [];
|
|
431
464
|
let parseFailures = [];
|
|
432
465
|
if (!opts._preloadedTests) {
|
|
433
|
-
const files = await discoverFiles(config.local.include, config.local.exclude, configDir);
|
|
466
|
+
const files = filterDiscoveredFiles(await discoverFiles(config.local.include, config.local.exclude, configDir), opts.sourceFiles);
|
|
434
467
|
const parsed = await parseLocalFiles(files, config, opts.tags);
|
|
435
468
|
resolvedTests = parsed.tests;
|
|
436
469
|
parseFailures = parsed.failures;
|
|
@@ -515,7 +548,7 @@ async function pushSingle(config, configDir, opts) {
|
|
|
515
548
|
const titleField = config.sync?.titleField ?? 'System.Title';
|
|
516
549
|
const conflictAction = config.sync?.conflictAction ?? 'overwrite';
|
|
517
550
|
const disableLocal = config.sync?.disableLocalChanges ?? false;
|
|
518
|
-
const byFolder =
|
|
551
|
+
const byFolder = (0, test_cases_1.usesGeneratedSuiteHierarchy)(config);
|
|
519
552
|
// Load local cache for conflict detection and skip optimisation
|
|
520
553
|
const cache = (0, cache_1.loadCache)(configDir);
|
|
521
554
|
// G: seed in-memory suite cache from persisted _suites to avoid redundant API calls
|
|
@@ -535,18 +568,24 @@ async function pushSingle(config, configDir, opts) {
|
|
|
535
568
|
const markAutomated = config.sync?.markAutomated ?? false;
|
|
536
569
|
const recoveredIds = new Set(); // "filePath:line" keys
|
|
537
570
|
let preloadedRemoteTcs;
|
|
571
|
+
let remoteByTitle;
|
|
538
572
|
const unlinkedWithAtName = resolvedTests.filter(t => !t.azureId && t.automatedTestName);
|
|
539
|
-
|
|
573
|
+
const shouldPreloadRemoteTcs = (markAutomated && unlinkedWithAtName.length > 0)
|
|
574
|
+
|| (opts.linkOnly && resolvedTests.some((test) => !test.azureId));
|
|
575
|
+
if (shouldPreloadRemoteTcs) {
|
|
540
576
|
try {
|
|
541
577
|
preloadedRemoteTcs = await (0, test_cases_1.getTestCasesInSuite)(client, config);
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
578
|
+
remoteByTitle = mapRemoteTestsByTitle(preloadedRemoteTcs);
|
|
579
|
+
if (markAutomated && unlinkedWithAtName.length > 0) {
|
|
580
|
+
const byAtName = new Map(preloadedRemoteTcs
|
|
581
|
+
.filter(tc => tc.automatedTestName)
|
|
582
|
+
.map(tc => [tc.automatedTestName, tc]));
|
|
583
|
+
for (const test of unlinkedWithAtName) {
|
|
584
|
+
const match = byAtName.get(test.automatedTestName);
|
|
585
|
+
if (match) {
|
|
586
|
+
test.azureId = match.id;
|
|
587
|
+
recoveredIds.add(`${test.filePath}:${test.line}`);
|
|
588
|
+
}
|
|
550
589
|
}
|
|
551
590
|
}
|
|
552
591
|
}
|
|
@@ -594,7 +633,19 @@ async function pushSingle(config, configDir, opts) {
|
|
|
594
633
|
const pushWorkers = Array.from({ length: Math.min(PUSH_CONCURRENCY, pushQueue.length) }, async () => {
|
|
595
634
|
while (pushQueue.length > 0) {
|
|
596
635
|
const test = pushQueue.shift();
|
|
636
|
+
const currentHierarchyKey = byFolder ? (0, test_cases_1.getGeneratedSuitePathKeyForTest)(config, test, configDir) : undefined;
|
|
637
|
+
const targetSuitePath = formatSuitePreview(currentHierarchyKey);
|
|
597
638
|
if (test.azureId) {
|
|
639
|
+
if (opts.createOnly || opts.linkOnly) {
|
|
640
|
+
reportProgress({
|
|
641
|
+
action: 'skipped',
|
|
642
|
+
filePath: test.filePath,
|
|
643
|
+
title: test.title,
|
|
644
|
+
azureId: test.azureId,
|
|
645
|
+
detail: opts.createOnly ? 'skipped by create-only mode' : 'already linked; skipped by link-only mode',
|
|
646
|
+
});
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
598
649
|
try {
|
|
599
650
|
const cached = cache[test.azureId];
|
|
600
651
|
// Use pre-fetched TC from cache; fall back to live fetch on miss
|
|
@@ -602,11 +653,21 @@ async function pushSingle(config, configDir, opts) {
|
|
|
602
653
|
? remoteTcCache.get(test.azureId)
|
|
603
654
|
: await (0, test_cases_1.getTestCase)(client, test.azureId, titleField);
|
|
604
655
|
if (!remote) {
|
|
656
|
+
if (opts.updateOnly) {
|
|
657
|
+
reportProgress({
|
|
658
|
+
action: 'skipped',
|
|
659
|
+
filePath: test.filePath,
|
|
660
|
+
title: test.title,
|
|
661
|
+
azureId: test.azureId,
|
|
662
|
+
detail: 'remote test case missing; skipped by update-only mode',
|
|
663
|
+
});
|
|
664
|
+
continue;
|
|
665
|
+
}
|
|
605
666
|
// TC was deleted from Azure — re-create it and write back the new ID.
|
|
606
667
|
let newId;
|
|
607
668
|
if (!opts.dryRun) {
|
|
608
669
|
const suiteIdOverride = byFolder
|
|
609
|
-
? await (0, test_cases_1.
|
|
670
|
+
? await (0, test_cases_1.getOrCreateGeneratedSuiteForTest)(client, config, test, configDir, suiteCache)
|
|
610
671
|
: await resolveTargetSuiteFromRouting(client, config, test, suiteCache);
|
|
611
672
|
newId = await (0, test_cases_1.createTestCase)(client, test, config, suiteIdOverride, configDir);
|
|
612
673
|
createdIds.add(newId);
|
|
@@ -616,7 +677,7 @@ async function pushSingle(config, configDir, opts) {
|
|
|
616
677
|
await (0, test_cases_1.addTestCaseToConditionSuites)(client, config, newId, test, conditionSuiteCache);
|
|
617
678
|
const created = await (0, test_cases_1.getTestCase)(client, newId, titleField);
|
|
618
679
|
if (created)
|
|
619
|
-
updateCacheEntry(cache, test, created);
|
|
680
|
+
updateCacheEntry(cache, test, created, currentHierarchyKey);
|
|
620
681
|
}
|
|
621
682
|
reportProgress({ action: 'created', filePath: test.filePath, title: test.title, azureId: newId });
|
|
622
683
|
continue;
|
|
@@ -625,23 +686,44 @@ async function pushSingle(config, configDir, opts) {
|
|
|
625
686
|
// Normalise local to @param@ before comparing so outlines don't report
|
|
626
687
|
// stepsChanged on every push after the first successful sync.
|
|
627
688
|
const { changedFields, diffDetail } = buildPushDiff(test, remote, config, cached);
|
|
628
|
-
|
|
689
|
+
const previousHierarchyKey = cached?.suitePathKey ?? (cached?.filePath
|
|
690
|
+
? (0, test_cases_1.getGeneratedSuitePathKey)(config, cached.filePath, configDir)
|
|
691
|
+
: undefined);
|
|
692
|
+
const suiteChanged = byFolder &&
|
|
693
|
+
previousHierarchyKey !== undefined &&
|
|
694
|
+
currentHierarchyKey !== undefined &&
|
|
695
|
+
previousHierarchyKey !== currentHierarchyKey;
|
|
696
|
+
const previousSuitePath = formatSuitePreview(previousHierarchyKey);
|
|
697
|
+
const effectiveChangedFields = suiteChanged ? [...changedFields, 'suite'] : changedFields;
|
|
698
|
+
const effectiveDiffDetail = suiteChanged
|
|
699
|
+
? [
|
|
700
|
+
...diffDetail,
|
|
701
|
+
{
|
|
702
|
+
field: 'suite',
|
|
703
|
+
remote: previousHierarchyKey || '(plan root)',
|
|
704
|
+
local: currentHierarchyKey || '(plan root)',
|
|
705
|
+
},
|
|
706
|
+
]
|
|
707
|
+
: diffDetail;
|
|
708
|
+
if (effectiveChangedFields.length === 0) {
|
|
629
709
|
// Update cache entry even on skip (changedDate may differ due to other fields)
|
|
630
|
-
updateCacheEntry(cache, test, remote);
|
|
631
|
-
reportProgress({ action: 'skipped', filePath: test.filePath, title: test.title, azureId: test.azureId });
|
|
710
|
+
updateCacheEntry(cache, test, remote, currentHierarchyKey);
|
|
711
|
+
reportProgress({ action: 'skipped', filePath: test.filePath, title: test.title, azureId: test.azureId, detail: 'no changes' });
|
|
632
712
|
continue;
|
|
633
713
|
}
|
|
634
714
|
// Conflict detection: remote was changed since last push AND local also differs
|
|
635
|
-
if (cached && remote.changedDate && remote.changedDate !== cached.changedDate) {
|
|
715
|
+
if (changedFields.length > 0 && cached && remote.changedDate && remote.changedDate !== cached.changedDate) {
|
|
636
716
|
const relFile = path.relative(configDir, test.filePath);
|
|
637
717
|
const conflict = {
|
|
638
718
|
action: 'conflict',
|
|
639
719
|
filePath: test.filePath,
|
|
640
720
|
title: test.title,
|
|
641
721
|
azureId: test.azureId,
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
722
|
+
targetSuitePath,
|
|
723
|
+
previousSuitePath,
|
|
724
|
+
changedFields: effectiveChangedFields,
|
|
725
|
+
diffDetail: effectiveDiffDetail,
|
|
726
|
+
detail: `${relFile}:${test.line} — changed fields: ${effectiveChangedFields.join(', ')}`,
|
|
645
727
|
};
|
|
646
728
|
if (conflictAction === 'skip') {
|
|
647
729
|
reportProgress(conflict);
|
|
@@ -655,24 +737,39 @@ async function pushSingle(config, configDir, opts) {
|
|
|
655
737
|
// 'overwrite' — fall through to update
|
|
656
738
|
}
|
|
657
739
|
if (!opts.dryRun) {
|
|
658
|
-
|
|
740
|
+
if (changedFields.length > 0) {
|
|
741
|
+
await (0, test_cases_1.updateTestCase)(client, test.azureId, test, config, configDir);
|
|
742
|
+
}
|
|
659
743
|
// Ensure the TC is in the configured suite (it may not be if the suite was
|
|
660
744
|
// changed in config, or if the TC was imported with an ID but never pushed before).
|
|
661
745
|
const updateSuiteId = byFolder
|
|
662
|
-
? await (0, test_cases_1.
|
|
746
|
+
? await (0, test_cases_1.getOrCreateGeneratedSuiteForTest)(client, config, test, configDir, suiteCache)
|
|
663
747
|
: await resolveTargetSuiteFromRouting(client, config, test, suiteCache) ?? config.testPlan.suiteId;
|
|
664
748
|
if (updateSuiteId) {
|
|
665
749
|
await (0, test_cases_1.addTestCaseToSuite)(client, config, test.azureId, updateSuiteId);
|
|
750
|
+
if (suiteChanged && previousHierarchyKey) {
|
|
751
|
+
const previousSuiteId = cached?.suitePathKey
|
|
752
|
+
? await (0, test_cases_1.resolveExistingSuiteForPathKey)(client, config, previousHierarchyKey, suiteCache)
|
|
753
|
+
: cached?.filePath
|
|
754
|
+
? await (0, test_cases_1.resolveExistingSuiteForFile)(client, config, cached.filePath, configDir, suiteCache)
|
|
755
|
+
: undefined;
|
|
756
|
+
if (previousSuiteId && previousSuiteId !== updateSuiteId) {
|
|
757
|
+
await (0, test_cases_1.removeTestCaseFromSuite)(client, config, test.azureId, previousSuiteId);
|
|
758
|
+
if ((0, test_cases_1.shouldCleanupEmptyGeneratedSuites)(config)) {
|
|
759
|
+
await (0, test_cases_1.pruneEmptyGeneratedSuitesForPathKey)(client, config, previousHierarchyKey, suiteCache);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
666
763
|
}
|
|
667
764
|
else {
|
|
668
765
|
await (0, test_cases_1.addTestCaseToRootSuite)(client, config, test.azureId);
|
|
669
766
|
}
|
|
670
767
|
await (0, test_cases_1.addTestCaseToConditionSuites)(client, config, test.azureId, test, conditionSuiteCache);
|
|
671
|
-
const updated = await (0, test_cases_1.getTestCase)(client, test.azureId, titleField);
|
|
768
|
+
const updated = changedFields.length > 0 ? await (0, test_cases_1.getTestCase)(client, test.azureId, titleField) : remote;
|
|
672
769
|
if (updated)
|
|
673
|
-
updateCacheEntry(cache, test, updated);
|
|
770
|
+
updateCacheEntry(cache, test, updated, currentHierarchyKey);
|
|
674
771
|
}
|
|
675
|
-
reportProgress({ action: 'updated', filePath: test.filePath, title: test.title, azureId: test.azureId, changedFields, diffDetail });
|
|
772
|
+
reportProgress({ action: 'updated', filePath: test.filePath, title: test.title, azureId: test.azureId, targetSuitePath, previousSuitePath, changedFields: effectiveChangedFields, diffDetail: effectiveDiffDetail });
|
|
676
773
|
}
|
|
677
774
|
catch (err) {
|
|
678
775
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -680,11 +777,51 @@ async function pushSingle(config, configDir, opts) {
|
|
|
680
777
|
}
|
|
681
778
|
}
|
|
682
779
|
else {
|
|
780
|
+
if (opts.updateOnly) {
|
|
781
|
+
reportProgress({
|
|
782
|
+
action: 'skipped',
|
|
783
|
+
filePath: test.filePath,
|
|
784
|
+
title: test.title,
|
|
785
|
+
detail: 'unlinked local spec; skipped by update-only mode',
|
|
786
|
+
});
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
if (opts.linkOnly) {
|
|
790
|
+
try {
|
|
791
|
+
const canonicalTitle = (0, test_cases_1.buildAzureSyncContent)(test, config.sync?.format).title;
|
|
792
|
+
const titleMatches = remoteByTitle?.get(canonicalTitle) ?? [];
|
|
793
|
+
if (titleMatches.length !== 1) {
|
|
794
|
+
const detail = titleMatches.length === 0
|
|
795
|
+
? 'no unique title match found in current remote scope'
|
|
796
|
+
: `multiple title matches found in current remote scope (${titleMatches.length})`;
|
|
797
|
+
reportProgress({ action: 'skipped', filePath: test.filePath, title: test.title, detail });
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
const matched = titleMatches[0];
|
|
801
|
+
test.azureId = matched.id;
|
|
802
|
+
recoveredIds.add(`${test.filePath}:${test.line}`);
|
|
803
|
+
if (!opts.dryRun && !disableLocal) {
|
|
804
|
+
pendingWritebacks.push({ test, newId: matched.id });
|
|
805
|
+
}
|
|
806
|
+
const detail = opts.dryRun
|
|
807
|
+
? 'would link to existing test case by exact title match'
|
|
808
|
+
: disableLocal
|
|
809
|
+
? 'matched existing test case by exact title match (local writeback skipped)'
|
|
810
|
+
: 'linked to existing test case by exact title match';
|
|
811
|
+
reportProgress({ action: 'linked', filePath: test.filePath, title: test.title, azureId: matched.id, detail });
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
816
|
+
reportProgress({ action: 'error', filePath: test.filePath, title: test.title, detail: msg });
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
683
820
|
try {
|
|
684
821
|
let newId;
|
|
685
822
|
if (!opts.dryRun) {
|
|
686
823
|
const suiteIdOverride = byFolder
|
|
687
|
-
? await (0, test_cases_1.
|
|
824
|
+
? await (0, test_cases_1.getOrCreateGeneratedSuiteForTest)(client, config, test, configDir, suiteCache)
|
|
688
825
|
: await resolveTargetSuiteFromRouting(client, config, test, suiteCache);
|
|
689
826
|
newId = await (0, test_cases_1.createTestCase)(client, test, config, suiteIdOverride, configDir);
|
|
690
827
|
createdIds.add(newId);
|
|
@@ -695,9 +832,9 @@ async function pushSingle(config, configDir, opts) {
|
|
|
695
832
|
// Fetch back to get changedDate for cache
|
|
696
833
|
const created = await (0, test_cases_1.getTestCase)(client, newId, titleField);
|
|
697
834
|
if (created)
|
|
698
|
-
updateCacheEntry(cache, test, created);
|
|
835
|
+
updateCacheEntry(cache, test, created, currentHierarchyKey);
|
|
699
836
|
}
|
|
700
|
-
reportProgress({ action: 'created', filePath: test.filePath, title: test.title, azureId: newId });
|
|
837
|
+
reportProgress({ action: 'created', filePath: test.filePath, title: test.title, azureId: newId, targetSuitePath });
|
|
701
838
|
}
|
|
702
839
|
catch (err) {
|
|
703
840
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -754,17 +891,17 @@ async function pushSingle(config, configDir, opts) {
|
|
|
754
891
|
}
|
|
755
892
|
}
|
|
756
893
|
// Removed TC detection is only safe on full-scope runs.
|
|
757
|
-
// Tag-filtered or condition-filtered pushes intentionally operate on a subset.
|
|
758
|
-
const shouldDetectRemoved = !opts.tags && !config.local.condition;
|
|
894
|
+
// Tag-filtered, source-file-filtered, create-only, or condition-filtered pushes intentionally operate on a subset.
|
|
895
|
+
const shouldDetectRemoved = !opts.tags && !opts.sourceFiles?.length && !opts.createOnly && !opts.linkOnly && !opts.updateOnly && !config.local.condition;
|
|
896
|
+
const localIds = new Set([
|
|
897
|
+
...resolvedTests.map((t) => t.azureId).filter(Boolean),
|
|
898
|
+
...createdIds,
|
|
899
|
+
]);
|
|
759
900
|
if (shouldDetectRemoved) {
|
|
760
901
|
try {
|
|
761
902
|
// Reuse the pre-loaded remote TCs if we already fetched them for automatedTestName
|
|
762
903
|
// matching, otherwise fetch now. This avoids a redundant round-trip.
|
|
763
904
|
const remoteTcs = preloadedRemoteTcs ?? await (0, test_cases_1.getTestCasesInSuite)(client, config);
|
|
764
|
-
const localIds = new Set([
|
|
765
|
-
...resolvedTests.map((t) => t.azureId).filter(Boolean),
|
|
766
|
-
...createdIds,
|
|
767
|
-
]);
|
|
768
905
|
for (const remote of remoteTcs) {
|
|
769
906
|
if (!localIds.has(remote.id)) {
|
|
770
907
|
if (!opts.dryRun) {
|
|
@@ -782,6 +919,22 @@ async function pushSingle(config, configDir, opts) {
|
|
|
782
919
|
}
|
|
783
920
|
catch { /* best-effort: don't fail the whole push */ }
|
|
784
921
|
}
|
|
922
|
+
if (!opts.dryRun && (0, test_cases_1.shouldCleanupEmptyGeneratedSuites)(config) && shouldDetectRemoved) {
|
|
923
|
+
const staleHierarchyKeys = [...new Set(Object.entries(cache)
|
|
924
|
+
.filter(([cacheKey]) => cacheKey !== '_suites')
|
|
925
|
+
.map(([cacheKey, entry]) => ({ azureId: Number(cacheKey), entry }))
|
|
926
|
+
.filter(({ azureId, entry }) => Number.isFinite(azureId) && !localIds.has(azureId) && !!entry)
|
|
927
|
+
.map(({ entry }) => entry.suitePathKey ?? (0, test_cases_1.getGeneratedSuitePathKey)(config, entry.filePath, configDir))
|
|
928
|
+
.filter((pathKey) => !!pathKey))];
|
|
929
|
+
for (const staleHierarchyKey of staleHierarchyKeys) {
|
|
930
|
+
try {
|
|
931
|
+
await (0, test_cases_1.pruneEmptyGeneratedSuitesForPathKey)(client, config, staleHierarchyKey, suiteCache);
|
|
932
|
+
}
|
|
933
|
+
catch {
|
|
934
|
+
// Best-effort cleanup; do not fail the push when stale branch pruning cannot complete.
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
785
938
|
if (!opts.dryRun) {
|
|
786
939
|
// G: persist suite name→id map so the next push avoids redundant API traversals
|
|
787
940
|
if (suiteCache.size > 0) {
|
|
@@ -803,7 +956,7 @@ async function pull(config, configDir, opts = {}) {
|
|
|
803
956
|
return pullSingle(config, configDir, opts);
|
|
804
957
|
}
|
|
805
958
|
async function pullSingle(config, configDir, opts) {
|
|
806
|
-
const files = await discoverFiles(config.local.include, config.local.exclude, configDir);
|
|
959
|
+
const files = filterDiscoveredFiles(await discoverFiles(config.local.include, config.local.exclude, configDir), opts.sourceFiles);
|
|
807
960
|
const parsed = await parseLocalFiles(files, config, opts.tags);
|
|
808
961
|
failOnParseErrors('pull', parsed.failures);
|
|
809
962
|
const tests = parsed.tests;
|
|
@@ -841,7 +994,7 @@ async function pullSingle(config, configDir, opts) {
|
|
|
841
994
|
const stepsChanged = remoteStepsText !== localStepsText;
|
|
842
995
|
const descriptionChanged = (remote.description ?? '') !== (test.description ?? '');
|
|
843
996
|
if (!titleChanged && !stepsChanged && !descriptionChanged) {
|
|
844
|
-
reportProgress({ action: 'skipped', filePath: test.filePath, title: test.title, azureId: test.azureId });
|
|
997
|
+
reportProgress({ action: 'skipped', filePath: test.filePath, title: test.title, azureId: test.azureId, detail: 'no changes' });
|
|
845
998
|
continue;
|
|
846
999
|
}
|
|
847
1000
|
if (!opts.dryRun) {
|
|
@@ -904,6 +1057,14 @@ async function pullSingle(config, configDir, opts) {
|
|
|
904
1057
|
detail: `Pull-create is not supported for local.type "${config.local.type}".`,
|
|
905
1058
|
});
|
|
906
1059
|
}
|
|
1060
|
+
else if (opts.sourceFiles?.length) {
|
|
1061
|
+
results.push({
|
|
1062
|
+
action: 'error',
|
|
1063
|
+
filePath: '',
|
|
1064
|
+
title: '',
|
|
1065
|
+
detail: 'Pull-create is not supported together with --source-file because partial runs cannot infer which unlinked remote tests to materialize.',
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
907
1068
|
else {
|
|
908
1069
|
try {
|
|
909
1070
|
const remoteTcs = await (0, test_cases_1.getTestCasesInSuite)(client, config);
|
|
@@ -939,10 +1100,17 @@ async function pullSingle(config, configDir, opts) {
|
|
|
939
1100
|
}
|
|
940
1101
|
// ─── Status ───────────────────────────────────────────────────────────────────
|
|
941
1102
|
async function status(config, configDir, opts = {}) {
|
|
942
|
-
return push(config, configDir, {
|
|
1103
|
+
return push(config, configDir, {
|
|
1104
|
+
dryRun: true,
|
|
1105
|
+
tags: opts.tags,
|
|
1106
|
+
sourceFiles: opts.sourceFiles,
|
|
1107
|
+
onProgress: opts.onProgress,
|
|
1108
|
+
onAiProgress: opts.onAiProgress,
|
|
1109
|
+
aiSummary: opts.aiSummary,
|
|
1110
|
+
});
|
|
943
1111
|
}
|
|
944
1112
|
// ─── Cache helpers ────────────────────────────────────────────────────────────
|
|
945
|
-
function updateCacheEntry(cache, test, remote) {
|
|
1113
|
+
function updateCacheEntry(cache, test, remote, suitePathKey) {
|
|
946
1114
|
if (!remote.changedDate)
|
|
947
1115
|
return;
|
|
948
1116
|
cache[remote.id] = {
|
|
@@ -955,6 +1123,7 @@ function updateCacheEntry(cache, test, remote) {
|
|
|
955
1123
|
remoteDescriptionHash: (0, cache_1.hashString)(remote.description),
|
|
956
1124
|
changedDate: remote.changedDate,
|
|
957
1125
|
filePath: test.filePath,
|
|
1126
|
+
suitePathKey,
|
|
958
1127
|
};
|
|
959
1128
|
}
|
|
960
1129
|
// ─── Apply remote changes to local file ───────────────────────────────────────
|
|
@@ -1180,7 +1349,7 @@ function buildPullMarkdownContent(tc, tagPrefix) {
|
|
|
1180
1349
|
* without running push, or when TCs are created directly in Azure without a local spec.
|
|
1181
1350
|
*/
|
|
1182
1351
|
async function detectStaleTestCases(config, configDir, opts = {}) {
|
|
1183
|
-
const files = await discoverFiles(config.local.include, config.local.exclude, configDir);
|
|
1352
|
+
const files = filterDiscoveredFiles(await discoverFiles(config.local.include, config.local.exclude, configDir), opts.sourceFiles);
|
|
1184
1353
|
const parsed = await parseLocalFiles(files, config, opts.tags);
|
|
1185
1354
|
failOnParseErrors('stale test case detection', parsed.failures);
|
|
1186
1355
|
const tests = parsed.tests;
|
|
@@ -1211,7 +1380,7 @@ async function detectStaleTestCases(config, configDir, opts = {}) {
|
|
|
1211
1380
|
* 2. Story coverage — % of referenced User Stories that have at least one linked spec
|
|
1212
1381
|
*/
|
|
1213
1382
|
async function coverageReport(config, configDir, opts = {}) {
|
|
1214
|
-
const files = await discoverFiles(config.local.include, config.local.exclude, configDir);
|
|
1383
|
+
const files = filterDiscoveredFiles(await discoverFiles(config.local.include, config.local.exclude, configDir), opts.sourceFiles);
|
|
1215
1384
|
const parsed = await parseLocalFiles(files, config, opts.tags);
|
|
1216
1385
|
failOnParseErrors('coverage report', parsed.failures);
|
|
1217
1386
|
const tests = parsed.tests;
|