@elisra-devops/docgen-data-provider 1.102.0 → 1.104.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.102.0",
3
+ "version": "1.104.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -28,6 +28,7 @@ export interface MewpL2RequirementWorkItem {
28
28
  requirementId: string;
29
29
  baseKey: string;
30
30
  title: string;
31
+ owner: string;
31
32
  subSystem: string;
32
33
  responsibility: string;
33
34
  linkedTestCaseIds: number[];
@@ -40,6 +41,7 @@ export interface MewpL2RequirementFamily {
40
41
  requirementId: string;
41
42
  baseKey: string;
42
43
  title: string;
44
+ owner: string;
43
45
  subSystem: string;
44
46
  responsibility: string;
45
47
  linkedTestCaseIds: number[];
@@ -84,7 +86,10 @@ export type MewpCoverageL3L4Cell = MewpL3L4Pair;
84
86
 
85
87
  export interface MewpCoverageRow {
86
88
  'L2 REQ ID': string;
89
+ 'SR #': string;
87
90
  'L2 REQ Title': string;
91
+ 'L2 REQ Full Title'?: string;
92
+ 'L2 Owner': string;
88
93
  'L2 SubSystem': string;
89
94
  'L2 Run Status': MewpRunStatus;
90
95
  'Bug ID': number | '';
@@ -61,7 +61,9 @@ export default class ResultDataProvider {
61
61
 
62
62
  private static readonly MEWP_L2_COVERAGE_COLUMNS = [
63
63
  'L2 REQ ID',
64
+ 'SR #',
64
65
  'L2 REQ Title',
66
+ 'L2 Owner',
65
67
  'L2 SubSystem',
66
68
  'L2 Run Status',
67
69
  'Bug ID',
@@ -824,6 +826,13 @@ export default class ResultDataProvider {
824
826
  }
825
827
 
826
828
  const preloadedStepXmlCount = stepsXmlByTestCase.size;
829
+ const latestWorkItemMapStats = await this.enrichMewpTestCaseMapsFromLatestWorkItems(
830
+ projectName,
831
+ [...allTestCaseIds],
832
+ stepsXmlByTestCase,
833
+ testCaseTitleMap,
834
+ testCaseDescriptionMap
835
+ );
827
836
  const fallbackStepLoadStats = await this.enrichMewpStepsXmlMapFromWorkItems(
828
837
  projectName,
829
838
  [...allTestCaseIds],
@@ -831,7 +840,11 @@ export default class ResultDataProvider {
831
840
  );
832
841
  logger.info(
833
842
  `MEWP internal validation steps source summary: testCases=${allTestCaseIds.size} ` +
834
- `fromSuitePayload=${preloadedStepXmlCount} fromWorkItemFallback=${fallbackStepLoadStats.loadedFromFallback} ` +
843
+ `fromSuitePayload=${preloadedStepXmlCount} ` +
844
+ `fromLatestWorkItem=${latestWorkItemMapStats.stepsLoadedFromLatest} ` +
845
+ `titleFromLatest=${latestWorkItemMapStats.titleLoadedFromLatest} ` +
846
+ `descriptionFromLatest=${latestWorkItemMapStats.descriptionLoadedFromLatest} ` +
847
+ `fromWorkItemFallback=${fallbackStepLoadStats.loadedFromFallback} ` +
835
848
  `stepsXmlAvailable=${stepsXmlByTestCase.size} unresolved=${fallbackStepLoadStats.unresolvedCount}`
836
849
  );
837
850
 
@@ -915,24 +928,6 @@ export default class ResultDataProvider {
915
928
  if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
916
929
  mentionedCodesByBase.get(baseKey)!.add(code);
917
930
  }
918
- if (traceCurrentTestCase) {
919
- logger.debug(
920
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
921
- event: 'test-case-start',
922
- tc: testCaseId,
923
- parsedSteps: executableSteps.length,
924
- stepsWithMentions: mentionEntries.length,
925
- mentionedCodes:
926
- [...mentionedL2Only].sort((a, b) => this.compareMewpRequirementCodes(a, b)).join('; ') ||
927
- '<none>',
928
- linkedCodesInTestCase:
929
- [...linkedFullCodes]
930
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
931
- .join('; ') || '<none>',
932
- })
933
- );
934
- }
935
-
936
931
  // Direction A logic:
937
932
  // 1) Base mention ("SR0054") is parent-level and considered covered only when
938
933
  // the whole family is covered across scoped test cases:
@@ -958,38 +953,12 @@ export default class ResultDataProvider {
958
953
 
959
954
  // Base mention ("SR0054") requires full family coverage across selected test cases.
960
955
  if (hasBaseMention) {
961
- const missingRequiredFamilyMembers = requiredFamilyMembers.filter(
962
- (memberCode) => !familyLinkedCodes.has(memberCode)
963
- );
964
956
  const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
965
957
  familyLinkedCodes.has(memberCode)
966
958
  );
967
959
  if (!isWholeFamilyCovered) {
968
960
  missingBaseWhenFamilyUncovered.add(baseKey);
969
961
  }
970
- if (traceCurrentTestCase) {
971
- logger.debug(
972
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
973
- event: 'base-family-coverage',
974
- tc: testCaseId,
975
- base: baseKey,
976
- baseMention: true,
977
- requiredFamily:
978
- requiredFamilyMembers
979
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
980
- .join('; ') || '<none>',
981
- linkedAcrossScope:
982
- [...familyLinkedCodes]
983
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
984
- .join('; ') || '<none>',
985
- missingRequired:
986
- missingRequiredFamilyMembers
987
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
988
- .join('; ') || '<none>',
989
- covered: isWholeFamilyCovered,
990
- })
991
- );
992
- }
993
962
  }
994
963
 
995
964
  // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
@@ -999,60 +968,20 @@ export default class ResultDataProvider {
999
968
  for (const code of missingSpecificMembers) {
1000
969
  missingSpecificMentionedNoFamily.add(code);
1001
970
  }
1002
- if (traceCurrentTestCase && mentionedSpecificMembers.length > 0) {
1003
- logger.debug(
1004
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1005
- event: 'specific-members-check',
1006
- tc: testCaseId,
1007
- base: baseKey,
1008
- specificMentioned:
1009
- mentionedSpecificMembers
1010
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1011
- .join('; ') || '<none>',
1012
- specificMissing:
1013
- missingSpecificMembers
1014
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1015
- .join('; ') || '<none>',
1016
- })
1017
- );
1018
- }
1019
971
  continue;
