@elisra-devops/docgen-data-provider 1.102.0 → 1.103.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.103.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',
@@ -915,24 +917,6 @@ export default class ResultDataProvider {
915
917
  if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
916
918
  mentionedCodesByBase.get(baseKey)!.add(code);
917
919
  }
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
920
  // Direction A logic:
937
921
  // 1) Base mention ("SR0054") is parent-level and considered covered only when
938
922
  // the whole family is covered across scoped test cases:
@@ -958,38 +942,12 @@ export default class ResultDataProvider {
958
942
 
959
943
  // Base mention ("SR0054") requires full family coverage across selected test cases.
960
944
  if (hasBaseMention) {
961
- const missingRequiredFamilyMembers = requiredFamilyMembers.filter(
962
- (memberCode) => !familyLinkedCodes.has(memberCode)
963
- );
964
945
  const isWholeFamilyCovered = requiredFamilyMembers.every((memberCode) =>
965
946
  familyLinkedCodes.has(memberCode)
966
947
  );
967
948
  if (!isWholeFamilyCovered) {
968
949
  missingBaseWhenFamilyUncovered.add(baseKey);
969
950
  }
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
951
  }
994
952
 
995
953
  // Specific mention ("SR0054-1") validates as exact-match only across scoped test cases.
@@ -999,60 +957,20 @@ export default class ResultDataProvider {
999
957
  for (const code of missingSpecificMembers) {
1000
958
  missingSpecificMentionedNoFamily.add(code);
1001
959
  }
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
960
  continue;
1020
961
  }
1021
962
 
1022
963
  // Fallback path when family data is unavailable for this base key.
1023
- const fallbackMissingSpecific: string[] = [];
1024
- let fallbackMissingBase = false;
1025
964
  for (const code of mentionedCodes) {
1026
965
  const hasSpecificSuffix = /-\d+$/.test(code);
1027
966
  if (hasSpecificSuffix) {
1028
967
  if (!linkedFullCodesAcrossTestCases.has(code)) {
1029
968
  missingSpecificMentionedNoFamily.add(code);
1030
- fallbackMissingSpecific.push(code);
1031
969
  }
1032
970
  } else if (!linkedBaseKeysAcrossTestCases.has(baseKey)) {
1033
971
  missingBaseWhenFamilyUncovered.add(baseKey);
1034
- fallbackMissingBase = true;
1035
972
  }
1036
973
  }
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
974
  }
1057
975
 
1058
976
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -1098,17 +1016,6 @@ export default class ResultDataProvider {
1098
1016
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
1099
1017
  appendMentionedButNotLinked(baseKey, stepRef);
1100
1018
  }
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
1019
  const sortedExtraLinked = [...new Set(extraLinked)]
1113
1020
  .map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
1114
1021
  .filter((code) => !!code)
@@ -1146,21 +1053,23 @@ export default class ResultDataProvider {
1146
1053
  const validationStatus: 'Pass' | 'Fail' =
1147
1054
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
1148
1055
  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
- );
1056
+ if (traceCurrentTestCase) {
1057
+ logger.debug(
1058
+ this.buildTaggedLogMessage(ResultDataProvider.MEWP_INTERNAL_VALIDATION_DIAGNOSTICS_TAG, {
1059
+ testCaseId,
1060
+ parsedSteps: executableSteps.length,
1061
+ stepsWithMentions: mentionEntries.length,
1062
+ customerIdsFound: mentionedL2Only.size,
1063
+ linkedRequirements: linkedFullCodes.size,
1064
+ mentionedButNotLinked:
1065
+ sortedMissingSpecificMentionedNoFamily.length +
1066
+ sortedMissingBaseWhenFamilyUncovered.length,
1067
+ linkedButNotMentioned: sortedExtraLinked.length,
1068
+ status: validationStatus,
1069
+ customerIdSample: [...mentionedL2Only].slice(0, 5).join(', '),
1070
+ })
1071
+ );
1072
+ }
1164
1073
 
