@skyramp/mcp 0.2.6 → 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/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 +83 -18
- 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 +97 -99
- package/build/prompts/testbot/testbot-prompts.d.ts +26 -0
- package/build/prompts/testbot/testbot-prompts.js +301 -46
- package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
- package/build/prompts/testbot/testbot-prompts.test.js +109 -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 +2 -2
- 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 +26 -7
- 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 +394 -0
- package/build/tools/submitReportTool.js +250 -41
- package/build/tools/submitReportTool.test.d.ts +1 -0
- package/build/tools/submitReportTool.test.js +511 -55
- package/build/tools/test-management/actionsTool.d.ts +10 -0
- package/build/tools/test-management/actionsTool.js +21 -7
- package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
- package/build/tools/test-management/analyzeChangesTool.js +150 -48
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
- package/build/tools/test-management/analyzeChangesTool.test.js +95 -78
- package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
- 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.js +39 -22
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
- package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
- 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/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 +242 -0
- package/build/utils/AnalysisStateManager.js +144 -3
- package/build/utils/AnalysisStateManager.test.d.ts +1 -0
- package/build/utils/AnalysisStateManager.test.js +98 -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.js +7 -7
- package/build/workspace/workspace.test.d.ts +1 -0
- 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 +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/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
|
@@ -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"));
|
|
@@ -75,11 +85,44 @@ describe("registerSubmitReportTool", () => {
|
|
|
75
85
|
expect(written.issuesFound).toHaveLength(1);
|
|
76
86
|
expect(written.testMaintenance).toHaveLength(1);
|
|
77
87
|
});
|
|
88
|
+
it("accepts and persists the per-item `repository` field", async () => {
|
|
89
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
90
|
+
tmpDirs.push(tmpDir);
|
|
91
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
92
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
93
|
+
const params = sampleReportParams(outputFile, stateFile);
|
|
94
|
+
// Attribute one item to a related repo, leave others on the primary.
|
|
95
|
+
params.newTestsCreated[0] = { ...params.newTestsCreated[0], repository: "acme/backend" };
|
|
96
|
+
params.testResults[0] = { ...params.testResults[0], repository: "acme/backend" };
|
|
97
|
+
params.issuesFound[0] = { ...params.issuesFound[0], repository: "acme/backend" };
|
|
98
|
+
const result = await handler(params);
|
|
99
|
+
expect(result.isError).toBeUndefined();
|
|
100
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
101
|
+
expect(written.newTestsCreated[0].repository).toBe("acme/backend");
|
|
102
|
+
expect(written.testResults[0].repository).toBe("acme/backend");
|
|
103
|
+
expect(written.issuesFound[0].repository).toBe("acme/backend");
|
|
104
|
+
// Items without a repository stay primary (undefined → omitted from JSON).
|
|
105
|
+
expect(written.testResults[1].repository).toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
it("normalizes a blank/whitespace-only `repository` to undefined", async () => {
|
|
108
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
109
|
+
tmpDirs.push(tmpDir);
|
|
110
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
111
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
112
|
+
const params = sampleReportParams(outputFile, stateFile);
|
|
113
|
+
params.testResults[0] = { ...params.testResults[0], repository: " " };
|
|
114
|
+
const result = await handler(params);
|
|
115
|
+
expect(result.isError).toBeUndefined();
|
|
116
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
117
|
+
// Blank string must not serialize as a distinct empty repo.
|
|
118
|
+
expect(written.testResults[0].repository).toBeUndefined();
|
|
119
|
+
});
|
|
78
120
|
it("should create parent directories if they don't exist", async () => {
|
|
79
121
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
80
122
|
tmpDirs.push(tmpDir);
|
|
81
123
|
const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
|
|
82
|
-
const
|
|
124
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
125
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
83
126
|
expect(result.isError).toBeUndefined();
|
|
84
127
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
85
128
|
expect(written.businessCaseAnalysis).toBeDefined();
|
|
@@ -88,11 +131,12 @@ describe("registerSubmitReportTool", () => {
|
|
|
88
131
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
89
132
|
tmpDirs.push(tmpDir);
|
|
90
133
|
const outputFile = path.join(tmpDir, "report.json");
|
|
134
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
91
135
|
const result = await handler({
|
|
92
136
|
summaryOutputFile: outputFile,
|
|
137
|
+
stateFile,
|
|
93
138
|
businessCaseAnalysis: "Simple config change.",
|
|
94
139
|
newTestsCreated: [],
|
|
95
|
-
testMaintenance: [],
|
|
96
140
|
testResults: [
|
|
97
141
|
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
|
|
98
142
|
],
|
|
@@ -101,12 +145,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
101
145
|
expect(result.isError).toBeUndefined();
|
|
102
146
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
103
147
|
expect(written.newTestsCreated).toEqual([]);
|
|
104
|
-
expect(written.testMaintenance).
|
|
148
|
+
expect(written.testMaintenance).toBeUndefined();
|
|
105
149
|
expect(written.issuesFound).toEqual([]);
|
|
106
150
|
expect(written.testResults).toHaveLength(1);
|
|
107
151
|
});
|
|
108
152
|
it("should return error for invalid file path", async () => {
|
|
109
|
-
const
|
|
153
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
154
|
+
tmpDirs.push(tmpDir);
|
|
155
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
156
|
+
const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
|
|
110
157
|
expect(result.isError).toBe(true);
|
|
111
158
|
expect(result.content[0].text).toContain("Failed to write report");
|
|
112
159
|
});
|
|
@@ -114,8 +161,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
114
161
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
115
162
|
tmpDirs.push(tmpDir);
|
|
116
163
|
const outputFile = path.join(tmpDir, "report.json");
|
|
164
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
117
165
|
const result = await handler({
|
|
118
|
-
...sampleReportParams(outputFile),
|
|
166
|
+
...sampleReportParams(outputFile, stateFile),
|
|
119
167
|
commitMessage: "add contract tests for /products endpoint",
|
|
120
168
|
});
|
|
121
169
|
expect(result.isError).toBeUndefined();
|
|
@@ -126,7 +174,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
126
174
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
127
175
|
tmpDirs.push(tmpDir);
|
|
128
176
|
const outputFile = path.join(tmpDir, "report.json");
|
|
129
|
-
const
|
|
177
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
178
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
130
179
|
expect(result.isError).toBeUndefined();
|
|
131
180
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
132
181
|
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
@@ -135,8 +184,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
135
184
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
136
185
|
tmpDirs.push(tmpDir);
|
|
137
186
|
const outputFile = path.join(tmpDir, "report.json");
|
|
187
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
138
188
|
const result = await handler({
|
|
139
|
-
...sampleReportParams(outputFile),
|
|
189
|
+
...sampleReportParams(outputFile, stateFile),
|
|
140
190
|
commitMessage: " line one\nline two\r\nline three ",
|
|
141
191
|
});
|
|
142
192
|
expect(result.isError).toBeUndefined();
|
|
@@ -148,9 +198,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
148
198
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
149
199
|
tmpDirs.push(tmpDir);
|
|
150
200
|
const outputFile = path.join(tmpDir, "report.json");
|
|
201
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
151
202
|
const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
|
|
152
203
|
const result = await handler({
|
|
153
|
-
...sampleReportParams(outputFile),
|
|
204
|
+
...sampleReportParams(outputFile, stateFile),
|
|
154
205
|
commitMessage: longMessage,
|
|
155
206
|
});
|
|
156
207
|
expect(result.isError).toBeUndefined();
|
|
@@ -158,24 +209,13 @@ describe("registerSubmitReportTool", () => {
|
|
|
158
209
|
expect(written.commitMessage).toBe(longMessage);
|
|
159
210
|
expect(written.commitMessage.length).toBeGreaterThan(72);
|
|
160
211
|
});
|
|
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
212
|
it("should use default commitMessage when whitespace-only", async () => {
|
|
174
213
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
175
214
|
tmpDirs.push(tmpDir);
|
|
176
215
|
const outputFile = path.join(tmpDir, "report.json");
|
|
216
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
177
217
|
const result = await handler({
|
|
178
|
-
...sampleReportParams(outputFile),
|
|
218
|
+
...sampleReportParams(outputFile, stateFile),
|
|
179
219
|
commitMessage: " \n\r\n ",
|
|
180
220
|
});
|
|
181
221
|
expect(result.isError).toBeUndefined();
|
|
@@ -186,8 +226,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
186
226
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
187
227
|
tmpDirs.push(tmpDir);
|
|
188
228
|
const outputFile = path.join(tmpDir, "report.json");
|
|
229
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
189
230
|
const result = await handler({
|
|
190
|
-
...sampleReportParams(outputFile),
|
|
231
|
+
...sampleReportParams(outputFile, stateFile),
|
|
191
232
|
nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
|
|
192
233
|
});
|
|
193
234
|
expect(result.isError).toBeUndefined();
|
|
@@ -200,7 +241,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
200
241
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
201
242
|
tmpDirs.push(tmpDir);
|
|
202
243
|
const outputFile = path.join(tmpDir, "report.json");
|
|
203
|
-
const
|
|
244
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
245
|
+
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
204
246
|
expect(result.isError).toBeUndefined();
|
|
205
247
|
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
206
248
|
expect(written.nextSteps).toEqual([]);
|
|
@@ -209,7 +251,8 @@ describe("registerSubmitReportTool", () => {
|
|
|
209
251
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
210
252
|
tmpDirs.push(tmpDir);
|
|
211
253
|
const outputFile = path.join(tmpDir, "report.json");
|
|
212
|
-
await
|
|
254
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
255
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
213
256
|
const raw = await fs.readFile(outputFile, "utf-8");
|
|
214
257
|
// Pretty-printed JSON should have indentation
|
|
215
258
|
expect(raw).toContain(" ");
|
|
@@ -220,8 +263,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
220
263
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
221
264
|
tmpDirs.push(tmpDir);
|
|
222
265
|
const outputFile = path.join(tmpDir, "report.json");
|
|
266
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
223
267
|
const params = {
|
|
224
|
-
...sampleReportParams(outputFile),
|
|
268
|
+
...sampleReportParams(outputFile, stateFile),
|
|
225
269
|
additionalRecommendations: [
|
|
226
270
|
{
|
|
227
271
|
testId: "integration-orders-patch-status",
|
|
@@ -293,6 +337,214 @@ describe("registerSubmitReportTool", () => {
|
|
|
293
337
|
expect(result.data.primaryEndpoint).toBeUndefined();
|
|
294
338
|
}
|
|
295
339
|
});
|
|
340
|
+
// Structured grounding fields for UI recommendations.
|
|
341
|
+
// ----------------------------------------------------------------------
|
|
342
|
+
// Sub-schemas — targetElement and pageContext — populated by the agent
|
|
343
|
+
// from a captured browser_blueprint element. UI-only via superRefine on
|
|
344
|
+
// the parent newTestSchema / additionalRecommendationSchema.
|
|
345
|
+
describe("targetElementSchema", () => {
|
|
346
|
+
const validTarget = {
|
|
347
|
+
role: "button",
|
|
348
|
+
accessibleName: "Save changes",
|
|
349
|
+
testId: "save-changes-btn",
|
|
350
|
+
stableId: null,
|
|
351
|
+
contextText: null,
|
|
352
|
+
mutability: "mutable",
|
|
353
|
+
widgetType: "native",
|
|
354
|
+
};
|
|
355
|
+
it("accepts a fully-populated targetElement", () => {
|
|
356
|
+
expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
|
|
357
|
+
});
|
|
358
|
+
it("accepts a minimal targetElement (only required fields)", () => {
|
|
359
|
+
const result = targetElementSchema.safeParse({
|
|
360
|
+
role: "heading",
|
|
361
|
+
accessibleName: "Notifications",
|
|
362
|
+
testId: null,
|
|
363
|
+
stableId: null,
|
|
364
|
+
contextText: null,
|
|
365
|
+
});
|
|
366
|
+
expect(result.success).toBe(true);
|
|
367
|
+
});
|
|
368
|
+
it("accepts contextText as a string array (repeating-row case)", () => {
|
|
369
|
+
const result = targetElementSchema.safeParse({
|
|
370
|
+
...validTarget,
|
|
371
|
+
contextText: ["customer@example.com", "$129.99", "Pending"],
|
|
372
|
+
});
|
|
373
|
+
expect(result.success).toBe(true);
|
|
374
|
+
});
|
|
375
|
+
it("rejects targetElement missing accessibleName", () => {
|
|
376
|
+
const { accessibleName, ...withoutName } = validTarget;
|
|
377
|
+
const result = targetElementSchema.safeParse(withoutName);
|
|
378
|
+
expect(result.success).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
it("rejects targetElement missing role", () => {
|
|
381
|
+
const { role, ...withoutRole } = validTarget;
|
|
382
|
+
const result = targetElementSchema.safeParse(withoutRole);
|
|
383
|
+
expect(result.success).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
it("rejects an invalid mutability enum value", () => {
|
|
386
|
+
const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
|
|
387
|
+
expect(result.success).toBe(false);
|
|
388
|
+
});
|
|
389
|
+
it("rejects an invalid widgetType enum value", () => {
|
|
390
|
+
const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
|
|
391
|
+
expect(result.success).toBe(false);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
describe("pageContextSchema (UI recommendation grounding)", () => {
|
|
395
|
+
it("accepts pageContext with both url and pageHash", () => {
|
|
396
|
+
const result = pageContextSchema.safeParse({
|
|
397
|
+
url: "http://localhost:5173/orders/1",
|
|
398
|
+
pageHash: "10:434266447",
|
|
399
|
+
});
|
|
400
|
+
expect(result.success).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
it("accepts pageContext without pageHash (optional)", () => {
|
|
403
|
+
const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
|
|
404
|
+
expect(result.success).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
it("rejects pageContext missing url", () => {
|
|
407
|
+
const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
|
|
408
|
+
expect(result.success).toBe(false);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
|
|
412
|
+
const validTarget = {
|
|
413
|
+
role: "button",
|
|
414
|
+
accessibleName: "Save changes",
|
|
415
|
+
testId: "save-changes-btn",
|
|
416
|
+
stableId: null,
|
|
417
|
+
contextText: null,
|
|
418
|
+
};
|
|
419
|
+
const validPage = {
|
|
420
|
+
url: "http://localhost:5173/orders/1",
|
|
421
|
+
pageHash: "10:434266447",
|
|
422
|
+
};
|
|
423
|
+
const baseUITest = {
|
|
424
|
+
testId: "ui-edit-order-discount-validation",
|
|
425
|
+
testType: TestType.UI,
|
|
426
|
+
category: "business_rule",
|
|
427
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
428
|
+
fileName: "edit_order_discount.spec.ts",
|
|
429
|
+
description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
|
|
430
|
+
reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
431
|
+
};
|
|
432
|
+
it("accepts a UI test with single-element targetElements + pageContext", () => {
|
|
433
|
+
const result = newTestSchema.safeParse({
|
|
434
|
+
...baseUITest,
|
|
435
|
+
targetElements: [validTarget],
|
|
436
|
+
pageContext: validPage,
|
|
437
|
+
});
|
|
438
|
+
expect(result.success).toBe(true);
|
|
439
|
+
});
|
|
440
|
+
it("accepts a UI test with multi-element targetElements (render-state test)", () => {
|
|
441
|
+
const result = newTestSchema.safeParse({
|
|
442
|
+
...baseUITest,
|
|
443
|
+
targetElements: [
|
|
444
|
+
{
|
|
445
|
+
role: "heading",
|
|
446
|
+
accessibleName: "Edit Order",
|
|
447
|
+
testId: "edit-order-heading",
|
|
448
|
+
stableId: null,
|
|
449
|
+
contextText: null,
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
role: "spinbutton",
|
|
453
|
+
accessibleName: "Discount (%)",
|
|
454
|
+
testId: "edit-order-input-discount",
|
|
455
|
+
stableId: null,
|
|
456
|
+
contextText: null,
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
pageContext: validPage,
|
|
460
|
+
});
|
|
461
|
+
expect(result.success).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
it("rejects a UI test without targetElements (required for UI grounding)", () => {
|
|
464
|
+
const result = newTestSchema.safeParse(baseUITest);
|
|
465
|
+
expect(result.success).toBe(false);
|
|
466
|
+
expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
|
|
467
|
+
});
|
|
468
|
+
it("accepts a UI test with targetElements: null (fallback case)", () => {
|
|
469
|
+
const result = newTestSchema.safeParse({
|
|
470
|
+
...baseUITest,
|
|
471
|
+
targetElements: null,
|
|
472
|
+
description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
|
|
473
|
+
reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
|
|
474
|
+
});
|
|
475
|
+
expect(result.success).toBe(true);
|
|
476
|
+
});
|
|
477
|
+
it("rejects a contract test that includes targetElements (UI-only)", () => {
|
|
478
|
+
const result = newTestSchema.safeParse({
|
|
479
|
+
testId: "contract-orders-patch",
|
|
480
|
+
testType: TestType.CONTRACT,
|
|
481
|
+
category: "business_rule",
|
|
482
|
+
endpoint: "PATCH /api/v1/orders/{id}",
|
|
483
|
+
fileName: "orders_patch_contract.py",
|
|
484
|
+
reasoning: "Validates request shape and 200/422 status codes",
|
|
485
|
+
targetElements: [validTarget],
|
|
486
|
+
});
|
|
487
|
+
expect(result.success).toBe(false);
|
|
488
|
+
if (!result.success) {
|
|
489
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
it("rejects an integration test that includes pageContext (UI-only)", () => {
|
|
493
|
+
const result = newTestSchema.safeParse({
|
|
494
|
+
testId: "integration-orders",
|
|
495
|
+
testType: TestType.INTEGRATION,
|
|
496
|
+
category: "business_rule",
|
|
497
|
+
endpoint: "POST /api/v1/orders",
|
|
498
|
+
fileName: "orders_integration.py",
|
|
499
|
+
reasoning: "Multi-step order lifecycle",
|
|
500
|
+
pageContext: validPage,
|
|
501
|
+
});
|
|
502
|
+
expect(result.success).toBe(false);
|
|
503
|
+
if (!result.success) {
|
|
504
|
+
expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
|
|
505
|
+
}
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
|
|
509
|
+
const validTarget = {
|
|
510
|
+
role: "button",
|
|
511
|
+
accessibleName: "Save changes",
|
|
512
|
+
testId: "save-changes-btn",
|
|
513
|
+
stableId: null,
|
|
514
|
+
contextText: null,
|
|
515
|
+
};
|
|
516
|
+
const validPage = { url: "http://localhost:5173/orders/1" };
|
|
517
|
+
it("accepts a UI rec with targetElements + pageContext", () => {
|
|
518
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
519
|
+
testId: "ui-edit-order-discount-validation",
|
|
520
|
+
testType: TestType.UI,
|
|
521
|
+
category: "business_rule",
|
|
522
|
+
steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
|
|
523
|
+
description: "Verify discount > 100 shows validation error",
|
|
524
|
+
priority: "medium",
|
|
525
|
+
reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
526
|
+
targetElements: [validTarget],
|
|
527
|
+
pageContext: validPage,
|
|
528
|
+
});
|
|
529
|
+
expect(result.success).toBe(true);
|
|
530
|
+
});
|
|
531
|
+
it("rejects a non-UI rec that includes targetElements", () => {
|
|
532
|
+
const result = additionalRecommendationSchema.safeParse({
|
|
533
|
+
testId: "integration-orders-patch",
|
|
534
|
+
testType: TestType.INTEGRATION,
|
|
535
|
+
category: "business_rule",
|
|
536
|
+
steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
|
|
537
|
+
description: "Verifies PATCH workflow",
|
|
538
|
+
priority: "high",
|
|
539
|
+
reasoning: "Catches mutation bugs",
|
|
540
|
+
targetElements: [validTarget],
|
|
541
|
+
});
|
|
542
|
+
expect(result.success).toBe(false);
|
|
543
|
+
if (!result.success) {
|
|
544
|
+
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
});
|
|
296
548
|
describe("analytics metrics", () => {
|
|
297
549
|
beforeEach(() => {
|
|
298
550
|
pushMCPToolEventMock.mockClear();
|
|
@@ -306,14 +558,15 @@ describe("registerSubmitReportTool", () => {
|
|
|
306
558
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
307
559
|
tmpDirs.push(tmpDir);
|
|
308
560
|
const outputFile = path.join(tmpDir, "report.json");
|
|
309
|
-
// sampleReportParams: 1 new test, 1 maintenance (
|
|
561
|
+
// sampleReportParams: 1 new test, 1 maintenance (no matching stateFile entry), 2 results
|
|
310
562
|
// (1 Pass + 1 Fail), 1 issue (no severity).
|
|
311
|
-
await
|
|
563
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
564
|
+
await handler(sampleReportParams(outputFile, stateFile));
|
|
312
565
|
const params = lastAnalyticsParams();
|
|
313
566
|
expect(params.testsGenerated).toBe("1");
|
|
314
567
|
expect(params.testsMaintained).toBe("1");
|
|
315
568
|
expect(params.recommendationsCount).toBe("0");
|
|
316
|
-
expect(params.maintenanceRecovered).toBe("
|
|
569
|
+
expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
|
|
317
570
|
expect(params.testsPassed).toBe("1");
|
|
318
571
|
expect(params.testsFailed).toBe("1");
|
|
319
572
|
expect(params.testsSkipped).toBe("0");
|
|
@@ -323,8 +576,9 @@ describe("registerSubmitReportTool", () => {
|
|
|
323
576
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
324
577
|
tmpDirs.push(tmpDir);
|
|
325
578
|
const outputFile = path.join(tmpDir, "report.json");
|
|
579
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
326
580
|
await handler({
|
|
327
|
-
...sampleReportParams(outputFile),
|
|
581
|
+
...sampleReportParams(outputFile, stateFile),
|
|
328
582
|
issuesFound: [
|
|
329
583
|
{ description: "Data corruption on checkout", severity: "critical" },
|
|
330
584
|
{ description: "Wrong total returned", severity: "high" },
|
|
@@ -344,8 +598,10 @@ describe("registerSubmitReportTool", () => {
|
|
|
344
598
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
345
599
|
tmpDirs.push(tmpDir);
|
|
346
600
|
const outputFile = path.join(tmpDir, "report.json");
|
|
601
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
347
602
|
await handler({
|
|
348
603
|
summaryOutputFile: outputFile,
|
|
604
|
+
stateFile,
|
|
349
605
|
businessCaseAnalysis: "Config-only change.",
|
|
350
606
|
newTestsCreated: [],
|
|
351
607
|
testMaintenance: [],
|
|
@@ -363,36 +619,236 @@ describe("registerSubmitReportTool", () => {
|
|
|
363
619
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
364
620
|
tmpDirs.push(tmpDir);
|
|
365
621
|
const outputFile = path.join(tmpDir, "report.json");
|
|
622
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
366
623
|
await handler({
|
|
367
|
-
...sampleReportParams(outputFile),
|
|
624
|
+
...sampleReportParams(outputFile, stateFile),
|
|
368
625
|
testMaintenance: [
|
|
369
626
|
// 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",
|
|
627
|
+
{ action: "UPDATE", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products", fileName: "products_contract_test.py", description: "Patched auth", beforeDetails: "", afterDetails: "" },
|
|
628
|
+
{ action: "REGENERATE", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders", fileName: "orders_smoke_test.py", description: "Rewrote for new shape", beforeDetails: "", afterDetails: "" },
|
|
372
629
|
// 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",
|
|
630
|
+
{ action: "IGNORE", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews", fileName: "reviews_smoke_test.py", description: "No action required", beforeDetails: "", afterDetails: "" },
|
|
631
|
+
{ action: "VERIFY", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons", fileName: "coupons_smoke_test.py", description: "Verified still valid", beforeDetails: "", afterDetails: "" },
|
|
375
632
|
],
|
|
376
633
|
});
|
|
377
634
|
const params = lastAnalyticsParams();
|
|
378
635
|
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
379
636
|
expect(params.testsMaintained).toBe("2");
|
|
380
|
-
//
|
|
381
|
-
expect(params.maintenanceRecovered).toBe("
|
|
637
|
+
// No matching stateFile entries — execution data unavailable
|
|
638
|
+
expect(params.maintenanceRecovered).toBe("0");
|
|
382
639
|
});
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
640
|
+
});
|
|
641
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
642
|
+
// testMaintenance output shapes
|
|
643
|
+
// Each case documents exactly what the MCP writes to JSON, which is what the
|
|
644
|
+
// testbot renderer reads. Keeping these in sync prevents silent regressions
|
|
645
|
+
// when toOutput or the stateFile enrichment path changes.
|
|
646
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
647
|
+
describe("testMaintenance output shapes", () => {
|
|
648
|
+
const baseMaintenance = {
|
|
649
|
+
testType: TestType.CONTRACT,
|
|
650
|
+
endpoint: "GET /api/v1/orders",
|
|
651
|
+
fileName: "orders_contract_test.py",
|
|
652
|
+
description: "Updated discount fields in assertions",
|
|
653
|
+
beforeDetails: "",
|
|
654
|
+
afterDetails: "",
|
|
655
|
+
};
|
|
656
|
+
async function writeAndRead(tmpDir, maintenance, extra) {
|
|
386
657
|
const outputFile = path.join(tmpDir, "report.json");
|
|
387
658
|
await handler({
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
],
|
|
659
|
+
summaryOutputFile: outputFile,
|
|
660
|
+
businessCaseAnalysis: "Test",
|
|
661
|
+
newTestsCreated: [],
|
|
662
|
+
testMaintenance: maintenance,
|
|
663
|
+
testResults: [],
|
|
664
|
+
issuesFound: [],
|
|
665
|
+
...extra,
|
|
393
666
|
});
|
|
394
|
-
const
|
|
395
|
-
|
|
667
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
668
|
+
return written.testMaintenance;
|
|
669
|
+
}
|
|
670
|
+
// ── No matching stateFile entry ───────────────────────────────────────────
|
|
671
|
+
it("UPDATE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
672
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
673
|
+
tmpDirs.push(tmpDir);
|
|
674
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
675
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
676
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
677
|
+
expect(result[0].afterStatus).toBe("Unknown");
|
|
678
|
+
expect(result[0].beforeDetails).toBe("");
|
|
679
|
+
expect(result[0].afterDetails).toBe("");
|
|
680
|
+
});
|
|
681
|
+
it("IGNORE with no matching stateFile entry: both Skipped", async () => {
|
|
682
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
683
|
+
tmpDirs.push(tmpDir);
|
|
684
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
685
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Ignore }], { stateFile });
|
|
686
|
+
expect(result[0].beforeStatus).toBe("Skipped");
|
|
687
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
688
|
+
expect(result[0].beforeDetails).toBe("");
|
|
689
|
+
expect(result[0].afterDetails).toBe("");
|
|
396
690
|
});
|
|
691
|
+
it("DELETE with no matching stateFile entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
692
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
693
|
+
tmpDirs.push(tmpDir);
|
|
694
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
695
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
696
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
697
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
698
|
+
});
|
|
699
|
+
// ── With matching stateFile entry ─────────────────────────────────────────
|
|
700
|
+
it("UPDATE with before=Pass after=Pass: status from stateFile, details from LLM", async () => {
|
|
701
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
702
|
+
tmpDirs.push(tmpDir);
|
|
703
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
704
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
705
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
706
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
707
|
+
}]);
|
|
708
|
+
const result = await writeAndRead(tmpDir, [{
|
|
709
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
710
|
+
beforeDetails: "4 passed in 15.09s",
|
|
711
|
+
afterDetails: "5 passed in 12.88s",
|
|
712
|
+
}], { stateFile });
|
|
713
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
714
|
+
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
715
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
716
|
+
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
717
|
+
});
|
|
718
|
+
it("UPDATE with before=Pass after=Fail: status from stateFile, details from LLM", async () => {
|
|
719
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
720
|
+
tmpDirs.push(tmpDir);
|
|
721
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
722
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
723
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
724
|
+
executionAfter: { status: TestExecutionStatus.Fail, errors: [], duration: 10940 },
|
|
725
|
+
}]);
|
|
726
|
+
const result = await writeAndRead(tmpDir, [{
|
|
727
|
+
...baseMaintenance, action: DriftAction.Update,
|
|
728
|
+
beforeDetails: "4 passed in 15.09s",
|
|
729
|
+
afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug",
|
|
730
|
+
}], { stateFile });
|
|
731
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
732
|
+
expect(result[0].afterStatus).toBe("Fail");
|
|
733
|
+
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
734
|
+
});
|
|
735
|
+
it("UPDATE with crashed execution: status is Error", async () => {
|
|
736
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
737
|
+
tmpDirs.push(tmpDir);
|
|
738
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
739
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
740
|
+
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
741
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
742
|
+
}]);
|
|
743
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
744
|
+
expect(result[0].beforeStatus).toBe("Error");
|
|
745
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
746
|
+
});
|
|
747
|
+
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
748
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
749
|
+
tmpDirs.push(tmpDir);
|
|
750
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
751
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
752
|
+
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
753
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
754
|
+
}]);
|
|
755
|
+
await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
756
|
+
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
757
|
+
});
|
|
758
|
+
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass", async () => {
|
|
759
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
760
|
+
tmpDirs.push(tmpDir);
|
|
761
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
762
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
763
|
+
// executionBefore absent — agent skipped before-phase
|
|
764
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
765
|
+
}]);
|
|
766
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Update }], { stateFile });
|
|
767
|
+
expect(result[0].beforeStatus).toBe("Unknown");
|
|
768
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
769
|
+
});
|
|
770
|
+
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped", async () => {
|
|
771
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
772
|
+
tmpDirs.push(tmpDir);
|
|
773
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
774
|
+
testFile: "/repo/backend/tests/orders_contract_test.py",
|
|
775
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
776
|
+
// executionAfter absent — file was deleted, nothing to run
|
|
777
|
+
}]);
|
|
778
|
+
const result = await writeAndRead(tmpDir, [{ ...baseMaintenance, action: DriftAction.Delete }], { stateFile });
|
|
779
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
780
|
+
expect(result[0].afterStatus).toBe("Skipped");
|
|
781
|
+
});
|
|
782
|
+
it("fileName matched by suffix: absolute stateFile path vs relative fileName", async () => {
|
|
783
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
784
|
+
tmpDirs.push(tmpDir);
|
|
785
|
+
const stateFile = await writeStateFile(tmpDir, [{
|
|
786
|
+
testFile: "/repo/backend/tests/python/orders_contract_test.py",
|
|
787
|
+
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15000 },
|
|
788
|
+
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12000 },
|
|
789
|
+
}]);
|
|
790
|
+
// fileName is relative — should still match via endsWith
|
|
791
|
+
const result = await writeAndRead(tmpDir, [{
|
|
792
|
+
...baseMaintenance,
|
|
793
|
+
action: DriftAction.Update,
|
|
794
|
+
fileName: "orders_contract_test.py",
|
|
795
|
+
}], { stateFile });
|
|
796
|
+
expect(result[0].beforeStatus).toBe("Pass");
|
|
797
|
+
expect(result[0].afterStatus).toBe("Pass");
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
it("testResult accepts videoPath field (optional)", async () => {
|
|
801
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
802
|
+
tmpDirs.push(tmpDir);
|
|
803
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
804
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
805
|
+
const result = await handler({
|
|
806
|
+
summaryOutputFile: outputFile,
|
|
807
|
+
stateFile,
|
|
808
|
+
businessCaseAnalysis: "UI changes for login flow",
|
|
809
|
+
newTestsCreated: [],
|
|
810
|
+
testMaintenance: [],
|
|
811
|
+
testResults: [
|
|
812
|
+
{
|
|
813
|
+
testType: TestType.UI,
|
|
814
|
+
endpoint: "GET /login",
|
|
815
|
+
status: "Pass",
|
|
816
|
+
details: "5.2s, login_ui_test.spec.ts",
|
|
817
|
+
videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
|
|
818
|
+
},
|
|
819
|
+
],
|
|
820
|
+
issuesFound: [],
|
|
821
|
+
});
|
|
822
|
+
expect(result.isError).toBeUndefined();
|
|
823
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
824
|
+
expect(written.testResults).toHaveLength(1);
|
|
825
|
+
expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
|
|
826
|
+
});
|
|
827
|
+
it("testResult accepts entry without videoPath (optional field)", async () => {
|
|
828
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
829
|
+
tmpDirs.push(tmpDir);
|
|
830
|
+
const outputFile = path.join(tmpDir, "report.json");
|
|
831
|
+
const stateFile = await writeStateFile(tmpDir, []);
|
|
832
|
+
const result = await handler({
|
|
833
|
+
summaryOutputFile: outputFile,
|
|
834
|
+
stateFile,
|
|
835
|
+
businessCaseAnalysis: "API endpoint changes",
|
|
836
|
+
newTestsCreated: [],
|
|
837
|
+
testMaintenance: [],
|
|
838
|
+
testResults: [
|
|
839
|
+
{
|
|
840
|
+
testType: TestType.CONTRACT,
|
|
841
|
+
endpoint: "POST /api/v1/products",
|
|
842
|
+
status: "Pass",
|
|
843
|
+
details: "2.1s, products_contract_test.py",
|
|
844
|
+
// videoPath intentionally omitted
|
|
845
|
+
},
|
|
846
|
+
],
|
|
847
|
+
issuesFound: [],
|
|
848
|
+
});
|
|
849
|
+
expect(result.isError).toBeUndefined();
|
|
850
|
+
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
851
|
+
expect(written.testResults).toHaveLength(1);
|
|
852
|
+
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
397
853
|
});
|
|
398
854
|
});
|