@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,876 +0,0 @@
|
|
|
1
|
-
// @ts-ignore
|
|
2
|
-
import { isRealResource, draftScenariosFromEndpoints, draftDiffDirectScenarios, inferResourceRelationships, draftResponseShapeScenarios, } from "./scenarioDrafting.js";
|
|
3
|
-
import { WorkspaceAuthType } from "./workspaceAuth.js";
|
|
4
|
-
describe("isRealResource", () => {
|
|
5
|
-
it("accepts normal resource names", () => {
|
|
6
|
-
expect(isRealResource("users")).toBe(true);
|
|
7
|
-
expect(isRealResource("products")).toBe(true);
|
|
8
|
-
expect(isRealResource("orders")).toBe(true);
|
|
9
|
-
});
|
|
10
|
-
it("accepts hyphenated resource names (not action verbs)", () => {
|
|
11
|
-
expect(isRealResource("saved-searches")).toBe(true);
|
|
12
|
-
expect(isRealResource("order-items")).toBe(true);
|
|
13
|
-
expect(isRealResource("user-profiles")).toBe(true);
|
|
14
|
-
expect(isRealResource("api-keys")).toBe(true);
|
|
15
|
-
});
|
|
16
|
-
it("rejects infra/monitoring tokens but accepts semantic endpoint names", () => {
|
|
17
|
-
expect(isRealResource("health")).toBe(false);
|
|
18
|
-
expect(isRealResource("ping")).toBe(false);
|
|
19
|
-
expect(isRealResource("dashboard")).toBe(false);
|
|
20
|
-
// login, logout, verify, webhook are real resources (phase 6a)
|
|
21
|
-
expect(isRealResource("login")).toBe(true);
|
|
22
|
-
expect(isRealResource("logout")).toBe(true);
|
|
23
|
-
expect(isRealResource("verify")).toBe(true);
|
|
24
|
-
expect(isRealResource("webhook")).toBe(true);
|
|
25
|
-
});
|
|
26
|
-
it("rejects action-verb-hyphenated patterns", () => {
|
|
27
|
-
expect(isRealResource("forgot-password")).toBe(false);
|
|
28
|
-
expect(isRealResource("reset-password")).toBe(false);
|
|
29
|
-
expect(isRealResource("verify-email")).toBe(false);
|
|
30
|
-
expect(isRealResource("confirm-order")).toBe(false);
|
|
31
|
-
expect(isRealResource("send-notification")).toBe(false);
|
|
32
|
-
expect(isRealResource("create-session")).toBe(false);
|
|
33
|
-
expect(isRealResource("delete-cache")).toBe(false);
|
|
34
|
-
});
|
|
35
|
-
it("is case-insensitive", () => {
|
|
36
|
-
expect(isRealResource("Login")).toBe(true); // login is now a real resource (phase 6a)
|
|
37
|
-
expect(isRealResource("VERIFY")).toBe(true); // verify is now a real resource (phase 6a)
|
|
38
|
-
expect(isRealResource("Forgot-Password")).toBe(false); // ACTION_VERB_HYPHEN still filters this
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
describe("draftScenariosFromEndpoints — irregular plural singularization", () => {
|
|
42
|
-
it("uses 'category' (not 'categorie') in FK field names for -ies plurals", () => {
|
|
43
|
-
const endpoints = [
|
|
44
|
-
{ path: "/api/categories", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
45
|
-
{ path: "/api/categories/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
|
|
46
|
-
{ path: "/api/items", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
47
|
-
{ path: "/api/items/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
|
|
48
|
-
];
|
|
49
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/categories" }]);
|
|
50
|
-
const allText = JSON.stringify(scenarios);
|
|
51
|
-
expect(allText).not.toContain("categorie_id");
|
|
52
|
-
expect(allText).not.toContain("\"categorie\"");
|
|
53
|
-
// The diff-direct integration scenario description should use "category" (singular)
|
|
54
|
-
const integrationScenario = scenarios.find((s) => s.scenarioName === "categories-post-lifecycle");
|
|
55
|
-
expect(integrationScenario).toBeDefined();
|
|
56
|
-
expect(integrationScenario.steps[0].description).toContain("category");
|
|
57
|
-
});
|
|
58
|
-
it("uses 'status' (not 'statu') in FK field names for -ses plurals", () => {
|
|
59
|
-
const endpoints = [
|
|
60
|
-
{ path: "/api/statuses", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
61
|
-
{ path: "/api/statuses/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
|
|
62
|
-
];
|
|
63
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/statuses" }]);
|
|
64
|
-
const allText = JSON.stringify(scenarios);
|
|
65
|
-
expect(allText).not.toContain("statu_id");
|
|
66
|
-
// The diff-direct integration scenario description should use "status" (singular)
|
|
67
|
-
const integrationScenario = scenarios.find((s) => s.scenarioName === "statuses-post-lifecycle");
|
|
68
|
-
expect(integrationScenario).toBeDefined();
|
|
69
|
-
expect(integrationScenario.steps[0].description).toContain("status");
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
describe("draftScenariosFromEndpoints", () => {
|
|
73
|
-
it("returns empty array for no endpoints", () => {
|
|
74
|
-
expect(draftScenariosFromEndpoints([])).toEqual([]);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
describe("draftScenariosFromEndpoints — attack-surface expansion", () => {
|
|
78
|
-
const endpoints = [
|
|
79
|
-
{ path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] },
|
|
80
|
-
{ path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/flows.py" }] },
|
|
81
|
-
{ path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
82
|
-
{ path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
83
|
-
];
|
|
84
|
-
it("drafts auth-boundary scenarios for same-file destructive bulk siblings", () => {
|
|
85
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
|
|
86
|
-
securityRelevantDiff: true,
|
|
87
|
-
changedEndpoints: [
|
|
88
|
-
{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" },
|
|
89
|
-
{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
|
|
90
|
-
],
|
|
91
|
-
});
|
|
92
|
-
const flowBulkDelete = scenarios.find(s => s.scenarioName === "flows-bulk-delete-post-auth-boundary");
|
|
93
|
-
const deploymentBulkDelete = scenarios.find(s => s.scenarioName === "deployments-bulk-delete-post-auth-boundary");
|
|
94
|
-
expect(flowBulkDelete).toBeDefined();
|
|
95
|
-
expect(flowBulkDelete.category).toBe("security_boundary");
|
|
96
|
-
expect(flowBulkDelete.testType).toBe("contract");
|
|
97
|
-
expect(flowBulkDelete.steps[0]).toMatchObject({
|
|
98
|
-
method: "POST",
|
|
99
|
-
path: "/flows/bulk_delete",
|
|
100
|
-
expectedStatusCode: 401,
|
|
101
|
-
});
|
|
102
|
-
expect(flowBulkDelete.description).toContain("destructive sibling");
|
|
103
|
-
expect(deploymentBulkDelete).toBeDefined();
|
|
104
|
-
expect(deploymentBulkDelete.steps[0]).toMatchObject({
|
|
105
|
-
method: "POST",
|
|
106
|
-
path: "/deployments/bulk_delete",
|
|
107
|
-
expectedStatusCode: 401,
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
it("does not expand when the diff lacks security signals", () => {
|
|
111
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
|
|
112
|
-
securityRelevantDiff: false,
|
|
113
|
-
changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
|
|
114
|
-
});
|
|
115
|
-
expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
|
|
116
|
-
});
|
|
117
|
-
it("uses the source-file resource for router-relative nested destructive endpoints", () => {
|
|
118
|
-
const scenarios = draftScenariosFromEndpoints([
|
|
119
|
-
{ path: "/{id:uuid}/schedules/{schedule_id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
120
|
-
{ path: "/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
121
|
-
{ path: "/{id:uuid}/schedules/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
122
|
-
], [], WorkspaceAuthType.ApiKey, {
|
|
123
|
-
securityRelevantDiff: true,
|
|
124
|
-
changedEndpoints: [
|
|
125
|
-
{ method: "DELETE", path: "/{id:uuid}/schedules/{schedule_id:uuid}", sourceFile: "src/prefect/server/api/deployments.py" },
|
|
126
|
-
],
|
|
127
|
-
});
|
|
128
|
-
expect(scenarios.some(s => s.steps.some(step => step.path === "/deployments/bulk_delete"))).toBe(true);
|
|
129
|
-
expect(scenarios.some(s => s.steps.some(step => step.path === "/{id:uuid}/schedules/bulk_delete"))).toBe(false);
|
|
130
|
-
});
|
|
131
|
-
it("preserves Express-style tenant params for attack-surface siblings", () => {
|
|
132
|
-
const scenarios = draftScenariosFromEndpoints([
|
|
133
|
-
{ path: "/:tenant_id/deployments/:id", methods: [{ method: "DELETE", sourceFile: "src/routes/deployments.ts" }] },
|
|
134
|
-
{ path: "/:tenant_id/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/deployments.ts" }] },
|
|
135
|
-
], [], WorkspaceAuthType.ApiKey, {
|
|
136
|
-
securityRelevantDiff: true,
|
|
137
|
-
changedEndpoints: [
|
|
138
|
-
{ method: "DELETE", path: "/:tenant_id/deployments/:id", sourceFile: "src/routes/deployments.ts" },
|
|
139
|
-
],
|
|
140
|
-
});
|
|
141
|
-
expect(scenarios.some(s => s.steps.some(step => step.path === "/:tenant_id/deployments/bulk_delete"))).toBe(true);
|
|
142
|
-
});
|
|
143
|
-
it("preserves Next-style tenant params for attack-surface siblings", () => {
|
|
144
|
-
const scenarios = draftScenariosFromEndpoints([
|
|
145
|
-
{ path: "/[tenant_id]/deployments/[id]", methods: [{ method: "DELETE", sourceFile: "src/app/api/deployments/route.ts" }] },
|
|
146
|
-
{ path: "/[tenant_id]/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/app/api/deployments/route.ts" }] },
|
|
147
|
-
], [], WorkspaceAuthType.ApiKey, {
|
|
148
|
-
securityRelevantDiff: true,
|
|
149
|
-
changedEndpoints: [
|
|
150
|
-
{ method: "DELETE", path: "/[tenant_id]/deployments/[id]", sourceFile: "src/app/api/deployments/route.ts" },
|
|
151
|
-
],
|
|
152
|
-
});
|
|
153
|
-
expect(scenarios.some(s => s.steps.some(step => step.path === "/[tenant_id]/deployments/bulk_delete"))).toBe(true);
|
|
154
|
-
});
|
|
155
|
-
it("preserves full tenant-scoped paths when the resource segment is explicit", () => {
|
|
156
|
-
const scenarios = draftScenariosFromEndpoints([
|
|
157
|
-
{ path: "/{tenant_id}/deployments/{id}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
158
|
-
{ path: "/{tenant_id}/deployments/bulk_delete", methods: [{ method: "POST", sourceFile: "src/prefect/server/api/deployments.py" }] },
|
|
159
|
-
], [], WorkspaceAuthType.ApiKey, {
|
|
160
|
-
securityRelevantDiff: true,
|
|
161
|
-
changedEndpoints: [
|
|
162
|
-
{ method: "DELETE", path: "/{tenant_id}/deployments/{id}", sourceFile: "src/prefect/server/api/deployments.py" },
|
|
163
|
-
],
|
|
164
|
-
});
|
|
165
|
-
expect(scenarios.some(s => s.steps.some(step => step.path === "/{tenant_id}/deployments/bulk_delete"))).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
it("does not invent a bulk sibling when no destructive sibling endpoint exists", () => {
|
|
168
|
-
const scenarios = draftScenariosFromEndpoints([{ path: "/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/prefect/server/api/flows.py" }] }], [], WorkspaceAuthType.ApiKey, {
|
|
169
|
-
securityRelevantDiff: true,
|
|
170
|
-
changedEndpoints: [{ method: "DELETE", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
|
|
171
|
-
});
|
|
172
|
-
expect(scenarios).toEqual([]);
|
|
173
|
-
});
|
|
174
|
-
it("does not expand ordinary changed GET endpoints", () => {
|
|
175
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, [], WorkspaceAuthType.ApiKey, {
|
|
176
|
-
securityRelevantDiff: true,
|
|
177
|
-
changedEndpoints: [{ method: "GET", path: "/{id:uuid}", sourceFile: "src/prefect/server/api/flows.py" }],
|
|
178
|
-
});
|
|
179
|
-
expect(scenarios.some(s => s.scenarioName.includes("bulk-delete"))).toBe(false);
|
|
180
|
-
});
|
|
181
|
-
it("does not promote unrelated destructive routes from the same source file", () => {
|
|
182
|
-
const scenarios = draftScenariosFromEndpoints([
|
|
183
|
-
{ path: "/flows/{id:uuid}", methods: [{ method: "DELETE", sourceFile: "src/routes/api.py" }] },
|
|
184
|
-
{ path: "/admin/bulk_delete", methods: [{ method: "POST", sourceFile: "src/routes/api.py" }] },
|
|
185
|
-
], [], WorkspaceAuthType.ApiKey, {
|
|
186
|
-
securityRelevantDiff: true,
|
|
187
|
-
changedEndpoints: [{ method: "DELETE", path: "/flows/{id:uuid}", sourceFile: "src/routes/api.py" }],
|
|
188
|
-
});
|
|
189
|
-
expect(scenarios).toEqual([]);
|
|
190
|
-
});
|
|
191
|
-
});
|
|
192
|
-
describe("inferResourceRelationships", () => {
|
|
193
|
-
it("returns empty map for endpoints with no nesting or FK fields", () => {
|
|
194
|
-
const endpoints = [
|
|
195
|
-
{ path: "/api/users", methods: [{ method: "GET" }] },
|
|
196
|
-
{ path: "/api/products", methods: [{ method: "GET" }] },
|
|
197
|
-
];
|
|
198
|
-
expect(inferResourceRelationships(endpoints).size).toBe(0);
|
|
199
|
-
});
|
|
200
|
-
it("detects parent→child relationship from path nesting (/parent/{id}/child)", () => {
|
|
201
|
-
const endpoints = [
|
|
202
|
-
{ path: "/api/collections/{id}/links", methods: [{ method: "GET" }] },
|
|
203
|
-
];
|
|
204
|
-
const rel = inferResourceRelationships(endpoints);
|
|
205
|
-
expect(rel.has("links")).toBe(true);
|
|
206
|
-
expect(rel.get("links").has("collections")).toBe(true);
|
|
207
|
-
});
|
|
208
|
-
it("detects FK relationship from request body field ({resource}_id)", () => {
|
|
209
|
-
const endpoints = [
|
|
210
|
-
{ path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { product_id: "abc" } } }] }] },
|
|
211
|
-
{ path: "/api/products", methods: [{ method: "GET" }] },
|
|
212
|
-
];
|
|
213
|
-
const rel = inferResourceRelationships(endpoints);
|
|
214
|
-
expect(rel.has("orders")).toBe(true);
|
|
215
|
-
expect(rel.get("orders").has("products")).toBe(true);
|
|
216
|
-
});
|
|
217
|
-
it("does not create self-reference", () => {
|
|
218
|
-
const endpoints = [
|
|
219
|
-
{ path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { order_id: "abc" } } }] }] },
|
|
220
|
-
];
|
|
221
|
-
const rel = inferResourceRelationships(endpoints);
|
|
222
|
-
// orders should not reference itself
|
|
223
|
-
expect(rel.get("orders")?.has("orders")).toBeFalsy();
|
|
224
|
-
});
|
|
225
|
-
it("ignores FK fields that point to unknown resources", () => {
|
|
226
|
-
const endpoints = [
|
|
227
|
-
{ path: "/api/orders", methods: [{ method: "POST", interactions: [{ request: { body: { nonexistent_resource_id: "abc" } } }] }] },
|
|
228
|
-
];
|
|
229
|
-
const rel = inferResourceRelationships(endpoints);
|
|
230
|
-
// nonexistent_resource is not a known endpoint resource, should be ignored
|
|
231
|
-
expect(rel.get("orders")?.size ?? 0).toBe(0);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
describe("draftDiffDirectScenarios", () => {
|
|
235
|
-
const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
|
|
236
|
-
it("returns empty array when newEndpoints is empty", () => {
|
|
237
|
-
const groups = makeGroups({ orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{id}" } });
|
|
238
|
-
expect(draftDiffDirectScenarios([], groups)).toEqual([]);
|
|
239
|
-
});
|
|
240
|
-
it("generates new_endpoint-category scenarios for a new PUT endpoint", () => {
|
|
241
|
-
const groups = makeGroups({
|
|
242
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
|
|
243
|
-
});
|
|
244
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
245
|
-
// PUT generates: mutation-recalc, contract, not-found (3 scenarios)
|
|
246
|
-
// Note: we intentionally skip the redundant "happy-path" integration for PUT/PATCH
|
|
247
|
-
// because mutation-recalc already provides comprehensive happy-path coverage
|
|
248
|
-
expect(scenarios.length).toBeGreaterThanOrEqual(3);
|
|
249
|
-
const newEndpointScenarios = scenarios.filter(s => s.category === "new_endpoint");
|
|
250
|
-
expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(2);
|
|
251
|
-
// Mutation-recalculation scenario is drafted first for PUT/PATCH
|
|
252
|
-
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
|
|
253
|
-
expect(mutationRecalc).toBeDefined();
|
|
254
|
-
expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
|
|
255
|
-
expect(mutationRecalc.description).toContain("Mutation test");
|
|
256
|
-
// Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
|
|
257
|
-
// Contract test
|
|
258
|
-
const contract = scenarios.find(s => s.testType === "contract" && s.steps[0].expectedStatusCode === 200);
|
|
259
|
-
expect(contract).toBeDefined();
|
|
260
|
-
// Not-found edge case
|
|
261
|
-
const notFound = scenarios.find(s => s.steps.some(st => st.expectedStatusCode === 404));
|
|
262
|
-
expect(notFound).toBeDefined();
|
|
263
|
-
});
|
|
264
|
-
it("generates mutation-recalc scenario for a new PATCH endpoint", () => {
|
|
265
|
-
const groups = makeGroups({
|
|
266
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/orders/{order_id}" },
|
|
267
|
-
});
|
|
268
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/orders/{order_id}" }], groups);
|
|
269
|
-
// Mutation-recalculation scenario is drafted for PATCH
|
|
270
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
271
|
-
expect(mutationRecalc).toBeDefined();
|
|
272
|
-
expect(mutationRecalc.testType).toBe("integration");
|
|
273
|
-
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
274
|
-
expect(mutationRecalc.priority).toBe("high");
|
|
275
|
-
expect(mutationRecalc.description).toContain("Mutation test");
|
|
276
|
-
expect(mutationRecalc.description).toContain("derived calculations");
|
|
277
|
-
// Should have 3 steps: POST (create) → PATCH (add items) → GET (verify)
|
|
278
|
-
expect(mutationRecalc.steps).toHaveLength(3);
|
|
279
|
-
expect(mutationRecalc.steps[0].method).toBe("POST");
|
|
280
|
-
expect(mutationRecalc.steps[1].method).toBe("PATCH");
|
|
281
|
-
expect(mutationRecalc.steps[2].method).toBe("GET");
|
|
282
|
-
// Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
|
|
283
|
-
});
|
|
284
|
-
it("integration scenario minimum steps: POST resource then PUT — LLM discovers prereqs from source code", () => {
|
|
285
|
-
const groups = makeGroups({
|
|
286
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
|
|
287
|
-
});
|
|
288
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
289
|
-
// Mutation-recalculation scenario comes first
|
|
290
|
-
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
|
|
291
|
-
expect(mutationRecalc).toBeDefined();
|
|
292
|
-
expect(mutationRecalc.steps.some(st => st.method === "POST" && st.path === "/api/orders")).toBe(true);
|
|
293
|
-
expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
|
|
294
|
-
// Note: happy-path integration removed for PUT/PATCH
|
|
295
|
-
// mutation-recalc already provides comprehensive happy-path coverage with POST → PUT → GET
|
|
296
|
-
// The minimum steps (POST + PUT) are already verified in the mutationRecalc check above
|
|
297
|
-
});
|
|
298
|
-
it("generates 3 scenarios for a new POST endpoint", () => {
|
|
299
|
-
const groups = makeGroups({
|
|
300
|
-
products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
|
|
301
|
-
});
|
|
302
|
-
const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
|
|
303
|
-
expect(scenarios.length).toBe(3); // integration, contract, validation
|
|
304
|
-
const types = scenarios.map(s => s.testType);
|
|
305
|
-
expect(types).toContain("integration");
|
|
306
|
-
expect(types).toContain("contract");
|
|
307
|
-
// validation error scenario (422)
|
|
308
|
-
expect(scenarios.some(s => s.steps.some(st => st.expectedStatusCode === 422))).toBe(true);
|
|
309
|
-
});
|
|
310
|
-
it("ignores unknown paths and skips action-verb segments", () => {
|
|
311
|
-
const groups = makeGroups({
|
|
312
|
-
orders: { basePath: "/api/orders", methods: ["POST", "DELETE"], paramPath: "/api/orders/{id}" },
|
|
313
|
-
});
|
|
314
|
-
const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/health" }], // 'health' is not a real resource
|
|
315
|
-
groups);
|
|
316
|
-
expect(scenarios).toEqual([]);
|
|
317
|
-
});
|
|
318
|
-
it("draftScenariosFromEndpoints passes newEndpoints through to diff-direct scenarios", () => {
|
|
319
|
-
const endpoints = [
|
|
320
|
-
{ path: "/api/orders", methods: ["GET", "POST", "PUT"] },
|
|
321
|
-
{ path: "/api/orders/{order_id}", methods: ["GET", "PUT", "DELETE"] },
|
|
322
|
-
];
|
|
323
|
-
const newEndpoints = [{ method: "PUT", path: "/api/orders/{order_id}" }];
|
|
324
|
-
const scenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
325
|
-
const diffDirect = scenarios.filter(s => s.category === "new_endpoint");
|
|
326
|
-
expect(diffDirect.length).toBeGreaterThan(0);
|
|
327
|
-
expect(diffDirect.some(s => s.steps.some(st => st.method === "PUT"))).toBe(true);
|
|
328
|
-
});
|
|
329
|
-
it("resolves router-relative param paths (e.g. /{order_id}) against known resourceGroups", () => {
|
|
330
|
-
const groups = makeGroups({
|
|
331
|
-
orders: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH", "DELETE"], paramPath: "/api/v1/orders/{order_id}" },
|
|
332
|
-
products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
|
|
333
|
-
});
|
|
334
|
-
// Diff scanner reports router-relative path, not the full API path
|
|
335
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{order_id}" }], groups);
|
|
336
|
-
expect(scenarios.length).toBeGreaterThanOrEqual(3);
|
|
337
|
-
// Mutation-recalculation scenario is drafted
|
|
338
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
339
|
-
expect(mutationRecalc).toBeDefined();
|
|
340
|
-
expect(mutationRecalc.testType).toBe("integration");
|
|
341
|
-
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
342
|
-
expect(mutationRecalc.steps[1].method).toBe("PATCH");
|
|
343
|
-
// Contract and not-found scenarios use the resolved full path
|
|
344
|
-
const contract = scenarios.find(s => s.scenarioName.includes("contract"));
|
|
345
|
-
expect(contract).toBeDefined();
|
|
346
|
-
expect(contract.steps[0].path).toBe("/api/v1/orders/{order_id}");
|
|
347
|
-
const notFound = scenarios.find(s => s.scenarioName.includes("not-found"));
|
|
348
|
-
expect(notFound).toBeDefined();
|
|
349
|
-
expect(notFound.steps[0].path).toBe("/api/v1/orders/{order_id}");
|
|
350
|
-
});
|
|
351
|
-
it("resolves router-relative base paths (e.g. /orders) against known resourceGroups", () => {
|
|
352
|
-
const groups = makeGroups({
|
|
353
|
-
orders: { basePath: "/api/v1/orders", methods: ["GET", "POST"], paramPath: "/api/v1/orders/{order_id}" },
|
|
354
|
-
});
|
|
355
|
-
const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/orders" }], groups);
|
|
356
|
-
expect(scenarios.length).toBeGreaterThanOrEqual(2);
|
|
357
|
-
const integration = scenarios.find(s => s.testType === "integration");
|
|
358
|
-
expect(integration).toBeDefined();
|
|
359
|
-
expect(integration.category).toBe("new_endpoint");
|
|
360
|
-
});
|
|
361
|
-
it("skips unresolvable router-relative paths", () => {
|
|
362
|
-
const groups = makeGroups({
|
|
363
|
-
products: { basePath: "/api/v1/products", methods: ["GET", "POST"], paramPath: "/api/v1/products/{id}" },
|
|
364
|
-
});
|
|
365
|
-
// /{nonexistent_id} doesn't match any known resource
|
|
366
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{nonexistent_id}" }], groups);
|
|
367
|
-
expect(scenarios).toEqual([]);
|
|
368
|
-
});
|
|
369
|
-
it("PR #195 regression: PATCH /{order_id} produces mutation-recalculate scenario in GENERATE slots", () => {
|
|
370
|
-
// Simulates the exact data from PR #195:
|
|
371
|
-
// - Full endpoint list with orders, products, reviews
|
|
372
|
-
// - newEndpoints has PATCH /{order_id} (router-relative)
|
|
373
|
-
const endpoints = [
|
|
374
|
-
{ path: "/api/v1/orders", methods: ["GET", "POST"] },
|
|
375
|
-
{ path: "/api/v1/orders/{order_id}", methods: ["GET", "PATCH", "DELETE"] },
|
|
376
|
-
{ path: "/api/v1/products", methods: ["GET", "POST"] },
|
|
377
|
-
{ path: "/api/v1/products/{product_id}", methods: ["GET", "PUT", "DELETE"] },
|
|
378
|
-
{ path: "/api/v1/reviews", methods: ["GET", "POST"] },
|
|
379
|
-
{ path: "/api/v1/reset", methods: ["POST"] },
|
|
380
|
-
];
|
|
381
|
-
const newEndpoints = [{ method: "PATCH", path: "/{order_id}" }];
|
|
382
|
-
const allScenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
383
|
-
// The diff-direct scenarios should include orders-patch-mutation-verify
|
|
384
|
-
const mutationRecalc = allScenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
385
|
-
expect(mutationRecalc).toBeDefined();
|
|
386
|
-
expect(mutationRecalc.category).toBe("new_endpoint"); // → CRITICAL priority tier
|
|
387
|
-
expect(mutationRecalc.steps).toHaveLength(3);
|
|
388
|
-
expect(mutationRecalc.steps[0].method).toBe("POST");
|
|
389
|
-
expect(mutationRecalc.steps[1].method).toBe("PATCH");
|
|
390
|
-
expect(mutationRecalc.steps[2].method).toBe("GET");
|
|
391
|
-
expect(mutationRecalc.description).toContain("derived calculations");
|
|
392
|
-
// Verify it outranks all non-new_endpoint scenarios (cascade-delete, unique-constraint, etc.)
|
|
393
|
-
const newEndpointScenarios = allScenarios.filter(s => s.category === "new_endpoint");
|
|
394
|
-
const otherScenarios = allScenarios.filter(s => s.category !== "new_endpoint");
|
|
395
|
-
expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(3);
|
|
396
|
-
// All new_endpoint scenarios should include our mutation-recalculate
|
|
397
|
-
expect(newEndpointScenarios.some(s => s.scenarioName === "orders-patch-mutation-verify")).toBe(true);
|
|
398
|
-
// Non-new_endpoint scenarios should NOT include mutation-recalculate
|
|
399
|
-
expect(otherScenarios.every(s => s.scenarioName !== "orders-patch-mutation-verify")).toBe(true);
|
|
400
|
-
});
|
|
401
|
-
});
|
|
402
|
-
describe("diffDirectMutationRecalc — diverse app types (no hardcoded field names)", () => {
|
|
403
|
-
const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
|
|
404
|
-
const testCases = [
|
|
405
|
-
{
|
|
406
|
-
name: "E-commerce: PATCH /orders/{order_id}",
|
|
407
|
-
resource: "orders",
|
|
408
|
-
group: { basePath: "/api/v1/orders", methods: ["GET", "POST", "PATCH"], paramPath: "/api/v1/orders/{order_id}" },
|
|
409
|
-
method: "PATCH",
|
|
410
|
-
expectedParamName: "order_id",
|
|
411
|
-
},
|
|
412
|
-
{
|
|
413
|
-
name: "Project management: PUT /projects/{id}",
|
|
414
|
-
resource: "projects",
|
|
415
|
-
group: { basePath: "/api/projects", methods: ["GET", "POST", "PUT"], paramPath: "/api/projects/{id}" },
|
|
416
|
-
method: "PUT",
|
|
417
|
-
expectedParamName: "id",
|
|
418
|
-
},
|
|
419
|
-
{
|
|
420
|
-
name: "Invoicing: PATCH /invoices/{invoice_id}",
|
|
421
|
-
resource: "invoices",
|
|
422
|
-
group: { basePath: "/v2/invoices", methods: ["GET", "POST", "PATCH"], paramPath: "/v2/invoices/{invoice_id}" },
|
|
423
|
-
method: "PATCH",
|
|
424
|
-
expectedParamName: "invoice_id",
|
|
425
|
-
},
|
|
426
|
-
{
|
|
427
|
-
name: "Education LMS: PUT /courses/{course_id}",
|
|
428
|
-
resource: "courses",
|
|
429
|
-
group: { basePath: "/api/courses", methods: ["GET", "POST", "PUT"], paramPath: "/api/courses/{course_id}" },
|
|
430
|
-
method: "PUT",
|
|
431
|
-
expectedParamName: "course_id",
|
|
432
|
-
},
|
|
433
|
-
{
|
|
434
|
-
name: "Healthcare: PATCH /prescriptions/{prescription_id}",
|
|
435
|
-
resource: "prescriptions",
|
|
436
|
-
group: { basePath: "/api/prescriptions", methods: ["GET", "POST", "PATCH"], paramPath: "/api/prescriptions/{prescription_id}" },
|
|
437
|
-
method: "PATCH",
|
|
438
|
-
expectedParamName: "prescription_id",
|
|
439
|
-
},
|
|
440
|
-
{
|
|
441
|
-
name: "Logistics: PUT /shipments/{shipment_id}",
|
|
442
|
-
resource: "shipments",
|
|
443
|
-
group: { basePath: "/api/shipments", methods: ["GET", "POST", "PUT"], paramPath: "/api/shipments/{shipment_id}" },
|
|
444
|
-
method: "PUT",
|
|
445
|
-
expectedParamName: "shipment_id",
|
|
446
|
-
},
|
|
447
|
-
{
|
|
448
|
-
name: "Restaurant: PATCH /menus/{menuId}",
|
|
449
|
-
resource: "menus",
|
|
450
|
-
group: { basePath: "/api/menus", methods: ["GET", "POST", "PATCH"], paramPath: "/api/menus/{menuId}" },
|
|
451
|
-
method: "PATCH",
|
|
452
|
-
expectedParamName: "menuId",
|
|
453
|
-
},
|
|
454
|
-
{
|
|
455
|
-
name: "Cloud billing: PUT /subscriptions/{subscription_id}",
|
|
456
|
-
resource: "subscriptions",
|
|
457
|
-
group: { basePath: "/billing/subscriptions", methods: ["GET", "POST", "PUT"], paramPath: "/billing/subscriptions/{subscription_id}" },
|
|
458
|
-
method: "PUT",
|
|
459
|
-
expectedParamName: "subscription_id",
|
|
460
|
-
},
|
|
461
|
-
{
|
|
462
|
-
name: "Warehouse: PATCH /inventories/{uuid}",
|
|
463
|
-
resource: "inventories",
|
|
464
|
-
group: { basePath: "/api/inventories", methods: ["GET", "POST", "PATCH"], paramPath: "/api/inventories/{uuid}" },
|
|
465
|
-
method: "PATCH",
|
|
466
|
-
expectedParamName: "uuid",
|
|
467
|
-
},
|
|
468
|
-
{
|
|
469
|
-
name: "Minimal: PATCH /items (no paramPath)",
|
|
470
|
-
resource: "items",
|
|
471
|
-
group: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
|
|
472
|
-
method: "PATCH",
|
|
473
|
-
expectedParamName: "item_id",
|
|
474
|
-
},
|
|
475
|
-
];
|
|
476
|
-
it.each(testCases)("$name — produces valid mutation-recalc scenario", ({ resource, group, method, expectedParamName }) => {
|
|
477
|
-
const groups = makeGroups({ [resource]: group });
|
|
478
|
-
const scenarios = draftDiffDirectScenarios([{ method, path: group.paramPath ?? group.basePath }], groups);
|
|
479
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
480
|
-
expect(mutationRecalc).toBeDefined();
|
|
481
|
-
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
482
|
-
expect(mutationRecalc.testType).toBe("integration");
|
|
483
|
-
expect(mutationRecalc.priority).toBe("high");
|
|
484
|
-
// Step 1: POST to create resource (all test cases have POST)
|
|
485
|
-
expect(mutationRecalc.steps[0].method).toBe("POST");
|
|
486
|
-
expect(mutationRecalc.steps[0].path).toBe(group.basePath);
|
|
487
|
-
// Step 2: PUT/PATCH with mutation
|
|
488
|
-
const mutationStep = mutationRecalc.steps[1];
|
|
489
|
-
expect(mutationStep.method).toBe(method);
|
|
490
|
-
// Note: bodyMustInclude removed in intent-based refactor - LLM discovers fields from source
|
|
491
|
-
// Verify NO hardcoded domain-specific field names
|
|
492
|
-
// Verify hints are descriptive (contain "e.g." or similar guidance)
|
|
493
|
-
// Chaining uses the actual path param name (only when paramPath exists)
|
|
494
|
-
if (group.paramPath) {
|
|
495
|
-
expect(mutationStep.chainsFrom).toBeDefined();
|
|
496
|
-
const chaining = mutationStep.chainsFrom;
|
|
497
|
-
expect(chaining.targetParam).toBe(expectedParamName);
|
|
498
|
-
expect(chaining.sourceStep).toBe(1);
|
|
499
|
-
expect(chaining.sourceField).toBe("<id-field>");
|
|
500
|
-
}
|
|
501
|
-
else {
|
|
502
|
-
expect(mutationStep.chainsFrom).toBeUndefined();
|
|
503
|
-
}
|
|
504
|
-
// Step 3: GET verification (if paramPath exists)
|
|
505
|
-
if (group.paramPath) {
|
|
506
|
-
expect(mutationRecalc.steps).toHaveLength(3);
|
|
507
|
-
const getStep = mutationRecalc.steps[2];
|
|
508
|
-
expect(getStep.method).toBe("GET");
|
|
509
|
-
// Note: expectedResponseFields removed in intent-based refactor
|
|
510
|
-
}
|
|
511
|
-
// chainingKeys uses the actual path param name, not hardcoded singular_id
|
|
512
|
-
expect(mutationRecalc.chainingKeys).toContain("id");
|
|
513
|
-
expect(mutationRecalc.chainingKeys).toContain(expectedParamName);
|
|
514
|
-
// Description is domain-agnostic
|
|
515
|
-
expect(mutationRecalc.description).toContain("Mutation test");
|
|
516
|
-
expect(mutationRecalc.description).toContain("derived calculations");
|
|
517
|
-
});
|
|
518
|
-
it("no paramPath: PATCH step targets basePath, no GET verification, fallback param name", () => {
|
|
519
|
-
const groups = makeGroups({
|
|
520
|
-
items: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
|
|
521
|
-
});
|
|
522
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/items" }], groups);
|
|
523
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
524
|
-
expect(mutationRecalc).toBeDefined();
|
|
525
|
-
// No paramPath → PATCH targets basePath, no GET step
|
|
526
|
-
expect(mutationRecalc.steps).toHaveLength(2);
|
|
527
|
-
expect(mutationRecalc.steps[1].path).toBe("/items");
|
|
528
|
-
// Fallback param name: singular + _id
|
|
529
|
-
expect(mutationRecalc.chainingKeys).toContain("item_id");
|
|
530
|
-
});
|
|
531
|
-
it("resource without POST: no create step, no chaining", () => {
|
|
532
|
-
const groups = makeGroups({
|
|
533
|
-
configs: { basePath: "/api/configs", methods: ["GET", "PATCH"], paramPath: "/api/configs/{config_id}" },
|
|
534
|
-
});
|
|
535
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/configs/{config_id}" }], groups);
|
|
536
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
537
|
-
expect(mutationRecalc).toBeDefined();
|
|
538
|
-
// No POST → starts with PATCH directly, no chaining
|
|
539
|
-
expect(mutationRecalc.steps[0].method).toBe("PATCH");
|
|
540
|
-
expect(mutationRecalc.steps[0].chainsFrom).toBeUndefined();
|
|
541
|
-
// Still has GET verification
|
|
542
|
-
expect(mutationRecalc.steps[1].method).toBe("GET");
|
|
543
|
-
expect(mutationRecalc.steps[1].chainsFrom).toBeUndefined();
|
|
544
|
-
});
|
|
545
|
-
it("generates auth-boundary scenario for each new endpoint (security_boundary, not new_endpoint)", () => {
|
|
546
|
-
const groups = makeGroups({
|
|
547
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
|
|
548
|
-
});
|
|
549
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
550
|
-
const authBoundary = scenarios.find(s => s.scenarioName === "orders-put-auth-boundary");
|
|
551
|
-
expect(authBoundary).toBeDefined();
|
|
552
|
-
expect(authBoundary.category).toBe("security_boundary");
|
|
553
|
-
expect(authBoundary.testType).toBe("contract");
|
|
554
|
-
expect(authBoundary.steps[0].method).toBe("PUT");
|
|
555
|
-
expect(authBoundary.steps[0].path).toBe("/api/orders/{order_id}");
|
|
556
|
-
expect(authBoundary.steps[0].expectedStatusCode).toBe(401);
|
|
557
|
-
expect(authBoundary.requiresAuth).toBe(false);
|
|
558
|
-
});
|
|
559
|
-
it("auth-boundary scenario is not duplicated for same method/path", () => {
|
|
560
|
-
const groups = makeGroups({
|
|
561
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT", "DELETE"], paramPath: "/api/orders/{order_id}" },
|
|
562
|
-
});
|
|
563
|
-
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }, { method: "DELETE", path: "/api/orders/{order_id}" }], groups);
|
|
564
|
-
const authBoundaries = scenarios.filter(s => s.scenarioName.includes("auth-boundary"));
|
|
565
|
-
// One for PUT, one for DELETE (not GET, not POST)
|
|
566
|
-
expect(authBoundaries).toHaveLength(2);
|
|
567
|
-
expect(authBoundaries.map(s => s.scenarioName).sort()).toEqual([
|
|
568
|
-
"orders-delete-auth-boundary",
|
|
569
|
-
"orders-put-auth-boundary",
|
|
570
|
-
]);
|
|
571
|
-
});
|
|
572
|
-
it("does NOT generate auth-boundary for GET endpoints (often public)", () => {
|
|
573
|
-
const groups = makeGroups({
|
|
574
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{order_id}" },
|
|
575
|
-
});
|
|
576
|
-
const scenarios = draftDiffDirectScenarios([{ method: "GET", path: "/api/orders/{order_id}" }], groups);
|
|
577
|
-
const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
|
|
578
|
-
expect(authBoundary).toBeUndefined();
|
|
579
|
-
});
|
|
580
|
-
it("does NOT generate auth-boundary for POST", () => {
|
|
581
|
-
const groups = makeGroups({
|
|
582
|
-
products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
|
|
583
|
-
});
|
|
584
|
-
const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
|
|
585
|
-
const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
|
|
586
|
-
expect(authBoundary).toBeUndefined();
|
|
587
|
-
});
|
|
588
|
-
});
|
|
589
|
-
describe("draftDiffDirectScenarios — GET without path param", () => {
|
|
590
|
-
const resourceGroups = new Map([
|
|
591
|
-
["products", {
|
|
592
|
-
basePath: "/api/v1/products",
|
|
593
|
-
methods: new Set(["GET", "POST"]),
|
|
594
|
-
paramPath: "/api/v1/products/{product_id}",
|
|
595
|
-
}],
|
|
596
|
-
]);
|
|
597
|
-
it("GET no-param: produces contract + collection scenario (LLM determines if query params apply)", () => {
|
|
598
|
-
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
|
|
599
|
-
const names = result.map(s => s.scenarioName);
|
|
600
|
-
expect(names).toContain("products-get-new-endpoint-contract");
|
|
601
|
-
expect(names).toContain("products-search-list-query");
|
|
602
|
-
const scenario = result.find(s => s.scenarioName === "products-search-list-query");
|
|
603
|
-
expect(scenario.category).toBe("new_endpoint");
|
|
604
|
-
// Description instructs LLM to read source for query param logic
|
|
605
|
-
expect(scenario.description).toMatch(/read diff source|query param/i);
|
|
606
|
-
});
|
|
607
|
-
it("GET no-param: produces zero cascade-delete or unique-constraint scenarios", () => {
|
|
608
|
-
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
|
|
609
|
-
const names = result.map(s => s.scenarioName);
|
|
610
|
-
expect(names.some(n => n.includes("cascade"))).toBe(false);
|
|
611
|
-
expect(names.some(n => n.includes("unique"))).toBe(false);
|
|
612
|
-
expect(names.some(n => n.includes("delete-blocked"))).toBe(false);
|
|
613
|
-
});
|
|
614
|
-
it("collection GET with POST: produces integration scenario (POST then GET)", () => {
|
|
615
|
-
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products" }], resourceGroups);
|
|
616
|
-
const scenario = result.find(s => s.scenarioName === "products-list-query");
|
|
617
|
-
expect(scenario).toBeDefined();
|
|
618
|
-
expect(scenario.testType).toBe("integration");
|
|
619
|
-
expect(scenario.steps[0].method).toBe("POST");
|
|
620
|
-
expect(scenario.steps[1].method).toBe("GET");
|
|
621
|
-
});
|
|
622
|
-
it("collection GET without POST: produces contract scenario (single GET step)", () => {
|
|
623
|
-
const readOnlyGroups = new Map([
|
|
624
|
-
["reports", {
|
|
625
|
-
basePath: "/api/v1/reports",
|
|
626
|
-
methods: new Set(["GET"]),
|
|
627
|
-
paramPath: undefined,
|
|
628
|
-
}],
|
|
629
|
-
]);
|
|
630
|
-
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/reports" }], readOnlyGroups);
|
|
631
|
-
const scenario = result.find(s => s.scenarioName === "reports-list-query");
|
|
632
|
-
expect(scenario).toBeDefined();
|
|
633
|
-
expect(scenario.testType).toBe("contract");
|
|
634
|
-
expect(scenario.steps).toHaveLength(1);
|
|
635
|
-
});
|
|
636
|
-
});
|
|
637
|
-
// ---------------------------------------------------------------------------
|
|
638
|
-
// capScenarios — enforced via draftScenariosFromEndpoints
|
|
639
|
-
// ---------------------------------------------------------------------------
|
|
640
|
-
describe("draftScenariosFromEndpoints — capScenarios hard limit", () => {
|
|
641
|
-
// Each PUT/PATCH new endpoint produces ~5 scenarios (mutation-verify, boundary-values,
|
|
642
|
-
// contract, not-found, auth-boundary). Seven resources × 2 methods each = 70 new
|
|
643
|
-
// endpoints → well above the 30-scenario cap.
|
|
644
|
-
function makeEndpointsAndNewEps(resourceNames) {
|
|
645
|
-
const endpoints = resourceNames.flatMap(r => [
|
|
646
|
-
{ path: `/api/${r}`, methods: [{ method: "GET" }, { method: "POST" }] },
|
|
647
|
-
{ path: `/api/${r}/{id}`, methods: [{ method: "GET" }, { method: "PUT" }, { method: "PATCH" }, { method: "DELETE" }] },
|
|
648
|
-
]);
|
|
649
|
-
const newEndpoints = resourceNames.flatMap(r => [
|
|
650
|
-
{ method: "PUT", path: `/api/${r}/{id}` },
|
|
651
|
-
{ method: "PATCH", path: `/api/${r}/{id}` },
|
|
652
|
-
]);
|
|
653
|
-
return { endpoints, newEndpoints };
|
|
654
|
-
}
|
|
655
|
-
it("caps output at 30 scenarios when many new endpoints are present", () => {
|
|
656
|
-
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
657
|
-
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
658
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
659
|
-
expect(result.length).toBeLessThanOrEqual(30);
|
|
660
|
-
});
|
|
661
|
-
it("always keeps CRITICAL (new_endpoint) scenarios under the cap", () => {
|
|
662
|
-
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
663
|
-
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
664
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
665
|
-
// new_endpoint is CRITICAL — verify at least some survive
|
|
666
|
-
const criticalOnes = result.filter((s) => s.category === "new_endpoint");
|
|
667
|
-
expect(criticalOnes.length).toBeGreaterThan(0);
|
|
668
|
-
});
|
|
669
|
-
it("CRITICAL items (new_endpoint) fill the cap first; security_boundary may be dropped", () => {
|
|
670
|
-
// With many new endpoints, diffDirectContract/NotFound/MutationVerify/BoundaryValues all
|
|
671
|
-
// produce category "new_endpoint" (CRITICAL). When CRITICAL items alone exceed 30, the hard
|
|
672
|
-
// cap slices them first — breadth picks for non-CRITICAL categories are not guaranteed.
|
|
673
|
-
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
674
|
-
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
675
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
676
|
-
// All 30 surviving scenarios should be CRITICAL (new_endpoint)
|
|
677
|
-
const nonCritical = result.filter((s) => s.category !== "new_endpoint");
|
|
678
|
-
// Non-critical (security_boundary) may be 0 — this is the documented cap behavior
|
|
679
|
-
expect(nonCritical.length).toBeLessThanOrEqual(result.length);
|
|
680
|
-
// And the cap is enforced
|
|
681
|
-
expect(result.length).toBeLessThanOrEqual(30);
|
|
682
|
-
});
|
|
683
|
-
it("does not cap when total scenarios are within the limit", () => {
|
|
684
|
-
const endpoints = [
|
|
685
|
-
{ path: "/api/users", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
686
|
-
{ path: "/api/users/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
|
|
687
|
-
];
|
|
688
|
-
const newEndpoints = [
|
|
689
|
-
{ method: "PUT", path: "/api/users/{id}" },
|
|
690
|
-
];
|
|
691
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
692
|
-
// A single PUT endpoint produces 5 scenarios — well within the 30-scenario cap
|
|
693
|
-
expect(result.length).toBeLessThanOrEqual(30);
|
|
694
|
-
expect(result.length).toBeGreaterThanOrEqual(4);
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
describe("path collision fix — resourceGroups disambiguation", () => {
|
|
698
|
-
it("unrelated paths sharing a last segment do not overwrite each other", () => {
|
|
699
|
-
// /api/v1/orders/search and /api/v1/products/search share 'search' as last segment.
|
|
700
|
-
// Before the fix, the second overwrote the first in resourceGroups.
|
|
701
|
-
// The scenario must reference the orders/search path specifically, not products/search.
|
|
702
|
-
const endpoints = [
|
|
703
|
-
{ path: "/api/v1/orders/search", methods: ["GET"] },
|
|
704
|
-
{ path: "/api/v1/products/search", methods: ["GET"] },
|
|
705
|
-
];
|
|
706
|
-
const newEndpoints = [{ method: "GET", path: "/api/v1/orders/search" }];
|
|
707
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
708
|
-
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
709
|
-
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
710
|
-
// Must contain orders/search
|
|
711
|
-
expect(allPaths.some((p) => p.includes("orders/search"))).toBe(true);
|
|
712
|
-
// Must NOT contain products/search — wrong path from a collision would appear here
|
|
713
|
-
expect(allPaths.some((p) => p.includes("products/search"))).toBe(false);
|
|
714
|
-
});
|
|
715
|
-
it("related paths (nested resources) still merge correctly", () => {
|
|
716
|
-
// /api/v1/users and /api/v1/users/{id} are related — should still merge
|
|
717
|
-
const endpoints = [
|
|
718
|
-
{ path: "/api/v1/users", methods: ["GET", "POST"] },
|
|
719
|
-
{ path: "/api/v1/users/{id}", methods: ["GET", "PUT", "DELETE"] },
|
|
720
|
-
];
|
|
721
|
-
const newEndpoints = [{ method: "POST", path: "/api/v1/users" }];
|
|
722
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
723
|
-
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
724
|
-
});
|
|
725
|
-
it("new endpoint under a disambiguated group is not silently dropped", () => {
|
|
726
|
-
// When two paths share a last segment, both are stored under disambiguated keys
|
|
727
|
-
// (e.g. "search::/api/v1/orders/search"). The fallback in draftDiffDirectScenarios
|
|
728
|
-
// must use the full key for resourceGroups.get(), not the stripped "search" name
|
|
729
|
-
// (which has no entry), otherwise the scenario is silently skipped.
|
|
730
|
-
const endpoints = [
|
|
731
|
-
{ path: "/api/v1/orders/search", methods: ["GET"] },
|
|
732
|
-
{ path: "/api/v1/products/search", methods: ["GET"] },
|
|
733
|
-
{ path: "/api/v1/orders", methods: ["GET", "POST"] },
|
|
734
|
-
{ path: "/api/v1/products", methods: ["GET", "POST"] },
|
|
735
|
-
];
|
|
736
|
-
const newEndpoints = [
|
|
737
|
-
{ method: "GET", path: "/api/v1/orders/search" },
|
|
738
|
-
{ method: "GET", path: "/api/v1/products/search" },
|
|
739
|
-
];
|
|
740
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
741
|
-
// Both new endpoints must produce at least one scenario each — neither should be dropped
|
|
742
|
-
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
743
|
-
expect(allPaths.some((p) => p.includes("orders/search") || p.includes("orders"))).toBe(true);
|
|
744
|
-
expect(allPaths.some((p) => p.includes("products/search") || p.includes("products"))).toBe(true);
|
|
745
|
-
});
|
|
746
|
-
it("paths sharing a string prefix but different resource names are not merged", () => {
|
|
747
|
-
// /api/v1/orders and /api/v1/orders-archive start with the same string
|
|
748
|
-
// but are different resources. startsWith alone would incorrectly merge them.
|
|
749
|
-
const endpoints = [
|
|
750
|
-
{ path: "/api/v1/orders", methods: ["GET", "POST"] },
|
|
751
|
-
{ path: "/api/v1/orders-archive", methods: ["GET"] },
|
|
752
|
-
];
|
|
753
|
-
const newEndpoints = [{ method: "POST", path: "/api/v1/orders" }];
|
|
754
|
-
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
755
|
-
expect(result.length).toBeGreaterThanOrEqual(1);
|
|
756
|
-
const allPaths = result.flatMap((s) => (s.steps ?? []).map((step) => step.path ?? ""));
|
|
757
|
-
// orders scenario must be present
|
|
758
|
-
expect(allPaths.some((p) => p.includes("/orders"))).toBe(true);
|
|
759
|
-
// orders-archive must not appear in scenarios targeting /orders
|
|
760
|
-
expect(allPaths.some((p) => p.includes("orders-archive"))).toBe(false);
|
|
761
|
-
});
|
|
762
|
-
});
|
|
763
|
-
describe("draftDiffDirectScenarios — execution endpoints", () => {
|
|
764
|
-
it("produces execution templates for /execute-style endpoints (non-GET)", () => {
|
|
765
|
-
const newEndpoints = [{ method: "POST", path: "/api/jobs/execute" }];
|
|
766
|
-
const resourceGroups = new Map([
|
|
767
|
-
["jobs", { basePath: "/api/jobs", methods: new Set(["GET", "POST"]), paramPath: "/api/jobs/{id}" }],
|
|
768
|
-
]);
|
|
769
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
|
|
770
|
-
const names = result.map((s) => s.scenarioName);
|
|
771
|
-
expect(names).toContain("jobs-post-execute-happy");
|
|
772
|
-
expect(names).toContain("jobs-post-execute-invalid");
|
|
773
|
-
expect(names).toContain("jobs-post-auth-boundary");
|
|
774
|
-
// Should NOT produce CRUD lifecycle templates
|
|
775
|
-
expect(names.some((n) => n.includes("integration") || n.includes("contract"))).toBe(false);
|
|
776
|
-
});
|
|
777
|
-
it("does not produce execution templates for GET on execution endpoints", () => {
|
|
778
|
-
const newEndpoints = [{ method: "GET", path: "/api/jobs/execute" }];
|
|
779
|
-
const resourceGroups = new Map([
|
|
780
|
-
["jobs", { basePath: "/api/jobs", methods: new Set(["GET"]), paramPath: "/api/jobs/{id}" }],
|
|
781
|
-
]);
|
|
782
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
|
|
783
|
-
const names = result.map((s) => s.scenarioName);
|
|
784
|
-
// GET should fall through to normal GET handling, not execution templates
|
|
785
|
-
expect(names.some((n) => n.includes("execute-happy"))).toBe(false);
|
|
786
|
-
expect(names.some((n) => n.includes("execute-invalid"))).toBe(false);
|
|
787
|
-
});
|
|
788
|
-
});
|
|
789
|
-
describe("draftDiffDirectScenarios — auth boundary status codes", () => {
|
|
790
|
-
const newEndpoints = [{ method: "DELETE", path: "/api/orders/{id}" }];
|
|
791
|
-
const resourceGroups = new Map([
|
|
792
|
-
["orders", { basePath: "/api/orders", methods: new Set(["GET", "POST", "DELETE"]), paramPath: "/api/orders/{id}" }],
|
|
793
|
-
]);
|
|
794
|
-
it("expects 403 for Bearer auth type", () => {
|
|
795
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.Bearer);
|
|
796
|
-
const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
|
|
797
|
-
expect(authScenario).toBeDefined();
|
|
798
|
-
expect(authScenario.steps[0].expectedStatusCode).toBe(403);
|
|
799
|
-
});
|
|
800
|
-
it("expects 403 for OAuth auth type (uses Bearer scheme)", () => {
|
|
801
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.OAuth);
|
|
802
|
-
const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
|
|
803
|
-
expect(authScenario).toBeDefined();
|
|
804
|
-
expect(authScenario.steps[0].expectedStatusCode).toBe(403);
|
|
805
|
-
});
|
|
806
|
-
it("expects 401 for other auth types", () => {
|
|
807
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups, WorkspaceAuthType.ApiKey);
|
|
808
|
-
const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
|
|
809
|
-
expect(authScenario).toBeDefined();
|
|
810
|
-
expect(authScenario.steps[0].expectedStatusCode).toBe(401);
|
|
811
|
-
});
|
|
812
|
-
it("expects 401 when no auth type specified", () => {
|
|
813
|
-
const result = draftDiffDirectScenarios(newEndpoints, resourceGroups);
|
|
814
|
-
const authScenario = result.find((s) => s.scenarioName.includes("auth-boundary"));
|
|
815
|
-
expect(authScenario).toBeDefined();
|
|
816
|
-
expect(authScenario.steps[0].expectedStatusCode).toBe(401);
|
|
817
|
-
});
|
|
818
|
-
});
|
|
819
|
-
describe("draftResponseShapeScenarios", () => {
|
|
820
|
-
it("drafts a contract scenario with expectedResponseFields when field hints are known", () => {
|
|
821
|
-
const hopEndpoints = [
|
|
822
|
-
{ path: "/users/me", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
|
|
823
|
-
];
|
|
824
|
-
const byFile = { "src/dtos/user.dto.ts": ["/users/me"] };
|
|
825
|
-
const addedFieldsByFile = { "src/dtos/user.dto.ts": ["initials"] };
|
|
826
|
-
const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, addedFieldsByFile);
|
|
827
|
-
expect(scenarios).toHaveLength(1);
|
|
828
|
-
const s = scenarios[0];
|
|
829
|
-
expect(s.category).toBe("breaking_change");
|
|
830
|
-
expect(s.priority).toBe("high");
|
|
831
|
-
expect(s.testType).toBe("contract");
|
|
832
|
-
expect(s.steps).toHaveLength(1);
|
|
833
|
-
expect(s.steps[0].method).toBe("GET");
|
|
834
|
-
expect(s.steps[0].path).toBe("/users/me");
|
|
835
|
-
expect(s.steps[0].expectedStatusCode).toBe(200);
|
|
836
|
-
expect(s.steps[0].expectedResponseFields).toEqual(["initials"]);
|
|
837
|
-
expect(s.scenarioName).toBe("user-response-shape-initials");
|
|
838
|
-
expect(s.bugCatchingTarget).toContain("initials");
|
|
839
|
-
expect(s.bugCatchingTarget).toContain("user.dto.ts");
|
|
840
|
-
expect(s.description).toContain("initials");
|
|
841
|
-
});
|
|
842
|
-
it("still drafts a scenario without expectedResponseFields when no field hints are known", () => {
|
|
843
|
-
const hopEndpoints = [
|
|
844
|
-
{ path: "/orders/{id}", methods: ["GET"], sourceFile: "src/controllers/order.controller.ts" },
|
|
845
|
-
];
|
|
846
|
-
const byFile = { "src/services/order.service.ts": ["/orders/{id}"] };
|
|
847
|
-
const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, {});
|
|
848
|
-
expect(scenarios).toHaveLength(1);
|
|
849
|
-
expect(scenarios[0].steps[0].expectedResponseFields).toBeUndefined();
|
|
850
|
-
expect(scenarios[0].scenarioName).toBe("order-response-shape-response-change");
|
|
851
|
-
});
|
|
852
|
-
it("caps drafted scenarios at 2 per file, preferring detail GETs over collection GETs", () => {
|
|
853
|
-
const hopEndpoints = [
|
|
854
|
-
{ path: "/users", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
|
|
855
|
-
{ path: "/users/{id}", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
|
|
856
|
-
{ path: "/users/me", methods: ["GET"], sourceFile: "src/controllers/user.controller.ts" },
|
|
857
|
-
];
|
|
858
|
-
const byFile = { "src/dtos/user.dto.ts": ["/users", "/users/{id}", "/users/me"] };
|
|
859
|
-
const scenarios = draftResponseShapeScenarios(hopEndpoints, byFile, {});
|
|
860
|
-
expect(scenarios).toHaveLength(2);
|
|
861
|
-
const paths = scenarios.map((s) => s.steps[0].path);
|
|
862
|
-
// Both detail-style GETs (path param or literal /me) win over the plain collection GET.
|
|
863
|
-
expect(paths).toEqual(expect.arrayContaining(["/users/{id}", "/users/me"]));
|
|
864
|
-
expect(paths).not.toContain("/users");
|
|
865
|
-
});
|
|
866
|
-
it("skips a file whose only hop-mapped endpoints have no GET method", () => {
|
|
867
|
-
const hopEndpoints = [
|
|
868
|
-
{ path: "/orders/{id}", methods: ["DELETE"], sourceFile: "src/controllers/order.controller.ts" },
|
|
869
|
-
];
|
|
870
|
-
const byFile = { "src/services/order.service.ts": ["/orders/{id}"] };
|
|
871
|
-
expect(draftResponseShapeScenarios(hopEndpoints, byFile, {})).toEqual([]);
|
|
872
|
-
});
|
|
873
|
-
it("returns an empty array when byFile is empty", () => {
|
|
874
|
-
expect(draftResponseShapeScenarios([], {}, {})).toEqual([]);
|
|
875
|
-
});
|
|
876
|
-
});
|