@skyramp/mcp 0.0.65 → 0.1.0-rc.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/playwright/traceRecordingPrompt.js +30 -36
- package/build/prompts/architectPersona.js +19 -0
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +11 -6
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +49 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +4 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +42 -50
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +125 -0
- package/build/prompts/test-recommendation/recommendationSections.js +121 -4
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +151 -9
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +416 -61
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +455 -63
- package/build/prompts/testbot/testbot-prompts.js +111 -100
- package/build/prompts/testbot/testbot-prompts.test.js +142 -0
- package/build/resources/analysisResources.js +13 -5
- package/build/services/ScenarioGenerationService.js +2 -2
- package/build/services/ScenarioGenerationService.test.js +35 -0
- package/build/services/TestExecutionService.js +1 -1
- package/build/tools/code-refactor/modularizationTool.js +2 -2
- package/build/tools/executeSkyrampTestTool.js +4 -3
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +51 -21
- package/build/tools/generate-tests/generateContractRestTool.js +26 -4
- package/build/tools/generate-tests/generateIntegrationRestTool.js +44 -13
- package/build/tools/generate-tests/generateScenarioRestTool.js +17 -39
- package/build/tools/generate-tests/generateUIRestTool.js +69 -4
- package/build/tools/submitReportTool.js +27 -13
- package/build/tools/test-management/analyzeChangesTool.js +32 -10
- package/build/tools/test-management/analyzeChangesTool.test.js +85 -0
- package/build/types/RepositoryAnalysis.js +25 -3
- package/build/types/TestRecommendation.js +5 -4
- package/build/types/TestTypes.js +44 -9
- package/build/utils/AnalysisStateManager.js +43 -9
- package/build/utils/AnalysisStateManager.test.js +35 -0
- package/build/utils/routeParsers.js +35 -0
- package/build/utils/routeParsers.test.js +66 -1
- package/build/utils/scenarioDrafting.js +207 -360
- package/build/utils/scenarioDrafting.test.js +191 -256
- package/build/utils/trace-parser.js +24 -6
- package/build/utils/trace-parser.test.js +140 -0
- package/node_modules/playwright/lib/mcp/browser/browserServerBackend.js +3 -0
- package/node_modules/playwright/lib/mcp/browser/tab.js +8 -1
- package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +3 -2
- package/node_modules/playwright/lib/mcp/browser/tools/navigate.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/snapshot.js +4 -4
- package/node_modules/playwright/lib/mcp/browser/tools/tabs.js +5 -4
- package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
- package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -9
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +304 -7
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +128 -20
- package/package.json +2 -2
- package/node_modules/playwright/lib/mcp/terminal/help.json +0 -32
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @ts-ignore
|
|
2
|
-
import { isRealResource, draftScenariosFromEndpoints, draftDiffDirectScenarios,
|
|
2
|
+
import { isRealResource, draftScenariosFromEndpoints, draftDiffDirectScenarios, inferResourceRelationships, } from "./scenarioDrafting.js";
|
|
3
3
|
describe("isRealResource", () => {
|
|
4
4
|
it("accepts normal resource names", () => {
|
|
5
5
|
expect(isRealResource("users")).toBe(true);
|
|
@@ -44,182 +44,33 @@ describe("draftScenariosFromEndpoints — irregular plural singularization", ()
|
|
|
44
44
|
{ path: "/api/items", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
45
45
|
{ path: "/api/items/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
|
|
46
46
|
];
|
|
47
|
-
const scenarios = draftScenariosFromEndpoints(endpoints);
|
|
47
|
+
const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/categories" }]);
|
|
48
48
|
const allText = JSON.stringify(scenarios);
|
|
49
49
|
expect(allText).not.toContain("categorie_id");
|
|
50
50
|
expect(allText).not.toContain("\"categorie\"");
|
|
51
|
-
// The
|
|
52
|
-
const
|
|
53
|
-
expect(
|
|
54
|
-
expect(
|
|
51
|
+
// The diff-direct integration scenario description should use "category" (singular)
|
|
52
|
+
const integrationScenario = scenarios.find((s) => s.scenarioName === "categories-post-lifecycle");
|
|
53
|
+
expect(integrationScenario).toBeDefined();
|
|
54
|
+
expect(integrationScenario.steps[0].description).toContain("category");
|
|
55
55
|
});
|
|
56
56
|
it("uses 'status' (not 'statu') in FK field names for -ses plurals", () => {
|
|
57
57
|
const endpoints = [
|
|
58
58
|
{ path: "/api/statuses", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
59
59
|
{ path: "/api/statuses/{id}", methods: [{ method: "GET" }, { method: "DELETE" }] },
|
|
60
60
|
];
|
|
61
|
-
const scenarios = draftScenariosFromEndpoints(endpoints);
|
|
61
|
+
const scenarios = draftScenariosFromEndpoints(endpoints, [{ method: "POST", path: "/api/statuses" }]);
|
|
62
62
|
const allText = JSON.stringify(scenarios);
|
|
63
63
|
expect(allText).not.toContain("statu_id");
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
expect(
|
|
64
|
+
// The diff-direct integration scenario description should use "status" (singular)
|
|
65
|
+
const integrationScenario = scenarios.find((s) => s.scenarioName === "statuses-post-lifecycle");
|
|
66
|
+
expect(integrationScenario).toBeDefined();
|
|
67
|
+
expect(integrationScenario.steps[0].description).toContain("status");
|
|
67
68
|
});
|
|
68
69
|
});
|
|
69
70
|
describe("draftScenariosFromEndpoints", () => {
|
|
70
71
|
it("returns empty array for no endpoints", () => {
|
|
71
72
|
expect(draftScenariosFromEndpoints([])).toEqual([]);
|
|
72
73
|
});
|
|
73
|
-
it("generates CRUD scenario for resource with multiple methods", () => {
|
|
74
|
-
const endpoints = [
|
|
75
|
-
{ path: "/api/products", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
76
|
-
{ path: "/api/products/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
|
|
77
|
-
];
|
|
78
|
-
const scenarios = draftScenariosFromEndpoints(endpoints);
|
|
79
|
-
expect(scenarios.length).toBeGreaterThan(0);
|
|
80
|
-
const crudScenario = scenarios.find((s) => /crud/i.test(s.scenarioName) || /products/i.test(s.scenarioName));
|
|
81
|
-
expect(crudScenario).toBeDefined();
|
|
82
|
-
});
|
|
83
|
-
it("generates cross-resource scenario when multiple resources exist", () => {
|
|
84
|
-
const endpoints = [
|
|
85
|
-
{ path: "/api/users", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
86
|
-
{ path: "/api/users/{id}", methods: [{ method: "GET" }] },
|
|
87
|
-
{ path: "/api/products", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
88
|
-
{ path: "/api/products/{id}", methods: [{ method: "GET" }] },
|
|
89
|
-
{ path: "/api/orders", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
90
|
-
{ path: "/api/orders/{id}", methods: [{ method: "GET" }] },
|
|
91
|
-
];
|
|
92
|
-
const scenarios = draftScenariosFromEndpoints(endpoints);
|
|
93
|
-
const crossResource = scenarios.find((s) => /integration/i.test(s.scenarioName) && s.category === "workflow");
|
|
94
|
-
expect(crossResource).toBeDefined();
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
describe("draftUniqueConstraintScenarios", () => {
|
|
98
|
-
function makeGroups(resources) {
|
|
99
|
-
const map = new Map();
|
|
100
|
-
for (const [name, info] of Object.entries(resources)) {
|
|
101
|
-
map.set(name, { basePath: info.basePath, methods: new Set(info.methods), paramPath: info.paramPath });
|
|
102
|
-
}
|
|
103
|
-
return map;
|
|
104
|
-
}
|
|
105
|
-
it("generates unique constraint scenario for POST-able resources", () => {
|
|
106
|
-
const groups = makeGroups({
|
|
107
|
-
users: { basePath: "/api/users", methods: ["GET", "POST"], paramPath: "/api/users/{id}" },
|
|
108
|
-
});
|
|
109
|
-
const scenarios = draftUniqueConstraintScenarios(groups);
|
|
110
|
-
expect(scenarios).toHaveLength(1);
|
|
111
|
-
expect(scenarios[0].scenarioName).toBe("users-unique-constraint");
|
|
112
|
-
expect(scenarios[0].category).toBe("business_rule");
|
|
113
|
-
expect(scenarios[0].steps[1].expectedStatusCode).toBe(409);
|
|
114
|
-
});
|
|
115
|
-
it("skips resources without POST", () => {
|
|
116
|
-
const groups = makeGroups({
|
|
117
|
-
products: { basePath: "/api/products", methods: ["GET"], paramPath: "/api/products/{id}" },
|
|
118
|
-
});
|
|
119
|
-
expect(draftUniqueConstraintScenarios(groups)).toHaveLength(0);
|
|
120
|
-
});
|
|
121
|
-
it("caps output at MAX_DRAFTED_SCENARIOS_PER_CATEGORY", () => {
|
|
122
|
-
const groups = makeGroups({
|
|
123
|
-
users: { basePath: "/api/users", methods: ["POST"] },
|
|
124
|
-
products: { basePath: "/api/products", methods: ["POST"] },
|
|
125
|
-
orders: { basePath: "/api/orders", methods: ["POST"] },
|
|
126
|
-
tags: { basePath: "/api/tags", methods: ["POST"] },
|
|
127
|
-
comments: { basePath: "/api/comments", methods: ["POST"] },
|
|
128
|
-
reviews: { basePath: "/api/reviews", methods: ["POST"] },
|
|
129
|
-
});
|
|
130
|
-
expect(draftUniqueConstraintScenarios(groups).length).toBeLessThanOrEqual(4);
|
|
131
|
-
});
|
|
132
|
-
it("skips action-like resources", () => {
|
|
133
|
-
const groups = makeGroups({
|
|
134
|
-
login: { basePath: "/api/login", methods: ["POST"] },
|
|
135
|
-
users: { basePath: "/api/users", methods: ["POST"] },
|
|
136
|
-
});
|
|
137
|
-
const scenarios = draftUniqueConstraintScenarios(groups);
|
|
138
|
-
expect(scenarios.every((s) => !s.scenarioName.includes("login"))).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
describe("draftCascadeDeleteScenarios", () => {
|
|
142
|
-
function makeGroups(resources) {
|
|
143
|
-
const map = new Map();
|
|
144
|
-
for (const [name, info] of Object.entries(resources)) {
|
|
145
|
-
map.set(name, { basePath: info.basePath, methods: new Set(info.methods), paramPath: info.paramPath });
|
|
146
|
-
}
|
|
147
|
-
return map;
|
|
148
|
-
}
|
|
149
|
-
it("generates both cascade and delete-blocked scenarios for resource pairs", () => {
|
|
150
|
-
const groups = makeGroups({
|
|
151
|
-
collections: { basePath: "/api/collections", methods: ["GET", "POST", "DELETE"], paramPath: "/api/collections/{id}" },
|
|
152
|
-
links: { basePath: "/api/links", methods: ["GET", "POST", "DELETE"], paramPath: "/api/links/{id}" },
|
|
153
|
-
});
|
|
154
|
-
const scenarios = draftCascadeDeleteScenarios(groups);
|
|
155
|
-
expect(scenarios).toHaveLength(2);
|
|
156
|
-
expect(scenarios[0].scenarioName).toContain("cascade-delete");
|
|
157
|
-
expect(scenarios[0].category).toBe("data_integrity");
|
|
158
|
-
// Cascade: step 4 expects 404
|
|
159
|
-
expect(scenarios[0].steps[3].expectedStatusCode).toBe(404);
|
|
160
|
-
// Blocked: step 3 expects 409, step 4 expects 200
|
|
161
|
-
expect(scenarios[1].scenarioName).toContain("delete-blocked");
|
|
162
|
-
expect(scenarios[1].steps[2].expectedStatusCode).toBe(409);
|
|
163
|
-
expect(scenarios[1].steps[3].expectedStatusCode).toBe(200);
|
|
164
|
-
});
|
|
165
|
-
it("requires both POST and DELETE", () => {
|
|
166
|
-
const groups = makeGroups({
|
|
167
|
-
users: { basePath: "/api/users", methods: ["GET", "POST"], paramPath: "/api/users/{id}" },
|
|
168
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{id}" },
|
|
169
|
-
});
|
|
170
|
-
expect(draftCascadeDeleteScenarios(groups)).toHaveLength(0);
|
|
171
|
-
});
|
|
172
|
-
it("requires paramPath on both resources", () => {
|
|
173
|
-
const groups = makeGroups({
|
|
174
|
-
users: { basePath: "/api/users", methods: ["GET", "POST", "DELETE"] },
|
|
175
|
-
orders: { basePath: "/api/orders", methods: ["GET", "POST", "DELETE"], paramPath: "/api/orders/{id}" },
|
|
176
|
-
});
|
|
177
|
-
expect(draftCascadeDeleteScenarios(groups)).toHaveLength(0);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
describe("draftPermissionBoundaryScenarios", () => {
|
|
181
|
-
function makeGroups(resources) {
|
|
182
|
-
const map = new Map();
|
|
183
|
-
for (const [name, info] of Object.entries(resources)) {
|
|
184
|
-
map.set(name, { basePath: info.basePath, methods: new Set(info.methods), paramPath: info.paramPath });
|
|
185
|
-
}
|
|
186
|
-
return map;
|
|
187
|
-
}
|
|
188
|
-
it("generates auth boundary scenario for mutable resources", () => {
|
|
189
|
-
const groups = makeGroups({
|
|
190
|
-
users: { basePath: "/api/users", methods: ["GET", "POST"], paramPath: "/api/users/{id}" },
|
|
191
|
-
});
|
|
192
|
-
const scenarios = draftPermissionBoundaryScenarios(groups);
|
|
193
|
-
expect(scenarios.length).toBeGreaterThanOrEqual(1);
|
|
194
|
-
expect(scenarios[0].scenarioName).toBe("users-auth-boundary");
|
|
195
|
-
expect(scenarios[0].category).toBe("security_boundary");
|
|
196
|
-
expect(scenarios[0].steps[0].expectedStatusCode).toBe(401);
|
|
197
|
-
});
|
|
198
|
-
it("generates cross-user isolation scenario when resource has GET+POST+paramPath", () => {
|
|
199
|
-
const groups = makeGroups({
|
|
200
|
-
documents: { basePath: "/api/documents", methods: ["GET", "POST"], paramPath: "/api/documents/{id}" },
|
|
201
|
-
});
|
|
202
|
-
const scenarios = draftPermissionBoundaryScenarios(groups);
|
|
203
|
-
const crossUser = scenarios.find((s) => s.scenarioName.includes("cross-user"));
|
|
204
|
-
expect(crossUser).toBeDefined();
|
|
205
|
-
expect(crossUser.steps[1].expectedStatusCode).toBe(403);
|
|
206
|
-
});
|
|
207
|
-
it("skips read-only resources", () => {
|
|
208
|
-
const groups = makeGroups({
|
|
209
|
-
reports: { basePath: "/api/reports", methods: ["GET"], paramPath: "/api/reports/{id}" },
|
|
210
|
-
});
|
|
211
|
-
expect(draftPermissionBoundaryScenarios(groups)).toHaveLength(0);
|
|
212
|
-
});
|
|
213
|
-
it("caps output at MAX_DRAFTED_SCENARIOS_PER_CATEGORY", () => {
|
|
214
|
-
const groups = makeGroups({
|
|
215
|
-
users: { basePath: "/api/users", methods: ["POST"], paramPath: "/api/users/{id}" },
|
|
216
|
-
products: { basePath: "/api/products", methods: ["POST"], paramPath: "/api/products/{id}" },
|
|
217
|
-
orders: { basePath: "/api/orders", methods: ["POST"], paramPath: "/api/orders/{id}" },
|
|
218
|
-
tags: { basePath: "/api/tags", methods: ["POST"], paramPath: "/api/tags/{id}" },
|
|
219
|
-
reviews: { basePath: "/api/reviews", methods: ["POST"], paramPath: "/api/reviews/{id}" },
|
|
220
|
-
});
|
|
221
|
-
expect(draftPermissionBoundaryScenarios(groups).length).toBeLessThanOrEqual(4);
|
|
222
|
-
});
|
|
223
74
|
});
|
|
224
75
|
describe("inferResourceRelationships", () => {
|
|
225
76
|
it("returns empty map for endpoints with no nesting or FK fields", () => {
|
|
@@ -263,49 +114,6 @@ describe("inferResourceRelationships", () => {
|
|
|
263
114
|
expect(rel.get("orders")?.size ?? 0).toBe(0);
|
|
264
115
|
});
|
|
265
116
|
});
|
|
266
|
-
describe("draftCascadeDeleteScenarios — with relationships parameter", () => {
|
|
267
|
-
function makeGroups(resources) {
|
|
268
|
-
const map = new Map();
|
|
269
|
-
for (const [name, info] of Object.entries(resources)) {
|
|
270
|
-
map.set(name, { basePath: info.basePath, methods: new Set(info.methods), paramPath: info.paramPath });
|
|
271
|
-
}
|
|
272
|
-
return map;
|
|
273
|
-
}
|
|
274
|
-
it("skips pairs with no detected relationship when a non-empty relationships map is provided", () => {
|
|
275
|
-
const groups = makeGroups({
|
|
276
|
-
users: { basePath: "/api/users", methods: ["POST", "DELETE"], paramPath: "/api/users/{id}" },
|
|
277
|
-
products: { basePath: "/api/products", methods: ["POST", "DELETE"], paramPath: "/api/products/{id}" },
|
|
278
|
-
});
|
|
279
|
-
// Non-empty map that knows about orders→users but says nothing about products↔users
|
|
280
|
-
const rel = new Map();
|
|
281
|
-
rel.set("orders", new Set(["users"])); // unrelated to our test pair
|
|
282
|
-
expect(draftCascadeDeleteScenarios(groups, rel)).toHaveLength(0);
|
|
283
|
-
});
|
|
284
|
-
it("uses heuristic pairing (first=parent) when relationships map is empty", () => {
|
|
285
|
-
const groups = makeGroups({
|
|
286
|
-
users: { basePath: "/api/users", methods: ["POST", "DELETE"], paramPath: "/api/users/{id}" },
|
|
287
|
-
products: { basePath: "/api/products", methods: ["POST", "DELETE"], paramPath: "/api/products/{id}" },
|
|
288
|
-
});
|
|
289
|
-
const rel = new Map();
|
|
290
|
-
// Empty map → falls back to heuristic: generates scenarios for all pairs
|
|
291
|
-
expect(draftCascadeDeleteScenarios(groups, rel).length).toBeGreaterThan(0);
|
|
292
|
-
});
|
|
293
|
-
it("uses relationship direction to set correct parent→child order", () => {
|
|
294
|
-
const groups = makeGroups({
|
|
295
|
-
users: { basePath: "/api/users", methods: ["POST", "DELETE"], paramPath: "/api/users/{id}" },
|
|
296
|
-
orders: { basePath: "/api/orders", methods: ["POST", "DELETE"], paramPath: "/api/orders/{id}" },
|
|
297
|
-
});
|
|
298
|
-
// orders depends on users (orders is the child)
|
|
299
|
-
const rel = new Map();
|
|
300
|
-
rel.set("orders", new Set(["users"]));
|
|
301
|
-
const scenarios = draftCascadeDeleteScenarios(groups, rel);
|
|
302
|
-
expect(scenarios.length).toBeGreaterThan(0);
|
|
303
|
-
// Parent should be users, child should be orders
|
|
304
|
-
expect(scenarios[0].scenarioName).toContain("users");
|
|
305
|
-
expect(scenarios[0].scenarioName).toContain("orders");
|
|
306
|
-
expect(scenarios[0].steps[0].path).toBe("/api/users"); // POST parent first
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
117
|
describe("draftDiffDirectScenarios", () => {
|
|
310
118
|
const makeGroups = (defs) => new Map(Object.entries(defs).map(([k, v]) => [k, { ...v, methods: new Set(v.methods) }]));
|
|
311
119
|
it("returns empty array when newEndpoints is empty", () => {
|
|
@@ -317,21 +125,18 @@ describe("draftDiffDirectScenarios", () => {
|
|
|
317
125
|
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
|
|
318
126
|
});
|
|
319
127
|
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
128
|
+
// PUT generates: mutation-recalc, contract, not-found (3 scenarios)
|
|
129
|
+
// Note: we intentionally skip the redundant "happy-path" integration for PUT/PATCH
|
|
130
|
+
// because mutation-recalc already provides comprehensive happy-path coverage
|
|
320
131
|
expect(scenarios.length).toBeGreaterThanOrEqual(3);
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
}
|
|
132
|
+
const newEndpointScenarios = scenarios.filter(s => s.category === "new_endpoint");
|
|
133
|
+
expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(2);
|
|
324
134
|
// Mutation-recalculation scenario is drafted first for PUT/PATCH
|
|
325
|
-
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("
|
|
135
|
+
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
|
|
326
136
|
expect(mutationRecalc).toBeDefined();
|
|
327
137
|
expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
|
|
328
|
-
expect(mutationRecalc.description).toContain("
|
|
329
|
-
//
|
|
330
|
-
const integration = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("happy-path"));
|
|
331
|
-
expect(integration).toBeDefined();
|
|
332
|
-
expect(integration.steps.some(st => st.method === "PUT")).toBe(true);
|
|
333
|
-
// Description prompts LLM to discover prerequisites from source code
|
|
334
|
-
expect(integration.description).toContain("prerequisites");
|
|
138
|
+
expect(mutationRecalc.description).toContain("Mutation test");
|
|
139
|
+
// Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
|
|
335
140
|
// Contract test
|
|
336
141
|
const contract = scenarios.find(s => s.testType === "contract" && s.steps[0].expectedStatusCode === 200);
|
|
337
142
|
expect(contract).toBeDefined();
|
|
@@ -345,21 +150,19 @@ describe("draftDiffDirectScenarios", () => {
|
|
|
345
150
|
});
|
|
346
151
|
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/orders/{order_id}" }], groups);
|
|
347
152
|
// Mutation-recalculation scenario is drafted for PATCH
|
|
348
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-
|
|
153
|
+
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
349
154
|
expect(mutationRecalc).toBeDefined();
|
|
350
155
|
expect(mutationRecalc.testType).toBe("integration");
|
|
351
156
|
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
352
157
|
expect(mutationRecalc.priority).toBe("high");
|
|
353
|
-
expect(mutationRecalc.description).toContain("
|
|
354
|
-
expect(mutationRecalc.description).toContain("derived
|
|
158
|
+
expect(mutationRecalc.description).toContain("Mutation test");
|
|
159
|
+
expect(mutationRecalc.description).toContain("derived calculations");
|
|
355
160
|
// Should have 3 steps: POST (create) → PATCH (add items) → GET (verify)
|
|
356
161
|
expect(mutationRecalc.steps).toHaveLength(3);
|
|
357
162
|
expect(mutationRecalc.steps[0].method).toBe("POST");
|
|
358
163
|
expect(mutationRecalc.steps[1].method).toBe("PATCH");
|
|
359
164
|
expect(mutationRecalc.steps[2].method).toBe("GET");
|
|
360
|
-
//
|
|
361
|
-
const happyPath = scenarios.find(s => s.scenarioName.includes("happy-path") && s.testType === "integration");
|
|
362
|
-
expect(happyPath).toBeDefined();
|
|
165
|
+
// Note: happy-path integration removed for PUT/PATCH - mutation-recalc covers this
|
|
363
166
|
});
|
|
364
167
|
it("integration scenario minimum steps: POST resource then PUT — LLM discovers prereqs from source code", () => {
|
|
365
168
|
const groups = makeGroups({
|
|
@@ -367,18 +170,13 @@ describe("draftDiffDirectScenarios", () => {
|
|
|
367
170
|
});
|
|
368
171
|
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
369
172
|
// Mutation-recalculation scenario comes first
|
|
370
|
-
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("
|
|
173
|
+
const mutationRecalc = scenarios.find(s => s.testType === "integration" && s.scenarioName.includes("mutation-verify"));
|
|
371
174
|
expect(mutationRecalc).toBeDefined();
|
|
372
175
|
expect(mutationRecalc.steps.some(st => st.method === "POST" && st.path === "/api/orders")).toBe(true);
|
|
373
176
|
expect(mutationRecalc.steps.some(st => st.method === "PUT")).toBe(true);
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
// Minimum steps: create the resource + call the new endpoint
|
|
378
|
-
expect(integration.steps.some(st => st.method === "POST" && st.path === "/api/orders")).toBe(true);
|
|
379
|
-
expect(integration.steps.some(st => st.method === "PUT")).toBe(true);
|
|
380
|
-
// No prerequisite steps pre-computed — LLM is instructed to discover them
|
|
381
|
-
expect(integration.steps[0].path).toBe("/api/orders"); // first step is the resource itself, not a prereq
|
|
177
|
+
// Note: happy-path integration removed for PUT/PATCH
|
|
178
|
+
// mutation-recalc already provides comprehensive happy-path coverage with POST → PUT → GET
|
|
179
|
+
// The minimum steps (POST + PUT) are already verified in the mutationRecalc check above
|
|
382
180
|
});
|
|
383
181
|
it("generates 3 scenarios for a new POST endpoint", () => {
|
|
384
182
|
const groups = makeGroups({
|
|
@@ -420,7 +218,7 @@ describe("draftDiffDirectScenarios", () => {
|
|
|
420
218
|
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/{order_id}" }], groups);
|
|
421
219
|
expect(scenarios.length).toBeGreaterThanOrEqual(3);
|
|
422
220
|
// Mutation-recalculation scenario is drafted
|
|
423
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-
|
|
221
|
+
const mutationRecalc = scenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
424
222
|
expect(mutationRecalc).toBeDefined();
|
|
425
223
|
expect(mutationRecalc.testType).toBe("integration");
|
|
426
224
|
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
@@ -465,23 +263,23 @@ describe("draftDiffDirectScenarios", () => {
|
|
|
465
263
|
];
|
|
466
264
|
const newEndpoints = [{ method: "PATCH", path: "/{order_id}" }];
|
|
467
265
|
const allScenarios = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
468
|
-
// The diff-direct scenarios should include orders-patch-
|
|
469
|
-
const mutationRecalc = allScenarios.find(s => s.scenarioName === "orders-patch-
|
|
266
|
+
// The diff-direct scenarios should include orders-patch-mutation-verify
|
|
267
|
+
const mutationRecalc = allScenarios.find(s => s.scenarioName === "orders-patch-mutation-verify");
|
|
470
268
|
expect(mutationRecalc).toBeDefined();
|
|
471
269
|
expect(mutationRecalc.category).toBe("new_endpoint"); // → CRITICAL priority tier
|
|
472
270
|
expect(mutationRecalc.steps).toHaveLength(3);
|
|
473
271
|
expect(mutationRecalc.steps[0].method).toBe("POST");
|
|
474
272
|
expect(mutationRecalc.steps[1].method).toBe("PATCH");
|
|
475
273
|
expect(mutationRecalc.steps[2].method).toBe("GET");
|
|
476
|
-
expect(mutationRecalc.description).toContain("derived
|
|
274
|
+
expect(mutationRecalc.description).toContain("derived calculations");
|
|
477
275
|
// Verify it outranks all non-new_endpoint scenarios (cascade-delete, unique-constraint, etc.)
|
|
478
276
|
const newEndpointScenarios = allScenarios.filter(s => s.category === "new_endpoint");
|
|
479
277
|
const otherScenarios = allScenarios.filter(s => s.category !== "new_endpoint");
|
|
480
278
|
expect(newEndpointScenarios.length).toBeGreaterThanOrEqual(3);
|
|
481
279
|
// All new_endpoint scenarios should include our mutation-recalculate
|
|
482
|
-
expect(newEndpointScenarios.some(s => s.scenarioName === "orders-patch-
|
|
280
|
+
expect(newEndpointScenarios.some(s => s.scenarioName === "orders-patch-mutation-verify")).toBe(true);
|
|
483
281
|
// Non-new_endpoint scenarios should NOT include mutation-recalculate
|
|
484
|
-
expect(otherScenarios.every(s => s.scenarioName !== "orders-patch-
|
|
282
|
+
expect(otherScenarios.every(s => s.scenarioName !== "orders-patch-mutation-verify")).toBe(true);
|
|
485
283
|
});
|
|
486
284
|
});
|
|
487
285
|
describe("diffDirectMutationRecalc — diverse app types (no hardcoded field names)", () => {
|
|
@@ -561,7 +359,7 @@ describe("diffDirectMutationRecalc — diverse app types (no hardcoded field nam
|
|
|
561
359
|
it.each(testCases)("$name — produces valid mutation-recalc scenario", ({ resource, group, method, expectedParamName }) => {
|
|
562
360
|
const groups = makeGroups({ [resource]: group });
|
|
563
361
|
const scenarios = draftDiffDirectScenarios([{ method, path: group.paramPath ?? group.basePath }], groups);
|
|
564
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-
|
|
362
|
+
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
565
363
|
expect(mutationRecalc).toBeDefined();
|
|
566
364
|
expect(mutationRecalc.category).toBe("new_endpoint");
|
|
567
365
|
expect(mutationRecalc.testType).toBe("integration");
|
|
@@ -572,17 +370,9 @@ describe("diffDirectMutationRecalc — diverse app types (no hardcoded field nam
|
|
|
572
370
|
// Step 2: PUT/PATCH with mutation
|
|
573
371
|
const mutationStep = mutationRecalc.steps[1];
|
|
574
372
|
expect(mutationStep.method).toBe(method);
|
|
575
|
-
|
|
576
|
-
expect(mutationStep.bodyMustInclude.length).toBe(3);
|
|
373
|
+
// Note: bodyMustInclude removed in intent-based refactor - LLM discovers fields from source
|
|
577
374
|
// Verify NO hardcoded domain-specific field names
|
|
578
|
-
for (const hint of mutationStep.bodyMustInclude) {
|
|
579
|
-
expect(hint).not.toBe("items");
|
|
580
|
-
expect(hint).not.toBe("product_id");
|
|
581
|
-
expect(hint).not.toBe("quantity");
|
|
582
|
-
expect(hint).not.toBe("total_amount");
|
|
583
|
-
}
|
|
584
375
|
// Verify hints are descriptive (contain "e.g." or similar guidance)
|
|
585
|
-
expect(mutationStep.bodyMustInclude.some(h => h.includes("e.g."))).toBe(true);
|
|
586
376
|
// Chaining uses the actual path param name (only when paramPath exists)
|
|
587
377
|
if (group.paramPath) {
|
|
588
378
|
expect(mutationStep.chainsFrom).toBeDefined();
|
|
@@ -599,27 +389,21 @@ describe("diffDirectMutationRecalc — diverse app types (no hardcoded field nam
|
|
|
599
389
|
expect(mutationRecalc.steps).toHaveLength(3);
|
|
600
390
|
const getStep = mutationRecalc.steps[2];
|
|
601
391
|
expect(getStep.method).toBe("GET");
|
|
602
|
-
|
|
603
|
-
// Verify NO hardcoded domain-specific field names in response fields
|
|
604
|
-
for (const field of getStep.expectedResponseFields) {
|
|
605
|
-
expect(field).not.toBe("items");
|
|
606
|
-
expect(field).not.toMatch(/^items\.\*/);
|
|
607
|
-
expect(field).not.toBe("total_amount");
|
|
608
|
-
}
|
|
392
|
+
// Note: expectedResponseFields removed in intent-based refactor
|
|
609
393
|
}
|
|
610
394
|
// chainingKeys uses the actual path param name, not hardcoded singular_id
|
|
611
395
|
expect(mutationRecalc.chainingKeys).toContain("id");
|
|
612
396
|
expect(mutationRecalc.chainingKeys).toContain(expectedParamName);
|
|
613
397
|
// Description is domain-agnostic
|
|
614
|
-
expect(mutationRecalc.description).toContain("
|
|
615
|
-
expect(mutationRecalc.description).toContain("
|
|
398
|
+
expect(mutationRecalc.description).toContain("Mutation test");
|
|
399
|
+
expect(mutationRecalc.description).toContain("derived calculations");
|
|
616
400
|
});
|
|
617
401
|
it("no paramPath: PATCH step targets basePath, no GET verification, fallback param name", () => {
|
|
618
402
|
const groups = makeGroups({
|
|
619
403
|
items: { basePath: "/items", methods: ["GET", "POST", "PATCH"] },
|
|
620
404
|
});
|
|
621
405
|
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/items" }], groups);
|
|
622
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-
|
|
406
|
+
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
623
407
|
expect(mutationRecalc).toBeDefined();
|
|
624
408
|
// No paramPath → PATCH targets basePath, no GET step
|
|
625
409
|
expect(mutationRecalc.steps).toHaveLength(2);
|
|
@@ -632,7 +416,7 @@ describe("diffDirectMutationRecalc — diverse app types (no hardcoded field nam
|
|
|
632
416
|
configs: { basePath: "/api/configs", methods: ["GET", "PATCH"], paramPath: "/api/configs/{config_id}" },
|
|
633
417
|
});
|
|
634
418
|
const scenarios = draftDiffDirectScenarios([{ method: "PATCH", path: "/api/configs/{config_id}" }], groups);
|
|
635
|
-
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-
|
|
419
|
+
const mutationRecalc = scenarios.find(s => s.scenarioName.endsWith("-mutation-verify"));
|
|
636
420
|
expect(mutationRecalc).toBeDefined();
|
|
637
421
|
// No POST → starts with PATCH directly, no chaining
|
|
638
422
|
expect(mutationRecalc.steps[0].method).toBe("PATCH");
|
|
@@ -641,4 +425,155 @@ describe("diffDirectMutationRecalc — diverse app types (no hardcoded field nam
|
|
|
641
425
|
expect(mutationRecalc.steps[1].method).toBe("GET");
|
|
642
426
|
expect(mutationRecalc.steps[1].chainsFrom).toBeUndefined();
|
|
643
427
|
});
|
|
428
|
+
it("generates auth-boundary scenario for each new endpoint (security_boundary, not new_endpoint)", () => {
|
|
429
|
+
const groups = makeGroups({
|
|
430
|
+
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT"], paramPath: "/api/orders/{order_id}" },
|
|
431
|
+
});
|
|
432
|
+
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }], groups);
|
|
433
|
+
const authBoundary = scenarios.find(s => s.scenarioName === "orders-put-auth-boundary");
|
|
434
|
+
expect(authBoundary).toBeDefined();
|
|
435
|
+
expect(authBoundary.category).toBe("security_boundary");
|
|
436
|
+
expect(authBoundary.testType).toBe("contract");
|
|
437
|
+
expect(authBoundary.steps[0].method).toBe("PUT");
|
|
438
|
+
expect(authBoundary.steps[0].path).toBe("/api/orders/{order_id}");
|
|
439
|
+
expect(authBoundary.steps[0].expectedStatusCode).toBe(401);
|
|
440
|
+
expect(authBoundary.requiresAuth).toBe(false);
|
|
441
|
+
});
|
|
442
|
+
it("auth-boundary scenario is not duplicated for same method/path", () => {
|
|
443
|
+
const groups = makeGroups({
|
|
444
|
+
orders: { basePath: "/api/orders", methods: ["GET", "POST", "PUT", "DELETE"], paramPath: "/api/orders/{order_id}" },
|
|
445
|
+
});
|
|
446
|
+
const scenarios = draftDiffDirectScenarios([{ method: "PUT", path: "/api/orders/{order_id}" }, { method: "DELETE", path: "/api/orders/{order_id}" }], groups);
|
|
447
|
+
const authBoundaries = scenarios.filter(s => s.scenarioName.includes("auth-boundary"));
|
|
448
|
+
// One for PUT, one for DELETE (not GET, not POST)
|
|
449
|
+
expect(authBoundaries).toHaveLength(2);
|
|
450
|
+
expect(authBoundaries.map(s => s.scenarioName).sort()).toEqual([
|
|
451
|
+
"orders-delete-auth-boundary",
|
|
452
|
+
"orders-put-auth-boundary",
|
|
453
|
+
]);
|
|
454
|
+
});
|
|
455
|
+
it("does NOT generate auth-boundary for GET endpoints (often public)", () => {
|
|
456
|
+
const groups = makeGroups({
|
|
457
|
+
orders: { basePath: "/api/orders", methods: ["GET", "POST"], paramPath: "/api/orders/{order_id}" },
|
|
458
|
+
});
|
|
459
|
+
const scenarios = draftDiffDirectScenarios([{ method: "GET", path: "/api/orders/{order_id}" }], groups);
|
|
460
|
+
const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
|
|
461
|
+
expect(authBoundary).toBeUndefined();
|
|
462
|
+
});
|
|
463
|
+
it("does NOT generate auth-boundary for POST", () => {
|
|
464
|
+
const groups = makeGroups({
|
|
465
|
+
products: { basePath: "/api/products", methods: ["GET", "POST"], paramPath: "/api/products/{id}" },
|
|
466
|
+
});
|
|
467
|
+
const scenarios = draftDiffDirectScenarios([{ method: "POST", path: "/api/products" }], groups);
|
|
468
|
+
const authBoundary = scenarios.find(s => s.scenarioName.includes("auth-boundary"));
|
|
469
|
+
expect(authBoundary).toBeUndefined();
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
describe("draftDiffDirectScenarios — GET without path param", () => {
|
|
473
|
+
const resourceGroups = new Map([
|
|
474
|
+
["products", {
|
|
475
|
+
basePath: "/api/v1/products",
|
|
476
|
+
methods: new Set(["GET", "POST"]),
|
|
477
|
+
paramPath: "/api/v1/products/{product_id}",
|
|
478
|
+
}],
|
|
479
|
+
]);
|
|
480
|
+
it("GET no-param: produces contract + collection scenario (LLM determines if query params apply)", () => {
|
|
481
|
+
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
|
|
482
|
+
const names = result.map(s => s.scenarioName);
|
|
483
|
+
expect(names).toContain("products-get-new-endpoint-contract");
|
|
484
|
+
expect(names).toContain("products-search-list-query");
|
|
485
|
+
const scenario = result.find(s => s.scenarioName === "products-search-list-query");
|
|
486
|
+
expect(scenario.category).toBe("new_endpoint");
|
|
487
|
+
// Description instructs LLM to read source for query param logic
|
|
488
|
+
expect(scenario.description).toMatch(/read diff source|query param/i);
|
|
489
|
+
});
|
|
490
|
+
it("GET no-param: produces zero cascade-delete or unique-constraint scenarios", () => {
|
|
491
|
+
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products/search" }], resourceGroups);
|
|
492
|
+
const names = result.map(s => s.scenarioName);
|
|
493
|
+
expect(names.some(n => n.includes("cascade"))).toBe(false);
|
|
494
|
+
expect(names.some(n => n.includes("unique"))).toBe(false);
|
|
495
|
+
expect(names.some(n => n.includes("delete-blocked"))).toBe(false);
|
|
496
|
+
});
|
|
497
|
+
it("collection GET with POST: produces integration scenario (POST then GET)", () => {
|
|
498
|
+
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/products" }], resourceGroups);
|
|
499
|
+
const scenario = result.find(s => s.scenarioName === "products-list-query");
|
|
500
|
+
expect(scenario).toBeDefined();
|
|
501
|
+
expect(scenario.testType).toBe("integration");
|
|
502
|
+
expect(scenario.steps[0].method).toBe("POST");
|
|
503
|
+
expect(scenario.steps[1].method).toBe("GET");
|
|
504
|
+
});
|
|
505
|
+
it("collection GET without POST: produces contract scenario (single GET step)", () => {
|
|
506
|
+
const readOnlyGroups = new Map([
|
|
507
|
+
["reports", {
|
|
508
|
+
basePath: "/api/v1/reports",
|
|
509
|
+
methods: new Set(["GET"]),
|
|
510
|
+
paramPath: undefined,
|
|
511
|
+
}],
|
|
512
|
+
]);
|
|
513
|
+
const result = draftDiffDirectScenarios([{ method: "GET", path: "/api/v1/reports" }], readOnlyGroups);
|
|
514
|
+
const scenario = result.find(s => s.scenarioName === "reports-list-query");
|
|
515
|
+
expect(scenario).toBeDefined();
|
|
516
|
+
expect(scenario.testType).toBe("contract");
|
|
517
|
+
expect(scenario.steps).toHaveLength(1);
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
// ---------------------------------------------------------------------------
|
|
521
|
+
// capScenarios — enforced via draftScenariosFromEndpoints
|
|
522
|
+
// ---------------------------------------------------------------------------
|
|
523
|
+
describe("draftScenariosFromEndpoints — capScenarios hard limit", () => {
|
|
524
|
+
// Each PUT/PATCH new endpoint produces ~5 scenarios (mutation-verify, boundary-values,
|
|
525
|
+
// contract, not-found, auth-boundary). Seven resources × 2 methods each = 70 new
|
|
526
|
+
// endpoints → well above the 30-scenario cap.
|
|
527
|
+
function makeEndpointsAndNewEps(resourceNames) {
|
|
528
|
+
const endpoints = resourceNames.flatMap(r => [
|
|
529
|
+
{ path: `/api/${r}`, methods: [{ method: "GET" }, { method: "POST" }] },
|
|
530
|
+
{ path: `/api/${r}/{id}`, methods: [{ method: "GET" }, { method: "PUT" }, { method: "PATCH" }, { method: "DELETE" }] },
|
|
531
|
+
]);
|
|
532
|
+
const newEndpoints = resourceNames.flatMap(r => [
|
|
533
|
+
{ method: "PUT", path: `/api/${r}/{id}` },
|
|
534
|
+
{ method: "PATCH", path: `/api/${r}/{id}` },
|
|
535
|
+
]);
|
|
536
|
+
return { endpoints, newEndpoints };
|
|
537
|
+
}
|
|
538
|
+
it("caps output at 30 scenarios when many new endpoints are present", () => {
|
|
539
|
+
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
540
|
+
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
541
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
542
|
+
expect(result.length).toBeLessThanOrEqual(30);
|
|
543
|
+
});
|
|
544
|
+
it("always keeps CRITICAL (new_endpoint) scenarios under the cap", () => {
|
|
545
|
+
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
546
|
+
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
547
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
548
|
+
// new_endpoint is CRITICAL — verify at least some survive
|
|
549
|
+
const criticalOnes = result.filter((s) => s.category === "new_endpoint");
|
|
550
|
+
expect(criticalOnes.length).toBeGreaterThan(0);
|
|
551
|
+
});
|
|
552
|
+
it("CRITICAL items (new_endpoint) fill the cap first; security_boundary may be dropped", () => {
|
|
553
|
+
// With many new endpoints, diffDirectContract/NotFound/MutationVerify/BoundaryValues all
|
|
554
|
+
// produce category "new_endpoint" (CRITICAL). When CRITICAL items alone exceed 30, the hard
|
|
555
|
+
// cap slices them first — breadth picks for non-CRITICAL categories are not guaranteed.
|
|
556
|
+
const resources = ["users", "products", "orders", "invoices", "shipments", "reviews", "payments", "discounts"];
|
|
557
|
+
const { endpoints, newEndpoints } = makeEndpointsAndNewEps(resources);
|
|
558
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
559
|
+
// All 30 surviving scenarios should be CRITICAL (new_endpoint)
|
|
560
|
+
const nonCritical = result.filter((s) => s.category !== "new_endpoint");
|
|
561
|
+
// Non-critical (security_boundary) may be 0 — this is the documented cap behavior
|
|
562
|
+
expect(nonCritical.length).toBeLessThanOrEqual(result.length);
|
|
563
|
+
// And the cap is enforced
|
|
564
|
+
expect(result.length).toBeLessThanOrEqual(30);
|
|
565
|
+
});
|
|
566
|
+
it("does not cap when total scenarios are within the limit", () => {
|
|
567
|
+
const endpoints = [
|
|
568
|
+
{ path: "/api/users", methods: [{ method: "GET" }, { method: "POST" }] },
|
|
569
|
+
{ path: "/api/users/{id}", methods: [{ method: "GET" }, { method: "PUT" }, { method: "DELETE" }] },
|
|
570
|
+
];
|
|
571
|
+
const newEndpoints = [
|
|
572
|
+
{ method: "PUT", path: "/api/users/{id}" },
|
|
573
|
+
];
|
|
574
|
+
const result = draftScenariosFromEndpoints(endpoints, newEndpoints);
|
|
575
|
+
// A single PUT endpoint produces 5 scenarios — well within the 30-scenario cap
|
|
576
|
+
expect(result.length).toBeLessThanOrEqual(30);
|
|
577
|
+
expect(result.length).toBeGreaterThanOrEqual(4);
|
|
578
|
+
});
|
|
644
579
|
});
|