@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.
- package/bin/modules/TicketsDataProvider.d.ts +9 -1
- package/bin/modules/TicketsDataProvider.js +93 -45
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.test.js +196 -21
- package/bin/tests/modules/ticketsDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +138 -43
- package/src/tests/modules/ticketsDataProvider.test.ts +242 -26
|
@@ -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
|
|
714
|
-
'System To Customer',
|
|
715
|
-
'System To Subsystem',
|
|
716
|
-
]);
|
|
790
|
+
const systemToSubsystemFolderNames = ['system to subsystem', 'system-to-subsystem', 'system subsystem'];
|
|
717
791
|
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
|
732
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
//
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
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
|
-
}
|
|
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 (
|
|
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
|
|
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
|
-
'
|
|
123
|
-
'
|
|
293
|
+
'system to subsystem',
|
|
294
|
+
'system-to-subsystem',
|
|
295
|
+
'system subsystem',
|
|
124
296
|
]);
|
|
125
297
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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).
|
|
138
|
-
expect(fetchRequirementsTraceQueriesForFolderSpy).
|
|
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
|
-
|
|
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
|
|
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, '
|
|
158
|
-
.
|
|
159
|
-
|
|
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
|
-
'
|
|
169
|
-
'
|
|
339
|
+
'system to subsystem',
|
|
340
|
+
'system-to-subsystem',
|
|
341
|
+
'system subsystem',
|
|
170
342
|
]);
|
|
171
|
-
expect(fetchRequirementsTraceQueriesForFolderSpy).
|
|
172
|
-
expect(fetchRequirementsTraceQueriesForFolderSpy).
|
|
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(
|