@elisra-devops/docgen-data-provider 1.85.0 → 1.87.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.87.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,69 @@ 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
+ const mentionedCodesList = [...mentionedCodes];
882
+ const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
883
+ const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
884
+
885
+ if (familyCodes?.size) {
886
+ // Base mention ("SR0054") validates against child coverage when children exist.
887
+ // If no child variants exist, fallback to the single standalone requirement code.
888
+ if (hasBaseMention) {
889
+ const familyCodesList = [...familyCodes];
890
+ const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
891
+ const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
892
+ const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
893
+
894
+ if (missingInTargetFamily.length > 0) {
895
+ const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
896
+ if (!hasAnyLinkedInFamily) {
897
+ missingBaseWhenFamilyUncovered.add(baseKey);
898
+ } else {
899
+ for (const code of missingInTargetFamily) {
900
+ missingFamilyMembers.add(code);
901
+ }
902
+ }
903
+ }
904
+ continue;
905
+ }
906
+
907
+ // Specific mention ("SR0054-1") validates as exact-match only.
908
+ for (const code of mentionedSpecificMembers) {
909
+ if (!linkedFullCodes.has(code)) {
910
+ missingSpecificMentionedNoFamily.add(code);
911
+ }
912
+ }
913
+ continue;
914
+ }
915
+
916
+ // Fallback path when family data is unavailable for this base key.
917
+ for (const code of mentionedCodes) {
918
+ const hasSpecificSuffix = /-\d+$/.test(code);
919
+ if (hasSpecificSuffix) {
920
+ if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
921
+ } else if (!linkedBaseKeys.has(baseKey)) {
922
+ missingBaseWhenFamilyUncovered.add(baseKey);
923
+ }
924
+ }
925
+ }
926
+
884
927
  // Direction B is family-based: if any member of a family is mentioned in Expected Result,
885
928
  // linked members of that same family are not considered "linked but not mentioned".
886
929
  const extraLinked = [...linkedFullCodes].filter((code) => {
@@ -899,13 +942,22 @@ export default class ResultDataProvider {
899
942
  mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
900
943
  };
901
944
 
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) {
945
+ const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
946
+ a.localeCompare(b)
947
+ );
948
+ const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
949
+ a.localeCompare(b)
950
+ );
951
+ const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
952
+ for (const code of sortedMissingSpecificMentionedNoFamily) {
905
953
  const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
906
954
  appendMentionedButNotLinked(code, stepRef);
907
955
  }