1020
972
  }
1021
973
 
1022
974
  // Fallback path when family data is unavailable for this base key.
1023
- const fallbackMissingSpecific: string[] = [];
1024
- let fallbackMissingBase = false;
1025
975
  for (const code of mentionedCodes) {
1026
976
  const hasSpecificSuffix = /-\d+$/.test(code);
1027
977
  if (hasSpecificSuffix) {
1028
978
  if (!linkedFullCodesAcrossTestCases.has(code)) {
1029
979
  missingSpecificMentionedNoFamily.add(code);
1030
- fallbackMissingSpecific.push(code);
1031
980
  }
1032
981
  } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
1033
982
  missingBaseWhenFamilyUncovered.add(baseKey);
1034
- fallbackMissingBase = true;
1035
983
  }
1036
984
  }
1037
- if (traceCurrentTestCase) {
1038
- logger.debug(
1039
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1040
- event: 'fallback-path',
1041
- tc: testCaseId,
1042
- base: baseKey,
1043
- fallbackUsed: true,
1044
- mentioned:
1045
- mentionedCodesList
1046
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1047
- .join('; ') || '<none>',
1048
- missingSpecific:
1049
- fallbackMissingSpecific
1050
- .sort((a, b) => this.compareMewpRequirementCodes(a, b))
1051
- .join('; ') || '<none>',
1052
- missingBase: fallbackMissingBase,
1053
- })
1054
- );
1055
- }
1056
985
  }
1057
986
 
1058
987
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -1098,17 +1027,6 @@ export default class ResultDataProvider {
1098
1027
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
1099
1028
  appendMentionedButNotLinked(baseKey, stepRef);
1100
1029
  }
