@elisra-devops/docgen-data-provider 1.88.0 → 1.90.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.88.0",
3
+ "version": "1.90.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,15 +13,10 @@ export interface MewpExternalFileRef {
13
13
  }
14
14
 
15
15
  export interface MewpCoverageRequestOptions {
16
- useRelFallback?: boolean;
17
16
  externalBugsFile?: MewpExternalFileRef | null;
18
17
  externalL3L4File?: MewpExternalFileRef | null;
19
18
  }
20
19
 
21
- export interface MewpInternalValidationRequestOptions {
22
- useRelFallback?: boolean;
23
- }
24
-
25
20
  export interface MewpRequirementStepSummary {
26
21
  passed: number;
27
22
  failed: number;
@@ -13,7 +13,6 @@ import type {
13
13
  MewpExternalTableValidationResult,
14
14
  MewpCoverageRow,
15
15
  MewpInternalValidationFlatPayload,
16
- MewpInternalValidationRequestOptions,
17
16
  MewpInternalValidationRow,
18
17
  MewpL2RequirementFamily,
19
18
  MewpL2RequirementWorkItem,
@@ -446,8 +445,7 @@ export default class ResultDataProvider {
446
445
  const testData = await this.fetchMewpScopedTestData(
447
446
  testPlanId,
448
447
  projectName,
449
- selectedSuiteIds,
450
- !!options?.useRelFallback
448
+ selectedSuiteIds
451
449
  );
452
450
 
453
451
  const allRequirements = await this.fetchMewpL2Requirements(projectName);
@@ -734,8 +732,7 @@ export default class ResultDataProvider {
734
732
  testPlanId: string,
735
733
  projectName: string,
736
734
  selectedSuiteIds: number[] | undefined,
737
- linkedQueryRequest?: any,
738
- options?: MewpInternalValidationRequestOptions
735
+ linkedQueryRequest?: any
739
736
  ): Promise<MewpInternalValidationFlatPayload> {
740
737
  const defaultPayload: MewpInternalValidationFlatPayload = {
741
738
  sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
@@ -748,8 +745,7 @@ export default class ResultDataProvider {
748
745
  const testData = await this.fetchMewpScopedTestData(
749
746
  testPlanId,
750
747
  projectName,
751
- selectedSuiteIds,
752
- !!options?.useRelFallback
748
+ selectedSuiteIds
753
749
  );
754
750
  const allRequirements = await this.fetchMewpL2Requirements(projectName);
755
751
  const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
@@ -808,34 +804,6 @@ export default class ResultDataProvider {
808
804
 
809
805
  for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
810
806
  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
807
  const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
840
808
  const parsedSteps =
841
809
  stepsXml && String(stepsXml).trim() !== ''
@@ -888,15 +856,6 @@ export default class ResultDataProvider {
888
856
  const linkedBaseKeys = new Set<string>(
889
857
  [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
890
858
  );
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
859
 
901
860
  const mentionedCodesByBase = new Map<string, Set<string>>();
902
861
  for (const code of mentionedL2Only) {
@@ -918,89 +877,49 @@ export default class ResultDataProvider {
918
877
  const mentionedCodesList = [...mentionedCodes];
919
878
  const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
920
879
  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
880
 
927
881
  if (familyCodes?.size) {
928
- familyAllCodes = [...familyCodes].sort((a, b) => a.localeCompare(b));
929
- familyLinkedCodes = familyAllCodes.filter((code) => linkedFullCodes.has(code));
930
882
  // Base mention ("SR0054") validates against child coverage when children exist.
931
883
  // If no child variants exist, fallback to the single standalone requirement code.
932
884
  if (hasBaseMention) {
933
885
  const familyCodesList = [...familyCodes];
934
886
  const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
935
887
  const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
936
- familyTargetCodes = [...targetFamilyCodes].sort((a, b) => a.localeCompare(b));
937
888
  const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
938
- familyMissingCodes = [...missingInTargetFamily].sort((a, b) => a.localeCompare(b));
939
889
 
940
890
  if (missingInTargetFamily.length > 0) {
941
891
  const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
942
892
  if (!hasAnyLinkedInFamily) {
943
893
  missingBaseWhenFamilyUncovered.add(baseKey);
944
- familyDecision = 'base-mentioned-family-uncovered';
945
894
  } else {
946
895
  for (const code of missingInTargetFamily) {
947
896
  missingFamilyMembers.add(code);
948
897
  }
949
- familyDecision = 'base-mentioned-family-partial-missing-children';
950
898
  }
951
- } else {
952
- familyDecision = 'base-mentioned-family-fully-covered';
953
899
  }
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
900
  continue;
961
901
  }
962
902
 
963
903
  // Specific mention ("SR0054-1") validates as exact-match only.
964
- const missingSpecificMembers: string[] = [];
965
904
  for (const code of mentionedSpecificMembers) {
966
905
  if (!linkedFullCodes.has(code)) {
967
906
  missingSpecificMentionedNoFamily.add(code);
968
- missingSpecificMembers.push(code);
969
907
  }
970
908
  }
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
909
  continue;
982
910
  }
983
911
 
984
912
  // Fallback path when family data is unavailable for this base key.
985
- const fallbackMissingSpecific: string[] = [];
986
- let fallbackBaseMissing = false;
987
913
  for (const code of mentionedCodes) {
988
914
  const hasSpecificSuffix = /-\d+$/.test(code);
989
915
  if (hasSpecificSuffix) {
990
916
  if (!linkedFullCodes.has(code)) {
991
917
  missingSpecificMentionedNoFamily.add(code);
992
- fallbackMissingSpecific.push(code);
993
918
  }
994
919
  } else if (!linkedBaseKeys.has(baseKey)) {
995
920
  missingBaseWhenFamilyUncovered.add(baseKey);
996
- fallbackBaseMissing = true;
997
921
  }
998
922
  }
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
923
  }
1005
924
 
1006
925
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -1063,16 +982,8 @@ export default class ResultDataProvider {
1063
982
  const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
1064
983
  return `${stepRef}: ${groupedRequirementList}`;
1065
984
  })
1066
- .join('; ');
985
+ .join('\n');
1067
986
  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
987
  const validationStatus: 'Pass' | 'Fail' =
1077
988
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1078
989
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -1534,139 +1445,10 @@ export default class ResultDataProvider {
1534
1445
  private async fetchMewpScopedTestData(
1535
1446
  testPlanId: string,
1536
1447
  projectName: string,
1537
- selectedSuiteIds: number[] | undefined,
1538
- useRelFallback: boolean
1448
+ selectedSuiteIds: number[] | undefined
1539
1449
  ): 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
- ];
1450
+ const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1451
+ return this.fetchTestData(suites, projectName, testPlanId, false);
1670
1452
  }
1671
1453
 
1672
1454
  private async loadExternalBugsByTestCase(
@@ -2010,16 +1792,6 @@ export default class ResultDataProvider {
2010
1792
  return out;
2011
1793
  }
2012
1794
 
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
1795
  private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
2024
1796
  const out = new Set<string>();
2025
1797
  const source = this.normalizeRequirementStepText(text);
@@ -2862,7 +2634,7 @@ export default class ResultDataProvider {
2862
2634
  if (sortedMembers.length <= 1) return sortedMembers[0];
2863
2635
  return `${baseKey}: ${sortedMembers.join(', ')}`;
2864
2636
  })
2865
- .join('; ');
2637
+ .join('\n');
2866
2638
  }
2867
2639
 
2868
2640
  private toMewpComparableText(value: any): string {
@@ -3183,6 +2955,140 @@ export default class ResultDataProvider {
3183
2955
  return testCases;
3184
2956
  }
3185
2957
 
2958
+ private attachSuiteTestCaseContextToPoints(testCasesItems: any[], testPointsItems: any[]): any[] {
2959
+ const points = Array.isArray(testPointsItems) ? testPointsItems : [];
2960
+ const testCases = Array.isArray(testCasesItems) ? testCasesItems : [];
2961
+ if (points.length === 0 || testCases.length === 0) return points;
2962
+
2963
+ const byTestCaseId = new Map<number, any>();
2964
+ for (const testCaseItem of testCases) {
2965
+ const testCaseId = Number(
2966
+ testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || 0
2967
+ );
2968
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0 || byTestCaseId.has(testCaseId)) continue;
2969
+ byTestCaseId.set(testCaseId, testCaseItem);
2970
+ }
2971
+
2972
+ return points.map((point: any) => {
2973
+ const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
2974
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return point;
2975
+ const suiteTestCase = byTestCaseId.get(testCaseId);
2976
+ if (!suiteTestCase) return point;
2977
+ return { ...point, suiteTestCase };
2978
+ });
2979
+ }
2980
+
2981
+ private extractWorkItemFieldsMap(workItemFields: any): Record<string, any> {
2982
+ const fields: Record<string, any> = {};
2983
+ if (!Array.isArray(workItemFields)) return fields;
2984
+ for (const field of workItemFields) {
2985
+ const keyCandidates = [field?.key, field?.name, field?.referenceName]
2986
+ .map((item) => String(item || '').trim())
2987
+ .filter((item) => !!item);
2988
+ for (const key of keyCandidates) {
2989
+ fields[key] = field?.value;
2990
+ }
2991
+ }
2992
+ return fields;
2993
+ }
2994
+
2995
+ private resolveSuiteTestCaseRevision(testCaseItem: any): number {
2996
+ const revisionCandidates = [
2997
+ testCaseItem?.workItem?.rev,
2998
+ testCaseItem?.workItem?.revision,
2999
+ testCaseItem?.workItem?.version,
3000
+ testCaseItem?.workItem?.workItemRevision,
3001
+ testCaseItem?.workItem?.workItemVersion,
3002
+ testCaseItem?.revision,
3003
+ testCaseItem?.workItemRevision,
3004
+ testCaseItem?.workItemVersion,
3005
+ ];
3006
+ for (const candidate of revisionCandidates) {
3007
+ const parsed = Number(candidate || 0);
3008
+ if (Number.isFinite(parsed) && parsed > 0) return parsed;
3009
+ }
3010
+ return 0;
3011
+ }
3012
+
3013
+ private buildWorkItemSnapshotFromSuiteTestCase(
3014
+ testCaseItem: any,
3015
+ fallbackTestCaseId: number,
3016
+ fallbackTestCaseName: string = ''
3017
+ ): any | null {
3018
+ if (!testCaseItem) return null;
3019
+
3020
+ const testCaseId = Number(
3021
+ testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || fallbackTestCaseId || 0
3022
+ );
3023
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return null;
3024
+
3025
+ const workItem = testCaseItem?.workItem || {};
3026
+ const stepsXml = this.extractStepsXmlFromTestCaseItem(testCaseItem);
3027
+ const fieldsFromList = this.extractWorkItemFieldsMap(workItem?.workItemFields);
3028
+ const fieldsFromMap = workItem?.fields || {};
3029
+ const fields = {
3030
+ ...fieldsFromList,
3031
+ ...fieldsFromMap,
3032
+ };
3033
+
3034
+ if (!fields['System.Title']) {
3035
+ const title = String(
3036
+ testCaseItem?.testCaseName || workItem?.name || testCaseItem?.name || fallbackTestCaseName || ''
3037
+ ).trim();
3038
+ if (title) {
3039
+ fields['System.Title'] = title;
3040
+ }
3041
+ }
3042
+ if (stepsXml && !fields['Microsoft.VSTS.TCM.Steps']) {
3043
+ fields['Microsoft.VSTS.TCM.Steps'] = stepsXml;
3044
+ }
3045
+
3046
+ return {
3047
+ id: testCaseId,
3048
+ rev: this.resolveSuiteTestCaseRevision(testCaseItem) || 1,
3049
+ fields,
3050
+ relations: Array.isArray(workItem?.relations) ? workItem.relations : [],
3051
+ };
3052
+ }
3053
+
3054
+ private async fetchWorkItemByRevision(
3055
+ projectName: string,
3056
+ workItemId: number,
3057
+ revision: number,
3058
+ expandAll: boolean = false
3059
+ ): Promise<any | null> {
3060
+ const id = Number(workItemId || 0);
3061
+ const rev = Number(revision || 0);
3062
+ if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(rev) || rev <= 0) return null;
3063
+
3064
+ const expandParam = expandAll ? '?$expand=all' : '';
3065
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
3066
+ try {
3067
+ return await TFSServices.getItemContent(url, this.token);
3068
+ } catch (error: any) {
3069
+ logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
3070
+ return null;
3071
+ }
3072
+ }
3073
+
3074
+ private async fetchWorkItemLatest(
3075
+ projectName: string,
3076
+ workItemId: number,
3077
+ expandAll: boolean = false
3078
+ ): Promise<any | null> {
3079
+ const id = Number(workItemId || 0);
3080
+ if (!Number.isFinite(id) || id <= 0) return null;
3081
+
3082
+ const expandParam = expandAll ? '?$expand=all' : '';
3083
+ const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
3084
+ try {
3085
+ return await TFSServices.getItemContent(url, this.token);
3086
+ } catch (error: any) {
3087
+ logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
3088
+ return null;
3089
+ }
3090
+ }
3091
+
3186
3092
  /**
3187
3093
  * Fetches result data based on the Work Item Test Reporter.
3188
3094
  *
@@ -3217,13 +3123,37 @@ export default class ResultDataProvider {
3217
3123
  logger.warn(`Invalid run result ${runId} or result ${resultId}`);
3218
3124
  return null;
3219
3125
  }
3220
- const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${point.testCaseId}?$expand=all`;
3221
- const testCaseData = await TFSServices.getItemContent(url, this.token);
3126
+ const suiteTestCaseItem = point?.suiteTestCase;
3127
+ const testCaseId = Number(
3128
+ point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
3129
+ );
3130
+ const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
3131
+ const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3132
+ suiteTestCaseItem,
3133
+ testCaseId,
3134
+ String(point?.testCaseName || '')
3135
+ );
3136
+ let testCaseData = await this.fetchWorkItemByRevision(
3137
+ projectName,
3138
+ testCaseId,
3139
+ suiteTestCaseRevision,
3140
+ isTestReporter
3141
+ );
3142
+ if (!testCaseData) {
3143
+ testCaseData = fallbackSnapshot;
3144
+ }
3145
+ if (!testCaseData) {
3146
+ testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
3147
+ }
3148
+ if (!testCaseData) {
3149
+ logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
3150
+ return null;
3151
+ }
3222
3152
  const newResultData: PlainTestResult = {
3223
3153
  id: 0,
3224
3154
  outcome: point.outcome,
3225
- revision: testCaseData?.rev || 1,
3226
- testCase: { id: point.testCaseId, name: point.testCaseName },
3155
+ revision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3156
+ testCase: { id: String(testCaseId), name: point.testCaseName },
3227
3157
  state: testCaseData?.fields?.['System.State'] || 'Active',
3228
3158
  priority: testCaseData?.fields?.['Microsoft.VSTS.TCM.Priority'] || 0,
3229
3159
  createdDate: testCaseData?.fields?.['System.CreatedDate'] || '0001-01-01T00:00:00',
@@ -3271,8 +3201,8 @@ export default class ResultDataProvider {
3271
3201
  selectedFieldSet.clear();
3272
3202
  return {
3273
3203
  ...newResultData,
3274
- stepsResultXml: testCaseData.fields['Microsoft.VSTS.TCM.Steps'] || undefined,
3275
- testCaseRevision: testCaseData.rev,
3204
+ stepsResultXml: testCaseData?.fields?.['Microsoft.VSTS.TCM.Steps'] || undefined,
3205
+ testCaseRevision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3276
3206
  filteredFields,
3277
3207
  relatedRequirements,
3278
3208
  relatedBugs,
@@ -3442,13 +3372,6 @@ export default class ResultDataProvider {
3442
3372
  );
3443
3373
  }
3444
3374
 
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
3375
  private extractCommentText(comment: AdoWorkItemComment): string {
3453
3376
  const rendered = comment?.renderedText;
3454
3377
  // In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
@@ -4210,9 +4133,17 @@ export default class ResultDataProvider {
4210
4133
  suite.testSuiteId
4211
4134
  );
4212
4135
  const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
4213
- const testPointsItems = !fetchCrossPlans
4214
- ? await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
4136
+ const rawTestPointsItems = !fetchCrossPlans
4137
+ ? await this.fetchTestPoints(
4138
+ projectName,
4139
+ testPlanId,
4140
+ suite.testSuiteId
4141
+ )
4215
4142
  : await this.fetchCrossTestPoints(projectName, testCaseIds);
4143
+ const testPointsItems = this.attachSuiteTestCaseContextToPoints(
4144
+ testCasesItems,
4145
+ rawTestPointsItems
4146
+ );
4216
4147
 
4217
4148
  return { ...suite, testPointsItems, testCasesItems };
4218
4149
  } catch (error: any) {
@@ -5074,6 +5005,7 @@ export default class ResultDataProvider {
5074
5005
  resultData.iterationDetails?.length > 0
5075
5006
  ? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
5076
5007
  : undefined;
5008
+ const testOutcome = this.getTestOutcome(resultData);
5077
5009
 
5078
5010
  if (!resultData?.testCase || !resultData?.testSuite) {
5079
5011
  logger.debug(
@@ -5126,15 +5058,14 @@ export default class ResultDataProvider {
5126
5058
  resultDataResponse.priority = resultData.priority;
5127
5059
  break;
5128
5060
  case 'testCaseResult':
5129
- const outcome = this.getTestOutcome(resultData);
5130
5061
  if (lastRunId === undefined || lastResultId === undefined) {
5131
5062
  resultDataResponse.testCaseResult = {
5132
- resultMessage: `${this.convertRunStatus(outcome)}`,
5063
+ resultMessage: `${this.convertRunStatus(testOutcome)}`,
5133
5064
  url: '',
5134
5065
  };
5135
5066
  } else {
5136
5067
  resultDataResponse.testCaseResult = {
5137
- resultMessage: `${this.convertRunStatus(outcome)} in Run ${lastRunId}`,
5068
+ resultMessage: `${this.convertRunStatus(testOutcome)} in Run ${lastRunId}`,
5138
5069
  url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
5139
5070
  };
5140
5071
  }