908
- for (const code of sortedMissingFamily) {
956
+ for (const baseKey of sortedMissingBaseWhenFamilyUncovered) {
957
+ const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
958
+ appendMentionedButNotLinked(baseKey, stepRef);
959
+ }
960
+ for (const code of sortedMissingFamilyMembers) {
909
961
  const baseKey = this.toRequirementKey(code);
910
962
  const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
911
963
  appendMentionedButNotLinked(code, stepRef);
@@ -929,11 +981,11 @@ export default class ResultDataProvider {
929
981
  return String(a[0]).localeCompare(String(b[0]));
930
982
  })
931
983
  .map(([stepRef, requirementIds]) => {
932
- const requirementList = [...requirementIds].sort((a, b) => a.localeCompare(b));
933
- return `${stepRef}: ${requirementList.join(', ')}`;
984
+ const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
985
+ return `${stepRef}: ${groupedRequirementList}`;
934
986
  })
935
987
  .join('; ');
936
- const linkedButNotMentioned = sortedExtraLinked.join('; ');
988
+ const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
937
989
  const validationStatus: 'Pass' | 'Fail' =
938
990
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
939
991
  if (validationStatus === 'Fail') diagnostics.failingRows += 1;
@@ -941,7 +993,11 @@ export default class ResultDataProvider {
941
993
  `MEWP internal validation parse diagnostics: ` +
942
994
  `testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
943
995
  `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
944
- `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${sortedMissingMentioned.length + sortedMissingFamily.length} ` +
996
+ `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
997
+ sortedMissingSpecificMentionedNoFamily.length +
998
+ sortedMissingBaseWhenFamilyUncovered.length +
999
+ sortedMissingFamilyMembers.length
1000
+ } ` +
945
1001
  `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
946
1002
  `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
947
1003
  );
@@ -2700,6 +2756,28 @@ export default class ResultDataProvider {
2700
2756
  return `SR${match[1]}`;
2701
2757
  }
2702
2758
 
2759
+ private formatRequirementCodesGroupedByFamily(codes: Iterable<string>): string {
2760
+ const byBaseKey = new Map<string, Set<string>>();
2761
+ for (const rawCode of codes || []) {
2762
+ const normalizedCode = this.normalizeMewpRequirementCodeWithSuffix(String(rawCode || ''));
2763
+ if (!normalizedCode) continue;
2764
+ const baseKey = this.toRequirementKey(normalizedCode) || normalizedCode;
2765
+ if (!byBaseKey.has(baseKey)) byBaseKey.set(baseKey, new Set<string>());
2766
+ byBaseKey.get(baseKey)!.add(normalizedCode);
2767
+ }
2768
+
2769
+ if (byBaseKey.size === 0) return '';
2770
+
2771
+ return [...byBaseKey.entries()]
2772
+ .sort((a, b) => a[0].localeCompare(b[0]))
2773
+ .map(([baseKey, members]) => {
2774
+ const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
2775
+ if (sortedMembers.length <= 1) return sortedMembers[0];
2776
+ return `${baseKey}: ${sortedMembers.join(', ')}`;
2777
+ })
2778
+ .join('; ');
2779
+ }
2780
+
2703
2781
  private toMewpComparableText(value: any): string {
2704
2782
  if (value === null || value === undefined) return '';
2705
2783
  if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
@@ -1986,6 +1986,226 @@ 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
+
2141
+ it('should report only base SR when an entire mentioned family is uncovered', async () => {
2142
+ jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
2143
+ jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
2144
+ jest.spyOn(resultDataProvider as any, 'fetchTestData').mockResolvedValueOnce([
2145
+ {
2146
+ testPointsItems: [{ testCaseId: 401, testCaseName: 'TC 401 - Family uncovered' }],
2147
+ testCasesItems: [
2148
+ {
2149
+ workItem: {
2150
+ id: 401,
2151
+ workItemFields: [{ key: 'Steps', value: '<steps id=\"mock-steps-tc-401\"></steps>' }],
2152
+ },
2153
+ },
2154
+ ],
2155
+ },
2156
+ ]);
2157
+ jest.spyOn(resultDataProvider as any, 'fetchMewpL2Requirements').mockResolvedValueOnce([
2158
+ {
2159
+ workItemId: 9001,
2160
+ requirementId: 'SR0054-1',
2161
+ baseKey: 'SR0054',
2162
+ title: 'SR0054 child 1',
2163
+ responsibility: 'ESUK',
2164
+ linkedTestCaseIds: [],
2165
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2166
+ },
2167
+ {
2168
+ workItemId: 9002,
2169
+ requirementId: 'SR0054-2',
2170
+ baseKey: 'SR0054',
2171
+ title: 'SR0054 child 2',
2172
+ responsibility: 'ESUK',
2173
+ linkedTestCaseIds: [],
2174
+ areaPath: 'MEWP\\Customer Requirements\\Level 2',
2175
+ },
2176
+ ]);
2177
+ jest.spyOn(resultDataProvider as any, 'buildLinkedRequirementsByTestCase').mockResolvedValueOnce(
2178
+ new Map([[401, { baseKeys: new Set<string>(), fullCodes: new Set<string>() }]])
2179
+ );
2180
+ jest.spyOn((resultDataProvider as any).testStepParserHelper, 'parseTestSteps').mockResolvedValueOnce([
2181
+ {
2182
+ stepId: '3',
2183
+ stepPosition: '3',
2184
+ action: 'Validate family root',
2185
+ expected: 'SR0054',
2186
+ isSharedStepTitle: false,
2187
+ },
2188
+ ]);
2189
+
2190
+ const result = await (resultDataProvider as any).getMewpInternalValidationFlatResults(
2191
+ '123',
2192
+ mockProjectName,
2193
+ [1]
2194
+ );
2195
+
2196
+ expect(result.rows).toHaveLength(1);
2197
+ expect(result.rows[0]).toEqual(
2198
+ expect.objectContaining({
2199
+ 'Test Case ID': 401,
2200
+ 'Mentioned but Not Linked': 'Step 3: SR0054',
2201
+ 'Linked but Not Mentioned': '',
2202
+ 'Validation Status': 'Fail',
2203
+ })
2204
+ );
2205
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-1');
2206
+ expect(String(result.rows[0]['Mentioned but Not Linked'] || '')).not.toContain('SR0054-2');
2207
+ });
2208
+
1989
2209
  it('should not duplicate Direction A discrepancy when same requirement is repeated in multiple steps', async () => {
1990
2210
  jest.spyOn(resultDataProvider as any, 'fetchTestPlanName').mockResolvedValueOnce('Plan A');
1991
2211
  jest.spyOn(resultDataProvider as any, 'fetchTestSuites').mockResolvedValueOnce([{ testSuiteId: 1 }]);
@@ -2325,7 +2545,7 @@ describe('ResultDataProvider', () => {
2325
2545
  expect(byTestCase.get(201)).toEqual(
2326
2546
  expect.objectContaining({
2327
2547
  'Test Case Title': 'TC 201 - Mixed discrepancies',
2328
- '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',
2329
2549
  'Linked but Not Mentioned': 'SR8888',
2330
2550
  'Validation Status': 'Fail',
2331
2551
  })
@@ -2662,6 +2882,115 @@ describe('ResultDataProvider', () => {
2662
2882
  });
2663
2883
  });
2664
2884
 
2885
+ describe('buildLinkedRequirementsByTestCase', () => {
2886
+ it('should map linked requirements only for supported test-case requirement relation types', async () => {
2887
+ const requirements = [
2888
+ {
2889
+ workItemId: 7001,
2890
+ requirementId: 'SR0054-1',
2891
+ baseKey: 'SR0054',
2892
+ linkedTestCaseIds: [],
2893
+ },
2894
+ ];
2895
+ const testData = [
2896
+ {
2897
+ testCasesItems: [{ workItem: { id: 1001 } }],
2898
+ testPointsItems: [],
2899
+ },
2900
+ ];
2901
+
2902
+ const fetchByIdsSpy = jest
2903
+ .spyOn(resultDataProvider as any, 'fetchWorkItemsByIds')
2904
+ .mockImplementation(async (...args: any[]) => {
2905
+ const ids = Array.isArray(args?.[1]) ? args[1] : [];
2906
+ const includeRelations = !!args?.[2];
2907
+ if (includeRelations) {
2908
+ return [
2909
+ {
2910
+ id: 1001,
2911
+ relations: [
2912
+ {
2913
+ rel: 'Microsoft.VSTS.Common.TestedBy-Reverse',
2914
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
2915
+ },
2916
+ {
2917
+ rel: 'System.LinkTypes.Related',
2918
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7001',
2919
+ },
2920
+ ],
2921
+ },
2922
+ ];
2923
+ }
2924
+ return ids.map((id) => ({
2925
+ id,
2926
+ fields: {
2927
+ 'System.WorkItemType': id === 7001 ? 'Requirement' : 'Test Case',
2928
+ },
2929
+ }));
2930
+ });
2931
+
2932
+ const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
2933
+ requirements,
2934
+ testData,
2935
+ mockProjectName
2936
+ );
2937
+
2938
+ expect(fetchByIdsSpy).toHaveBeenCalled();
2939
+ expect(linked.get(1001)?.baseKeys?.has('SR0054')).toBe(true);
2940
+ expect(linked.get(1001)?.fullCodes?.has('SR0054-1')).toBe(true);
2941
+ });
2942
+
2943
+ it('should ignore unsupported relation types when linking test case to requirements', async () => {
2944
+ const requirements = [
2945
+ {
2946
+ workItemId: 7002,
2947
+ requirementId: 'SR0099-1',
2948
+ baseKey: 'SR0099',
2949
+ linkedTestCaseIds: [],
2950
+ },
2951
+ ];
2952
+ const testData = [
2953
+ {
2954
+ testCasesItems: [{ workItem: { id: 1002 } }],
2955
+ testPointsItems: [],
2956
+ },
2957
+ ];
2958
+
2959
+ jest.spyOn(resultDataProvider as any, 'fetchWorkItemsByIds').mockImplementation(async (...args: any[]) => {
2960
+ const ids = Array.isArray(args?.[1]) ? args[1] : [];
2961
+ const includeRelations = !!args?.[2];
2962
+ if (includeRelations) {
2963
+ return [
2964
+ {
2965
+ id: 1002,
2966
+ relations: [
2967
+ {
2968
+ rel: 'System.LinkTypes.Related',
2969
+ url: 'https://dev.azure.com/org/project/_apis/wit/workItems/7002',
2970
+ },
2971
+ ],
2972
+ },
2973
+ ];
2974
+ }
2975
+ return ids.map((id) => ({
2976
+ id,
2977
+ fields: {
2978
+ 'System.WorkItemType': id === 7002 ? 'Requirement' : 'Test Case',
2979
+ },
2980
+ }));
2981
+ });
2982
+
2983
+ const linked = await (resultDataProvider as any).buildLinkedRequirementsByTestCase(
2984
+ requirements,
2985
+ testData,
2986
+ mockProjectName
2987
+ );
2988
+
2989
+ expect(linked.get(1002)?.baseKeys?.has('SR0099')).toBe(false);
2990
+ expect(linked.get(1002)?.fullCodes?.has('SR0099-1')).toBe(false);
2991
+ });
2992
+ });
2993
+
2665
2994
  describe('MEWP rel fallback scoping', () => {
2666
2995
  it('should fallback to previous Rel run when latest selected Rel has no run for a test case', async () => {
2667
2996
  const suites = [