@skyramp/mcp 0.3.2-rc.pom-3 → 0.3.2
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 +1 -0
- package/build/commands/commandLibrary.js +19 -13
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +201 -0
- package/build/index.js +80 -6
- package/build/prompts/code-reuse.js +3 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
- package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
- package/build/prompts/local-dev/local-dev-plan.js +429 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +190 -0
- package/build/prompts/pom-aware-code-reuse.js +12 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +37 -14
- package/build/prompts/sut-setup/shared.js +16 -12
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +24 -7
- package/build/prompts/test-recommendation/scopeAssessment.js +111 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +32 -52
- package/build/recommendation/planRanker.d.ts +15 -2
- package/build/recommendation/planRanker.js +76 -5
- package/build/resources/testbotResource.js +2 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +8 -3
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/services/TestGenerationService.js +39 -21
- package/build/services/containerEnv.js +3 -1
- package/build/tool-phases.js +6 -0
- package/build/tools/code-refactor/codeReuseTool.js +43 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
- package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
- package/build/tools/code-refactor/reuse-outcome.js +158 -0
- package/build/tools/code-refactor/reuse-state.d.ts +45 -0
- package/build/tools/code-refactor/reuse-state.js +140 -0
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/executeSkyrampTestTool.d.ts +11 -0
- package/build/tools/executeSkyrampTestTool.js +62 -21
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
- package/build/tools/generate-tests/generateMockRestTool.js +391 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generate-tests/planGuard.js +2 -22
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
- package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
- package/build/tools/localDevWorkerComposeTool.js +264 -0
- package/build/tools/one-click/oneClickTool.d.ts +13 -0
- package/build/tools/one-click/oneClickTool.js +195 -24
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/queryProxyMocksTool.d.ts +70 -0
- package/build/tools/queryProxyMocksTool.js +522 -0
- package/build/tools/runExistingTestsTool.d.ts +31 -0
- package/build/tools/runExistingTestsTool.js +214 -13
- package/build/tools/submitReportTool.js +38 -3
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +63 -40
- package/build/tools/test-management/registerTestPlanTool.js +55 -7
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/FrontendIntegration.d.ts +3 -0
- package/build/types/FrontendIntegration.js +3 -0
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/Recommendation.d.ts +20 -0
- package/build/types/Recommendation.js +32 -6
- package/build/types/RepositoryAnalysis.d.ts +131 -14
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/ReuseOutcome.d.ts +63 -0
- package/build/types/ReuseOutcome.js +33 -0
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +25 -7
- package/build/types/TestTypes.js +22 -6
- package/build/types/TestbotReport.d.ts +7 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +26 -0
- package/build/utils/AnalysisStateManager.js +31 -1
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +99 -14
- package/build/utils/featureFlags.d.ts +31 -0
- package/build/utils/featureFlags.js +37 -0
- package/build/utils/frontendIntegration.js +8 -1
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +15 -0
- package/build/utils/logger.js +1 -1
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/pom-scope/pom-files.js +7 -0
- package/build/utils/pom-verify/bindings.js +11 -1
- package/build/utils/pom-verify/re-exports.d.ts +10 -0
- package/build/utils/pom-verify/re-exports.js +61 -0
- package/build/utils/pom-verify/resolve.d.ts +4 -1
- package/build/utils/pom-verify/resolve.js +7 -4
- package/build/utils/pom-verify/verify.d.ts +5 -0
- package/build/utils/pom-verify/verify.js +25 -3
- package/build/utils/progress.js +10 -5
- package/build/utils/proxy-terminal.js +3 -3
- package/build/utils/routeParsers.d.ts +3 -9
- package/build/utils/routeParsers.js +79 -4
- package/build/utils/utils.js +2 -2
- package/build/utils/versions.d.ts +4 -3
- package/build/utils/versions.js +3 -1
- package/build/utils/workspaceAuth.d.ts +46 -0
- package/build/utils/workspaceAuth.js +156 -1
- package/build/workspace/testSuites.d.ts +2 -1
- package/build/workspace/testSuites.js +1 -1
- package/build/workspace/workspace.d.ts +108 -32
- package/build/workspace/workspace.js +32 -4
- package/package.json +5 -2
- package/build/adapters/jestAdapter.test.d.ts +0 -1
- package/build/adapters/jestAdapter.test.js +0 -93
- package/build/adapters/mochaAdapter.test.d.ts +0 -1
- package/build/adapters/mochaAdapter.test.js +0 -63
- package/build/adapters/playwrightAdapter.test.d.ts +0 -1
- package/build/adapters/playwrightAdapter.test.js +0 -169
- package/build/adapters/pytestAdapter.test.d.ts +0 -1
- package/build/adapters/pytestAdapter.test.js +0 -90
- package/build/prompts/code-reuse.test.d.ts +0 -1
- package/build/prompts/code-reuse.test.js +0 -62
- package/build/prompts/pom-aware-code-reuse.test.d.ts +0 -1
- package/build/prompts/pom-aware-code-reuse.test.js +0 -11
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +0 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -51
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -264
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +0 -1
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +0 -126
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +0 -1
- package/build/prompts/test-recommendation/promptPlan.test.js +0 -336
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +0 -1
- package/build/prompts/test-recommendation/scopeAssessment.test.js +0 -371
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +0 -1925
- package/build/prompts/testbot/testbot-prompts.test.d.ts +0 -1
- package/build/prompts/testbot/testbot-prompts.test.js +0 -451
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +0 -138
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +0 -66
- package/build/recommendation/discriminators.test.d.ts +0 -1
- package/build/recommendation/discriminators.test.js +0 -324
- package/build/recommendation/diversity.test.d.ts +0 -1
- package/build/recommendation/diversity.test.js +0 -77
- package/build/recommendation/planRanker.test.d.ts +0 -1
- package/build/recommendation/planRanker.test.js +0 -110
- package/build/resources/testbotResource.test.d.ts +0 -1
- package/build/resources/testbotResource.test.js +0 -44
- package/build/services/AnalyticsService.test.d.ts +0 -1
- package/build/services/AnalyticsService.test.js +0 -86
- package/build/services/ScenarioGenerationService.integration.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.integration.test.js +0 -162
- package/build/services/ScenarioGenerationService.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.test.js +0 -414
- package/build/services/TestDiscoveryService.test.d.ts +0 -1
- package/build/services/TestDiscoveryService.test.js +0 -983
- package/build/services/TestExecutionService.test.d.ts +0 -1
- package/build/services/TestExecutionService.test.js +0 -1032
- package/build/services/TestGenerationService.test.d.ts +0 -1
- package/build/services/TestGenerationService.test.js +0 -578
- package/build/tool-phase-coverage.test.d.ts +0 -1
- package/build/tool-phase-coverage.test.js +0 -49
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +0 -1
- package/build/tools/code-refactor/codeReuseTool.test.js +0 -572
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +0 -433
- package/build/tools/generate-tests/generateContractRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateContractRestTool.test.js +0 -142
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +0 -159
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateLoadRestTool.test.js +0 -169
- package/build/tools/generate-tests/generateUIRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateUIRestTool.test.js +0 -32
- package/build/tools/generate-tests/planGuard.test.d.ts +0 -1
- package/build/tools/generate-tests/planGuard.test.js +0 -185
- package/build/tools/generate-tests/scenarioLint.test.d.ts +0 -1
- package/build/tools/generate-tests/scenarioLint.test.js +0 -100
- package/build/tools/runExistingTestsTool.test.d.ts +0 -1
- package/build/tools/runExistingTestsTool.test.js +0 -379
- package/build/tools/submitReportTool.test.d.ts +0 -1
- package/build/tools/submitReportTool.test.js +0 -1428
- package/build/tools/test-management/actionsTool.test.d.ts +0 -1
- package/build/tools/test-management/actionsTool.test.js +0 -436
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeChangesTool.test.js +0 -522
- package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +0 -385
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +0 -1
- package/build/tools/test-management/registerTestPlanTool.test.js +0 -296
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +0 -1
- package/build/tools/trace/resolveSaveStoragePath.test.js +0 -17
- package/build/tools/trace/resolveSessionPaths.test.d.ts +0 -1
- package/build/tools/trace/resolveSessionPaths.test.js +0 -104
- package/build/tools/trace/sessionState.test.d.ts +0 -1
- package/build/tools/trace/sessionState.test.js +0 -17
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +0 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +0 -139
- package/build/tools/workspace/serviceUpsert.test.d.ts +0 -1
- package/build/tools/workspace/serviceUpsert.test.js +0 -50
- package/build/utils/AnalysisStateManager.test.d.ts +0 -1
- package/build/utils/AnalysisStateManager.test.js +0 -134
- package/build/utils/dartRouteExtractor.test.d.ts +0 -1
- package/build/utils/dartRouteExtractor.test.js +0 -307
- package/build/utils/docker.test.d.ts +0 -1
- package/build/utils/docker.test.js +0 -114
- package/build/utils/featureFlags.test.d.ts +0 -1
- package/build/utils/featureFlags.test.js +0 -81
- package/build/utils/fileWalk.test.d.ts +0 -1
- package/build/utils/fileWalk.test.js +0 -252
- package/build/utils/frontendIntegration.test.d.ts +0 -1
- package/build/utils/frontendIntegration.test.js +0 -229
- package/build/utils/frontendSelectors.test.d.ts +0 -1
- package/build/utils/frontendSelectors.test.js +0 -118
- package/build/utils/gitStaging.test.d.ts +0 -1
- package/build/utils/gitStaging.test.js +0 -111
- package/build/utils/httpDefaults.test.d.ts +0 -1
- package/build/utils/httpDefaults.test.js +0 -21
- package/build/utils/importerHop.test.d.ts +0 -1
- package/build/utils/importerHop.test.js +0 -469
- package/build/utils/pathAffinityClassification.test.d.ts +0 -1
- package/build/utils/pathAffinityClassification.test.js +0 -208
- package/build/utils/planMatchKeys.test.d.ts +0 -1
- package/build/utils/planMatchKeys.test.js +0 -123
- package/build/utils/pom-scope/index.test.d.ts +0 -1
- package/build/utils/pom-scope/index.test.js +0 -278
- package/build/utils/pom-scope/pom-files.test.d.ts +0 -1
- package/build/utils/pom-scope/pom-files.test.js +0 -29
- package/build/utils/pom-scope/scoring.test.d.ts +0 -1
- package/build/utils/pom-scope/scoring.test.js +0 -39
- package/build/utils/pom-scope/selector-extractor.test.d.ts +0 -1
- package/build/utils/pom-scope/selector-extractor.test.js +0 -67
- package/build/utils/pom-verify/bindings.test.d.ts +0 -1
- package/build/utils/pom-verify/bindings.test.js +0 -164
- package/build/utils/pom-verify/calls.test.d.ts +0 -1
- package/build/utils/pom-verify/calls.test.js +0 -61
- package/build/utils/pom-verify/resolve.test.d.ts +0 -1
- package/build/utils/pom-verify/resolve.test.js +0 -114
- package/build/utils/pom-verify/verify.test.d.ts +0 -1
- package/build/utils/pom-verify/verify.test.js +0 -374
- package/build/utils/pr-comment-parser.test.d.ts +0 -1
- package/build/utils/pr-comment-parser.test.js +0 -428
- package/build/utils/progress.test.d.ts +0 -1
- package/build/utils/progress.test.js +0 -37
- package/build/utils/projectMetadata.test.d.ts +0 -1
- package/build/utils/projectMetadata.test.js +0 -172
- package/build/utils/pythonMountPrefixes.test.d.ts +0 -1
- package/build/utils/pythonMountPrefixes.test.js +0 -113
- package/build/utils/repoScanner.test.d.ts +0 -1
- package/build/utils/repoScanner.test.js +0 -190
- package/build/utils/reportVerification.test.d.ts +0 -1
- package/build/utils/reportVerification.test.js +0 -185
- package/build/utils/routeParsers.test.d.ts +0 -1
- package/build/utils/routeParsers.test.js +0 -1011
- package/build/utils/scenarioDrafting.test.d.ts +0 -1
- package/build/utils/scenarioDrafting.test.js +0 -876
- package/build/utils/sourceRouteExtractor.test.d.ts +0 -1
- package/build/utils/sourceRouteExtractor.test.js +0 -738
- package/build/utils/telemetry.test.d.ts +0 -1
- package/build/utils/telemetry.test.js +0 -70
- package/build/utils/trace-parser.test.d.ts +0 -6
- package/build/utils/trace-parser.test.js +0 -140
- package/build/utils/uiPageEnumerator.test.d.ts +0 -1
- package/build/utils/uiPageEnumerator.test.js +0 -824
- package/build/utils/utils.test.d.ts +0 -1
- package/build/utils/utils.test.js +0 -103
- package/build/utils/walkerCharacterization.test.d.ts +0 -1
- package/build/utils/walkerCharacterization.test.js +0 -233
- package/build/utils/workspaceAuth.test.d.ts +0 -1
- package/build/utils/workspaceAuth.test.js +0 -260
- package/build/workspace/testSuites.test.d.ts +0 -1
- package/build/workspace/testSuites.test.js +0 -23
- package/build/workspace/workspace.test.d.ts +0 -1
- package/build/workspace/workspace.test.js +0 -264
|
@@ -1,1428 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
-
import { jest } from "@jest/globals";
|
|
3
|
-
import { TestType } from "../types/TestTypes.js";
|
|
4
|
-
import { DriftAction } from "../types/TestAnalysis.js";
|
|
5
|
-
import { TestExecutionStatus } from "../types/TestExecution.js";
|
|
6
|
-
import * as fs from "fs/promises";
|
|
7
|
-
import * as path from "path";
|
|
8
|
-
import * as os from "os";
|
|
9
|
-
import { execFileSync } from "child_process";
|
|
10
|
-
import { z } from "zod";
|
|
11
|
-
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
12
|
-
logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
|
|
13
|
-
}));
|
|
14
|
-
jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
|
|
15
|
-
AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
|
|
16
|
-
}));
|
|
17
|
-
// @ts-ignore
|
|
18
|
-
const { registerSubmitReportTool, additionalRecommendationSchema, newTestSchema, targetElementSchema, pageContextSchema } = await import("./submitReportTool.js");
|
|
19
|
-
const { AnalyticsService } = await import("../services/AnalyticsService.js");
|
|
20
|
-
const { CandidateSource } = await import("../types/Recommendation.js");
|
|
21
|
-
const pushMCPToolEventMock = AnalyticsService.pushMCPToolEvent;
|
|
22
|
-
function captureToolHandler() {
|
|
23
|
-
let handler;
|
|
24
|
-
let inputSchema;
|
|
25
|
-
const fakeServer = {
|
|
26
|
-
registerTool: (_name, opts, fn) => {
|
|
27
|
-
handler = fn;
|
|
28
|
-
inputSchema = opts.inputSchema;
|
|
29
|
-
},
|
|
30
|
-
};
|
|
31
|
-
registerSubmitReportTool(fakeServer);
|
|
32
|
-
// Mirrors the real MCP SDK's registerTool: validates args against inputSchema via Zod
|
|
33
|
-
// before invoking the handler, so tests exercise the real tool contract (refinements
|
|
34
|
-
// included) instead of bypassing validation entirely.
|
|
35
|
-
return async (params) => {
|
|
36
|
-
const parsed = z.object(inputSchema).safeParse(params);
|
|
37
|
-
if (!parsed.success) {
|
|
38
|
-
return { isError: true, content: [{ type: "text", text: parsed.error.message }] };
|
|
39
|
-
}
|
|
40
|
-
return handler(parsed.data);
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
async function writeStateFile(dir, existingTests) {
|
|
44
|
-
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
45
|
-
// maintenanceVerdicts: [] simulates skyramp_actions having already run with nothing to
|
|
46
|
-
// do — the realistic state for tests not specifically exercising the maintenance flow.
|
|
47
|
-
await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts: [], metadata: sampleMetadata(dir) }), "utf-8");
|
|
48
|
-
return stateFile;
|
|
49
|
-
}
|
|
50
|
-
function sampleMetadata(dir) {
|
|
51
|
-
return {
|
|
52
|
-
sessionId: "test-session",
|
|
53
|
-
stateType: "unified",
|
|
54
|
-
repositoryPath: dir,
|
|
55
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
56
|
-
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
// SKYR-3906: state written by skyramp_actions after drift-analysis reconciliation.
|
|
60
|
-
async function writeReconciledStateFile(dir, existingTests, maintenanceVerdicts, ignoredTestFiles = []) {
|
|
61
|
-
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
62
|
-
await fs.writeFile(stateFile, JSON.stringify({ existingTests, maintenanceVerdicts, ignoredTestFiles, metadata: sampleMetadata(dir) }), "utf-8");
|
|
63
|
-
return stateFile;
|
|
64
|
-
}
|
|
65
|
-
function sampleReportParams(outputFile, stateFile) {
|
|
66
|
-
return {
|
|
67
|
-
summaryOutputFile: outputFile,
|
|
68
|
-
stateFile,
|
|
69
|
-
businessCaseAnalysis: "This PR adds product search. Tests needed for filtering.",
|
|
70
|
-
newTestsCreated: [
|
|
71
|
-
{ testId: "smoke-get-products-search", testType: TestType.SMOKE, category: "workflow", endpoint: "GET /api/v1/products/search", fileName: "search_smoke_test.py", description: "Searches products by category filter and asserts matching results are returned", reasoning: "Validates product search returns correct results for category filter" },
|
|
72
|
-
],
|
|
73
|
-
testResults: [
|
|
74
|
-
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "2.1s, products_smoke_test.py" },
|
|
75
|
-
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products/search", status: "Fail", details: "3.4s, search_smoke_test.py" },
|
|
76
|
-
],
|
|
77
|
-
issuesFound: [{ description: "Search endpoint returns 500 with category filter" }],
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
describe("registerSubmitReportTool", () => {
|
|
81
|
-
let handler;
|
|
82
|
-
let tmpDirs = [];
|
|
83
|
-
beforeAll(() => {
|
|
84
|
-
handler = captureToolHandler();
|
|
85
|
-
});
|
|
86
|
-
afterEach(async () => {
|
|
87
|
-
for (const dir of tmpDirs) {
|
|
88
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
89
|
-
}
|
|
90
|
-
tmpDirs = [];
|
|
91
|
-
});
|
|
92
|
-
it("should register the tool on the server with nextSteps in schema", () => {
|
|
93
|
-
const registerToolMock = jest.fn();
|
|
94
|
-
const fakeServer = { registerTool: registerToolMock };
|
|
95
|
-
registerSubmitReportTool(fakeServer);
|
|
96
|
-
expect(registerToolMock).toHaveBeenCalledWith("skyramp_submit_report", expect.objectContaining({
|
|
97
|
-
description: expect.any(String),
|
|
98
|
-
inputSchema: expect.objectContaining({
|
|
99
|
-
nextSteps: expect.any(Object),
|
|
100
|
-
}),
|
|
101
|
-
}), expect.any(Function));
|
|
102
|
-
});
|
|
103
|
-
it("should write JSON report to the specified file", async () => {
|
|
104
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
105
|
-
tmpDirs.push(tmpDir);
|
|
106
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
107
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
108
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
109
|
-
expect(result.isError).toBeUndefined();
|
|
110
|
-
expect(result.content[0].text).toContain("Report submitted successfully");
|
|
111
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
112
|
-
expect(written.businessCaseAnalysis).toBe("This PR adds product search. Tests needed for filtering.");
|
|
113
|
-
expect(written.newTestsCreated).toHaveLength(1);
|
|
114
|
-
expect(written.testResults).toHaveLength(2);
|
|
115
|
-
expect(written.issuesFound).toHaveLength(1);
|
|
116
|
-
expect(written.testMaintenance).toBeUndefined();
|
|
117
|
-
});
|
|
118
|
-
it("accepts and persists the per-item `repository` field", async () => {
|
|
119
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
120
|
-
tmpDirs.push(tmpDir);
|
|
121
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
122
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
123
|
-
const params = sampleReportParams(outputFile, stateFile);
|
|
124
|
-
// Attribute one item to a related repo, leave others on the primary.
|
|
125
|
-
params.newTestsCreated[0] = { ...params.newTestsCreated[0], repository: "acme/backend" };
|
|
126
|
-
params.testResults[0] = { ...params.testResults[0], repository: "acme/backend" };
|
|
127
|
-
params.issuesFound[0] = { ...params.issuesFound[0], repository: "acme/backend" };
|
|
128
|
-
const result = await handler(params);
|
|
129
|
-
expect(result.isError).toBeUndefined();
|
|
130
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
131
|
-
expect(written.newTestsCreated[0].repository).toBe("acme/backend");
|
|
132
|
-
expect(written.testResults[0].repository).toBe("acme/backend");
|
|
133
|
-
expect(written.issuesFound[0].repository).toBe("acme/backend");
|
|
134
|
-
// Items without a repository stay primary (undefined → omitted from JSON).
|
|
135
|
-
expect(written.testResults[1].repository).toBeUndefined();
|
|
136
|
-
});
|
|
137
|
-
it("normalizes a blank/whitespace-only `repository` to undefined", async () => {
|
|
138
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
139
|
-
tmpDirs.push(tmpDir);
|
|
140
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
141
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
142
|
-
const params = sampleReportParams(outputFile, stateFile);
|
|
143
|
-
params.testResults[0] = { ...params.testResults[0], repository: " " };
|
|
144
|
-
const result = await handler(params);
|
|
145
|
-
expect(result.isError).toBeUndefined();
|
|
146
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
147
|
-
// Blank string must not serialize as a distinct empty repo.
|
|
148
|
-
expect(written.testResults[0].repository).toBeUndefined();
|
|
149
|
-
});
|
|
150
|
-
it("should create parent directories if they don't exist", async () => {
|
|
151
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
152
|
-
tmpDirs.push(tmpDir);
|
|
153
|
-
const outputFile = path.join(tmpDir, "nested", "dirs", "report.json");
|
|
154
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
155
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
156
|
-
expect(result.isError).toBeUndefined();
|
|
157
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
158
|
-
expect(written.businessCaseAnalysis).toBeDefined();
|
|
159
|
-
});
|
|
160
|
-
it("should handle empty arrays for optional sections", async () => {
|
|
161
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
162
|
-
tmpDirs.push(tmpDir);
|
|
163
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
164
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
165
|
-
const result = await handler({
|
|
166
|
-
summaryOutputFile: outputFile,
|
|
167
|
-
stateFile,
|
|
168
|
-
businessCaseAnalysis: "Simple config change.",
|
|
169
|
-
newTestsCreated: [],
|
|
170
|
-
testResults: [
|
|
171
|
-
{ testType: TestType.SMOKE, endpoint: "GET /api/v1/products", status: "Pass", details: "1.0s, test.py" },
|
|
172
|
-
],
|
|
173
|
-
issuesFound: [],
|
|
174
|
-
});
|
|
175
|
-
expect(result.isError).toBeUndefined();
|
|
176
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
177
|
-
expect(written.newTestsCreated).toEqual([]);
|
|
178
|
-
expect(written.testMaintenance).toBeUndefined();
|
|
179
|
-
expect(written.issuesFound).toEqual([]);
|
|
180
|
-
expect(written.testResults).toHaveLength(1);
|
|
181
|
-
});
|
|
182
|
-
it("should return error for invalid file path", async () => {
|
|
183
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
184
|
-
tmpDirs.push(tmpDir);
|
|
185
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
186
|
-
const result = await handler(sampleReportParams("/nonexistent/readonly/path/report.json", stateFile));
|
|
187
|
-
expect(result.isError).toBe(true);
|
|
188
|
-
expect(result.content[0].text).toContain("Failed to write report");
|
|
189
|
-
});
|
|
190
|
-
it("should write commitMessage when provided", async () => {
|
|
191
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
192
|
-
tmpDirs.push(tmpDir);
|
|
193
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
194
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
195
|
-
const result = await handler({
|
|
196
|
-
...sampleReportParams(outputFile, stateFile),
|
|
197
|
-
commitMessage: "add contract tests for /products endpoint",
|
|
198
|
-
});
|
|
199
|
-
expect(result.isError).toBeUndefined();
|
|
200
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
201
|
-
expect(written.commitMessage).toBe("add contract tests for /products endpoint");
|
|
202
|
-
});
|
|
203
|
-
it("should use default commitMessage when omitted", async () => {
|
|
204
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
205
|
-
tmpDirs.push(tmpDir);
|
|
206
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
207
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
208
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
209
|
-
expect(result.isError).toBeUndefined();
|
|
210
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
211
|
-
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
212
|
-
});
|
|
213
|
-
it("should sanitize commitMessage (collapse newlines, trim whitespace)", async () => {
|
|
214
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
215
|
-
tmpDirs.push(tmpDir);
|
|
216
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
217
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
218
|
-
const result = await handler({
|
|
219
|
-
...sampleReportParams(outputFile, stateFile),
|
|
220
|
-
commitMessage: " line one\nline two\r\nline three ",
|
|
221
|
-
});
|
|
222
|
-
expect(result.isError).toBeUndefined();
|
|
223
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
224
|
-
expect(written.commitMessage).toBe("line one line two line three");
|
|
225
|
-
// No length cap — truncation is the consumer's responsibility (SKYR-3757).
|
|
226
|
-
});
|
|
227
|
-
it("should preserve commitMessage longer than 72 chars without truncation", async () => {
|
|
228
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
229
|
-
tmpDirs.push(tmpDir);
|
|
230
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
231
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
232
|
-
const longMessage = "add contract and integration tests for the new PATCH /orders/{order_id} endpoint including discount recalculation and line-item edits";
|
|
233
|
-
const result = await handler({
|
|
234
|
-
...sampleReportParams(outputFile, stateFile),
|
|
235
|
-
commitMessage: longMessage,
|
|
236
|
-
});
|
|
237
|
-
expect(result.isError).toBeUndefined();
|
|
238
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
239
|
-
expect(written.commitMessage).toBe(longMessage);
|
|
240
|
-
expect(written.commitMessage.length).toBeGreaterThan(72);
|
|
241
|
-
});
|
|
242
|
-
it("should use default commitMessage when whitespace-only", async () => {
|
|
243
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
244
|
-
tmpDirs.push(tmpDir);
|
|
245
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
246
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
247
|
-
const result = await handler({
|
|
248
|
-
...sampleReportParams(outputFile, stateFile),
|
|
249
|
-
commitMessage: " \n\r\n ",
|
|
250
|
-
});
|
|
251
|
-
expect(result.isError).toBeUndefined();
|
|
252
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
253
|
-
expect(written.commitMessage).toBe("Added recommendations by Skyramp Testbot.");
|
|
254
|
-
});
|
|
255
|
-
it("includes nextSteps array in written JSON", async () => {
|
|
256
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
257
|
-
tmpDirs.push(tmpDir);
|
|
258
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
259
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
260
|
-
const result = await handler({
|
|
261
|
-
...sampleReportParams(outputFile, stateFile),
|
|
262
|
-
nextSteps: ["Check your targetSetupCommand — endpoints returned 404"],
|
|
263
|
-
});
|
|
264
|
-
expect(result.isError).toBeUndefined();
|
|
265
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
266
|
-
expect(written.nextSteps).toEqual([
|
|
267
|
-
"Check your targetSetupCommand — endpoints returned 404",
|
|
268
|
-
]);
|
|
269
|
-
});
|
|
270
|
-
it("defaults nextSteps to empty array when omitted", async () => {
|
|
271
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
272
|
-
tmpDirs.push(tmpDir);
|
|
273
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
274
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
275
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
276
|
-
expect(result.isError).toBeUndefined();
|
|
277
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
278
|
-
expect(written.nextSteps).toEqual([]);
|
|
279
|
-
});
|
|
280
|
-
it("should produce valid JSON with pretty formatting", async () => {
|
|
281
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
282
|
-
tmpDirs.push(tmpDir);
|
|
283
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
284
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
285
|
-
await handler(sampleReportParams(outputFile, stateFile));
|
|
286
|
-
const raw = await fs.readFile(outputFile, "utf-8");
|
|
287
|
-
// Pretty-printed JSON should have indentation
|
|
288
|
-
expect(raw).toContain(" ");
|
|
289
|
-
// Should not contain summaryOutputFile in the output
|
|
290
|
-
expect(raw).not.toContain("summaryOutputFile");
|
|
291
|
-
});
|
|
292
|
-
it("additionalRecommendations serialises primaryEndpoint into the report JSON", async () => {
|
|
293
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
294
|
-
tmpDirs.push(tmpDir);
|
|
295
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
296
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
297
|
-
const params = {
|
|
298
|
-
...sampleReportParams(outputFile, stateFile),
|
|
299
|
-
additionalRecommendations: [
|
|
300
|
-
{
|
|
301
|
-
testId: "integration-orders-patch-status",
|
|
302
|
-
testType: TestType.INTEGRATION,
|
|
303
|
-
category: "business_rule",
|
|
304
|
-
primaryEndpoint: "PATCH /api/v1/orders/{order_id}",
|
|
305
|
-
scenarioName: "orders-patch-status-transition",
|
|
306
|
-
steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order status" }],
|
|
307
|
-
description: "Verifies status transitions persist across PATCH calls",
|
|
308
|
-
priority: "high",
|
|
309
|
-
reasoning: "Catches broken state machine transitions on the new PATCH endpoint",
|
|
310
|
-
},
|
|
311
|
-
],
|
|
312
|
-
};
|
|
313
|
-
const result = await handler(params);
|
|
314
|
-
expect(result.isError).toBeUndefined();
|
|
315
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
316
|
-
expect(written.additionalRecommendations[0].primaryEndpoint).toBe("PATCH /api/v1/orders/{order_id}");
|
|
317
|
-
});
|
|
318
|
-
it("additionalRecommendationSchema rejects a contract test missing primaryEndpoint", () => {
|
|
319
|
-
const result = additionalRecommendationSchema.safeParse({
|
|
320
|
-
testId: "contract-orders-patch-404",
|
|
321
|
-
testType: TestType.CONTRACT,
|
|
322
|
-
category: "error_handling",
|
|
323
|
-
// primaryEndpoint intentionally omitted — required for contract tests
|
|
324
|
-
scenarioName: "orders-patch-not-found",
|
|
325
|
-
steps: [{ method: "PATCH", path: "/api/v1/orders/99999", description: "Patch non-existent order" }],
|
|
326
|
-
description: "Verifies 404 for unknown order_id",
|
|
327
|
-
priority: "high",
|
|
328
|
-
reasoning: "Validates the 404 guard on the new PATCH endpoint",
|
|
329
|
-
});
|
|
330
|
-
expect(result.success).toBe(false);
|
|
331
|
-
const issue = result.error?.issues.find(i => i.path.includes("primaryEndpoint"));
|
|
332
|
-
expect(issue).toBeDefined();
|
|
333
|
-
});
|
|
334
|
-
it("additionalRecommendationSchema accepts a contract test with primaryEndpoint", () => {
|
|
335
|
-
const result = additionalRecommendationSchema.safeParse({
|
|
336
|
-
testId: "contract-orders-patch-404",
|
|
337
|
-
testType: TestType.CONTRACT,
|
|
338
|
-
category: "error_handling",
|
|
339
|
-
primaryEndpoint: "PATCH /api/v1/orders/{order_id}",
|
|
340
|
-
scenarioName: "orders-patch-not-found",
|
|
341
|
-
steps: [{ method: "PATCH", path: "/api/v1/orders/99999", description: "Patch non-existent order" }],
|
|
342
|
-
description: "Verifies 404 for unknown order_id",
|
|
343
|
-
priority: "high",
|
|
344
|
-
reasoning: "Validates the 404 guard on the new PATCH endpoint",
|
|
345
|
-
});
|
|
346
|
-
expect(result.success).toBe(true);
|
|
347
|
-
});
|
|
348
|
-
it("additionalRecommendationSchema accepts a multi-step integration test without primaryEndpoint", () => {
|
|
349
|
-
// For multi-step scenarios, primaryEndpoint is optional — steps is the authoritative source
|
|
350
|
-
const result = additionalRecommendationSchema.safeParse({
|
|
351
|
-
testId: "integration-orders-patch-mutation",
|
|
352
|
-
testType: TestType.INTEGRATION,
|
|
353
|
-
category: "business_rule",
|
|
354
|
-
// primaryEndpoint intentionally omitted for multi-step scenario
|
|
355
|
-
scenarioName: "orders-patch-mutation-verify",
|
|
356
|
-
steps: [
|
|
357
|
-
{ method: "POST", path: "/api/v1/products", description: "Create product" },
|
|
358
|
-
{ method: "POST", path: "/api/v1/orders", description: "Create order" },
|
|
359
|
-
{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" },
|
|
360
|
-
],
|
|
361
|
-
description: "Verifies total_amount recalculates after PATCH",
|
|
362
|
-
priority: "high",
|
|
363
|
-
reasoning: "Catches item accumulation bug",
|
|
364
|
-
});
|
|
365
|
-
expect(result.success).toBe(true);
|
|
366
|
-
if (result.success) {
|
|
367
|
-
expect(result.data.primaryEndpoint).toBeUndefined();
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
// Structured grounding fields for UI recommendations.
|
|
371
|
-
// ----------------------------------------------------------------------
|
|
372
|
-
// Sub-schemas — targetElement and pageContext — populated by the agent
|
|
373
|
-
// from a captured browser_blueprint element. UI-only via superRefine on
|
|
374
|
-
// the parent newTestSchema / additionalRecommendationSchema.
|
|
375
|
-
describe("targetElementSchema", () => {
|
|
376
|
-
const validTarget = {
|
|
377
|
-
role: "button",
|
|
378
|
-
accessibleName: "Save changes",
|
|
379
|
-
testId: "save-changes-btn",
|
|
380
|
-
stableId: null,
|
|
381
|
-
contextText: null,
|
|
382
|
-
mutability: "mutable",
|
|
383
|
-
widgetType: "native",
|
|
384
|
-
};
|
|
385
|
-
it("accepts a fully-populated targetElement", () => {
|
|
386
|
-
expect(targetElementSchema.safeParse(validTarget).success).toBe(true);
|
|
387
|
-
});
|
|
388
|
-
it("accepts a minimal targetElement (only required fields)", () => {
|
|
389
|
-
const result = targetElementSchema.safeParse({
|
|
390
|
-
role: "heading",
|
|
391
|
-
accessibleName: "Notifications",
|
|
392
|
-
testId: null,
|
|
393
|
-
stableId: null,
|
|
394
|
-
contextText: null,
|
|
395
|
-
});
|
|
396
|
-
expect(result.success).toBe(true);
|
|
397
|
-
});
|
|
398
|
-
it("accepts contextText as a string array (repeating-row case)", () => {
|
|
399
|
-
const result = targetElementSchema.safeParse({
|
|
400
|
-
...validTarget,
|
|
401
|
-
contextText: ["customer@example.com", "$129.99", "Pending"],
|
|
402
|
-
});
|
|
403
|
-
expect(result.success).toBe(true);
|
|
404
|
-
});
|
|
405
|
-
it("rejects targetElement missing accessibleName", () => {
|
|
406
|
-
const { accessibleName: _accessibleName, ...withoutName } = validTarget;
|
|
407
|
-
const result = targetElementSchema.safeParse(withoutName);
|
|
408
|
-
expect(result.success).toBe(false);
|
|
409
|
-
});
|
|
410
|
-
it("rejects targetElement missing role", () => {
|
|
411
|
-
const { role: _role, ...withoutRole } = validTarget;
|
|
412
|
-
const result = targetElementSchema.safeParse(withoutRole);
|
|
413
|
-
expect(result.success).toBe(false);
|
|
414
|
-
});
|
|
415
|
-
it("rejects an invalid mutability enum value", () => {
|
|
416
|
-
const result = targetElementSchema.safeParse({ ...validTarget, mutability: "sometimes" });
|
|
417
|
-
expect(result.success).toBe(false);
|
|
418
|
-
});
|
|
419
|
-
it("rejects an invalid widgetType enum value", () => {
|
|
420
|
-
const result = targetElementSchema.safeParse({ ...validTarget, widgetType: "fancy" });
|
|
421
|
-
expect(result.success).toBe(false);
|
|
422
|
-
});
|
|
423
|
-
});
|
|
424
|
-
describe("pageContextSchema (UI recommendation grounding)", () => {
|
|
425
|
-
it("accepts pageContext with both url and pageHash", () => {
|
|
426
|
-
const result = pageContextSchema.safeParse({
|
|
427
|
-
url: "http://localhost:5173/orders/1",
|
|
428
|
-
pageHash: "10:434266447",
|
|
429
|
-
});
|
|
430
|
-
expect(result.success).toBe(true);
|
|
431
|
-
});
|
|
432
|
-
it("accepts pageContext without pageHash (optional)", () => {
|
|
433
|
-
const result = pageContextSchema.safeParse({ url: "http://localhost:5173/orders/1" });
|
|
434
|
-
expect(result.success).toBe(true);
|
|
435
|
-
});
|
|
436
|
-
it("rejects pageContext missing url", () => {
|
|
437
|
-
const result = pageContextSchema.safeParse({ pageHash: "10:abc" });
|
|
438
|
-
expect(result.success).toBe(false);
|
|
439
|
-
});
|
|
440
|
-
});
|
|
441
|
-
describe("newTestSchema with grounding fields (UI recommendation grounding)", () => {
|
|
442
|
-
const validTarget = {
|
|
443
|
-
role: "button",
|
|
444
|
-
accessibleName: "Save changes",
|
|
445
|
-
testId: "save-changes-btn",
|
|
446
|
-
stableId: null,
|
|
447
|
-
contextText: null,
|
|
448
|
-
};
|
|
449
|
-
const validPage = {
|
|
450
|
-
url: "http://localhost:5173/orders/1",
|
|
451
|
-
pageHash: "10:434266447",
|
|
452
|
-
};
|
|
453
|
-
const baseUITest = {
|
|
454
|
-
testId: "ui-edit-order-discount-validation",
|
|
455
|
-
testType: TestType.UI,
|
|
456
|
-
category: "business_rule",
|
|
457
|
-
endpoint: "PATCH /api/v1/orders/{id}",
|
|
458
|
-
fileName: "edit_order_discount.spec.ts",
|
|
459
|
-
description: "Navigates to the Edit Order form, enters a discount > 100, and asserts that the validation error appears and the Save button is disabled.",
|
|
460
|
-
reasoning: "Verifies the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
461
|
-
};
|
|
462
|
-
it("accepts a UI test with single-element targetElements + pageContext", () => {
|
|
463
|
-
const result = newTestSchema.safeParse({
|
|
464
|
-
...baseUITest,
|
|
465
|
-
targetElements: [validTarget],
|
|
466
|
-
pageContext: validPage,
|
|
467
|
-
});
|
|
468
|
-
expect(result.success).toBe(true);
|
|
469
|
-
});
|
|
470
|
-
it("accepts a UI test with multi-element targetElements (render-state test)", () => {
|
|
471
|
-
const result = newTestSchema.safeParse({
|
|
472
|
-
...baseUITest,
|
|
473
|
-
targetElements: [
|
|
474
|
-
{
|
|
475
|
-
role: "heading",
|
|
476
|
-
accessibleName: "Edit Order",
|
|
477
|
-
testId: "edit-order-heading",
|
|
478
|
-
stableId: null,
|
|
479
|
-
contextText: null,
|
|
480
|
-
},
|
|
481
|
-
{
|
|
482
|
-
role: "spinbutton",
|
|
483
|
-
accessibleName: "Discount (%)",
|
|
484
|
-
testId: "edit-order-input-discount",
|
|
485
|
-
stableId: null,
|
|
486
|
-
contextText: null,
|
|
487
|
-
},
|
|
488
|
-
],
|
|
489
|
-
pageContext: validPage,
|
|
490
|
-
});
|
|
491
|
-
expect(result.success).toBe(true);
|
|
492
|
-
});
|
|
493
|
-
it("rejects a UI test without targetElements (required for UI grounding)", () => {
|
|
494
|
-
const result = newTestSchema.safeParse(baseUITest);
|
|
495
|
-
expect(result.success).toBe(false);
|
|
496
|
-
expect(JSON.stringify(result.error?.issues)).toContain("targetElements is required for testType: 'ui'");
|
|
497
|
-
});
|
|
498
|
-
it("accepts a UI test with targetElements: null (fallback case)", () => {
|
|
499
|
-
const result = newTestSchema.safeParse({
|
|
500
|
-
...baseUITest,
|
|
501
|
-
targetElements: null,
|
|
502
|
-
description: "[no-blueprint-data] Navigates to Edit Order form and checks discount validation error.",
|
|
503
|
-
reasoning: "[no-blueprint-data] Verifies the discount validation error appears on the Edit Order form when discount > 100.",
|
|
504
|
-
});
|
|
505
|
-
expect(result.success).toBe(true);
|
|
506
|
-
});
|
|
507
|
-
it("rejects a contract test that includes targetElements (UI-only)", () => {
|
|
508
|
-
const result = newTestSchema.safeParse({
|
|
509
|
-
testId: "contract-orders-patch",
|
|
510
|
-
testType: TestType.CONTRACT,
|
|
511
|
-
category: "business_rule",
|
|
512
|
-
endpoint: "PATCH /api/v1/orders/{id}",
|
|
513
|
-
fileName: "orders_patch_contract.py",
|
|
514
|
-
reasoning: "Validates request shape and 200/422 status codes",
|
|
515
|
-
targetElements: [validTarget],
|
|
516
|
-
description: "calls the PATCH /api/v1/orders/{id} API and validates it implements its contract.",
|
|
517
|
-
});
|
|
518
|
-
expect(result.success).toBe(false);
|
|
519
|
-
if (!result.success) {
|
|
520
|
-
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
it("rejects an integration test that includes pageContext (UI-only)", () => {
|
|
524
|
-
const result = newTestSchema.safeParse({
|
|
525
|
-
testId: "integration-orders",
|
|
526
|
-
testType: TestType.INTEGRATION,
|
|
527
|
-
category: "business_rule",
|
|
528
|
-
endpoint: "POST /api/v1/orders",
|
|
529
|
-
fileName: "orders_integration.py",
|
|
530
|
-
reasoning: "Multi-step order lifecycle",
|
|
531
|
-
pageContext: validPage,
|
|
532
|
-
description: "calls the POST /api/v1/orders API to create an order and then do stuff.",
|
|
533
|
-
});
|
|
534
|
-
expect(result.success).toBe(false);
|
|
535
|
-
if (!result.success) {
|
|
536
|
-
expect(result.error.issues.some((i) => i.path.includes("pageContext"))).toBe(true);
|
|
537
|
-
}
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
describe("additionalRecommendationSchema with grounding fields (UI recommendation grounding)", () => {
|
|
541
|
-
const validTarget = {
|
|
542
|
-
role: "button",
|
|
543
|
-
accessibleName: "Save changes",
|
|
544
|
-
testId: "save-changes-btn",
|
|
545
|
-
stableId: null,
|
|
546
|
-
contextText: null,
|
|
547
|
-
};
|
|
548
|
-
const validPage = { url: "http://localhost:5173/orders/1" };
|
|
549
|
-
it("accepts a UI rec with targetElements + pageContext", () => {
|
|
550
|
-
const result = additionalRecommendationSchema.safeParse({
|
|
551
|
-
testId: "ui-edit-order-discount-validation",
|
|
552
|
-
testType: TestType.UI,
|
|
553
|
-
category: "business_rule",
|
|
554
|
-
steps: [{ description: "Open Edit Order, type 150 in discount, click Save" }],
|
|
555
|
-
description: "Verify discount > 100 shows validation error",
|
|
556
|
-
priority: "medium",
|
|
557
|
-
reasoning: "Verify that the **Save changes** button (testid `save-changes-btn`) on the Edit Order form shows the validation error when discount > 100.",
|
|
558
|
-
targetElements: [validTarget],
|
|
559
|
-
pageContext: validPage,
|
|
560
|
-
});
|
|
561
|
-
expect(result.success).toBe(true);
|
|
562
|
-
});
|
|
563
|
-
it("rejects a non-UI rec that includes targetElements", () => {
|
|
564
|
-
const result = additionalRecommendationSchema.safeParse({
|
|
565
|
-
testId: "integration-orders-patch",
|
|
566
|
-
testType: TestType.INTEGRATION,
|
|
567
|
-
category: "business_rule",
|
|
568
|
-
steps: [{ method: "PATCH", path: "/api/v1/orders/1", description: "Patch order" }],
|
|
569
|
-
description: "Verifies PATCH workflow",
|
|
570
|
-
priority: "high",
|
|
571
|
-
reasoning: "Catches mutation bugs",
|
|
572
|
-
targetElements: [validTarget],
|
|
573
|
-
});
|
|
574
|
-
expect(result.success).toBe(false);
|
|
575
|
-
if (!result.success) {
|
|
576
|
-
expect(result.error.issues.some((i) => i.path.includes("targetElements"))).toBe(true);
|
|
577
|
-
}
|
|
578
|
-
});
|
|
579
|
-
});
|
|
580
|
-
describe("analytics metrics", () => {
|
|
581
|
-
beforeEach(() => {
|
|
582
|
-
pushMCPToolEventMock.mockClear();
|
|
583
|
-
});
|
|
584
|
-
function lastAnalyticsParams() {
|
|
585
|
-
expect(pushMCPToolEventMock).toHaveBeenCalled();
|
|
586
|
-
const calls = pushMCPToolEventMock.mock.calls;
|
|
587
|
-
return calls[calls.length - 1][2];
|
|
588
|
-
}
|
|
589
|
-
it("emits per-run count metrics derived from the report", async () => {
|
|
590
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
591
|
-
tmpDirs.push(tmpDir);
|
|
592
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
593
|
-
// sampleReportParams: 1 new test, 2 results (1 Pass + 1 Fail), 1 issue (no severity).
|
|
594
|
-
// Plus 1 maintenance verdict (no matching existingTests entry) from state.
|
|
595
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [], [
|
|
596
|
-
{ testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Updated auth flow in existing tests", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
|
|
597
|
-
]);
|
|
598
|
-
await handler(sampleReportParams(outputFile, stateFile));
|
|
599
|
-
const params = lastAnalyticsParams();
|
|
600
|
-
expect(params.testsGenerated).toBe("1");
|
|
601
|
-
expect(params.testsMaintained).toBe("1");
|
|
602
|
-
expect(params.recommendationsCount).toBe("0");
|
|
603
|
-
expect(params.maintenanceRecovered).toBe("0"); // no matching stateFile entry → no execution data
|
|
604
|
-
expect(params.testsPassed).toBe("1");
|
|
605
|
-
expect(params.testsFailed).toBe("1");
|
|
606
|
-
expect(params.testsSkipped).toBe("0");
|
|
607
|
-
expect(params.issuesFound).toBe("1");
|
|
608
|
-
});
|
|
609
|
-
it("breaks issues down by severity", async () => {
|
|
610
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
611
|
-
tmpDirs.push(tmpDir);
|
|
612
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
613
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
614
|
-
await handler({
|
|
615
|
-
...sampleReportParams(outputFile, stateFile),
|
|
616
|
-
issuesFound: [
|
|
617
|
-
{ description: "Data corruption on checkout", severity: "critical" },
|
|
618
|
-
{ description: "Wrong total returned", severity: "high" },
|
|
619
|
-
{ description: "Another wrong total", severity: "high" },
|
|
620
|
-
{ description: "Minor gap", severity: "medium" },
|
|
621
|
-
{ description: "Unlabeled issue" },
|
|
622
|
-
],
|
|
623
|
-
});
|
|
624
|
-
const params = lastAnalyticsParams();
|
|
625
|
-
expect(params.issuesFound).toBe("5");
|
|
626
|
-
expect(params.issuesCritical).toBe("1");
|
|
627
|
-
expect(params.issuesHigh).toBe("2");
|
|
628
|
-
expect(params.issuesMedium).toBe("1");
|
|
629
|
-
expect(params.issuesLow).toBe("0");
|
|
630
|
-
});
|
|
631
|
-
it("reports zero metrics for an empty report", async () => {
|
|
632
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
633
|
-
tmpDirs.push(tmpDir);
|
|
634
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
635
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
636
|
-
await handler({
|
|
637
|
-
summaryOutputFile: outputFile,
|
|
638
|
-
stateFile,
|
|
639
|
-
businessCaseAnalysis: "Config-only change.",
|
|
640
|
-
newTestsCreated: [],
|
|
641
|
-
testResults: [],
|
|
642
|
-
issuesFound: [],
|
|
643
|
-
});
|
|
644
|
-
const params = lastAnalyticsParams();
|
|
645
|
-
expect(params.testsGenerated).toBe("0");
|
|
646
|
-
expect(params.testsMaintained).toBe("0");
|
|
647
|
-
expect(params.maintenanceRecovered).toBe("0");
|
|
648
|
-
expect(params.testsPassed).toBe("0");
|
|
649
|
-
expect(params.issuesFound).toBe("0");
|
|
650
|
-
});
|
|
651
|
-
it("testsMaintained counts only change actions, not VERIFY/IGNORE no-ops", async () => {
|
|
652
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
653
|
-
tmpDirs.push(tmpDir);
|
|
654
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
655
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [], [
|
|
656
|
-
// Real change
|
|
657
|
-
{ testFilePath: "/repo/products_contract_test.py", action: DriftAction.Update, rationale: "Patched auth", testType: TestType.CONTRACT, endpoint: "GET /api/v1/products" },
|
|
658
|
-
{ testFilePath: "/repo/orders_smoke_test.py", action: DriftAction.Regenerate, rationale: "Rewrote for new shape", testType: TestType.SMOKE, endpoint: "GET /api/v1/orders" },
|
|
659
|
-
// No-ops — assessed but not modified
|
|
660
|
-
{ testFilePath: "/repo/reviews_smoke_test.py", action: DriftAction.Ignore, rationale: "No action required", testType: TestType.SMOKE, endpoint: "GET /api/v1/reviews" },
|
|
661
|
-
{ testFilePath: "/repo/coupons_smoke_test.py", action: DriftAction.Verify, rationale: "Verified still valid", testType: TestType.SMOKE, endpoint: "GET /api/v1/coupons" },
|
|
662
|
-
]);
|
|
663
|
-
await handler(sampleReportParams(outputFile, stateFile));
|
|
664
|
-
const params = lastAnalyticsParams();
|
|
665
|
-
// 2 change actions (UPDATE + REGENERATE), not 4
|
|
666
|
-
expect(params.testsMaintained).toBe("2");
|
|
667
|
-
// No matching stateFile entries — execution data unavailable
|
|
668
|
-
expect(params.maintenanceRecovered).toBe("0");
|
|
669
|
-
});
|
|
670
|
-
});
|
|
671
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
672
|
-
// testMaintenance output shapes
|
|
673
|
-
// Each case documents exactly what the MCP writes to JSON, which is what the
|
|
674
|
-
// testbot renderer reads. Keeping these in sync prevents silent regressions
|
|
675
|
-
// when toOutput or the stateFile enrichment path changes.
|
|
676
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
677
|
-
describe("testMaintenance output shapes", () => {
|
|
678
|
-
const baseVerdict = {
|
|
679
|
-
testFilePath: "/repo/backend/tests/orders_contract_test.py",
|
|
680
|
-
rationale: "Updated discount fields in assertions",
|
|
681
|
-
testType: TestType.CONTRACT,
|
|
682
|
-
endpoint: "GET /api/v1/orders",
|
|
683
|
-
};
|
|
684
|
-
async function submitAndReadMaintenance(tmpDir, stateFile, testMaintenanceDetails = []) {
|
|
685
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
686
|
-
const result = await handler({
|
|
687
|
-
summaryOutputFile: outputFile,
|
|
688
|
-
stateFile,
|
|
689
|
-
businessCaseAnalysis: "Test",
|
|
690
|
-
newTestsCreated: [],
|
|
691
|
-
testMaintenanceDetails,
|
|
692
|
-
testResults: [],
|
|
693
|
-
issuesFound: [],
|
|
694
|
-
});
|
|
695
|
-
if (result.isError)
|
|
696
|
-
throw new Error(`handler rejected: ${result.content?.[0]?.text}`);
|
|
697
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
698
|
-
return written.testMaintenance;
|
|
699
|
-
}
|
|
700
|
-
// ── No matching existingTests entry ───────────────────────────────────────
|
|
701
|
-
it("UPDATE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Unknown", async () => {
|
|
702
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
703
|
-
tmpDirs.push(tmpDir);
|
|
704
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
705
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
706
|
-
expect(result[0].beforeStatus).toBe("Unknown");
|
|
707
|
-
expect(result[0].afterStatus).toBe("Unknown");
|
|
708
|
-
expect(result[0].beforeDetails).toBe("");
|
|
709
|
-
expect(result[0].afterDetails).toBe("");
|
|
710
|
-
});
|
|
711
|
-
it("IGNORE with no matching existingTests entry: both Skipped", async () => {
|
|
712
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
713
|
-
tmpDirs.push(tmpDir);
|
|
714
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Ignore }]);
|
|
715
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
716
|
-
expect(result[0].beforeStatus).toBe("Skipped");
|
|
717
|
-
expect(result[0].afterStatus).toBe("Skipped");
|
|
718
|
-
expect(result[0].beforeDetails).toBe("");
|
|
719
|
-
expect(result[0].afterDetails).toBe("");
|
|
720
|
-
});
|
|
721
|
-
it("DELETE with no matching existingTests entry: beforeStatus=Unknown, afterStatus=Skipped", async () => {
|
|
722
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
723
|
-
tmpDirs.push(tmpDir);
|
|
724
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
725
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile);
|
|
726
|
-
expect(result[0].beforeStatus).toBe("Unknown");
|
|
727
|
-
expect(result[0].afterStatus).toBe("Skipped");
|
|
728
|
-
});
|
|
729
|
-
// ── With matching existingTests entry ─────────────────────────────────────
|
|
730
|
-
it("UPDATE with before=Pass after=Pass: status from stateFile, details from the LLM-drafted testMaintenanceDetails", async () => {
|
|
731
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
732
|
-
tmpDirs.push(tmpDir);
|
|
733
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
734
|
-
testFile: baseVerdict.testFilePath,
|
|
735
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
736
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 12880 },
|
|
737
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
738
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
739
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "5 passed in 12.88s" },
|
|
740
|
-
]);
|
|
741
|
-
expect(result[0].beforeStatus).toBe("Pass");
|
|
742
|
-
expect(result[0].beforeDetails).toBe("4 passed in 15.09s");
|
|
743
|
-
expect(result[0].afterStatus).toBe("Pass");
|
|
744
|
-
expect(result[0].afterDetails).toBe("5 passed in 12.88s");
|
|
745
|
-
});
|
|
746
|
-
it("UPDATE with before=Pass after=Fail: rejects when afterDetails is missing for a recorded execution", async () => {
|
|
747
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
748
|
-
tmpDirs.push(tmpDir);
|
|
749
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
750
|
-
testFile: baseVerdict.testFilePath,
|
|
751
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
752
|
-
executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
|
|
753
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
754
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
755
|
-
const result = await handler({
|
|
756
|
-
summaryOutputFile: outputFile,
|
|
757
|
-
stateFile,
|
|
758
|
-
businessCaseAnalysis: "Test",
|
|
759
|
-
newTestsCreated: [],
|
|
760
|
-
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "" }], // afterDetails left blank
|
|
761
|
-
testResults: [],
|
|
762
|
-
issuesFound: [],
|
|
763
|
-
});
|
|
764
|
-
expect(result.isError).toBe(true);
|
|
765
|
-
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
766
|
-
});
|
|
767
|
-
it("UPDATE with before=Pass after=Fail: afterDetails carries the LLM's drafted root cause when supplied", async () => {
|
|
768
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
769
|
-
tmpDirs.push(tmpDir);
|
|
770
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
771
|
-
testFile: baseVerdict.testFilePath,
|
|
772
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 15090 },
|
|
773
|
-
executionAfter: { status: TestExecutionStatus.Fail, errors: ["check_schema fails, test isolation bug"], duration: 10940 },
|
|
774
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
775
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
776
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 15.09s", afterDetails: "FAILED test_orders_get — check_schema fails, test isolation bug" },
|
|
777
|
-
]);
|
|
778
|
-
expect(result[0].beforeStatus).toBe("Pass");
|
|
779
|
-
expect(result[0].afterStatus).toBe("Fail");
|
|
780
|
-
expect(result[0].afterDetails).toBe("FAILED test_orders_get — check_schema fails, test isolation bug");
|
|
781
|
-
});
|
|
782
|
-
it("UPDATE with crashed execution: status is Error", async () => {
|
|
783
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
784
|
-
tmpDirs.push(tmpDir);
|
|
785
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
786
|
-
testFile: baseVerdict.testFilePath,
|
|
787
|
-
executionBefore: { status: TestExecutionStatus.Error, errors: ["Connection refused"], duration: 500 },
|
|
788
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
789
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
790
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
791
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "Error — Connection refused", afterDetails: "5 passed in 2.00s" },
|
|
792
|
-
]);
|
|
793
|
-
expect(result[0].beforeStatus).toBe("Error");
|
|
794
|
-
expect(result[0].afterStatus).toBe("Pass");
|
|
795
|
-
});
|
|
796
|
-
it("UPDATE Fail→Pass: counted as maintenanceRecovered in analytics", async () => {
|
|
797
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
798
|
-
tmpDirs.push(tmpDir);
|
|
799
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
800
|
-
testFile: baseVerdict.testFilePath,
|
|
801
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["FAILED test_foo - assert False"], duration: 5000 },
|
|
802
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5000 },
|
|
803
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
804
|
-
await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
805
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED test_foo - assert False", afterDetails: "5 passed in 5.00s" },
|
|
806
|
-
]);
|
|
807
|
-
expect(pushMCPToolEventMock.mock.calls.at(-1)?.[2].maintenanceRecovered).toBe("1");
|
|
808
|
-
});
|
|
809
|
-
it("UPDATE with no before-execution recorded: beforeStatus=Unknown, afterStatus=Pass, no beforeDetails required", async () => {
|
|
810
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
811
|
-
tmpDirs.push(tmpDir);
|
|
812
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
813
|
-
testFile: baseVerdict.testFilePath,
|
|
814
|
-
// executionBefore absent — agent skipped before-phase
|
|
815
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
816
|
-
}], [{ ...baseVerdict, action: DriftAction.Update }]);
|
|
817
|
-
// Only afterDetails is meaningful — there's no recorded before-execution to summarize,
|
|
818
|
-
// so beforeDetails is the empty-string sentinel (the field is required, not omittable).
|
|
819
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
820
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "", afterDetails: "5 passed in 2.00s" },
|
|
821
|
-
]);
|
|
822
|
-
expect(result[0].beforeStatus).toBe("Unknown");
|
|
823
|
-
expect(result[0].afterStatus).toBe("Pass");
|
|
824
|
-
});
|
|
825
|
-
it("DELETE with before-execution, no after (file deleted): beforeStatus=Pass, afterStatus=Skipped, no afterDetails required", async () => {
|
|
826
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
827
|
-
tmpDirs.push(tmpDir);
|
|
828
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
829
|
-
testFile: baseVerdict.testFilePath,
|
|
830
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
831
|
-
// executionAfter absent — file was deleted, nothing to run
|
|
832
|
-
}], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
833
|
-
// Only beforeDetails is meaningful — there's no recorded after-execution to summarize,
|
|
834
|
-
// so afterDetails is the empty-string sentinel (the field is required, not omittable).
|
|
835
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
836
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" },
|
|
837
|
-
]);
|
|
838
|
-
expect(result[0].beforeStatus).toBe("Pass");
|
|
839
|
-
expect(result[0].afterStatus).toBe("Skipped");
|
|
840
|
-
});
|
|
841
|
-
it("REGENERATE with before=Fail after=Pass: requires both details, rejects when either is missing", async () => {
|
|
842
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
843
|
-
tmpDirs.push(tmpDir);
|
|
844
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
845
|
-
testFile: baseVerdict.testFilePath,
|
|
846
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["schema drift"], duration: 4000 },
|
|
847
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 3000 },
|
|
848
|
-
}], [{ ...baseVerdict, action: DriftAction.Regenerate }]);
|
|
849
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
850
|
-
const rejected = await handler({
|
|
851
|
-
summaryOutputFile: outputFile,
|
|
852
|
-
stateFile,
|
|
853
|
-
businessCaseAnalysis: "Test",
|
|
854
|
-
newTestsCreated: [],
|
|
855
|
-
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "" }], // afterDetails left blank
|
|
856
|
-
testResults: [],
|
|
857
|
-
issuesFound: [],
|
|
858
|
-
});
|
|
859
|
-
expect(rejected.isError).toBe(true);
|
|
860
|
-
expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
861
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
862
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "FAILED — schema drift", afterDetails: "regenerated test now passes" },
|
|
863
|
-
]);
|
|
864
|
-
expect(result[0].beforeStatus).toBe("Fail");
|
|
865
|
-
expect(result[0].afterStatus).toBe("Pass");
|
|
866
|
-
});
|
|
867
|
-
it("VERIFY with a recorded execution: requires details even though VERIFY is not a change action", async () => {
|
|
868
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
869
|
-
tmpDirs.push(tmpDir);
|
|
870
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
871
|
-
testFile: baseVerdict.testFilePath,
|
|
872
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 1500 },
|
|
873
|
-
}], [{ ...baseVerdict, action: DriftAction.Verify }]);
|
|
874
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
875
|
-
const rejected = await handler({
|
|
876
|
-
summaryOutputFile: outputFile,
|
|
877
|
-
stateFile,
|
|
878
|
-
businessCaseAnalysis: "Test",
|
|
879
|
-
newTestsCreated: [],
|
|
880
|
-
testMaintenanceDetails: [],
|
|
881
|
-
testResults: [],
|
|
882
|
-
issuesFound: [],
|
|
883
|
-
});
|
|
884
|
-
expect(rejected.isError).toBe(true);
|
|
885
|
-
expect(rejected.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
886
|
-
const result = await submitAndReadMaintenance(tmpDir, stateFile, [
|
|
887
|
-
{ testFilePath: baseVerdict.testFilePath, beforeDetails: "confirmed still passes, no changes needed", afterDetails: "" },
|
|
888
|
-
]);
|
|
889
|
-
expect(result[0].beforeStatus).toBe("Pass");
|
|
890
|
-
});
|
|
891
|
-
it("DELETE with a spurious executionAfter recorded: afterDetails is still required despite DELETE's default afterStatus being Skipped", async () => {
|
|
892
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
893
|
-
tmpDirs.push(tmpDir);
|
|
894
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
895
|
-
testFile: baseVerdict.testFilePath,
|
|
896
|
-
executionBefore: { status: TestExecutionStatus.Pass, errors: [], duration: 2000 },
|
|
897
|
-
// executionAfter present even though the file should have been deleted — documents
|
|
898
|
-
// that the required-details gate keys off recorded executions, not the action type.
|
|
899
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 1800 },
|
|
900
|
-
}], [{ ...baseVerdict, action: DriftAction.Delete }]);
|
|
901
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
902
|
-
const rejected = await handler({
|
|
903
|
-
summaryOutputFile: outputFile,
|
|
904
|
-
stateFile,
|
|
905
|
-
businessCaseAnalysis: "Test",
|
|
906
|
-
newTestsCreated: [],
|
|
907
|
-
testMaintenanceDetails: [{ testFilePath: baseVerdict.testFilePath, beforeDetails: "4 passed in 2.00s", afterDetails: "" }], // afterDetails left blank
|
|
908
|
-
testResults: [],
|
|
909
|
-
issuesFound: [],
|
|
910
|
-
});
|
|
911
|
-
expect(rejected.isError).toBe(true);
|
|
912
|
-
expect(rejected.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
913
|
-
});
|
|
914
|
-
});
|
|
915
|
-
it("testResult accepts videoPath field (optional)", async () => {
|
|
916
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
917
|
-
tmpDirs.push(tmpDir);
|
|
918
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
919
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
920
|
-
const result = await handler({
|
|
921
|
-
summaryOutputFile: outputFile,
|
|
922
|
-
stateFile,
|
|
923
|
-
businessCaseAnalysis: "UI changes for login flow",
|
|
924
|
-
newTestsCreated: [],
|
|
925
|
-
testMaintenance: [],
|
|
926
|
-
testResults: [
|
|
927
|
-
{
|
|
928
|
-
testType: TestType.UI,
|
|
929
|
-
endpoint: "GET /login",
|
|
930
|
-
status: "Pass",
|
|
931
|
-
details: "5.2s, login_ui_test.spec.ts",
|
|
932
|
-
videoPath: "/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm",
|
|
933
|
-
},
|
|
934
|
-
],
|
|
935
|
-
issuesFound: [],
|
|
936
|
-
});
|
|
937
|
-
expect(result.isError).toBeUndefined();
|
|
938
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
939
|
-
expect(written.testResults).toHaveLength(1);
|
|
940
|
-
expect(written.testResults[0].videoPath).toBe("/workspace/.skyramp/videos/login_ui_test-abc12345/video.webm");
|
|
941
|
-
});
|
|
942
|
-
it("testResult accepts entry without videoPath (optional field)", async () => {
|
|
943
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
944
|
-
tmpDirs.push(tmpDir);
|
|
945
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
946
|
-
const stateFile = await writeStateFile(tmpDir, []);
|
|
947
|
-
const result = await handler({
|
|
948
|
-
summaryOutputFile: outputFile,
|
|
949
|
-
stateFile,
|
|
950
|
-
businessCaseAnalysis: "API endpoint changes",
|
|
951
|
-
newTestsCreated: [],
|
|
952
|
-
testMaintenance: [],
|
|
953
|
-
testResults: [
|
|
954
|
-
{
|
|
955
|
-
testType: TestType.CONTRACT,
|
|
956
|
-
endpoint: "POST /api/v1/products",
|
|
957
|
-
status: "Pass",
|
|
958
|
-
details: "2.1s, products_contract_test.py",
|
|
959
|
-
// videoPath intentionally omitted
|
|
960
|
-
},
|
|
961
|
-
],
|
|
962
|
-
issuesFound: [],
|
|
963
|
-
});
|
|
964
|
-
expect(result.isError).toBeUndefined();
|
|
965
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
966
|
-
expect(written.testResults).toHaveLength(1);
|
|
967
|
-
expect(written.testResults[0].videoPath).toBeUndefined();
|
|
968
|
-
});
|
|
969
|
-
describe("testMaintenance derived from state (SKYR-3906)", () => {
|
|
970
|
-
it("derives testMaintenance from stateData.maintenanceVerdicts when testMaintenance is omitted", async () => {
|
|
971
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
972
|
-
tmpDirs.push(tmpDir);
|
|
973
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
974
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{ testFile: "/repo/tests/orders_contract_test.py" }], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
975
|
-
const result = await handler({
|
|
976
|
-
summaryOutputFile: outputFile,
|
|
977
|
-
stateFile,
|
|
978
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
979
|
-
newTestsCreated: [],
|
|
980
|
-
// testMaintenance intentionally omitted — should be derived from state.
|
|
981
|
-
testResults: [],
|
|
982
|
-
issuesFound: [],
|
|
983
|
-
});
|
|
984
|
-
expect(result.isError).toBeUndefined();
|
|
985
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
986
|
-
expect(written.testMaintenance).toEqual([
|
|
987
|
-
expect.objectContaining({
|
|
988
|
-
fileName: "orders_contract_test.py",
|
|
989
|
-
testType: TestType.CONTRACT,
|
|
990
|
-
endpoint: "GET /api/v1/orders",
|
|
991
|
-
action: DriftAction.Update,
|
|
992
|
-
description: "Field added",
|
|
993
|
-
}),
|
|
994
|
-
]);
|
|
995
|
-
});
|
|
996
|
-
it("rejects when maintenanceVerdicts is undefined and existing tests were present — skyramp_actions was never called", async () => {
|
|
997
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
998
|
-
tmpDirs.push(tmpDir);
|
|
999
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1000
|
-
// Plain writeStateFile with no maintenanceVerdicts override — undefined, distinct
|
|
1001
|
-
// from skyramp_actions having run and recorded an empty array.
|
|
1002
|
-
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1003
|
-
await fs.writeFile(stateFile, JSON.stringify({
|
|
1004
|
-
existingTests: [{ testFile: "/repo/tests/orders_contract_test.py" }],
|
|
1005
|
-
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
|
|
1006
|
-
}), "utf-8");
|
|
1007
|
-
const result = await handler({
|
|
1008
|
-
summaryOutputFile: outputFile,
|
|
1009
|
-
stateFile,
|
|
1010
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
1011
|
-
newTestsCreated: [],
|
|
1012
|
-
testResults: [],
|
|
1013
|
-
issuesFound: [],
|
|
1014
|
-
});
|
|
1015
|
-
expect(result.isError).toBe(true);
|
|
1016
|
-
expect(result.content[0].text).toContain("skyramp_actions was not called");
|
|
1017
|
-
});
|
|
1018
|
-
it("allows maintenanceVerdicts to be undefined when there were no existing tests to assess", async () => {
|
|
1019
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1020
|
-
tmpDirs.push(tmpDir);
|
|
1021
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1022
|
-
// No existing tests at all — nothing skyramp_actions could have lost, so skipping
|
|
1023
|
-
// it entirely (never writing maintenanceVerdicts) is a valid, not an error, state.
|
|
1024
|
-
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1025
|
-
await fs.writeFile(stateFile, JSON.stringify({
|
|
1026
|
-
existingTests: [],
|
|
1027
|
-
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
|
|
1028
|
-
}), "utf-8");
|
|
1029
|
-
const result = await handler({
|
|
1030
|
-
summaryOutputFile: outputFile,
|
|
1031
|
-
stateFile,
|
|
1032
|
-
businessCaseAnalysis: "Adds a new endpoint with no prior coverage",
|
|
1033
|
-
newTestsCreated: [],
|
|
1034
|
-
testResults: [],
|
|
1035
|
-
issuesFound: [],
|
|
1036
|
-
});
|
|
1037
|
-
expect(result.isError).toBeUndefined();
|
|
1038
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
1039
|
-
expect(written.testMaintenance).toBeUndefined();
|
|
1040
|
-
});
|
|
1041
|
-
it("rejects when a related repo has reconciled maintenanceVerdicts — testMaintenance has no repository field to carry them", async () => {
|
|
1042
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1043
|
-
tmpDirs.push(tmpDir);
|
|
1044
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1045
|
-
const stateFile = path.join(tmpDir, "analyze-changes-state.json");
|
|
1046
|
-
const now = "2026-01-01T00:00:00.000Z";
|
|
1047
|
-
await fs.writeFile(stateFile, JSON.stringify({
|
|
1048
|
-
existingTests: [],
|
|
1049
|
-
maintenanceVerdicts: [],
|
|
1050
|
-
metadata: { sessionId: "test-session", stateType: "unified", repositoryPath: tmpDir, createdAt: now, updatedAt: now },
|
|
1051
|
-
relatedRepos: {
|
|
1052
|
-
"acme/backend": {
|
|
1053
|
-
repositoryPath: "/repo/backend",
|
|
1054
|
-
data: {
|
|
1055
|
-
existingTests: [],
|
|
1056
|
-
maintenanceVerdicts: [
|
|
1057
|
-
{ testFilePath: "/repo/backend/tests/orders_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" },
|
|
1058
|
-
],
|
|
1059
|
-
},
|
|
1060
|
-
},
|
|
1061
|
-
},
|
|
1062
|
-
}), "utf-8");
|
|
1063
|
-
const result = await handler({
|
|
1064
|
-
summaryOutputFile: outputFile,
|
|
1065
|
-
stateFile,
|
|
1066
|
-
businessCaseAnalysis: "Test",
|
|
1067
|
-
newTestsCreated: [],
|
|
1068
|
-
testResults: [],
|
|
1069
|
-
issuesFound: [],
|
|
1070
|
-
});
|
|
1071
|
-
expect(result.isError).toBe(true);
|
|
1072
|
-
expect(result.content[0].text).toContain("acme/backend");
|
|
1073
|
-
});
|
|
1074
|
-
it("rejects when a recorded execution has no matching testMaintenanceDetails entry at all", async () => {
|
|
1075
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1076
|
-
tmpDirs.push(tmpDir);
|
|
1077
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1078
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1079
|
-
testFile: "/repo/tests/orders_contract_test.py",
|
|
1080
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1081
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1082
|
-
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1083
|
-
const result = await handler({
|
|
1084
|
-
summaryOutputFile: outputFile,
|
|
1085
|
-
stateFile,
|
|
1086
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
1087
|
-
newTestsCreated: [],
|
|
1088
|
-
// testMaintenanceDetails omitted entirely, despite both executions being recorded.
|
|
1089
|
-
testResults: [],
|
|
1090
|
-
issuesFound: [],
|
|
1091
|
-
});
|
|
1092
|
-
expect(result.isError).toBe(true);
|
|
1093
|
-
expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
1094
|
-
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
1095
|
-
});
|
|
1096
|
-
it("rejects a whitespace-only testMaintenanceDetails summary as if it were missing", async () => {
|
|
1097
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1098
|
-
tmpDirs.push(tmpDir);
|
|
1099
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1100
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1101
|
-
testFile: "/repo/tests/orders_contract_test.py",
|
|
1102
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1103
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1104
|
-
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1105
|
-
const result = await handler({
|
|
1106
|
-
summaryOutputFile: outputFile,
|
|
1107
|
-
stateFile,
|
|
1108
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
1109
|
-
newTestsCreated: [],
|
|
1110
|
-
testMaintenanceDetails: [{
|
|
1111
|
-
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1112
|
-
beforeDetails: " ",
|
|
1113
|
-
afterDetails: "5 passed in 5.09s",
|
|
1114
|
-
}],
|
|
1115
|
-
testResults: [],
|
|
1116
|
-
issuesFound: [],
|
|
1117
|
-
});
|
|
1118
|
-
expect(result.isError).toBe(true);
|
|
1119
|
-
expect(result.content[0].text).toContain("orders_contract_test.py (beforeDetails)");
|
|
1120
|
-
});
|
|
1121
|
-
it("prefers the LLM-drafted testMaintenanceDetails summary when supplied", async () => {
|
|
1122
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1123
|
-
tmpDirs.push(tmpDir);
|
|
1124
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1125
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1126
|
-
testFile: "/repo/tests/orders_contract_test.py",
|
|
1127
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1128
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1129
|
-
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1130
|
-
const result = await handler({
|
|
1131
|
-
summaryOutputFile: outputFile,
|
|
1132
|
-
stateFile,
|
|
1133
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
1134
|
-
newTestsCreated: [],
|
|
1135
|
-
testMaintenanceDetails: [{
|
|
1136
|
-
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1137
|
-
beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
|
|
1138
|
-
afterDetails: "5 passed in 5.09s",
|
|
1139
|
-
}],
|
|
1140
|
-
testResults: [],
|
|
1141
|
-
issuesFound: [],
|
|
1142
|
-
});
|
|
1143
|
-
expect(result.isError).toBeUndefined();
|
|
1144
|
-
const written = JSON.parse(await fs.readFile(outputFile, "utf-8"));
|
|
1145
|
-
expect(written.testMaintenance[0].beforeDetails).toBe("FAILED test_orders_get — auth middleware not enforced for the new field");
|
|
1146
|
-
expect(written.testMaintenance[0].afterDetails).toBe("5 passed in 5.09s");
|
|
1147
|
-
});
|
|
1148
|
-
it("rejects when only one of beforeDetails/afterDetails is supplied but both executions were recorded", async () => {
|
|
1149
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1150
|
-
tmpDirs.push(tmpDir);
|
|
1151
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1152
|
-
const stateFile = await writeReconciledStateFile(tmpDir, [{
|
|
1153
|
-
testFile: "/repo/tests/orders_contract_test.py",
|
|
1154
|
-
executionBefore: { status: TestExecutionStatus.Fail, errors: ["assert 403 got 200"], duration: 4200 },
|
|
1155
|
-
executionAfter: { status: TestExecutionStatus.Pass, errors: [], duration: 5090 },
|
|
1156
|
-
}], [{ testFilePath: "/repo/tests/orders_contract_test.py", action: DriftAction.Update, rationale: "Field added", testType: TestType.CONTRACT, endpoint: "GET /api/v1/orders" }]);
|
|
1157
|
-
const result = await handler({
|
|
1158
|
-
summaryOutputFile: outputFile,
|
|
1159
|
-
stateFile,
|
|
1160
|
-
businessCaseAnalysis: "Adds a field to orders",
|
|
1161
|
-
newTestsCreated: [],
|
|
1162
|
-
testMaintenanceDetails: [{
|
|
1163
|
-
testFilePath: "/repo/tests/orders_contract_test.py",
|
|
1164
|
-
beforeDetails: "FAILED test_orders_get — auth middleware not enforced for the new field",
|
|
1165
|
-
afterDetails: "", // left blank, but an after-execution was recorded — must be rejected.
|
|
1166
|
-
}],
|
|
1167
|
-
testResults: [],
|
|
1168
|
-
issuesFound: [],
|
|
1169
|
-
});
|
|
1170
|
-
expect(result.isError).toBe(true);
|
|
1171
|
-
expect(result.content[0].text).toContain("orders_contract_test.py (afterDetails)");
|
|
1172
|
-
expect(result.content[0].text).not.toContain("beforeDetails");
|
|
1173
|
-
});
|
|
1174
|
-
});
|
|
1175
|
-
describe("approvedPlan cross-check (SKYR-3879 Path B)", () => {
|
|
1176
|
-
async function writeStateFileWithPlan(dir, approvedPlan) {
|
|
1177
|
-
const stateFile = path.join(dir, "analyze-changes-state.json");
|
|
1178
|
-
await fs.writeFile(stateFile, JSON.stringify({
|
|
1179
|
-
existingTests: [],
|
|
1180
|
-
maintenanceVerdicts: [],
|
|
1181
|
-
...(approvedPlan ? { approvedPlan } : {}),
|
|
1182
|
-
metadata: sampleMetadata(dir),
|
|
1183
|
-
}), "utf-8");
|
|
1184
|
-
return stateFile;
|
|
1185
|
-
}
|
|
1186
|
-
const APPROVED_PLAN = {
|
|
1187
|
-
planId: "plan-42",
|
|
1188
|
-
createdAt: "2026-01-01T00:00:00.000Z",
|
|
1189
|
-
generate: [
|
|
1190
|
-
{
|
|
1191
|
-
candidateId: "products-search-abcd1234",
|
|
1192
|
-
scenarioName: "products_search",
|
|
1193
|
-
testType: "contract",
|
|
1194
|
-
category: "business_rule",
|
|
1195
|
-
source: CandidateSource.AGENT,
|
|
1196
|
-
matchKeys: ["name:products-search", "ep:contract:GET:/api/v1/products/search"],
|
|
1197
|
-
},
|
|
1198
|
-
],
|
|
1199
|
-
additional: [],
|
|
1200
|
-
demotions: [],
|
|
1201
|
-
};
|
|
1202
|
-
function contractNewTest(overrides = {}) {
|
|
1203
|
-
return {
|
|
1204
|
-
testId: "contract-get-products-search",
|
|
1205
|
-
testType: TestType.CONTRACT,
|
|
1206
|
-
category: "business_rule",
|
|
1207
|
-
endpoint: "GET /api/v1/products/search",
|
|
1208
|
-
fileName: "products_search_contract_test.py",
|
|
1209
|
-
description: "Searches products and asserts the response schema.",
|
|
1210
|
-
reasoning: "Validates the contract for the new search endpoint.",
|
|
1211
|
-
...overrides,
|
|
1212
|
-
};
|
|
1213
|
-
}
|
|
1214
|
-
it("passes when every gated newTestsCreated entry matches an approved GENERATE item", async () => {
|
|
1215
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1216
|
-
tmpDirs.push(tmpDir);
|
|
1217
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1218
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
|
|
1219
|
-
const result = await handler({
|
|
1220
|
-
summaryOutputFile: outputFile,
|
|
1221
|
-
stateFile,
|
|
1222
|
-
businessCaseAnalysis: "Adds product search.",
|
|
1223
|
-
newTestsCreated: [contractNewTest()],
|
|
1224
|
-
testResults: [],
|
|
1225
|
-
issuesFound: [],
|
|
1226
|
-
});
|
|
1227
|
-
expect(result.isError).toBeUndefined();
|
|
1228
|
-
});
|
|
1229
|
-
it("rejects an unapproved gated entry, naming the offending entry and the approved list", async () => {
|
|
1230
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1231
|
-
tmpDirs.push(tmpDir);
|
|
1232
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1233
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
|
|
1234
|
-
const result = await handler({
|
|
1235
|
-
summaryOutputFile: outputFile,
|
|
1236
|
-
stateFile,
|
|
1237
|
-
businessCaseAnalysis: "Adds an unregistered contract test.",
|
|
1238
|
-
newTestsCreated: [
|
|
1239
|
-
contractNewTest({
|
|
1240
|
-
testId: "contract-get-products-unregistered",
|
|
1241
|
-
endpoint: "GET /api/v1/products/unregistered",
|
|
1242
|
-
}),
|
|
1243
|
-
],
|
|
1244
|
-
testResults: [],
|
|
1245
|
-
issuesFound: [],
|
|
1246
|
-
});
|
|
1247
|
-
expect(result.isError).toBe(true);
|
|
1248
|
-
expect(result.content[0].text).toContain("plan-42");
|
|
1249
|
-
expect(result.content[0].text).toContain("contract-get-products-unregistered");
|
|
1250
|
-
expect(result.content[0].text).toContain("products_search");
|
|
1251
|
-
});
|
|
1252
|
-
it("accepts a gated entry matching an approved ADDITIONAL item — backfill parity with planGuard", async () => {
|
|
1253
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1254
|
-
tmpDirs.push(tmpDir);
|
|
1255
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1256
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, {
|
|
1257
|
-
...APPROVED_PLAN,
|
|
1258
|
-
additional: [
|
|
1259
|
-
{
|
|
1260
|
-
candidateId: "products-backfill-efgh5678",
|
|
1261
|
-
scenarioName: "products_backfill",
|
|
1262
|
-
testType: "contract",
|
|
1263
|
-
category: "business_rule",
|
|
1264
|
-
source: CandidateSource.AGENT,
|
|
1265
|
-
matchKeys: ["name:products-backfill", "ep:contract:GET:/api/v1/products/backfill"],
|
|
1266
|
-
},
|
|
1267
|
-
],
|
|
1268
|
-
});
|
|
1269
|
-
const result = await handler({
|
|
1270
|
-
summaryOutputFile: outputFile,
|
|
1271
|
-
stateFile,
|
|
1272
|
-
businessCaseAnalysis: "GENERATE item failed; backfilled from ADDITIONAL as the plan instructs.",
|
|
1273
|
-
newTestsCreated: [
|
|
1274
|
-
contractNewTest({
|
|
1275
|
-
testId: "contract-get-products-backfill",
|
|
1276
|
-
endpoint: "GET /api/v1/products/backfill",
|
|
1277
|
-
}),
|
|
1278
|
-
],
|
|
1279
|
-
testResults: [],
|
|
1280
|
-
issuesFound: [],
|
|
1281
|
-
});
|
|
1282
|
-
expect(result.isError).toBeUndefined();
|
|
1283
|
-
});
|
|
1284
|
-
it("rejects an ADDITIONAL-matching entry when the GENERATE list is empty — zero-budget plans have nothing to backfill", async () => {
|
|
1285
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1286
|
-
tmpDirs.push(tmpDir);
|
|
1287
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1288
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, {
|
|
1289
|
-
...APPROVED_PLAN,
|
|
1290
|
-
generate: [],
|
|
1291
|
-
additional: APPROVED_PLAN.generate,
|
|
1292
|
-
});
|
|
1293
|
-
const result = await handler({
|
|
1294
|
-
summaryOutputFile: outputFile,
|
|
1295
|
-
stateFile,
|
|
1296
|
-
businessCaseAnalysis: "Generated a test on a zero-budget plan.",
|
|
1297
|
-
newTestsCreated: [contractNewTest()],
|
|
1298
|
-
testResults: [],
|
|
1299
|
-
issuesFound: [],
|
|
1300
|
-
});
|
|
1301
|
-
expect(result.isError).toBe(true);
|
|
1302
|
-
expect(result.content[0].text).toContain("plan-42");
|
|
1303
|
-
});
|
|
1304
|
-
it("does not check newTestsCreated when approvedPlan is absent (back-compat)", async () => {
|
|
1305
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1306
|
-
tmpDirs.push(tmpDir);
|
|
1307
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1308
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, undefined);
|
|
1309
|
-
const result = await handler({
|
|
1310
|
-
summaryOutputFile: outputFile,
|
|
1311
|
-
stateFile,
|
|
1312
|
-
businessCaseAnalysis: "No plan was ever registered for this run.",
|
|
1313
|
-
newTestsCreated: [contractNewTest({ endpoint: "GET /api/v1/products/anything" })],
|
|
1314
|
-
testResults: [],
|
|
1315
|
-
issuesFound: [],
|
|
1316
|
-
});
|
|
1317
|
-
expect(result.isError).toBeUndefined();
|
|
1318
|
-
});
|
|
1319
|
-
it("does not gate UI/E2E entries even when unmatched — those tools are outside planGuard's scope", async () => {
|
|
1320
|
-
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-test-"));
|
|
1321
|
-
tmpDirs.push(tmpDir);
|
|
1322
|
-
const outputFile = path.join(tmpDir, "report.json");
|
|
1323
|
-
const stateFile = await writeStateFileWithPlan(tmpDir, APPROVED_PLAN);
|
|
1324
|
-
const result = await handler({
|
|
1325
|
-
summaryOutputFile: outputFile,
|
|
1326
|
-
stateFile,
|
|
1327
|
-
businessCaseAnalysis: "Adds a UI test not present in the approved plan.",
|
|
1328
|
-
newTestsCreated: [
|
|
1329
|
-
{
|
|
1330
|
-
testId: "ui-checkout-flow",
|
|
1331
|
-
testType: TestType.UI,
|
|
1332
|
-
category: "workflow",
|
|
1333
|
-
endpoint: "N/A",
|
|
1334
|
-
fileName: "checkout_ui_test.spec.ts",
|
|
1335
|
-
description: "Walks through the checkout flow. [no-blueprint-data]",
|
|
1336
|
-
reasoning: "Covers the new checkout UI. [no-blueprint-data]",
|
|
1337
|
-
targetElements: null,
|
|
1338
|
-
},
|
|
1339
|
-
],
|
|
1340
|
-
testResults: [],
|
|
1341
|
-
issuesFound: [],
|
|
1342
|
-
});
|
|
1343
|
-
expect(result.isError).toBeUndefined();
|
|
1344
|
-
});
|
|
1345
|
-
});
|
|
1346
|
-
});
|
|
1347
|
-
// SKYR-3883: end-to-end coverage of the working-tree verification gate. The unit
|
|
1348
|
-
// behavior lives in reportVerification.test.ts; these exercise the wiring through
|
|
1349
|
-
// the real tool handler (feature-flag gating, git enumeration, error surfacing).
|
|
1350
|
-
describe("submit_report working-tree verification (SKYR-3883)", () => {
|
|
1351
|
-
let handler;
|
|
1352
|
-
let tmpDirs = [];
|
|
1353
|
-
const prevFlag = process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
1354
|
-
beforeAll(() => {
|
|
1355
|
-
handler = captureToolHandler();
|
|
1356
|
-
});
|
|
1357
|
-
beforeEach(() => {
|
|
1358
|
-
process.env.SKYRAMP_FEATURE_TESTBOT = "1";
|
|
1359
|
-
});
|
|
1360
|
-
afterEach(async () => {
|
|
1361
|
-
if (prevFlag === undefined)
|
|
1362
|
-
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
1363
|
-
else
|
|
1364
|
-
process.env.SKYRAMP_FEATURE_TESTBOT = prevFlag;
|
|
1365
|
-
for (const dir of tmpDirs)
|
|
1366
|
-
await fs.rm(dir, { recursive: true, force: true });
|
|
1367
|
-
tmpDirs = [];
|
|
1368
|
-
});
|
|
1369
|
-
async function initGitRepo() {
|
|
1370
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-wt-"));
|
|
1371
|
-
tmpDirs.push(dir);
|
|
1372
|
-
execFileSync("git", ["init", "-q"], { cwd: dir });
|
|
1373
|
-
execFileSync("git", ["config", "user.email", "t@t.dev"], { cwd: dir });
|
|
1374
|
-
execFileSync("git", ["config", "user.name", "t"], { cwd: dir });
|
|
1375
|
-
return dir;
|
|
1376
|
-
}
|
|
1377
|
-
it("rejects a report whose newTestsCreated file was never written", async () => {
|
|
1378
|
-
const dir = await initGitRepo();
|
|
1379
|
-
const outputFile = path.join(dir, "report.json");
|
|
1380
|
-
const stateFile = await writeStateFile(dir, []);
|
|
1381
|
-
// search_smoke_test.py is claimed but never created → unbacked.
|
|
1382
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
1383
|
-
expect(result.isError).toBe(true);
|
|
1384
|
-
expect(result.content[0].text).toContain("not backed");
|
|
1385
|
-
expect(result.content[0].text).toContain("search_smoke_test.py");
|
|
1386
|
-
});
|
|
1387
|
-
it("accepts the report once the claimed file exists in the working tree", async () => {
|
|
1388
|
-
const dir = await initGitRepo();
|
|
1389
|
-
const outputFile = path.join(dir, "report.json");
|
|
1390
|
-
const stateFile = await writeStateFile(dir, []);
|
|
1391
|
-
await fs.writeFile(path.join(dir, "search_smoke_test.py"), "def test_search(): pass\n");
|
|
1392
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
1393
|
-
expect(result.isError).toBeUndefined();
|
|
1394
|
-
});
|
|
1395
|
-
it("rejects an UPDATE maintenance verdict with no working-tree change, exempting DELETE", async () => {
|
|
1396
|
-
const dir = await initGitRepo();
|
|
1397
|
-
const outputFile = path.join(dir, "report.json");
|
|
1398
|
-
const stateFile = await writeReconciledStateFile(dir, [], [
|
|
1399
|
-
{ testFilePath: path.join(dir, "dcim/tests/test_filtersets.py"), action: DriftAction.Update, rationale: "physical_address filter", testType: TestType.INTEGRATION, endpoint: "GET /api/dcim/sites/" },
|
|
1400
|
-
{ testFilePath: path.join(dir, "dcim/tests/gone.py"), action: DriftAction.Delete, rationale: "obsolete", testType: TestType.INTEGRATION, endpoint: "GET /api/dcim/sites/" },
|
|
1401
|
-
]);
|
|
1402
|
-
// Keep the newTestsCreated claim backed so the failure isolates to the verdict.
|
|
1403
|
-
await fs.writeFile(path.join(dir, "search_smoke_test.py"), "x\n");
|
|
1404
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
1405
|
-
expect(result.isError).toBe(true);
|
|
1406
|
-
expect(result.content[0].text).toContain("test_filtersets.py");
|
|
1407
|
-
// DELETE is applied at delivery, never a working-tree claim — must not be flagged.
|
|
1408
|
-
expect(result.content[0].text).not.toContain("gone.py");
|
|
1409
|
-
});
|
|
1410
|
-
it("skips verification entirely when the testbot feature flag is off", async () => {
|
|
1411
|
-
delete process.env.SKYRAMP_FEATURE_TESTBOT;
|
|
1412
|
-
const dir = await initGitRepo();
|
|
1413
|
-
const outputFile = path.join(dir, "report.json");
|
|
1414
|
-
const stateFile = await writeStateFile(dir, []);
|
|
1415
|
-
// Claimed file absent, but the gate is off → report goes through.
|
|
1416
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
1417
|
-
expect(result.isError).toBeUndefined();
|
|
1418
|
-
});
|
|
1419
|
-
it("degrades to a pass when repositoryPath is not a git repo (verification can't run)", async () => {
|
|
1420
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "submit-report-nogit-"));
|
|
1421
|
-
tmpDirs.push(dir);
|
|
1422
|
-
const outputFile = path.join(dir, "report.json");
|
|
1423
|
-
const stateFile = await writeStateFile(dir, []); // repositoryPath=dir, but no git init
|
|
1424
|
-
const result = await handler(sampleReportParams(outputFile, stateFile));
|
|
1425
|
-
// git status throws → caught → warning → report not falsely rejected.
|
|
1426
|
-
expect(result.isError).toBeUndefined();
|
|
1427
|
-
});
|
|
1428
|
-
});
|