@elisra-devops/docgen-data-provider 1.84.0 → 1.86.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.84.0",
3
+ "version": "1.86.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -478,6 +478,10 @@ export default class ResultDataProvider {
478
478
  testData
479
479
  );
480
480
  const requirementSapWbsByBaseKey = this.buildRequirementSapWbsByBaseKey(allRequirements);
481
+ const testCaseResponsibilityById = await this.buildMewpTestCaseResponsibilityMap(
482
+ testData,
483
+ projectName
484
+ );
481
485
  const externalBugsByTestCase = await this.loadExternalBugsByTestCase(options?.externalBugsFile);
482
486
  const externalL3L4ByBaseKey = await this.loadExternalL3L4ByBaseKey(
483
487
  options?.externalL3L4File,
@@ -690,7 +694,8 @@ export default class ResultDataProvider {
690
694
  linkedRequirementsByTestCase,
691
695
  externalL3L4ByBaseKey,
692
696
  externalBugsByTestCase,
693
- externalJoinKeysByL2
697
+ externalJoinKeysByL2,
698
+ testCaseResponsibilityById
694
699
  );
695
700
  const coverageRowStats = rows.reduce(
696
701
  (acc, row) => {
@@ -843,18 +848,6 @@ export default class ResultDataProvider {
843
848
  [...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
844
849
  );
845
850
 
846
- const expectedFamilyCodes = new Set<string>();
847
- for (const baseKey of mentionedBaseKeys) {
848
- const familyCodes = requirementFamilies.get(baseKey);
849
- if (familyCodes?.size) {
850
- familyCodes.forEach((code) => expectedFamilyCodes.add(code));
851
- } else {
852
- for (const code of mentionedL2Only) {
853
- if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
854
- }
855
- }
856
- }
857
-
858
851
  const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
859
852
  const linkedFullCodes =
860
853
  scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
@@ -868,14 +861,48 @@ export default class ResultDataProvider {
868
861
  [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
869
862
  );
870
863
 
871
- const missingMentioned = [...mentionedL2Only].filter((code) => {
864
+ const mentionedCodesByBase = new Map<string, Set<string>>();
865
+ for (const code of mentionedL2Only) {
872
866
  const baseKey = this.toRequirementKey(code);
873
- if (!baseKey) return false;
874
- const hasSpecificSuffix = /-\d+$/.test(code);
875
- if (hasSpecificSuffix) return !linkedFullCodes.has(code);
876
- return !linkedBaseKeys.has(baseKey);
877
- });
878
- const missingFamily = [...expectedFamilyCodes].filter((code) => !linkedFullCodes.has(code));
867
+ if (!baseKey) continue;
868
+ if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
869
+ mentionedCodesByBase.get(baseKey)!.add(code);
870
+ }
871
+
872
+ // Context-based direction A logic:
873
+ // 1) If no member of family is linked -> report only base SR (Step X: SR0054).
874
+ // 2) If family is partially linked -> report only specific missing members.
875
+ // 3) If family fully linked -> report nothing for that family.
876
+ const missingBaseWhenFamilyUncovered = new Set<string>();
877
+ const missingSpecificMentionedNoFamily = new Set<string>();
878
+ const missingFamilyMembers = new Set<string>();
879
+ for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
880
+ const familyCodes = requirementFamilies.get(baseKey);
881
+ if (familyCodes?.size) {
882
+ const missingInFamily = [...familyCodes].filter((code) => !linkedFullCodes.has(code));
883
+ if (missingInFamily.length === 0) continue;
884
+ const linkedInFamilyCount = familyCodes.size - missingInFamily.length;
885
+ if (linkedInFamilyCount === 0) {
886
+ missingBaseWhenFamilyUncovered.add(baseKey);
887
+ } else {
888
+ for (const code of missingInFamily) {
889
+ missingFamilyMembers.add(code);
890
+ }
891
+ }
892
+ continue;
893
+ }
894
+
895
+ // Fallback path when family data is unavailable for this base key.
896
+ for (const code of mentionedCodes) {
897
+ const hasSpecificSuffix = /-\d+$/.test(code);
898
+ if (hasSpecificSuffix) {
899
+ if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
900
+ } else if (!linkedBaseKeys.has(baseKey)) {
901
+ missingBaseWhenFamilyUncovered.add(baseKey);
902
+ }
903
+ }
904
+ }
905
+
879
906
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
880
907
  // linked members of that same family are not considered "linked but not mentioned".
881
908
  const extraLinked = [...linkedFullCodes].filter((code) => {
@@ -894,13 +921,22 @@ export default class ResultDataProvider {
894
921
  mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
895
922
  };
896
923
 
897
- const sortedMissingMentioned = [...new Set(missingMentioned)].sort((a, b) => a.localeCompare(b));
898
- const sortedMissingFamily = [...new Set(missingFamily)].sort((a, b) => a.localeCompare(b));
899
- for (const code of sortedMissingMentioned) {
924
+ const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
925
+ a.localeCompare(b)
926
+ );
927
+ const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
928
+ a.localeCompare(b)
929
+ );
930
+ const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
931
+ for (const code of sortedMissingSpecificMentionedNoFamily) {
900
932
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
901
933
  appendMentionedButNotLinked(code, stepRef);
902
934
  }
903
- for (const code of sortedMissingFamily) {
935
+ for (const baseKey of sortedMissingBaseWhenFamilyUncovered) {
936
+ const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
937
+ appendMentionedButNotLinked(baseKey, stepRef);
938
+ }
939
+ for (const code of sortedMissingFamilyMembers) {
904
940
  const baseKey = this.toRequirementKey(code);
905
941
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
906
942
  appendMentionedButNotLinked(code, stepRef);
@@ -936,7 +972,11 @@ export default class ResultDataProvider {
936
972
  `MEWP internal validation parse diagnostics: ` +
937
973
  `testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
938
974
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
939
- `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${sortedMissingMentioned.length + sortedMissingFamily.length} ` +
975
+ `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
976
+ sortedMissingSpecificMentionedNoFamily.length +
977
+ sortedMissingBaseWhenFamilyUncovered.length +
978
+ sortedMissingFamilyMembers.length
979
+ } ` +
940
980
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
941
981
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
942
982
  );
@@ -1158,7 +1198,8 @@ export default class ResultDataProvider {
1158
1198
  linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
1159
1199
  l3l4ByBaseKey: Map<string, MewpL3L4Pair[]>,
1160
1200
  externalBugsByTestCase: Map<number, MewpBugLink[]>,
1161
- externalJoinKeysByL2?: Map<string, Set<string>>
1201
+ externalJoinKeysByL2?: Map<string, Set<string>>,
1202
+ testCaseResponsibilityById?: Map<number, string>
1162
1203
  ): MewpCoverageRow[] {
1163
1204
  const rows: MewpCoverageRow[] = [];
1164
1205
  const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
@@ -1202,7 +1243,8 @@ export default class ResultDataProvider {
1202
1243
  ...bug,
1203
1244
  responsibility: this.resolveCoverageBugResponsibility(
1204
1245
  String(bug?.responsibility || ''),
1205
- requirement
1246
+ requirement,
1247
+ String(testCaseResponsibilityById?.get(testCaseId) || '')
1206
1248
  ),
1207
1249
  });
1208
1250
  }
@@ -1246,18 +1288,127 @@ export default class ResultDataProvider {
1246
1288
 
1247
1289
  private resolveCoverageBugResponsibility(
1248
1290
  rawResponsibility: string,
1249
- requirement: Pick<MewpL2RequirementFamily, 'responsibility'>
1291
+ requirement: Pick<MewpL2RequirementFamily, 'responsibility'>,
1292
+ testCaseResponsibility: string = ''
1250
1293
  ): string {
1251
- const direct = String(rawResponsibility || '').trim();
1294
+ const normalizeDisplay = (value: string): string => {
1295
+ const direct = String(value || '').trim();
1296
+ if (!direct) return '';
1297
+ const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(direct));
1298
+ if (resolved === 'ESUK') return 'ESUK';
1299
+ if (resolved === 'IL') return 'Elisra';
1300
+ return direct;
1301
+ };
1302
+
1303
+ const direct = normalizeDisplay(rawResponsibility);
1252
1304
  if (direct && direct.toLowerCase() !== 'unknown') return direct;
1253
1305
 
1254
- const requirementResponsibility = String(requirement?.responsibility || '')
1255
- .trim()
1256
- .toUpperCase();
1257
- if (requirementResponsibility === 'ESUK') return 'ESUK';
1258
- if (requirementResponsibility === 'IL' || requirementResponsibility === 'ELISRA') return 'Elisra';
1306
+ const fromTestCase = normalizeDisplay(testCaseResponsibility);
1307
+ if (fromTestCase && fromTestCase.toLowerCase() !== 'unknown') return fromTestCase;
1308
+
1309
+ const fromRequirement = normalizeDisplay(String(requirement?.responsibility || ''));
1310
+ if (fromRequirement && fromRequirement.toLowerCase() !== 'unknown') return fromRequirement;
1259
1311
 
1260
- return direct || 'Unknown';
1312
+ return direct || fromTestCase || fromRequirement || 'Unknown';
1313
+ }
1314
+
1315
+ private async buildMewpTestCaseResponsibilityMap(
1316
+ testData: any[],
1317
+ projectName: string
1318
+ ): Promise<Map<number, string>> {
1319
+ const out = new Map<number, string>();
1320
+ const unresolved = new Set<number>();
1321
+
1322
+ const extractFromWorkItemFields = (workItemFields: any): Record<string, any> => {
1323
+ const fields: Record<string, any> = {};
1324
+ if (!Array.isArray(workItemFields)) return fields;
1325
+ for (const field of workItemFields) {
1326
+ const keyCandidates = [field?.key, field?.name, field?.referenceName]
1327
+ .map((item) => String(item || '').trim())
1328
+ .filter((item) => !!item);
1329
+ for (const key of keyCandidates) {
1330
+ fields[key] = field?.value;
1331
+ }
1332
+ }
1333
+ return fields;
1334
+ };
1335
+
1336
+ for (const suite of testData || []) {
1337
+ const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
1338
+ for (const testCase of testCasesItems) {
1339
+ const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
1340
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
1341
+
1342
+ const workItemFieldsMap = testCase?.workItem?.fields || {};
1343
+ const workItemFieldsList = extractFromWorkItemFields(testCase?.workItem?.workItemFields);
1344
+ const directFieldAliases = Object.fromEntries(
1345
+ Object.entries({
1346
+ 'System.AreaPath':
1347
+ testCase?.workItem?.areaPath ||
1348
+ testCase?.workItem?.AreaPath ||
1349
+ testCase?.areaPath ||
1350
+ testCase?.AreaPath ||
1351
+ testCase?.testCase?.areaPath ||
1352
+ testCase?.testCase?.AreaPath,
1353
+ 'Area Path':
1354
+ testCase?.workItem?.['Area Path'] ||
1355
+ testCase?.['Area Path'] ||
1356
+ testCase?.testCase?.['Area Path'],
1357
+ AreaPath:
1358
+ testCase?.workItem?.AreaPath ||
1359
+ testCase?.AreaPath ||
1360
+ testCase?.testCase?.AreaPath,
1361
+ }).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '')
1362
+ );
1363
+ const fromWorkItem = this.deriveMewpTestCaseResponsibility({
1364
+ ...directFieldAliases,
1365
+ ...workItemFieldsList,
1366
+ ...workItemFieldsMap,
1367
+ });
1368
+
1369
+ if (fromWorkItem) {
1370
+ out.set(testCaseId, fromWorkItem);
1371
+ } else {
1372
+ unresolved.add(testCaseId);
1373
+ }
1374
+ }
1375
+
1376
+ const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
1377
+ for (const testPoint of testPointsItems) {
1378
+ const testCaseId = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
1379
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
1380
+ const fromPoint = this.deriveMewpTestCaseResponsibility({
1381
+ 'System.AreaPath': testPoint?.areaPath || testPoint?.AreaPath,
1382
+ 'Area Path': testPoint?.['Area Path'],
1383
+ AreaPath: testPoint?.AreaPath,
1384
+ });
1385
+ if (fromPoint) {
1386
+ out.set(testCaseId, fromPoint);
1387
+ unresolved.delete(testCaseId);
1388
+ } else {
1389
+ unresolved.add(testCaseId);
1390
+ }
1391
+ }
1392
+ }
1393
+
1394
+ if (unresolved.size > 0) {
1395
+ try {
1396
+ const workItems = await this.fetchWorkItemsByIds(projectName, [...unresolved], false);
1397
+ for (const workItem of workItems || []) {
1398
+ const testCaseId = Number(workItem?.id || 0);
1399
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
1400
+ const resolved = this.deriveMewpTestCaseResponsibility(workItem?.fields || {});
1401
+ if (!resolved) continue;
1402
+ out.set(testCaseId, resolved);
1403
+ }
1404
+ } catch (error: any) {
1405
+ logger.warn(
1406
+ `MEWP coverage: failed to enrich test-case responsibility fallback: ${error?.message || error}`
1407
+ );
1408
+ }
1409
+ }
1410
+
1411
+ return out;
1261
1412
  }
1262
1413
 
1263
1414
  private resolveMewpL2RunStatus(input: {
@@ -2457,11 +2608,42 @@ export default class ResultDataProvider {
2457
2608
  if (fromExplicitLabel) return fromExplicitLabel;
2458
2609
  if (explicitSapWbsByLabel) return explicitSapWbsByLabel;
2459
2610
 
2460
- const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
2461
- const fromAreaPath = this.resolveMewpResponsibility(areaPath);
2462
- if (fromAreaPath) return fromAreaPath;
2611
+ const areaPathCandidates = [
2612
+ fields?.['System.AreaPath'],
2613
+ fields?.['Area Path'],
2614
+ fields?.['AreaPath'],
2615
+ ];
2616
+ for (const candidate of areaPathCandidates) {
2617
+ const normalized = this.toMewpComparableText(candidate);
2618
+ const resolved = this.resolveMewpResponsibility(normalized);
2619
+ if (resolved) return resolved;
2620
+ }
2621
+
2622
+ const keyHints = ['sapwbs', 'responsibility', 'owner', 'areapath', 'area path'];
2623
+ for (const [key, value] of Object.entries(fields || {})) {
2624
+ const normalizedKey = String(key || '').toLowerCase();
2625
+ if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
2626
+ const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(value));
2627
+ if (resolved) return resolved;
2628
+ }
2629
+
2630
+ return '';
2631
+ }
2632
+
2633
+ // Test-case responsibility must come from test-case path context (not SAPWBS).
2634
+ private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
2635
+ const areaPathCandidates = [
2636
+ fields?.['System.AreaPath'],
2637
+ fields?.['Area Path'],
2638
+ fields?.['AreaPath'],
2639
+ ];
2640
+ for (const candidate of areaPathCandidates) {
2641
+ const normalized = this.toMewpComparableText(candidate);
2642
+ const resolved = this.resolveMewpResponsibility(normalized);
2643
+ if (resolved) return resolved;
2644
+ }
2463
2645
 
2464
- const keyHints = ['sapwbs', 'responsibility', 'owner'];
2646
+ const keyHints = ['areapath', 'area path', 'responsibility', 'owner'];
2465
2647
  for (const [key, value] of Object.entries(fields || {})) {
2466
2648
  const normalizedKey = String(key || '').toLowerCase();
2467
2649
  if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
@@ -2905,7 +3087,6 @@ export default class ResultDataProvider {
2905
3087
  logger.warn(`Invalid run result ${runId} or result ${resultId}`);
2906
3088
  return null;
2907
3089
  }
2908
- logger.warn(`Current Test point for Test case ${point.testCaseId} is in Active state`);
2909
3090
  const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${point.testCaseId}?$expand=all`;
2910
3091
  const testCaseData = await TFSServices.getItemContent(url, this.token);
2911
3092
  const newResultData: PlainTestResult = {