@elisra-devops/docgen-data-provider 1.100.0 → 1.101.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.100.0",
3
+ "version": "1.101.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3361,12 +3361,6 @@ export default class ResultDataProvider {
3361
3361
  const id = Number(workItemId || 0);
3362
3362
  const parsedAsOf = new Date(String(asOfTimestamp || '').trim());
3363
3363
  if (!Number.isFinite(id) || id <= 0 || Number.isNaN(parsedAsOf.getTime())) return null;
3364
-
3365
- logger.debug(
3366
- `[RunlessResolver] Fetching work item ${id} by asOf (raw="${String(
3367
- asOfTimestamp || ''
3368
- )}", normalized="${parsedAsOf.toISOString()}", expandAll=${String(expandAll)})`
3369
- );
3370
3364
  const query: string[] = [`asOf=${encodeURIComponent(parsedAsOf.toISOString())}`];
3371
3365
  if (expandAll) {
3372
3366
  query.push(`$expand=all`);
@@ -3408,19 +3402,48 @@ export default class ResultDataProvider {
3408
3402
  return String(stepsXml || '').trim() !== '';
3409
3403
  }
3410
3404
 
3411
- private logRunlessSnapshotDecision(testCaseId: number, source: string, snapshot: any | null): void {
3412
- if (!snapshot) {
3413
- logger.debug(`[RunlessResolver] TC ${testCaseId}: source=${source}, snapshot=none`);
3414
- return;
3405
+ /**
3406
+ * Resolves the suite-aware revision for a test point.
3407
+ */
3408
+ private resolvePointRevision(testCase: any, point: any): number {
3409
+ return Number(
3410
+ this.resolveSuiteTestCaseRevision(testCase) ||
3411
+ this.resolveSuiteTestCaseRevision(point?.suiteTestCase) ||
3412
+ 0
3413
+ );
3414
+ }
3415
+
3416
+ /**
3417
+ * Builds ordered keys used to match a point with fetched iteration payloads.
3418
+ */
3419
+ private buildIterationLookupCandidates(input: {
3420
+ testCaseId: any;
3421
+ lastRunId?: any;
3422
+ lastResultId?: any;
3423
+ testPointId?: any;
3424
+ testCaseRevision?: any;
3425
+ }): string[] {
3426
+ const candidates: string[] = [];
3427
+ const addCandidate = (key: string) => {
3428
+ if (key && !candidates.includes(key)) {
3429
+ candidates.push(key);
3430
+ }
3431
+ };
3432
+
3433
+ addCandidate(this.buildIterationLookupKey(input));
3434
+
3435
+ const revision = Number(input?.testCaseRevision || 0);
3436
+ if (Number.isFinite(revision) && revision > 0) {
3437
+ addCandidate(
3438
+ this.buildIterationLookupKey({
3439
+ testCaseId: input?.testCaseId,
3440
+ testCaseRevision: revision,
3441
+ })
3442
+ );
3415
3443
  }
3416
3444
 
3417
- const stepsXml = this.extractStepsXmlFromFieldsMap(snapshot?.fields || {});
3418
- const stepsLength = String(stepsXml || '').trim().length;
3419
- logger.debug(
3420
- `[RunlessResolver] TC ${testCaseId}: source=${source}, rev=${String(
3421
- snapshot?.rev ?? ''
3422
- )}, hasSteps=${String(stepsLength > 0)}, stepsLength=${String(stepsLength)}`
3423
- );
3445
+ addCandidate(`${Number(input?.testCaseId || 0)}`);
3446
+ return candidates;
3424
3447
  }
3425
3448
 
3426
3449
  /**
@@ -3444,13 +3467,10 @@ export default class ResultDataProvider {
3444
3467
  pointAsOfTimestamp,
3445
3468
  expandAll
3446
3469
  );
3447
- this.logRunlessSnapshotDecision(testCaseId, 'asOf', asOfSnapshot);
3448
3470
  if (asOfSnapshot) {
3449
3471
  if (this.hasStepsInWorkItemSnapshot(asOfSnapshot)) return asOfSnapshot;
3450
3472
  bestSnapshotWithoutSteps = asOfSnapshot;
3451
3473
  }
3452
- } else {
3453
- logger.debug(`[RunlessResolver] TC ${testCaseId}: asOf timestamp is empty, skipping asOf fetch`);
3454
3474
  }
3455
3475
 
3456
3476
  const revisionSnapshot = await this.fetchWorkItemByRevision(
@@ -3459,20 +3479,17 @@ export default class ResultDataProvider {
3459
3479
  suiteTestCaseRevision,
3460
3480
  expandAll
3461
3481
  );
3462
- this.logRunlessSnapshotDecision(testCaseId, `revision:${String(suiteTestCaseRevision)}`, revisionSnapshot);
3463
3482
  if (revisionSnapshot) {
3464
3483
  if (this.hasStepsInWorkItemSnapshot(revisionSnapshot)) return revisionSnapshot;
3465
3484
  bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || revisionSnapshot;
3466
3485
  }
3467
3486
 
3468
- this.logRunlessSnapshotDecision(testCaseId, 'suiteSnapshot', fallbackSnapshot);
3469
3487
  if (fallbackSnapshot) {
3470
3488
  if (this.hasStepsInWorkItemSnapshot(fallbackSnapshot)) return fallbackSnapshot;
3471
3489
  bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || fallbackSnapshot;
3472
3490
  }
3473
3491
 
3474
3492
  const latestSnapshot = await this.fetchWorkItemLatest(projectName, testCaseId, expandAll);
3475
- this.logRunlessSnapshotDecision(testCaseId, 'latest', latestSnapshot);
3476
3493
  if (latestSnapshot) {
3477
3494
  if (this.hasStepsInWorkItemSnapshot(latestSnapshot)) return latestSnapshot;
3478
3495
  bestSnapshotWithoutSteps = bestSnapshotWithoutSteps || latestSnapshot;
@@ -3524,13 +3541,6 @@ export default class ResultDataProvider {
3524
3541
  const pointAsOfTimestamp = useRunlessAsOf
3525
3542
  ? String(point?.pointAsOfTimestamp || '').trim()
3526
3543
  : '';
3527
- logger.debug(
3528
- `[RunlessResolver] Start TC ${String(testCaseId)}: useRunlessAsOf=${String(
3529
- useRunlessAsOf
3530
- )}, pointAsOfTimestamp="${pointAsOfTimestamp}", suiteRevision=${String(
3531
- suiteTestCaseRevision
3532
- )}, pointOutcome="${String(point?.outcome || '')}"`
3533
- );
3534
3544
  const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
3535
3545
  suiteTestCaseItem,
3536
3546
  testCaseId,
@@ -4365,14 +4375,6 @@ export default class ResultDataProvider {
4365
4375
  };
4366
4376
  if (!Number.isFinite(pointTestCaseId) || pointTestCaseId <= 0) continue;
4367
4377
 
4368
- if (!testCaseById.has(pointTestCaseId) && isTestReporter) {
4369
- logger.debug(
4370
- `[RunlessResolver] Missing suite testCase payload for point testCaseId=${String(
4371
- pointTestCaseId
4372
- )}; using point fallback for alignment`
4373
- );
4374
- }
4375
-
4376
4378
  const testCaseWorkItemFields = Array.isArray(testCase?.workItem?.workItemFields)
4377
4379
  ? testCase.workItem.workItemFields
4378
4380
  : [];
@@ -4382,34 +4384,16 @@ export default class ResultDataProvider {
4382
4384
  continue;
4383
4385
  }
4384
4386
  }
4385
- const iterationKey = this.buildIterationLookupKey({
4387
+ const iterationLookupKeys = this.buildIterationLookupCandidates({
4386
4388
  testCaseId: testCase.workItem.id,
4387
4389
  lastRunId: point?.lastRunId,
4388
4390
  lastResultId: point?.lastResultId,
4389
4391
  testPointId: point?.testPointId,
4390
- testCaseRevision:
4391
- this.resolveSuiteTestCaseRevision(testCase) ||
4392
- this.resolveSuiteTestCaseRevision(point?.suiteTestCase),
4392
+ testCaseRevision: this.resolvePointRevision(testCase, point),
4393
4393
  });
4394
- const fallbackRevision = Number(
4395
- this.resolveSuiteTestCaseRevision(testCase) ||
4396
- this.resolveSuiteTestCaseRevision(point?.suiteTestCase) ||
4397
- 0
4398
- );
4399
- const fallbackRevisionKey =
4400
- Number.isFinite(fallbackRevision) && fallbackRevision > 0
4401
- ? this.buildIterationLookupKey({
4402
- testCaseId: testCase.workItem.id,
4403
- testCaseRevision: fallbackRevision,
4404
- })
4405
- : '';
4406
- const fallbackCaseOnlyKey = `${testCase.workItem.id}`;
4394
+ const fetchedTestCaseFromMap = iterationLookupKeys.find((key) => iterationsMap[key]) || '';
4407
4395
  const fetchedTestCase =
4408
- iterationsMap[iterationKey] ||
4409
- (fallbackRevisionKey && fallbackRevisionKey !== iterationKey
4410
- ? iterationsMap[fallbackRevisionKey]
4411
- : undefined) ||
4412
- (iterationKey !== fallbackCaseOnlyKey ? iterationsMap[fallbackCaseOnlyKey] : undefined) ||
4396
+ (fetchedTestCaseFromMap ? iterationsMap[fetchedTestCaseFromMap] : undefined) ||
4413
4397
  (includeNotRunTestCases ? testCase : undefined);
4414
4398
  // First check if fetchedTestCase exists
4415
4399
  if (!fetchedTestCase) continue;
@@ -4480,11 +4464,6 @@ export default class ResultDataProvider {
4480
4464
  if (resultObjectsToAdd.length > 0) {
4481
4465
  detailedResults.push(...resultObjectsToAdd);
4482
4466
  } else {
4483
- logger.debug(
4484
- `[RunlessResolver] No step rows generated for testCaseId=${String(
4485
- point?.testCaseId || ''
4486
- )}; falling back to test-level row`
4487
- );
4488
4467
  detailedResults.push(
4489
4468
  options.createResultObject({
4490
4469
  testItem,
@@ -4570,13 +4549,6 @@ export default class ResultDataProvider {
4570
4549
  testCaseRevision: iterationItem?.testCaseRevision,
4571
4550
  });
4572
4551
  map[key] = iterationItem;
4573
- if (isTestReporter && iterationItem?.iteration) {
4574
- logger.debug(
4575
- `[RunlessResolver] createIterationsMap: mapped runless testCaseId=${String(
4576
- iterationItem?.testCaseId
4577
- )} to key=${key}`
4578
- );
4579
- }
4580
4552
  } else if (iterationItem?.iteration && !isTestReporter) {
4581
4553
  const key = this.buildIterationLookupKey({
4582
4554
  testCaseId: iterationItem?.testCaseId,
@@ -4824,11 +4796,6 @@ export default class ResultDataProvider {
4824
4796
  resultData.stepsResultXml,
4825
4797
  sharedStepIdToRevisionLookupMap
4826
4798
  );
4827
- logger.debug(
4828
- `[RunlessResolver] TC ${String(point?.testCaseId || resultData?.testCase?.id || '')}: parseTestSteps xmlLength=${String(
4829
- String(resultData.stepsResultXml || '').length
4830
- )}, parsedSteps=${String(stepsList.length)}, actionResultsBeforeMap=${String(actionResults.length)}`
4831
- );
4832
4799
 
4833
4800
  sharedStepIdToRevisionLookupMap.clear();
4834
4801
 
@@ -4852,11 +4819,6 @@ export default class ResultDataProvider {
4852
4819
  iteration.actionResults = actionResults
4853
4820
  .filter((result: any) => result.stepPosition)
4854
4821
  .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
4855
- logger.debug(
4856
- `[RunlessResolver] TC ${String(point?.testCaseId || resultData?.testCase?.id || '')}: mappedActionResults=${String(
4857
- iteration.actionResults?.length || 0
4858
- )} (from existing iteration actionResults)`
4859
- );
4860
4822
  } else {
4861
4823
  // Fallback for runs that have no action results: emit test definition steps as Not Run.
4862
4824
  iteration.actionResults = stepsList
@@ -4872,11 +4834,6 @@ export default class ResultDataProvider {
4872
4834
  actionPath: String(step?.stepPosition ?? ''),
4873
4835
  }))
4874
4836
  .sort((a: any, b: any) => this.compareActionResults(a.stepPosition, b.stepPosition));
4875
- logger.debug(
4876
- `[RunlessResolver] TC ${String(point?.testCaseId || resultData?.testCase?.id || '')}: fallbackActionResults=${String(
4877
- iteration.actionResults?.length || 0
4878
- )} (from parsed steps)`
4879
- );
4880
4837
  }
4881
4838
  }
4882
4839
 
@@ -5083,6 +5083,131 @@ describe('ResultDataProvider', () => {
5083
5083
  });
5084
5084
  });
5085
5085
 
5086
+ describe('runless key/resolution edge cases', () => {
5087
+ it('should prioritize run key over point/revision in buildIterationLookupKey', () => {
5088
+ const key = (resultDataProvider as any).buildIterationLookupKey({
5089
+ testCaseId: 217916,
5090
+ lastRunId: 101,
5091
+ lastResultId: 202,
5092
+ testPointId: 303,
5093
+ testCaseRevision: 18,
5094
+ });
5095
+
5096
+ expect(key).toBe('101-202-217916');
5097
+ });
5098
+
5099
+ it('should treat blank run identifiers as missing and use point/revision fallback keys', () => {
5100
+ const pointKey = (resultDataProvider as any).buildIterationLookupKey({
5101
+ testCaseId: 217916,
5102
+ lastRunId: ' ',
5103
+ lastResultId: '',
5104
+ testPointId: 303,
5105
+ testCaseRevision: 18,
5106
+ });
5107
+ const revKey = (resultDataProvider as any).buildIterationLookupKey({
5108
+ testCaseId: 217916,
5109
+ lastRunId: ' ',
5110
+ lastResultId: '',
5111
+ testCaseRevision: 18,
5112
+ });
5113
+
5114
+ expect(pointKey).toBe('point-303-217916');
5115
+ expect(revKey).toBe('rev-217916-18');
5116
+ });
5117
+
5118
+ it('should deduplicate candidate keys when primary key is already case-only', () => {
5119
+ const keys = (resultDataProvider as any).buildIterationLookupCandidates({
5120
+ testCaseId: 217916,
5121
+ });
5122
+
5123
+ expect(keys).toEqual(['217916']);
5124
+ });
5125
+
5126
+ it('should resolve point revision from suite test case when test case revision is missing', () => {
5127
+ const revision = (resultDataProvider as any).resolvePointRevision(
5128
+ { workItem: { workItemFields: [] } },
5129
+ {
5130
+ suiteTestCase: {
5131
+ workItem: {
5132
+ workItemFields: [{ key: 'System.Rev', value: 23 }],
5133
+ },
5134
+ },
5135
+ }
5136
+ );
5137
+
5138
+ expect(revision).toBe(23);
5139
+ });
5140
+
5141
+ it('should short-circuit runless resolution when asOf snapshot already has steps', async () => {
5142
+ const asOfSnapshot = { rev: 18, fields: { 'Microsoft.VSTS.TCM.Steps': '<steps><step id="1"/></steps>' } };
5143
+ const asOfSpy = jest.spyOn(resultDataProvider as any, 'fetchWorkItemByAsOf').mockResolvedValueOnce(asOfSnapshot);
5144
+ const revSpy = jest.spyOn(resultDataProvider as any, 'fetchWorkItemByRevision').mockResolvedValueOnce(null);
5145
+ const latestSpy = jest.spyOn(resultDataProvider as any, 'fetchWorkItemLatest').mockResolvedValueOnce(null);
5146
+
5147
+ const res = await (resultDataProvider as any).resolveRunlessTestCaseData(
5148
+ mockProjectName,
5149
+ 217916,
5150
+ 18,
5151
+ '2025-05-20T00:47:30.610Z',
5152
+ null,
5153
+ true
5154
+ );
5155
+
5156
+ expect(res).toBe(asOfSnapshot);
5157
+ expect(asOfSpy).toHaveBeenCalledTimes(1);
5158
+ expect(revSpy).not.toHaveBeenCalled();
5159
+ expect(latestSpy).not.toHaveBeenCalled();
5160
+ });
5161
+
5162
+ it('should keep earliest snapshot when no source has steps', async () => {
5163
+ const asOfSnapshot = { rev: 1, fields: { 'Microsoft.VSTS.TCM.Steps': '' } };
5164
+ jest
5165
+ .spyOn(resultDataProvider as any, 'fetchWorkItemByAsOf')
5166
+ .mockResolvedValueOnce(asOfSnapshot);
5167
+ jest
5168
+ .spyOn(resultDataProvider as any, 'fetchWorkItemByRevision')
5169
+ .mockResolvedValueOnce({ rev: 2, fields: { 'Microsoft.VSTS.TCM.Steps': ' ' } });
5170
+ const latestSnapshot = { rev: 3, fields: { 'Microsoft.VSTS.TCM.Steps': '' } };
5171
+ jest.spyOn(resultDataProvider as any, 'fetchWorkItemLatest').mockResolvedValueOnce(latestSnapshot);
5172
+
5173
+ const res = await (resultDataProvider as any).resolveRunlessTestCaseData(
5174
+ mockProjectName,
5175
+ 217916,
5176
+ 18,
5177
+ '2025-05-20T00:47:30.610Z',
5178
+ null,
5179
+ true
5180
+ );
5181
+
5182
+ expect(res).toBe(asOfSnapshot);
5183
+ });
5184
+
5185
+ it('should return latest snapshot when only latest has steps', async () => {
5186
+ jest
5187
+ .spyOn(resultDataProvider as any, 'fetchWorkItemByAsOf')
5188
+ .mockResolvedValueOnce({ rev: 1, fields: { 'Microsoft.VSTS.TCM.Steps': '' } });
5189
+ jest
5190
+ .spyOn(resultDataProvider as any, 'fetchWorkItemByRevision')
5191
+ .mockResolvedValueOnce({ rev: 2, fields: { 'Microsoft.VSTS.TCM.Steps': '' } });
5192
+ const latestSnapshot = {
5193
+ rev: 3,
5194
+ fields: { 'Microsoft.VSTS.TCM.Steps': '<steps><step id="1"/></steps>' },
5195
+ };
5196
+ jest.spyOn(resultDataProvider as any, 'fetchWorkItemLatest').mockResolvedValueOnce(latestSnapshot);
5197
+
5198
+ const res = await (resultDataProvider as any).resolveRunlessTestCaseData(
5199
+ mockProjectName,
5200
+ 217916,
5201
+ 18,
5202
+ '2025-05-20T00:47:30.610Z',
5203
+ null,
5204
+ true
5205
+ );
5206
+
5207
+ expect(res).toBe(latestSnapshot);
5208
+ });
5209
+ });
5210
+
5086
5211
  describe('createIterationsMap', () => {
5087
5212
  it('should create iterations map from results', () => {
5088
5213
  // Arrange