@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.
@@ -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(`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`),
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(`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`),
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(`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=5.1`),
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(`/${project}/_apis/wit/queries/Shared%20Queries?$depth=2&$expand=all&api-version=7.1`),
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 (url.includes('/_apis/wit/workitemsbatch') && url.includes('api-version=7.1') && method === 'post') {
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 (url.includes('/_apis/wit/workitemsbatch') && url.includes('api-version=5.1') && method === 'post') {
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 (url.includes('/_apis/wit/workitemsbatch') && url.includes('api-version=7.1') && method === 'post') {
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 (url.includes('/_apis/wit/workitemsbatch') && url.includes('api-version=7.1') && method === 'post') {
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
  });