@skyramp/mcp 0.2.7 → 0.2.8-rc.1
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/build/index.js +5 -2
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
- package/build/prompts/sut-setup/shared.d.ts +39 -0
- package/build/prompts/sut-setup/shared.js +132 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
- package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
- package/build/prompts/testbot/testbot-prompts.js +113 -22
- package/build/prompts/testbot/testbot-prompts.test.js +95 -45
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.js +2 -2
- package/build/services/TestDiscoveryService.d.ts +2 -2
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestExecutionService.d.ts +10 -1
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.js +306 -0
- package/build/tools/executeSkyrampTestTool.js +26 -7
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/submitReportTool.d.ts +10 -0
- package/build/tools/submitReportTool.js +106 -39
- package/build/tools/submitReportTool.test.js +296 -50
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
- package/build/tools/test-management/analyzeChangesTool.js +106 -41
- package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
- package/build/tools/workspace/serviceUpsert.d.ts +12 -0
- package/build/tools/workspace/serviceUpsert.js +23 -0
- package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
- package/build/tools/workspace/serviceUpsert.test.js +50 -0
- package/build/types/TestAnalysis.d.ts +2 -1
- package/build/types/TestExecution.d.ts +12 -2
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestbotReport.d.ts +68 -0
- package/build/types/TestbotReport.js +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.js +3 -0
- package/build/utils/AnalysisStateManager.d.ts +61 -1
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.js +98 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/routeParsers.d.ts +0 -10
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/utils.d.ts +7 -1
- package/build/utils/utils.js +10 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +10 -10
- package/build/workspace/workspace.js +7 -7
- package/build/workspace/workspace.test.js +6 -6
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
- package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -8,7 +8,7 @@ import { simpleGit } from "simple-git";
|
|
|
8
8
|
import { logger } from "../../utils/logger.js";
|
|
9
9
|
import { parseWorkspaceAuthType, getDefaultAuthHeader, WorkspaceAuthType, readWorkspaceConfigRaw } from "../../utils/workspaceAuth.js";
|
|
10
10
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
11
|
-
import { StateManager, registerSession, storeSessionData, setTestsRepoDir, } from "../../utils/AnalysisStateManager.js";
|
|
11
|
+
import { StateManager, registerSession, storeSessionData, setTestsRepoDir, getActiveRunStatePath, setActiveRunStatePath, } from "../../utils/AnalysisStateManager.js";
|
|
12
12
|
import { buildRecommendationPrompt } from "../../prompts/test-recommendation/test-recommendation-prompt.js";
|
|
13
13
|
import { hasFlutterSdkDep, isFrontendFile, isTestFile } from "../../prompts/test-recommendation/scopeAssessment.js";
|
|
14
14
|
import { enumerateCandidateUiPages } from "../../utils/uiPageEnumerator.js";
|
|
@@ -24,6 +24,32 @@ import { buildAnalysisOutputText } from "../../prompts/test-recommendation/analy
|
|
|
24
24
|
import { parseTraceFile, discoverTraceFiles, discoverPlaywrightZips, } from "../../utils/trace-parser.js";
|
|
25
25
|
import { TestSource } from "../../types/TestAnalysis.js";
|
|
26
26
|
import { parsePRComments } from "../../utils/pr-comment-parser.js";
|
|
27
|
+
// Generic module names that appear as imports in virtually every router file.
|
|
28
|
+
// Matching against these would extract ALL prefix= tokens from mount files,
|
|
29
|
+
// flooding changedResources with unrelated resources.
|
|
30
|
+
const GENERIC_MODULE_NAMES = new Set([
|
|
31
|
+
"models", "utils", "helpers", "schemas", "types", "constants",
|
|
32
|
+
"config", "settings", "exceptions", "errors", "base", "common",
|
|
33
|
+
"app", "main", "index", "server", "init", "deps", "dependencies",
|
|
34
|
+
]);
|
|
35
|
+
function resolveMountPrefixTokens(mountFileContents, changedFiles) {
|
|
36
|
+
const tokens = changedFiles.flatMap((file) => {
|
|
37
|
+
const mod = path.basename(file).replace(/\.[^.]+$/, "").toLowerCase();
|
|
38
|
+
if (GENERIC_MODULE_NAMES.has(mod))
|
|
39
|
+
return [];
|
|
40
|
+
const modRe = new RegExp(`\\b${mod.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
41
|
+
return mountFileContents.flatMap((content) => {
|
|
42
|
+
if (!modRe.test(content.toLowerCase()))
|
|
43
|
+
return [];
|
|
44
|
+
const matches = [...content.matchAll(/prefix\s*=\s*["'](\/?[^"']+)/g)];
|
|
45
|
+
return matches.flatMap((m) => {
|
|
46
|
+
const token = extractResourceFromPath(m[1]);
|
|
47
|
+
return token !== "unknown" ? [token] : [];
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
return [...new Set(tokens)];
|
|
52
|
+
}
|
|
27
53
|
/** Exported for testing: maps a parsed trace result to a TraceFile. */
|
|
28
54
|
export function buildTraceFileEntry(tracePath, result) {
|
|
29
55
|
return {
|
|
@@ -339,11 +365,10 @@ export const analyzeChangesInputSchema = {
|
|
|
339
365
|
.number()
|
|
340
366
|
.optional()
|
|
341
367
|
.describe("GitHub PR number. When provided, fetches previous TestBot comments on this PR and skips re-recommending tests already suggested in earlier commits — reduces duplicate recommendations across multiple pushes to the same PR."),
|
|
342
|
-
|
|
368
|
+
repository: z
|
|
343
369
|
.string()
|
|
344
|
-
.refine((v) => path.isAbsolute(v), { message: "stateOutputFile must be an absolute path" })
|
|
345
370
|
.optional()
|
|
346
|
-
.describe("
|
|
371
|
+
.describe("The owner/repo this call analyzes (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — for the primary AND every related repo. All calls in one Testbot run share ONE state file automatically (you do NOT pass a file path); each repo's analysis is stored as its own section within it. The primary's analysis is kept at the root and tagged with its owner/repo; related repos are stored as keyed sections. Omit only for a single-repo run."),
|
|
347
372
|
testsRepoDir: z
|
|
348
373
|
.string()
|
|
349
374
|
.refine((v) => path.isAbsolute(v), { message: "testsRepoDir must be an absolute path" })
|
|
@@ -568,6 +593,30 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
568
593
|
// testsRepoDir is a cross-repo path override — honour it when set.
|
|
569
594
|
// Otherwise always scan the full repo root.
|
|
570
595
|
const testDir = params.testsRepoDir ?? undefined;
|
|
596
|
+
const routerMountContext = grepRouterMountingContext(params.repositoryPath);
|
|
597
|
+
// Read contents once — used for token extraction and LLM prompt injection.
|
|
598
|
+
const ROUTER_INLINE_LIMIT = 4096;
|
|
599
|
+
const routerMountContextContents = routerMountContext.map((f) => {
|
|
600
|
+
try {
|
|
601
|
+
const abs = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
|
|
602
|
+
if (fs.statSync(abs).size > ROUTER_INLINE_LIMIT * 10)
|
|
603
|
+
return ""; // skip very large files
|
|
604
|
+
return fs.readFileSync(abs, "utf-8");
|
|
605
|
+
}
|
|
606
|
+
catch {
|
|
607
|
+
return "";
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
// Size-capped subset for LLM prompt injection.
|
|
611
|
+
const ROUTER_INLINE_MAX_FILES = 3;
|
|
612
|
+
const routerFileContents = routerMountContext
|
|
613
|
+
.slice(0, ROUTER_INLINE_MAX_FILES)
|
|
614
|
+
.flatMap((f, i) => {
|
|
615
|
+
const content = routerMountContextContents[i] ?? "";
|
|
616
|
+
if (!content || content.length > ROUTER_INLINE_LIMIT)
|
|
617
|
+
return [];
|
|
618
|
+
return [{ file: f, content: content.trimEnd() }];
|
|
619
|
+
});
|
|
571
620
|
// Compute changedResources from classified endpoints for test discovery filtering.
|
|
572
621
|
// undefined → full-repo mode (no diff context)
|
|
573
622
|
// [] → PR mode, no endpoints found → skip external tests
|
|
@@ -596,14 +645,22 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
596
645
|
}
|
|
597
646
|
else {
|
|
598
647
|
// All endpoints resolved to "unknown" (e.g. NestJS relative paths) —
|
|
599
|
-
// extract resource names from
|
|
600
|
-
const
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
648
|
+
// first try to extract resource names from router mount prefixes.
|
|
649
|
+
const changedSourceFiles = classifiedEndpoints.changedFiles ?? [];
|
|
650
|
+
const mountedResources = resolveMountPrefixTokens(routerMountContextContents, changedSourceFiles);
|
|
651
|
+
if (mountedResources.length > 0) {
|
|
652
|
+
changedResources = mountedResources;
|
|
653
|
+
}
|
|
654
|
+
else {
|
|
655
|
+
// Fall back to extracting resource names from changed file paths.
|
|
656
|
+
const candidateFiles = classifiedEndpoints.unmatchedFiles.length > 0
|
|
657
|
+
? classifiedEndpoints.unmatchedFiles
|
|
658
|
+
: classifiedEndpoints.changedFiles ?? [];
|
|
659
|
+
const fromFiles = candidateFiles
|
|
660
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
661
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
662
|
+
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
663
|
+
}
|
|
607
664
|
}
|
|
608
665
|
}
|
|
609
666
|
else if (classifiedEndpoints.unmatchedFiles.length > 0) {
|
|
@@ -622,7 +679,15 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
622
679
|
changedResources = fromFiles.length > 0 ? fromFiles : ["unknown"];
|
|
623
680
|
}
|
|
624
681
|
else {
|
|
625
|
-
|
|
682
|
+
// No endpoints and no unmatched files — the diff touches only non-route
|
|
683
|
+
// files (e.g. schema, model, DTO). Extract resource tokens from the
|
|
684
|
+
// changed file paths so tests that import the changed symbol are still
|
|
685
|
+
// surfaced for drift assessment. Fall back to [] only if extraction
|
|
686
|
+
// yields nothing (e.g. a file with a non-resource name like utils.py).
|
|
687
|
+
const fromChangedFiles = (classifiedEndpoints.changedFiles ?? [])
|
|
688
|
+
.map((f) => extractResourceFromPath(f.replace(/\.[^./]+$/, "").replace(FRAMEWORK_SUFFIX_RE, "")))
|
|
689
|
+
.filter((r, i, arr) => r !== "unknown" && arr.indexOf(r) === i);
|
|
690
|
+
changedResources = fromChangedFiles.length > 0 ? fromChangedFiles : [];
|
|
626
691
|
}
|
|
627
692
|
}
|
|
628
693
|
let existingTests = [];
|
|
@@ -903,10 +968,15 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
903
968
|
sourceFile: m.sourceFile ?? "",
|
|
904
969
|
})))
|
|
905
970
|
: [];
|
|
971
|
+
const removedEndpointsForDrafting = classifiedEndpoints?.removedEndpoints.flatMap((ep) => ep.methods.map((m) => ({
|
|
972
|
+
method: m,
|
|
973
|
+
path: ep.path,
|
|
974
|
+
sourceFile: ep.sourceFile,
|
|
975
|
+
}))) ?? [];
|
|
906
976
|
const codeInferredScenarios = draftScenariosFromEndpoints(skeletonEndpoints, scenarioDraftSeed, wsAuthType, {
|
|
907
977
|
changedEndpoints: changedEndpointsForSecurityExpansion,
|
|
908
978
|
securityRelevantDiff,
|
|
909
|
-
});
|
|
979
|
+
}, removedEndpointsForDrafting);
|
|
910
980
|
let allDraftedScenarios = codeInferredScenarios;
|
|
911
981
|
if (traceResult && traceResult.userFlows.length > 0) {
|
|
912
982
|
const traceScenarios = traceResult.userFlows
|
|
@@ -1109,7 +1179,6 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1109
1179
|
// can be persisted to the state file for downstream tools (health, drift).
|
|
1110
1180
|
// Without them, analyzeTestHealth would work only off the static catalog
|
|
1111
1181
|
// which has wrong paths for nested resources and unsupported frameworks.
|
|
1112
|
-
const routerMountContext = grepRouterMountingContext(params.repositoryPath);
|
|
1113
1182
|
const routeLikeUnmatchedFiles = [];
|
|
1114
1183
|
for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
|
|
1115
1184
|
const routeLike = SOURCE_EXTS.test(file) &&
|
|
@@ -1138,25 +1207,6 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1138
1207
|
diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
|
|
1139
1208
|
await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
|
|
1140
1209
|
}
|
|
1141
|
-
// Read router mount files server-side (size-capped) so the LLM has them
|
|
1142
|
-
// inline and doesn't need an extra read step when no spec is available.
|
|
1143
|
-
const ROUTER_INLINE_LIMIT = 4096; // bytes — skip files larger than ~4 KB
|
|
1144
|
-
const ROUTER_INLINE_MAX_FILES = 3;
|
|
1145
|
-
const routerFileContents = routerMountContext
|
|
1146
|
-
.slice(0, ROUTER_INLINE_MAX_FILES)
|
|
1147
|
-
.flatMap((f) => {
|
|
1148
|
-
try {
|
|
1149
|
-
const absPath = path.isAbsolute(f) ? f : path.join(params.repositoryPath, f);
|
|
1150
|
-
const stat = fs.statSync(absPath);
|
|
1151
|
-
if (stat.size > ROUTER_INLINE_LIMIT)
|
|
1152
|
-
return [];
|
|
1153
|
-
const content = fs.readFileSync(absPath, "utf-8").trimEnd();
|
|
1154
|
-
return [{ file: f, content }];
|
|
1155
|
-
}
|
|
1156
|
-
catch {
|
|
1157
|
-
return [];
|
|
1158
|
-
}
|
|
1159
|
-
});
|
|
1160
1210
|
// Compute UI context from the diff's changed files using the shared
|
|
1161
1211
|
// `isFrontendFile` classifier. Persisting this in the stateFile lets
|
|
1162
1212
|
// skyramp_analyze_test_health and the recommendation prompt consume the
|
|
@@ -1252,22 +1302,37 @@ export function registerAnalyzeChangesTool(server) {
|
|
|
1252
1302
|
relevantExternalTestPaths,
|
|
1253
1303
|
},
|
|
1254
1304
|
};
|
|
1255
|
-
// Clean up old state files (>24 hours) before creating new one
|
|
1256
|
-
const stateDir = params.stateOutputFile
|
|
1257
|
-
? path.dirname(path.resolve(params.stateOutputFile))
|
|
1258
|
-
: undefined;
|
|
1305
|
+
// Clean up old state files (>24 hours) before creating new one.
|
|
1259
1306
|
try {
|
|
1260
|
-
await StateManager.cleanupOldFiles(24
|
|
1307
|
+
await StateManager.cleanupOldFiles(24);
|
|
1261
1308
|
}
|
|
1262
1309
|
catch (error) {
|
|
1263
1310
|
logger.warning(`Failed to cleanup old state files: ${error.message}`);
|
|
1264
1311
|
}
|
|
1265
|
-
|
|
1266
|
-
|
|
1312
|
+
// Resolve the state file. Multi-repo (`repository` set): all calls in one
|
|
1313
|
+
// Testbot run share ONE state file — the first call creates it and records
|
|
1314
|
+
// the run anchor; later related-repo calls reuse it (no agent-provided path,
|
|
1315
|
+
// which previously caused filename hallucinations). Single-repo (`repository`
|
|
1316
|
+
// absent): always a fresh per-call file — byte-identical to prior behavior.
|
|
1317
|
+
const activeRunPath = params.repository
|
|
1318
|
+
? getActiveRunStatePath()
|
|
1319
|
+
: undefined;
|
|
1320
|
+
const stateManager = activeRunPath
|
|
1321
|
+
? StateManager.fromStatePath(activeRunPath)
|
|
1322
|
+
: new StateManager("analysis", sessionId);
|
|
1323
|
+
// `repository` identifies which repo this analysis belongs to. The primary
|
|
1324
|
+
// (first write of the run, or matching the root's recorded repository) is
|
|
1325
|
+
// stored at the root; a related repo is stored as a keyed section within the
|
|
1326
|
+
// same run-scoped stateFile.
|
|
1327
|
+
await stateManager.writeRepoData(unifiedState, {
|
|
1328
|
+
repo: params.repository,
|
|
1267
1329
|
repositoryPath: params.repositoryPath,
|
|
1268
1330
|
step: "analyze_changes",
|
|
1269
1331
|
});
|
|
1270
1332
|
const stateFile = stateManager.getStatePath();
|
|
1333
|
+
// Anchor the run's state file so subsequent related-repo calls reuse it.
|
|
1334
|
+
if (params.repository)
|
|
1335
|
+
setActiveRunStatePath(stateFile);
|
|
1271
1336
|
registerSession(sessionId, stateFile);
|
|
1272
1337
|
try {
|
|
1273
1338
|
await server.server.sendResourceListChanged();
|
|
@@ -49,14 +49,24 @@ jest.unstable_mockModule("../../utils/trace-parser.js", () => ({
|
|
|
49
49
|
jest.unstable_mockModule("../../utils/pr-comment-parser.js", () => ({
|
|
50
50
|
parsePRComments: jest.fn(),
|
|
51
51
|
}));
|
|
52
|
-
jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () =>
|
|
53
|
-
|
|
52
|
+
jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => {
|
|
53
|
+
const makeInstance = () => ({
|
|
54
54
|
writeData: jest.fn(),
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
writeRepoData: jest.fn(),
|
|
56
|
+
getStatePath: jest.fn().mockReturnValue("/tmp/skyramp/analysis-state.json"),
|
|
57
|
+
});
|
|
58
|
+
const StateManager = jest.fn().mockImplementation(makeInstance);
|
|
59
|
+
StateManager.fromStatePath = jest.fn().mockImplementation(makeInstance);
|
|
60
|
+
return {
|
|
61
|
+
StateManager,
|
|
62
|
+
registerSession: jest.fn(),
|
|
63
|
+
storeSessionData: jest.fn(),
|
|
64
|
+
setTestsRepoDir: jest.fn(),
|
|
65
|
+
getActiveRunStatePath: jest.fn(),
|
|
66
|
+
setActiveRunStatePath: jest.fn(),
|
|
67
|
+
clearActiveRunStatePath: jest.fn(),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
60
70
|
// Keep every real workspaceAuth export the subject's import graph needs
|
|
61
71
|
// (readWorkspaceConfigRaw, getDefaultAuthHeader, WorkspaceAuthType, …) and
|
|
62
72
|
// override only parseWorkspaceAuthType. requireActual can't load ESM
|
|
@@ -99,28 +109,6 @@ describe("isSecurityRelevantDiff", () => {
|
|
|
99
109
|
expect(isSecurityRelevantDiff("move compression middleware to server setup")).toBe(false);
|
|
100
110
|
});
|
|
101
111
|
});
|
|
102
|
-
describe("analyzeChangesInputSchema — stateOutputFile validation", () => {
|
|
103
|
-
it("accepts a valid absolute path", () => {
|
|
104
|
-
const result = schema.safeParse({
|
|
105
|
-
repositoryPath: "/repo",
|
|
106
|
-
stateOutputFile: "/tmp/analyze-changes-state.json",
|
|
107
|
-
});
|
|
108
|
-
expect(result.success).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
it("rejects a relative path for stateOutputFile", () => {
|
|
111
|
-
// stateOutputFile must be absolute so the caller can guarantee the file location.
|
|
112
|
-
// Relative paths are silently ambiguous and should be rejected.
|
|
113
|
-
const result = schema.safeParse({
|
|
114
|
-
repositoryPath: "/repo",
|
|
115
|
-
stateOutputFile: "relative/path/state.json",
|
|
116
|
-
});
|
|
117
|
-
expect(result.success).toBe(false);
|
|
118
|
-
});
|
|
119
|
-
it("accepts absence of stateOutputFile (optional field)", () => {
|
|
120
|
-
const result = schema.safeParse({ repositoryPath: "/repo" });
|
|
121
|
-
expect(result.success).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
});
|
|
124
112
|
describe("automatic old files cleanup", () => {
|
|
125
113
|
let cleanupOldFilesSpy;
|
|
126
114
|
beforeEach(() => {
|
|
@@ -128,31 +116,23 @@ describe("automatic old files cleanup", () => {
|
|
|
128
116
|
cleanupOldFilesSpy = jest.fn().mockResolvedValue(0);
|
|
129
117
|
StateManager.cleanupOldFiles = cleanupOldFilesSpy;
|
|
130
118
|
});
|
|
131
|
-
it("calls cleanupOldFiles with default temp dir
|
|
132
|
-
// This test verifies the cleanup call is made without stateDir when using default location
|
|
119
|
+
it("calls cleanupOldFiles with default temp dir", async () => {
|
|
133
120
|
expect(StateManager.cleanupOldFiles).toBeDefined();
|
|
134
121
|
await StateManager.cleanupOldFiles(24, undefined);
|
|
135
122
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined);
|
|
136
123
|
});
|
|
137
|
-
it("calls cleanupOldFiles with custom dir
|
|
138
|
-
// This test verifies the cleanup call is made with the directory of stateOutputFile
|
|
124
|
+
it("calls cleanupOldFiles with custom dir", async () => {
|
|
139
125
|
const customPath = "/custom/dir";
|
|
140
126
|
await StateManager.cleanupOldFiles(24, customPath);
|
|
141
127
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, customPath);
|
|
142
128
|
});
|
|
143
|
-
it("calls cleanupOldFiles with empty stateTypes
|
|
144
|
-
// analyzeTestHealthTool passes [] so state files (skyramp-analysis-*, skyramp-recommendation-*)
|
|
145
|
-
// are never deleted — the caller still needs args.stateFile for skyramp_actions.
|
|
129
|
+
it("calls cleanupOldFiles with empty stateTypes", async () => {
|
|
146
130
|
await StateManager.cleanupOldFiles(24, undefined, []);
|
|
147
131
|
expect(cleanupOldFilesSpy).toHaveBeenCalledWith(24, undefined, []);
|
|
148
132
|
});
|
|
149
133
|
it("continues execution if cleanup fails", async () => {
|
|
150
|
-
// Cleanup failures should not crash the analyze flow
|
|
151
134
|
cleanupOldFilesSpy.mockRejectedValue(new Error("permission denied"));
|
|
152
|
-
await expect(StateManager.cleanupOldFiles(24).catch(() =>
|
|
153
|
-
// In the real code, this is caught and logged as a warning
|
|
154
|
-
return Promise.resolve();
|
|
155
|
-
})).resolves.toBeUndefined();
|
|
135
|
+
await expect(StateManager.cleanupOldFiles(24).catch(() => Promise.resolve())).resolves.toBeUndefined();
|
|
156
136
|
});
|
|
157
137
|
});
|
|
158
138
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -15,11 +15,15 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
15
15
|
idempotentHint: true,
|
|
16
16
|
openWorldHint: false,
|
|
17
17
|
},
|
|
18
|
-
description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow
|
|
18
|
+
description: `Generate drift assessment instructions for existing tests — second step of the unified Test Health Analysis Flow.`,
|
|
19
19
|
inputSchema: {
|
|
20
20
|
stateFile: z
|
|
21
21
|
.string()
|
|
22
|
-
.describe("Path to state file
|
|
22
|
+
.describe("Path to the analyze-changes-state.json file returned by skyramp_analyze_changes (the stateFile field in its output). Do NOT pass summaryOutputFile or testbot-result.txt here."),
|
|
23
|
+
repository: z
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("The owner/repo whose analysis section to assess (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for the primary repo, or a related repo's owner/repo for that repo's section within the run-scoped stateFile. Omit only for a single-repo run."),
|
|
23
27
|
},
|
|
24
28
|
outputSchema: {
|
|
25
29
|
prompt: z.string().describe("LLM drift analysis prompt and assessment instructions."),
|
|
@@ -27,17 +31,20 @@ export function registerAnalyzeTestHealthTool(server) {
|
|
|
27
31
|
}, async (args) => {
|
|
28
32
|
let errorResult;
|
|
29
33
|
try {
|
|
30
|
-
logger.info(`Analyzing test health from state file: ${args.stateFile}`);
|
|
31
|
-
// Load UnifiedAnalysisState
|
|
34
|
+
logger.info(`Analyzing test health from state file: ${args.stateFile}${args.repository ? ` (repository: ${args.repository})` : ""}`);
|
|
35
|
+
// Load UnifiedAnalysisState for the requested repo section (primary when
|
|
36
|
+
// repository omitted/matches the root) from the run-scoped state file.
|
|
32
37
|
const stateManager = StateManager.fromStatePath(args.stateFile);
|
|
33
|
-
const stateData = await stateManager.
|
|
34
|
-
const
|
|
35
|
-
const repositoryPath = fullState?.metadata.repositoryPath || "";
|
|
38
|
+
const stateData = await stateManager.readRepoData(args.repository);
|
|
39
|
+
const repositoryPath = (await stateManager.getRepoRepositoryPath(args.repository)) || "";
|
|
36
40
|
if (!stateData) {
|
|
37
|
-
return toolError(`State file is empty or invalid: ${args.stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
41
|
+
return toolError(`State file is empty or invalid: ${args.stateFile}${args.repository ? ` (no section for repository ${args.repository})` : ""}. Call skyramp_analyze_changes first to generate a valid state file.`);
|
|
38
42
|
}
|
|
39
43
|
if (!repositoryPath || typeof repositoryPath !== "string") {
|
|
40
|
-
return toolError(`repositoryPath not found in state file metadata. The
|
|
44
|
+
return toolError(`repositoryPath not found in state file metadata. The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt. Re-run skyramp_analyze_changes to regenerate it.`);
|
|
45
|
+
}
|
|
46
|
+
if (!Array.isArray(stateData.existingTests)) {
|
|
47
|
+
return toolError(`stateFile does not contain test discovery data (existingTests missing). The stateFile parameter must be the analyze-changes-state.json path returned by skyramp_analyze_changes — not summaryOutputFile or testbot-result.txt.`);
|
|
41
48
|
}
|
|
42
49
|
// Skyramp tests: full drift analysis + all actions permitted.
|
|
43
50
|
// Relevant external tests (user-written, relevant to this PR's endpoints): drift analysis
|
|
@@ -7,6 +7,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
7
7
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
8
8
|
import { SKYRAMP_IMAGE_VERSION } from "../../utils/versions.js";
|
|
9
9
|
import { validateAndConsumeScanToken } from "./initScanWorkspaceTool.js";
|
|
10
|
+
import { upsertServicesByRepo } from "./serviceUpsert.js";
|
|
10
11
|
const require = createRequire(import.meta.url);
|
|
11
12
|
const MCP_VERSION = require("../../../package.json").version || "";
|
|
12
13
|
function getExecutorVersion() {
|
|
@@ -16,8 +17,8 @@ const TOOL_NAME = "skyramp_init_workspace";
|
|
|
16
17
|
// writeWorkspaceConfig() writes the YAML directly (rather than via
|
|
17
18
|
// WorkspaceConfigManager.addService) so all services are written in a single
|
|
18
19
|
// pass without N+1 reads. The workspace serviceSchema accepts the full authType
|
|
19
|
-
// set (incl. cookie/session/token)
|
|
20
|
-
// directly — no local schema extension needed.
|
|
20
|
+
// set (incl. cookie/session/token) and the multi-repo `repository` field, so input
|
|
21
|
+
// services are validated against it directly — no local schema extension needed.
|
|
21
22
|
const initializeWorkspaceSchema = z.object({
|
|
22
23
|
workspacePath: z
|
|
23
24
|
.string()
|
|
@@ -32,6 +33,10 @@ const initializeWorkspaceSchema = z.object({
|
|
|
32
33
|
.boolean()
|
|
33
34
|
.default(false)
|
|
34
35
|
.describe("Set to true to overwrite an existing workspace file. Use when the user explicitly requests it, or when recovering from a workspace validation failure (schema mismatch, unknown fields). Default is false."),
|
|
36
|
+
merge: z
|
|
37
|
+
.boolean()
|
|
38
|
+
.default(false)
|
|
39
|
+
.describe("Set to true to MERGE the provided services into an existing workspace.yml instead of overwriting it. The primary use is registering a related repository's services into one unified workspace file (each service carrying its `repository`), but it applies whenever you want to add services without discarding existing ones. Services are upserted by the composite (repository, serviceName), so same-named services in different repos coexist. Ignored on first init (no existing file). Default is false."),
|
|
35
40
|
});
|
|
36
41
|
/**
|
|
37
42
|
* Write a YAML workspace config to disk, bypassing the library's restricted
|
|
@@ -72,13 +77,17 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
72
77
|
try {
|
|
73
78
|
const workspacePath = params.workspacePath;
|
|
74
79
|
const manager = new WorkspaceConfigManager(workspacePath);
|
|
75
|
-
|
|
76
|
-
if
|
|
80
|
+
const alreadyExists = await manager.exists();
|
|
81
|
+
// Check if already initialized. In merge mode we proceed (read the
|
|
82
|
+
// existing file and upsert into it) instead of refusing — this is how a
|
|
83
|
+
// multi-repo run registers each additional repo's services into one
|
|
84
|
+
// unified workspace.yml without a per-repo file.
|
|
85
|
+
if (alreadyExists && !params.force && !params.merge) {
|
|
77
86
|
return {
|
|
78
87
|
content: [
|
|
79
88
|
{
|
|
80
89
|
type: "text",
|
|
81
|
-
text: `Workspace already initialized at ${manager.getConfigPath()}. Set force: true to overwrite.`,
|
|
90
|
+
text: `Workspace already initialized at ${manager.getConfigPath()}. Set force: true to overwrite, or merge: true to add services to it.`,
|
|
82
91
|
},
|
|
83
92
|
],
|
|
84
93
|
isError: false,
|
|
@@ -107,24 +116,30 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
107
116
|
isError: true,
|
|
108
117
|
};
|
|
109
118
|
}
|
|
110
|
-
// Initialize
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
119
|
+
// Initialize (fresh) or read (merge into existing). Merge mode preserves
|
|
120
|
+
// the existing primary repo identity + previously-registered services;
|
|
121
|
+
// initialize() would reset both, dropping other repos' services.
|
|
122
|
+
let config;
|
|
123
|
+
if (params.merge && alreadyExists) {
|
|
124
|
+
config = await manager.read();
|
|
125
|
+
config = await manager.updateMetadata({
|
|
126
|
+
mcpVersion: MCP_VERSION,
|
|
127
|
+
executorVersion: getExecutorVersion(),
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
config = await manager.initialize();
|
|
132
|
+
config = await manager.updateMetadata({
|
|
133
|
+
mcpVersion: MCP_VERSION,
|
|
134
|
+
executorVersion: getExecutorVersion(),
|
|
135
|
+
});
|
|
127
136
|
}
|
|
137
|
+
// Batch all service upserts: update in memory, write once (direct YAML
|
|
138
|
+
// write avoids N+1 reads and a mid-loop validation error). Upsert is keyed
|
|
139
|
+
// on the composite (repo, serviceName) — see upsertServicesByRepo — so a
|
|
140
|
+
// same-named service from an additional repo is a distinct entry, not an
|
|
141
|
+
// overwrite of the primary's.
|
|
142
|
+
const services = upsertServicesByRepo(config.services ?? [], params.services);
|
|
128
143
|
config.services = services;
|
|
129
144
|
if (config.metadata) {
|
|
130
145
|
config.metadata.updatedAt = new Date().toISOString();
|
|
@@ -142,6 +157,8 @@ export function registerInitializeWorkspaceTool(server) {
|
|
|
142
157
|
lines.push(`\nServices (${config.services.length}):`);
|
|
143
158
|
for (const svc of config.services) {
|
|
144
159
|
const parts = [svc.serviceName];
|
|
160
|
+
if (svc.repository)
|
|
161
|
+
parts.push(svc.repository);
|
|
145
162
|
if (svc.language)
|
|
146
163
|
parts.push(svc.language);
|
|
147
164
|
if (svc.framework)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Service } from "../../workspace/workspace.js";
|
|
2
|
+
/**
|
|
3
|
+
* Upsert `incoming` services into `existing`, keyed on the composite
|
|
4
|
+
* (repository, serviceName). A service's `repository` (absent → primary,
|
|
5
|
+
* normalized to "") is part of its identity, so a same-named service from a
|
|
6
|
+
* different repo is a distinct entry rather than an overwrite. Mutates and
|
|
7
|
+
* returns `existing`.
|
|
8
|
+
*
|
|
9
|
+
* Lives in its own module (no `import.meta`) so it can be unit-tested under
|
|
10
|
+
* ts-jest's CommonJS transform without loading the tool's ESM-only deps.
|
|
11
|
+
*/
|
|
12
|
+
export declare function upsertServicesByRepo(existing: Service[], incoming: Service[]): Service[];
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upsert `incoming` services into `existing`, keyed on the composite
|
|
3
|
+
* (repository, serviceName). A service's `repository` (absent → primary,
|
|
4
|
+
* normalized to "") is part of its identity, so a same-named service from a
|
|
5
|
+
* different repo is a distinct entry rather than an overwrite. Mutates and
|
|
6
|
+
* returns `existing`.
|
|
7
|
+
*
|
|
8
|
+
* Lives in its own module (no `import.meta`) so it can be unit-tested under
|
|
9
|
+
* ts-jest's CommonJS transform without loading the tool's ESM-only deps.
|
|
10
|
+
*/
|
|
11
|
+
export function upsertServicesByRepo(existing, incoming) {
|
|
12
|
+
for (const svc of incoming) {
|
|
13
|
+
const repoKey = svc.repository || "";
|
|
14
|
+
const idx = existing.findIndex((s) => (s.repository || "") === repoKey && s.serviceName === svc.serviceName);
|
|
15
|
+
if (idx >= 0) {
|
|
16
|
+
existing[idx] = svc;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
existing.push(svc);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return existing;
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { upsertServicesByRepo } from "./serviceUpsert.js";
|
|
2
|
+
// upsertServicesByRepo is the merge=true core: it upserts incoming services into
|
|
3
|
+
// the existing list keyed on the composite (repository, serviceName), so a
|
|
4
|
+
// same-named service from a different repo is a distinct entry rather than an
|
|
5
|
+
// overwrite. This is what makes the unified multi-repo workspace.yml correct.
|
|
6
|
+
describe("upsertServicesByRepo", () => {
|
|
7
|
+
const svc = (serviceName, repository, testDirectory) => ({ serviceName, repository, testDirectory });
|
|
8
|
+
it("keeps same-named services from different repos as distinct entries", () => {
|
|
9
|
+
const existing = [svc("api", undefined, "tests/primary")];
|
|
10
|
+
const result = upsertServicesByRepo(existing, [
|
|
11
|
+
svc("api", "letsramp/api-insight", "tests/py"),
|
|
12
|
+
]);
|
|
13
|
+
expect(result).toHaveLength(2);
|
|
14
|
+
expect(result.find((s) => !s.repository)?.testDirectory).toBe("tests/primary");
|
|
15
|
+
expect(result.find((s) => s.repository === "letsramp/api-insight")?.testDirectory).toBe("tests/py");
|
|
16
|
+
});
|
|
17
|
+
it("updates only the matching (repository, serviceName) entry in place", () => {
|
|
18
|
+
const existing = [
|
|
19
|
+
svc("api", undefined, "tests/primary"),
|
|
20
|
+
svc("api", "letsramp/api-insight", "tests/py"),
|
|
21
|
+
];
|
|
22
|
+
// Re-register the related repo's service with a new testDirectory.
|
|
23
|
+
const result = upsertServicesByRepo(existing, [
|
|
24
|
+
svc("api", "letsramp/api-insight", "tests/py-v2"),
|
|
25
|
+
]);
|
|
26
|
+
expect(result).toHaveLength(2);
|
|
27
|
+
// Primary untouched.
|
|
28
|
+
expect(result.find((s) => !s.repository)?.testDirectory).toBe("tests/primary");
|
|
29
|
+
// Related repo's entry updated in place (not duplicated).
|
|
30
|
+
const related = result.filter((s) => s.repository === "letsramp/api-insight");
|
|
31
|
+
expect(related).toHaveLength(1);
|
|
32
|
+
expect(related[0].testDirectory).toBe("tests/py-v2");
|
|
33
|
+
});
|
|
34
|
+
it("treats an absent repository as the primary (normalized to '')", () => {
|
|
35
|
+
const existing = [svc("api", undefined, "tests/primary")];
|
|
36
|
+
// A second primary write (no repository) upserts the SAME entry.
|
|
37
|
+
const result = upsertServicesByRepo(existing, [svc("api", undefined, "tests/primary-v2")]);
|
|
38
|
+
expect(result).toHaveLength(1);
|
|
39
|
+
expect(result[0].testDirectory).toBe("tests/primary-v2");
|
|
40
|
+
});
|
|
41
|
+
it("appends brand-new services from multiple repos", () => {
|
|
42
|
+
const result = upsertServicesByRepo([], [
|
|
43
|
+
svc("web", "letsramp/frontend", "tests/web"),
|
|
44
|
+
svc("api", "letsramp/backend", "tests/api"),
|
|
45
|
+
svc("api", undefined, "tests/primary"),
|
|
46
|
+
]);
|
|
47
|
+
expect(result).toHaveLength(3);
|
|
48
|
+
expect(new Set(result.map((s) => `${s.repository ?? ""}|${s.serviceName}`))).toEqual(new Set(["letsramp/frontend|web", "letsramp/backend|api", "|api"]));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -3,18 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These types define the structure for test execution results
|
|
5
5
|
*/
|
|
6
|
+
/** Execution outcome for a test run recorded in the maintenance report. */
|
|
7
|
+
export declare enum TestExecutionStatus {
|
|
8
|
+
Pass = "Pass",
|
|
9
|
+
Fail = "Fail",
|
|
10
|
+
Error = "Error",
|
|
11
|
+
/** Deliberately not executed — VERIFY/IGNORE actions or DELETE (file removed). */
|
|
12
|
+
Skipped = "Skipped",
|
|
13
|
+
/** Execution was expected (UPDATE/REGENERATE) but no result was recorded. */
|
|
14
|
+
Unknown = "Unknown"
|
|
15
|
+
}
|
|
6
16
|
export interface TestExecutionData {
|
|
7
|
-
|
|
17
|
+
status: TestExecutionStatus;
|
|
8
18
|
executedAt: string;
|
|
9
19
|
duration: number;
|
|
10
20
|
errors: string[];
|
|
11
21
|
warnings: string[];
|
|
12
|
-
crashed: boolean;
|
|
13
22
|
output?: string;
|
|
14
23
|
exitCode?: number;
|
|
15
24
|
}
|
|
16
25
|
export interface TestExecutionResult extends TestExecutionData {
|
|
17
26
|
testFile: string;
|
|
27
|
+
videoPath?: string;
|
|
18
28
|
}
|
|
19
29
|
export interface BatchExecutionResult {
|
|
20
30
|
totalTests: number;
|
|
@@ -3,4 +3,14 @@
|
|
|
3
3
|
*
|
|
4
4
|
* These types define the structure for test execution results
|
|
5
5
|
*/
|
|
6
|
-
|
|
6
|
+
/** Execution outcome for a test run recorded in the maintenance report. */
|
|
7
|
+
export var TestExecutionStatus;
|
|
8
|
+
(function (TestExecutionStatus) {
|
|
9
|
+
TestExecutionStatus["Pass"] = "Pass";
|
|
10
|
+
TestExecutionStatus["Fail"] = "Fail";
|
|
11
|
+
TestExecutionStatus["Error"] = "Error";
|
|
12
|
+
/** Deliberately not executed — VERIFY/IGNORE actions or DELETE (file removed). */
|
|
13
|
+
TestExecutionStatus["Skipped"] = "Skipped";
|
|
14
|
+
/** Execution was expected (UPDATE/REGENERATE) but no result was recorded. */
|
|
15
|
+
TestExecutionStatus["Unknown"] = "Unknown";
|
|
16
|
+
})(TestExecutionStatus || (TestExecutionStatus = {}));
|