@elisra-devops/docgen-data-provider 1.109.0 → 1.110.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.
@@ -78,7 +78,7 @@ const HISTORICAL_WORK_ITEM_FIELDS = [
78
78
 
79
79
  /** Default fields fetched per work item in tree/flat query parsing. */
80
80
  const WI_DEFAULT_FIELDS =
81
- 'System.Description,System.Title,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
81
+ 'System.Description,System.Title,System.WorkItemType,Microsoft.VSTS.TCM.ReproSteps,Microsoft.VSTS.CMMI.Symptom';
82
82
 
83
83
  export default class TicketsDataProvider {
84
84
  orgUrl: string = '';
@@ -666,6 +666,83 @@ export default class TicketsDataProvider {
666
666
  return { systemRequirementsQueryTree };
667
667
  }
668
668
 
669
+ /**
670
+ * Fetches flat Customer/System requirement queries for SysRS traceability.
671
+ * OneHop/tree queries are intentionally excluded for this picker.
672
+ */
673
+ private async fetchFlatUpstreamQueries(
674
+ queries: any,
675
+ excludedFolderNames: string[] = [],
676
+ allowedTypes: string[] = ['requirement'],
677
+ ) {
678
+ const { tree1: systemRequirementsQueryTree } = await this.structureFetchedQueries(
679
+ queries,
680
+ false,
681
+ null,
682
+ allowedTypes,
683
+ [],
684
+ undefined,
685
+ undefined,
686
+ false,
687
+ excludedFolderNames,
688
+ true,
689
+ false,
690
+ );
691
+ return { systemRequirementsQueryTree };
692
+ }
693
+
694
+ private async fetchCustomerRequirementQueries(queries: any) {
695
+ const systemRequirementsQueryTree = await this.structureCustomerRequirementQueries(queries);
696
+ return { systemRequirementsQueryTree };
697
+ }
698
+
699
+ private async structureCustomerRequirementQueries(
700
+ rootQuery: any,
701
+ parentId: any = null,
702
+ ): Promise<any> {
703
+ try {
704
+ if (!rootQuery?.hasChildren) {
705
+ const isExecutableQuery =
706
+ !rootQuery?.isFolder && ['flat', 'tree', 'oneHop'].includes(rootQuery?.queryType);
707
+ return isExecutableQuery ? this.buildQueryNode(rootQuery, parentId) : null;
708
+ }
709
+
710
+ if (!rootQuery.children) {
711
+ const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
712
+ const currentQuery = await TFSServices.getItemContent(queryUrl, this.token);
713
+ if (!currentQuery) {
714
+ return null;
715
+ }
716
+ return await this.structureCustomerRequirementQueries(currentQuery, currentQuery.id);
717
+ }
718
+
719
+ const childResults = await Promise.all(
720
+ rootQuery.children.map((child: any) =>
721
+ this.structureCustomerRequirementQueries(child, rootQuery.id),
722
+ ),
723
+ );
724
+ const children = childResults.filter((child: any) => child !== null);
725
+
726
+ return children.length > 0
727
+ ? {
728
+ id: rootQuery.id,
729
+ pId: parentId,
730
+ value: rootQuery.name,
731
+ title: rootQuery.name,
732
+ children,
733
+ }
734
+ : null;
735
+ } catch (err: any) {
736
+ logger.error(
737
+ `Error occurred while constructing the customer query list ${err.message} with query ${JSON.stringify(
738
+ rootQuery,
739
+ )}`,
740
+ );
741
+ logger.error(`Error stack ${err.message}`);
742
+ return null;
743
+ }
744
+ }
745
+
669
746
  private async fetchSrsQueries(rootQueries: any) {
670
747
  const srsFolder = await this.findQueryFolderByName(rootQueries, 'srs');
671
748
  if (!srsFolder) {
@@ -710,32 +787,42 @@ export default class TicketsDataProvider {
710
787
  const { root: sysRsRoot, found: sysRsRootFound } = await this.getDocTypeRoot(rootQueries, 'sysrs');
711
788
  logger.debug(`[GetSharedQueries][sysrs] using ${sysRsRootFound ? 'dedicated folder' : 'root queries'}`);
712
789
 
713
- const systemRequirementsQueries = await this.fetchSystemRequirementQueries(sysRsRoot, [
714
- 'System To Customer',
715
- 'System To Subsystem',
716
- ]);
790
+ const systemToSubsystemFolderNames = ['system to subsystem', 'system-to-subsystem', 'system subsystem'];
717
791
 
718
- const systemToCustomerFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
719
- 'system to customer',
720
- 'system-to-customer',
721
- 'system customer',
722
- 'subsystem to system',
723
- 'customer to system',
724
- ]);
725
- const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(sysRsRoot, [
726
- 'system to subsystem',
727
- 'system-to-subsystem',
728
- 'system subsystem',
729
- ]);
792
+ const systemRequirementsQueries = await this.fetchSystemRequirementQueries(
793
+ sysRsRoot,
794
+ systemToSubsystemFolderNames,
795
+ );
730
796
 
731
- const subsystemToSystemRequirementsQueries =
732
- await this.fetchRequirementsTraceQueriesForFolder(systemToCustomerFolder);
797
+ const systemToSubsystemFolder = await this.findChildFolderByPossibleNames(
798
+ sysRsRoot,
799
+ systemToSubsystemFolderNames,
800
+ );
733
801
  const systemToSubsystemRequirementsQueries =
734
802
  await this.fetchRequirementsTraceQueriesForFolder(systemToSubsystemFolder);
735
803
 
804
+ // Customer/System requirements (traceability table) picker: scan the entire
805
+ // dedicated SysRS folder for executable query nodes. The selected query
806
+ // defines the customer-side candidate set, so discovery does not infer
807
+ // "customer" semantics from WIQL. Scope is intentionally limited to the
808
+ // dedicated `sysrs` folder so unrelated queries from the Shared Queries root
809
+ // do not leak into the picker.
810
+ let customerRequirementsQueries: any = null;
811
+ if (sysRsRootFound) {
812
+ customerRequirementsQueries = await this.fetchCustomerRequirementQueries(sysRsRoot);
813
+ } else {
814
+ logger.debug(
815
+ '[GetSharedQueries][sysrs] dedicated sysrs folder not found; skipping customer-requirements picker',
816
+ );
817
+ }
818
+
736
819
  return {
737
820
  systemRequirementsQueries,
738
- subsystemToSystemRequirementsQueries,
821
+ customerRequirementsQueries,
822
+ // Legacy field retained for backward compatibility with callers/tests.
823
+ // The sub-system -> system trace table is out of scope for v0 and no
824
+ // longer sourced from a hardcoded "System to Customer" folder.
825
+ subsystemToSystemRequirementsQueries: null,
739
826
  systemToSubsystemRequirementsQueries,
740
827
  };
741
828
  }
@@ -2208,42 +2295,46 @@ export default class TicketsDataProvider {
2208
2295
  }
2209
2296
 
2210
2297
  async PopulateWorkItemsByIds(workItemsArray: any[] = [], projectName: string = ''): Promise<any[]> {
2211
- let url = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
2212
- let res: any[] = [];
2213
- let divByMax = Math.floor(workItemsArray.length / 200);
2214
- let modulusByMax = workItemsArray.length % 200;
2215
- //iterating
2216
- for (let i = 0; i < divByMax; i++) {
2217
- let from = i * 200;
2218
- let to = (i + 1) * 200;
2219
- let currentIds = workItemsArray.slice(from, to);
2220
- try {
2221
- let subRes = await TFSServices.getItemContent(url, this.token, 'post', {
2298
+ const baseUrl = `${this.orgUrl}${projectName}/_apis/wit/workitemsbatch`;
2299
+ // On-prem Azure DevOps Server rejects this POST with HTTP 400 unless an
2300
+ // api-version is explicitly set on the URL. Use the same fallback chain
2301
+ // (7.1 -> 5.1 -> no version) the historical batch hydration already uses,
2302
+ // so the helper stays compatible with ADO cloud and older on-prem tenants.
2303
+ const postBatch = (currentIds: any[]) =>
2304
+ this.withHistoricalApiVersionFallback('populate-workitems-batch', (apiVersion) =>
2305
+ TFSServices.getItemContent(this.appendApiVersion(baseUrl, apiVersion), this.token, 'post', {
2222
2306
  $expand: 'Relations',
2223
2307
  ids: currentIds,
2224
- });
2225
- res = [...res, ...subRes.value];
2308
+ }),
2309
+ ).then(({ result }) => result);
2310
+
2311
+ const res: any[] = [];
2312
+ const divByMax = Math.floor(workItemsArray.length / 200);
2313
+ const modulusByMax = workItemsArray.length % 200;
2314
+ for (let i = 0; i < divByMax; i++) {
2315
+ const from = i * 200;
2316
+ const to = (i + 1) * 200;
2317
+ const currentIds = workItemsArray.slice(from, to);
2318
+ try {
2319
+ const subRes = await postBatch(currentIds);
2320
+ res.push(...subRes.value);
2226
2321
  } catch (error) {
2227
2322
  logger.error(`error populating workitems array`);
2228
2323
  logger.error(JSON.stringify(error));
2229
2324
  return [];
2230
2325
  }
2231
2326
  }
2232
- //compliting the rimainder
2233
2327
  if (modulusByMax !== 0) {
2234
2328
  try {
2235
- let currentIds = workItemsArray.slice(workItemsArray.length - modulusByMax, workItemsArray.length);
2236
- let subRes = await TFSServices.getItemContent(url, this.token, 'post', {
2237
- $expand: 'Relations',
2238
- ids: currentIds,
2239
- });
2240
- res = [...res, ...subRes.value];
2329
+ const currentIds = workItemsArray.slice(workItemsArray.length - modulusByMax, workItemsArray.length);
2330
+ const subRes = await postBatch(currentIds);
2331
+ res.push(...subRes.value);
2241
2332
  } catch (error) {
2242
2333
  logger.error(`error populating workitems array`);
2243
2334
  logger.error(JSON.stringify(error));
2244
2335
  return [];
2245
2336
  }
2246
- } //if
2337
+ }
2247
2338
 
2248
2339
  return res;
2249
2340
  }
@@ -2494,7 +2585,7 @@ export default class TicketsDataProvider {
2494
2585
  * Recursively structures fetched queries into two hierarchical trees (tree1 and tree2)
2495
2586
  * by matching WIQL against allowed Source/Target types and optional area filters.
2496
2587
  * Supports leaf queries of type:
2497
- * - oneHop (always)
2588
+ * - oneHop (when includeOneHopQueries === true)
2498
2589
  * - tree (when includeTreeQueries === true)
2499
2590
  * - flat (when includeFlatQueries === true)
2500
2591
  *
@@ -2508,6 +2599,7 @@ export default class TicketsDataProvider {
2508
2599
  * @param includeTreeQueries - Include 'tree' queries in addition to 'oneHop'. Defaults to `false`.
2509
2600
  * @param excludedFolderNames - Optional list of folder names to skip entirely (case-insensitive exact match).
2510
2601
  * @param includeFlatQueries - Include 'flat' queries (matched by [System.WorkItemType] and [System.AreaPath]). Defaults to `false`.
2602
+ * @param includeOneHopQueries - Include 'oneHop' queries. Defaults to `true` for legacy callers.
2511
2603
  * @returns A promise resolving to an object with `tree1` and `tree2` nodes, or `null` for each when none match.
2512
2604
  * @throws Logs an error if an exception occurs during processing.
2513
2605
  */
@@ -2522,6 +2614,7 @@ export default class TicketsDataProvider {
2522
2614
  includeTreeQueries: boolean = false,
2523
2615
  excludedFolderNames: string[] = [],
2524
2616
  includeFlatQueries: boolean = false,
2617
+ includeOneHopQueries: boolean = true,
2525
2618
  workItemTypeCache?: Map<string, string | null>,
2526
2619
  ): Promise<any> {
2527
2620
  try {
@@ -2540,7 +2633,7 @@ export default class TicketsDataProvider {
2540
2633
  if (!rootQuery.hasChildren) {
2541
2634
  const isLeafCandidate =
2542
2635
  !rootQuery.isFolder &&
2543
- (rootQuery.queryType === 'oneHop' ||
2636
+ ((includeOneHopQueries && rootQuery.queryType === 'oneHop') ||
2544
2637
  (includeTreeQueries && rootQuery.queryType === 'tree') ||
2545
2638
  (includeFlatQueries && rootQuery.queryType === 'flat'));
2546
2639
  if (isLeafCandidate) {
@@ -2636,6 +2729,7 @@ export default class TicketsDataProvider {
2636
2729
  includeTreeQueries,
2637
2730
  excludedFolderNames,
2638
2731
  includeFlatQueries,
2732
+ includeOneHopQueries,
2639
2733
  typeCache,
2640
2734
  );
2641
2735
  }
@@ -2654,6 +2748,7 @@ export default class TicketsDataProvider {
2654
2748
  includeTreeQueries,
2655
2749
  excludedFolderNames,
2656
2750
  includeFlatQueries,
2751
+ includeOneHopQueries,
2657
2752
  typeCache,
2658
2753
  ),
2659
2754
  ),
@@ -93,11 +93,179 @@ describe('TicketsDataProvider', () => {
93
93
  });
94
94
  });
95
95
 
96
+ describe('fetchFlatUpstreamQueries', () => {
97
+ it('should allow only Requirement flat queries and exclude oneHop/tree query types', async () => {
98
+ const structureSpy = jest
99
+ .spyOn(ticketsDataProvider as any, 'structureFetchedQueries')
100
+ .mockResolvedValue({ tree1: { id: 't1' }, tree2: null });
101
+
102
+ await (ticketsDataProvider as any).fetchFlatUpstreamQueries({ hasChildren: false }, []);
103
+
104
+ expect(structureSpy.mock.calls[0][3]).toEqual(['requirement']);
105
+ expect(structureSpy.mock.calls[0][7]).toBe(false);
106
+ expect(structureSpy.mock.calls[0][9]).toBe(true);
107
+ expect(structureSpy.mock.calls[0][10]).toBe(false);
108
+ });
109
+
110
+ it('should allow Requirement flat queries for fallback discovery', async () => {
111
+ const structureSpy = jest
112
+ .spyOn(ticketsDataProvider as any, 'structureFetchedQueries')
113
+ .mockResolvedValue({ tree1: { id: 't1' }, tree2: null });
114
+
115
+ await (ticketsDataProvider as any).fetchFlatUpstreamQueries(
116
+ { hasChildren: false },
117
+ ['system to subsystem', 'system-to-subsystem', 'system subsystem'],
118
+ ['requirement'],
119
+ );
120
+
121
+ expect(structureSpy.mock.calls[0][3]).toEqual(['requirement']);
122
+ expect(structureSpy.mock.calls[0][8]).toEqual([
123
+ 'system to subsystem',
124
+ 'system-to-subsystem',
125
+ 'system subsystem',
126
+ ]);
127
+ expect(structureSpy.mock.calls[0][9]).toBe(true);
128
+ expect(structureSpy.mock.calls[0][10]).toBe(false);
129
+ });
130
+
131
+ it('should let structureFetchedQueries exclude oneHop leaves only for flat-only callers', async () => {
132
+ const result = await (ticketsDataProvider as any).structureFetchedQueries(
133
+ {
134
+ id: 'one-hop',
135
+ hasChildren: false,
136
+ isFolder: false,
137
+ queryType: 'oneHop',
138
+ wiql: "[Source].[System.WorkItemType] = 'Requirement'",
139
+ },
140
+ false,
141
+ null,
142
+ ['requirement'],
143
+ [],
144
+ undefined,
145
+ undefined,
146
+ false,
147
+ [],
148
+ true,
149
+ false,
150
+ );
151
+
152
+ expect(result).toEqual({ tree1: null, tree2: null });
153
+ });
154
+ });
155
+
156
+ describe('fetchCustomerRequirementQueries', () => {
157
+ it('should return flat, tree, and oneHop query nodes without WIQL type filtering', async () => {
158
+ const root = {
159
+ id: 'root',
160
+ name: 'SYSRS',
161
+ isFolder: true,
162
+ hasChildren: true,
163
+ children: [
164
+ {
165
+ id: 'flat-query',
166
+ name: 'Flat Customer Pull',
167
+ isFolder: false,
168
+ hasChildren: false,
169
+ queryType: 'flat',
170
+ wiql: "SELECT * FROM WorkItems WHERE [System.WorkItemType] = 'Task'",
171
+ columns: [],
172
+ _links: { wiql: { href: 'flat-url' } },
173
+ },
174
+ {
175
+ id: 'tree-query',
176
+ name: 'Tree Customer Pull',
177
+ isFolder: false,
178
+ hasChildren: false,
179
+ queryType: 'tree',
180
+ wiql: "SELECT * FROM WorkItemLinks WHERE [System.Links.LinkType] = 'System.LinkTypes.Hierarchy-Forward'",
181
+ columns: [],
182
+ _links: { wiql: { href: 'tree-url' } },
183
+ },
184
+ {
185
+ id: 'onehop-query',
186
+ name: 'OneHop Customer Pull',
187
+ isFolder: false,
188
+ hasChildren: false,
189
+ queryType: 'oneHop',
190
+ wiql: "SELECT * FROM WorkItemLinks WHERE [System.Links.LinkType] = 'System.LinkTypes.Related'",
191
+ columns: [],
192
+ _links: { wiql: { href: 'onehop-url' } },
193
+ },
194
+ {
195
+ id: 'unsupported-query',
196
+ name: 'Unsupported',
197
+ isFolder: false,
198
+ hasChildren: false,
199
+ queryType: 'invalid',
200
+ _links: { wiql: { href: 'unsupported-url' } },
201
+ },
202
+ ],
203
+ };
204
+
205
+ const result = await (ticketsDataProvider as any).fetchCustomerRequirementQueries(root, []);
206
+
207
+ expect(result.systemRequirementsQueryTree.children.map((child: any) => child.queryType)).toEqual([
208
+ 'flat',
209
+ 'tree',
210
+ 'oneHop',
211
+ ]);
212
+ expect(result.systemRequirementsQueryTree.children.map((child: any) => child.wiql.href)).toEqual([
213
+ 'flat-url',
214
+ 'tree-url',
215
+ 'onehop-url',
216
+ ]);
217
+ });
218
+
219
+ it('should include system-to-subsystem queries because customer discovery has no folder exclusions', async () => {
220
+ const root = {
221
+ id: 'root',
222
+ name: 'SYSRS',
223
+ isFolder: true,
224
+ hasChildren: true,
225
+ children: [
226
+ {
227
+ id: 'excluded-folder',
228
+ name: 'System To Subsystem',
229
+ isFolder: true,
230
+ hasChildren: true,
231
+ children: [
232
+ {
233
+ id: 'system-to-subsystem-query',
234
+ name: 'System To Subsystem Query',
235
+ isFolder: false,
236
+ hasChildren: false,
237
+ queryType: 'tree',
238
+ _links: { wiql: { href: 'system-to-subsystem-url' } },
239
+ },
240
+ ],
241
+ },
242
+ {
243
+ id: 'included-query',
244
+ name: 'Included Query',
245
+ isFolder: false,
246
+ hasChildren: false,
247
+ queryType: 'oneHop',
248
+ _links: { wiql: { href: 'included-url' } },
249
+ },
250
+ ],
251
+ };
252
+
253
+ const result = await (ticketsDataProvider as any).fetchCustomerRequirementQueries(root);
254
+
255
+ expect(result.systemRequirementsQueryTree.children).toHaveLength(2);
256
+ expect(result.systemRequirementsQueryTree.children[0].children[0]).toEqual(
257
+ expect.objectContaining({ id: 'system-to-subsystem-query', queryType: 'tree' }),
258
+ );
259
+ expect(result.systemRequirementsQueryTree.children[1]).toEqual(
260
+ expect.objectContaining({ id: 'included-query', queryType: 'oneHop' }),
261
+ );
262
+ });
263
+ });
264
+
96
265
  describe('fetchSysRsQueries', () => {
97
- it('should use sysrs root, exclude trace folders from system requirements and resolve both trace directions', async () => {
266
+ it('should scan the dedicated sysrs folder for customer queries regardless of query type', async () => {
98
267
  const rootQueries = { id: 'root' };
99
268
  const sysRsRoot = { id: 'sysrs-root', name: 'SYSRS' };
100
- const subsystemToSystemFolder = { id: 'fwd-folder', name: 'System To Customer' };
101
269
  const systemToSubsystemFolder = { id: 'rev-folder', name: 'System To Subsystem' };
102
270
 
103
271
  const getDocTypeRootSpy = jest
@@ -106,45 +274,49 @@ describe('TicketsDataProvider', () => {
106
274
  const fetchSystemRequirementQueriesSpy = jest
107
275
  .spyOn(ticketsDataProvider as any, 'fetchSystemRequirementQueries')
108
276
  .mockResolvedValue({ systemRequirementsQueryTree: { id: 'system-tree' } });
277
+ const fetchCustomerRequirementQueriesSpy = jest
278
+ .spyOn(ticketsDataProvider as any, 'fetchCustomerRequirementQueries')
279
+ .mockResolvedValue({ systemRequirementsQueryTree: { id: 'customer-tree' } });
109
280
  const findChildFolderByPossibleNamesSpy = jest
110
281
  .spyOn(ticketsDataProvider as any, 'findChildFolderByPossibleNames')
111
- .mockResolvedValueOnce(subsystemToSystemFolder)
112
282
  .mockResolvedValueOnce(systemToSubsystemFolder);
113
283
  const fetchRequirementsTraceQueriesForFolderSpy = jest
114
284
  .spyOn(ticketsDataProvider as any, 'fetchRequirementsTraceQueriesForFolder')
115
- .mockResolvedValueOnce({ id: 'fwd-trace-tree' })
116
285
  .mockResolvedValueOnce({ id: 'rev-trace-tree' });
117
286
 
118
287
  const result = await (ticketsDataProvider as any).fetchSysRsQueries(rootQueries);
119
288
 
120
289
  expect(getDocTypeRootSpy).toHaveBeenCalledWith(rootQueries, 'sysrs');
290
+ // fetchSystemRequirementQueries only excludes the sub-system trace folder;
291
+ // the legacy System-to-Customer exclusion list is gone.
121
292
  expect(fetchSystemRequirementQueriesSpy).toHaveBeenCalledWith(sysRsRoot, [
122
- 'System To Customer',
123
- 'System To Subsystem',
293
+ 'system to subsystem',
294
+ 'system-to-subsystem',
295
+ 'system subsystem',
124
296
  ]);
125
297
 
126
- expect(findChildFolderByPossibleNamesSpy).toHaveBeenNthCalledWith(
127
- 1,
128
- sysRsRoot,
129
- expect.arrayContaining(['system to customer', 'subsystem to system', 'customer to system']),
130
- );
131
- expect(findChildFolderByPossibleNamesSpy).toHaveBeenNthCalledWith(
132
- 2,
298
+ // Only one folder lookup remains (sub-system trace); no System-to-Customer lookup.
299
+ expect(findChildFolderByPossibleNamesSpy).toHaveBeenCalledTimes(1);
300
+ expect(findChildFolderByPossibleNamesSpy).toHaveBeenCalledWith(
133
301
  sysRsRoot,
134
302
  expect.arrayContaining(['system to subsystem', 'system-to-subsystem']),
135
303
  );
136
304
 
137
- expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenNthCalledWith(1, subsystemToSystemFolder);
138
- expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenNthCalledWith(2, systemToSubsystemFolder);
305
+ expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenCalledTimes(1);
306
+ expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenCalledWith(systemToSubsystemFolder);
307
+
308
+ // Customer picker scans the whole sysRsRoot with no query/folder exclusions.
309
+ expect(fetchCustomerRequirementQueriesSpy).toHaveBeenCalledWith(sysRsRoot);
139
310
 
140
311
  expect(result).toEqual({
141
312
  systemRequirementsQueries: { systemRequirementsQueryTree: { id: 'system-tree' } },
142
- subsystemToSystemRequirementsQueries: { id: 'fwd-trace-tree' },
313
+ customerRequirementsQueries: { systemRequirementsQueryTree: { id: 'customer-tree' } },
314
+ subsystemToSystemRequirementsQueries: null,
143
315
  systemToSubsystemRequirementsQueries: { id: 'rev-trace-tree' },
144
316
  });
145
317
  });
146
318
 
147
- it('should fall back to root queries and return null trace trees when trace folders are missing', async () => {
319
+ it('should return null customer/trace trees when the dedicated sysrs folder is missing (no fallback scan)', async () => {
148
320
  const rootQueries = { id: 'root' };
149
321
 
150
322
  jest
@@ -153,25 +325,29 @@ describe('TicketsDataProvider', () => {
153
325
  const fetchSystemRequirementQueriesSpy = jest
154
326
  .spyOn(ticketsDataProvider as any, 'fetchSystemRequirementQueries')
155
327
  .mockResolvedValue({ systemRequirementsQueryTree: { id: 'system-tree' } });
156
- jest
157
- .spyOn(ticketsDataProvider as any, 'findChildFolderByPossibleNames')
158
- .mockResolvedValueOnce(null)
159
- .mockResolvedValueOnce(null);
328
+ const fetchCustomerRequirementQueriesSpy = jest
329
+ .spyOn(ticketsDataProvider as any, 'fetchCustomerRequirementQueries')
330
+ .mockResolvedValue({ systemRequirementsQueryTree: { id: 'should-not-be-used' } });
331
+ jest.spyOn(ticketsDataProvider as any, 'findChildFolderByPossibleNames').mockResolvedValueOnce(null);
160
332
  const fetchRequirementsTraceQueriesForFolderSpy = jest
161
333
  .spyOn(ticketsDataProvider as any, 'fetchRequirementsTraceQueriesForFolder')
162
- .mockResolvedValueOnce(null)
163
334
  .mockResolvedValueOnce(null);
164
335
 
165
336
  const result = await (ticketsDataProvider as any).fetchSysRsQueries(rootQueries);
166
337
 
167
338
  expect(fetchSystemRequirementQueriesSpy).toHaveBeenCalledWith(rootQueries, [
168
- 'System To Customer',
169
- 'System To Subsystem',
339
+ 'system to subsystem',
340
+ 'system-to-subsystem',
341
+ 'system subsystem',
170
342
  ]);
171
- expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenNthCalledWith(1, null);
172
- expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenNthCalledWith(2, null);
343
+ expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenCalledTimes(1);
344
+ expect(fetchRequirementsTraceQueriesForFolderSpy).toHaveBeenCalledWith(null);
345
+ // No fallback scan when the dedicated sysrs folder is absent — otherwise
346
+ // unrelated flat queries from the Shared Queries root would leak in.
347
+ expect(fetchCustomerRequirementQueriesSpy).not.toHaveBeenCalled();
173
348
  expect(result).toEqual({
174
349
  systemRequirementsQueries: { systemRequirementsQueryTree: { id: 'system-tree' } },
350
+ customerRequirementsQueries: null,
175
351
  subsystemToSystemRequirementsQueries: null,
176
352
  systemToSubsystemRequirementsQueries: null,
177
353
  });
@@ -1353,6 +1529,46 @@ describe('TicketsDataProvider', () => {
1353
1529
  expect(result).toBeDefined();
1354
1530
  });
1355
1531
 
1532
+ // Regression: WI_DEFAULT_FIELDS must include System.WorkItemType so that
1533
+ // downstream Requirement-type filters (e.g. the customer-coverage table)
1534
+ // do not drop every row because workItemType resolves to empty string.
1535
+ it('should request System.WorkItemType in default fields for flat queries and expose workItemType on parsed items', async () => {
1536
+ const mockWiqlHref = 'https://example.com/wiql';
1537
+ const mockQueryResult = {
1538
+ queryType: 'flat',
1539
+ workItems: [{ id: 42, url: 'https://example.com/wi/42' }],
1540
+ };
1541
+ const mockWiResponse = {
1542
+ fields: {
1543
+ 'System.Title': 'Customer Requirement',
1544
+ 'System.WorkItemType': 'Requirement',
1545
+ 'System.Description': 'Desc',
1546
+ },
1547
+ _links: { html: { href: 'https://example.com/wi/42' } },
1548
+ };
1549
+
1550
+ (TFSServices.getItemContent as jest.Mock)
1551
+ .mockResolvedValueOnce(mockQueryResult)
1552
+ .mockResolvedValueOnce(mockWiResponse);
1553
+
1554
+ const result: any = await ticketsDataProvider.GetQueryResultsFromWiql(
1555
+ mockWiqlHref,
1556
+ false,
1557
+ new Map<number, Set<any>>(),
1558
+ );
1559
+
1560
+ // The per-item fetch URL must request System.WorkItemType.
1561
+ const perItemCall = (TFSServices.getItemContent as jest.Mock).mock.calls.find(
1562
+ (call: any[]) => typeof call[0] === 'string' && call[0].startsWith('https://example.com/wi/42'),
1563
+ );
1564
+ expect(perItemCall).toBeDefined();
1565
+ expect(perItemCall[0]).toContain('System.WorkItemType');
1566
+
1567
+ // The parsed item must expose workItemType so downstream filters work.
1568
+ expect(Array.isArray(result)).toBe(true);
1569
+ expect(result[0]).toMatchObject({ id: 42, workItemType: 'Requirement' });
1570
+ });
1571
+
1356
1572
  it('should return undefined when queryType is not supported', async () => {
1357
1573
  (TFSServices.getItemContent as jest.Mock).mockResolvedValueOnce({ queryType: 'Unknown' });
1358
1574
  const res = await ticketsDataProvider.GetQueryResultsFromWiql(