@skyramp/mcp 0.3.1 → 0.3.2-rc.pom-2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -1,6 +1,10 @@
1
1
  import { z } from "zod";
2
+ import { readFile } from "fs/promises";
3
+ import * as path from "path";
2
4
  import { logger } from "../../utils/logger.js";
3
- import { getCodeReusePrompt } from "../../prompts/code-reuse.js";
5
+ import { getCodeReusePrompt, isPomAwareTarget } from "../../prompts/code-reuse.js";
6
+ import { selectScopedPoms } from "../../utils/pom-scope/index.js";
7
+ import { verifyReuse } from "../../utils/pom-verify/index.js";
4
8
  import { codeRefactoringSchema, languageSchema, } from "../../types/TestTypes.js";
5
9
  import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
6
10
  import { AnalyticsService } from "../../services/AnalyticsService.js";
@@ -14,8 +18,55 @@ const codeReuseSchema = z.object({
14
18
  modularizeCode: codeRefactoringSchema.shape.modularizeCode,
15
19
  prompt: z
16
20
  .string()
21
+ .default("")
17
22
  .describe("The code content to analyze and optimize for code reuse"),
23
+ verify: z
24
+ .boolean()
25
+ .default(false)
26
+ .describe("Verify a previously written refactored test's POM calls against source instead of returning the reuse prompt"),
18
27
  });
28
+ /** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
29
+ export function formatVerifyReport(r) {
30
+ if (r.ok) {
31
+ const lines = [`VERIFICATION PASSED — ${r.checkedCalls} calls checked.`];
32
+ if (r.unverifiable.length > 0) {
33
+ lines.push(`Unverifiable (informational): ${r.unverifiable.join(", ")}`);
34
+ }
35
+ return lines.join("\n");
36
+ }
37
+ const lines = ["VERIFICATION FAILED"];
38
+ for (const v of r.violations) {
39
+ const location = v.searched.length > 1 ? `${v.searched[0]} (+1-hop)` : v.searched[0];
40
+ lines.push(`${v.binding}.${v.member} — not found in ${location}. Available members: ${v.availableMembers.join(", ")}`);
41
+ }
42
+ if (r.parseError) {
43
+ lines.push(`Parse error (fix the syntax and re-verify): ${r.parseError}`);
44
+ }
45
+ lines.push("Each violation may be remapped AT MOST ONCE, and only to a member listed above. If it is flagged again, or you are not confident, restore the raw inline sequence (from your mapping table or `<testFile>.raw.bak`) with a `// kept inline: no matching POM method` comment. Re-run this verification after every fix pass. Do not finish until it reports PASSED.");
46
+ return lines.join("\n\n");
47
+ }
48
+ /** Renders the zero-reuse compliance-gate failure: a spec with 0 verified POM calls whose raw
49
+ * selectors nonetheless overlap tier-1 POM candidates — i.e. reuse was skipped, not unavailable. */
50
+ export function formatZeroReuseGateFailure(tier1) {
51
+ const lines = [
52
+ "VERIFICATION FAILED — this test contains no page-object reuse, but detection found reusable POM candidates for its selectors:",
53
+ ...tier1.map((p) => `- ${p.file} (matched: ${p.matchedTokens.join(", ")})`),
54
+ "Map the matching actions to these files' methods/properties (re-run skyramp_reuse_code for the full workflow if needed). A raw locator that matches a POM property verbatim is evidence TO substitute, never a reason to stay inline. Only finish with raw locators if you add a comment for each file above — `// kept inline: <file basename> — <reason it cannot cover this test>` — then re-run this verification, which will accept the documented files.",
55
+ ];
56
+ return lines.join("\n");
57
+ }
58
+ /** True when `specContent` already carries a `// kept inline: <basename> …` comment line naming
59
+ * this candidate's file — the machine-checkable escape for `formatZeroReuseGateFailure`'s
60
+ * documentation instruction (and the same marker the demote flow already writes). */
61
+ function isKeptInlineDocumented(specContent, file) {
62
+ const basename = path.basename(file).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
63
+ // Anchored full-format match: `// kept inline: <basename> — <non-empty reason>`.
64
+ // The basename must start right after the marker (no substring collisions with
65
+ // longer filenames) and a dash-separated reason is required (a bare basename is
66
+ // not documentation — the reason is what makes the escape PR-reviewable).
67
+ const re = new RegExp(`//\\s*kept inline:\\s*${basename}\\s*[—–-]+\\s*\\S`);
68
+ return specContent.split("\n").some((line) => re.test(line));
69
+ }
19
70
  const TOOL_NAME = "skyramp_reuse_code";
