@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,3 +1,6 @@
|
|
|
1
|
+
import { ScenarioSource } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
import { TestType } from "../types/TestTypes.js";
|
|
3
|
+
import { CATEGORY_PRIORITY } from "../types/TestRecommendation.js";
|
|
1
4
|
/**
|
|
2
5
|
* Cap on drafted scenarios per category to prevent prompt bloat on large APIs.
|
|
3
6
|
* Value of 4 balances coverage breadth (enough to surface patterns across
|
|
@@ -148,318 +151,44 @@ export function draftScenariosFromEndpoints(endpoints, newEndpoints = []) {
|
|
|
148
151
|
});
|
|
149
152
|
}
|
|
150
153
|
}
|
|
151
|
-
// ── Cross-resource integration scenarios ──
|
|
152
|
-
const relationships = inferResourceRelationships(endpoints);
|
|
153
|
-
const hasRelationships = relationships.size > 0;
|
|
154
|
-
const creatableResources = Array.from(resourceGroups.keys()).filter(r => isRealResource(r) && resourceGroups.get(r).methods.has("POST"));
|
|
155
|
-
const MAX_CROSS_RESOURCE = Math.min(Math.max(3, Math.floor(creatableResources.length / 2)), 8);
|
|
156
|
-
let crossCount = 0;
|
|
157
|
-
for (let i = 0; i < creatableResources.length && crossCount < MAX_CROSS_RESOURCE; i++) {
|
|
158
|
-
for (let j = i + 1; j < creatableResources.length && crossCount < MAX_CROSS_RESOURCE; j++) {
|
|
159
|
-
const r1 = creatableResources[i];
|
|
160
|
-
const r2 = creatableResources[j];
|
|
161
|
-
if (hasRelationships) {
|
|
162
|
-
// Only pair resources with a detected relationship; skip unrelated ones.
|
|
163
|
-
const r2DependsOnR1 = relationships.get(r2)?.has(r1) || relationships.get(r2)?.has(singularize(r1));
|
|
164
|
-
const r1DependsOnR2 = relationships.get(r1)?.has(r2) || relationships.get(r1)?.has(singularize(r2));
|
|
165
|
-
if (!r2DependsOnR1 && !r1DependsOnR2)
|
|
166
|
-
continue;
|
|
167
|
-
// Use relationship direction: parent is the one that doesn't depend on the other.
|
|
168
|
-
const [parent, child] = r2DependsOnR1 ? [r1, r2] : [r2, r1];
|
|
169
|
-
const gParent = resourceGroups.get(parent);
|
|
170
|
-
const gChild = resourceGroups.get(child);
|
|
171
|
-
const singularParent = singularize(parent);
|
|
172
|
-
const singularChild = singularize(child);
|
|
173
|
-
scenarios.push(buildCrossResourceScenario(parent, child, gParent, gChild, singularParent, singularChild));
|
|
174
|
-
}
|
|
175
|
-
else {
|
|
176
|
-
// No relationship signals found — fall back to sequential pairing (existing behaviour).
|
|
177
|
-
const g1 = resourceGroups.get(r1);
|
|
178
|
-
const g2 = resourceGroups.get(r2);
|
|
179
|
-
scenarios.push(buildCrossResourceScenario(r1, r2, g1, g2, singularize(r1), singularize(r2)));
|
|
180
|
-
}
|
|
181
|
-
crossCount++;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
// ── CRUD lifecycle scenarios ──
|
|
185
|
-
for (const [resource, group] of resourceGroups) {
|
|
186
|
-
const m = group.methods;
|
|
187
|
-
if (!m.has("POST") || !m.has("GET") || !isRealResource(resource))
|
|
188
|
-
continue;
|
|
189
|
-
const steps = [];
|
|
190
|
-
let order = 1;
|
|
191
|
-
const singular = singularize(resource);
|
|
192
|
-
steps.push({ order: order++, method: "POST", path: group.basePath, description: `Create a new ${singular}`, interactionType: "success", expectedStatusCode: 201 });
|
|
193
|
-
if (group.paramPath)
|
|
194
|
-
steps.push({ order: order++, method: "GET", path: group.paramPath, description: `Get the created ${singular} by ID`, interactionType: "success", expectedStatusCode: 200 });
|
|
195
|
-
if ((m.has("PUT") || m.has("PATCH")) && group.paramPath)
|
|
196
|
-
steps.push({ order: order++, method: m.has("PUT") ? "PUT" : "PATCH", path: group.paramPath, description: `Update the ${singular}`, interactionType: "success", expectedStatusCode: 200 });
|
|
197
|
-
if (m.has("DELETE") && group.paramPath)
|
|
198
|
-
steps.push({ order: order++, method: "DELETE", path: group.paramPath, description: `Delete the ${singular}`, interactionType: "success", expectedStatusCode: 204 });
|
|
199
|
-
scenarios.push({
|
|
200
|
-
scenarioName: `${resource}-crud-lifecycle`,
|
|
201
|
-
description: `CRUD lifecycle for ${resource}: create, read, update, delete`,
|
|
202
|
-
category: "crud",
|
|
203
|
-
priority: "medium",
|
|
204
|
-
steps,
|
|
205
|
-
chainingKeys: ["id", `${singular}_id`],
|
|
206
|
-
requiresAuth: true,
|
|
207
|
-
estimatedComplexity: "moderate",
|
|
208
|
-
source: "code-inferred",
|
|
209
|
-
testType: "integration",
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
scenarios.push(...draftUniqueConstraintScenarios(resourceGroups));
|
|
213
|
-
scenarios.push(...draftCascadeDeleteScenarios(resourceGroups, relationships));
|
|
214
|
-
scenarios.push(...draftPermissionBoundaryScenarios(resourceGroups));
|
|
215
154
|
scenarios.push(...draftDiffDirectScenarios(newEndpoints, resourceGroups));
|
|
216
|
-
return scenarios;
|
|
155
|
+
return capScenarios(scenarios);
|
|
217
156
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
if (gChild.paramPath && gChild.methods.has("DELETE")) {
|
|
240
|
-
steps.push({
|
|
241
|
-
order: order++, method: "DELETE", path: gChild.paramPath,
|
|
242
|
-
description: `Clean up: delete the ${singularChild}`,
|
|
243
|
-
interactionType: "success", expectedStatusCode: 204,
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
return {
|
|
247
|
-
scenarioName: `${parent}-${child}-integration`,
|
|
248
|
-
description: `Multi-resource integration: create ${singularParent}, create ${singularChild} linked to it, verify, clean up`,
|
|
249
|
-
category: "workflow",
|
|
250
|
-
priority: "high",
|
|
251
|
-
steps,
|
|
252
|
-
chainingKeys: [`${singularParent}_id`, `${singularChild}_id`, "id"],
|
|
253
|
-
requiresAuth: true,
|
|
254
|
-
estimatedComplexity: "complex",
|
|
255
|
-
source: "code-inferred",
|
|
256
|
-
testType: "integration",
|
|
257
|
-
};
|
|
258
|
-
}
|
|
259
|
-
export function draftUniqueConstraintScenarios(resourceGroups) {
|
|
260
|
-
const scenarios = [];
|
|
261
|
-
for (const [resource, group] of resourceGroups) {
|
|
262
|
-
if (scenarios.length >= MAX_DRAFTED_SCENARIOS_PER_CATEGORY)
|
|
263
|
-
break;
|
|
264
|
-
if (!group.methods.has("POST") || !isRealResource(resource))
|
|
265
|
-
continue;
|
|
266
|
-
const singular = singularize(resource);
|
|
267
|
-
scenarios.push({
|
|
268
|
-
scenarioName: `${resource}-unique-constraint`,
|
|
269
|
-
description: `Verify unique constraint: create ${singular}, attempt duplicate creation, expect 409 Conflict`,
|
|
270
|
-
category: "business_rule",
|
|
271
|
-
priority: "high",
|
|
272
|
-
steps: [
|
|
273
|
-
{
|
|
274
|
-
order: 1, method: "POST", path: group.basePath,
|
|
275
|
-
description: `Create a ${singular} with unique fields`,
|
|
276
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
order: 2, method: "POST", path: group.basePath,
|
|
280
|
-
description: `Attempt to create a duplicate ${singular} with the same unique fields — expect 409 Conflict`,
|
|
281
|
-
interactionType: "error", expectedStatusCode: 409,
|
|
282
|
-
},
|
|
283
|
-
...(group.paramPath && group.methods.has("GET") ? [{
|
|
284
|
-
order: 3, method: "GET", path: group.paramPath,
|
|
285
|
-
description: `Verify the original ${singular} is unchanged after duplicate attempt`,
|
|
286
|
-
interactionType: "success", expectedStatusCode: 200,
|
|
287
|
-
}] : []),
|
|
288
|
-
],
|
|
289
|
-
chainingKeys: ["id", `${singular}_id`],
|
|
290
|
-
requiresAuth: true,
|
|
291
|
-
estimatedComplexity: "moderate",
|
|
292
|
-
source: "code-inferred",
|
|
293
|
-
testType: "integration",
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
return scenarios;
|
|
297
|
-
}
|
|
298
|
-
export function draftCascadeDeleteScenarios(resourceGroups, relationships = new Map()) {
|
|
299
|
-
const scenarios = [];
|
|
300
|
-
const hasRelationships = relationships.size > 0;
|
|
301
|
-
const deletableResources = Array.from(resourceGroups.keys()).filter(r => isRealResource(r) && resourceGroups.get(r).methods.has("POST") && resourceGroups.get(r).methods.has("DELETE"));
|
|
302
|
-
for (let i = 0; i < deletableResources.length; i++) {
|
|
303
|
-
for (let j = i + 1; j < deletableResources.length; j++) {
|
|
304
|
-
if (scenarios.length >= MAX_DRAFTED_SCENARIOS_PER_CATEGORY)
|
|
305
|
-
break;
|
|
306
|
-
const r1 = deletableResources[i];
|
|
307
|
-
const r2 = deletableResources[j];
|
|
308
|
-
// Determine relationship direction: which is parent, which is child?
|
|
309
|
-
let parent, child;
|
|
310
|
-
if (hasRelationships) {
|
|
311
|
-
const r2DependsOnR1 = relationships.get(r2)?.has(r1) || relationships.get(r2)?.has(singularize(r1));
|
|
312
|
-
const r1DependsOnR2 = relationships.get(r1)?.has(r2) || relationships.get(r1)?.has(singularize(r2));
|
|
313
|
-
if (!r2DependsOnR1 && !r1DependsOnR2)
|
|
314
|
-
continue; // no detected relationship — skip
|
|
315
|
-
[parent, child] = r2DependsOnR1 ? [r1, r2] : [r2, r1];
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
// Fallback: treat first as parent (existing behaviour)
|
|
319
|
-
[parent, child] = [r1, r2];
|
|
320
|
-
}
|
|
321
|
-
const gParent = resourceGroups.get(parent);
|
|
322
|
-
const gChild = resourceGroups.get(child);
|
|
323
|
-
if (!gParent.paramPath || !gChild.paramPath)
|
|
324
|
-
continue;
|
|
325
|
-
const singularParent = singularize(parent);
|
|
326
|
-
const singularChild = singularize(child);
|
|
327
|
-
// Scenario A: cascade delete — parent delete removes child
|
|
328
|
-
scenarios.push({
|
|
329
|
-
scenarioName: `${parent}-${child}-cascade-delete`,
|
|
330
|
-
description: `Verify cascade delete: create ${singularParent}, create ${singularChild} referencing it, delete ${singularParent}, verify ${singularChild} is also deleted`,
|
|
331
|
-
category: "data_integrity",
|
|
332
|
-
priority: "high",
|
|
333
|
-
steps: [
|
|
334
|
-
{
|
|
335
|
-
order: 1, method: "POST", path: gParent.basePath,
|
|
336
|
-
description: `Create a ${singularParent}`,
|
|
337
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
338
|
-
},
|
|
339
|
-
{
|
|
340
|
-
order: 2, method: "POST", path: gChild.basePath,
|
|
341
|
-
description: `Create a ${singularChild} referencing the ${singularParent}`,
|
|
342
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
343
|
-
chainsFrom: { sourceStep: 1, sourceField: `${singularParent}_id`, sourceLocation: "body", targetParam: `${singularParent}_id`, targetLocation: "body" },
|
|
344
|
-
},
|
|
345
|
-
{
|
|
346
|
-
order: 3, method: "DELETE", path: gParent.paramPath,
|
|
347
|
-
description: `Delete the ${singularParent} — expect 204 if cascade is configured`,
|
|
348
|
-
interactionType: "success", expectedStatusCode: 204,
|
|
349
|
-
},
|
|
350
|
-
{
|
|
351
|
-
order: 4, method: "GET", path: gChild.paramPath,
|
|
352
|
-
description: `Verify ${singularChild} was cascade-deleted — expect 404`,
|
|
353
|
-
interactionType: "error", expectedStatusCode: 404,
|
|
354
|
-
},
|
|
355
|
-
],
|
|
356
|
-
chainingKeys: [`${singularParent}_id`, `${singularChild}_id`, "id"],
|
|
357
|
-
requiresAuth: true,
|
|
358
|
-
estimatedComplexity: "complex",
|
|
359
|
-
source: "code-inferred",
|
|
360
|
-
testType: "integration",
|
|
361
|
-
});
|
|
362
|
-
// Scenario B: delete blocked by referential integrity — parent cannot be removed while children exist
|
|
363
|
-
if (scenarios.length < MAX_DRAFTED_SCENARIOS_PER_CATEGORY) {
|
|
364
|
-
scenarios.push({
|
|
365
|
-
scenarioName: `${parent}-${child}-delete-blocked`,
|
|
366
|
-
description: `Verify referential integrity: create ${singularParent} with ${singularChild}, attempt to delete ${singularParent}, expect rejection (409/422), verify ${singularChild} is intact`,
|
|
367
|
-
category: "data_integrity",
|
|
368
|
-
priority: "high",
|
|
369
|
-
steps: [
|
|
370
|
-
{
|
|
371
|
-
order: 1, method: "POST", path: gParent.basePath,
|
|
372
|
-
description: `Create a ${singularParent}`,
|
|
373
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
374
|
-
},
|
|
375
|
-
{
|
|
376
|
-
order: 2, method: "POST", path: gChild.basePath,
|
|
377
|
-
description: `Create a ${singularChild} referencing the ${singularParent}`,
|
|
378
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
379
|
-
chainsFrom: { sourceStep: 1, sourceField: `${singularParent}_id`, sourceLocation: "body", targetParam: `${singularParent}_id`, targetLocation: "body" },
|
|
380
|
-
},
|
|
381
|
-
{
|
|
382
|
-
order: 3, method: "DELETE", path: gParent.paramPath,
|
|
383
|
-
description: `Attempt to delete the ${singularParent} — expect 409 Conflict or 422 if referential integrity blocks it`,
|
|
384
|
-
interactionType: "error", expectedStatusCode: 409,
|
|
385
|
-
},
|
|
386
|
-
{
|
|
387
|
-
order: 4, method: "GET", path: gChild.paramPath,
|
|
388
|
-
description: `Verify ${singularChild} is still intact after blocked delete — expect 200`,
|
|
389
|
-
interactionType: "success", expectedStatusCode: 200,
|
|
390
|
-
},
|
|
391
|
-
],
|
|
392
|
-
chainingKeys: [`${singularParent}_id`, `${singularChild}_id`, "id"],
|
|
393
|
-
requiresAuth: true,
|
|
394
|
-
estimatedComplexity: "complex",
|
|
395
|
-
source: "code-inferred",
|
|
396
|
-
testType: "integration",
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (scenarios.length >= MAX_DRAFTED_SCENARIOS_PER_CATEGORY)
|
|
401
|
-
break;
|
|
402
|
-
}
|
|
403
|
-
return scenarios;
|
|
404
|
-
}
|
|
405
|
-
export function draftPermissionBoundaryScenarios(resourceGroups) {
|
|
406
|
-
const scenarios = [];
|
|
407
|
-
for (const [resource, group] of resourceGroups) {
|
|
408
|
-
if (scenarios.length >= MAX_DRAFTED_SCENARIOS_PER_CATEGORY)
|
|
409
|
-
break;
|
|
410
|
-
if (!isRealResource(resource))
|
|
411
|
-
continue;
|
|
412
|
-
const singular = singularize(resource);
|
|
413
|
-
const protectedMethods = ["POST", "PUT", "PATCH", "DELETE"].filter(m => group.methods.has(m));
|
|
414
|
-
if (protectedMethods.length === 0)
|
|
415
|
-
continue;
|
|
416
|
-
const targetMethod = protectedMethods[0];
|
|
417
|
-
const targetPath = (targetMethod === "POST") ? group.basePath : (group.paramPath || group.basePath);
|
|
418
|
-
scenarios.push({
|
|
419
|
-
scenarioName: `${resource}-auth-boundary`,
|
|
420
|
-
description: `Verify auth boundary: call ${targetMethod} ${resource} without authentication, expect 401 Unauthorized`,
|
|
421
|
-
category: "security_boundary",
|
|
422
|
-
priority: "high",
|
|
423
|
-
steps: [
|
|
424
|
-
{
|
|
425
|
-
order: 1, method: targetMethod, path: targetPath,
|
|
426
|
-
description: `Call ${targetMethod} ${targetPath} without authentication — expect 401 Unauthorized`,
|
|
427
|
-
interactionType: "error", expectedStatusCode: 401,
|
|
428
|
-
},
|
|
429
|
-
],
|
|
430
|
-
chainingKeys: [],
|
|
431
|
-
requiresAuth: false,
|
|
432
|
-
estimatedComplexity: "simple",
|
|
433
|
-
source: "code-inferred",
|
|
434
|
-
testType: "contract",
|
|
435
|
-
});
|
|
436
|
-
if (group.paramPath && group.methods.has("GET") && group.methods.has("POST") && scenarios.length < MAX_DRAFTED_SCENARIOS_PER_CATEGORY) {
|
|
437
|
-
scenarios.push({
|
|
438
|
-
scenarioName: `${resource}-cross-user-isolation`,
|
|
439
|
-
description: `Verify cross-user isolation: user A creates ${singular}, user B tries to access it, expect 403 Forbidden`,
|
|
440
|
-
category: "security_boundary",
|
|
441
|
-
priority: "high",
|
|
442
|
-
steps: [
|
|
443
|
-
{
|
|
444
|
-
order: 1, method: "POST", path: group.basePath,
|
|
445
|
-
description: `User A creates a ${singular}`,
|
|
446
|
-
interactionType: "success", expectedStatusCode: 201,
|
|
447
|
-
},
|
|
448
|
-
{
|
|
449
|
-
order: 2, method: "GET", path: group.paramPath,
|
|
450
|
-
description: `User B attempts to access user A's ${singular} — expect 403 Forbidden`,
|
|
451
|
-
interactionType: "error", expectedStatusCode: 403,
|
|
452
|
-
},
|
|
453
|
-
],
|
|
454
|
-
chainingKeys: ["id", `${singular}_id`],
|
|
455
|
-
requiresAuth: true,
|
|
456
|
-
estimatedComplexity: "moderate",
|
|
457
|
-
source: "code-inferred",
|
|
458
|
-
testType: "integration",
|
|
459
|
-
});
|
|
157
|
+
const MAX_TOTAL_SCENARIOS = 30;
|
|
158
|
+
const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
159
|
+
/**
|
|
160
|
+
* Enforce a global cap on drafted scenarios while preserving category diversity.
|
|
161
|
+
*
|
|
162
|
+
* 1. CRITICAL (new_endpoint / diff-direct) scenarios prioritized first.
|
|
163
|
+
* 2. One scenario per non-empty category guaranteed (breadth).
|
|
164
|
+
* 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
|
|
165
|
+
* 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
|
|
166
|
+
*/
|
|
167
|
+
function capScenarios(scenarios) {
|
|
168
|
+
if (scenarios.length <= MAX_TOTAL_SCENARIOS)
|
|
169
|
+
return scenarios;
|
|
170
|
+
const critical = scenarios.filter(s => CATEGORY_PRIORITY[s.category] === "CRITICAL");
|
|
171
|
+
const rest = scenarios.filter(s => CATEGORY_PRIORITY[s.category] !== "CRITICAL");
|
|
172
|
+
const breadthPicks = [];
|
|
173
|
+
const seenCategories = new Set();
|
|
174
|
+
for (const s of rest) {
|
|
175
|
+
if (!seenCategories.has(s.category)) {
|
|
176
|
+
seenCategories.add(s.category);
|
|
177
|
+
breadthPicks.push(s);
|
|
460
178
|
}
|
|
461
179
|
}
|
|
462
|
-
|
|
180
|
+
const breadthSet = new Set(breadthPicks);
|
|
181
|
+
const remaining = rest.filter(s => !breadthSet.has(s));
|
|
182
|
+
remaining.sort((a, b) => {
|
|
183
|
+
const ta = TIER_ORDER[CATEGORY_PRIORITY[a.category] ?? "LOW"] ?? 1;
|
|
184
|
+
const tb = TIER_ORDER[CATEGORY_PRIORITY[b.category] ?? "LOW"] ?? 1;
|
|
185
|
+
return tb - ta;
|
|
186
|
+
});
|
|
187
|
+
const budget = MAX_TOTAL_SCENARIOS - critical.length - breadthPicks.length;
|
|
188
|
+
const filler = remaining.slice(0, Math.max(budget, 0));
|
|
189
|
+
const combined = [...critical, ...breadthPicks, ...filler];
|
|
190
|
+
// Enforce the hard cap even if critical set alone exceeds it
|
|
191
|
+
return combined.slice(0, MAX_TOTAL_SCENARIOS);
|
|
463
192
|
}
|
|
464
193
|
// ── Diff-direct scenario drafting ──
|
|
465
194
|
// Generates targeted scenarios for each new endpoint in the branch diff.
|
|
@@ -477,57 +206,53 @@ function diffDirectIntegration(method, resource, singular, group) {
|
|
|
477
206
|
if (method === "POST") {
|
|
478
207
|
steps.push({
|
|
479
208
|
order: 1, method: "POST", path: group.basePath,
|
|
480
|
-
description: `Create
|
|
209
|
+
description: `Create ${singular} and verify response`,
|
|
481
210
|
interactionType: "success", expectedStatusCode: 201,
|
|
482
211
|
});
|
|
483
212
|
if (group.paramPath && group.methods.has("GET")) {
|
|
484
213
|
steps.push({
|
|
485
214
|
order: 2, method: "GET", path: group.paramPath,
|
|
486
|
-
description: `
|
|
215
|
+
description: `Retrieve created ${singular} and verify fields`,
|
|
487
216
|
interactionType: "success", expectedStatusCode: 200,
|
|
488
217
|
chainsFrom: { sourceStep: 1, sourceField: "id", sourceLocation: "body", targetParam: `${singular}_id`, targetLocation: "path" },
|
|
489
218
|
});
|
|
490
219
|
}
|
|
491
220
|
}
|
|
492
|
-
else {
|
|
493
|
-
// PUT/PATCH/DELETE: create the resource first so it exists
|
|
221
|
+
else if (method === "DELETE") {
|
|
494
222
|
if (group.methods.has("POST")) {
|
|
495
223
|
steps.push({
|
|
496
224
|
order: 1, method: "POST", path: group.basePath,
|
|
497
|
-
description: `Create
|
|
225
|
+
description: `Create ${singular} to delete`,
|
|
498
226
|
interactionType: "success", expectedStatusCode: 201,
|
|
499
227
|
});
|
|
500
228
|
}
|
|
501
229
|
const targetPath = group.paramPath ?? group.basePath;
|
|
502
|
-
const sourceStep = steps.length; // 1 if create step exists, 0 otherwise
|
|
503
230
|
steps.push({
|
|
504
|
-
order: steps.length + 1, method, path: targetPath,
|
|
505
|
-
description:
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
interactionType: "success", expectedStatusCode: method === "DELETE" ? 204 : 200,
|
|
509
|
-
...(sourceStep > 0 ? { chainsFrom: { sourceStep, sourceField: "id", sourceLocation: "body", targetParam: `${singular}_id`, targetLocation: "path" } } : {}),
|
|
231
|
+
order: steps.length + 1, method: "DELETE", path: targetPath,
|
|
232
|
+
description: `Delete ${singular}`,
|
|
233
|
+
interactionType: "success", expectedStatusCode: 204,
|
|
234
|
+
...(steps.length > 0 ? { chainsFrom: { sourceStep: 1, sourceField: "id", sourceLocation: "body", targetParam: `${singular}_id`, targetLocation: "path" } } : {}),
|
|
510
235
|
});
|
|
511
|
-
if (
|
|
236
|
+
if (group.paramPath && group.methods.has("GET")) {
|
|
512
237
|
steps.push({
|
|
513
238
|
order: steps.length + 1, method: "GET", path: group.paramPath,
|
|
514
|
-
description: `Verify
|
|
239
|
+
description: `Verify ${singular} is deleted (404)`,
|
|
515
240
|
interactionType: "error", expectedStatusCode: 404,
|
|
516
241
|
});
|
|
517
242
|
}
|
|
518
243
|
}
|
|
519
244
|
const suffix = method === "DELETE" ? "delete" : method.toLowerCase();
|
|
520
245
|
return {
|
|
521
|
-
scenarioName: `${resource}-${suffix}-
|
|
522
|
-
description: `
|
|
246
|
+
scenarioName: `${resource}-${suffix}-lifecycle`,
|
|
247
|
+
description: `Lifecycle test: ${method} ${group.basePath} — verify ${method === "DELETE" ? "delete and subsequent 404" : "create and read-back"} flow`,
|
|
523
248
|
category: "new_endpoint",
|
|
524
249
|
priority: "high",
|
|
525
250
|
steps,
|
|
526
251
|
chainingKeys: ["id", `${singular}_id`],
|
|
527
252
|
requiresAuth: true,
|
|
528
|
-
estimatedComplexity: "
|
|
529
|
-
source:
|
|
530
|
-
testType:
|
|
253
|
+
estimatedComplexity: "moderate",
|
|
254
|
+
source: ScenarioSource.CodeInferred,
|
|
255
|
+
testType: TestType.INTEGRATION,
|
|
531
256
|
};
|
|
532
257
|
}
|
|
533
258
|
function diffDirectContract(method, path, resource) {
|
|
@@ -541,8 +266,8 @@ function diffDirectContract(method, path, resource) {
|
|
|
541
266
|
chainingKeys: [],
|
|
542
267
|
requiresAuth: true,
|
|
543
268
|
estimatedComplexity: "simple",
|
|
544
|
-
source:
|
|
545
|
-
testType:
|
|
269
|
+
source: ScenarioSource.CodeInferred,
|
|
270
|
+
testType: TestType.CONTRACT,
|
|
546
271
|
};
|
|
547
272
|
}
|
|
548
273
|
function diffDirectNotFound(method, path, resource, singular) {
|
|
@@ -555,8 +280,8 @@ function diffDirectNotFound(method, path, resource, singular) {
|
|
|
555
280
|
chainingKeys: [],
|
|
556
281
|
requiresAuth: true,
|
|
557
282
|
estimatedComplexity: "simple",
|
|
558
|
-
source:
|
|
559
|
-
testType:
|
|
283
|
+
source: ScenarioSource.CodeInferred,
|
|
284
|
+
testType: TestType.CONTRACT,
|
|
560
285
|
};
|
|
561
286
|
}
|
|
562
287
|
function diffDirectValidation(method, path, resource) {
|
|
@@ -569,8 +294,8 @@ function diffDirectValidation(method, path, resource) {
|
|
|
569
294
|
chainingKeys: [],
|
|
570
295
|
requiresAuth: true,
|
|
571
296
|
estimatedComplexity: "simple",
|
|
572
|
-
source:
|
|
573
|
-
testType:
|
|
297
|
+
source: ScenarioSource.CodeInferred,
|
|
298
|
+
testType: TestType.CONTRACT,
|
|
574
299
|
};
|
|
575
300
|
}
|
|
576
301
|
/**
|
|
@@ -579,6 +304,47 @@ function diffDirectValidation(method, path, resource) {
|
|
|
579
304
|
* derived totals (total_amount, item_count, subtotal) are recalculated.
|
|
580
305
|
* This pattern catches the most common class of user-reported bugs.
|
|
581
306
|
*/
|
|
307
|
+
function diffDirectBoundaryValues(method, resource, singular, group) {
|
|
308
|
+
const steps = [];
|
|
309
|
+
const targetPath = group.paramPath ?? group.basePath;
|
|
310
|
+
const pathParamName = group.paramPath?.match(/\{([^}]+)\}/)?.[1] ?? `${singular}_id`;
|
|
311
|
+
if (group.methods.has("POST")) {
|
|
312
|
+
steps.push({
|
|
313
|
+
order: 1,
|
|
314
|
+
method: "POST",
|
|
315
|
+
path: group.basePath,
|
|
316
|
+
description: `Create a ${singular} for boundary testing`,
|
|
317
|
+
interactionType: "success",
|
|
318
|
+
expectedStatusCode: 201,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
steps.push({
|
|
322
|
+
order: steps.length + 1,
|
|
323
|
+
method,
|
|
324
|
+
path: targetPath,
|
|
325
|
+
description: `${method} with valid boundary values (e.g. 0, maximum allowed, empty collection) — expect 200`,
|
|
326
|
+
interactionType: "success",
|
|
327
|
+
expectedStatusCode: 200,
|
|
328
|
+
// Signal the execution plan that a body is required. The agent reads source
|
|
329
|
+
// to discover the actual numeric/percentage field names to test at boundaries.
|
|
330
|
+
bodyMustInclude: ["numeric_or_percentage_field"],
|
|
331
|
+
...(steps.length > 0 && group.paramPath
|
|
332
|
+
? { chainsFrom: { sourceStep: 1, sourceField: "id", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
|
|
333
|
+
: {}),
|
|
334
|
+
});
|
|
335
|
+
return {
|
|
336
|
+
scenarioName: `${resource}-${method.toLowerCase()}-boundary-values`,
|
|
337
|
+
description: `Boundary test: ${method} ${targetPath} — test valid boundary values (0%, 100%, minimum, maximum allowed by schema). Read source to identify numeric/percentage fields and their constraints; assert the response reflects the boundary value correctly.`,
|
|
338
|
+
category: "new_endpoint",
|
|
339
|
+
priority: "high",
|
|
340
|
+
steps,
|
|
341
|
+
chainingKeys: ["id", pathParamName],
|
|
342
|
+
requiresAuth: true,
|
|
343
|
+
estimatedComplexity: "moderate",
|
|
344
|
+
source: ScenarioSource.CodeInferred,
|
|
345
|
+
testType: TestType.INTEGRATION,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
582
348
|
function diffDirectMutationRecalc(method, resource, singular, group) {
|
|
583
349
|
const steps = [];
|
|
584
350
|
if (group.methods.has("POST")) {
|
|
@@ -586,7 +352,7 @@ function diffDirectMutationRecalc(method, resource, singular, group) {
|
|
|
586
352
|
order: 1,
|
|
587
353
|
method: "POST",
|
|
588
354
|
path: group.basePath,
|
|
589
|
-
description: `Create a ${singular}
|
|
355
|
+
description: `Create a ${singular} for testing`,
|
|
590
356
|
interactionType: "success",
|
|
591
357
|
expectedStatusCode: 201,
|
|
592
358
|
});
|
|
@@ -598,10 +364,9 @@ function diffDirectMutationRecalc(method, resource, singular, group) {
|
|
|
598
364
|
order: steps.length + 1,
|
|
599
365
|
method,
|
|
600
366
|
path: targetPath,
|
|
601
|
-
description: `${method} the ${singular}
|
|
367
|
+
description: `${method} the ${singular} with valid changes — read source to find mutable fields`,
|
|
602
368
|
interactionType: "success",
|
|
603
369
|
expectedStatusCode: 200,
|
|
604
|
-
bodyMustInclude: ["child collection array (e.g. items)", "FK reference to parent resource (e.g. product_id)", "quantity or amount"],
|
|
605
370
|
...(sourceStep > 0 && group.paramPath
|
|
606
371
|
? { chainsFrom: { sourceStep, sourceField: "id", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
|
|
607
372
|
: {}),
|
|
@@ -611,39 +376,111 @@ function diffDirectMutationRecalc(method, resource, singular, group) {
|
|
|
611
376
|
order: steps.length + 1,
|
|
612
377
|
method: "GET",
|
|
613
378
|
path: group.paramPath,
|
|
614
|
-
description: `Verify the ${singular} reflects
|
|
379
|
+
description: `Verify the ${singular} reflects changes — check any derived/calculated fields`,
|
|
615
380
|
interactionType: "success",
|
|
616
381
|
expectedStatusCode: 200,
|
|
617
|
-
expectedResponseFields: ["child collection array", "each child's FK reference", "each child's quantity", "recalculated total"],
|
|
618
382
|
...(sourceStep > 0 && group.paramPath
|
|
619
383
|
? { chainsFrom: { sourceStep, sourceField: "id", sourceLocation: "body", targetParam: pathParamName, targetLocation: "path" } }
|
|
620
384
|
: {}),
|
|
621
385
|
});
|
|
622
386
|
}
|
|
623
387
|
return {
|
|
624
|
-
scenarioName: `${resource}-${method.toLowerCase()}-
|
|
625
|
-
description: `Mutation
|
|
388
|
+
scenarioName: `${resource}-${method.toLowerCase()}-mutation-verify`,
|
|
389
|
+
description: `Mutation test: ${method} ${targetPath} — update fields and verify derived calculations (totals, discounts, sums, quantities) are recomputed correctly. Test at least one calculation-affecting change (e.g. discount percentage, quantity update, item addition/removal).`,
|
|
626
390
|
category: "new_endpoint",
|
|
627
391
|
priority: "high",
|
|
628
392
|
steps,
|
|
629
393
|
chainingKeys: ["id", pathParamName],
|
|
630
394
|
requiresAuth: true,
|
|
631
395
|
estimatedComplexity: "complex",
|
|
632
|
-
source:
|
|
633
|
-
testType:
|
|
396
|
+
source: ScenarioSource.CodeInferred,
|
|
397
|
+
testType: TestType.INTEGRATION,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function diffDirectAuthBoundary(method, path, resource) {
|
|
401
|
+
return {
|
|
402
|
+
scenarioName: `${resource}-${method.toLowerCase()}-auth-boundary`,
|
|
403
|
+
description: `Auth boundary: ${method} ${path} without authentication returns 401 Unauthorized`,
|
|
404
|
+
category: "security_boundary",
|
|
405
|
+
priority: "high",
|
|
406
|
+
steps: [{ order: 1, method, path, description: `${method} ${path} without Authorization header — expect 401 Unauthorized`, interactionType: "error", expectedStatusCode: 401 }],
|
|
407
|
+
chainingKeys: [],
|
|
408
|
+
requiresAuth: false,
|
|
409
|
+
estimatedComplexity: "simple",
|
|
410
|
+
source: ScenarioSource.CodeInferred,
|
|
411
|
+
testType: TestType.CONTRACT,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Draft a unified scenario for GET endpoints without path params (collection or search).
|
|
416
|
+
* Rather than hardcoding keyword detection, the description instructs the LLM to read
|
|
417
|
+
* the diff source and determine whether query params are involved, then draft variations
|
|
418
|
+
* accordingly.
|
|
419
|
+
*/
|
|
420
|
+
function diffDirectGetCollection(path, resource, singular, group) {
|
|
421
|
+
const steps = [];
|
|
422
|
+
if (group.methods.has("POST")) {
|
|
423
|
+
steps.push({
|
|
424
|
+
order: 1,
|
|
425
|
+
method: "POST",
|
|
426
|
+
path: group.basePath,
|
|
427
|
+
description: `Create a ${singular} for query verification`,
|
|
428
|
+
interactionType: "success",
|
|
429
|
+
expectedStatusCode: 201,
|
|
430
|
+
});
|
|
431
|
+
steps.push({
|
|
432
|
+
order: 2,
|
|
433
|
+
method: "GET",
|
|
434
|
+
path,
|
|
435
|
+
description: `Query ${resource} and verify results include the created item`,
|
|
436
|
+
interactionType: "success",
|
|
437
|
+
expectedStatusCode: 200,
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
else {
|
|
441
|
+
steps.push({
|
|
442
|
+
order: 1,
|
|
443
|
+
method: "GET",
|
|
444
|
+
path,
|
|
445
|
+
description: `Query ${resource} and verify response structure`,
|
|
446
|
+
interactionType: "success",
|
|
447
|
+
expectedStatusCode: 200,
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
// Include a path-specific suffix in scenarioName to avoid collision when multiple GET
|
|
451
|
+
// collection endpoints exist for the same resource (e.g. /products and /products/featured).
|
|
452
|
+
// Strip API prefix segments (api, v1, ...) and param segments, take the last 1-2 meaningful ones.
|
|
453
|
+
const meaningfulSegs = path.split("/").filter(s => s && !s.startsWith("{") && !SKIP_SEGMENTS.has(s));
|
|
454
|
+
const pathSlug = meaningfulSegs.slice(-2).join("-") || resource;
|
|
455
|
+
const uniqueName = pathSlug === resource ? `${resource}-list-query` : `${pathSlug}-list-query`;
|
|
456
|
+
return {
|
|
457
|
+
scenarioName: uniqueName,
|
|
458
|
+
description: `List/query test: GET ${path} — read source to find supported query params (filters, pagination, search), then test with and without them`,
|
|
459
|
+
category: "new_endpoint",
|
|
460
|
+
priority: "high",
|
|
461
|
+
steps,
|
|
462
|
+
chainingKeys: ["id", `${singular}_id`],
|
|
463
|
+
requiresAuth: true,
|
|
464
|
+
estimatedComplexity: group.methods.has("POST") ? "moderate" : "simple",
|
|
465
|
+
source: ScenarioSource.CodeInferred,
|
|
466
|
+
testType: group.methods.has("POST") ? TestType.INTEGRATION : TestType.CONTRACT,
|
|
634
467
|
};
|
|
635
468
|
}
|
|
636
469
|
/**
|
|
637
470
|
* Draft scenarios that directly test each new endpoint in the branch diff.
|
|
638
471
|
*
|
|
639
|
-
* For each new endpoint,
|
|
640
|
-
* PUT/PATCH → integration
|
|
641
|
-
* POST → integration
|
|
642
|
-
* DELETE → integration
|
|
472
|
+
* For each new endpoint, method-specific scenarios are produced:
|
|
473
|
+
* PUT/PATCH → mutation-recalc integration, boundary-values integration, contract, not-found
|
|
474
|
+
* POST → lifecycle integration (create + optional GET), contract, validation error
|
|
475
|
+
* DELETE → lifecycle integration (create + delete + 404), contract
|
|
643
476
|
* GET /{id} → contract, not-found
|
|
477
|
+
* GET /coll → contract + collection-list integration (create-then-GET) or contract-only
|
|
644
478
|
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
479
|
+
* Additionally, PUT/PATCH/DELETE methods get an auth-boundary scenario
|
|
480
|
+
* (category "security_boundary" → HIGH priority, not CRITICAL) so it
|
|
481
|
+
* lands in ADDITIONAL deterministically instead of depending on LLM
|
|
482
|
+
* supplement. GET is excluded (often public); POST is excluded because
|
|
483
|
+
* the security-boundary supplement tier covers it.
|
|
647
484
|
*/
|
|
648
485
|
export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
649
486
|
if (newEndpoints.length === 0)
|
|
@@ -688,27 +525,29 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
|
688
525
|
}
|
|
689
526
|
}
|
|
690
527
|
}
|
|
691
|
-
// When extractResourceName returns null
|
|
692
|
-
//
|
|
693
|
-
|
|
694
|
-
if (!resource) {
|
|
528
|
+
// When extractResourceName returns null OR the resource isn't in resourceGroups,
|
|
529
|
+
// fall back to finding a matching group via prefix or suffix matching.
|
|
530
|
+
if (!resource || !resourceGroups.has(resource)) {
|
|
695
531
|
let bestMatch = null;
|
|
696
532
|
for (const [rName, rGroup] of resourceGroups) {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
533
|
+
// Prefix match: epPath starts with group's basePath (e.g. /products/search → /products)
|
|
534
|
+
if (epPath.startsWith(rGroup.basePath + "/") || epPath === rGroup.basePath) {
|
|
535
|
+
if (!bestMatch || rGroup.basePath.length > bestMatch.length) {
|
|
536
|
+
bestMatch = { name: rName, path: rGroup.basePath, length: rGroup.basePath.length, isPrefixMatch: true };
|
|
700
537
|
}
|
|
701
538
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
539
|
+
// Suffix match for router-relative paths (e.g. /{order_id} → /api/v1/orders/{order_id})
|
|
540
|
+
if (rGroup.paramPath && pathSuffixMatches(rGroup.paramPath, epPath)) {
|
|
541
|
+
if (!bestMatch || rGroup.paramPath.length > bestMatch.length) {
|
|
542
|
+
bestMatch = { name: rName, path: rGroup.paramPath, length: rGroup.paramPath.length, isPrefixMatch: false };
|
|
705
543
|
}
|
|
706
544
|
}
|
|
707
545
|
}
|
|
708
546
|
if (!bestMatch)
|
|
709
547
|
continue;
|
|
710
548
|
resource = bestMatch.name;
|
|
711
|
-
|
|
549
|
+
// For prefix matches (action sub-paths), keep original path; for suffix matches, use resolved path
|
|
550
|
+
resolvedPath = bestMatch.isPrefixMatch ? epPath : bestMatch.path;
|
|
712
551
|
}
|
|
713
552
|
const group = resourceGroups.get(resource);
|
|
714
553
|
if (!group)
|
|
@@ -723,8 +562,10 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
|
723
562
|
};
|
|
724
563
|
for (const method of methods) {
|
|
725
564
|
if (method === "PUT" || method === "PATCH") {
|
|
565
|
+
// mutation-recalc tests the primary calculation flow
|
|
726
566
|
add(diffDirectMutationRecalc(method, resource, singular, group));
|
|
727
|
-
|
|
567
|
+
// boundary-values tests edge cases (0, max, empty)
|
|
568
|
+
add(diffDirectBoundaryValues(method, resource, singular, group));
|
|
728
569
|
add(diffDirectContract(method, resolvedPath, resource));
|
|
729
570
|
if (hasPathParam)
|
|
730
571
|
add(diffDirectNotFound(method, resolvedPath, resource, singular));
|
|
@@ -742,6 +583,12 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups) {
|
|
|
742
583
|
add(diffDirectContract(method, resolvedPath, resource));
|
|
743
584
|
add(diffDirectNotFound(method, resolvedPath, resource, singular));
|
|
744
585
|
}
|
|
586
|
+
else if (method === "GET" && !hasPathParam) {
|
|
587
|
+
add(diffDirectContract(method, resolvedPath, resource));
|
|
588
|
+
add(diffDirectGetCollection(resolvedPath, resource, singular, group));
|
|
589
|
+
}
|
|
590
|
+
if (method === "PUT" || method === "PATCH" || method === "DELETE")
|
|
591
|
+
add(diffDirectAuthBoundary(method, resolvedPath, resource));
|
|
745
592
|
}
|
|
746
593
|
}
|
|
747
594
|
return scenarios;
|