@elisra-devops/docgen-data-provider 1.84.0 → 1.86.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 +2 -0
- package/bin/modules/ResultDataProvider.js +201 -45
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +269 -0
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/modules/ResultDataProvider.ts +221 -40
- package/src/tests/modules/ResultDataProvider.test.ts +324 -0
package/package.json
CHANGED
|
@@ -478,6 +478,10 @@ export default class ResultDataProvider {
|
|
|
478
478
|
testData
|
|
479
479
|
);
|
|
480
480
|
const requirementSapWbsByBaseKey = this.buildRequirementSapWbsByBaseKey(allRequirements);
|
|
481
|
+
const testCaseResponsibilityById = await this.buildMewpTestCaseResponsibilityMap(
|
|
482
|
+
testData,
|
|
483
|
+
projectName
|
|
484
|
+
);
|
|
481
485
|
const externalBugsByTestCase = await this.loadExternalBugsByTestCase(options?.externalBugsFile);
|
|
482
486
|
const externalL3L4ByBaseKey = await this.loadExternalL3L4ByBaseKey(
|
|
483
487
|
options?.externalL3L4File,
|
|
@@ -690,7 +694,8 @@ export default class ResultDataProvider {
|
|
|
690
694
|
linkedRequirementsByTestCase,
|
|
691
695
|
externalL3L4ByBaseKey,
|
|
692
696
|
externalBugsByTestCase,
|
|
693
|
-
externalJoinKeysByL2
|
|
697
|
+
externalJoinKeysByL2,
|
|
698
|
+
testCaseResponsibilityById
|
|
694
699
|
);
|
|
695
700
|
const coverageRowStats = rows.reduce(
|
|
696
701
|
(acc, row) => {
|
|
@@ -843,18 +848,6 @@ export default class ResultDataProvider {
|
|
|
843
848
|
[...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
844
849
|
);
|
|
845
850
|
|
|
846
|
-
const expectedFamilyCodes = new Set<string>();
|
|
847
|
-
for (const baseKey of mentionedBaseKeys) {
|
|
848
|
-
const familyCodes = requirementFamilies.get(baseKey);
|
|
849
|
-
if (familyCodes?.size) {
|
|
850
|
-
familyCodes.forEach((code) => expectedFamilyCodes.add(code));
|
|
851
|
-
} else {
|
|
852
|
-
for (const code of mentionedL2Only) {
|
|
853
|
-
if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
|
|
854
|
-
}
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
851
|
const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
|
|
859
852
|
const linkedFullCodes =
|
|
860
853
|
scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
|
|
@@ -868,14 +861,48 @@ export default class ResultDataProvider {
|
|
|
868
861
|
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
869
862
|
);
|
|
870
863
|
|
|
871
|
-
const
|
|
864
|
+
const mentionedCodesByBase = new Map<string, Set<string>>();
|
|
865
|
+
for (const code of mentionedL2Only) {
|
|
872
866
|
const baseKey = this.toRequirementKey(code);
|
|
873
|
-
if (!baseKey)
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
867
|
+
if (!baseKey) continue;
|
|
868
|
+
if (!mentionedCodesByBase.has(baseKey)) mentionedCodesByBase.set(baseKey, new Set<string>());
|
|
869
|
+
mentionedCodesByBase.get(baseKey)!.add(code);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Context-based direction A logic:
|
|
873
|
+
// 1) If no member of family is linked -> report only base SR (Step X: SR0054).
|
|
874
|
+
// 2) If family is partially linked -> report only specific missing members.
|
|
875
|
+
// 3) If family fully linked -> report nothing for that family.
|
|
876
|
+
const missingBaseWhenFamilyUncovered = new Set<string>();
|
|
877
|
+
const missingSpecificMentionedNoFamily = new Set<string>();
|
|
878
|
+
const missingFamilyMembers = new Set<string>();
|
|
879
|
+
for (const [baseKey, mentionedCodes] of mentionedCodesByBase.entries()) {
|
|
880
|
+
const familyCodes = requirementFamilies.get(baseKey);
|
|
881
|
+
if (familyCodes?.size) {
|
|
882
|
+
const missingInFamily = [...familyCodes].filter((code) => !linkedFullCodes.has(code));
|
|
883
|
+
if (missingInFamily.length === 0) continue;
|
|
884
|
+
const linkedInFamilyCount = familyCodes.size - missingInFamily.length;
|
|
885
|
+
if (linkedInFamilyCount === 0) {
|
|
886
|
+
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
887
|
+
} else {
|
|
888
|
+
for (const code of missingInFamily) {
|
|
889
|
+
missingFamilyMembers.add(code);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Fallback path when family data is unavailable for this base key.
|
|
896
|
+
for (const code of mentionedCodes) {
|
|
897
|
+
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
898
|
+
if (hasSpecificSuffix) {
|
|
899
|
+
if (!linkedFullCodes.has(code)) missingSpecificMentionedNoFamily.add(code);
|
|
900
|
+
} else if (!linkedBaseKeys.has(baseKey)) {
|
|
901
|
+
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
879
906
|
// Direction B is family-based: if any member of a family is mentioned in Expected Result,
|
|
880
907
|
// linked members of that same family are not considered "linked but not mentioned".
|
|
881
908
|
const extraLinked = [...linkedFullCodes].filter((code) => {
|
|
@@ -894,13 +921,22 @@ export default class ResultDataProvider {
|
|
|
894
921
|
mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
|
|
895
922
|
};
|
|
896
923
|
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
924
|
+
const sortedMissingSpecificMentionedNoFamily = [...missingSpecificMentionedNoFamily].sort((a, b) =>
|
|
925
|
+
a.localeCompare(b)
|
|
926
|
+
);
|
|
927
|
+
const sortedMissingBaseWhenFamilyUncovered = [...missingBaseWhenFamilyUncovered].sort((a, b) =>
|
|
928
|
+
a.localeCompare(b)
|
|
929
|
+
);
|
|
930
|
+
const sortedMissingFamilyMembers = [...missingFamilyMembers].sort((a, b) => a.localeCompare(b));
|
|
931
|
+
for (const code of sortedMissingSpecificMentionedNoFamily) {
|
|
900
932
|
const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
|
|
901
933
|
appendMentionedButNotLinked(code, stepRef);
|
|
902
934
|
}
|
|
903
|
-
for (const
|
|
935
|
+
for (const baseKey of sortedMissingBaseWhenFamilyUncovered) {
|
|
936
|
+
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
937
|
+
appendMentionedButNotLinked(baseKey, stepRef);
|
|
938
|
+
}
|
|
939
|
+
for (const code of sortedMissingFamilyMembers) {
|
|
904
940
|
const baseKey = this.toRequirementKey(code);
|
|
905
941
|
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
906
942
|
appendMentionedButNotLinked(code, stepRef);
|
|
@@ -936,7 +972,11 @@ export default class ResultDataProvider {
|
|
|
936
972
|
`MEWP internal validation parse diagnostics: ` +
|
|
937
973
|
`testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
|
|
938
974
|
`stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
|
|
939
|
-
`linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
|
|
975
|
+
`linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${
|
|
976
|
+
sortedMissingSpecificMentionedNoFamily.length +
|
|
977
|
+
sortedMissingBaseWhenFamilyUncovered.length +
|
|
978
|
+
sortedMissingFamilyMembers.length
|
|
979
|
+
} ` +
|
|
940
980
|
`linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
|
|
941
981
|
`customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
|
|
942
982
|
);
|
|
@@ -1158,7 +1198,8 @@ export default class ResultDataProvider {
|
|
|
1158
1198
|
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
|
|
1159
1199
|
l3l4ByBaseKey: Map<string, MewpL3L4Pair[]>,
|
|
1160
1200
|
externalBugsByTestCase: Map<number, MewpBugLink[]>,
|
|
1161
|
-
externalJoinKeysByL2?: Map<string, Set<string
|
|
1201
|
+
externalJoinKeysByL2?: Map<string, Set<string>>,
|
|
1202
|
+
testCaseResponsibilityById?: Map<number, string>
|
|
1162
1203
|
): MewpCoverageRow[] {
|
|
1163
1204
|
const rows: MewpCoverageRow[] = [];
|
|
1164
1205
|
const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
|
|
@@ -1202,7 +1243,8 @@ export default class ResultDataProvider {
|
|
|
1202
1243
|
...bug,
|
|
1203
1244
|
responsibility: this.resolveCoverageBugResponsibility(
|
|
1204
1245
|
String(bug?.responsibility || ''),
|
|
1205
|
-
requirement
|
|
1246
|
+
requirement,
|
|
1247
|
+
String(testCaseResponsibilityById?.get(testCaseId) || '')
|
|
1206
1248
|
),
|
|
1207
1249
|
});
|
|
1208
1250
|
}
|
|
@@ -1246,18 +1288,127 @@ export default class ResultDataProvider {
|
|
|
1246
1288
|
|
|
1247
1289
|
private resolveCoverageBugResponsibility(
|
|
1248
1290
|
rawResponsibility: string,
|
|
1249
|
-
requirement: Pick<MewpL2RequirementFamily, 'responsibility'
|
|
1291
|
+
requirement: Pick<MewpL2RequirementFamily, 'responsibility'>,
|
|
1292
|
+
testCaseResponsibility: string = ''
|
|
1250
1293
|
): string {
|
|
1251
|
-
const
|
|
1294
|
+
const normalizeDisplay = (value: string): string => {
|
|
1295
|
+
const direct = String(value || '').trim();
|
|
1296
|
+
if (!direct) return '';
|
|
1297
|
+
const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(direct));
|
|
1298
|
+
if (resolved === 'ESUK') return 'ESUK';
|
|
1299
|
+
if (resolved === 'IL') return 'Elisra';
|
|
1300
|
+
return direct;
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
const direct = normalizeDisplay(rawResponsibility);
|
|
1252
1304
|
if (direct && direct.toLowerCase() !== 'unknown') return direct;
|
|
1253
1305
|
|
|
1254
|
-
const
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (
|
|
1306
|
+
const fromTestCase = normalizeDisplay(testCaseResponsibility);
|
|
1307
|
+
if (fromTestCase && fromTestCase.toLowerCase() !== 'unknown') return fromTestCase;
|
|
1308
|
+
|
|
1309
|
+
const fromRequirement = normalizeDisplay(String(requirement?.responsibility || ''));
|
|
1310
|
+
if (fromRequirement && fromRequirement.toLowerCase() !== 'unknown') return fromRequirement;
|
|
1259
1311
|
|
|
1260
|
-
return direct || 'Unknown';
|
|
1312
|
+
return direct || fromTestCase || fromRequirement || 'Unknown';
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
private async buildMewpTestCaseResponsibilityMap(
|
|
1316
|
+
testData: any[],
|
|
1317
|
+
projectName: string
|
|
1318
|
+
): Promise<Map<number, string>> {
|
|
1319
|
+
const out = new Map<number, string>();
|
|
1320
|
+
const unresolved = new Set<number>();
|
|
1321
|
+
|
|
1322
|
+
const extractFromWorkItemFields = (workItemFields: any): Record<string, any> => {
|
|
1323
|
+
const fields: Record<string, any> = {};
|
|
1324
|
+
if (!Array.isArray(workItemFields)) return fields;
|
|
1325
|
+
for (const field of workItemFields) {
|
|
1326
|
+
const keyCandidates = [field?.key, field?.name, field?.referenceName]
|
|
1327
|
+
.map((item) => String(item || '').trim())
|
|
1328
|
+
.filter((item) => !!item);
|
|
1329
|
+
for (const key of keyCandidates) {
|
|
1330
|
+
fields[key] = field?.value;
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return fields;
|
|
1334
|
+
};
|
|
1335
|
+
|
|
1336
|
+
for (const suite of testData || []) {
|
|
1337
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1338
|
+
for (const testCase of testCasesItems) {
|
|
1339
|
+
const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1340
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
|
|
1341
|
+
|
|
1342
|
+
const workItemFieldsMap = testCase?.workItem?.fields || {};
|
|
1343
|
+
const workItemFieldsList = extractFromWorkItemFields(testCase?.workItem?.workItemFields);
|
|
1344
|
+
const directFieldAliases = Object.fromEntries(
|
|
1345
|
+
Object.entries({
|
|
1346
|
+
'System.AreaPath':
|
|
1347
|
+
testCase?.workItem?.areaPath ||
|
|
1348
|
+
testCase?.workItem?.AreaPath ||
|
|
1349
|
+
testCase?.areaPath ||
|
|
1350
|
+
testCase?.AreaPath ||
|
|
1351
|
+
testCase?.testCase?.areaPath ||
|
|
1352
|
+
testCase?.testCase?.AreaPath,
|
|
1353
|
+
'Area Path':
|
|
1354
|
+
testCase?.workItem?.['Area Path'] ||
|
|
1355
|
+
testCase?.['Area Path'] ||
|
|
1356
|
+
testCase?.testCase?.['Area Path'],
|
|
1357
|
+
AreaPath:
|
|
1358
|
+
testCase?.workItem?.AreaPath ||
|
|
1359
|
+
testCase?.AreaPath ||
|
|
1360
|
+
testCase?.testCase?.AreaPath,
|
|
1361
|
+
}).filter(([, value]) => value !== undefined && value !== null && String(value).trim() !== '')
|
|
1362
|
+
);
|
|
1363
|
+
const fromWorkItem = this.deriveMewpTestCaseResponsibility({
|
|
1364
|
+
...directFieldAliases,
|
|
1365
|
+
...workItemFieldsList,
|
|
1366
|
+
...workItemFieldsMap,
|
|
1367
|
+
});
|
|
1368
|
+
|
|
1369
|
+
if (fromWorkItem) {
|
|
1370
|
+
out.set(testCaseId, fromWorkItem);
|
|
1371
|
+
} else {
|
|
1372
|
+
unresolved.add(testCaseId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1377
|
+
for (const testPoint of testPointsItems) {
|
|
1378
|
+
const testCaseId = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
|
|
1379
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
|
|
1380
|
+
const fromPoint = this.deriveMewpTestCaseResponsibility({
|
|
1381
|
+
'System.AreaPath': testPoint?.areaPath || testPoint?.AreaPath,
|
|
1382
|
+
'Area Path': testPoint?.['Area Path'],
|
|
1383
|
+
AreaPath: testPoint?.AreaPath,
|
|
1384
|
+
});
|
|
1385
|
+
if (fromPoint) {
|
|
1386
|
+
out.set(testCaseId, fromPoint);
|
|
1387
|
+
unresolved.delete(testCaseId);
|
|
1388
|
+
} else {
|
|
1389
|
+
unresolved.add(testCaseId);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
if (unresolved.size > 0) {
|
|
1395
|
+
try {
|
|
1396
|
+
const workItems = await this.fetchWorkItemsByIds(projectName, [...unresolved], false);
|
|
1397
|
+
for (const workItem of workItems || []) {
|
|
1398
|
+
const testCaseId = Number(workItem?.id || 0);
|
|
1399
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || out.has(testCaseId)) continue;
|
|
1400
|
+
const resolved = this.deriveMewpTestCaseResponsibility(workItem?.fields || {});
|
|
1401
|
+
if (!resolved) continue;
|
|
1402
|
+
out.set(testCaseId, resolved);
|
|
1403
|
+
}
|
|
1404
|
+
} catch (error: any) {
|
|
1405
|
+
logger.warn(
|
|
1406
|
+
`MEWP coverage: failed to enrich test-case responsibility fallback: ${error?.message || error}`
|
|
1407
|
+
);
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
return out;
|
|
1261
1412
|
}
|
|
1262
1413
|
|
|
1263
1414
|
private resolveMewpL2RunStatus(input: {
|
|
@@ -2457,11 +2608,42 @@ export default class ResultDataProvider {
|
|
|
2457
2608
|
if (fromExplicitLabel) return fromExplicitLabel;
|
|
2458
2609
|
if (explicitSapWbsByLabel) return explicitSapWbsByLabel;
|
|
2459
2610
|
|
|
2460
|
-
const
|
|
2461
|
-
|
|
2462
|
-
|
|
2611
|
+
const areaPathCandidates = [
|
|
2612
|
+
fields?.['System.AreaPath'],
|
|
2613
|
+
fields?.['Area Path'],
|
|
2614
|
+
fields?.['AreaPath'],
|
|
2615
|
+
];
|
|
2616
|
+
for (const candidate of areaPathCandidates) {
|
|
2617
|
+
const normalized = this.toMewpComparableText(candidate);
|
|
2618
|
+
const resolved = this.resolveMewpResponsibility(normalized);
|
|
2619
|
+
if (resolved) return resolved;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
const keyHints = ['sapwbs', 'responsibility', 'owner', 'areapath', 'area path'];
|
|
2623
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
2624
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
2625
|
+
if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
2626
|
+
const resolved = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
2627
|
+
if (resolved) return resolved;
|
|
2628
|
+
}
|
|
2629
|
+
|
|
2630
|
+
return '';
|
|
2631
|
+
}
|
|
2632
|
+
|
|
2633
|
+
// Test-case responsibility must come from test-case path context (not SAPWBS).
|
|
2634
|
+
private deriveMewpTestCaseResponsibility(fields: Record<string, any>): string {
|
|
2635
|
+
const areaPathCandidates = [
|
|
2636
|
+
fields?.['System.AreaPath'],
|
|
2637
|
+
fields?.['Area Path'],
|
|
2638
|
+
fields?.['AreaPath'],
|
|
2639
|
+
];
|
|
2640
|
+
for (const candidate of areaPathCandidates) {
|
|
2641
|
+
const normalized = this.toMewpComparableText(candidate);
|
|
2642
|
+
const resolved = this.resolveMewpResponsibility(normalized);
|
|
2643
|
+
if (resolved) return resolved;
|
|
2644
|
+
}
|
|
2463
2645
|
|
|
2464
|
-
const keyHints = ['
|
|
2646
|
+
const keyHints = ['areapath', 'area path', 'responsibility', 'owner'];
|
|
2465
2647
|
for (const [key, value] of Object.entries(fields || {})) {
|
|
2466
2648
|
const normalizedKey = String(key || '').toLowerCase();
|
|
2467
2649
|
if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
@@ -2905,7 +3087,6 @@ export default class ResultDataProvider {
|
|
|
2905
3087
|
logger.warn(`Invalid run result ${runId} or result ${resultId}`);
|
|
2906
3088
|
return null;
|
|
2907
3089
|
}
|
|
2908
|
-
logger.warn(`Current Test point for Test case ${point.testCaseId} is in Active state`);
|
|
2909
3090
|
const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${point.testCaseId}?$expand=all`;
|
|
2910
3091
|
const testCaseData = await TFSServices.getItemContent(url, this.token);
|
|
2911
3092
|
const newResultData: PlainTestResult = {
|