20
71
  export function registerCodeReuseTool(server) {
21
72
  server.registerTool(TOOL_NAME, {
@@ -51,8 +102,8 @@ export function registerCodeReuseTool(server) {
51
102
  **CRITICAL**: NON TRACE BASED TESTS ARE ALREADY MODULARIZED AND DO NOT NEED CODE REUSE.
52
103
  The tool will provide step-by-step instructions that MUST be followed exactly.
53
104
 
54
- **POM-AWARE MODE (TypeScript/Playwright only):**
55
- For projects with \`language: "typescript"\` and \`framework: "playwright"\`, this tool first checks whether the project has an existing Page Object Model (POM) library (\`pageobjects/\` directories or \`*.page.ts\` files). If POMs are found, the test is refactored to use those POM classes and methods — replacing raw inline locators with the existing abstractions. SkyrampUtils consolidation is skipped on this path. If no POMs are found, the tool falls back to the standard SkyrampUtils path. New POM creation is out of scope — existing POMs are reused only. After POM-aware code reuse, do NOT call skyramp_modularization.`,
105
+ **POM-AWARE MODE (TypeScript/JavaScript + Playwright):**
106
+ For projects with \`language: "typescript" or "javascript"\` and \`framework: "playwright"\`, this tool first checks whether the project has an existing Page Object Model (POM) library (\`pageobjects/\` directories or \`*.page.ts\` / \`*.page.js\` files). If POMs are found, the test is refactored to use those POM classes and methods — replacing raw inline locators with the existing abstractions. SkyrampUtils consolidation is skipped on this path. If no POMs are found, the tool falls back to the standard SkyrampUtils path. New POM creation is out of scope — existing POMs are reused only. After POM-aware code reuse, do NOT call skyramp_modularization.`,
56
107
  inputSchema: codeReuseSchema.shape,
57
108
  _meta: {
58
109
  keywords: [
@@ -66,6 +117,52 @@ export function registerCodeReuseTool(server) {
66
117
  }, async (params) => {
67
118
  let errorResult;
68
119
  try {
120
+ if (params.verify) {
121
+ try {
122
+ const r = await verifyReuse(params.testFile, params.language);
123
+ // A spec with 0 checked calls, 0 violations, 0 unverifiable entries, and no parse error
124
+ // still carries all of its raw selectors verbatim, so running detection against it is
125
+ // meaningful — a spec with real bindings (checkedCalls > 0), a deep-chain-only reuse
126
+ // (unverifiable > 0), or broken syntax (parseError) never reaches this branch: those
127
+ // need their own remediation, not the mapping-instructions gate. Scoping errors (or a
128
+ // legitimately POM-less repo, tier1 === []) fall through to the normal PASSED response
129
+ // below: this gate must never turn an infra error into a false compliance failure.
130
+ if (r.checkedCalls === 0 &&
131
+ r.violations.length === 0 &&
132
+ r.unverifiable.length === 0 &&
133
+ !r.parseError &&
134
+ isPomAwareTarget(params.language, params.framework)) {
135
+ try {
136
+ const specContent = await readFile(params.testFile, "utf8");
137
+ const { tier1, diagnostics } = await selectScopedPoms({ testFile: params.testFile, specContent });
138
+ // Only fire on candidates found via conventional POM dirs (globs) — the selector-grep
139
+ // discovery fallback also matches ordinary in-repo frontend source (app components
140
+ // legitimately contain the spec's test-ids by construction), which is not evidence of
141
+ // skipped reuse in a POM-less repo.
142
+ if (diagnostics.discovery === "globs" && tier1.length > 0) {
143
+ const undocumented = tier1.filter((p) => !isKeptInlineDocumented(specContent, p.file));
144
+ if (undocumented.length > 0) {
145
+ return { content: [{ type: "text", text: formatZeroReuseGateFailure(undocumented) }] };
146
+ }
147
+ }
148
+ }
149
+ catch (err) {
150
+ logger.warning("Zero-reuse compliance gate scoping failed — falling back to normal verify report", { error: String(err) });
151
+ }
152
+ }
153
+ return { content: [{ type: "text", text: formatVerifyReport(r) }] };
154
+ }
155
+ catch (err) {
156
+ return {
157
+ content: [
158
+ {
159
+ type: "text",
160
+ text: `VERIFICATION ERROR (treat as unverified, do not retry more than once): ${String(err)}`,
161
+ },
162
+ ],
163
+ };
164
+ }
165
+ }
69
166
  logger.info("Analyzing code for reuse opportunities", {
70
167
  testFile: params.testFile,
71
168
  language: params.language,
@@ -73,7 +170,36 @@ export function registerCodeReuseTool(server) {
73
170
  });
74
171
  // Stage so testbot includes the generated files in its output commit.
75
172
  await stageGeneratedPaths(params.testFile);
76
- const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework);
173
+ // Deterministic POM pre-scoping (TS/JS+Playwright only). Never blocks:
174
+ // any error or empty result falls back to the agent-driven STEP 1.
175
+ let scopedPoms;
176
+ if (isPomAwareTarget(params.language, params.framework)) {
177
+ try {
178
+ const t0 = Date.now();
179
+ const { tier1, tier2, diagnostics } = await selectScopedPoms({
180
+ testFile: params.testFile,
181
+ specContent: params.prompt,
182
+ });
183
+ if (tier1.length + tier2.length > 0) {
184
+ scopedPoms = { tier1, tier2 };
185
+ }
186
+ else if (diagnostics.selectors > 0 && diagnostics.discovery !== "skipped-too-large") {
187
+ // A clean scan ran (glob or selector-grep) and found zero overlap — distinct from
188
+ // "scoping wasn't attempted" (no selectors) or "scan was skipped" (repo too large).
189
+ scopedPoms = { tier1: [], tier2: [], scannedNoOverlap: true };
190
+ }
191
+ logger.info("POM pre-scoping", {
192
+ ...diagnostics,
193
+ tier1: tier1.length,
194
+ tier2: tier2.length,
195
+ ms: Date.now() - t0,
196
+ });
197
+ }
198
+ catch (err) {
199
+ logger.warning("POM pre-scoping failed — falling back to agent-driven detection", { error: String(err) });
200
+ }
201
+ }
202
+ const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework, scopedPoms);
77
203
  return {
78
204
  content: [
79
205
  {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,290 @@
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest } from "@jest/globals";
3
+ import * as fs from "fs";
4
+ import * as os from "os";
5
+ import * as path from "path";
6
+ jest.unstable_mockModule("../../utils/logger.js", () => ({
7
+ logger: { info: jest.fn(), error: jest.fn(), warning: jest.fn(), debug: jest.fn() },
8
+ }));
9
+ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
10
+ AnalyticsService: { pushMCPToolEvent: jest.fn().mockResolvedValue(undefined) },
11
+ }));
12
+ jest.unstable_mockModule("../../utils/gitStaging.js", () => ({
13
+ stageGeneratedPaths: jest.fn().mockResolvedValue(undefined),
14
+ }));
15
+ jest.unstable_mockModule("../../utils/pom-verify/index.js", () => ({
16
+ verifyReuse: jest.fn().mockResolvedValue({ ok: true, violations: [], unverifiable: [], checkedCalls: 3 }),
17
+ }));
18
+ const { registerCodeReuseTool, formatVerifyReport, formatZeroReuseGateFailure } = await import("./codeReuseTool.js");
19
+ const { stageGeneratedPaths } = await import("../../utils/gitStaging.js");
20
+ const { verifyReuse } = await import("../../utils/pom-verify/index.js");
21
+ function captureToolHandler() {
22
+ let handler;
23
+ registerCodeReuseTool({ registerTool: (_n, _o, fn) => { handler = fn; } });
24
+ return handler;
25
+ }
26
+ describe("skyramp_reuse_code — verify mode", () => {
27
+ it("returns the verification report and skips prompt generation/staging when verify:true", async () => {
28
+ const handler = captureToolHandler();
29
+ const result = await handler({
30
+ testFile: "t.spec.ts",
31
+ language: "typescript",
32
+ framework: "playwright",
33
+ modularizeCode: false,
34
+ prompt: "",
35
+ verify: true,
36
+ });
37
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 3 calls checked.");
38
+ expect(verifyReuse).toHaveBeenCalledWith("t.spec.ts", "typescript");
39
+ expect(stageGeneratedPaths).not.toHaveBeenCalled();
40
+ });
41
+ });
42
+ describe("skyramp_reuse_code — verify mode zero-reuse compliance gate", () => {
43
+ it("fails verification when a zero-binding spec's selectors match a fixture POM file", async () => {
44
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
45
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-"));
46
+ try {
47
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
48
+ const pages = path.join(dir, "pages");
49
+ fs.mkdirSync(pages);
50
+ fs.writeFileSync(path.join(pages, "loginPage.js"), `export class LoginPage { constructor(page){ this.signIn = page.locator('[data-test="login-sign-in"]'); } }\n`);
51
+ const testsDir = path.join(dir, "tests");
52
+ fs.mkdirSync(testsDir);
53
+ const testFile = path.join(testsDir, "generated.spec.js");
54
+ fs.writeFileSync(testFile, `await page.locator('[data-test="login-sign-in"]').click();\n`);
55
+ const handler = captureToolHandler();
56
+ const result = await handler({
57
+ testFile,
58
+ language: "javascript",
59
+ framework: "playwright",
60
+ modularizeCode: false,
61
+ prompt: "",
62
+ verify: true,
63
+ });
64
+ expect(result.content[0].text).toContain("VERIFICATION FAILED — this test contains no page-object reuse, but detection found reusable POM candidates for its selectors:");
65
+ expect(result.content[0].text).toContain("loginPage.js");
66
+ }
67
+ finally {
68
+ fs.rmSync(dir, { recursive: true, force: true });
69
+ }
70
+ });
71
+ it("returns the normal PASSED response when a zero-binding spec's selectors only match discovery-fallback app source (no conventional POM dirs)", async () => {
72
+ // A POM-less repo whose in-repo frontend legitimately contains the spec's test-ids (built the
73
+ // markup, not a page-object layer) must not be gated — the selector-grep discovery fallback
74
+ // finding overlap here is not evidence that reuse was skipped.
75
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
76
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-nomatch-"));
77
+ try {
78
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
79
+ fs.mkdirSync(path.join(dir, "src", "components"), { recursive: true });
80
+ fs.writeFileSync(path.join(dir, "src", "components", "LoginForm.jsx"), `export function LoginForm() {\n return (<form>\n <input data-test="login-email" />\n <input data-test="login-password" />\n <button data-test="login-sign-in">Sign in</button>\n </form>);\n}\n`);
81
+ const testsDir = path.join(dir, "tests");
82
+ fs.mkdirSync(testsDir);
83
+ const testFile = path.join(testsDir, "generated.spec.js");
84
+ fs.writeFileSync(testFile, `await page.locator('[data-test="login-email"]').fill('a@b.c');\nawait page.locator('[data-test="login-password"]').fill('x');\nawait page.locator('[data-test="login-sign-in"]').click();\n`);
85
+ const handler = captureToolHandler();
86
+ const result = await handler({
87
+ testFile,
88
+ language: "javascript",
89
+ framework: "playwright",
90
+ modularizeCode: false,
91
+ prompt: "",
92
+ verify: true,
93
+ });
94
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 0 calls checked.");
95
+ }
96
+ finally {
97
+ fs.rmSync(dir, { recursive: true, force: true });
98
+ }
99
+ });
100
+ it("does not fire the gate when a deep-chain-only reuse leaves unverifiable entries (0 checked calls is not 0 reuse)", async () => {
101
+ // Reuse expressed entirely as a 2-hop chain (assetPage.section.method()) is non-strict, so
102
+ // verifyReuse reports it under `unverifiable`, not `checkedCalls`. Without the unverifiable
103
+ // guard this would look identical to a zero-reuse spec and falsely fire the gate.
104
+ verifyReuse.mockResolvedValueOnce({
105
+ ok: true,
106
+ violations: [],
107
+ unverifiable: ["assetPage.section.method"],
108
+ checkedCalls: 0,
109
+ });
110
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-deepchain-"));
111
+ try {
112
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
113
+ const pages = path.join(dir, "pages");
114
+ fs.mkdirSync(pages);
115
+ fs.writeFileSync(path.join(pages, "assetPage.js"), `export class AssetPage { constructor(page){ this.entryBtn = page.locator('[data-test="asset-entry"]'); } }\n`);
116
+ const testsDir = path.join(dir, "tests");
117
+ fs.mkdirSync(testsDir);
118
+ const testFile = path.join(testsDir, "refactored.spec.js");
119
+ fs.writeFileSync(testFile, `await assetPage.section.method();\nawait page.locator('[data-test="asset-entry"]').click();\n`);
120
+ const handler = captureToolHandler();
121
+ const result = await handler({
122
+ testFile,
123
+ language: "javascript",
124
+ framework: "playwright",
125
+ modularizeCode: false,
126
+ prompt: "",
127
+ verify: true,
128
+ });
129
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 0 calls checked.\nUnverifiable (informational): assetPage.section.method");
130
+ }
131
+ finally {
132
+ fs.rmSync(dir, { recursive: true, force: true });
133
+ }
134
+ });
135
+ it("falls through to PASSED when every tier1 candidate is documented with a `// kept inline:` comment", async () => {
136
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
137
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-documented-"));
138
+ try {
139
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
140
+ const pages = path.join(dir, "pages");
141
+ fs.mkdirSync(pages);
142
+ fs.writeFileSync(path.join(pages, "loginPage.js"), `export class LoginPage { constructor(page){ this.signIn = page.locator('[data-test="login-sign-in"]'); } }\n`);
143
+ const testsDir = path.join(dir, "tests");
144
+ fs.mkdirSync(testsDir);
145
+ const testFile = path.join(testsDir, "generated.spec.js");
146
+ fs.writeFileSync(testFile, `// kept inline: loginPage.js — this flow doesn't own the sign-in frame\nawait page.locator('[data-test="login-sign-in"]').click();\n`);
147
+ const handler = captureToolHandler();
148
+ const result = await handler({
149
+ testFile,
150
+ language: "javascript",
151
+ framework: "playwright",
152
+ modularizeCode: false,
153
+ prompt: "",
154
+ verify: true,
155
+ });
156
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 0 calls checked.");
157
+ }
158
+ finally {
159
+ fs.rmSync(dir, { recursive: true, force: true });
160
+ }
161
+ });
162
+ it("does NOT accept a bare-basename kept-inline comment (a reason is required)", async () => {
163
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
164
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-bare-"));
165
+ try {
166
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
167
+ const pages = path.join(dir, "pages");
168
+ fs.mkdirSync(pages);
169
+ fs.writeFileSync(path.join(pages, "loginPage.js"), `export class LoginPage { constructor(page){ this.signIn = page.locator('[data-test="login-sign-in"]'); } }\n`);
170
+ const testsDir = path.join(dir, "tests");
171
+ fs.mkdirSync(testsDir);
172
+ const testFile = path.join(testsDir, "generated.spec.js");
173
+ fs.writeFileSync(testFile,
174
+ // bare basename, no dash-separated reason — the lazy bypass the format check rejects
175
+ `// kept inline: loginPage.js\nawait page.locator('[data-test="login-sign-in"]').click();\n`);
176
+ const handler = captureToolHandler();
177
+ const result = await handler({
178
+ testFile,
179
+ language: "javascript",
180
+ framework: "playwright",
181
+ modularizeCode: false,
182
+ prompt: "",
183
+ verify: true,
184
+ });
185
+ expect(result.content[0].text).toContain("VERIFICATION FAILED — this test contains no page-object reuse");
186
+ }
187
+ finally {
188
+ fs.rmSync(dir, { recursive: true, force: true });
189
+ }
190
+ });
191
+ it("surfaces the parse-error remediation, not the gate text, when the spec has broken syntax", async () => {
192
+ verifyReuse.mockResolvedValueOnce({
193
+ ok: false,
194
+ violations: [],
195
+ unverifiable: [],
196
+ checkedCalls: 0,
197
+ parseError: "SyntaxError: Unexpected end of input",
198
+ });
199
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-parseerror-"));
200
+ try {
201
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
202
+ const pages = path.join(dir, "pages");
203
+ fs.mkdirSync(pages);
204
+ fs.writeFileSync(path.join(pages, "loginPage.js"), `export class LoginPage { constructor(page){ this.signIn = page.locator('[data-test="login-sign-in"]'); } }\n`);
205
+ const testsDir = path.join(dir, "tests");
206
+ fs.mkdirSync(testsDir);
207
+ const testFile = path.join(testsDir, "generated.spec.js");
208
+ fs.writeFileSync(testFile, `await page.locator('[data-test="login-sign-in"]').click(\n`); // unbalanced paren
209
+ const handler = captureToolHandler();
210
+ const result = await handler({
211
+ testFile,
212
+ language: "javascript",
213
+ framework: "playwright",
214
+ modularizeCode: false,
215
+ prompt: "",
216
+ verify: true,
217
+ });
218
+ expect(result.content[0].text).toContain("Parse error (fix the syntax and re-verify): SyntaxError: Unexpected end of input");
219
+ expect(result.content[0].text).not.toContain("VERIFICATION FAILED — this test contains no page-object reuse");
220
+ }
221
+ finally {
222
+ fs.rmSync(dir, { recursive: true, force: true });
223
+ }
224
+ });
225
+ it("never fires the gate for a non-Playwright framework, even with matching POM candidates", async () => {
226
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
227
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "code-reuse-gate-nonplaywright-"));
228
+ try {
229
+ fs.writeFileSync(path.join(dir, "package.json"), "{}");
230
+ const pages = path.join(dir, "pages");
231
+ fs.mkdirSync(pages);
232
+ fs.writeFileSync(path.join(pages, "loginPage.js"), `export class LoginPage { constructor(page){ this.signIn = page.locator('[data-test="login-sign-in"]'); } }\n`);
233
+ const testsDir = path.join(dir, "tests");
234
+ fs.mkdirSync(testsDir);
235
+ const testFile = path.join(testsDir, "generated.spec.js");
236
+ fs.writeFileSync(testFile, `await page.locator('[data-test="login-sign-in"]').click();\n`);
237
+ const handler = captureToolHandler();
238
+ const result = await handler({
239
+ testFile,
240
+ language: "javascript",
241
+ framework: "cypress",
242
+ modularizeCode: false,
243
+ prompt: "",
244
+ verify: true,
245
+ });
246
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 0 calls checked.");
247
+ }
248
+ finally {
249
+ fs.rmSync(dir, { recursive: true, force: true });
250
+ }
251
+ });
252
+ it("falls through to PASSED when POM scoping fails (e.g. testFile in a nonexistent directory)", async () => {
253
+ verifyReuse.mockResolvedValueOnce({ ok: true, violations: [], unverifiable: [], checkedCalls: 0 });
254
+ const testFile = path.join(os.tmpdir(), "code-reuse-gate-does-not-exist", "generated.spec.js");
255
+ const handler = captureToolHandler();
256
+ const result = await handler({
257
+ testFile,
258
+ language: "javascript",
259
+ framework: "playwright",
260
+ modularizeCode: false,
261
+ prompt: "",
262
+ verify: true,
263
+ });
264
+ expect(result.content[0].text).toBe("VERIFICATION PASSED — 0 calls checked.");
265
+ });
266
+ });
267
+ describe("formatVerifyReport", () => {
268
+ it("reports a violation block with the fix-rule paragraph, not the passed message", () => {
269
+ const text = formatVerifyReport({
270
+ ok: false,
271
+ violations: [{ binding: "pm", member: "fakeMethod", searched: ["a.ts", "b.ts"], availableMembers: ["realMethod"] }],
272
+ unverifiable: [],
273
+ checkedCalls: 1,
274
+ });
275
+ expect(text).toContain("VERIFICATION FAILED");
276
+ expect(text).toContain("pm.fakeMethod — not found in a.ts (+1-hop). Available members: realMethod");
277
+ expect(text).toContain("Do not finish until it reports PASSED.");
278
+ });
279
+ });
280
+ describe("formatZeroReuseGateFailure", () => {
281
+ it("lists each tier1 candidate with its matched tokens", () => {
282
+ const text = formatZeroReuseGateFailure([
283
+ { file: "pages/loginPage.js", score: 1, rareHits: 1, matchedTokens: ["login-sign-in", "login-submit"] },
284
+ ]);
285
+ expect(text).toContain("VERIFICATION FAILED — this test contains no page-object reuse, but detection found reusable POM candidates for its selectors:");
286
+ expect(text).toContain("- pages/loginPage.js (matched: login-sign-in, login-submit)");
287
+ expect(text).toContain("A raw locator that matches a POM property verbatim is evidence TO substitute, never a reason to stay inline.");
288
+ expect(text).toContain("Only finish with raw locators if you add a comment for each file above — `// kept inline: <file basename> — <reason it cannot cover this test>` — then re-run this verification, which will accept the documented files.");
289
+ });
290
+ });
@@ -7,7 +7,7 @@ import { makeProgressReporter } from "../utils/progress.js";
7
7
  import { TestExecutionStatus } from "../types/TestExecution.js";
8
8
  import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
9
9
  import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
10
- import { StateManager } from "../utils/AnalysisStateManager.js";
10
+ import { StateManager, getTestsRepoDir } from "../utils/AnalysisStateManager.js";
11
11
  import { TestSource } from "../types/TestAnalysis.js";
12
12
  import { logger } from "../utils/logger.js";
13
13
  import { toolError } from "../utils/utils.js";
@@ -108,10 +108,15 @@ export function registerExecuteSkyrampTestTool(server) {
108
108
  }
109
109
  // Send initial progress
110
110
  await sendProgress(0, 100, "Starting test execution...");
111
+ // Cross-repo run (SKYR-3819): tests delivered under the run's testsRepoDir
112
+ // (set by skyramp_analyze_changes, run-scoped) live outside workspacePath.
113
+ // Thread it through so the executor mounts the test repo and service
114
+ // matching still resolves the SUT baseUrl/dockerNetwork.
115
+ const testRepoPath = getTestsRepoDir();
111
116
  // Always resolve workspace config for dockerNetwork (host networking)
112
117
  // and optionally inject SKYRAMP_TEST_BASE_URL if not already set.
113
118
  if (params.workspacePath) {
114
- const { baseUrl, dockerNetwork, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language);
119
+ const { baseUrl, dockerNetwork, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language, testRepoPath);
115
120
  useHostNetwork = !!dockerNetwork;
116
121
  if (!process.env.SKYRAMP_TEST_BASE_URL) {
117
122
  if (baseUrl) {
@@ -142,6 +147,7 @@ export function registerExecuteSkyrampTestTool(server) {
142
147
  const result = await executionService.executeTest({
143
148
  testFile: params.testFile,
144
149
  workspacePath: params.workspacePath,
150
+ testRepoPath,
145
151
  language: params.language,
146
152
  testType: params.testType,
147
153
  token: effectiveToken,
@@ -6,9 +6,10 @@ export declare const stepSchema: z.ZodEffects<z.ZodObject<{
6
6
  path: z.ZodString;
7
7
  requestBody: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
8
8
  queryParams: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
9
- responseBody: z.ZodOptional<z.ZodString>;
9
+ responseBody: z.ZodEffects<z.ZodOptional<z.ZodString>, string | undefined, string | undefined>;
10
10
  statusCode: z.ZodOptional<z.ZodNumber>;
11
11
  responseHeaders: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodArray<z.ZodString, "many">>>;
12
+ uniqueFields: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
12
13
  }, "strip", z.ZodTypeAny, {
13
14
  path: string;
14
15
  method: HttpMethod;
@@ -17,6 +18,7 @@ export declare const stepSchema: z.ZodEffects<z.ZodObject<{
17
18
  requestBody?: string | undefined;
18
19
  responseBody?: string | undefined;
19
20
  responseHeaders?: Record<string, string[]> | undefined;
21
+ uniqueFields?: string[] | undefined;
20
22
  }, {
21
23
  path: string;
22
24
  method: HttpMethod;
@@ -25,6 +27,7 @@ export declare const stepSchema: z.ZodEffects<z.ZodObject<{
25
27
  requestBody?: string | undefined;
26
28
  responseBody?: string | undefined;
27
29
  responseHeaders?: Record<string, string[]> | undefined;
30
+ uniqueFields?: string[] | undefined;
28
31
  }>, {
29
32
  path: string;
30
33
  method: HttpMethod;
@@ -33,6 +36,7 @@ export declare const stepSchema: z.ZodEffects<z.ZodObject<{
33
36
  requestBody?: string | undefined;
34
37
  responseBody?: string | undefined;
35
38
  responseHeaders?: Record<string, string[]> | undefined;
39
+ uniqueFields?: string[] | undefined;
36
40
  }, {
37
41
  path: string;
38
42
  method: HttpMethod;
@@ -41,5 +45,6 @@ export declare const stepSchema: z.ZodEffects<z.ZodObject<{
41
45
  requestBody?: string | undefined;
42
46
  responseBody?: string | undefined;
43
47
  responseHeaders?: Record<string, string[]> | undefined;
48
+ uniqueFields?: string[] | undefined;
44
49
  }>;
45
50
  export declare function registerBatchScenarioTestTool(server: McpServer): void;