1165
1074
  rows.push({
1166
1075
  'Test Case ID': testCaseId,
@@ -1309,7 +1218,7 @@ export default class ResultDataProvider {
1309
1218
  private createMewpCoverageRow(
1310
1219
  requirement: Pick<
1311
1220
  MewpL2RequirementFamily,
1312
- 'workItemId' | 'requirementId' | 'title' | 'subSystem' | 'responsibility'
1221
+ 'workItemId' | 'requirementId' | 'title' | 'owner' | 'subSystem' | 'responsibility'
1313
1222
  >,
1314
1223
  runStatus: MewpRunStatus,
1315
1224
  bug: MewpCoverageBugCell,
@@ -1317,12 +1226,18 @@ export default class ResultDataProvider {
1317
1226
  ): MewpCoverageRow {
1318
1227
  const l2ReqIdNumeric = Number(requirement?.workItemId || 0);
1319
1228
  const l2ReqId = l2ReqIdNumeric > 0 ? String(l2ReqIdNumeric) : '';
1229
+ const srNumber = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
1320
1230
  const l2ReqTitle = this.toMewpComparableText(requirement.title);
1231
+ const reqName = this.deriveMewpRequirementDisplayName(srNumber, l2ReqTitle);
1232
+ const l2Owner = this.toMewpComparableText(requirement.owner);
1321
1233
  const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
1322
1234
 
1323
1235
  return {
1324
1236
  'L2 REQ ID': l2ReqId,
1325
- 'L2 REQ Title': l2ReqTitle,
1237
+ 'SR #': srNumber,
1238
+ 'L2 REQ Title': reqName,
1239
+ 'L2 REQ Full Title': l2ReqTitle,
1240
+ 'L2 Owner': l2Owner,
1326
1241
  'L2 SubSystem': l2SubSystem,
1327
1242
  'L2 Run Status': runStatus,
1328
1243
  'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
@@ -1335,6 +1250,20 @@ export default class ResultDataProvider {
1335
1250
  };
1336
1251
  }
1337
1252
 
1253
+ private deriveMewpRequirementDisplayName(requirementCode: string, title: string): string {
1254
+ const normalizedTitle = this.toMewpComparableText(title);
1255
+ if (!normalizedTitle) return '';
1256
+
1257
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(requirementCode || '');
1258
+ if (!normalizedCode) return normalizedTitle;
1259
+
1260
+ const escapedCode = normalizedCode.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1261
+ const codePrefixPattern = new RegExp(`^${escapedCode}(?:\\s*[:\\-–—]\\s*|\\s+)`, 'i');
1262
+ const withoutCodePrefix = normalizedTitle.replace(codePrefixPattern, '').trim();
1263
+ if (!withoutCodePrefix) return normalizedTitle;
1264
+ return withoutCodePrefix;
1265
+ }
1266
+
1338
1267
  private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
1339
1268
  return { id: '' as '', title: '', responsibility: '' };
1340
1269
  }
@@ -1655,14 +1584,17 @@ export default class ResultDataProvider {
1655
1584
  }
1656
1585
 
1657
1586
  private buildRequirementSapWbsByBaseKey(
1658
- requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
1587
+ requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'owner' | 'responsibility'>>
1659
1588
  ): Map<string, string> {
1660
1589
  const out = new Map<string, string>();
1661
1590
  for (const requirement of requirements || []) {
1662
1591
  const baseKey = String(requirement?.baseKey || '').trim();
1663
1592
  if (!baseKey) continue;
1664
1593
 
1665
- const normalized = this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1594
+ const rawOwner = this.toMewpComparableText(requirement?.owner);
1595
+ const normalized =
1596
+ this.resolveMewpResponsibility(rawOwner) ||
1597
+ this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
1666
1598
  if (!normalized) continue;
1667
1599
 
1668
1600
  const existing = out.get(baseKey) || '';
@@ -2299,6 +2231,7 @@ export default class ResultDataProvider {
2299
2231
  requirementId,
2300
2232
  baseKey: this.toRequirementKey(requirementId),
2301
2233
  title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
2234
+ owner: this.deriveMewpRequirementOwner(fields),
2302
2235
  subSystem: this.deriveMewpSubSystem(fields),
2303
2236
  responsibility: this.deriveMewpResponsibility(fields),
2304
2237
  linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
@@ -2348,6 +2281,7 @@ export default class ResultDataProvider {
2348
2281
  if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
2349
2282
  if (!areaPath.includes('\\mop')) score += 2;
2350
2283
  if (String(item?.title || '').trim()) score += 1;
2284
+ if (String(item?.owner || '').trim()) score += 1;
2351
2285
  if (String(item?.subSystem || '').trim()) score += 1;
2352
2286
  if (String(item?.responsibility || '').trim()) score += 1;
2353
2287
  return score;
@@ -2384,6 +2318,7 @@ export default class ResultDataProvider {
2384
2318
  requirementId: String(family?.representative?.requirementId || baseKey),
2385
2319
  baseKey,
2386
2320
  title: String(family?.representative?.title || ''),
2321
+ owner: String(family?.representative?.owner || ''),
2387
2322
  subSystem: String(family?.representative?.subSystem || ''),
2388
2323
  responsibility: String(family?.representative?.responsibility || ''),
2389
2324
  linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
@@ -2835,6 +2770,24 @@ export default class ResultDataProvider {
2835
2770
  return '';
2836
2771
  }
2837
2772
 
2773
+ // L2 owner is sourced only from requirement SAPWBS fields.
2774
+ private deriveMewpRequirementOwner(fields: Record<string, any>): string {
2775
+ const directCandidates = [fields?.['Custom.SAPWBS'], fields?.['SAPWBS']];
2776
+ for (const candidate of directCandidates) {
2777
+ const normalized = this.toMewpComparableText(candidate);
2778
+ if (normalized) return normalized;
2779
+ }
2780
+
2781
+ for (const [key, value] of Object.entries(fields || {})) {
2782
+ const normalizedKey = String(key || '').toLowerCase();
2783
+ if (!normalizedKey.includes('sapwbs')) continue;
2784
+ const normalizedValue = this.toMewpComparableText(value);
2785
+ if (normalizedValue) return normalizedValue;
2786
+ }
2787
+
2788
+ return '';
2789
+ }
2790
+
2838
2791
  // Test-case responsibility must come from test-case path context (not SAPWBS).
2839
2792
  private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
2840
2793
  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