1101
- if (traceCurrentTestCase) {
1102
- logger.debug(
1103
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_TRACE_TAG, {
1104
- event: 'direction-a-summary',
1105
- tc: testCaseId,
1106
- missingSpecific: sortedMissingSpecificMentionedNoFamily.join('; ') || '<none>',
1107
- missingBase: sortedMissingBaseWhenFamilyUncovered.join('; ') || '<none>',
1108
- })
1109
- );
1110
- }
1111
-
1112
1030
  const sortedExtraLinked = [...new Set(extraLinked)]
1113
1031
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
1114
1032
  .filter((code) => !!code)
@@ -1146,21 +1064,23 @@ export default class ResultDataProvider {
1146
1064
  const validationStatus: 'Pass' | 'Fail' =
1147
1065
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1148
1066
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
1149
- logger.debug(
1150
- this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1151
- testCaseId,
1152
- parsedSteps: executableSteps.length,
1153
- stepsWithMentions: mentionEntries.length,
1154
- customerIdsFound: mentionedL2Only.size,
1155
- linkedRequirements: linkedFullCodes.size,
1156
- mentionedButNotLinked:
1157
- sortedMissingSpecificMentionedNoFamily.length +
1158
- sortedMissingBaseWhenFamilyUncovered.length,
1159
- linkedButNotMentioned: sortedExtraLinked.length,
1160
- status: validationStatus,
1161
- customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1162
- })
1163
- );
1067
+ if (traceCurrentTestCase) {
1068
+ logger.debug(
1069
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1070
+ testCaseId,
1071
+ parsedSteps: executableSteps.length,
1072
+ stepsWithMentions: mentionEntries.length,
1073
+ customerIdsFound: mentionedL2Only.size,
1074
+ linkedRequirements: linkedFullCodes.size,
1075
+ mentionedButNotLinked:
1076
+ sortedMissingSpecificMentionedNoFamily.length +
1077
+ sortedMissingBaseWhenFamilyUncovered.length,
1078
+ linkedButNotMentioned: sortedExtraLinked.length,
1079
+ status: validationStatus,
1080
+ customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1081
+ })
1082
+ );
1083
+ }
1164
1084
 
