@skyramp/mcp 0.2.6 → 0.2.150-rc.mntnc
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/commands/commandLibrary.d.ts +12 -0
- package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
- package/build/commands/testThisEndpointCommand.d.ts +2 -0
- package/build/index.d.ts +2 -0
- package/build/index.js +6 -4
- package/build/playwright/PlaywrightTraceService.d.ts +32 -0
- package/build/playwright/index.d.ts +3 -0
- package/build/playwright/registerPlaywrightTools.d.ts +8 -0
- package/build/playwright/registerPlaywrightTools.js +12 -11
- package/build/playwright/traceRecordingPrompt.d.ts +13 -0
- package/build/playwright/traceRecordingPrompt.js +3 -11
- package/build/prompts/code-reuse.d.ts +1 -0
- package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
- package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
- package/build/prompts/fix-error-prompt.d.ts +1 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
- package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
- package/build/prompts/personas.d.ts +21 -0
- package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
- package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
- 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/actionsInstructions.d.ts +35 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
- package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
- package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
- package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
- package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
- package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
- package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
- package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
- package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
- package/build/prompts/testbot/testbot-prompts.js +213 -42
- package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
- package/build/prompts/testbot/testbot-prompts.test.js +15 -117
- package/build/resources/analysisResources.d.ts +17 -0
- package/build/resources/progressResource.d.ts +2 -0
- package/build/resources/sutSetupResource.d.ts +2 -0
- package/build/resources/sutSetupResource.js +45 -0
- package/build/resources/testbotResource.d.ts +2 -0
- package/build/resources/testbotResource.js +1 -1
- package/build/services/AnalyticsService.d.ts +13 -0
- package/build/services/AnalyticsService.test.d.ts +1 -0
- package/build/services/AnalyticsService.test.js +86 -0
- package/build/services/ModularizationService.d.ts +11 -0
- package/build/services/ScenarioGenerationService.d.ts +37 -0
- package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
- package/build/services/ScenarioGenerationService.integration.test.js +4 -0
- package/build/services/ScenarioGenerationService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.d.ts +128 -0
- package/build/services/TestDiscoveryService.js +22 -12
- package/build/services/TestDiscoveryService.test.d.ts +1 -0
- package/build/services/TestDiscoveryService.test.js +4 -3
- package/build/services/TestExecutionService.d.ts +55 -0
- package/build/services/TestExecutionService.js +158 -26
- package/build/services/TestExecutionService.test.d.ts +1 -0
- package/build/services/TestExecutionService.test.js +339 -25
- package/build/services/TestGenerationService.d.ts +56 -0
- package/build/services/TestGenerationService.test.d.ts +1 -0
- package/build/services/TestGenerationService.test.js +10 -8
- package/build/services/containerEnv.d.ts +14 -0
- package/build/tool-phase-coverage.test.d.ts +1 -0
- package/build/tool-phase-coverage.test.js +2 -0
- package/build/tool-phases.d.ts +38 -0
- package/build/tools/auth/loginTool.d.ts +2 -0
- package/build/tools/auth/logoutTool.d.ts +2 -0
- package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
- package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
- package/build/tools/executeSkyrampTestTool.d.ts +2 -0
- package/build/tools/executeSkyrampTestTool.js +17 -4
- package/build/tools/fixErrorTool.d.ts +2 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
- package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
- package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
- package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
- package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
- package/build/tools/generate-tests/generateUIRestTool.js +1 -1
- package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
- package/build/tools/one-click/oneClickTool.d.ts +11 -0
- package/build/tools/submitReportTool.d.ts +384 -0
- package/build/tools/submitReportTool.js +225 -37
- package/build/tools/submitReportTool.test.d.ts +1 -0
- package/build/tools/submitReportTool.test.js +479 -55
- package/build/tools/test-management/actionsTool.d.ts +10 -0
- package/build/tools/test-management/actionsTool.js +10 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +26 -0
- package/build/tools/test-management/analyzeChangesTool.js +128 -48
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeChangesTool.test.js +80 -73
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
- package/build/tools/test-management/index.d.ts +3 -0
- package/build/tools/test-management/index.js +0 -1
- package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
- package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
- package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
- package/build/tools/trace/sessionState.d.ts +2 -0
- package/build/tools/trace/sessionState.test.d.ts +1 -0
- package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
- package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
- package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
- package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
- package/build/types/OneClickCommands.d.ts +68 -0
- package/build/types/RepositoryAnalysis.d.ts +3004 -0
- package/build/types/TestAnalysis.d.ts +97 -0
- package/build/types/TestExecution.d.ts +63 -0
- package/build/types/TestExecution.js +11 -1
- package/build/types/TestRecommendation.d.ts +12 -0
- package/build/types/TestTypes.d.ts +234 -0
- 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 +182 -0
- package/build/utils/AnalysisStateManager.js +10 -1
- package/build/utils/AnalysisStateManager.test.d.ts +1 -0
- package/build/utils/analyze-openapi.d.ts +1 -0
- package/build/utils/branchDiff.d.ts +18 -0
- package/build/utils/dartRouteExtractor.d.ts +45 -0
- package/build/utils/dartRouteExtractor.test.d.ts +1 -0
- package/build/utils/docker.d.ts +19 -0
- package/build/utils/docker.test.d.ts +1 -0
- package/build/utils/docker.test.js +10 -9
- package/build/utils/featureFlags.d.ts +37 -0
- package/build/utils/featureFlags.test.d.ts +1 -0
- package/build/utils/gitStaging.d.ts +24 -0
- package/build/utils/gitStaging.test.d.ts +1 -0
- package/build/utils/gitStaging.test.js +13 -7
- package/build/utils/httpDefaults.d.ts +10 -0
- package/build/utils/httpDefaults.test.d.ts +1 -0
- package/build/utils/initAgent.d.ts +36 -0
- package/build/utils/language-helper.d.ts +6 -0
- package/build/utils/logger.d.ts +11 -0
- package/build/utils/normalizeParams.d.ts +14 -0
- package/build/utils/normalizeSkyrampImports.d.ts +13 -0
- package/build/utils/pr-comment-parser.d.ts +31 -0
- package/build/utils/pr-comment-parser.test.d.ts +1 -0
- package/build/utils/pr-comment-parser.test.js +6 -5
- package/build/utils/projectMetadata.d.ts +15 -0
- package/build/utils/projectMetadata.test.d.ts +1 -0
- package/build/utils/proxy-terminal.d.ts +2 -0
- package/build/utils/repoScanner.d.ts +27 -0
- package/build/utils/repoScanner.test.d.ts +1 -0
- package/build/utils/routeParsers.d.ts +53 -0
- package/build/utils/routeParsers.js +31 -38
- package/build/utils/routeParsers.test.d.ts +1 -0
- package/build/utils/scenarioDrafting.d.ts +71 -0
- package/build/utils/scenarioDrafting.js +23 -1
- package/build/utils/scenarioDrafting.test.d.ts +1 -0
- package/build/utils/skyrampMdContent.d.ts +5 -0
- package/build/utils/sourceRouteExtractor.d.ts +48 -0
- package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
- package/build/utils/telemetry.d.ts +32 -0
- package/build/utils/telemetry.test.d.ts +1 -0
- package/build/utils/trace-parser.d.ts +32 -0
- package/build/utils/trace-parser.test.d.ts +6 -0
- package/build/utils/uiPageEnumerator.d.ts +146 -0
- package/build/utils/uiPageEnumerator.js +58 -14
- package/build/utils/uiPageEnumerator.test.d.ts +1 -0
- package/build/utils/uiPageEnumerator.test.js +223 -43
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +10 -0
- package/build/utils/utils.test.d.ts +1 -0
- package/build/utils/versions.d.ts +3 -0
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +127 -0
- package/build/utils/workspaceAuth.test.d.ts +1 -0
- package/build/utils/workspaceAuth.test.js +25 -25
- package/build/workspace/index.d.ts +1 -0
- package/build/workspace/workspace.d.ts +390 -0
- package/build/workspace/workspace.test.d.ts +1 -0
- package/node_modules/playwright/LICENSE +202 -0
- package/node_modules/playwright/NOTICE +5 -0
- package/node_modules/playwright/README.md +168 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
- package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
- package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.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.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +16 -4
- package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
- package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- 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/traceViewer/index.CP3Y8Zsb.js +0 -2
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
// @ts-
|
|
2
|
-
import {
|
|
1
|
+
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
+
import { jest } from "@jest/globals";
|
|
3
3
|
import { TestType } from "../types/TestTypes.js";
|
|
4
4
|
import { DriftAction } from "../types/TestAnalysis.js";
|
|
5
|
-
import {
|
|
5
|
+
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
6
6
|
import * as fs from "fs/promises";
|
|
7
7
|
import * as path from "path";
|
|
8
8
|
import * as os from "os";
|
|
9
|
-
jest.
|
|
10
|
-
logger: { info: jest.fn(), error: jest.fn() },
|
|
9
|
+
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
10
|
+
logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
|
|
11
11
|
}));
|
|
12
|
-
jest.
|
|
12
|
+
jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
|
|
13
13
|
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
14
14
|
}));
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
const { registerSubmitReportTool, additionalRecommendationSchema, newTestSchema, targetElementSchema, pageContextSchema } = await import("./submitReportTool.js");
|
|
17
|
+
const { AnalyticsService } = await import("../services/AnalyticsService.js");
|
|
15
18
|
const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
|
|
16
19
|
function captureToolHandler() {
|
|
17
20
|
let handler;
|
|
@@ -23,14 +26,20 @@ function captureToolHandler() {
|
|
|
23
26
|
registerSubmitReportTool(fakeServer);
|
|
24
27
|
return handler;
|
|
25
28
|
}
|
|
26
|
-
function
|
|
29
|
+
async function writeStateFile(dir, existingTests) {
|
|
30
|
+
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
31
|
+
await fs.writeFile(stateFile, JSON.stringify({ existingTests }), "utf-8");
|
|
32
|
+
return stateFile;
|
|
33
|
+
}
|
|
34
|
+
function sampleReportParams(outputFile, stateFile) {
|
|
27
35
|
return {
|
|
28
36
|
summaryOutputFile: outputFile,
|
|
37
|
+
stateFile,
|
|
29
38
|
businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
|
|
30
39
|
newTestsCreated: [
|
|
31
40
|
{ testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", reasoning: "Validates product search returns correct results for category filter" },
|
|
32
41
|
],
|
|
33
|
-
testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests",
|
|
42
|
+
testMaintenance: [{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Updated auth flow in existing tests", beforeDetails: "", afterDetails: "" }],
|
|
34
43
|
testResults: [
|
|
35
44
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
|
|
36
45
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
|
|
@@ -65,7 +74,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
65
74
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
66
75
|
tmpDirs.push(tmpDir);
|
|
67
76
|
const outputFile = path.join(tmpDir, "report.json");
|
|
68
|
-
const
|
|
77
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
78
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
69
79
|
expect(result.isError).toBeUndefined();
|
|
70
80
|
expect(result.content[0].text).toContain("Report submitted successfully");
|
|
71
81
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
@@ -79,7 +89,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
79
89
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
80
90
|
tmpDirs.push(tmpDir);
|
|
81
91
|
const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
|
|
82
|
-
const
|
|
92
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
83
94
|
expect(result.isError).toBeUndefined();
|
|
84
95
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
85
96
|
expect(written.businessCaseAnalysis).toBeDefined();
|
|
@@ -88,11 +99,12 @@ describe("registerSubmitReportTool", () => {
|
|
|
88
99
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
89
100
|
tmpDirs.push(tmpDir);
|
|
90
101
|
const outputFile = path.join(tmpDir, "report.json");
|
|
102
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
91
103
|
const result = await handler({
|
|
92
104
|
summaryOutputFile: outputFile,
|
|
105
|
+
stateFile,
|
|
93
106
|
businessCaseAnalysis: "Simple config change.",
|
|
94
107
|
newTestsCreated: [],
|
|
95
|
-
testMaintenance: [],
|
|
96
108
|
testResults: [
|
|
97
109
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
|
|
98
110
|
],
|
|
@@ -101,12 +113,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
101
113
|
expect(result.isError).toBeUndefined();
|
|
102
114
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
103
115
|
expect(written.newTestsCreated).toEqual([]);
|
|
104
|
-
expect(written.testMaintenance).
|
|
116
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
105
117
|
expect(written.issuesFound).toEqual([]);
|
|
106
118
|
expect(written.testResults).toHaveLength(1);
|
|
107
119
|
});
|
|
108
120
|
it("should return error for invalid file path", async () => {
|
|
109
|
-
const
|
|
121
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
122
|
+
tmpDirs.push(tmpDir);
|
|
123
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
124
|
+
const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
|
|
110
125
|
expect(result.isError).toBe(true);
|
|
111
126
|
expect(result.content[0].text).toContain("Failed to write report");
|
|
112
127
|
});
|
|
@@ -114,8 +129,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
114
129
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
115
130
|
tmpDirs.push(tmpDir);
|
|
116
131
|
const outputFile = path.join(tmpDir, "report.json");
|
|
132
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
117
133
|
const result = await handler({
|
|
118
|
-
...sampleReportParams(outputFile),
|
|
134
|
+
...sampleReportParams(outputFile, stateFile),
|
|
119
135
|
commitMessage: "add contract tests for /products endpoint",
|
|
120
136
|
});
|
|
121
137
|
expect(result.isError).toBeUndefined();
|
|
@@ -126,7 +142,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
126
142
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
127
143
|
tmpDirs.push(tmpDir);
|
|
128
144
|
const outputFile = path.join(tmpDir, "report.json");
|
|
129
|
-
const
|
|
145
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
146
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
130
147
|
expect(result.isError).toBeUndefined();
|
|
131
148
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
132
149
|
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
@@ -135,8 +152,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
135
152
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
136
153
|
tmpDirs.push(tmpDir);
|
|
137
154
|
const outputFile = path.join(tmpDir, "report.json");
|
|
155
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
138
156
|
const result = await handler({
|
|
139
|
-
...sampleReportParams(outputFile),
|
|
157
|
+
...sampleReportParams(outputFile, stateFile),
|
|
140
158
|
commitMessage: " line one\nline two\r\nline three ",
|
|
141
159
|
});
|
|
142
160
|
expect(result.isError).toBeUndefined();
|
|
@@ -148,9 +166,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
148
166
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
149
167
|
tmpDirs.push(tmpDir);
|
|
150
168
|
const outputFile = path.join(tmpDir, "report.json");
|
|
169
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
151
170
|
const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
|
|
152
171
|
const result = await handler({
|
|
153
|
-
...sampleReportParams(outputFile),
|
|
172
|
+
...sampleReportParams(outputFile, stateFile),
|
|
154
173
|
commitMessage: longMessage,
|
|
155
174
|
});
|
|
156
175
|
expect(result.isError).toBeUndefined();
|
|
@@ -158,24 +177,13 @@ describe("registerSubmitReportTool", () => {
|
|
|
158
177
|
expect(written.commitMessage).toBe(longMessage);
|
|
159
178
|
expect(written.commitMessage.length).toBeGreaterThan(72);
|
|
160
179
|
});
|
|
161
|
-
it("should use default commitMessage when provided as empty string", async () => {
|
|
162
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
163
|
-
tmpDirs.push(tmpDir);
|
|
164
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
165
|
-
const result = await handler({
|
|
166
|
-
...sampleReportParams(outputFile),
|
|
167
|
-
commitMessage: "",
|
|
168
|
-
});
|
|
169
|
-
expect(result.isError).toBeUndefined();
|
|
170
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
171
|
-
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
172
|
-
});
|
|
173
180
|
it("should use default commitMessage when whitespace-only", async () => {
|
|
174
181
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
175
182
|
tmpDirs.push(tmpDir);
|
|
176
183
|
const outputFile = path.join(tmpDir, "report.json");
|
|
184
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
177
185
|
const result = await handler({
|
|
178
|
-
...sampleReportParams(outputFile),
|
|
186
|
+
...sampleReportParams(outputFile, stateFile),
|
|
179
187
|
commitMessage: " \n\r\n ",
|
|
180
188
|
});
|
|
181
189
|
expect(result.isError).toBeUndefined();
|
|
@@ -186,8 +194,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
186
194
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
187
195
|
tmpDirs.push(tmpDir);
|
|
188
196
|
const outputFile = path.join(tmpDir, "report.json");
|
|
197
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
189
198
|
const result = await handler({
|
|
190
|
-
...sampleReportParams(outputFile),
|
|
199
|
+
...sampleReportParams(outputFile, stateFile),
|
|
191
200
|
nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
|
|
192
201
|
});
|
|
193
202
|
expect(result.isError).toBeUndefined();
|
|
@@ -200,7 +209,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
200
209
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
201
210
|
tmpDirs.push(tmpDir);
|
|
202
211
|
const outputFile = path.join(tmpDir, "report.json");
|
|
203
|
-
const
|
|
212
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
213
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
204
214
|
expect(result.isError).toBeUndefined();
|
|
205
215
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
206
216
|
expect(written.nextSteps).toEqual([]);
|
|
@@ -209,7 +219,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
209
219
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
210
220
|
tmpDirs.push(tmpDir);
|
|
211
221
|
const outputFile = path.join(tmpDir, "report.json");
|
|
212
|
-
await
|
|
222
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
223
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
213
224
|
const raw = await fs.readFile(outputFile, "utf-8");
|
|
214
225
|
// Pretty-printed JSON should have indentation
|
|
215
226
|
expect(raw).toContain(" ");
|
|
@@ -220,8 +231,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
220
231
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
221
232
|
tmpDirs.push(tmpDir);
|
|
222
233
|
const outputFile = path.join(tmpDir, "report.json");
|
|
234
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
223
235
|
const params = {
|
|
224
|
-
...sampleReportParams(outputFile),
|
|
236
|
+
...sampleReportParams(outputFile, stateFile),
|
|
225
237
|
additionalRecommendations: [
|
|
226
238
|
{
|
|
227
239
|
testId: "integration-orders-patch-status",
|
|
@@ -293,6 +305,214 @@ describe("registerSubmitReportTool", () => {
|
|
|
293
305
|
expect(result.data.primaryEndpoint).toBeUndefined();
|
|
294
306
|
}
|
|
295
307
|
});
|
|
308
|
+
// Structured grounding fields for UI recommendations.
|
|
309
|
+
// ----------------------------------------------------------------------
|
|
310
|
+
// Sub-schemas — targetElement and pageContext — populated by the agent
|
|
311
|
+
// from a captured browser_blueprint element. UI-only via superRefine on
|
|
312
|
+
// the parent newTestSchema / additionalRecommendationSchema.
|
|
313
|
+
describe("targetElementSchema", () => {
|
|
314
|
+
const validTarget = {
|
|
315
|
+
role: "button",
|
|
316
|
+
accessibleName: "Save changes",
|
|
317
|
+
testId: "save-changes-btn",
|
|
318
|
+
stableId: null,
|
|
319
|
+
contextText: null,
|
|
320
|
+
mutability: "mutable",
|
|
321
|
+
widgetType: "native",
|
|
322
|
+
};
|
|
323
|
+
it("accepts a fully-populated targetElement", () => {
|
|
324
|
+
expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
it("accepts a minimal targetElement (only required fields)", () => {
|
|
327
|
+
const result = targetElementSchema.safeParse({
|
|
328
|
+
role: "heading",
|
|
329
|
+
accessibleName: "Notifications",
|
|
330
|
+
testId: null,
|
|
331
|
+
stableId: null,
|
|
332
|
+
contextText: null,
|
|
333
|
+
});
|
|
334
|
+
expect(result.success).toBe(true);
|
|
335
|
+
});
|
|
336
|
+
it("accepts contextText as a string array (repeating-row case)", () => {
|
|
337
|
+
const result = targetElementSchema.safeParse({
|
|
338
|
+
...validTarget,
|
|
339
|
+
contextText: ["customer@example.com", "$129.99", "Pending"],
|
|
340
|
+
});
|
|
341
|
+
expect(result.success).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
it("rejects targetElement missing accessibleName", () => {
|
|
344
|
+
const { accessibleName, ...withoutName } = validTarget;
|
|
345
|
+
const result = targetElementSchema.safeParse(withoutName);
|
|
346
|
+
expect(result.success).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
it("rejects targetElement missing role", () => {
|
|
349
|
+
const { role, ...withoutRole } = validTarget;
|
|
350
|
+
const result = targetElementSchema.safeParse(withoutRole);
|
|
351
|
+
expect(result.success).toBe(false);
|
|
352
|
+
});
|
|
353
|
+
it("rejects an invalid mutability enum value", () => {
|
|
354
|
+
const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
|
|
355
|
+
expect(result.success).toBe(false);
|
|
356
|
+
});
|
|
357
|
+
it("rejects an invalid widgetType enum value", () => {
|
|
358
|
+
const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
|
|
359
|
+
expect(result.success).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
describe("pageContextSchema (UI recommendation grounding)", () => {
|
|
363
|
+
it("accepts pageContext with both url and pageHash", () => {
|
|
364
|
+
const result = pageContextSchema.safeParse({
|
|
365
|
+
url: "http://localhost:5173/orders/1",
|
|
366
|
+
pageHash: "10:434266447",
|
|
367
|
+
});
|
|
368
|
+
expect(result.success).toBe(true);
|
|
369
|
+
});
|
|
370
|
+
it("accepts pageContext without pageHash (optional)", () => {
|
|
371
|
+
const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
|
|
372
|
+
expect(result.success).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
it("rejects pageContext missing url", () => {
|
|
375
|
+
const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
|
|
376
|
+
expect(result.success).toBe(false);
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
|
|
380
|
+
const validTarget = {
|
|
381
|
+
role: "button",
|
|
382
|
+
accessibleName: "Save changes",
|
|
383
|
+
testId: "save-changes-btn",
|
|
384
|
+
stableId: null,
|
|
385
|
+
contextText: null,
|
|
386
|
+
};
|
|
387
|
+
const validPage = {
|
|
388
|
+
url: "http://localhost:5173/orders/1",
|
|
389
|
+
pageHash: "10:434266447",
|
|
390
|
+
};
|
|
391
|
+
const baseUITest = {
|
|
392
|
+
testId: "ui-edit-order-discount-validation",
|
|
393
|
+
testType: TestType.UI,
|
|
394
|
+
category: "business_rule",
|
|
395
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
396
|
+
fileName: "edit_order_discount.spec.ts",
|
|
397
|
+
description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
|
|
398
|
+
reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
399
|
+
};
|
|
400
|
+
it("accepts a UI test with single-element targetElements + pageContext", () => {
|
|
401
|
+
const result = newTestSchema.safeParse({
|
|
402
|
+
...baseUITest,
|
|
403
|
+
targetElements: [validTarget],
|
|
404
|
+
pageContext: validPage,
|
|
405
|
+
});
|
|
406
|
+
expect(result.success).toBe(true);
|
|
407
|
+
});
|
|
408
|
+
it("accepts a UI test with multi-element targetElements (render-state test)", () => {
|
|
409
|
+
const result = newTestSchema.safeParse({
|
|
410
|
+
...baseUITest,
|
|
411
|
+
targetElements: [
|
|
412
|
+
{
|
|
413
|
+
role: "heading",
|
|
414
|
+
accessibleName: "Edit Order",
|
|
415
|
+
testId: "edit-order-heading",
|
|
416
|
+
stableId: null,
|
|
417
|
+
contextText: null,
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
role: "spinbutton",
|
|
421
|
+
accessibleName: "Discount (%)",
|
|
422
|
+
testId: "edit-order-input-discount",
|
|
423
|
+
stableId: null,
|
|
424
|
+
contextText: null,
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
pageContext: validPage,
|
|
428
|
+
});
|
|
429
|
+
expect(result.success).toBe(true);
|
|
430
|
+
});
|
|
431
|
+
it("rejects a UI test without targetElements (required for UI grounding)", () => {
|
|
432
|
+
const result = newTestSchema.safeParse(baseUITest);
|
|
433
|
+
expect(result.success).toBe(false);
|
|
434
|
+
expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
|
|
435
|
+
});
|
|
436
|
+
it("accepts a UI test with targetElements: null (fallback case)", () => {
|
|
437
|
+
const result = newTestSchema.safeParse({
|
|
438
|
+
...baseUITest,
|
|
439
|
+
targetElements: null,
|
|
440
|
+
description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
|
|
441
|
+
reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
|
|
442
|
+
});
|
|
443
|
+
expect(result.success).toBe(true);
|
|
444
|
+
});
|
|
445
|
+
it("rejects a contract test that includes targetElements (UI-only)", () => {
|
|
446
|
+
const result = newTestSchema.safeParse({
|
|
447
|
+
testId: "contract-orders-patch",
|
|
448
|
+
testType: TestType.CONTRACT,
|
|
449
|
+
category: "business_rule",
|
|
450
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
451
|
+
fileName: "orders_patch_contract.py",
|
|
452
|
+
reasoning: "Validates request shape and 200/422 status codes",
|
|
453
|
+
targetElements: [validTarget],
|
|
454
|
+
});
|
|
455
|
+
expect(result.success).toBe(false);
|
|
456
|
+
if (!result.success) {
|
|
457
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
it("rejects an integration test that includes pageContext (UI-only)", () => {
|
|
461
|
+
const result = newTestSchema.safeParse({
|
|
462
|
+
testId: "integration-orders",
|
|
463
|
+
testType: TestType.INTEGRATION,
|
|
464
|
+
category: "business_rule",
|
|
465
|
+
endpoint: "POST /api/v1/orders",
|
|
466
|
+
fileName: "orders_integration.py",
|
|
467
|
+
reasoning: "Multi-step order lifecycle",
|
|
468
|
+
pageContext: validPage,
|
|
469
|
+
});
|
|
470
|
+
expect(result.success).toBe(false);
|
|
471
|
+
if (!result.success) {
|
|
472
|
+
expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
});
|
|
476
|
+
describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
|
|
477
|
+
const validTarget = {
|
|
478
|
+
role: "button",
|
|
479
|
+
accessibleName: "Save changes",
|
|
480
|
+
testId: "save-changes-btn",
|
|
481
|
+
stableId: null,
|
|
482
|
+
contextText: null,
|
|
483
|
+
};
|
|
484
|
+
const validPage = { url: "http://localhost:5173/orders/1" };
|
|
485
|
+
it("accepts a UI rec with targetElements + pageContext", () => {
|
|
486
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
487
|
+
testId: "ui-edit-order-discount-validation",
|
|
488
|
+
testType: TestType.UI,
|
|
489
|
+
category: "business_rule",
|
|
490
|
+
steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
|
|
491
|
+
description: "Verify discount > 100 shows validation error",
|
|
492
|
+
priority: "medium",
|
|
493
|
+
reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
494
|
+
targetElements: [validTarget],
|
|
495
|
+
pageContext: validPage,
|
|
496
|
+
});
|
|
497
|
+
expect(result.success).toBe(true);
|
|
498
|
+
});
|
|
499
|
+
it("rejects a non-UI rec that includes targetElements", () => {
|
|
500
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
501
|
+
testId: "integration-orders-patch",
|
|
502
|
+
testType: TestType.INTEGRATION,
|
|
503
|
+
category: "business_rule",
|
|
504
|
+
steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
|
|
505
|
+
description: "Verifies PATCH workflow",
|
|
506
|
+
priority: "high",
|
|
507
|
+
reasoning: "Catches mutation bugs",
|
|
508
|
+
targetElements: [validTarget],
|
|
509
|
+
});
|
|
510
|
+
expect(result.success).toBe(false);
|
|
511
|
+
if (!result.success) {
|
|
512
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
});
|
|
296
516
|
describe("analytics metrics", () => {
|
|
297
517
|
beforeEach(() => {
|
|
298
518
|
pushMCPToolEventMock.mockClear();
|
|
@@ -306,14 +526,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
306
526
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
307
527
|
tmpDirs.push(tmpDir);
|
|
308
528
|
const outputFile = path.join(tmpDir, "report.json");
|
|
309
|
-
// sampleReportParams: 1 new test, 1 maintenance (
|
|
529
|
+
// sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
|
|
310
530
|
// (1 Pass + 1 Fail), 1 issue (no severity).
|
|
311
|
-
await
|
|
531
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
532
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
312
533
|
const params = lastAnalyticsParams();
|
|
313
534
|
expect(params.testsGenerated).toBe("1");
|
|
314
535
|
expect(params.testsMaintained).toBe("1");
|
|
315
536
|
expect(params.recommendationsCount).toBe("0");
|
|
316
|
-
expect(params.maintenanceRecovered).toBe("
|
|
537
|
+
expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
|
|
317
538
|
expect(params.testsPassed).toBe("1");
|
|
318
539
|
expect(params.testsFailed).toBe("1");
|
|
319
540
|
expect(params.testsSkipped).toBe("0");
|
|
@@ -323,8 +544,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
323
544
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
324
545
|
tmpDirs.push(tmpDir);
|
|
325
546
|
const outputFile = path.join(tmpDir, "report.json");
|
|
547
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
326
548
|
await handler({
|
|
327
|
-
...sampleReportParams(outputFile),
|
|
549
|
+
...sampleReportParams(outputFile, stateFile),
|
|
328
550
|
issuesFound: [
|
|
329
551
|
{ description: "Data corruption on checkout", severity: "critical" },
|
|
330
552
|
{ description: "Wrong total returned", severity: "high" },
|
|
@@ -344,8 +566,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
344
566
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
345
567
|
tmpDirs.push(tmpDir);
|
|
346
568
|
const outputFile = path.join(tmpDir, "report.json");
|
|
569
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
347
570
|
await handler({
|
|
348
571
|
summaryOutputFile: outputFile,
|
|
572
|
+
stateFile,
|
|
349
573
|
businessCaseAnalysis: "Config-only change.",
|
|
350
574
|
newTestsCreated: [],
|
|
351
575
|
testMaintenance: [],
|
|
@@ -363,36 +587,236 @@ describe("registerSubmitReportTool", () => {
|
|
|
363
587
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
364
588
|
tmpDirs.push(tmpDir);
|
|
365
589
|
const outputFile = path.join(tmpDir, "report.json");
|
|
590
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
366
591
|
await handler({
|
|
367
|
-
...sampleReportParams(outputFile),
|
|
592
|
+
...sampleReportParams(outputFile, stateFile),
|
|
368
593
|
testMaintenance: [
|
|
369
594
|
// Real change
|
|
370
|
-
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth",
|
|
371
|
-
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape",
|
|
595
|
+
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth", beforeDetails: "", afterDetails: "" },
|
|
596
|
+
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeDetails: "", afterDetails: "" },
|
|
372
597
|
// No-ops — assessed but not modified
|
|
373
|
-
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required",
|
|
374
|
-
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid",
|
|
598
|
+
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required", beforeDetails: "", afterDetails: "" },
|
|
599
|
+
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
375
600
|
],
|
|
376
601
|
});
|
|
377
602
|
const params = lastAnalyticsParams();
|
|
378
603
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
379
604
|
expect(params.testsMaintained).toBe("2");
|
|
380
|
-
//
|
|
381
|
-
expect(params.maintenanceRecovered).toBe("
|
|
605
|
+
// No matching stateFile entries — execution data unavailable
|
|
606
|
+
expect(params.maintenanceRecovered).toBe("0");
|
|
382
607
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
608
|
+
});
|
|
609
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
610
|
+
// testMaintenance output shapes
|
|
611
|
+
// Each case documents exactly what the MCP writes to JSON, which is what the
|
|
612
|
+
// testbot renderer reads. Keeping these in sync prevents silent regressions
|
|
613
|
+
// when toOutput or the stateFile enrichment path changes.
|
|
614
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
615
|
+
describe("testMaintenance output shapes", () => {
|
|
616
|
+
const baseMaintenance = {
|
|
617
|
+
testType: TestType.CONTRACT,
|
|
618
|
+
endpoint: "GET /api/v1/orders",
|
|
619
|
+
fileName: "orders_contract_test.py",
|
|
620
|
+
description: "Updated discount fields in assertions",
|
|
621
|
+
beforeDetails: "",
|
|
622
|
+
afterDetails: "",
|
|
623
|
+
};
|
|
624
|
+
async function writeAndRead(tmpDir, maintenance, extra) {
|
|
386
625
|
const outputFile = path.join(tmpDir, "report.json");
|
|
387
626
|
await handler({
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
],
|
|
627
|
+
summaryOutputFile: outputFile,
|
|
628
|
+
businessCaseAnalysis: "Test",
|
|
629
|
+
newTestsCreated: [],
|
|
630
|
+
testMaintenance: maintenance,
|
|
631
|
+
testResults: [],
|
|
632
|
+
issuesFound: [],
|
|
633
|
+
...extra,
|
|
393
634
|
});
|
|
394
|
-
const
|
|
395
|
-
|
|
635
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
636
|
+
return written.testMaintenance;
|
|
637
|
+
}
|
|
638
|
+
// ── No matching stateFile entry ───────────────────────────────────────────
|
|
639
|
+
it("UPDATE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
640
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
641
|
+
tmpDirs.push(tmpDir);
|
|
642
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
643
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
644
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
645
|
+
expect(result[0].afterStatus).toBe("Unknown");
|
|
646
|
+
expect(result[0].beforeDetails).toBe("");
|
|
647
|
+
expect(result[0].afterDetails).toBe("");
|
|
648
|
+
});
|
|
649
|
+
it("IGNORE with no matching stateFile entry: both Skipped", async () => {
|
|
650
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
651
|
+
tmpDirs.push(tmpDir);
|
|
652
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
653
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Ignore }], { stateFile });
|
|
654
|
+
expect(result[0].beforeStatus).toBe("Skipped");
|
|
655
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
656
|
+
expect(result[0].beforeDetails).toBe("");
|
|
657
|
+
expect(result[0].afterDetails).toBe("");
|
|
658
|
+
});
|
|
659
|
+
it("DELETE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
660
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
661
|
+
tmpDirs.push(tmpDir);
|
|
662
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
663
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
664
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
665
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
666
|
+
});
|
|
667
|
+
// ── With matching stateFile entry ─────────────────────────────────────────
|
|
668
|
+
it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
|
|
669
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
670
|
+
tmpDirs.push(tmpDir);
|
|
671
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
672
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
673
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
674
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
675
|
+
}]);
|
|
676
|
+
const result = await writeAndRead(tmpDir, [{
|
|
677
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
678
|
+
beforeDetails: "4 passed in 15.09s",
|
|
679
|
+
afterDetails: "5 passed in 12.88s",
|
|
680
|
+
}], { stateFile });
|
|
681
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
682
|
+
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
683
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
684
|
+
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
685
|
+
});
|
|
686
|
+
it("UPDATE with before=Pass after=Fail: status from stateFile, details from LLM", async () => {
|
|
687
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
688
|
+
tmpDirs.push(tmpDir);
|
|
689
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
690
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
691
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
692
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
693
|
+
}]);
|
|
694
|
+
const result = await writeAndRead(tmpDir, [{
|
|
695
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
696
|
+
beforeDetails: "4 passed in 15.09s",
|
|
697
|
+
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
698
|
+
}], { stateFile });
|
|
699
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
700
|
+
expect(result[0].afterStatus).toBe("Fail");
|
|
701
|
+
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
702
|
+
});
|
|
703
|
+
it("UPDATE with crashed execution: status is Error", async () => {
|
|
704
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
705
|
+
tmpDirs.push(tmpDir);
|
|
706
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
707
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
708
|
+
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
709
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
710
|
+
}]);
|
|
711
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
712
|
+
expect(result[0].beforeStatus).toBe("Error");
|
|
713
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
714
|
+
});
|
|
715
|
+
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
716
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
717
|
+
tmpDirs.push(tmpDir);
|
|
718
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
719
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
720
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
721
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
722
|
+
}]);
|
|
723
|
+
await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
724
|
+
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
725
|
+
});
|
|
726
|
+
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
|
|
727
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
728
|
+
tmpDirs.push(tmpDir);
|
|
729
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
730
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
731
|
+
// executionBefore absent — agent skipped before-phase
|
|
732
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
733
|
+
}]);
|
|
734
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
735
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
736
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
737
|
+
});
|
|
738
|
+
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
|
|
739
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
740
|
+
tmpDirs.push(tmpDir);
|
|
741
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
742
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
743
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
744
|
+
// executionAfter absent — file was deleted, nothing to run
|
|
745
|
+
}]);
|
|
746
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
747
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
748
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
749
|
+
});
|
|
750
|
+
it("fileName matched by suffix: absolute stateFile path vs relative fileName", async () => {
|
|
751
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
752
|
+
tmpDirs.push(tmpDir);
|
|
753
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
754
|
+
testFile: "/repo/backend/tests/python/orders_contract_test.py",
|
|
755
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15000 },
|
|
756
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12000 },
|
|
757
|
+
}]);
|
|
758
|
+
// fileName is relative — should still match via endsWith
|
|
759
|
+
const result = await writeAndRead(tmpDir, [{
|
|
760
|
+
...baseMaintenance,
|
|
761
|
+
action: DriftAction.Update,
|
|
762
|
+
fileName: "orders_contract_test.py",
|
|
763
|
+
}], { stateFile });
|
|
764
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
765
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
it("testResult accepts videoPath field (optional)", async () => {
|
|
769
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
770
|
+
tmpDirs.push(tmpDir);
|
|
771
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
772
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
773
|
+
const result = await handler({
|
|
774
|
+
summaryOutputFile: outputFile,
|
|
775
|
+
stateFile,
|
|
776
|
+
businessCaseAnalysis: "UI changes for login flow",
|
|
777
|
+
newTestsCreated: [],
|
|
778
|
+
testMaintenance: [],
|
|
779
|
+
testResults: [
|
|
780
|
+
{
|
|
781
|
+
testType: TestType.UI,
|
|
782
|
+
endpoint: "GET /login",
|
|
783
|
+
status: "Pass",
|
|
784
|
+
details: "5.2s, login_ui_test.spec.ts",
|
|
785
|
+
videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
issuesFound: [],
|
|
396
789
|
});
|
|
790
|
+
expect(result.isError).toBeUndefined();
|
|
791
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
792
|
+
expect(written.testResults).toHaveLength(1);
|
|
793
|
+
expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
|
|
794
|
+
});
|
|
795
|
+
it("testResult accepts entry without videoPath (optional field)", async () => {
|
|
796
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
797
|
+
tmpDirs.push(tmpDir);
|
|
798
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
799
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
800
|
+
const result = await handler({
|
|
801
|
+
summaryOutputFile: outputFile,
|
|
802
|
+
stateFile,
|
|
803
|
+
businessCaseAnalysis: "API endpoint changes",
|
|
804
|
+
newTestsCreated: [],
|
|
805
|
+
testMaintenance: [],
|
|
806
|
+
testResults: [
|
|
807
|
+
{
|
|
808
|
+
testType: TestType.CONTRACT,
|
|
809
|
+
endpoint: "POST /api/v1/products",
|
|
810
|
+
status: "Pass",
|
|
811
|
+
details: "2.1s, products_contract_test.py",
|
|
812
|
+
// videoPath intentionally omitted
|
|
813
|
+
},
|
|
814
|
+
],
|
|
815
|
+
issuesFound: [],
|
|
816
|
+
});
|
|
817
|
+
expect(result.isError).toBeUndefined();
|
|
818
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
819
|
+
expect(written.testResults).toHaveLength(1);
|
|
820
|
+
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
397
821
|
});
|
|
398
822
|
});
|