@skyramp/mcp 0.3.5 → 0.3.6-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/playwright/registerPlaywrightTools.js +92 -30
- package/build/playwright/traceRecordingPrompt.d.ts +6 -0
- package/build/playwright/traceRecordingPrompt.js +6 -2
- package/build/prompts/code-reuse.d.ts +1 -2
- package/build/prompts/code-reuse.js +182 -77
- package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
- package/build/prompts/modularization/integration-test-modularization.js +83 -41
- package/build/prompts/modularization/render.d.ts +18 -0
- package/build/prompts/modularization/render.js +12 -0
- package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
- package/build/prompts/modularization/ui-test-modularization.js +89 -47
- package/build/prompts/pom-aware-code-reuse.js +3 -1
- package/build/prompts/shared-helper-policy.d.ts +57 -0
- package/build/prompts/shared-helper-policy.js +135 -0
- package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
- package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
- package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
- package/build/prompts/test-recommendation/recommendationShared.js +90 -16
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
- package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
- package/build/prompts/testbot/testbot-prompts.js +80 -34
- package/build/recommendation/budgeters/shared.js +105 -27
- package/build/recommendation/discriminators.js +13 -2
- package/build/recommendation/planRanker.d.ts +6 -6
- package/build/recommendation/planRanker.js +6 -61
- package/build/services/AnalyticsService.d.ts +7 -0
- package/build/services/AnalyticsService.js +7 -1
- package/build/services/ModularizationService.js +1 -3
- package/build/services/TestDiscoveryService.d.ts +0 -2
- package/build/services/TestDiscoveryService.js +2 -37
- package/build/services/TestGenerationService.d.ts +16 -0
- package/build/services/TestGenerationService.js +86 -10
- package/build/services/containerEnv.js +13 -12
- package/build/tools/code-refactor/codeReuseTool.js +279 -93
- package/build/tools/code-refactor/enhance-state.d.ts +49 -0
- package/build/tools/code-refactor/enhance-state.js +109 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
- package/build/tools/code-refactor/modularizationTool.js +9 -2
- package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
- package/build/tools/code-refactor/reuse-outcome.js +14 -4
- package/build/tools/code-refactor/reuse-state.d.ts +127 -5
- package/build/tools/code-refactor/reuse-state.js +628 -16
- package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
- package/build/tools/code-refactor/utils-verify-gates.js +100 -0
- package/build/tools/code-refactor/verify-gates.d.ts +2 -1
- package/build/tools/code-refactor/verify-gates.js +90 -25
- package/build/tools/executeSkyrampTestTool.d.ts +19 -0
- package/build/tools/executeSkyrampTestTool.js +158 -8
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
- package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
- package/build/tools/generate-tests/generateUIRestTool.js +22 -0
- package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
- package/build/tools/generate-tests/scenarioLint.js +127 -19
- package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
- package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
- package/build/tools/submitReportTool.d.ts +38 -38
- package/build/tools/submitReportTool.js +411 -114
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +71 -10
- package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
- package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
- package/build/tools/test-management/registerTestPlanTool.js +70 -12
- package/build/types/Recommendation.d.ts +34 -5
- package/build/types/RepositoryAnalysis.d.ts +133 -114
- package/build/types/RepositoryAnalysis.js +1 -1
- package/build/types/ReuseOutcome.d.ts +102 -6
- package/build/types/ReuseOutcome.js +16 -2
- package/build/types/TestRecommendation.js +21 -3
- package/build/types/TestTypes.js +14 -8
- package/build/types/TestbotReport.d.ts +10 -1
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +57 -1
- package/build/utils/AnalysisStateManager.js +54 -5
- package/build/utils/branchDiff.d.ts +10 -0
- package/build/utils/branchDiff.js +28 -0
- package/build/utils/changedRoutes.d.ts +29 -0
- package/build/utils/changedRoutes.js +87 -0
- package/build/utils/featureFlags.d.ts +21 -0
- package/build/utils/featureFlags.js +23 -0
- package/build/utils/frontendIntegration.js +34 -4
- package/build/utils/importerHop.d.ts +2 -8
- package/build/utils/importerHop.js +15 -53
- package/build/utils/pathMatching.d.ts +38 -0
- package/build/utils/pathMatching.js +71 -0
- package/build/utils/pathSignatures.d.ts +22 -0
- package/build/utils/pathSignatures.js +57 -0
- package/build/utils/planMatchKeys.d.ts +16 -3
- package/build/utils/planMatchKeys.js +26 -10
- package/build/utils/pluralization.d.ts +10 -0
- package/build/utils/pluralization.js +18 -0
- package/build/utils/pom-catalog-parse.d.ts +52 -0
- package/build/utils/pom-catalog-parse.js +141 -0
- package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
- package/build/utils/pom-scope/selector-extractor.js +34 -8
- package/build/utils/pom-verify/verify.d.ts +6 -5
- package/build/utils/pom-verify/verify.js +8 -6
- package/build/utils/reportVerification.d.ts +64 -4
- package/build/utils/reportVerification.js +228 -3
- package/build/utils/reuseRouting.d.ts +3 -0
- package/build/utils/reuseRouting.js +50 -0
- package/build/utils/routeParsers.d.ts +2 -0
- package/build/utils/routeParsers.js +65 -8
- package/build/utils/scenarioDrafting.d.ts +1 -1
- package/build/utils/scenarioDrafting.js +57 -45
- package/build/utils/subjectEndpoints.d.ts +19 -0
- package/build/utils/subjectEndpoints.js +98 -0
- package/build/utils/testFileClassification.d.ts +11 -0
- package/build/utils/testFileClassification.js +47 -0
- package/build/utils/uiPageEnumerator.d.ts +45 -19
- package/build/utils/uiPageEnumerator.js +95 -51
- package/build/utils/utils-verify/allow.d.ts +16 -0
- package/build/utils/utils-verify/allow.js +68 -0
- package/build/utils/utils-verify/call-sites.d.ts +34 -0
- package/build/utils/utils-verify/call-sites.js +154 -0
- package/build/utils/utils-verify/index.d.ts +7 -0
- package/build/utils/utils-verify/index.js +7 -0
- package/build/utils/utils-verify/language-spec.d.ts +91 -0
- package/build/utils/utils-verify/language-spec.js +210 -0
- package/build/utils/utils-verify/locate.d.ts +39 -0
- package/build/utils/utils-verify/locate.js +199 -0
- package/build/utils/utils-verify/parse.d.ts +34 -0
- package/build/utils/utils-verify/parse.js +177 -0
- package/build/utils/utils-verify/stage.d.ts +24 -0
- package/build/utils/utils-verify/stage.js +107 -0
- package/build/utils/utils-verify/verify.d.ts +63 -0
- package/build/utils/utils-verify/verify.js +168 -0
- package/build/utils/utils.d.ts +3 -1
- package/build/utils/utils.js +3 -1
- package/build/workspace/workspace.d.ts +32 -32
- package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
- package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
- package/node_modules/playwright/package.json +1 -1
- package/package.json +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +0 -5
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import * as fs from "fs/promises";
|
|
2
3
|
import { execFile } from "child_process";
|
|
3
4
|
import { promisify } from "util";
|
|
4
5
|
import { testFileMatches } from "./utils.js";
|
|
@@ -72,8 +73,14 @@ export async function listChangedFiles(repoRoot) {
|
|
|
72
73
|
}
|
|
73
74
|
/**
|
|
74
75
|
* Cross-check report claims against files that actually changed in the working
|
|
75
|
-
* tree, returning a human-readable description for each claim
|
|
76
|
-
*
|
|
76
|
+
* tree, returning a human-readable description for each claim whose file has no
|
|
77
|
+
* change. An empty result means every claim is backed.
|
|
78
|
+
*
|
|
79
|
+
* Not to be confused with `findInvalidSourceCitations` below. This one is about
|
|
80
|
+
* files the agent says IT wrote — a created test, a maintained test — so the test
|
|
81
|
+
* is "did this file change in this run". That one is about application code the
|
|
82
|
+
* agent POINTS AT as the home of a defect, which it never edits, so the test is
|
|
83
|
+
* "does this file and symbol exist in the checkout at all".
|
|
77
84
|
*
|
|
78
85
|
* The delivery path can only ship what the agent actually edited (SKYR-3883), so
|
|
79
86
|
* a report must never claim file work the tree doesn't reflect. This is the
|
|
@@ -96,7 +103,7 @@ export async function listChangedFiles(repoRoot) {
|
|
|
96
103
|
* - Claims for files outside this checkout (cross-repo `repository`, or an
|
|
97
104
|
* absolute path resolving outside `repoRoot`) — a different checkout owns them.
|
|
98
105
|
*/
|
|
99
|
-
export function
|
|
106
|
+
export function findUnchangedFileClaims(input) {
|
|
100
107
|
const { repoRoot, changedFiles, newTests, verdicts, primaryRepository } = input;
|
|
101
108
|
const unbacked = [];
|
|
102
109
|
const isBacked = (claimedPath) => {
|
|
@@ -139,3 +146,221 @@ export function findUnbackedClaims(input) {
|
|
|
139
146
|
}
|
|
140
147
|
return unbacked;
|
|
141
148
|
}
|
|
149
|
+
/** Both ways out of a citation rejection, appended to every message so the agent
|
|
150
|
+
* never has to guess that dropping the citation is allowed. */
|
|
151
|
+
const CITATION_REMEDY = "Read the file you mean, correct sourceFile/sourceSymbol, and resubmit — or remove the citation if you cannot confirm it in source.";
|
|
152
|
+
/** How many same-basename paths to offer for a missing file. Enough to converge in
|
|
153
|
+
* one retry, few enough that the message stays readable. */
|
|
154
|
+
const MAX_PATH_SUGGESTIONS = 3;
|
|
155
|
+
/** True when `abs` names something strictly below `root`. */
|
|
156
|
+
function isInsideRoot(root, abs) {
|
|
157
|
+
const rel = path.relative(root, abs);
|
|
158
|
+
return !!rel && !rel.startsWith("..") && !path.isAbsolute(rel);
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Resolve a cited path under one checkout, or return null when it lands outside.
|
|
162
|
+
* A citation names a file INSIDE the repo, so `../../etc/passwd` and an absolute
|
|
163
|
+
* path from another tree are invalid citations — not files to go looking for.
|
|
164
|
+
*/
|
|
165
|
+
function resolveInRoot(root, cited) {
|
|
166
|
+
// The contract is a repository-relative path. An absolute one would slip past
|
|
167
|
+
// the containment check whenever it happens to sit under root, because
|
|
168
|
+
// path.resolve() discards root — and it is unusable as a review anchor.
|
|
169
|
+
if (path.isAbsolute(cited))
|
|
170
|
+
return null;
|
|
171
|
+
const abs = path.resolve(root, cited);
|
|
172
|
+
return isInsideRoot(root, abs) ? abs : null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Repo-relative paths whose basename equals the cited file's basename, so a
|
|
176
|
+
* rejection can name the file the agent probably meant. `git ls-files` does the
|
|
177
|
+
* filtering (a pathspec, not a full listing) to stay cheap on a large repo; a
|
|
178
|
+
* non-git directory or missing git yields no suggestions rather than an error.
|
|
179
|
+
*/
|
|
180
|
+
async function suggestSimilarPaths(roots, cited) {
|
|
181
|
+
const base = path.basename(cited);
|
|
182
|
+
if (!base)
|
|
183
|
+
return [];
|
|
184
|
+
const matches = [];
|
|
185
|
+
for (const root of roots) {
|
|
186
|
+
try {
|
|
187
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "-z", "--", `*${base}`, base], {
|
|
188
|
+
cwd: root,
|
|
189
|
+
});
|
|
190
|
+
for (const p of stdout.split("\0")) {
|
|
191
|
+
// The pathspec also matches "myproducts.py" for "products.py"; keep only
|
|
192
|
+
// an exact basename hit so a suggestion is a real alternative spelling.
|
|
193
|
+
if (p && path.basename(p) === base)
|
|
194
|
+
matches.push(p);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Not a git checkout, or git unavailable — no suggestions to offer.
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return matches.slice(0, MAX_PATH_SUGGESTIONS);
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Check every `sourceFile`/`sourceSymbol` citation on the submitted issuesFound
|
|
205
|
+
* entries against the repository, and say which repo each one belongs to.
|
|
206
|
+
* Returns one message per DEFINITE mismatch — an empty `invalid` means the report
|
|
207
|
+
* may be written — plus, per entry, the owner/repo the citation resolved in.
|
|
208
|
+
*
|
|
209
|
+
* The attribution is the reason this resolves against every checkout instead of
|
|
210
|
+
* the first hit: the consumer must not have to guess a cited file's repo from the
|
|
211
|
+
* path, which is what makes it link the primary repo's copy of a related repo's
|
|
212
|
+
* file. Where the citation resolves is the answer, so it is returned rather than
|
|
213
|
+
* discarded. Only a related repo is stamped: an absent `repository` already means
|
|
214
|
+
* the primary repo downstream, so a primary-repo citation ships as submitted.
|
|
215
|
+
*
|
|
216
|
+
* Rejects four things: a path that no known checkout contains, a symbol absent
|
|
217
|
+
* from the file that was found, an attribution that names a different repo than
|
|
218
|
+
* the one holding the code, and a file that resolves in several named checkouts
|
|
219
|
+
* with no attribution to choose between them. Everything uncertain is ACCEPTED —
|
|
220
|
+
* no checkout, an unreadable file, a checkout with no owner/repo, a directory
|
|
221
|
+
* listing that fails. The check must behave the same on any repo, in any
|
|
222
|
+
* language, at any size, and it must never become a loop the agent cannot exit.
|
|
223
|
+
* Symbol matching is a plain substring search for the same reason: no parser, so
|
|
224
|
+
* no language it silently mishandles.
|
|
225
|
+
*/
|
|
226
|
+
export async function findInvalidSourceCitations(input) {
|
|
227
|
+
const invalid = [];
|
|
228
|
+
const repository = [];
|
|
229
|
+
const checkouts = input.checkouts.filter((c) => c.root && c.root !== "unknown");
|
|
230
|
+
if (checkouts.length === 0)
|
|
231
|
+
return { invalid, repository }; // Cannot verify anything — accept.
|
|
232
|
+
const roots = checkouts.map((c) => c.root);
|
|
233
|
+
for (let i = 0; i < input.issues.length; i++) {
|
|
234
|
+
const cited = input.issues[i].sourceFile?.trim();
|
|
235
|
+
if (!cited)
|
|
236
|
+
continue; // No citation on this entry — nothing to check.
|
|
237
|
+
const candidates = [];
|
|
238
|
+
for (const checkout of checkouts) {
|
|
239
|
+
const abs = resolveInRoot(checkout.root, cited);
|
|
240
|
+
if (abs)
|
|
241
|
+
candidates.push({ checkout, abs });
|
|
242
|
+
}
|
|
243
|
+
if (candidates.length === 0) {
|
|
244
|
+
invalid.push(`issuesFound[${i}] cites ${cited}, which resolves outside every repository checkout in this run. ` +
|
|
245
|
+
`Cite a path inside the checkout, relative to the repository root. ${CITATION_REMEDY}`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
// One relative path can exist in several checkouts of a multi-repo run, and
|
|
249
|
+
// the citation does not say which one it means. Keep every hit, so the symbol
|
|
250
|
+
// below is looked for in all of them instead of only the first.
|
|
251
|
+
const found = [];
|
|
252
|
+
let unreadable = false;
|
|
253
|
+
for (const candidate of candidates) {
|
|
254
|
+
try {
|
|
255
|
+
if (!(await fs.stat(candidate.abs)).isFile())
|
|
256
|
+
continue;
|
|
257
|
+
// stat() and readFile() both follow symlinks, so a link out of the tree
|
|
258
|
+
// would let a citation resolve against content outside the checkout.
|
|
259
|
+
// Compare the real paths before the file counts as found.
|
|
260
|
+
if (isInsideRoot(await fs.realpath(candidate.checkout.root), await fs.realpath(candidate.abs))) {
|
|
261
|
+
found.push(candidate);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
// "Not there" is the answer we want. Any other error (permissions, I/O)
|
|
266
|
+
// means the check could not run, which is never grounds to reject.
|
|
267
|
+
if (err.code !== "ENOENT")
|
|
268
|
+
unreadable = true;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (found.length === 0) {
|
|
272
|
+
if (unreadable)
|
|
273
|
+
continue; // Could not look — accept.
|
|
274
|
+
const suggestions = await suggestSimilarPaths(roots, cited);
|
|
275
|
+
invalid.push(`issuesFound[${i}] cites ${cited} but that file does not exist in the repository` +
|
|
276
|
+
(suggestions.length > 0 ? ` (closest matches: ${suggestions.join(", ")})` : "") +
|
|
277
|
+
`. ${CITATION_REMEDY}`);
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// The citation resolves in every checkout of `found`; a symbol narrows that to
|
|
281
|
+
// the copies that actually contain it, which is what the attribution below is
|
|
282
|
+
// taken from.
|
|
283
|
+
let resolved = found;
|
|
284
|
+
const symbol = input.issues[i].sourceSymbol?.trim();
|
|
285
|
+
if (symbol) {
|
|
286
|
+
const withSymbol = [];
|
|
287
|
+
let readAny = false;
|
|
288
|
+
for (const hit of found) {
|
|
289
|
+
try {
|
|
290
|
+
const content = await fs.readFile(hit.abs, "utf8");
|
|
291
|
+
readAny = true;
|
|
292
|
+
if (content.includes(symbol))
|
|
293
|
+
withSymbol.push(hit);
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// Could not read this copy — try the next one.
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (!readAny)
|
|
300
|
+
continue; // Could not read any of them — accept.
|
|
301
|
+
if (withSymbol.length === 0) {
|
|
302
|
+
invalid.push(`issuesFound[${i}] cites ${symbol} in ${cited}, but that text does not appear anywhere in the file. ` +
|
|
303
|
+
CITATION_REMEDY);
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
resolved = withSymbol;
|
|
307
|
+
}
|
|
308
|
+
const attribution = attributeCitation({
|
|
309
|
+
entry: `issuesFound[${i}]`,
|
|
310
|
+
cited,
|
|
311
|
+
symbol,
|
|
312
|
+
declared: input.issues[i].repository?.trim(),
|
|
313
|
+
resolved: resolved.map((hit) => hit.checkout),
|
|
314
|
+
});
|
|
315
|
+
if (attribution.message)
|
|
316
|
+
invalid.push(attribution.message);
|
|
317
|
+
repository[i] = attribution.repository;
|
|
318
|
+
}
|
|
319
|
+
return { invalid, repository };
|
|
320
|
+
}
|
|
321
|
+
/** Distinct owner/repo names of the checkouts a citation resolved in, in order.
|
|
322
|
+
* A checkout the run could not name contributes nothing: it can be neither
|
|
323
|
+
* stamped nor offered as a candidate. */
|
|
324
|
+
function namesOf(resolved) {
|
|
325
|
+
return [...new Set(resolved.map((c) => c.repository).filter((r) => !!r))];
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Reconcile where a citation resolved with what the entry says about it, giving
|
|
329
|
+
* back the owner/repo to stamp, a rejection message, or neither.
|
|
330
|
+
*/
|
|
331
|
+
function attributeCitation(args) {
|
|
332
|
+
const { entry, cited, symbol, declared, resolved } = args;
|
|
333
|
+
const names = namesOf(resolved);
|
|
334
|
+
const where = symbol ? `${cited} (${symbol})` : cited;
|
|
335
|
+
if (declared) {
|
|
336
|
+
// The entry names its repo, so the citation is checked against THAT checkout.
|
|
337
|
+
// owner/repo is case-insensitive on GitHub, so a differently-cased attribution
|
|
338
|
+
// is the same repo — accepted, and re-stamped in the run's own spelling rather
|
|
339
|
+
// than sent back for a rewrite that would change nothing.
|
|
340
|
+
const match = resolved.find((c) => c.repository && c.repository.toLowerCase() === declared.toLowerCase());
|
|
341
|
+
if (match)
|
|
342
|
+
return match.repository === declared ? {} : { repository: match.repository };
|
|
343
|
+
// It resolved somewhere else. Only say so when the run can name where —
|
|
344
|
+
// otherwise there is nothing to correct the attribution to.
|
|
345
|
+
if (names.length === 0)
|
|
346
|
+
return {};
|
|
347
|
+
return {
|
|
348
|
+
message: `${entry} is attributed to ${declared}, but ${where} resolves in ${names.join(", ")} — not in ${declared}. ` +
|
|
349
|
+
`Set repository to the repo that holds the code, or cite a file that exists in ${declared}.`,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (resolved.length > 1) {
|
|
353
|
+
// Several checkouts hold this path, and the entry does not say which is meant.
|
|
354
|
+
if (names.length > 1) {
|
|
355
|
+
return {
|
|
356
|
+
message: `${entry} cites ${where}, which exists in more than one repository of this run (${names.join(", ")}), ` +
|
|
357
|
+
`and the entry does not say which one it is about. Set repository to the owner/repo whose file you read.`,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return {}; // Cannot name the alternatives — accept, and stamp nothing.
|
|
361
|
+
}
|
|
362
|
+
// Exactly one checkout. Stamp it unless it is the primary, whose findings are
|
|
363
|
+
// identified downstream by having NO repository — see SourceCitationCheck.
|
|
364
|
+
const [only] = resolved;
|
|
365
|
+
return only.primary || !only.repository ? {} : { repository: only.repository };
|
|
366
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function isBrowserTestType(testType?: string): boolean;
|
|
2
|
+
export declare function isModularizeFirstTarget(testType?: string, language?: string): boolean;
|
|
3
|
+
export declare function isPomAwareTarget(language: string, framework?: string, testType?: string): boolean;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing predicates for the code-reuse flow. Kept apart from the prompt
|
|
3
|
+
* modules so the generation services can route on them without importing the
|
|
4
|
+
* prompt text and its POM-catalog dependencies.
|
|
5
|
+
*/
|
|
6
|
+
import { isPomReuseEnabled, isUtilsReuseEnabled } from "./featureFlags.js";
|
|
7
|
+
// Page objects model browser pages — mapping an API-level test onto them is never
|
|
8
|
+
// meaningful, so only browser test types (or an unspecified type, for backwards
|
|
9
|
+
// compatibility) may take the POM path regardless of the feature flag.
|
|
10
|
+
export function isBrowserTestType(testType) {
|
|
11
|
+
return testType === undefined || testType === "ui" || testType === "e2e";
|
|
12
|
+
}
|
|
13
|
+
// Modularize-first flows: fresh codegen output carries no helper functions, so
|
|
14
|
+
// skyramp_modularization runs BEFORE skyramp_reuse_code and the reuse pass seeds
|
|
15
|
+
// the shared utils file from the helpers it created. The utils flag is the
|
|
16
|
+
// single gate for every test type — the modularization prompts' per-step
|
|
17
|
+
// extraction reads the same predicate, so the hand-off, the extraction and the
|
|
18
|
+
// seeding step agree on every codeReuse flow. Integration qualifies whenever the flag is on;
|
|
19
|
+
// UI only while the POM path is off (POM refactoring supersedes modularization,
|
|
20
|
+
// and the testbot UI flow asks for codeReuse unconditionally, so the flag is what
|
|
21
|
+
// keeps the default-off behavior a no-op reuse pass). e2e and load are not
|
|
22
|
+
// routed here: load has no modularization routing at all, e2e is deferred.
|
|
23
|
+
// Deliberately keyed on the POM *flag*, not on isPomAwareTarget: with the flag
|
|
24
|
+
// on, a UI target the POM path does not cover keeps today's plain reuse pass.
|
|
25
|
+
// Composing utils reuse with the POM path is a separate change; this predicate
|
|
26
|
+
// only decides the flag-off UI flow. The modularization prompts read it too, so
|
|
27
|
+
// extraction and seeding agree for every codeReuse flow; a modularizeCode-only
|
|
28
|
+
// call (no codeReuse) still receives the shared-helper rules — harmless without
|
|
29
|
+
// a reuse pass, and the tool cannot see codeReuse.
|
|
30
|
+
export function isModularizeFirstTarget(testType, language) {
|
|
31
|
+
if (!isUtilsReuseEnabled())
|
|
32
|
+
return false;
|
|
33
|
+
if (testType === "integration")
|
|
34
|
+
return true;
|
|
35
|
+
if (testType !== "ui" || isPomReuseEnabled())
|
|
36
|
+
return false;
|
|
37
|
+
// Shared browser helpers are Playwright TS/JS; other UI languages have no
|
|
38
|
+
// utils-file convention here and would fall through to a Python file name.
|
|
39
|
+
const lang = language?.toLowerCase();
|
|
40
|
+
return lang === "typescript" || lang === "javascript";
|
|
41
|
+
}
|
|
42
|
+
export function isPomAwareTarget(language, framework, testType) {
|
|
43
|
+
if (!isPomReuseEnabled())
|
|
44
|
+
return false;
|
|
45
|
+
if (!isBrowserTestType(testType))
|
|
46
|
+
return false;
|
|
47
|
+
const lang = language.toLowerCase();
|
|
48
|
+
return ((lang === "typescript" || lang === "javascript") &&
|
|
49
|
+
framework?.toLowerCase() === "playwright");
|
|
50
|
+
}
|
|
@@ -35,6 +35,8 @@ export declare function parseRouteLine(line: string, sourceFile: string): Parsed
|
|
|
35
35
|
* aggregator's `APIRouter(prefix="/api")` yields `/api/recipes`, not `/recipes`.
|
|
36
36
|
*/
|
|
37
37
|
export declare function parseFileEndpoints(content: string, sourceFile: string, mountPrefixes?: Map<string, string>): ParsedDiffEndpoint[];
|
|
38
|
+
export declare function isParamSegment(segment: string): boolean;
|
|
39
|
+
export declare function isOpaqueIdSegment(segment: string): boolean;
|
|
38
40
|
export declare const SKIP_PATH_SEGMENTS: Set<string>;
|
|
39
41
|
/**
|
|
40
42
|
* True for version/prefix path segments — "v1".."v99" and the literal "api".
|
|
@@ -127,10 +127,27 @@ export function parseRouteLine(line, sourceFile) {
|
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
|
-
// Go (Gin, Echo, Chi): <ident>.GET/POST/...("
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
130
|
+
// Go (Gin, Echo, Chi): <ident>.GET/POST/...("path", handler)
|
|
131
|
+
//
|
|
132
|
+
// An all-caps method token (Gin/Echo convention: `g.GET(...)`, `r.POST(...)`)
|
|
133
|
+
// is unambiguously a route registration — nobody writes `query.GET(...)` — so
|
|
134
|
+
// it's accepted even without a leading slash (real Gin routes like
|
|
135
|
+
// `g.GET("ping", ...)` omit it). A CamelCase token (Chi's `r.Get`/`r.Post`)
|
|
136
|
+
// shares call syntax with a plain getter — `query.Get("term")`,
|
|
137
|
+
// `Header.Get("Accept-Version")` — so it only counts as a route when the
|
|
138
|
+
// captured path starts with "/", which every Chi route does and a getter's
|
|
139
|
+
// argument never does.
|
|
140
|
+
const ginUpperMatch = stripped.match(/\w+\.(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)\s*\(\s*["']([^"'?#]+)/);
|
|
141
|
+
if (ginUpperMatch && sourceFile.endsWith(".go")) {
|
|
142
|
+
return { method: ginUpperMatch[1], path: ginUpperMatch[2], sourceFile };
|
|
143
|
+
}
|
|
144
|
+
const ginCamelMatch = stripped.match(/\w+\.(Get|Post|Put|Patch|Delete|Head|Options)\s*\(\s*["'](\/[^"'?#]*)/);
|
|
145
|
+
if (ginCamelMatch && sourceFile.endsWith(".go")) {
|
|
146
|
+
return {
|
|
147
|
+
method: ginCamelMatch[1].toUpperCase(),
|
|
148
|
+
path: ginCamelMatch[2],
|
|
149
|
+
sourceFile,
|
|
150
|
+
};
|
|
134
151
|
}
|
|
135
152
|
// Go stdlib: http.HandleFunc/Handle
|
|
136
153
|
const goHandleMatch = stripped.match(/(?:HandleFunc|Handle)\s*\(\s*["']([^"'?#]+)/);
|
|
@@ -490,6 +507,45 @@ export function parseFileEndpoints(content, sourceFile, mountPrefixes) {
|
|
|
490
507
|
path: ep.path && !ep.path.startsWith("/") ? `/${ep.path}` : ep.path,
|
|
491
508
|
}));
|
|
492
509
|
}
|
|
510
|
+
/**
|
|
511
|
+
* One path segment that names a parameter rather than a resource, in any route
|
|
512
|
+
* syntax the scanners read: OpenAPI "{id}", Express/NestJS ":id", and Next.js
|
|
513
|
+
* "[id]" / "[...id]" / "[[...id]]" (the double-bracket form needs its own
|
|
514
|
+
* alternative — it contains an inner "]", which the single-bracket form's
|
|
515
|
+
* [^\]]+ can never span).
|
|
516
|
+
*/
|
|
517
|
+
// The colon form may carry an Express/Fastify constraint or modifier —
|
|
518
|
+
// `:id(\d+)`, `:id?`, `:id*`, `:id+`. Those are still parameters, and a
|
|
519
|
+
// resource name is never one, so the predicate has to accept them or
|
|
520
|
+
// extractResourceFromPath returns the whole segment as the resource.
|
|
521
|
+
const PARAM_SEGMENT_RE = /^\{[^}]+\}$|^:[A-Za-z_]\w*(?:\([^)]*\))?[?*+]?$|^\[\[[^\]]+\]\]$|^\[[^\]]+\]$/;
|
|
522
|
+
export function isParamSegment(segment) {
|
|
523
|
+
return PARAM_SEGMENT_RE.test(segment);
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* One path segment that is an id VALUE rather than a name: a UUID, digits only,
|
|
527
|
+
* or a long hex run (a Mongo ObjectId is 24). A caller writes a literal id in a
|
|
528
|
+
* path where the route declares a parameter, and an id can never be a resource
|
|
529
|
+
* name, so resource derivation must skip it exactly as it skips a parameter.
|
|
530
|
+
* SKYR-4214: an accepted UUID produced the key
|
|
531
|
+
* "DELETE::a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11::integration", which matches
|
|
532
|
+
* nothing, so the candidate escaped dedup entirely.
|
|
533
|
+
*
|
|
534
|
+
* The 16-character floor on the hex rule keeps real resource names safe: a name
|
|
535
|
+
* built only from the letters a-f and digits, 16 characters or longer, is not a
|
|
536
|
+
* word.
|
|
537
|
+
*/
|
|
538
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
539
|
+
export function isOpaqueIdSegment(segment) {
|
|
540
|
+
// The sign belongs to the value: an edge-case step writes `/orders/-1` to
|
|
541
|
+
// probe a negative id, and without the sign that segment became the resource
|
|
542
|
+
// name, so the key could never match real order coverage.
|
|
543
|
+
return UUID_RE.test(segment) || /^-?\d+$/.test(segment) || /^[0-9a-f]{16,}$/i.test(segment);
|
|
544
|
+
}
|
|
545
|
+
/** A segment that names neither a resource nor a sub-resource. */
|
|
546
|
+
function isNonResourceSegment(segment) {
|
|
547
|
+
return isParamSegment(segment) || isOpaqueIdSegment(segment);
|
|
548
|
+
}
|
|
493
549
|
export const SKIP_PATH_SEGMENTS = new Set(["api", "v1", "v2", "v3", "public"]);
|
|
494
550
|
/**
|
|
495
551
|
* True for version/prefix path segments — "v1".."v99" and the literal "api".
|
|
@@ -510,14 +566,15 @@ export function isVersionLikeSegment(segment) {
|
|
|
510
566
|
*/
|
|
511
567
|
export function extractResourceFromPath(endpointPath) {
|
|
512
568
|
const segments = endpointPath.split("/").filter(Boolean);
|
|
513
|
-
const meaningful = segments.filter((s) => !s
|
|
569
|
+
const meaningful = segments.filter((s) => !isNonResourceSegment(s) && !SKIP_PATH_SEGMENTS.has(s));
|
|
514
570
|
if (meaningful.length === 0)
|
|
515
571
|
return "unknown";
|
|
516
|
-
// Sub-resource pattern: /…/<parent
|
|
517
|
-
// Detected when the last raw segment
|
|
572
|
+
// Sub-resource pattern: /…/<parent>/<id>/<child>
|
|
573
|
+
// Detected when the last raw segment names a resource and the one before it
|
|
574
|
+
// does not — an id in any spelling, not only "{param}".
|
|
518
575
|
const last = segments[segments.length - 1];
|
|
519
576
|
const secondLast = segments.length >= 2 ? segments[segments.length - 2] : "";
|
|
520
|
-
if (!last
|
|
577
|
+
if (!isNonResourceSegment(last) && isNonResourceSegment(secondLast) && meaningful.length >= 2) {
|
|
521
578
|
return `${meaningful[meaningful.length - 2]}_${meaningful[meaningful.length - 1]}`;
|
|
522
579
|
}
|
|
523
580
|
return meaningful[meaningful.length - 1];
|
|
@@ -46,7 +46,7 @@ export declare function draftScenariosFromEndpoints(endpoints: Array<EndpointInp
|
|
|
46
46
|
/**
|
|
47
47
|
* Enforce a global cap on drafted scenarios while preserving category diversity.
|
|
48
48
|
*
|
|
49
|
-
* 1. CRITICAL (
|
|
49
|
+
* 1. CRITICAL (bug_caught) scenarios prioritized first.
|
|
50
50
|
* 2. One scenario per non-empty category guaranteed (breadth).
|
|
51
51
|
* 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
|
|
52
52
|
* 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
|
|
@@ -4,6 +4,8 @@ import { CATEGORY_PRIORITY, PriorityTier } from "../types/TestRecommendation.js"
|
|
|
4
4
|
import { inferExpectedStatus } from "./httpDefaults.js";
|
|
5
5
|
import { WorkspaceAuthType } from "./workspaceAuth.js";
|
|
6
6
|
import { deriveResourceToken } from "./importerHop.js";
|
|
7
|
+
import { isSegmentPrefix, pathSuffixMatches } from "./pathMatching.js";
|
|
8
|
+
import { singularize } from "./pluralization.js";
|
|
7
9
|
// Only tokens that are structurally non-testable (meta, infra, static assets).
|
|
8
10
|
// Semantic tokens like login, search, webhook, payment are removed — the LLM
|
|
9
11
|
// already knows how to handle them and the filter was suppressing valid surfaces.
|
|
@@ -19,35 +21,10 @@ function extractExecutionVerb(endpointPath) {
|
|
|
19
21
|
const match = EXECUTION_SUFFIX.exec(endpointPath);
|
|
20
22
|
return match ? match[1].toLowerCase() : "execute";
|
|
21
23
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Returns true when `a` starts with `b` at a path-segment boundary.
|
|
24
|
-
* Plain `startsWith` is not sufficient — "/orders-archive".startsWith("/orders") is true
|
|
25
|
-
* even though they are different resources. Requiring the next character to be "/" or
|
|
26
|
-
* end-of-string ensures only genuine path prefixes are matched.
|
|
27
|
-
*/
|
|
28
|
-
const isSegmentPrefix = (a, b) => a.startsWith(b) && (a[b.length] === "/" || a[b.length] === undefined);
|
|
29
24
|
export function isRealResource(r) {
|
|
30
25
|
return !ACTION_PATTERN.test(r) && !ACTION_VERB_HYPHEN.test(r);
|
|
31
26
|
}
|
|
32
27
|
const SKIP_SEGMENTS = new Set(["api", "v1", "v2", "v3", "public"]);
|
|
33
|
-
/**
|
|
34
|
-
* Convert a plural resource name to its singular form for use in field names
|
|
35
|
-
* and step descriptions.
|
|
36
|
-
*
|
|
37
|
-
* Handles the most common irregular plurals found in REST API paths:
|
|
38
|
-
* -ies → -y (categories → category, companies → company)
|
|
39
|
-
* -ses → -s (statuses → status, classes → class)
|
|
40
|
-
* Falls back to removing the trailing "s" for regular plurals.
|
|
41
|
-
*/
|
|
42
|
-
function singularize(word) {
|
|
43
|
-
if (word.endsWith("ies") && word.length > 3) {
|
|
44
|
-
return word.slice(0, -3) + "y";
|
|
45
|
-
}
|
|
46
|
-
if (word.endsWith("ses") && word.length > 4) {
|
|
47
|
-
return word.slice(0, -2); // statuses → status
|
|
48
|
-
}
|
|
49
|
-
return word.endsWith("s") && word.length > 1 ? word.slice(0, -1) : word;
|
|
50
|
-
}
|
|
51
28
|
/**
|
|
52
29
|
* Extract the primary resource name from an endpoint path.
|
|
53
30
|
* E.g. "/api/v1/flow-costs/{cost_id}" → "flow-costs"
|
|
@@ -145,10 +122,38 @@ export function inferResourceRelationships(endpoints) {
|
|
|
145
122
|
}
|
|
146
123
|
export function draftScenariosFromEndpoints(endpoints, newEndpoints = [], wsAuthType, options = {}, removedEndpoints = []) {
|
|
147
124
|
const scenarios = [];
|
|
125
|
+
// A drafted step path reaches the agent as the plan's `endpoint` and as the
|
|
126
|
+
// URL the generated test calls, so it has to be the spelling an HTTP client
|
|
127
|
+
// accepts. A route scanned from Express or NestJS source arrives as
|
|
128
|
+
// `/members/:uid`; the agent then spends one of its two repair attempts
|
|
129
|
+
// fixing the URL. Measured on eval run 32606742433, fixture
|
|
130
|
+
// cc15-org-reviewer-role: "2-attempt limit reached after URL fix", and the
|
|
131
|
+
// test failed on an unrelated assertion it had no attempt left to correct.
|
|
132
|
+
//
|
|
133
|
+
// Normalize here, at the one place the drafter reads an endpoint, rather than
|
|
134
|
+
// rewriting every stored route: `extractResourceFromPath` already treats both
|
|
135
|
+
// spellings as a parameter, so nothing else in the pipeline needs the change.
|
|
136
|
+
// Every path this function compares must be normalized together. Converting
|
|
137
|
+
// `endpoints` alone made the attack-surface sibling search compare a brace
|
|
138
|
+
// path against a colon `changedEndpoints` entry and find nothing.
|
|
139
|
+
endpoints = endpoints.map((ep) => ({ ...ep, path: toBraceParams(ep.path) }));
|
|
140
|
+
newEndpoints = newEndpoints.map((ep) => ({ ...ep, path: toBraceParams(ep.path) }));
|
|
141
|
+
removedEndpoints = removedEndpoints.map((ep) => ({ ...ep, path: toBraceParams(ep.path) }));
|
|
142
|
+
if (options.changedEndpoints) {
|
|
143
|
+
options = {
|
|
144
|
+
...options,
|
|
145
|
+
changedEndpoints: options.changedEndpoints.map((ep) => ({ ...ep, path: toBraceParams(ep.path) })),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
148
|
const resourceGroups = new Map();
|
|
149
149
|
for (const ep of endpoints) {
|
|
150
150
|
const segments = ep.path.split("/").filter(Boolean);
|
|
151
|
-
|
|
151
|
+
// Was `!s.startsWith("{")` — brace-only, so a colon-style segment became the
|
|
152
|
+
// resource and the drafted scenario was named ":uid-delete-auth-boundary".
|
|
153
|
+
// `isPathParam` is this file's own predicate and already accepts both forms;
|
|
154
|
+
// it deliberately does NOT reject a literal id (see its comment, SKYR-4229),
|
|
155
|
+
// so this changes the param spelling only.
|
|
156
|
+
const nonParamSegs = segments.filter(s => !isPathParam(s) && !SKIP_SEGMENTS.has(s));
|
|
152
157
|
const resource = nonParamSegs[nonParamSegs.length - 1] || "unknown";
|
|
153
158
|
const hasParam = /\{/.test(ep.path);
|
|
154
159
|
const resourceSegIdx = segments.lastIndexOf(resource);
|
|
@@ -221,7 +226,7 @@ const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
|
|
|
221
226
|
/**
|
|
222
227
|
* Enforce a global cap on drafted scenarios while preserving category diversity.
|
|
223
228
|
*
|
|
224
|
-
* 1. CRITICAL (
|
|
229
|
+
* 1. CRITICAL (bug_caught) scenarios prioritized first.
|
|
225
230
|
* 2. One scenario per non-empty category guaranteed (breadth).
|
|
226
231
|
* 3. Remaining budget filled by priority tier (HIGH > MEDIUM > LOW).
|
|
227
232
|
* 4. Hard cap at MAX_TOTAL_SCENARIOS — applied to the combined output.
|
|
@@ -254,9 +259,13 @@ export function capScenarios(scenarios) {
|
|
|
254
259
|
}
|
|
255
260
|
// ── Diff-direct scenario drafting ──
|
|
256
261
|
// Generates targeted scenarios for each new endpoint in the branch diff.
|
|
257
|
-
// These get category "new_endpoint" which maps to the
|
|
258
|
-
//
|
|
259
|
-
//
|
|
262
|
+
// These get category "new_endpoint", which maps to the MEDIUM priority tier in
|
|
263
|
+
// the scorer — BELOW business_rule, security_boundary and the other categories
|
|
264
|
+
// that state what a test proves. They compete for GENERATE slots on merit
|
|
265
|
+
// rather than auto-filling ahead of everything else; CRITICAL is reserved for
|
|
266
|
+
// bug_caught, which targets an actually identified flaw. Level with the
|
|
267
|
+
// structural categories was still too high: the last rank key is the
|
|
268
|
+
// candidateId, so among same-tier candidates the alphabet decided.
|
|
260
269
|
/**
|
|
261
270
|
* Build the minimum steps for a diff-direct integration scenario.
|
|
262
271
|
* Prerequisite resources (e.g. POST /products before POST /orders) are NOT
|
|
@@ -525,9 +534,27 @@ const SOURCE_RESOURCE_SKIP_SEGMENTS = new Set([
|
|
|
525
534
|
...SKIP_SEGMENTS,
|
|
526
535
|
"app", "apps", "handler", "handlers", "index", "page", "pages", "route", "routes", "router", "server", "src",
|
|
527
536
|
]);
|
|
537
|
+
/**
|
|
538
|
+
* `:name` to `{name}`, including an Express/Fastify constraint or modifier
|
|
539
|
+
* (`:id(\d+)`, `:id?`). The match starts at a segment boundary and needs a
|
|
540
|
+
* letter or underscore first, so a port or a mid-segment colon is left alone.
|
|
541
|
+
* Next.js `[id]` stays as written — those mirror file paths.
|
|
542
|
+
*/
|
|
543
|
+
function toBraceParams(path) {
|
|
544
|
+
return (path ?? "").replace(/(^|\/):([A-Za-z_]\w*)(\([^)]*\))?[?*+]?/g, "$1{$2}");
|
|
545
|
+
}
|
|
528
546
|
function pathSegments(path) {
|
|
529
547
|
return path.split("/").filter(Boolean);
|
|
530
548
|
}
|
|
549
|
+
/**
|
|
550
|
+
* Recognises a declared parameter, NOT a literal id value. A UUID, digits or a
|
|
551
|
+
* long hex segment reads as a resource name here, unlike routeParsers.ts's
|
|
552
|
+
* isParamSegment + isOpaqueIdSegment. So `collectionPathForChangedEndpoint`
|
|
553
|
+
* does not reduce `/orders/<uuid>` to `/orders`, and no attack-surface
|
|
554
|
+
* auth-boundary scenario is drafted for that endpoint. Left as-is on purpose:
|
|
555
|
+
* fixing it drafts MORE scenarios, which needs measurement. SKYR-4229 tracks
|
|
556
|
+
* it; pathSegmentClassification.characterization.test.ts pins today's answer.
|
|
557
|
+
*/
|
|
531
558
|
function isPathParam(segment) {
|
|
532
559
|
return /^\{[^}]+\}$/.test(segment) ||
|
|
533
560
|
/^:[A-Za-z_][A-Za-z0-9_]*$/.test(segment) ||
|
|
@@ -733,21 +760,6 @@ export function draftDiffDirectScenarios(newEndpoints, resourceGroups, wsAuthTyp
|
|
|
733
760
|
methods.add(ep.method.toUpperCase());
|
|
734
761
|
grouped.set(ep.path, methods);
|
|
735
762
|
}
|
|
736
|
-
// Segment-boundary suffix match: avoids "/orders" matching "/preorders".
|
|
737
|
-
const pathSuffixMatches = (fullPath, suffix) => {
|
|
738
|
-
if (fullPath === suffix)
|
|
739
|
-
return true;
|
|
740
|
-
const fullSegs = fullPath.split("/").filter(Boolean);
|
|
741
|
-
const suffixSegs = suffix.split("/").filter(Boolean);
|
|
742
|
-
if (suffixSegs.length === 0 || suffixSegs.length > fullSegs.length)
|
|
743
|
-
return false;
|
|
744
|
-
const offset = fullSegs.length - suffixSegs.length;
|
|
745
|
-
for (let i = 0; i < suffixSegs.length; i++) {
|
|
746
|
-
if (fullSegs[offset + i] !== suffixSegs[i])
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
return true;
|
|
750
|
-
};
|
|
751
763
|
for (const [epPath, methods] of grouped) {
|
|
752
764
|
let resource = extractResourceName(epPath);
|
|
753
765
|
let resolvedPath = epPath;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { DraftedScenario, SubjectEndpoint } from "../types/RepositoryAnalysis.js";
|
|
2
|
+
import { ChangedRoute } from "./changedRoutes.js";
|
|
3
|
+
export type { SubjectEndpoint };
|
|
4
|
+
/**
|
|
5
|
+
* The endpoints a scenario tests, in step order.
|
|
6
|
+
*
|
|
7
|
+
* 1. Every step on a changed diff line that names a resource. A test written
|
|
8
|
+
* for a pull request is about what the pull request changed, and a
|
|
9
|
+
* scenario can exercise more than one changed endpoint.
|
|
10
|
+
* 2. If no step matches: the last mutating step that is not a teardown step.
|
|
11
|
+
* Earlier mutations are usually setup (POST /products before PATCH /orders).
|
|
12
|
+
* 3. If no mutating step remains: the last step.
|
|
13
|
+
*
|
|
14
|
+
* Returns an empty list only for a scenario with no steps. Every caller must
|
|
15
|
+
* treat an empty list as "no information", never as "covered".
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveSubjectEndpoints(scenario: DraftedScenario, opts: {
|
|
18
|
+
changedRoutes?: ChangedRoute[];
|
|
19
|
+
}): SubjectEndpoint[];
|