@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.
Files changed (159) hide show
  1. package/build/playwright/registerPlaywrightTools.js +92 -30
  2. package/build/playwright/traceRecordingPrompt.d.ts +6 -0
  3. package/build/playwright/traceRecordingPrompt.js +6 -2
  4. package/build/prompts/code-reuse.d.ts +1 -2
  5. package/build/prompts/code-reuse.js +182 -77
  6. package/build/prompts/modularization/integration-test-modularization.d.ts +2 -0
  7. package/build/prompts/modularization/integration-test-modularization.js +83 -41
  8. package/build/prompts/modularization/render.d.ts +18 -0
  9. package/build/prompts/modularization/render.js +12 -0
  10. package/build/prompts/modularization/ui-test-modularization.d.ts +3 -1
  11. package/build/prompts/modularization/ui-test-modularization.js +89 -47
  12. package/build/prompts/pom-aware-code-reuse.js +3 -1
  13. package/build/prompts/shared-helper-policy.d.ts +57 -0
  14. package/build/prompts/shared-helper-policy.js +135 -0
  15. package/build/prompts/test-recommendation/diffExecutionPlan.js +62 -56
  16. package/build/prompts/test-recommendation/fullRepoCatalog.js +19 -8
  17. package/build/prompts/test-recommendation/recommendationShared.d.ts +28 -6
  18. package/build/prompts/test-recommendation/recommendationShared.js +90 -16
  19. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +22 -0
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +2 -2
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +3 -3
  22. package/build/prompts/testbot/testbot-prompts.js +88 -33
  23. package/build/recommendation/budgeters/shared.js +105 -27
  24. package/build/recommendation/discriminators.js +13 -2
  25. package/build/recommendation/planRanker.d.ts +6 -6
  26. package/build/recommendation/planRanker.js +6 -61
  27. package/build/services/AnalyticsService.d.ts +7 -0
  28. package/build/services/AnalyticsService.js +7 -1
  29. package/build/services/ModularizationService.js +1 -3
  30. package/build/services/TestDiscoveryService.d.ts +0 -2
  31. package/build/services/TestDiscoveryService.js +2 -37
  32. package/build/services/TestGenerationService.d.ts +16 -0
  33. package/build/services/TestGenerationService.js +86 -10
  34. package/build/services/containerEnv.js +13 -12
  35. package/build/tools/code-refactor/codeReuseTool.js +279 -93
  36. package/build/tools/code-refactor/enhance-state.d.ts +49 -0
  37. package/build/tools/code-refactor/enhance-state.js +109 -0
  38. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -1
  39. package/build/tools/code-refactor/modularizationTool.js +9 -2
  40. package/build/tools/code-refactor/reuse-outcome.d.ts +23 -1
  41. package/build/tools/code-refactor/reuse-outcome.js +14 -4
  42. package/build/tools/code-refactor/reuse-state.d.ts +127 -5
  43. package/build/tools/code-refactor/reuse-state.js +628 -16
  44. package/build/tools/code-refactor/utils-verify-gates.d.ts +26 -0
  45. package/build/tools/code-refactor/utils-verify-gates.js +100 -0
  46. package/build/tools/code-refactor/verify-gates.d.ts +2 -1
  47. package/build/tools/code-refactor/verify-gates.js +90 -25
  48. package/build/tools/executeSkyrampTestTool.d.ts +19 -0
  49. package/build/tools/executeSkyrampTestTool.js +158 -8
  50. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +2 -2
  51. package/build/tools/generate-tests/generateE2ERestTool.js +16 -0
  52. package/build/tools/generate-tests/generateUIRestTool.d.ts +1 -0
  53. package/build/tools/generate-tests/generateUIRestTool.js +22 -0
  54. package/build/tools/generate-tests/scenarioLint.d.ts +2 -0
  55. package/build/tools/generate-tests/scenarioLint.js +127 -19
  56. package/build/tools/generate-tests/trace-reuse-guard.d.ts +20 -0
  57. package/build/tools/generate-tests/trace-reuse-guard.js +93 -0
  58. package/build/tools/submitReportTool.d.ts +38 -38
  59. package/build/tools/submitReportTool.js +487 -104
  60. package/build/tools/test-management/analyzeChangesTool.d.ts +24 -1
  61. package/build/tools/test-management/analyzeChangesTool.js +75 -12
  62. package/build/tools/test-management/analyzeTestHealthTool.js +7 -7
  63. package/build/tools/test-management/registerTestPlanTool.d.ts +203 -0
  64. package/build/tools/test-management/registerTestPlanTool.js +149 -23
  65. package/build/types/Recommendation.d.ts +34 -5
  66. package/build/types/RepositoryAnalysis.d.ts +133 -114
  67. package/build/types/RepositoryAnalysis.js +1 -1
  68. package/build/types/ReuseOutcome.d.ts +102 -6
  69. package/build/types/ReuseOutcome.js +16 -2
  70. package/build/types/TestRecommendation.js +21 -3
  71. package/build/types/TestTypes.js +14 -8
  72. package/build/types/TestbotReport.d.ts +25 -3
  73. package/build/types/index.d.ts +2 -2
  74. package/build/types/index.js +1 -1
  75. package/build/utils/AnalysisStateManager.d.ts +69 -1
  76. package/build/utils/AnalysisStateManager.js +69 -5
  77. package/build/utils/branchDiff.d.ts +10 -0
  78. package/build/utils/branchDiff.js +28 -0
  79. package/build/utils/changedRoutes.d.ts +29 -0
  80. package/build/utils/changedRoutes.js +87 -0
  81. package/build/utils/featureFlags.d.ts +21 -0
  82. package/build/utils/featureFlags.js +23 -0
  83. package/build/utils/frontendIntegration.js +34 -4
  84. package/build/utils/importerHop.d.ts +2 -8
  85. package/build/utils/importerHop.js +15 -53
  86. package/build/utils/pathMatching.d.ts +38 -0
  87. package/build/utils/pathMatching.js +71 -0
  88. package/build/utils/pathSignatures.d.ts +22 -0
  89. package/build/utils/pathSignatures.js +57 -0
  90. package/build/utils/planMatchKeys.d.ts +16 -3
  91. package/build/utils/planMatchKeys.js +26 -10
  92. package/build/utils/pluralization.d.ts +10 -0
  93. package/build/utils/pluralization.js +18 -0
  94. package/build/utils/pom-catalog-parse.d.ts +52 -0
  95. package/build/utils/pom-catalog-parse.js +141 -0
  96. package/build/utils/pom-scope/selector-extractor.d.ts +12 -0
  97. package/build/utils/pom-scope/selector-extractor.js +34 -8
  98. package/build/utils/pom-verify/verify.d.ts +6 -5
  99. package/build/utils/pom-verify/verify.js +8 -6
  100. package/build/utils/reportLanguage.d.ts +43 -0
  101. package/build/utils/reportLanguage.js +125 -0
  102. package/build/utils/reportVerification.d.ts +74 -4
  103. package/build/utils/reportVerification.js +259 -3
  104. package/build/utils/reuseRouting.d.ts +3 -0
  105. package/build/utils/reuseRouting.js +50 -0
  106. package/build/utils/routeParsers.d.ts +2 -0
  107. package/build/utils/routeParsers.js +65 -8
  108. package/build/utils/scenarioDrafting.d.ts +1 -1
  109. package/build/utils/scenarioDrafting.js +57 -45
  110. package/build/utils/subjectEndpoints.d.ts +19 -0
  111. package/build/utils/subjectEndpoints.js +98 -0
  112. package/build/utils/testFileClassification.d.ts +11 -0
  113. package/build/utils/testFileClassification.js +47 -0
  114. package/build/utils/uiPageEnumerator.d.ts +45 -19
  115. package/build/utils/uiPageEnumerator.js +95 -51
  116. package/build/utils/utils-verify/allow.d.ts +16 -0
  117. package/build/utils/utils-verify/allow.js +68 -0
  118. package/build/utils/utils-verify/call-sites.d.ts +34 -0
  119. package/build/utils/utils-verify/call-sites.js +154 -0
  120. package/build/utils/utils-verify/index.d.ts +7 -0
  121. package/build/utils/utils-verify/index.js +7 -0
  122. package/build/utils/utils-verify/language-spec.d.ts +91 -0
  123. package/build/utils/utils-verify/language-spec.js +210 -0
  124. package/build/utils/utils-verify/locate.d.ts +39 -0
  125. package/build/utils/utils-verify/locate.js +199 -0
  126. package/build/utils/utils-verify/parse.d.ts +34 -0
  127. package/build/utils/utils-verify/parse.js +177 -0
  128. package/build/utils/utils-verify/stage.d.ts +24 -0
  129. package/build/utils/utils-verify/stage.js +107 -0
  130. package/build/utils/utils-verify/verify.d.ts +63 -0
  131. package/build/utils/utils-verify/verify.js +168 -0
  132. package/build/utils/utils.d.ts +3 -1
  133. package/build/utils/utils.js +3 -1
  134. package/build/workspace/workspace.d.ts +32 -32
  135. package/node_modules/playwright/lib/mcp/skyramp/assertTool.js +9 -5
  136. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +16 -0
  137. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +2 -0
  138. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +115 -14
  139. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +13 -1
  140. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  141. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +2 -0
  142. package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +27 -253
  143. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-DtudTj_v.js → codeMirrorModule-DJMC4zNo.js} +1 -1
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BW82eAUI.js +196 -0
  145. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  146. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-FNMuBzX1.js → codeMirrorModule-CZfp96qZ.js} +1 -1
  147. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-gpLo02E0.js +809 -0
  148. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.Bq1r1URj.js +2 -0
  149. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  150. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.VEfqi1qN.js +5 -0
  151. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  152. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  153. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +1 -1
  154. package/node_modules/playwright/package.json +1 -1
  155. package/package.json +2 -2
  156. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +0 -422
  157. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +0 -1035
  158. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +0 -2
  159. 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
- const nonParamSegs = segments.filter(s => !s.startsWith("{") && !SKIP_SEGMENTS.has(s));
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 (new_endpoint / diff-direct) scenarios prioritized first.
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 CRITICAL priority tier
258
- // in the scorer, so they always fill the GENERATE slots before any structural
259
- // scenario — aligning the plan with what the LLM naturally wants to do anyway.
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
- export type CandidateUiPageStrategy = "framework-route-grep" | "source-grounded-routes" | "dart-go-router" | "root-fallback";
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
- /** Fully qualified URL (workspace baseUrl + framework-derived path). */
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 no service has a usable URL.
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 in order: framework route grep (1) → source-grounded
141
- * routes (2) → root fallback (3). Each strategy is consulted only if the
142
- * previous one produced no candidates. Returns whatever the ladder produces
143
- * (possibly empty if the workspace has no frontend baseUrl AND none of the
144
- * earlier strategies matched).
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[]>;