@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/bin/modules/ResultDataProvider.d.ts +8 -1
- package/bin/modules/ResultDataProvider.js +32 -46
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +84 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +44 -87
- package/src/tests/modules/ResultDataProvider.test.ts +125 -0
package/package.json
CHANGED
|
@@ -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
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3414
|
-
|
|
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
|
-
|
|
3418
|
-
|
|
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
|
|
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
|
|
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[
|
|
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
|