@elisra-devops/docgen-data-provider 1.87.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.87.0",
3
+ "version": "1.89.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,13 +13,13 @@ 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;
18
+ debugMode?: boolean;
19
19
  }
20
20
 
21
21
  export interface MewpInternalValidationRequestOptions {
22
- useRelFallback?: boolean;
22
+ debugMode?: boolean;
23
23
  }
24
24
 
25
25
  export interface MewpRequirementStepSummary {
@@ -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 = {
@@ -917,7 +928,9 @@ export default class ResultDataProvider {
917
928
  for (const code of mentionedCodes) {
918
929
  const hasSpecificSuffix = /-\d+$/.test(code);
919
930
  if (hasSpecificSuffix) {
920
- if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
931
+ if (!linkedFullCodes.has(code)) {
932
+ missingSpecificMentionedNoFamily.add(code);
933
+ }
921
934
  } else if (!linkedBaseKeys.has(baseKey)) {
922
935
  missingBaseWhenFamilyUncovered.add(baseKey);
923
936
  }
@@ -984,7 +997,7 @@ export default class ResultDataProvider {
984
997
  const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
985
998
  return `${stepRef}: ${groupedRequirementList}`;
986
999
  })
987
- .join('; ');
1000
+ .join('\n');
988
1001
  const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
989
1002
  const validationStatus: 'Pass' | 'Fail' =
990
1003
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
@@ -1447,139 +1460,10 @@ export default class ResultDataProvider {
1447
1460
  private async fetchMewpScopedTestData(
1448
1461
  testPlanId: string,
1449
1462
  projectName: string,
1450
- selectedSuiteIds: number[] | undefined,
1451
- useRelFallback: boolean
1463
+ selectedSuiteIds: number[] | undefined
1452
1464
  ): Promise<any[]> {
1453
- if (!useRelFallback) {
1454
- const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1455
- return this.fetchTestData(suites, projectName, testPlanId, false);
1456
- }
1457
-
1458
- const selectedSuites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1459
- const selectedRel = this.resolveMaxRelNumberFromSuites(selectedSuites);
1460
- if (selectedRel <= 0) {
1461
- return this.fetchTestData(selectedSuites, projectName, testPlanId, false);
1462
- }
1463
-
1464
- const allSuites = await this.fetchTestSuites(testPlanId, projectName, undefined, true);
1465
- const relScopedSuites = allSuites.filter((suite) => {
1466
- const rel = this.extractRelNumberFromSuite(suite);
1467
- return rel > 0 && rel <= selectedRel;
1468
- });
1469
- const suitesForFetch = relScopedSuites.length > 0 ? relScopedSuites : selectedSuites;
1470
- const rawTestData = await this.fetchTestData(suitesForFetch, projectName, testPlanId, false);
1471
- return this.reduceToLatestRelRunPerTestCase(rawTestData);
1472
- }
1473
-
1474
- private extractRelNumberFromSuite(suite: any): number {
1475
- const candidates = [
1476
- suite?.suiteName,
1477
- suite?.parentSuiteName,
1478
- suite?.suitePath,
1479
- suite?.testGroupName,
1480
- ];
1481
- const pattern = /(?:^|[^a-z0-9])rel\s*([0-9]+)/i;
1482
- for (const item of candidates) {
1483
- const match = pattern.exec(String(item || ''));
1484
- if (!match) continue;
1485
- const parsed = Number(match[1]);
1486
- if (Number.isFinite(parsed) && parsed > 0) {
1487
- return parsed;
1488
- }
1489
- }
1490
- return 0;
1491
- }
1492
-
1493
- private resolveMaxRelNumberFromSuites(suites: any[]): number {
1494
- let maxRel = 0;
1495
- for (const suite of suites || []) {
1496
- const rel = this.extractRelNumberFromSuite(suite);
1497
- if (rel > maxRel) maxRel = rel;
1498
- }
1499
- return maxRel;
1500
- }
1501
-
1502
- private reduceToLatestRelRunPerTestCase(testData: any[]): any[] {
1503
- type Candidate = {
1504
- point: any;
1505
- rel: number;
1506
- runId: number;
1507
- resultId: number;
1508
- hasRun: boolean;
1509
- };
1510
-
1511
- const candidatesByTestCase = new Map<number, Candidate[]>();
1512
- const testCaseDefinitionById = new Map<number, any>();
1513
-
1514
- for (const suite of testData || []) {
1515
- const rel = this.extractRelNumberFromSuite(suite);
1516
- const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
1517
- const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
1518
-
1519
- for (const testCase of testCasesItems) {
1520
- const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
1521
- if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
1522
- if (!testCaseDefinitionById.has(testCaseId)) {
1523
- testCaseDefinitionById.set(testCaseId, testCase);
1524
- }
1525
- }
1526
-
1527
- for (const point of testPointsItems) {
1528
- const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
1529
- if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
1530
-
1531
- const runId = Number(point?.lastRunId || 0);
1532
- const resultId = Number(point?.lastResultId || 0);
1533
- const hasRun = runId > 0 && resultId > 0;
1534
- if (!candidatesByTestCase.has(testCaseId)) {
1535
- candidatesByTestCase.set(testCaseId, []);
1536
- }
1537
- candidatesByTestCase.get(testCaseId)!.push({
1538
- point,
1539
- rel,
1540
- runId,
1541
- resultId,
1542
- hasRun,
1543
- });
1544
- }
1545
- }
1546
-
1547
- const selectedPoints: any[] = [];
1548
- const selectedTestCaseIds = new Set<number>();
1549
- for (const [testCaseId, candidates] of candidatesByTestCase.entries()) {
1550
- const sorted = [...candidates].sort((a, b) => {
1551
- if (a.hasRun !== b.hasRun) return a.hasRun ? -1 : 1;
1552
- if (a.rel !== b.rel) return b.rel - a.rel;
1553
- if (a.runId !== b.runId) return b.runId - a.runId;
1554
- return b.resultId - a.resultId;
1555
- });
1556
- const chosen = sorted[0];
1557
- if (!chosen?.point) continue;
1558
- selectedPoints.push(chosen.point);
1559
- selectedTestCaseIds.add(testCaseId);
1560
- }
1561
-
1562
- const selectedTestCases: any[] = [];
1563
- for (const testCaseId of selectedTestCaseIds) {
1564
- const definition = testCaseDefinitionById.get(testCaseId);
1565
- if (definition) {
1566
- selectedTestCases.push(definition);
1567
- }
1568
- }
1569
-
1570
- return [
1571
- {
1572
- testSuiteId: 'MEWP_REL_SCOPED',
1573
- suiteId: 'MEWP_REL_SCOPED',
1574
- suiteName: 'MEWP Rel Scoped',
1575
- parentSuiteId: '',
1576
- parentSuiteName: '',
1577
- suitePath: 'MEWP Rel Scoped',
1578
- testGroupName: 'MEWP Rel Scoped',
1579
- testPointsItems: selectedPoints,
1580
- testCasesItems: selectedTestCases,
1581
- },
1582
- ];
1465
+ const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
1466
+ return this.fetchTestData(suites, projectName, testPlanId, false);
1583
1467
  }
1584
1468
 
1585
1469
  private async loadExternalBugsByTestCase(
@@ -1923,16 +1807,6 @@ export default class ResultDataProvider {
1923
1807
  return out;
1924
1808
  }
1925
1809
 
1926
- private extractRequirementCodesFromExpectedSteps(steps: TestSteps[], includeSuffix: boolean): Set<string> {
1927
- const out = new Set<string>();
1928
- for (const step of Array.isArray(steps) ? steps : []) {
1929
- if (step?.isSharedStepTitle) continue;
1930
- const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
1931
- codes.forEach((code) => out.add(code));
1932
- }
1933
- return out;
1934
- }
1935
-
1936
1810
  private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
1937
1811
  const out = new Set<string>();
1938
1812
  const source = this.normalizeRequirementStepText(text);
@@ -2775,7 +2649,7 @@ export default class ResultDataProvider {
2775
2649
  if (sortedMembers.length <= 1) return sortedMembers[0];
2776
2650
  return `${baseKey}: ${sortedMembers.join(', ')}`;
2777
2651
  })
2778
- .join('; ');
2652
+ .join('\n');
2779
2653
  }
2780
2654
 
2781
2655
  private toMewpComparableText(value: any): string {
@@ -3096,6 +2970,140 @@ export default class ResultDataProvider {
3096
2970
  return testCases;
3097
2971
  }
3098
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
+
3099
3107
  /**
3100
3108
  * Fetches result data based on the Work Item Test Reporter.
3101
3109
  *
@@ -3130,13 +3138,37 @@ export default class ResultDataProvider {
3130
3138
  logger.warn(`Invalid run result ${runId} or result ${resultId}`);
3131
3139
  return null;
3132
3140
  }
3133
- const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${point.testCaseId}?$expand=all`;
3134
- 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
+ }
3135
3167
  const newResultData: PlainTestResult = {
3136
3168
  id: 0,
3137
3169
  outcome: point.outcome,
3138
- revision: testCaseData?.rev || 1,
3139
- testCase: { id: point.testCaseId, name: point.testCaseName },
3170
+ revision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3171
+ testCase: { id: String(testCaseId), name: point.testCaseName },
3140
3172
  state: testCaseData?.fields?.['System.State'] || 'Active',
3141
3173
  priority: testCaseData?.fields?.['Microsoft.VSTS.TCM.Priority'] || 0,
3142
3174
  createdDate: testCaseData?.fields?.['System.CreatedDate'] || '0001-01-01T00:00:00',
@@ -3184,8 +3216,8 @@ export default class ResultDataProvider {
3184
3216
  selectedFieldSet.clear();
3185
3217
  return {
3186
3218
  ...newResultData,
3187
- stepsResultXml: testCaseData.fields['Microsoft.VSTS.TCM.Steps'] || undefined,
3188
- testCaseRevision: testCaseData.rev,
3219
+ stepsResultXml: testCaseData?.fields?.['Microsoft.VSTS.TCM.Steps'] || undefined,
3220
+ testCaseRevision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
3189
3221
  filteredFields,
3190
3222
  relatedRequirements,
3191
3223
  relatedBugs,
@@ -3355,13 +3387,6 @@ export default class ResultDataProvider {
3355
3387
  );
3356
3388
  }
3357
3389
 
3358
- private isRunResultDebugEnabled(): boolean {
3359
- return (
3360
- String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '').toLowerCase() === 'true' ||
3361
- String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '') === '1'
3362
- );
3363
- }
3364
-
3365
3390
  private extractCommentText(comment: AdoWorkItemComment): string {
3366
3391
  const rendered = comment?.renderedText;
3367
3392
  // In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
@@ -3848,6 +3873,79 @@ export default class ResultDataProvider {
3848
3873
  return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId);
3849
3874
  }
3850
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
+
3851
3949
  /**
3852
3950
  * Converts a run status string into a human-readable format.
3853
3951
  *
@@ -4123,9 +4221,17 @@ export default class ResultDataProvider {
4123
4221
  suite.testSuiteId
4124
4222
  );
4125
4223
  const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
4126
- const testPointsItems = !fetchCrossPlans
4127
- ? await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId)
4224
+ const rawTestPointsItems = !fetchCrossPlans
4225
+ ? await this.fetchTestPoints(
4226
+ projectName,
4227
+ testPlanId,
4228
+ suite.testSuiteId
4229
+ )
4128
4230
  : await this.fetchCrossTestPoints(projectName, testCaseIds);
4231
+ const testPointsItems = this.attachSuiteTestCaseContextToPoints(
4232
+ testCasesItems,
4233
+ rawTestPointsItems
4234
+ );
4129
4235
 
4130
4236
  return { ...suite, testPointsItems, testCasesItems };
4131
4237
  } catch (error: any) {
@@ -4275,6 +4381,11 @@ export default class ResultDataProvider {
4275
4381
  resultData.iterationDetails.push(iteration);
4276
4382
  }
4277
4383
 
4384
+ const originalActionResultsCount = Array.isArray(iteration?.actionResults)
4385
+ ? iteration.actionResults.length
4386
+ : 0;
4387
+ resultData._debugOriginalActionResultsCount = originalActionResultsCount;
4388
+
4278
4389
  if (resultData.stepsResultXml && iteration) {
4279
4390
  const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
4280
4391
  const actionResultsWithSharedModels = actionResults.filter(
@@ -4987,6 +5098,7 @@ export default class ResultDataProvider {
4987
5098
  resultData.iterationDetails?.length > 0
4988
5099
  ? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
4989
5100
  : undefined;
5101
+ const debugOutcome = this.getTestOutcome(resultData);
4990
5102
 
4991
5103
  if (!resultData?.testCase || !resultData?.testSuite) {
4992
5104
  logger.debug(
@@ -5020,6 +5132,9 @@ export default class ResultDataProvider {
5020
5132
  relatedCRs: resultData.relatedCRs || undefined,
5021
5133
  lastRunResult: undefined as any,
5022
5134
  customFields: {}, // Create an object to store custom fields
5135
+ _debugTestOutcome: debugOutcome,
5136
+ _debugTestCaseState: String(resultData?.state || ''),
5137
+ _debugOriginalActionResultsCount: Number(resultData?._debugOriginalActionResultsCount ?? -1),
5023
5138
  };
5024
5139
 
5025
5140
  // Process all custom fields from resultData.filteredFields
@@ -5039,15 +5154,14 @@ export default class ResultDataProvider {
5039
5154
  resultDataResponse.priority = resultData.priority;
5040
5155
  break;
5041
5156
  case 'testCaseResult':
5042
- const outcome = this.getTestOutcome(resultData);
5043
5157
  if (lastRunId === undefined || lastResultId === undefined) {
5044
5158
  resultDataResponse.testCaseResult = {
5045
- resultMessage: `${this.convertRunStatus(outcome)}`,
5159
+ resultMessage: `${this.convertRunStatus(debugOutcome)}`,
5046
5160
  url: '',
5047
5161
  };
5048
5162
  } else {
5049
5163
  resultDataResponse.testCaseResult = {
5050
- resultMessage: `${this.convertRunStatus(outcome)} in Run ${lastRunId}`,
5164
+ resultMessage: `${this.convertRunStatus(debugOutcome)} in Run ${lastRunId}`,
5051
5165
  url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
5052
5166
  };
5053
5167
  }