@elisra-devops/docgen-data-provider 1.107.0 → 1.108.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 +2 -0
- package/bin/modules/TicketsDataProvider.js +59 -9
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/tests/modules/ticketsDataProvider.historical.test.js +267 -4
- package/bin/tests/modules/ticketsDataProvider.historical.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/TicketsDataProvider.ts +81 -19
- package/src/tests/modules/ticketsDataProvider.historical.test.ts +341 -8
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TFSServices } from '../../helpers/tfs';
|
|
2
2
|
import TicketsDataProvider from '../../modules/TicketsDataProvider';
|
|
3
|
+
import logger from '../../utils/logger';
|
|
3
4
|
|
|
4
5
|
jest.mock('../../helpers/tfs');
|
|
5
6
|
jest.mock('../../utils/logger', () => ({
|
|
@@ -47,7 +48,9 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
47
48
|
const result = await provider.GetHistoricalQueries(project);
|
|
48
49
|
|
|
49
50
|
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
50
|
-
expect.stringContaining(
|
|
51
|
+
expect.stringContaining(
|
|
52
|
+
`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`,
|
|
53
|
+
),
|
|
51
54
|
token,
|
|
52
55
|
);
|
|
53
56
|
expect(result).toEqual([
|
|
@@ -110,11 +113,15 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
110
113
|
|
|
111
114
|
expect(result).toEqual([{ id: 'q-51', queryName: 'V5 Query', path: 'Shared Queries' }]);
|
|
112
115
|
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
113
|
-
expect.stringContaining(
|
|
116
|
+
expect.stringContaining(
|
|
117
|
+
`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`,
|
|
118
|
+
),
|
|
114
119
|
token,
|
|
115
120
|
);
|
|
116
121
|
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
117
|
-
expect.stringContaining(
|
|
122
|
+
expect.stringContaining(
|
|
123
|
+
`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=5.1`,
|
|
124
|
+
),
|
|
118
125
|
token,
|
|
119
126
|
);
|
|
120
127
|
});
|
|
@@ -130,7 +137,9 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
130
137
|
await provider.GetHistoricalQueries(project, 'Shared Queries');
|
|
131
138
|
|
|
132
139
|
expect(TFSServices.getItemContent).toHaveBeenCalledWith(
|
|
133
|
-
expect.stringContaining(
|
|
140
|
+
expect.stringContaining(
|
|
141
|
+
`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`,
|
|
142
|
+
),
|
|
134
143
|
token,
|
|
135
144
|
);
|
|
136
145
|
});
|
|
@@ -146,7 +155,11 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
146
155
|
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
147
156
|
return { workItems: [{ id: 101 }, { id: 102 }] };
|
|
148
157
|
}
|
|
149
|
-
if (
|
|
158
|
+
if (
|
|
159
|
+
url.includes('/_apis/wit/workitemsbatch') &&
|
|
160
|
+
url.includes('api-version=7.1') &&
|
|
161
|
+
method === 'post'
|
|
162
|
+
) {
|
|
150
163
|
expect(data.asOf).toBe(asOfIso);
|
|
151
164
|
return {
|
|
152
165
|
value: [
|
|
@@ -222,7 +235,11 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
222
235
|
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
223
236
|
return { workItems: allIds.map((id) => ({ id })) };
|
|
224
237
|
}
|
|
225
|
-
if (
|
|
238
|
+
if (
|
|
239
|
+
url.includes('/_apis/wit/workitemsbatch') &&
|
|
240
|
+
url.includes('api-version=5.1') &&
|
|
241
|
+
method === 'post'
|
|
242
|
+
) {
|
|
226
243
|
return {
|
|
227
244
|
value: (Array.isArray(data?.ids) ? data.ids : []).map((id: number) => ({
|
|
228
245
|
id,
|
|
@@ -268,7 +285,11 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
268
285
|
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
269
286
|
return { workItems: [{ id: 101 }, { id: 102 }] };
|
|
270
287
|
}
|
|
271
|
-
if (
|
|
288
|
+
if (
|
|
289
|
+
url.includes('/_apis/wit/workitemsbatch') &&
|
|
290
|
+
url.includes('api-version=7.1') &&
|
|
291
|
+
method === 'post'
|
|
292
|
+
) {
|
|
272
293
|
throw {
|
|
273
294
|
response: {
|
|
274
295
|
status: 500,
|
|
@@ -322,6 +343,146 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
322
343
|
expect(result.rows.map((row: any) => row.id)).toEqual([101, 102]);
|
|
323
344
|
});
|
|
324
345
|
|
|
346
|
+
it('GetHistoricalQueryResults skips work items that do not exist at as-of time and logs warning', async () => {
|
|
347
|
+
const asOfIso = '2026-01-01T10:00:00.000Z';
|
|
348
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
349
|
+
async (url: string, _pat: string, method?: string, data?: any) => {
|
|
350
|
+
if (url.includes('/_apis/wit/queries/q-missing-asof') && url.includes('api-version=7.1')) {
|
|
351
|
+
return { name: 'Missing AsOf Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
|
|
352
|
+
}
|
|
353
|
+
if (url.includes('/_apis/wit/wiql?') && url.includes('api-version=7.1') && method === 'post') {
|
|
354
|
+
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
355
|
+
return { workItems: [{ id: 101 }, { id: 102 }] };
|
|
356
|
+
}
|
|
357
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
358
|
+
throw {
|
|
359
|
+
response: {
|
|
360
|
+
status: 500,
|
|
361
|
+
data: { message: 'workitemsbatch failed' },
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
if (url.includes('/_apis/wit/workitems/101')) {
|
|
366
|
+
return {
|
|
367
|
+
id: 101,
|
|
368
|
+
rev: 3,
|
|
369
|
+
fields: {
|
|
370
|
+
'System.WorkItemType': 'Requirement',
|
|
371
|
+
'System.Title': 'Req 101',
|
|
372
|
+
'System.State': 'Active',
|
|
373
|
+
'System.AreaPath': 'Proj\\Area',
|
|
374
|
+
'System.IterationPath': 'Proj\\Iter',
|
|
375
|
+
'System.ChangedDate': '2025-12-30T10:00:00Z',
|
|
376
|
+
},
|
|
377
|
+
relations: [],
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
if (url.includes('/_apis/wit/workitems/102')) {
|
|
381
|
+
throw {
|
|
382
|
+
response: {
|
|
383
|
+
status: 404,
|
|
384
|
+
data: {
|
|
385
|
+
message:
|
|
386
|
+
'The work item 102 does not exist at time 12/31/2025 11:56:00 PM. It might have been deleted.',
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
throw new Error(`unexpected URL: ${url}`);
|
|
392
|
+
},
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
const result = await provider.GetHistoricalQueryResults('q-missing-asof', project, asOfIso);
|
|
396
|
+
|
|
397
|
+
expect(result.total).toBe(1);
|
|
398
|
+
expect(result.skippedWorkItemsCount).toBe(1);
|
|
399
|
+
expect(result.rows.map((row: any) => row.id)).toEqual([101]);
|
|
400
|
+
expect(
|
|
401
|
+
(logger.warn as jest.Mock).mock.calls.some((call) =>
|
|
402
|
+
String(call[0]).includes('skipping work item 102'),
|
|
403
|
+
),
|
|
404
|
+
).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
it('GetHistoricalQueryResults returns empty result when all work items are missing at as-of time', async () => {
|
|
408
|
+
const asOfIso = '2026-01-01T10:00:00.000Z';
|
|
409
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
410
|
+
async (url: string, _pat: string, method?: string, data?: any) => {
|
|
411
|
+
if (url.includes('/_apis/wit/queries/q-all-missing-asof') && url.includes('api-version=7.1')) {
|
|
412
|
+
return { name: 'All Missing AsOf Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
|
|
413
|
+
}
|
|
414
|
+
if (url.includes('/_apis/wit/wiql?') && method === 'post') {
|
|
415
|
+
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
416
|
+
return { workItems: [{ id: 201 }, { id: 202 }] };
|
|
417
|
+
}
|
|
418
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
419
|
+
throw {
|
|
420
|
+
response: {
|
|
421
|
+
status: 500,
|
|
422
|
+
data: { message: 'workitemsbatch failed' },
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (url.includes('/_apis/wit/workitems/201') || url.includes('/_apis/wit/workitems/202')) {
|
|
427
|
+
const id = url.includes('/201') ? 201 : 202;
|
|
428
|
+
throw {
|
|
429
|
+
response: {
|
|
430
|
+
status: 404,
|
|
431
|
+
data: {
|
|
432
|
+
message: `The work item ${id} does not exist at time 12/31/2025 11:56:00 PM.`,
|
|
433
|
+
},
|
|
434
|
+
},
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`unexpected URL: ${url}`);
|
|
438
|
+
},
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const result = await provider.GetHistoricalQueryResults('q-all-missing-asof', project, asOfIso);
|
|
442
|
+
|
|
443
|
+
expect(result.total).toBe(0);
|
|
444
|
+
expect(result.rows).toEqual([]);
|
|
445
|
+
expect(result.skippedWorkItemsCount).toBe(2);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('GetHistoricalQueryResults still throws for non-missing historical errors', async () => {
|
|
449
|
+
const asOfIso = '2026-01-01T10:00:00.000Z';
|
|
450
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
451
|
+
async (url: string, _pat: string, method?: string, data?: any) => {
|
|
452
|
+
if (url.includes('/_apis/wit/queries/q-auth-error') && url.includes('api-version=7.1')) {
|
|
453
|
+
return { name: 'Auth Error Q', wiql: 'SELECT [System.Id] FROM WorkItems' };
|
|
454
|
+
}
|
|
455
|
+
if (url.includes('/_apis/wit/wiql?') && method === 'post') {
|
|
456
|
+
expect(String(data?.query || '')).toContain(`ASOF '${asOfIso}'`);
|
|
457
|
+
return { workItems: [{ id: 901 }] };
|
|
458
|
+
}
|
|
459
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
460
|
+
throw {
|
|
461
|
+
response: {
|
|
462
|
+
status: 500,
|
|
463
|
+
data: { message: 'workitemsbatch failed' },
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (url.includes('/_apis/wit/workitems/901')) {
|
|
468
|
+
throw {
|
|
469
|
+
response: {
|
|
470
|
+
status: 401,
|
|
471
|
+
data: { message: 'Unauthorized' },
|
|
472
|
+
},
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
throw new Error(`unexpected URL: ${url}`);
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
await expect(provider.GetHistoricalQueryResults('q-auth-error', project, asOfIso)).rejects.toEqual(
|
|
480
|
+
expect.objectContaining({
|
|
481
|
+
response: expect.objectContaining({ status: 401 }),
|
|
482
|
+
}),
|
|
483
|
+
);
|
|
484
|
+
});
|
|
485
|
+
|
|
325
486
|
it('GetHistoricalQueryResults falls back to WIQL-by-id when inline WIQL fails', async () => {
|
|
326
487
|
const asOfIso = '2026-01-01T10:00:00.000Z';
|
|
327
488
|
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
@@ -342,7 +503,11 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
342
503
|
expect(url).toContain(`asOf=${encodeURIComponent(asOfIso)}`);
|
|
343
504
|
return { workItems: [{ id: 3001 }] };
|
|
344
505
|
}
|
|
345
|
-
if (
|
|
506
|
+
if (
|
|
507
|
+
url.includes('/_apis/wit/workitemsbatch') &&
|
|
508
|
+
url.includes('api-version=7.1') &&
|
|
509
|
+
method === 'post'
|
|
510
|
+
) {
|
|
346
511
|
return {
|
|
347
512
|
value: [
|
|
348
513
|
{
|
|
@@ -540,4 +705,172 @@ describe('TicketsDataProvider historical queries', () => {
|
|
|
540
705
|
updatedCount: 2,
|
|
541
706
|
});
|
|
542
707
|
});
|
|
708
|
+
|
|
709
|
+
it('CompareHistoricalQueryResults supports missing work items on one side and reports Added/Deleted', async () => {
|
|
710
|
+
const baselineIso = '2025-12-20T00:00:00.000Z';
|
|
711
|
+
const compareIso = '2025-12-30T00:00:00.000Z';
|
|
712
|
+
|
|
713
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
714
|
+
async (url: string, _pat: string, method?: string, data?: any) => {
|
|
715
|
+
if (url.includes('/_apis/wit/queries/q-compare-missing-side') && url.includes('api-version=7.1')) {
|
|
716
|
+
return { name: 'Compare Missing Side', wiql: 'SELECT [System.Id] FROM WorkItems' };
|
|
717
|
+
}
|
|
718
|
+
if (url.includes('/_apis/wit/wiql?') && method === 'post') {
|
|
719
|
+
const query = String(data?.query || '');
|
|
720
|
+
if (query.includes(baselineIso)) {
|
|
721
|
+
return { workItems: [{ id: 1001 }, { id: 1002 }, { id: 1003 }] };
|
|
722
|
+
}
|
|
723
|
+
if (query.includes(compareIso)) {
|
|
724
|
+
return { workItems: [{ id: 1001 }, { id: 1002 }, { id: 1003 }] };
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
728
|
+
throw {
|
|
729
|
+
response: {
|
|
730
|
+
status: 500,
|
|
731
|
+
data: { message: 'workitemsbatch failed' },
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
if (url.includes('/_apis/wit/workitems/1001')) {
|
|
736
|
+
return {
|
|
737
|
+
id: 1001,
|
|
738
|
+
rev: 1,
|
|
739
|
+
fields: {
|
|
740
|
+
'System.WorkItemType': 'Requirement',
|
|
741
|
+
'System.Title': 'Stable Item',
|
|
742
|
+
'System.State': 'Active',
|
|
743
|
+
'System.ChangedDate': compareIso,
|
|
744
|
+
},
|
|
745
|
+
relations: [],
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
if (
|
|
749
|
+
url.includes(`asOf=${encodeURIComponent(baselineIso)}`) &&
|
|
750
|
+
url.includes('/_apis/wit/workitems/1002')
|
|
751
|
+
) {
|
|
752
|
+
throw {
|
|
753
|
+
response: {
|
|
754
|
+
status: 404,
|
|
755
|
+
data: { message: 'The work item 1002 does not exist at time 12/20/2025 12:00:00 AM.' },
|
|
756
|
+
},
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
if (
|
|
760
|
+
url.includes(`asOf=${encodeURIComponent(compareIso)}`) &&
|
|
761
|
+
url.includes('/_apis/wit/workitems/1002')
|
|
762
|
+
) {
|
|
763
|
+
return {
|
|
764
|
+
id: 1002,
|
|
765
|
+
rev: 4,
|
|
766
|
+
fields: {
|
|
767
|
+
'System.WorkItemType': 'Bug',
|
|
768
|
+
'System.Title': 'Added Later',
|
|
769
|
+
'System.State': 'New',
|
|
770
|
+
'System.ChangedDate': compareIso,
|
|
771
|
+
},
|
|
772
|
+
relations: [],
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
if (
|
|
776
|
+
url.includes(`asOf=${encodeURIComponent(baselineIso)}`) &&
|
|
777
|
+
url.includes('/_apis/wit/workitems/1003')
|
|
778
|
+
) {
|
|
779
|
+
return {
|
|
780
|
+
id: 1003,
|
|
781
|
+
rev: 2,
|
|
782
|
+
fields: {
|
|
783
|
+
'System.WorkItemType': 'Bug',
|
|
784
|
+
'System.Title': 'Deleted Later',
|
|
785
|
+
'System.State': 'Active',
|
|
786
|
+
'System.ChangedDate': baselineIso,
|
|
787
|
+
},
|
|
788
|
+
relations: [],
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
if (
|
|
792
|
+
url.includes(`asOf=${encodeURIComponent(compareIso)}`) &&
|
|
793
|
+
url.includes('/_apis/wit/workitems/1003')
|
|
794
|
+
) {
|
|
795
|
+
throw {
|
|
796
|
+
response: {
|
|
797
|
+
status: 404,
|
|
798
|
+
data: { message: 'The work item 1003 does not exist at time 12/30/2025 12:00:00 AM.' },
|
|
799
|
+
},
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
throw new Error(`unexpected URL: ${url}`);
|
|
803
|
+
},
|
|
804
|
+
);
|
|
805
|
+
|
|
806
|
+
const result = await provider.CompareHistoricalQueryResults(
|
|
807
|
+
'q-compare-missing-side',
|
|
808
|
+
project,
|
|
809
|
+
baselineIso,
|
|
810
|
+
compareIso,
|
|
811
|
+
);
|
|
812
|
+
|
|
813
|
+
const byId = new Map<number, any>(result.rows.map((row: any) => [row.id, row]));
|
|
814
|
+
expect(byId.get(1001)?.compareStatus).toBe('No changes');
|
|
815
|
+
expect(byId.get(1002)?.compareStatus).toBe('Added');
|
|
816
|
+
expect(byId.get(1003)?.compareStatus).toBe('Deleted');
|
|
817
|
+
expect(result.skippedWorkItems).toEqual(
|
|
818
|
+
expect.objectContaining({ baselineCount: 1, compareToCount: 1, totalDistinct: 2 }),
|
|
819
|
+
);
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
it('CompareHistoricalQueryResults excludes work items missing at both dates and can return empty set', async () => {
|
|
823
|
+
const baselineIso = '2025-01-01T00:00:00.000Z';
|
|
824
|
+
const compareIso = '2025-01-02T00:00:00.000Z';
|
|
825
|
+
|
|
826
|
+
(TFSServices.getItemContent as jest.Mock).mockImplementation(
|
|
827
|
+
async (url: string, _pat: string, method?: string, data?: any) => {
|
|
828
|
+
if (url.includes('/_apis/wit/queries/q-compare-all-missing') && url.includes('api-version=7.1')) {
|
|
829
|
+
return { name: 'Compare All Missing', wiql: 'SELECT [System.Id] FROM WorkItems' };
|
|
830
|
+
}
|
|
831
|
+
if (url.includes('/_apis/wit/wiql?') && method === 'post') {
|
|
832
|
+
const query = String(data?.query || '');
|
|
833
|
+
if (query.includes(baselineIso) || query.includes(compareIso)) {
|
|
834
|
+
return { workItems: [{ id: 777 }] };
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
if (url.includes('/_apis/wit/workitemsbatch') && method === 'post') {
|
|
838
|
+
throw {
|
|
839
|
+
response: {
|
|
840
|
+
status: 500,
|
|
841
|
+
data: { message: 'workitemsbatch failed' },
|
|
842
|
+
},
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
if (url.includes('/_apis/wit/workitems/777')) {
|
|
846
|
+
throw {
|
|
847
|
+
response: {
|
|
848
|
+
status: 404,
|
|
849
|
+
data: { message: 'The work item 777 does not exist at time 01/01/2025 12:00:00 AM.' },
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
throw new Error(`unexpected URL: ${url}`);
|
|
854
|
+
},
|
|
855
|
+
);
|
|
856
|
+
|
|
857
|
+
const result = await provider.CompareHistoricalQueryResults(
|
|
858
|
+
'q-compare-all-missing',
|
|
859
|
+
project,
|
|
860
|
+
baselineIso,
|
|
861
|
+
compareIso,
|
|
862
|
+
);
|
|
863
|
+
|
|
864
|
+
expect(result.rows).toEqual([]);
|
|
865
|
+
expect(result.summary).toEqual({
|
|
866
|
+
addedCount: 0,
|
|
867
|
+
deletedCount: 0,
|
|
868
|
+
changedCount: 0,
|
|
869
|
+
noChangeCount: 0,
|
|
870
|
+
updatedCount: 0,
|
|
871
|
+
});
|
|
872
|
+
expect(result.skippedWorkItems).toEqual(
|
|
873
|
+
expect.objectContaining({ baselineCount: 1, compareToCount: 1, totalDistinct: 1 }),
|
|
874
|
+
);
|
|
875
|
+
});
|
|
543
876
|
});
|