@elisra-devops/docgen-data-provider 1.85.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.85.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",
@@ -848,18 +848,6 @@ export default class ResultDataProvider {
848
848
  [...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
849
849
  );
850
850
 
851
- const expectedFamilyCodes = new Set<string>();
852
- for (const baseKey of mentionedBaseKeys) {
853
- const familyCodes = requirementFamilies.get(baseKey);
854
- if (familyCodes?.size) {
855
- familyCodes.forEach((code) => expectedFamilyCodes.add(code));
856
- } else {
857
- for (const code of mentionedL2Only) {
858
- if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
859
- }
860
- }
861
- }
862
-
863
851
  const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
864
852
  const linkedFullCodes =
865
853
  scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
@@ -873,14 +861,48 @@ export default class ResultDataProvider {
873
861
  [...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
874
862
  );
875
863
 
876
- const missingMentioned = [...mentionedL2Only].filter((code) => {
864
+ const mentionedCodesByBase = new Map<string, Set<string>>();
865
+ for (const code of mentionedL2Only) {
877
866
  const baseKey = this.toRequirementKey(code);
878
- if (!baseKey) return false;
879
- const hasSpecificSuffix = /-\d+$/.test(code);
880
- if (hasSpecificSuffix) return !linkedFullCodes.has(code);
881
- return !linkedBaseKeys.has(baseKey);
882
- });
883
- 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
+
884
906
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
885
907
  // linked members of that same family are not considered "linked but not mentioned".
886
908
  const extraLinked = [...linkedFullCodes].filter((code) => {
@@ -899,13 +921,22 @@ export default class ResultDataProvider {
899
921
  mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
900
922
  };
901
923
 
902
- const sortedMissingMentioned = [...new Set(missingMentioned)].sort((a, b) => a.localeCompare(b));
903
- const sortedMissingFamily = [...new Set(missingFamily)].sort((a, b) => a.localeCompare(b));
904
- 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) {
905
932
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
906
933
  appendMentionedButNotLinked(code, stepRef);
907
934
  }
908
- 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) {
909
940
  const baseKey = this.toRequirementKey(code);
910
941
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
911
942
  appendMentionedButNotLinked(code, stepRef);
@@ -941,7 +972,11 @@ export default class ResultDataProvider {
941
972
  `MEWP internal validation parse diagnostics: ` +
942
973
  `testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
943
974
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
944
- `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${sortedMissingMentioned.length + sortedMissingFamily.length} ` +
975
+ `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
976
+ sortedMissingSpecificMentionedNoFamily.length +
977
+ sortedMissingBaseWhenFamilyUncovered.length +
978
+ sortedMissingFamilyMembers.length
979
+ } ` +
945
980
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
946
981
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
947
982
  );
@@ -1986,6 +1986,74 @@ describe('ResultDataProvider', () => {
1986
1986
  );
1987
1987
  });
1988
1988
 
1989
+ it('should report only base SR when an entire mentioned family is uncovered', 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: 401, testCaseName: 'TC 401 - Family uncovered' }],
1995
+ testCasesItems: [
1996
+ {
1997
+ workItem: {
1998
+ id: 401,
1999
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-401\"></steps>' }],
2000
+ },
2001
+ },
2002
+ ],
2003
+ },
2004
+ ]);
2005
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2006
+ {
2007
+ workItemId: 9001,
2008
+ requirementId: 'SR0054-1',
2009
+ baseKey: 'SR0054',
2010
+ title: 'SR0054 child 1',
2011
+ responsibility: 'ESUK',
2012
+ linkedTestCaseIds: [],
2013
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2014
+ },
2015
+ {
2016
+ workItemId: 9002,
2017
+ requirementId: 'SR0054-2',
2018
+ baseKey: 'SR0054',
2019
+ title: 'SR0054 child 2',
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([[401, { baseKeys: new Set<string>(), fullCodes: new Set<string>() }]])
2027
+ );
2028
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2029
+ {
2030
+ stepId: '3',
2031
+ stepPosition: '3',
2032
+ action: 'Validate family root',
2033
+ expected: 'SR0054',
2034
+ isSharedStepTitle: false,
2035
+ },
2036
+ ]);
2037
+
2038
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2039
+ '123',
2040
+ mockProjectName,
2041
+ [1]
2042
+ );
2043
+
2044
+ expect(result.rows).toHaveLength(1);
2045
+ expect(result.rows[0]).toEqual(
2046
+ expect.objectContaining({
2047
+ 'Test Case ID': 401,
2048
+ 'Mentioned but Not Linked': 'Step 3: SR0054',
2049
+ 'Linked but Not Mentioned': '',
2050
+ 'Validation Status': 'Fail',
2051
+ })
2052
+ );
2053
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-1');
2054
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-2');
2055
+ });
2056
+
1989
2057
  it('should not duplicate Direction A discrepancy when same requirement is repeated in multiple steps', async () => {
1990
2058
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
2059
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2662,6 +2730,115 @@ describe('ResultDataProvider', () => {
2662
2730
  });
2663
2731
  });
2664
2732
 
2733
+ describe('buildLinkedRequirementsByTestCase', () => {
2734
+ it('should map linked requirements only for supported test-case requirement relation types', async () => {
2735
+ const requirements = [
2736
+ {
2737
+ workItemId: 7001,
2738
+ requirementId: 'SR0054-1',
2739
+ baseKey: 'SR0054',
2740
+ linkedTestCaseIds: [],
2741
+ },
2742
+ ];
2743
+ const testData = [
2744
+ {
2745
+ testCasesItems: [{ workItem: { id: 1001 } }],
2746
+ testPointsItems: [],
2747
+ },
2748
+ ];
2749
+
2750
+ const fetchByIdsSpy = jest
2751
+ .spyOn(resultDataProvider as any, 'fetchWorkItemsByIds')
2752
+ .mockImplementation(async (...args: any[]) => {
2753
+ const ids = Array.isArray(args?.[1]) ? args[1] : [];
2754
+ const includeRelations = !!args?.[2];
2755
+ if (includeRelations) {
2756
+ return [
2757
+ {
2758
+ id: 1001,
2759
+ relations: [
2760
+ {
2761
+ rel: 'Microsoft.VSTS.Common.TestedBy-Reverse',
2762
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
2763
+ },
2764
+ {
2765
+ rel: 'System.LinkTypes.Related',
2766
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
2767
+ },
2768
+ ],
2769
+ },
2770
+ ];
2771
+ }
2772
+ return ids.map((id) => ({
2773
+ id,
2774
+ fields: {
2775
+ 'System.WorkItemType': id === 7001 ? 'Requirement' : 'Test Case',
2776
+ },
2777
+ }));
2778
+ });
2779
+
2780
+ const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
2781
+ requirements,
2782
+ testData,
2783
+ mockProjectName
2784
+ );
2785
+
2786
+ expect(fetchByIdsSpy).toHaveBeenCalled();
2787
+ expect(linked.get(1001)?.baseKeys?.has('SR0054')).toBe(true);
2788
+ expect(linked.get(1001)?.fullCodes?.has('SR0054-1')).toBe(true);
2789
+ });
2790
+
2791
+ it('should ignore unsupported relation types when linking test case to requirements', async () => {
2792
+ const requirements = [
2793
+ {
2794
+ workItemId: 7002,
2795
+ requirementId: 'SR0099-1',
2796
+ baseKey: 'SR0099',
2797
+ linkedTestCaseIds: [],
2798
+ },
2799
+ ];
2800
+ const testData = [
2801
+ {
2802
+ testCasesItems: [{ workItem: { id: 1002 } }],
2803
+ testPointsItems: [],
2804
+ },
2805
+ ];
2806
+
2807
+ jest.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds').mockImplementation(async (...args: any[]) => {
2808
+ const ids = Array.isArray(args?.[1]) ? args[1] : [];
2809
+ const includeRelations = !!args?.[2];
2810
+ if (includeRelations) {
2811
+ return [
2812
+ {
2813
+ id: 1002,
2814
+ relations: [
2815
+ {
2816
+ rel: 'System.LinkTypes.Related',
2817
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7002',
2818
+ },
2819
+ ],
2820
+ },
2821
+ ];
2822
+ }
2823
+ return ids.map((id) => ({
2824
+ id,
2825
+ fields: {
2826
+ 'System.WorkItemType': id === 7002 ? 'Requirement' : 'Test Case',
2827
+ },
2828
+ }));
2829
+ });
2830
+
2831
+ const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
2832
+ requirements,
2833
+ testData,
2834
+ mockProjectName
2835
+ );
2836
+
2837
+ expect(linked.get(1002)?.baseKeys?.has('SR0099')).toBe(false);
2838
+ expect(linked.get(1002)?.fullCodes?.has('SR0099-1')).toBe(false);
2839
+ });
2840
+ });
2841
+
2665
2842
  describe('MEWP rel fallback scoping', () => {
2666
2843
  it('should fallback to previous Rel run when latest selected Rel has no run for a test case', async () => {
2667
2844
  const suites = [