@elisra-devops/docgen-data-provider 1.86.0 → 1.88.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.86.0",
3
+ "version": "1.88.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -808,6 +808,34 @@ export default class ResultDataProvider {
808
808
 
809
809
  for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
810
810
  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
+ };
811
839
  const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
812
840
  const parsedSteps =
813
841
  stepsXml && String(stepsXml).trim() !== ''
@@ -860,6 +888,15 @@ export default class ResultDataProvider {
860
888
  const linkedBaseKeys = new Set<string>(
861
889
  [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
862
890
  );
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
+ );
863
900
 
864
901
  const mentionedCodesByBase = new Map<string, Set<string>>();
865
902
  for (const code of mentionedL2Only) {
@@ -878,29 +915,92 @@ export default class ResultDataProvider {
878
915
  const missingFamilyMembers = new Set<string>();
879
916
  for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
880
917
  const familyCodes = requirementFamilies.get(baseKey);
918
+ const mentionedCodesList = [...mentionedCodes];
919
+ const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
920
+ 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
+
881
927
  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);
928
+ familyAllCodes = [...familyCodes].sort((a, b) => a.localeCompare(b));
929
+ familyLinkedCodes = familyAllCodes.filter((code) => linkedFullCodes.has(code));
930
+ // Base mention ("SR0054") validates against child coverage when children exist.
931
+ // If no child variants exist, fallback to the single standalone requirement code.
932
+ if (hasBaseMention) {
933
+ const familyCodesList = [...familyCodes];
934
+ const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
935
+ const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
936
+ familyTargetCodes = [...targetFamilyCodes].sort((a, b) => a.localeCompare(b));
937
+ const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
938
+ familyMissingCodes = [...missingInTargetFamily].sort((a, b) => a.localeCompare(b));
939
+
940
+ if (missingInTargetFamily.length > 0) {
941
+ const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
942
+ if (!hasAnyLinkedInFamily) {
943
+ missingBaseWhenFamilyUncovered.add(baseKey);
944
+ familyDecision = 'base-mentioned-family-uncovered';
945
+ } else {
946
+ for (const code of missingInTargetFamily) {
947
+ missingFamilyMembers.add(code);
948
+ }
949
+ familyDecision = 'base-mentioned-family-partial-missing-children';
950
+ }
951
+ } else {
952
+ familyDecision = 'base-mentioned-family-fully-covered';
953
+ }
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
+ continue;
961
+ }
962
+
963
+ // Specific mention ("SR0054-1") validates as exact-match only.
964
+ const missingSpecificMembers: string[] = [];
965
+ for (const code of mentionedSpecificMembers) {
966
+ if (!linkedFullCodes.has(code)) {
967
+ missingSpecificMentionedNoFamily.add(code);
968
+ missingSpecificMembers.push(code);
890
969
  }
891
970
  }
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
+ );
892
981
  continue;
893
982
  }
894
983
 
895
984
  // Fallback path when family data is unavailable for this base key.
985
+ const fallbackMissingSpecific: string[] = [];
986
+ let fallbackBaseMissing = false;
896
987
  for (const code of mentionedCodes) {
897
988
  const hasSpecificSuffix = /-\d+$/.test(code);
898
989
  if (hasSpecificSuffix) {
899
- if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
990
+ if (!linkedFullCodes.has(code)) {
991
+ missingSpecificMentionedNoFamily.add(code);
992
+ fallbackMissingSpecific.push(code);
993
+ }
900
994
  } else if (!linkedBaseKeys.has(baseKey)) {
901
995
  missingBaseWhenFamilyUncovered.add(baseKey);
996
+ fallbackBaseMissing = true;
902
997
  }
903
998
  }
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
+ );
904
1004
  }
905
1005
 
906
1006
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
@@ -960,11 +1060,19 @@ export default class ResultDataProvider {
960
1060
  return String(a[0]).localeCompare(String(b[0]));
961
1061
  })
962
1062
  .map(([stepRef, requirementIds]) => {
963
- const requirementList = [...requirementIds].sort((a, b) => a.localeCompare(b));
964
- return `${stepRef}: ${requirementList.join(', ')}`;
1063
+ const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
1064
+ return `${stepRef}: ${groupedRequirementList}`;
965
1065
  })
966
1066
  .join('; ');
967
- const linkedButNotMentioned = sortedExtraLinked.join('; ');
1067
+ 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
+ );
968
1076
  const validationStatus: 'Pass' | 'Fail' =
969
1077
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
970
1078
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -2735,6 +2843,28 @@ export default class ResultDataProvider {
2735
2843
  return `SR${match[1]}`;
2736
2844
  }
2737
2845
 
2846
+ private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2847
+ const byBaseKey = new Map<string, Set<string>>();
2848
+ for (const rawCode of codes || []) {
2849
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(String(rawCode || ''));
2850
+ if (!normalizedCode) continue;
2851
+ const baseKey = this.toRequirementKey(normalizedCode) || normalizedCode;
2852
+ if (!byBaseKey.has(baseKey)) byBaseKey.set(baseKey, new Set<string>());
2853
+ byBaseKey.get(baseKey)!.add(normalizedCode);
2854
+ }
2855
+
2856
+ if (byBaseKey.size === 0) return '';
2857
+
2858
+ return [...byBaseKey.entries()]
2859
+ .sort((a, b) => a[0].localeCompare(b[0]))
2860
+ .map(([baseKey, members]) => {
2861
+ const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2862
+ if (sortedMembers.length <= 1) return sortedMembers[0];
2863
+ return `${baseKey}: ${sortedMembers.join(', ')}`;
2864
+ })
2865
+ .join('; ');
2866
+ }
2867
+
2738
2868
  private toMewpComparableText(value: any): string {
2739
2869
  if (value === null || value === undefined) return '';
2740
2870
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
@@ -1986,6 +1986,158 @@ describe('ResultDataProvider', () => {
1986
1986
  );
1987
1987
  });