1165
1085
  rows.push({
1166
1086
  'Test Case ID': testCaseId,
@@ -1309,7 +1229,7 @@ export default class ResultDataProvider {
1309
1229
  private createMewpCoverageRow(
1310
1230
  requirement: Pick<
1311
1231
  MewpL2RequirementFamily,
1312
- 'workItemId' | 'requirementId' | 'title' | 'subSystem' | 'responsibility'
1232
+ 'workItemId' | 'requirementId' | 'title' | 'owner' | 'subSystem' | 'responsibility'
1313
1233
  >,
1314
1234
  runStatus: MewpRunStatus,
1315
1235
  bug: MewpCoverageBugCell,
@@ -1317,12 +1237,18 @@ export default class ResultDataProvider {
1317
1237
  ): MewpCoverageRow {
1318
1238
  const l2ReqIdNumeric = Number(requirement?.workItemId || 0);
1319
1239
  const l2ReqId = l2ReqIdNumeric > 0 ? String(l2ReqIdNumeric) : '';
1240
+ const srNumber = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
1320
1241
  const l2ReqTitle = this.toMewpComparableText(requirement.title);
1242
+ const reqName = this.deriveMewpRequirementDisplayName(srNumber, l2ReqTitle);
1243
+ const l2Owner = this.toMewpComparableText(requirement.owner);
1321
1244
  const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
1322
1245
 
1323
1246
  return {
1324
1247
  'L2 REQ ID': l2ReqId,
1325
- 'L2 REQ Title': l2ReqTitle,
1248
+ 'SR #': srNumber,
1249
+ 'L2 REQ Title': reqName,
1250
+ 'L2 REQ Full Title': l2ReqTitle,
1251
+ 'L2 Owner': l2Owner,
1326
1252
  'L2 SubSystem': l2SubSystem,
1327
1253
  'L2 Run Status': runStatus,
1328
1254
  'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
@@ -1335,6 +1261,20 @@ export default class ResultDataProvider {
1335
1261
  };
1336
1262
  }
1337
1263
 
1264
+ private deriveMewpRequirementDisplayName(requirementCode: string, title: string): string {
1265
+ const normalizedTitle = this.toMewpComparableText(title);
1266
+ if (!normalizedTitle) return '';
1267
+
1268
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(requirementCode || '');
1269
+ if (!normalizedCode) return normalizedTitle;
1270
+
1271
+ const escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1272
+ const codePrefixPattern = new RegExp(`^${escapedCode}(?:\\s*[:\\-–—]\\s*|\\s+)`, 'i');
1273
+ const withoutCodePrefix = normalizedTitle.replace(codePrefixPattern, '').trim();
1274
+ if (!withoutCodePrefix) return normalizedTitle;
1275
+ return withoutCodePrefix;
1276
+ }
1277
+
1338
1278
  private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
1339
1279
  return { id: '' as '', title: '', responsibility: '' };
1340
1280
  }
@@ -1655,14 +1595,17 @@ export default class ResultDataProvider {
1655
1595
  }
1656
1596
 
1657
1597
  private buildRequirementSapWbsByBaseKey(
1658
- requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
1598
+ requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'owner' | 'responsibility'>>
1659
1599
  ): Map<string, string> {
1660
1600
  const out = new Map<string, string>();
1661
1601
  for (const requirement of requirements || []) {
1662
1602
  const baseKey = String(requirement?.baseKey || '').trim();
1663
1603
  if (!baseKey) continue;
1664
1604
 
1665
- const normalized = this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1605
+ const rawOwner = this.toMewpComparableText(requirement?.owner);
1606
+ const normalized =
1607
+ this.resolveMewpResponsibility(rawOwner) ||
1608
+ this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1666
1609
  if (!normalized) continue;
1667
1610
 
1668
1611
  const existing = out.get(baseKey) || '';
@@ -1812,6 +1755,81 @@ export default class ResultDataProvider {
1812
1755
  return map;
1813
1756
  }
1814
1757
 
1758
+ private async enrichMewpTestCaseMapsFromLatestWorkItems(
1759
+ projectName: string,
1760
+ testCaseIds: number[],
1761
+ stepsXmlByTestCase: Map<number, string>,
1762
+ testCaseTitleMap: Map<number, string>,
1763
+ testCaseDescriptionMap: Map<number, string>
1764
+ ): Promise<{ stepsLoadedFromLatest: number; titleLoadedFromLatest: number; descriptionLoadedFromLatest: number }> {
1765
+ const uniqueIds = [...new Set(testCaseIds)]
1766
+ .map((id) => Number(id))
1767
+ .filter((id) => Number.isFinite(id) && id > 0);
1768
+
1769
+ if (uniqueIds.length === 0) {
1770
+ return {
1771
+ stepsLoadedFromLatest: 0,
1772
+ titleLoadedFromLatest: 0,
1773
+ descriptionLoadedFromLatest: 0,
1774
+ };
1775
+ }
1776
+
1777
+ let stepsLoadedFromLatest = 0;
1778
+ let titleLoadedFromLatest = 0;
1779
+ let descriptionLoadedFromLatest = 0;
1780
+
1781
+ try {
1782
+ const latestWorkItems = await this.fetchWorkItemsByIds(projectName, uniqueIds, false);
1783
+ for (const workItem of latestWorkItems || []) {
1784
+ const id = Number(workItem?.id || 0);
1785
+ if (!Number.isFinite(id) || id <= 0) continue;
1786
+
1787
+ const fields = workItem?.fields || {};
1788
+ const latestStepsXml = this.extractStepsXmlFromFieldsMap(fields);
1789
+ if (latestStepsXml) {
1790
+ const previous = String(stepsXmlByTestCase.get(id) || '');
1791
+ if (previous !== latestStepsXml) {
1792
+ stepsLoadedFromLatest += 1;
1793
+ }
1794
+ stepsXmlByTestCase.set(id, latestStepsXml);
1795
+ }
1796
+
1797
+ const latestTitle = this.toMewpComparableText(
1798
+ this.getFieldValueByName(fields, 'System.Title') ?? this.getFieldValueByName(fields, 'Title')
1799
+ );
1800
+ if (latestTitle) {
1801
+ const previous = String(testCaseTitleMap.get(id) || '');
1802
+ if (previous !== latestTitle) {
1803
+ titleLoadedFromLatest += 1;
1804
+ }
1805
+ testCaseTitleMap.set(id, latestTitle);
1806
+ }
1807
+
1808
+ const latestDescription = this.toMewpComparableText(
1809
+ this.getFieldValueByName(fields, 'System.Description') ??
1810
+ this.getFieldValueByName(fields, 'Description')
1811
+ );
1812
+ if (latestDescription) {
1813
+ const previous = String(testCaseDescriptionMap.get(id) || '');
1814
+ if (previous !== latestDescription) {
1815
+ descriptionLoadedFromLatest += 1;
1816
+ }
1817
+ testCaseDescriptionMap.set(id, latestDescription);
1818
+ }
1819
+ }
1820
+ } catch (error: any) {
1821
+ logger.warn(
1822
+ `MEWP internal validation: failed to load latest test-case fields: ${error?.message || error}`
1823
+ );
1824
+ }
1825
+
1826
+ return {
1827
+ stepsLoadedFromLatest,
1828
+ titleLoadedFromLatest,
1829
+ descriptionLoadedFromLatest,
1830
+ };
1831
+ }
1832
+
1815
1833
  private extractMewpTestCaseId(runResult: any): number {
1816
1834
  const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
1817
1835
  return Number.isFinite(testCaseId) ? testCaseId : 0;
@@ -2299,6 +2317,7 @@ export default class ResultDataProvider {
2299
2317
  requirementId,
2300
2318
  baseKey: this.toRequirementKey(requirementId),
2301
2319
  title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
2320
+ owner: this.deriveMewpRequirementOwner(fields),
2302
2321
  subSystem: this.deriveMewpSubSystem(fields),
2303
2322
  responsibility: this.deriveMewpResponsibility(fields),
2304
2323
  linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
@@ -2348,6 +2367,7 @@ export default class ResultDataProvider {
2348
2367
  if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
2349
2368
  if (!areaPath.includes('\\mop')) score += 2;
2350
2369
  if (String(item?.title || '').trim()) score += 1;
2370
+ if (String(item?.owner || '').trim()) score += 1;
2351
2371
  if (String(item?.subSystem || '').trim()) score += 1;
2352
2372
  if (String(item?.responsibility || '').trim()) score += 1;
2353
2373
  return score;
@@ -2384,6 +2404,7 @@ export default class ResultDataProvider {
2384
2404
  requirementId: String(family?.representative?.requirementId || baseKey),
2385
2405
  baseKey,
2386
2406
  title: String(family?.representative?.title || ''),
2407
+ owner: String(family?.representative?.owner || ''),
2387
2408
  subSystem: String(family?.representative?.subSystem || ''),
2388
2409
  responsibility: String(family?.representative?.responsibility || ''),
2389
2410
  linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
@@ -2835,6 +2856,24 @@ export default class ResultDataProvider {
2835
2856
  return '';
2836
2857
  }
2837
2858
 
2859
+ // L2 owner is sourced only from requirement SAPWBS fields.
2860
+ private deriveMewpRequirementOwner(fields: Record<string, any>): string {
2861
+ const directCandidates = [fields?.['Custom.SAPWBS'], fields?.['SAPWBS']];
2862
+ for (const candidate of directCandidates) {
2863
+ const normalized = this.toMewpComparableText(candidate);
2864
+ if (normalized) return normalized;
2865
+ }
2866
+
2867
+ for (const [key, value] of Object.entries(fields || {})) {
2868
+ const normalizedKey = String(key || '').toLowerCase();
2869
+ if (!normalizedKey.includes('sapwbs')) continue;
2870
+ const normalizedValue = this.toMewpComparableText(value);
2871
+ if (normalizedValue) return normalizedValue;
2872
+ }
2873
+
2874
+ return '';
2875
+ }
2876
+
2838
2877
  // Test-case responsibility must come from test-case path context (not SAPWBS).
2839
2878
  private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
2840
2879
  const areaPathCandidates = [
@@ -1126,6 +1126,62 @@ describe('ResultDataProvider', () => {
1126
1126
  });
1127
1127
 
1128
1128
  describe('getMewpL2CoverageFlatResults', () => {
1129
+ it('should split SR prefix into SR # + L2 REQ Title while preserving full title', () => {
1130
+ const row = (resultDataProvider as any).createMewpCoverageRow(
1131
+ {
1132
+ workItemId: 5054,
1133
+ requirementId: 'SR0054',
1134
+ title: 'SR0054 - Engine startup coverage',
1135
+ owner: 'ESUK',
1136
+ subSystem: 'Propulsion',
1137
+ responsibility: 'ESUK',
1138
+ },
1139
+ 'Pass',
1140
+ { id: '', title: '', responsibility: '' },
1141
+ { l3Id: '', l3Title: '', l4Id: '', l4Title: '' }
1142
+ );
1143
+
1144
+ expect(row).toEqual(
1145
+ expect.objectContaining({
1146
+ 'L2 REQ ID': '5054',
1147
+ 'SR #': 'SR0054',
1148
+ 'L2 REQ Title': 'Engine startup coverage',
1149
+ 'L2 REQ Full Title': 'SR0054 - Engine startup coverage',
1150
+ 'L2 Owner': 'ESUK',
1151
+ 'L2 SubSystem': 'Propulsion',
1152
+ })
1153
+ );
1154
+ });
1155
+
1156
+ it('should keep full title when title is only the SR token', () => {
1157
+ const row = (resultDataProvider as any).createMewpCoverageRow(
1158
+ {
1159
+ workItemId: 9999,
1160
+ requirementId: 'SR9999',
1161
+ title: 'SR9999',
1162
+ owner: 'IL',
1163
+ subSystem: '',
1164
+ responsibility: 'IL',
1165
+ },
1166
+ 'Not Run',
1167
+ { id: '', title: '', responsibility: '' },
1168
+ { l3Id: '', l3Title: '', l4Id: '', l4Title: '' }
1169
+ );
1170
+
1171
+ expect(row['L2 REQ Title']).toBe('SR9999');
1172
+ expect(row['L2 REQ Full Title']).toBe('SR9999');
1173
+ expect(row['L2 Owner']).toBe('IL');
1174
+ });
1175
+
1176
+ it('should resolve L2 run status precedence: Fail > Not Run > Pass', () => {
1177
+ const resolve = (resultDataProvider as any).resolveMewpL2RunStatus.bind(resultDataProvider as any);
1178
+
1179
+ expect(resolve({ passed: 2, failed: 1, notRun: 3, hasAnyTestCase: true })).toBe('Fail');
1180
+ expect(resolve({ passed: 5, failed: 0, notRun: 1, hasAnyTestCase: true })).toBe('Not Run');
1181
+ expect(resolve({ passed: 1, failed: 0, notRun: 0, hasAnyTestCase: true })).toBe('Pass');
1182
+ expect(resolve({ passed: 0, failed: 0, notRun: 0, hasAnyTestCase: true })).toBe('Not Run');
1183
+ });
1184
+
1129
1185
  it('should fetch MEWP scoped test data from selected suites', async () => {
1130
1186
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1131
1187
  const scopedSpy = jest
@@ -1234,7 +1290,7 @@ describe('ResultDataProvider', () => {
1234
1290
  expect(result).toEqual(
1235
1291
  expect.objectContaining({
1236
1292
  sheetName: expect.stringContaining('MEWP L2 Coverage'),
1237
- columnOrder: expect.arrayContaining(['L2 REQ ID', 'L2 REQ Title', 'L2 Run Status']),
1293
+ columnOrder: expect.arrayContaining(['L2 REQ ID', 'SR #', 'L2 REQ Title', 'L2 Owner', 'L2 Run Status']),
1238
1294
  })
1239
1295
  );
1240
1296
 
@@ -3449,6 +3505,100 @@ describe('ResultDataProvider', () => {
3449
3505
  })
3450
3506
  );
3451
3507
  });
3508
+
3509
+ it('should prefer latest test-case revision fields (title/description/steps) in internal validation flow', async () => {
3510
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
3511
+ jest.spyOn(resultDataProvider as any, 'fetchMewpScopedTestData').mockResolvedValueOnce([
3512
+ {
3513
+ testPointsItems: [{ testCaseId: 777, testCaseName: 'TC 777 (snapshot)' }],
3514
+ testCasesItems: [
3515
+ {
3516
+ workItem: {
3517
+ id: 777,
3518
+ workItemFields: [
3519
+ { key: 'System.Title', value: 'TC 777 (snapshot title)' },
3520
+ {
3521
+ key: 'Microsoft.VSTS.TCM.Steps',
3522
+ value:
3523
+ '<steps><step id="2" type="ActionStep"><parameterizedString isformatted="true">Action</parameterizedString><parameterizedString isformatted="true">SR0001</parameterizedString></step></steps>',
3524
+ },
3525
+ { key: 'System.Description', value: '<p>Snapshot description</p>' },
3526
+ ],
3527
+ },
3528
+ },
3529
+ ],
3530
+ },
3531
+ ]);
3532
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
3533
+ {
3534
+ workItemId: 9777,
3535
+ requirementId: 'SR7777',
3536
+ baseKey: 'SR7777',
3537
+ title: 'Req 7777',
3538
+ responsibility: 'ESUK',
3539
+ linkedTestCaseIds: [777],
3540
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
3541
+ },
3542
+ ]);
3543
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
3544
+ new Map([
3545
+ [
3546
+ 777,
3547
+ {
3548
+ baseKeys: new Set(['SR7777']),
3549
+ fullCodes: new Set(['SR7777']),
3550
+ },
3551
+ ],
3552
+ ])
3553
+ );
3554
+ const fetchWorkItemsByIdsSpy = jest
3555
+ .spyOn(resultDataProvider as any, 'fetchWorkItemsByIds')
3556
+ .mockResolvedValueOnce([
3557
+ {
3558
+ id: 777,
3559
+ fields: {
3560
+ 'System.Title': 'TC 777 (latest title)',
3561
+ 'System.Description':
3562
+ '<p><b><u>Trial specific assumptions, constraints, dependencies and requirements</u></b></p><p>SR7777</p>',
3563
+ 'Microsoft.VSTS.TCM.Steps':
3564
+ '<steps><step id="2" type="ActionStep"><parameterizedString isformatted="true">Action</parameterizedString><parameterizedString isformatted="true">SR7777</parameterizedString></step></steps>',
3565
+ },
3566
+ },
3567
+ ]);
3568
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockImplementation(
3569
+ async (...args: any[]) => [
3570
+ {
3571
+ stepId: '1',
3572
+ stepPosition: '1',
3573
+ action: 'Action',
3574
+ expected: String(args?.[0] || '').includes('SR7777') ? 'SR7777' : 'SR0001',
3575
+ isSharedStepTitle: false,
3576
+ },
3577
+ ]
3578
+ );
3579
+
3580
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
3581
+ '123',
3582
+ mockProjectName,
3583
+ [1]
3584
+ );
3585
+
3586
+ expect(fetchWorkItemsByIdsSpy).toHaveBeenCalledWith(mockProjectName, [777], false);
3587
+ expect((resultDataProvider as any).testStepParserHelper.parseTestSteps).toHaveBeenCalledWith(
3588
+ expect.stringContaining('SR7777'),
3589
+ expect.any(Map)
3590
+ );
3591
+ expect(result.rows).toHaveLength(1);
3592
+ expect(result.rows[0]).toEqual(
3593
+ expect.objectContaining({
3594
+ 'Test Case ID': 777,
3595
+ 'Test Case Title': 'TC 777 (latest title)',
3596
+ 'Mentioned but Not Linked': '',
3597
+ 'Linked but Not Mentioned': '',
3598
+ 'Validation Status': 'Pass',
3599
+ })
3600
+ );
3601
+ });
3452
3602
  });
3453
3603
 
3454
3604
  describe('buildLinkedRequirementsByTestCase', () => {