@elisra-devops/docgen-data-provider 1.88.0 → 1.89.0

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.
@@ -446,8 +446,7 @@ export default class ResultDataProvider {
446
446
  const testData = await this.fetchMewpScopedTestData(
447
447
  testPlanId,
448
448
  projectName,
449
- selectedSuiteIds,
450
- !!options?.useRelFallback
449
+ selectedSuiteIds
451
450
  );
452
451
 
453
452
  const allRequirements = await this.fetchMewpL2Requirements(projectName);
@@ -549,6 +548,9 @@ export default class ResultDataProvider {
549
548
  const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
550
549
  const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
551
550
  const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
551
+ if (options?.debugMode) {
552
+ this.logMewpRunScenarioDebugMatrix(runResults, `coverage plan=${testPlanId}`);
553
+ }
552
554
  for (const runResult of runResults) {
553
555
  const testCaseId = this.extractMewpTestCaseId(runResult);
554
556
  const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
@@ -748,8 +750,7 @@ export default class ResultDataProvider {
748
750
  const testData = await this.fetchMewpScopedTestData(
749
751
  testPlanId,
750
752
  projectName,
751
- selectedSuiteIds,
752
- !!options?.useRelFallback
753
+ selectedSuiteIds
753
754
  );
754
755
  const allRequirements = await this.fetchMewpL2Requirements(projectName);
755
756
  const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
@@ -795,6 +796,16 @@ export default class ResultDataProvider {
795
796
  `fromSuitePayload=${preloadedStepXmlCount} fromWorkItemFallback=${fallbackStepLoadStats.loadedFromFallback} ` +
796
797
  `stepsXmlAvailable=${stepsXmlByTestCase.size} unresolved=${fallbackStepLoadStats.unresolvedCount}`
797
798
  );
799
+ if (options?.debugMode) {
800
+ const debugRunResults = await this.fetchAllResultDataTestReporter(
801
+ testData,
802
+ projectName,
803
+ [],
804
+ false,
805
+ false
806
+ );
807
+ this.logMewpRunScenarioDebugMatrix(debugRunResults, `internal-validation plan=${testPlanId}`);
808
+ }
798
809
 
799
810
  const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
800
811
  const diagnostics = {
@@ -808,34 +819,6 @@ export default class ResultDataProvider {
808
819
 
809
820
  for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
810
821
  diagnostics.totalTestCases += 1;
811
- const logList = (items: Iterable<string>, max = 12): string => {
812
- const values = [...items].map((item) => String(item || '').trim()).filter((item) => !!item);
813
- const shown = values.slice(0, max);
814
- const suffix = values.length > max ? ` ...(+${values.length - max} more)` : '';
815
- return shown.join(', ') + suffix;
816
- };
817
- const logByFamily = (items: Iterable<string>, maxFamilies = 8, maxMembers = 10): string => {
818
- const map = new Map<string, Set<string>>();
819
- for (const item of items || []) {
820
- const normalized = this.normalizeMewpRequirementCodeWithSuffix(String(item || ''));
821
- if (!normalized) continue;
822
- const base = this.toRequirementKey(normalized) || normalized;
823
- if (!map.has(base)) map.set(base, new Set<string>());
824
- map.get(base)!.add(normalized);
825
- }
826
- const entries = [...map.entries()]
827
- .sort((a, b) => a[0].localeCompare(b[0]))
828
- .slice(0, maxFamilies)
829
- .map(([base, members]) => {
830
- const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
831
- const shownMembers = sortedMembers.slice(0, maxMembers);
832
- const membersSuffix =
833
- sortedMembers.length > maxMembers ? ` ...(+${sortedMembers.length - maxMembers} more)` : '';
834
- return `${base}=[${shownMembers.join(', ')}${membersSuffix}]`;
835
- });
836
- const suffix = map.size > maxFamilies ? ` ...(+${map.size - maxFamilies} families)` : '';
837
- return entries.join(' | ') + suffix;
838
- };
839
822
  const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
840
823
  const parsedSteps =
841
824
  stepsXml && String(stepsXml).trim() !== ''
@@ -888,15 +871,6 @@ export default class ResultDataProvider {
888
871
  const linkedBaseKeys = new Set<string>(
889
872
  [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
890
873
  );
891
- const mentionStepSample = mentionEntries
892
- .slice(0, 8)
893
- .map((entry) => `${entry.stepRef}=[${logList(entry.codes, 6)}]`)
894
- .join(' | ');
895
- logger.debug(
896
- `MEWP internal validation trace: testCaseId=${testCaseId} ` +
897
- `mentionSteps=${mentionEntries.length} mentionStepSample='${mentionStepSample}' ` +
898
- `mentionedL2ByFamily='${logByFamily(mentionedL2Only)}' linkedByFamily='${logByFamily(linkedFullCodes)}'`
899
- );
900
874
 
901
875
  const mentionedCodesByBase = new Map<string, Set<string>>();
902
876
  for (const code of mentionedL2Only) {
@@ -918,89 +892,49 @@ export default class ResultDataProvider {
918
892
  const mentionedCodesList = [...mentionedCodes];
919
893
  const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
920
894
  const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
921
- let familyDecision = 'no-action';
922
- let familyTargetCodes: string[] = [];
923
- let familyMissingCodes: string[] = [];
924
- let familyLinkedCodes: string[] = [];
925
- let familyAllCodes: string[] = [];
926
895
 
927
896
  if (familyCodes?.size) {
928
- familyAllCodes = [...familyCodes].sort((a, b) => a.localeCompare(b));
929
- familyLinkedCodes = familyAllCodes.filter((code) => linkedFullCodes.has(code));
930
897
  // Base mention ("SR0054") validates against child coverage when children exist.
931
898
  // If no child variants exist, fallback to the single standalone requirement code.
932
899
  if (hasBaseMention) {
933
900
  const familyCodesList = [...familyCodes];
934
901
  const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
935
902
  const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
936
- familyTargetCodes = [...targetFamilyCodes].sort((a, b) => a.localeCompare(b));
937
903
  const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
938
- familyMissingCodes = [...missingInTargetFamily].sort((a, b) => a.localeCompare(b));
939
904
 
940
905
  if (missingInTargetFamily.length > 0) {
941
906
  const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
942
907
  if (!hasAnyLinkedInFamily) {
943
908
  missingBaseWhenFamilyUncovered.add(baseKey);
944
- familyDecision = 'base-mentioned-family-uncovered';
945
909
  } else {
946
910
  for (const code of missingInTargetFamily) {
947
911
  missingFamilyMembers.add(code);
948
912
  }
949
- familyDecision = 'base-mentioned-family-partial-missing-children';
950
913
  }
951
- } else {
952
- familyDecision = 'base-mentioned-family-fully-covered';
953
914
  }
954
- logger.debug(
955
- `MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
956
- `mode=baseMention mentioned='${logList(mentionedCodesList)}' familyAll='${logList(familyAllCodes)}' ` +
957
- `target='${logList(familyTargetCodes)}' linked='${logList(familyLinkedCodes)}' ` +
958
- `missing='${logList(familyMissingCodes)}' decision=${familyDecision}`
959
- );
960
915
  continue;
961
916
  }
962
917
 
963
918
  // Specific mention ("SR0054-1") validates as exact-match only.
964
- const missingSpecificMembers: string[] = [];
965
919
  for (const code of mentionedSpecificMembers) {
966
920
  if (!linkedFullCodes.has(code)) {
967
921
  missingSpecificMentionedNoFamily.add(code);
968
- missingSpecificMembers.push(code);
969
922
  }
970
923
  }
971
- familyDecision =
972
- missingSpecificMembers.length > 0
973
- ? 'specific-mentioned-exact-missing'
974
- : 'specific-mentioned-exact-covered';
975
- logger.debug(
976
- `MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
977
- `mode=specificMention mentioned='${logList(mentionedCodesList)}' familyAll='${logList(familyAllCodes)}' ` +
978
- `linked='${logList(familyLinkedCodes)}' missingSpecific='${logList(missingSpecificMembers)}' ` +
979
- `decision=${familyDecision}`
980
- );
981
924
  continue;
982
925
  }
983
926
 
984
927
  // Fallback path when family data is unavailable for this base key.
985
- const fallbackMissingSpecific: string[] = [];
986
- let fallbackBaseMissing = false;
987
928
  for (const code of mentionedCodes) {
988
929
  const hasSpecificSuffix = /-\d+$/.test(code);
989
930
  if (hasSpecificSuffix) {
990
931
  if (!linkedFullCodes.has(code)) {
991
932
  missingSpecificMentionedNoFamily.add(code);
992
- fallbackMissingSpecific.push(code);
993
933
  }
994
934
  } else if (!linkedBaseKeys.has(baseKey)) {
995
935
  missingBaseWhenFamilyUncovered.add(baseKey);
996
- fallbackBaseMissing = true;
997
936
  }
998
937
  }
999
- logger.debug(
1000
- `MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
1001
- `mode=noFamilyData mentioned='${logList(mentionedCodesList)}' linkedBasePresent=${linkedBaseKeys.has(baseKey)} ` +
1002
- `missingSpecific='${logList(fallbackMissingSpecific)}' missingBase=${fallbackBaseMissing}`
1003
- );
1004
938
  }
1005
939
 
1006
940
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -1063,16 +997,8 @@ export default class ResultDataProvider {
1063
997
  const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
1064
998
  return `${stepRef}: ${groupedRequirementList}`;
1065
999
  })
1066
- .join('; ');
1000
+ .join('\n');
1067
1001
  const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
1068
- const rawMentionedByStepForLog = [...mentionedButNotLinkedByStep.entries()]
1069
- .map(([stepRef, requirementIds]) => `${stepRef}=[${logList(requirementIds, 8)}]`)
1070
- .join(' | ');
1071
- logger.debug(
1072
- `MEWP internal validation grouped diagnostics: testCaseId=${testCaseId} ` +
1073
- `rawMentionedByStep='${rawMentionedByStepForLog}' groupedMentioned='${mentionedButNotLinked}' ` +
1074
- `rawLinkedOnlyByFamily='${logByFamily(sortedExtraLinked)}' groupedLinkedOnly='${linkedButNotMentioned}'`
1075
- );
1076
1002
  const validationStatus: 'Pass' | 'Fail' =
1077
1003
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1078
1004
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -1534,139 +1460,10 @@ export default class ResultDataProvider {
1534
1460
  private async fetchMewpScopedTestData(
1535
1461
  testPlanId: string,
1536
1462
  projectName: string,
1537
- selectedSuiteIds: number[] | undefined,
1538
- useRelFallback: boolean
1463
+ selectedSuiteIds: number[] | undefined
1539
1464
  ): Promise<any[]> {
1540
- if (!useRelFallback) {
1541
- const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1542
- return this.fetchTestData(suites, projectName, testPlanId, false);
1543
- }
1544
-
1545
- const selectedSuites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1546
- const selectedRel = this.resolveMaxRelNumberFromSuites(selectedSuites);
1547
- if (selectedRel <= 0) {
1548
- return this.fetchTestData(selectedSuites, projectName, testPlanId, false);
1549
- }
1550
-
1551
- const allSuites = await this.fetchTestSuites(testPlanId, projectName, undefined, true);
1552
- const relScopedSuites = allSuites.filter((suite) => {
1553
- const rel = this.extractRelNumberFromSuite(suite);
1554
- return rel > 0 && rel <= selectedRel;
1555
- });
1556
- const suitesForFetch = relScopedSuites.length > 0 ? relScopedSuites : selectedSuites;
1557
- const rawTestData = await this.fetchTestData(suitesForFetch, projectName, testPlanId, false);
1558
- return this.reduceToLatestRelRunPerTestCase(rawTestData);
1559
- }
1560
-
1561
- private extractRelNumberFromSuite(suite: any): number {
1562
- const candidates = [
1563
- suite?.suiteName,
1564
- suite?.parentSuiteName,
1565
- suite?.suitePath,
1566
- suite?.testGroupName,
1567
- ];
1568
- const pattern = /(?:^|[^a-z0-9])rel\s*([0-9]+)/i;
1569
- for (const item of candidates) {
1570
- const match = pattern.exec(String(item || ''));
1571
- if (!match) continue;
1572
- const parsed = Number(match[1]);
1573
- if (Number.isFinite(parsed) && parsed > 0) {
1574
- return parsed;
1575
- }
1576
- }
1577
- return 0;
1578
- }
1579
-
1580
- private resolveMaxRelNumberFromSuites(suites: any[]): number {
1581
- let maxRel = 0;
1582
- for (const suite of suites || []) {
1583
- const rel = this.extractRelNumberFromSuite(suite);
1584
- if (rel > maxRel) maxRel = rel;
1585
- }
1586
- return maxRel;
1587
- }
1588
-
1589
- private reduceToLatestRelRunPerTestCase(testData: any[]): any[] {
1590
- type Candidate = {
1591
- point: any;
1592
- rel: number;
1593
- runId: number;
1594
- resultId: number;
1595
- hasRun: boolean;
1596
- };
1597
-
1598
- const candidatesByTestCase = new Map<number, Candidate[]>();
1599
- const testCaseDefinitionById = new Map<number, any>();
1600
-
1601
- for (const suite of testData || []) {
1602
- const rel = this.extractRelNumberFromSuite(suite);
1603
- const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
1604
- const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
1605
-
1606
- for (const testCase of testCasesItems) {
1607
- const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
1608
- if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
1609
- if (!testCaseDefinitionById.has(testCaseId)) {
1610
- testCaseDefinitionById.set(testCaseId, testCase);
1611
- }
1612
- }
1613
-
1614
- for (const point of testPointsItems) {
1615
- const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
1616
- if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
1617
-
1618
- const runId = Number(point?.lastRunId || 0);
1619
- const resultId = Number(point?.lastResultId || 0);
1620
- const hasRun = runId > 0 && resultId > 0;
1621
- if (!candidatesByTestCase.has(testCaseId)) {
1622
- candidatesByTestCase.set(testCaseId, []);
1623
- }
1624
- candidatesByTestCase.get(testCaseId)!.push({
1625
- point,
1626
- rel,
1627
- runId,
1628
- resultId,
1629
- hasRun,
1630
- });
1631
- }
1632
- }
1633
-
1634
- const selectedPoints: any[] = [];
1635
- const selectedTestCaseIds = new Set<number>();
1636
- for (const [testCaseId, candidates] of candidatesByTestCase.entries()) {
1637
- const sorted = [...candidates].sort((a, b) => {
1638
- if (a.hasRun !== b.hasRun) return a.hasRun ? -1 : 1;
1639
- if (a.rel !== b.rel) return b.rel - a.rel;
1640
- if (a.runId !== b.runId) return b.runId - a.runId;
1641
- return b.resultId - a.resultId;
1642
- });
1643
- const chosen = sorted[0];
1644
- if (!chosen?.point) continue;
1645
- selectedPoints.push(chosen.point);
1646
- selectedTestCaseIds.add(testCaseId);
1647
- }
1648
-
1649
- const selectedTestCases: any[] = [];
1650
- for (const testCaseId of selectedTestCaseIds) {
1651
- const definition = testCaseDefinitionById.get(testCaseId);
1652
- if (definition) {
1653
- selectedTestCases.push(definition);
1654
- }
1655
- }
1656
-
1657
- return [
1658
- {
1659
- testSuiteId: 'MEWP_REL_SCOPED',
1660
- suiteId: 'MEWP_REL_SCOPED',
1661
- suiteName: 'MEWP Rel Scoped',
1662
- parentSuiteId: '',
1663
- parentSuiteName: '',
1664
- suitePath: 'MEWP Rel Scoped',
1665
- testGroupName: 'MEWP Rel Scoped',
1666
- testPointsItems: selectedPoints,
1667
- testCasesItems: selectedTestCases,
1668
- },
1669
- ];
1465
+ const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1466
+ return this.fetchTestData(suites, projectName, testPlanId, false);
1670
1467
  }
1671
1468
 
1672
1469
  private async loadExternalBugsByTestCase(
@@ -2010,16 +1807,6 @@ export default class ResultDataProvider {
2010
1807
  return out;
2011
1808
  }
2012
1809
 
2013
- private extractRequirementCodesFromExpectedSteps(steps: TestSteps[], includeSuffix: boolean): Set<string> {
2014
- const out = new Set<string>();
2015
- for (const step of Array.isArray(steps) ? steps : []) {
2016
- if (step?.isSharedStepTitle) continue;
2017
- const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
2018
- codes.forEach((code) => out.add(code));
2019
- }
2020
- return out;
2021
- }
2022
-
2023
1810
  private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
2024
1811
  const out = new Set<string>();
2025
1812
  const source = this.normalizeRequirementStepText(text);
@@ -2862,7 +2649,7 @@ export default class ResultDataProvider {
2862
2649
  if (sortedMembers.length <= 1) return sortedMembers[0];
2863
2650
  return `${baseKey}: ${sortedMembers.join(', ')}`;
2864
2651
  })
2865
- .join('; ');
2652
+ .join('\n');
2866
2653
  }
2867
2654
 
2868
2655
  private toMewpComparableText(value: any): string {
@@ -3183,6 +2970,140 @@ export default class ResultDataProvider {
3183
2970
  return testCases;
3184
2971
  }
3185
2972
 
2973
+ private attachSuiteTestCaseContextToPoints(testCasesItems: any[], testPointsItems: any[]): any[] {
2974
+ const points = Array.isArray(testPointsItems) ? testPointsItems : [];
2975
+ const testCases = Array.isArray(testCasesItems) ? testCasesItems : [];
2976
+ if (points.length === 0 || testCases.length === 0) return points;
2977
+
2978
+ const byTestCaseId = new Map<number, any>();
2979
+ for (const testCaseItem of testCases) {
2980
+ const testCaseId = Number(
2981
+ testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || 0
2982
+ );
2983
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0 || byTestCaseId.has(testCaseId)) continue;
2984
+ byTestCaseId.set(testCaseId, testCaseItem);
2985
+ }
2986
+
2987
+ return points.map((point: any) => {
2988
+ const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
2989
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return point;
2990
+ const suiteTestCase = byTestCaseId.get(testCaseId);
2991
+ if (!suiteTestCase) return point;
2992
+ return { ...point, suiteTestCase };
2993
+ });
2994
+ }
2995
+
2996
+ private extractWorkItemFieldsMap(workItemFields: any): Record<string, any> {
2997
+ const fields: Record<string, any> = {};
2998
+ if (!Array.isArray(workItemFields)) return fields;
2999
+ for (const field of workItemFields) {
3000
+ const keyCandidates = [field?.key, field?.name, field?.referenceName]
3001
+ .map((item) => String(item || '').trim())
3002
+ .filter((item) => !!item);
3003
+ for (const key of keyCandidates) {
3004
+ fields[key] = field?.value;
3005
+ }
3006
+ }
3007
+ return fields;
3008
+ }
3009
+
3010
+ private resolveSuiteTestCaseRevision(testCaseItem: any): number {
3011
+ const revisionCandidates = [
3012
+ testCaseItem?.workItem?.rev,
3013
+ testCaseItem?.workItem?.revision,
3014
+ testCaseItem?.workItem?.version,
3015
+ testCaseItem?.workItem?.workItemRevision,
3016
+ testCaseItem?.workItem?.workItemVersion,
3017
+ testCaseItem?.revision,
3018
+ testCaseItem?.workItemRevision,
3019
+ testCaseItem?.workItemVersion,
3020
+ ];
3021
+ for (const candidate of revisionCandidates) {
3022
+ const parsed = Number(candidate || 0);
3023
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3024
+ }
3025
+ return 0;
3026
+ }
3027
+
3028
+ private buildWorkItemSnapshotFromSuiteTestCase(
3029
+ testCaseItem: any,
3030
+ fallbackTestCaseId: number,
3031
+ fallbackTestCaseName: string = ''
3032
+ ): any | null {
3033
+ if (!testCaseItem) return null;
3034
+
3035
+ const testCaseId = Number(
3036
+ testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || fallbackTestCaseId || 0
3037
+ );
3038
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return null;
3039
+
3040
+ const workItem = testCaseItem?.workItem || {};
3041
+ const stepsXml = this.extractStepsXmlFromTestCaseItem(testCaseItem);
3042
+ const fieldsFromList = this.extractWorkItemFieldsMap(workItem?.workItemFields);
3043
+ const fieldsFromMap = workItem?.fields || {};
3044
+ const fields = {
3045
+ ...fieldsFromList,
3046
+ ...fieldsFromMap,
3047
+ };
3048
+
3049
+ if (!fields['System.Title']) {
3050
+ const title = String(
3051
+ testCaseItem?.testCaseName || workItem?.name || testCaseItem?.name || fallbackTestCaseName || ''
3052
+ ).trim();
3053
+ if (title) {
3054
+ fields['System.Title'] = title;
3055
+ }
3056
+ }
3057
+ if (stepsXml && !fields['Microsoft.VSTS.TCM.Steps']) {
3058
+ fields['Microsoft.VSTS.TCM.Steps'] = stepsXml;
3059
+ }
3060
+
3061
+ return {
3062
+ id: testCaseId,
3063
+ rev: this.resolveSuiteTestCaseRevision(testCaseItem) || 1,
3064
+ fields,
3065
+ relations: Array.isArray(workItem?.relations) ? workItem.relations : [],
3066
+ };
3067
+ }
3068
+
3069
+ private async fetchWorkItemByRevision(
3070
+ projectName: string,
3071
+ workItemId: number,
3072
+ revision: number,
3073
+ expandAll: boolean = false
3074
+ ): Promise<any | null> {
3075
+ const id = Number(workItemId || 0);
3076
+ const rev = Number(revision || 0);
3077
+ if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(rev) || rev <= 0) return null;
3078
+
3079
+ const expandParam = expandAll ? '?$expand=all' : '';
3080
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
3081
+ try {
3082
+ return await TFSServices.getItemContent(url, this.token);
3083
+ } catch (error: any) {
3084
+ logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
3085
+ return null;
3086
+ }
3087
+ }
3088
+
3089
+ private async fetchWorkItemLatest(
3090
+ projectName: string,
3091
+ workItemId: number,
3092
+ expandAll: boolean = false
3093
+ ): Promise<any | null> {
3094
+ const id = Number(workItemId || 0);
3095
+ if (!Number.isFinite(id) || id <= 0) return null;
3096
+
3097
+ const expandParam = expandAll ? '?$expand=all' : '';
3098
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
3099
+ try {
3100
+ return await TFSServices.getItemContent(url, this.token);
3101
+ } catch (error: any) {
3102
+ logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
3103
+ return null;
3104
+ }
3105
+ }
3106
+
3186
3107
  /**
3187
3108
  * Fetches result data based on the Work Item Test Reporter.
3188
3109
  *
@@ -3217,13 +3138,37 @@ export default class ResultDataProvider {
3217
3138
  logger.warn(`Invalid run result ${runId} or result ${resultId}`);
3218
3139
  return null;
3219
3140
  }
3220
- const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${point.testCaseId}?$expand=all`;
3221
- const testCaseData = await TFSServices.getItemContent(url, this.token);
3141
+ const suiteTestCaseItem = point?.suiteTestCase;
3142
+ const testCaseId = Number(
3143
+ point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
3144
+ );
3145
+ const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
3146
+ const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3147
+ suiteTestCaseItem,
3148
+ testCaseId,
3149
+ String(point?.testCaseName || '')
3150
+ );
3151
+ let testCaseData = await this.fetchWorkItemByRevision(
3152
+ projectName,
3153
+ testCaseId,
3154
+ suiteTestCaseRevision,
3155
+ isTestReporter
3156
+ );
3157
+ if (!testCaseData) {
3158
+ testCaseData = fallbackSnapshot;
3159
+ }
3160
+ if (!testCaseData) {
3161
+ testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
3162
+ }
3163
+ if (!testCaseData) {
3164
+ logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
3165
+ return null;
3166
+ }
3222
3167
  const newResultData: PlainTestResult = {
3223
3168
  id: 0,
3224
3169
  outcome: point.outcome,
3225
- revision: testCaseData?.rev || 1,
3226
- testCase: { id: point.testCaseId, name: point.testCaseName },
3170
+ revision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3171
+ testCase: { id: String(testCaseId), name: point.testCaseName },
3227
3172
  state: testCaseData?.fields?.['System.State'] || 'Active',
3228
3173
  priority: testCaseData?.fields?.['Microsoft.VSTS.TCM.Priority'] || 0,
3229
3174
  createdDate: testCaseData?.fields?.['System.CreatedDate'] || '0001-01-01T00:00:00',
@@ -3271,8 +3216,8 @@ export default class ResultDataProvider {
3271
3216
  selectedFieldSet.clear();
3272
3217
  return {
3273
3218
  ...newResultData,
3274
- stepsResultXml: testCaseData.fields['Microsoft.VSTS.TCM.Steps'] || undefined,
3275
- testCaseRevision: testCaseData.rev,
3219
+ stepsResultXml: testCaseData?.fields?.['Microsoft.VSTS.TCM.Steps'] || undefined,
3220
+ testCaseRevision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3276
3221
  filteredFields,
3277
3222
  relatedRequirements,
3278
3223
  relatedBugs,
@@ -3442,13 +3387,6 @@ export default class ResultDataProvider {
3442
3387
  );
3443
3388
  }
3444
3389
 
3445
- private isRunResultDebugEnabled(): boolean {
3446
- return (
3447
- String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '').toLowerCase() === 'true' ||
3448
- String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '') === '1'
3449
- );
3450
- }
3451
-
3452
3390
  private extractCommentText(comment: AdoWorkItemComment): string {
3453
3391
  const rendered = comment?.renderedText;
3454
3392
  // In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
@@ -3935,6 +3873,79 @@ export default class ResultDataProvider {
3935
3873
  return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId);
3936
3874
  }
3937
3875
 
3876
+ private logMewpRunScenarioDebugMatrix(runResults: any[], contextLabel: string): void {
3877
+ const results = Array.isArray(runResults) ? runResults : [];
3878
+ const matrix = {
3879
+ total: results.length,
3880
+ passOrFailWithActionResults: 0,
3881
+ runWithNoActionResults: 0,
3882
+ notApplicable: 0,
3883
+ noRunHistoryActive: 0,
3884
+ other: 0,
3885
+ };
3886
+ const samples = {
3887
+ passOrFailWithActionResults: [] as number[],
3888
+ runWithNoActionResults: [] as number[],
3889
+ notApplicable: [] as number[],
3890
+ noRunHistoryActive: [] as number[],
3891
+ other: [] as number[],
3892
+ };
3893
+
3894
+ const pushSample = (bucket: keyof typeof samples, id: number) => {
3895
+ if (!Number.isFinite(id) || id <= 0) return;
3896
+ if (samples[bucket].length >= 5) return;
3897
+ samples[bucket].push(id);
3898
+ };
3899
+
3900
+ for (const item of results) {
3901
+ const testCaseId = Number(item?.testCaseId || item?.testCase?.id || 0);
3902
+ const hasRun = Number(item?.lastRunId || 0) > 0 && Number(item?.lastResultId || 0) > 0;
3903
+ const rawOutcome = String(item?._debugTestOutcome || '').trim().toLowerCase();
3904
+ const rawState = String(item?._debugTestCaseState || '').trim().toLowerCase();
3905
+ const originalActionResultsCount = Number(item?._debugOriginalActionResultsCount ?? -1);
3906
+
3907
+ if (rawOutcome === 'notapplicable' || rawOutcome === 'not applicable') {
3908
+ matrix.notApplicable += 1;
3909
+ pushSample('notApplicable', testCaseId);
3910
+ continue;
3911
+ }
3912
+
3913
+ if (hasRun && (rawOutcome === 'passed' || rawOutcome === 'failed') && originalActionResultsCount > 0) {
3914
+ matrix.passOrFailWithActionResults += 1;
3915
+ pushSample('passOrFailWithActionResults', testCaseId);
3916
+ continue;
3917
+ }
3918
+
3919
+ if (hasRun && originalActionResultsCount === 0) {
3920
+ matrix.runWithNoActionResults += 1;
3921
+ pushSample('runWithNoActionResults', testCaseId);
3922
+ continue;
3923
+ }
3924
+
3925
+ if (!hasRun && rawState === 'active') {
3926
+ matrix.noRunHistoryActive += 1;
3927
+ pushSample('noRunHistoryActive', testCaseId);
3928
+ continue;
3929
+ }
3930
+
3931
+ matrix.other += 1;
3932
+ pushSample('other', testCaseId);
3933
+ }
3934
+
3935
+ logger.info(
3936
+ `MEWP run debug matrix (${contextLabel}): total=${matrix.total}; ` +
3937
+ `passOrFailWithActionResults=${matrix.passOrFailWithActionResults}; ` +
3938
+ `runWithNoActionResults=${matrix.runWithNoActionResults}; ` +
3939
+ `notApplicable=${matrix.notApplicable}; ` +
3940
+ `noRunHistoryActive=${matrix.noRunHistoryActive}; other=${matrix.other}; ` +
3941
+ `samplePassFail=${samples.passOrFailWithActionResults.join(',') || '-'}; ` +
3942
+ `sampleNoAction=${samples.runWithNoActionResults.join(',') || '-'}; ` +
3943
+ `sampleNA=${samples.notApplicable.join(',') || '-'}; ` +
3944
+ `sampleNoRunActive=${samples.noRunHistoryActive.join(',') || '-'}; ` +
3945
+ `sampleOther=${samples.other.join(',') || '-'}`
3946
+ );
3947
+ }
3948
+
3938
3949
  /**
3939
3950
  * Converts a run status string into a human-readable format.
3940
3951
  *
@@ -4210,9 +4221,17 @@ export default class ResultDataProvider {
4210
4221
  suite.testSuiteId
4211
4222
  );
4212
4223
  const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
4213
- const testPointsItems = !fetchCrossPlans
4214
- ? await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
4224
+ const rawTestPointsItems = !fetchCrossPlans
4225
+ ? await this.fetchTestPoints(
4226
+ projectName,
4227
+ testPlanId,
4228
+ suite.testSuiteId
4229
+ )
4215
4230
  : await this.fetchCrossTestPoints(projectName, testCaseIds);
4231
+ const testPointsItems = this.attachSuiteTestCaseContextToPoints(
4232
+ testCasesItems,
4233
+ rawTestPointsItems
4234
+ );
4216
4235
 
4217
4236
  return { ...suite, testPointsItems, testCasesItems };
4218
4237
  } catch (error: any) {
@@ -4362,6 +4381,11 @@ export default class ResultDataProvider {
4362
4381
  resultData.iterationDetails.push(iteration);
4363
4382
  }
4364
4383
 
4384
+ const originalActionResultsCount = Array.isArray(iteration?.actionResults)
4385
+ ? iteration.actionResults.length
4386
+ : 0;
4387
+ resultData._debugOriginalActionResultsCount = originalActionResultsCount;
4388
+
4365
4389
  if (resultData.stepsResultXml && iteration) {
4366
4390
  const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
4367
4391
  const actionResultsWithSharedModels = actionResults.filter(
@@ -5074,6 +5098,7 @@ export default class ResultDataProvider {
5074
5098
  resultData.iterationDetails?.length > 0
5075
5099
  ? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
5076
5100
  : undefined;
5101
+ const debugOutcome = this.getTestOutcome(resultData);
5077
5102
 
5078
5103
  if (!resultData?.testCase || !resultData?.testSuite) {
5079
5104
  logger.debug(
@@ -5107,6 +5132,9 @@ export default class ResultDataProvider {
5107
5132
  relatedCRs: resultData.relatedCRs || undefined,
5108
5133
  lastRunResult: undefined as any,
5109
5134
  customFields: {}, // Create an object to store custom fields
5135
+ _debugTestOutcome: debugOutcome,
5136
+ _debugTestCaseState: String(resultData?.state || ''),
5137
+ _debugOriginalActionResultsCount: Number(resultData?._debugOriginalActionResultsCount ?? -1),
5110
5138
  };
5111
5139
 
5112
5140
  // Process all custom fields from resultData.filteredFields
@@ -5126,15 +5154,14 @@ export default class ResultDataProvider {
5126
5154
  resultDataResponse.priority = resultData.priority;
5127
5155
  break;
5128
5156
  case 'testCaseResult':
5129
- const outcome = this.getTestOutcome(resultData);
5130
5157
  if (lastRunId === undefined || lastResultId === undefined) {
5131
5158
  resultDataResponse.testCaseResult = {
5132
- resultMessage: `${this.convertRunStatus(outcome)}`,
5159
+ resultMessage: `${this.convertRunStatus(debugOutcome)}`,
5133
5160
  url: '',
5134
5161
  };
5135
5162
  } else {
5136
5163
  resultDataResponse.testCaseResult = {
5137
- resultMessage: `${this.convertRunStatus(outcome)} in Run ${lastRunId}`,
5164
+ resultMessage: `${this.convertRunStatus(debugOutcome)} in Run ${lastRunId}`,
5138
5165
  url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
5139
5166
  };
5140
5167
  }