1988
1988
 
1989
+ it('should pass when a base SR mention is fully covered by its only linked child', async () => {
1990
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
1992
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
1993
+ {
1994
+ testPointsItems: [{ testCaseId: 402, testCaseName: 'TC 402 - Single child covered' }],
1995
+ testCasesItems: [
1996
+ {
1997
+ workItem: {
1998
+ id: 402,
1999
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-402\"></steps>' }],
2000
+ },
2001
+ },
2002
+ ],
2003
+ },
2004
+ ]);
2005
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2006
+ {
2007
+ workItemId: 9101,
2008
+ requirementId: 'SR0054',
2009
+ baseKey: 'SR0054',
2010
+ title: 'SR0054 parent',
2011
+ responsibility: 'ESUK',
2012
+ linkedTestCaseIds: [],
2013
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2014
+ },
2015
+ {
2016
+ workItemId: 9102,
2017
+ requirementId: 'SR0054-1',
2018
+ baseKey: 'SR0054',
2019
+ title: 'SR0054 child 1',
2020
+ responsibility: 'ESUK',
2021
+ linkedTestCaseIds: [],
2022
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2023
+ },
2024
+ ]);
2025
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2026
+ new Map([
2027
+ [
2028
+ 402,
2029
+ {
2030
+ baseKeys: new Set(['SR0054']),
2031
+ fullCodes: new Set(['SR0054-1']),
2032
+ },
2033
+ ],
2034
+ ])
2035
+ );
2036
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2037
+ {
2038
+ stepId: '3',
2039
+ stepPosition: '3',
2040
+ action: 'Validate family root',
2041
+ expected: 'SR0054',
2042
+ isSharedStepTitle: false,
2043
+ },
2044
+ ]);
2045
+
2046
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2047
+ '123',
2048
+ mockProjectName,
2049
+ [1]
2050
+ );
2051
+
2052
+ expect(result.rows).toHaveLength(1);
2053
+ expect(result.rows[0]).toEqual(
2054
+ expect.objectContaining({
2055
+ 'Test Case ID': 402,
2056
+ 'Mentioned but Not Linked': '',
2057
+ 'Linked but Not Mentioned': '',
2058
+ 'Validation Status': 'Pass',
2059
+ })
2060
+ );
2061
+ });
2062
+
2063
+ it('should group linked-but-not-mentioned requirements by SR family', async () => {
2064
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2065
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2066
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2067
+ {
2068
+ testPointsItems: [{ testCaseId: 403, testCaseName: 'TC 403 - Linked only' }],
2069
+ testCasesItems: [
2070
+ {
2071
+ workItem: {
2072
+ id: 403,
2073
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-403\"></steps>' }],
2074
+ },
2075
+ },
2076
+ ],
2077
+ },
2078
+ ]);
2079
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2080
+ {
2081
+ workItemId: 9201,
2082
+ requirementId: 'SR0054-1',
2083
+ baseKey: 'SR0054',
2084
+ title: 'SR0054 child 1',
2085
+ responsibility: 'ESUK',
2086
+ linkedTestCaseIds: [],
2087
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2088
+ },
2089
+ {
2090
+ workItemId: 9202,
2091
+ requirementId: 'SR0054-2',
2092
+ baseKey: 'SR0054',
2093
+ title: 'SR0054 child 2',
2094
+ responsibility: 'ESUK',
2095
+ linkedTestCaseIds: [],
2096
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2097
+ },
2098
+ {
2099
+ workItemId: 9203,
2100
+ requirementId: 'SR0100-1',
2101
+ baseKey: 'SR0100',
2102
+ title: 'SR0100 child 1',
2103
+ responsibility: 'ESUK',
2104
+ linkedTestCaseIds: [],
2105
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2106
+ },
2107
+ ]);
2108
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2109
+ new Map([
2110
+ [
2111
+ 403,
2112
+ {
2113
+ baseKeys: new Set(['SR0054', 'SR0100']),
2114
+ fullCodes: new Set(['SR0054-1', 'SR0054-2', 'SR0100-1']),
2115
+ },
2116
+ ],
2117
+ ])
2118
+ );
2119
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2120
+ {
2121
+ stepId: '1',
2122
+ stepPosition: '1',
2123
+ action: 'Action only',
2124
+ expected: '',
2125
+ isSharedStepTitle: false,
2126
+ },
2127
+ ]);
2128
+
2129
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2130
+ '123',
2131
+ mockProjectName,
2132
+ [1]
2133
+ );
2134
+
2135
+ expect(result.rows).toHaveLength(1);
2136
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).toBe('');
2137
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0054: SR0054-1, SR0054-2');
2138
+ expect(String(result.rows[0]['Linked but Not Mentioned'] || '')).toContain('SR0100-1');
2139
+ });
2140
+
1989
2141
  it('should report only base SR when an entire mentioned family is uncovered', async () => {
1990
2142
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
2143
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2393,7 +2545,7 @@ describe('ResultDataProvider', () => {
2393
2545
  expect(byTestCase.get(201)).toEqual(
2394
2546
  expect.objectContaining({
2395
2547
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2396
- 'Mentioned but Not Linked': 'Step 1: SR0095-3, SR0511-1, SR0511-2',
2548
+ 'Mentioned but Not Linked': 'Step 1: SR0095-3; SR0511: SR0511-1, SR0511-2',
2397
2549
  'Linked but Not Mentioned': 'SR8888',
2398
2550
  'Validation Status': 'Fail',
2399
2551
  })