@skyramp/mcp 0.3.4 → 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 +88 -33
- 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 +487 -104
- package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
- package/build/tools/test-management/analyzeChangesTool.js +75 -12
- 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 +149 -23
- 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 +25 -3
- package/build/types/index.d.ts +2 -2
- package/build/types/index.js +1 -1
- package/build/utils/AnalysisStateManager.d.ts +69 -1
- package/build/utils/AnalysisStateManager.js +69 -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/reportLanguage.d.ts +43 -0
- package/build/utils/reportLanguage.js +125 -0
- package/build/utils/reportVerification.d.ts +74 -4
- package/build/utils/reportVerification.js +259 -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
|
@@ -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[];
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { findStepOnChangedRoute } from "./changedRoutes.js";
|
|
2
|
+
import { extractResourceFromPath, isParamSegment } from "./routeParsers.js";
|
|
3
|
+
const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
4
|
+
/**
|
|
5
|
+
* A final-position DELETE that removes a resource an earlier POST created is
|
|
6
|
+
* cleanup, not the subject. SKYR-4214 measured the cost of counting it: the
|
|
7
|
+
* same fixture keyed as POST::flows on one run and DELETE::flows on the next,
|
|
8
|
+
* decided only by whether the model appended a cleanup step.
|
|
9
|
+
*
|
|
10
|
+
* The step count matters. A two-step POST-then-DELETE has no room for a
|
|
11
|
+
* verification step, so the DELETE is what the test is about; the cleanup shape
|
|
12
|
+
* always has a step between the create and the delete. A scenario that asserts
|
|
13
|
+
* the delete worked (…DELETE, then GET 404) leaves the DELETE off the final
|
|
14
|
+
* position, so it is not excluded either.
|
|
15
|
+
*/
|
|
16
|
+
function isTeardownStep(steps, index) {
|
|
17
|
+
const step = steps[index];
|
|
18
|
+
if (index !== steps.length - 1)
|
|
19
|
+
return false;
|
|
20
|
+
if ((step.method ?? "").toUpperCase() !== "DELETE")
|
|
21
|
+
return false;
|
|
22
|
+
if (steps.length < 3)
|
|
23
|
+
return false;
|
|
24
|
+
const resource = extractResourceFromPath(step.path ?? "");
|
|
25
|
+
// "unknown" is the sentinel for a path that names no resource, so comparing
|
|
26
|
+
// it by equality makes any two unidentifiable paths look like the same
|
|
27
|
+
// resource: `POST /` … `DELETE /{id}` read as create-then-cleanup and the
|
|
28
|
+
// DELETE — the real subject — got skipped. Only a named resource can pair.
|
|
29
|
+
if (resource === "unknown")
|
|
30
|
+
return false;
|
|
31
|
+
return steps.slice(0, index).some((earlier) => (earlier.method ?? "").toUpperCase() === "POST" &&
|
|
32
|
+
extractResourceFromPath(earlier.path ?? "") === resource);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* A route declaration made only of parameter segments (or none at all, e.g.
|
|
36
|
+
* "/" or "") names no resource, so it cannot tell one endpoint from another —
|
|
37
|
+
* every step's path ends in the same param segment. SKYR-4214: a changed
|
|
38
|
+
* `@router.delete("/{id}")` line matched a step's trailing DELETE cleanup
|
|
39
|
+
* step just as well as the step it was actually meant to catch, which
|
|
40
|
+
* reinstated the bug this file exists to fix. Rule 1 only accepts a route
|
|
41
|
+
* that contributes at least one non-parameter segment.
|
|
42
|
+
*/
|
|
43
|
+
function routeIdentifiesResource(routePath) {
|
|
44
|
+
return routePath.split("/").filter(Boolean).some((segment) => !isParamSegment(segment));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The endpoints a scenario tests, in step order.
|
|
48
|
+
*
|
|
49
|
+
* 1. Every step on a changed diff line that names a resource. A test written
|
|
50
|
+
* for a pull request is about what the pull request changed, and a
|
|
51
|
+
* scenario can exercise more than one changed endpoint.
|
|
52
|
+
* 2. If no step matches: the last mutating step that is not a teardown step.
|
|
53
|
+
* Earlier mutations are usually setup (POST /products before PATCH /orders).
|
|
54
|
+
* 3. If no mutating step remains: the last step.
|
|
55
|
+
*
|
|
56
|
+
* Returns an empty list only for a scenario with no steps. Every caller must
|
|
57
|
+
* treat an empty list as "no information", never as "covered".
|
|
58
|
+
*/
|
|
59
|
+
export function resolveSubjectEndpoints(scenario, opts) {
|
|
60
|
+
const steps = scenario.steps ?? [];
|
|
61
|
+
if (steps.length === 0)
|
|
62
|
+
return [];
|
|
63
|
+
const changedRoutes = (opts.changedRoutes ?? []).filter((route) => routeIdentifiesResource(route.path));
|
|
64
|
+
if (changedRoutes.length > 0) {
|
|
65
|
+
// Attribute PER ROUTE, not per step. A diff-extracted route is local to its
|
|
66
|
+
// own router declaration — the mount prefix lives in another file, so a bare
|
|
67
|
+
// `POST /items` suffix-matches a step on `/orders/{id}/items` and one on
|
|
68
|
+
// `/products/{id}/items` equally. Recording both would invent a subject the
|
|
69
|
+
// diff does not name, and dedup then needs every unrelated key covered
|
|
70
|
+
// before the candidate can be removed.
|
|
71
|
+
//
|
|
72
|
+
// A route matching exactly one step names that step. A route matching
|
|
73
|
+
// several names none of them: there is no evidence for choosing, and a
|
|
74
|
+
// guess is worse than an omission. Other routes still contribute, so a
|
|
75
|
+
// scenario that genuinely tests two changed endpoints keeps both subjects.
|
|
76
|
+
const attributed = [];
|
|
77
|
+
for (const route of changedRoutes) {
|
|
78
|
+
const matches = steps.filter((step) => findStepOnChangedRoute([step], [route]));
|
|
79
|
+
const distinct = new Map(matches.map((step) => [`${step.method} ${step.path}`, step]));
|
|
80
|
+
if (distinct.size === 1)
|
|
81
|
+
attributed.push([...distinct.values()][0]);
|
|
82
|
+
}
|
|
83
|
+
// Keep step order and drop the duplicates two routes can both name.
|
|
84
|
+
const onChanged = steps.filter((step) => attributed.includes(step));
|
|
85
|
+
if (onChanged.length > 0) {
|
|
86
|
+
return onChanged.map((step) => ({ method: step.method, path: step.path }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (let i = steps.length - 1; i >= 0; i--) {
|
|
90
|
+
if (!MUTATING_METHODS.has((steps[i].method ?? "").toUpperCase()))
|
|
91
|
+
continue;
|
|
92
|
+
if (isTeardownStep(steps, i))
|
|
93
|
+
continue;
|
|
94
|
+
return [{ method: steps[i].method, path: steps[i].path }];
|
|
95
|
+
}
|
|
96
|
+
const last = steps[steps.length - 1];
|
|
97
|
+
return [{ method: last.method, path: last.path }];
|
|
98
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single definition of "discovery treats this path as a test file", shared by
|
|
3
|
+
* discovery itself and by callers that must agree with what discovery classified as
|
|
4
|
+
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
5
|
+
* is external coverage here but is not a test file to `isTestFile` in
|
|
6
|
+
* scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
|
|
7
|
+
*
|
|
8
|
+
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
9
|
+
* separator, which a repo-relative path has not got, so one is prepended.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isDiscoveredTestFile(filePath: string): boolean;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
// Test file naming patterns — match against basename.
|
|
3
|
+
// Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
|
|
4
|
+
// outside a recognized test directory are still discovered.
|
|
5
|
+
const TEST_FILE_PATTERNS = [
|
|
6
|
+
/^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
|
|
7
|
+
/.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
|
|
8
|
+
/.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
|
|
9
|
+
/.*\..*spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.e2e-spec.ts, *.unit-spec.ts
|
|
10
|
+
/.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
|
|
11
|
+
/.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
|
|
12
|
+
/.*_spec\.rb$/, // *_spec.rb (RSpec)
|
|
13
|
+
/.*\.test\.php$/, // *.test.php
|
|
14
|
+
/.*Test\.php$/, // *Test.php (PHPUnit)
|
|
15
|
+
// Skyramp-generated test suffixes — must be matched even outside test dirs
|
|
16
|
+
/.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
|
|
17
|
+
];
|
|
18
|
+
// Directory patterns that signal test-owned files — works with both / and \ separators.
|
|
19
|
+
// Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
|
|
20
|
+
// Playwright, Cypress, and other frameworks for page objects and support code.
|
|
21
|
+
const TEST_DIR_PATTERNS = [
|
|
22
|
+
/[\\/]tests?[\\/]/,
|
|
23
|
+
/[\\/]__tests__[\\/]/,
|
|
24
|
+
/[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
|
|
25
|
+
/[\\/]spec[\\/]/,
|
|
26
|
+
/[\\/]e2e[\\/]/,
|
|
27
|
+
/[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
|
|
28
|
+
/[\\/]playwright[\\/]/, // Playwright fixtures and helpers
|
|
29
|
+
/[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
|
|
30
|
+
/[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
|
|
31
|
+
/[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
|
|
32
|
+
];
|
|
33
|
+
/**
|
|
34
|
+
* The single definition of "discovery treats this path as a test file", shared by
|
|
35
|
+
* discovery itself and by callers that must agree with what discovery classified as
|
|
36
|
+
* external coverage. A narrower predicate silently disagrees: `tests/test_orders.py`
|
|
37
|
+
* is external coverage here but is not a test file to `isTestFile` in
|
|
38
|
+
* scopeAssessment.ts, which only knows the Skyramp and JS/TS spec spellings.
|
|
39
|
+
*
|
|
40
|
+
* Accepts absolute or repo-relative paths. The directory patterns need a leading
|
|
41
|
+
* separator, which a repo-relative path has not got, so one is prepended.
|
|
42
|
+
*/
|
|
43
|
+
export function isDiscoveredTestFile(filePath) {
|
|
44
|
+
if (TEST_FILE_PATTERNS.some((p) => p.test(path.basename(filePath))))
|
|
45
|
+
return true;
|
|
46
|
+
return TEST_DIR_PATTERNS.some((p) => p.test(`/${filePath}`));
|
|
47
|
+
}
|
|
@@ -14,6 +14,12 @@
|
|
|
14
14
|
* Next.js (app/ and pages/), Nuxt / Vue Router file-based, and SvelteKit.
|
|
15
15
|
* Gated on detection of a framework config file in the repo.
|
|
16
16
|
*
|
|
17
|
+
* Strategy 1b — Import graph
|
|
18
|
+
* For a changed file that is not itself a route (a component, a constants
|
|
19
|
+
* module), map the production files that import it through the same
|
|
20
|
+
* filesystem-route mapper. Uses the importer list the production-importer integration
|
|
21
|
+
* check already computed. Merged with strategy 1 rather than tried after it.
|
|
22
|
+
*
|
|
17
23
|
* Strategy 2 — Source-grounded routes
|
|
18
24
|
* Walks router/App files with the TS Compiler API for code-defined
|
|
19
25
|
* route declarations like `<Route path="/cart" element={<Cart/>}>`.
|
|
@@ -26,16 +32,38 @@
|
|
|
26
32
|
* Strategy 3 — Root fallback
|
|
27
33
|
* Read the workspace's frontend baseUrl and treat it as the single
|
|
28
34
|
* candidate page. Always available when a frontend service is configured.
|
|
35
|
+
*
|
|
36
|
+
* When no frontend baseUrl can be resolved, strategies 1/1b still run and their
|
|
37
|
+
* results are returned as bare URL paths (`baseUrlResolved: false`) so the agent
|
|
38
|
+
* can prefix the base URL it discovers; strategies 2–3 need a host and are skipped.
|
|
29
39
|
*/
|
|
30
|
-
|
|
40
|
+
import type { FrontendFileIntegration } from "../types/FrontendIntegration.js";
|
|
41
|
+
export type CandidateUiPageStrategy = "framework-route-grep" | "import-graph" | "source-grounded-routes" | "dart-go-router" | "root-fallback";
|
|
31
42
|
export interface CandidateUiPage {
|
|
32
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Fully qualified URL (workspace baseUrl + framework-derived path), or a bare
|
|
45
|
+
* URL path when `baseUrlResolved` is false.
|
|
46
|
+
*/
|
|
33
47
|
url: string;
|
|
34
48
|
/** Source files that suggested this page (for traceability). */
|
|
35
49
|
sourcedFrom: string[];
|
|
36
50
|
/** Which strategy enumerated it. */
|
|
37
51
|
strategy: CandidateUiPageStrategy;
|
|
52
|
+
/**
|
|
53
|
+
* Present (false) only when no frontend baseUrl could be resolved and `url`
|
|
54
|
+
* is a bare path the agent must prefix. Absent means `url` is complete.
|
|
55
|
+
*/
|
|
56
|
+
baseUrlResolved?: false;
|
|
57
|
+
/** import-graph only: the route files that import `sourcedFrom` and produced this path. */
|
|
58
|
+
via?: string[];
|
|
38
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Upper bound on candidate pages. Strategy 1b is bounded by changed files ×
|
|
62
|
+
* importers, and every candidate costs the agent a navigate + a capture, so the
|
|
63
|
+
* list is cut here — direct route matches first, then import-graph pages that
|
|
64
|
+
* render the most changed files.
|
|
65
|
+
*/
|
|
66
|
+
export declare const MAX_CANDIDATE_PAGES = 10;
|
|
39
67
|
/**
|
|
40
68
|
* File-system → URL mapping for filesystem-routed frameworks.
|
|
41
69
|
*
|
|
@@ -63,15 +91,7 @@ export declare function frameworkFileToUrlPath(filePath: string): string | null;
|
|
|
63
91
|
* and the `frontendFiles[]`-derived candidate frontend roots.
|
|
64
92
|
*/
|
|
65
93
|
export declare function detectsFilesystemRouting(repositoryPath: string): boolean;
|
|
66
|
-
/**
|
|
67
|
-
* Strategy 1 implementation. Filters frontend files through the framework
|
|
68
|
-
* route mapper; collects the URL paths that mapped successfully.
|
|
69
|
-
*
|
|
70
|
-
* Suppressed when the repo doesn't have a recognized filesystem-routing
|
|
71
|
-
* framework config — see `detectsFilesystemRouting`. Without that guard,
|
|
72
|
-
* repos with conventional `pages/` directories but code-defined routes
|
|
73
|
-
* (e.g. React with React Router) produce false-positive URLs.
|
|
74
|
-
*/
|
|
94
|
+
/** Strategy 1 only (no import graph), joined to `baseUrl`. */
|
|
75
95
|
export declare function findCandidatePagesByFrameworkRoute(frontendFiles: string[], baseUrl: string, repositoryPath?: string): CandidateUiPage[];
|
|
76
96
|
/**
|
|
77
97
|
* Strategy 2 implementation. Walks router/App files with the TS Compiler API
|
|
@@ -126,21 +146,27 @@ export declare function findRootFallbackPage(repositoryPath: string): Promise<Ca
|
|
|
126
146
|
* is the picked frontend service's serviceName, sanitized via
|
|
127
147
|
* `sanitizeServiceName`)
|
|
128
148
|
* 2. Global override: `SKYRAMP_TEST_BASE_URL` (single service or all
|
|
129
|
-
* services share one URL)
|
|
149
|
+
* services share one URL). Also honored when the workspace declares no
|
|
150
|
+
* services at all — the action can know the URL without a workspace entry.
|
|
130
151
|
* 3. Static workspace.yml value: `api.baseUrl` from the picked frontend
|
|
131
152
|
* service
|
|
132
153
|
*
|
|
133
|
-
* Returns null when
|
|
154
|
+
* Returns null when neither the environment nor any service has a usable URL.
|
|
134
155
|
*
|
|
135
156
|
* Exported (vs private) so analyze_changes and future tools can reuse the
|
|
136
157
|
* same selection without re-implementing it.
|
|
137
158
|
*/
|
|
138
159
|
export declare function pickFrontendBaseUrl(repositoryPath: string): Promise<string | null>;
|
|
139
160
|
/**
|
|
140
|
-
* Run the strategy ladder
|
|
141
|
-
* routes (2) → root fallback (3). Each
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
161
|
+
* Run the strategy ladder: framework route grep + import graph (1/1b) →
|
|
162
|
+
* source-grounded routes (2) → Dart GoRouter (2.5) → root fallback (3). Each
|
|
163
|
+
* later strategy is consulted only if the earlier ones produced no candidates.
|
|
164
|
+
*
|
|
165
|
+
* `integration` is the production-importer check for the same files; when given,
|
|
166
|
+
* changed non-route files resolve to the pages that import them.
|
|
167
|
+
*
|
|
168
|
+
* Without a resolvable frontend baseUrl only strategies 1/1b can run; their
|
|
169
|
+
* paths are returned with `baseUrlResolved: false` for the agent to prefix.
|
|
170
|
+
* Returns empty only when nothing resolves at all.
|
|
145
171
|
*/
|
|
146
|
-
export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[]): Promise<CandidateUiPage[]>;
|
|
172
|
+
export declare function enumerateCandidateUiPages(repositoryPath: string, frontendFiles: string[], integration?: FrontendFileIntegration[]): Promise<CandidateUiPage[]>;
|