@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
@@ -1,15 +1,21 @@
1
1
  import { z } from "zod";
2
+ import * as path from "path";
2
3
  import { logger } from "../../utils/logger.js";
3
- import { getCodeReusePrompt, isPomAwareTarget } from "../../prompts/code-reuse.js";
4
- import { selectScopedPoms } from "../../utils/pom-scope/index.js";
4
+ import { toolError } from "../../utils/utils.js";
5
+ import { getCodeReusePrompt } from "../../prompts/code-reuse.js";
6
+ import { isPomAwareTarget } from "../../utils/reuseRouting.js";
7
+ import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
5
8
  import { verifyReuse } from "../../utils/pom-verify/index.js";
6
- import { infraGateFailure, zeroReuseGateFailure, composeVerifyText } from "./verify-gates.js";
7
- import { recordCandidates, recordNoPomLayer, recordVerifyOutcome } from "./reuse-state.js";
8
- import { codeRefactoringSchema, languageSchema, } from "../../types/TestTypes.js";
9
+ import { infraGateFailure, zeroReuseGateFailure, composeVerifyText, } from "./verify-gates.js";
10
+ import { recordCandidates, recordNoPomLayer, recordVerifyOutcome, recordUtilsReuseIssued, recordUtilsVerifyOutcome, recordUtilsVerifyError, pendingModularization, } from "./reuse-state.js";
11
+ import { formatUtilsVerifyText } from "./utils-verify-gates.js";
12
+ import { stageUtilsArtifacts, utilsSpecFor, verifyUtils, } from "../../utils/utils-verify/index.js";
13
+ import { codeRefactoringSchema, languageSchema, TestType, } from "../../types/TestTypes.js";
9
14
  import { SKYRAMP_UTILS_HEADER } from "../../utils/utils.js";
10
15
  import { AnalyticsService } from "../../services/AnalyticsService.js";
11
- import { isPomReuseEnabled } from "../../utils/featureFlags.js";
16
+ import { isPomReuseEnabled, isUtilsReuseEnabled, } from "../../utils/featureFlags.js";
12
17
  import { stageGeneratedPaths } from "../../utils/gitStaging.js";
18
+ import { currentRunStateFile } from "../../utils/AnalysisStateManager.js";
13
19
  const codeReuseSchema = z.object({
14
20
  testFile: z
15
21
  .string()
@@ -24,15 +30,255 @@ const codeReuseSchema = z.object({
24
30
  verify: z
25
31
  .boolean()
26
32
  .default(false)
27
- .describe("Verify a previously written refactored test's POM calls against source instead of returning the reuse prompt"),
33
+ .describe("Verify a previously refactored test instead of returning the reuse prompt. POM path (browser tests): checks the test's POM calls against source; requires the discovery pass — this tool called for the same testFile without `verify` — to have run first. SkyrampUtils path (every other test type): stages the shared utils file for the output commit and checks its invariants (API helpers: one helper per method+path, status-code-only assertions, method+resource names; browser helpers: actions and structural waits only, intent names). Call it after the reuse edits are written; skyramp_enhance_assertions and skyramp_execute_test refuse until it has passed."),
34
+ testType: z
35
+ .nativeEnum(TestType)
36
+ .optional()
37
+ .describe("The type of the test being refactored (shared TestType vocabulary). The tool routes on it server-side: browser types (ui, e2e) may take the POM path; every other type takes the SkyrampUtils path. Omit when unknown."),
28
38
  });
29
39
  const TOOL_NAME = "skyramp_reuse_code";
40
+ /**
41
+ * Specs whose discovery pass has run, keyed by `<run state file>:<resolved spec path>`.
42
+ *
43
+ * Deliberately in memory rather than in the run state file. The question it answers
44
+ * — has discovery run for this spec, in this run? — is intra-run and intra-process
45
+ * (one Testbot run is one MCP server process, the same assumption the run anchor in
46
+ * AnalysisStateManager rests on), and nothing outside this file reads it. Persisting
47
+ * it would put a precondition into a shared read-modify-write blob, where the reuse
48
+ * fan-out's concurrent verify calls can drop records.
49
+ *
50
+ * A server restart mid-run empties this, costing one extra discovery pass for a spec
51
+ * already refactored. That is self-correcting: the refusal IS the discovery pass.
52
+ */
53
+ const discoveredSpecs = new Set();
54
+ /** The key this spec is marked under, or undefined when there is no run to enforce
55
+ * the precondition in — standalone/IDE calls have no run to have recorded a pass in.
56
+ *
57
+ * The run's state file is part of the key rather than merely a gate, so one process
58
+ * that ever serves two runs cannot carry run 1's marks into run 2. A stale mark would
59
+ * stamp a spec that never received the procedure, which is the failure this guard
60
+ * exists to close. */
61
+ function markKeyFor(testFile) {
62
+ const stateFile = currentRunStateFile();
63
+ return stateFile === undefined
64
+ ? undefined
65
+ : `${stateFile}:${path.resolve(testFile)}`;
66
+ }
67
+ /** Prefixed to the discovery result when verify mode is called before it.
68
+ *
69
+ * Leads with `VERIFICATION <STATE>` because every other verify-mode response does —
70
+ * PASSED, FAILED, SKIPPED, ERROR — and the prompts tell the agent to read that line
71
+ * ("finish only when it reports PASSED"). A fresh top-level keyword would be a state
72
+ * it was never taught. NOT RUN rather than SKIPPED: SKIPPED already means "nothing to
73
+ * verify, carry on", which is the opposite action to this one.
74
+ *
75
+ * Worded to fit either branch that follows — the refactoring procedure, or the "no
76
+ * reusable POM layer detected" abstention — and to leave the "verify when you are
77
+ * done" instruction to that content, which already carries it in both branches. */
78
+ const DISCOVER_FIRST_NOTICE = `VERIFICATION NOT RUN — this test file has not been through code-reuse discovery. That pass runs now, and its result follows below.
79
+
80
+ Discovery is per test file: it scopes page-object candidates to this file's own selectors, so discovery done for another spec does not cover this one. Verification confirms only that the page-object calls a spec ALREADY contains resolve against their source — it cannot name the calls the spec should have made. That is why verifying first reports a spec that reuses almost nothing as PASSED.
81
+
82
+ Follow the result below for this test file; it states what to do next.`;
30
83
  // Only advertised when SKYRAMP_FEATURE_POM_REUSE=1 — with the flag off this tool
31
84
  // never takes the POM path, so describing it would promise behavior it won't do.
32
85
  const POM_AWARE_MODE_DESCRIPTION = `
33
86
 
34
87
  **POM-AWARE MODE (TypeScript/JavaScript + Playwright):**
35
88
  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 — on this path that supersedes WORKFLOW SUMMARY step 6.`;
89
+ /** Whether a `verify: true` call must be turned into the discovery pass it skipped.
90
+ *
91
+ * The two modes return very different things — discovery returns the whole refactoring
92
+ * procedure, `verify: true` a one-line stamp — and nothing ordered them, so a spec could
93
+ * be stamped having never received the procedure (measured: 2 of 3 specs in one run). The
94
+ * rule existed as prompt prose and was skipped anyway; it is a precondition here instead.
95
+ *
96
+ * `isPomAwareTarget` also carries the feature flag and the browser-test-type check, so a
97
+ * call that could never have reached the POM procedure is never refused for lacking it.
98
+ * Enforced only inside a run — see `markKeyFor`. */
99
+ function mustDiscoverFirst(params, markKey) {
100
+ return (markKey !== undefined &&
101
+ isPomAwareTarget(params.language, params.framework, params.testType) &&
102
+ !discoveredSpecs.has(markKey));
103
+ }
104
+ /** Verify mode for the SkyrampUtils path: stage the shared utils file(s) this spec
105
+ * wrote or imports — located by header, in code, after the edits exist — then check
106
+ * their invariants and record the verdict that clears the utils debt.
107
+ *
108
+ * Staging comes FIRST and is unconditional: a utils file that fails verification is
109
+ * still the file the test imports, and an unstaged one ships a test that cannot run
110
+ * at all (SKYR-4196 Part B run 2). */
111
+ async function runUtilsVerify(params) {
112
+ if (!isUtilsReuseEnabled()) {
113
+ return {
114
+ content: [
115
+ {
116
+ type: "text",
117
+ text: "VERIFICATION SKIPPED — SkyrampUtils code reuse is disabled (SKYRAMP_FEATURE_UTILS_REUSE). Nothing to verify; continue.",
118
+ },
119
+ ],
120
+ };
121
+ }
122
+ if (!utilsSpecFor(params.language)) {
123
+ // Say so rather than record a NoUtilsFile verdict nothing established.
124
+ return {
125
+ content: [
126
+ {
127
+ type: "text",
128
+ text: `VERIFICATION SKIPPED — shared-helper verification does not cover ${params.language}; nothing was staged or checked. Continue.`,
129
+ },
130
+ ],
131
+ };
132
+ }
133
+ try {
134
+ const staging = await stageUtilsArtifacts(params.testFile, params.language);
135
+ const r = await verifyUtils({
136
+ testFile: params.testFile,
137
+ language: params.language,
138
+ testType: params.testType,
139
+ });
140
+ // A utils file git refused to stage ships a test importing a module the output
141
+ // commit lacks — the exact failure this gate exists for — so it fails the pass
142
+ // whatever the file's invariants say.
143
+ if (staging.failed.length > 0)
144
+ r.ok = false;
145
+ const recorded = await recordUtilsVerifyOutcome(params.testFile, r, params.language, params.framework, params.testType);
146
+ return {
147
+ content: [
148
+ {
149
+ type: "text",
150
+ text: formatUtilsVerifyText(r, {
151
+ staged: staging.staged,
152
+ unstaged: staging.failed,
153
+ recorded,
154
+ }),
155
+ },
156
+ ],
157
+ };
158
+ }
159
+ catch (err) {
160
+ logger.warning("SkyrampUtils verification threw — recording a verify error so the debt fails open", {
161
+ testFile: params.testFile,
162
+ error: String(err),
163
+ });
164
+ await recordUtilsVerifyError(params.testFile);
165
+ return toolError(`VERIFICATION ERROR — the shared-helper verifier could not run: ${String(err)}. This is not something to retry blindly; ` +
166
+ `it does not block skyramp_enhance_assertions or skyramp_execute_test. If the cause is in your files (unreadable utils file), fix it and verify once more.`);
167
+ }
168
+ }
169
+ /** Verify mode: check the page-object calls the delivered spec already contains. */
170
+ async function runVerify(params) {
171
+ // Route on the same predicate the prompt path routes on. A target the POM path
172
+ // never took must not reach the POM verifier — it is the sole writer of the POM
173
+ // reuse summary, and a POM-less run would otherwise emit POM numbers — so every
174
+ // non-POM target, including browser tests with the POM flag off, takes the
175
+ // SkyrampUtils verifier, which is what the prompt path handed it.
176
+ if (!isPomAwareTarget(params.language, params.framework, params.testType)) {
177
+ return runUtilsVerify(params);
178
+ }
179
+ try {
180
+ const r = await verifyReuse(params.testFile, params.language);
181
+ const gate = (await infraGateFailure(params, r)) ??
182
+ (await zeroReuseGateFailure(params, r));
183
+ // Record what this pass established before returning the text report:
184
+ // every number in it is already in hand here, so the agent is never
185
+ // asked to read one back out and retype it later.
186
+ await recordVerifyOutcome(params.testFile, r, gate !== undefined, params.language);
187
+ return {
188
+ content: [{ type: "text", text: composeVerifyText(gate, r) }],
189
+ };
190
+ }
191
+ catch (err) {
192
+ logger.warning("POM verification threw", {
193
+ testFile: params.testFile,
194
+ error: String(err),
195
+ });
196
+ return toolError(`VERIFICATION ERROR (treat as unverified, do not retry more than once): ${String(err)}`);
197
+ }
198
+ }
199
+ /** Discovery mode: the POM-aware refactoring procedure, scoped to this one spec.
200
+ *
201
+ * Both the ordinary discovery call and the refused-verify path go through here, so the
202
+ * workflow a refused verify receives cannot drift from the one a first call receives. */
203
+ async function runDiscovery(params, markKey) {
204
+ logger.info("Analyzing code for reuse opportunities", {
205
+ testFile: params.testFile,
206
+ language: params.language,
207
+ framework: params.framework,
208
+ });
209
+ // Stage so testbot includes the generated files in its output commit.
210
+ await stageGeneratedPaths(params.testFile);
211
+ // Deterministic POM pre-scoping (TS/JS+Playwright browser tests only).
212
+ // Never blocks: any error or empty result falls back to the agent-driven STEP 1.
213
+ let scopedPoms;
214
+ const pomAware = isPomAwareTarget(params.language, params.framework, params.testType);
215
+ // The SkyrampUtils path records its debt here, where the instructions are issued:
216
+ // from this point the tools that follow reuse on this spec refuse until a
217
+ // `verify: true` pass has staged and checked the utils file (SKYR-4220).
218
+ // Only for languages the verifier covers: a debt the verify pass cannot pay (Java —
219
+ // utils reuse is disabled for it, and there is no analyzer) would clear as
220
+ // "no shared utils file" without anything being staged or checked.
221
+ // Gated by the same flag that gates the rest of SkyrampUtils reuse: with it off the
222
+ // testbot prompt never asks for reuse and tells the agent a no-op is normal, so a debt
223
+ // recorded here would block enhance/execute on today's default configuration.
224
+ if (!pomAware && isUtilsReuseEnabled() && utilsSpecFor(params.language)) {
225
+ await recordUtilsReuseIssued(params.testFile, params.language, params.framework, params.testType);
226
+ }
227
+ if (pomAware) {
228
+ try {
229
+ const t0 = Date.now();
230
+ const { tier1, tier2, diagnostics } = await selectScopedPoms({
231
+ testFile: params.testFile,
232
+ specContent: params.prompt,
233
+ });
234
+ if (tier1.length + tier2.length > 0) {
235
+ scopedPoms = { tier1, tier2 };
236
+ // Only report candidates found through the conventional POM globs.
237
+ //
238
+ // The selector-grep fallback also matches ordinary in-repo frontend
239
+ // source — app components legitimately contain the spec's test-ids by
240
+ // construction — so in a repo with no page-object layer at all it still
241
+ // yields "candidates". Reporting those would render, on a POM-less repo,
242
+ // "4 candidate POM files detected" next to zero reuse: each number true
243
+ // in isolation, but together they assert reusable page objects existed
244
+ // and were ignored. `zeroReuseGateFailure` already draws this exact line
245
+ // for the same reason; the report has to draw it too.
246
+ //
247
+ // The agent is still handed the grep-discovered files to consider —
248
+ // only the customer-facing count is withheld.
249
+ if (diagnostics.discovery === "globs") {
250
+ await recordCandidates(params.testFile, tier1.length, params.language);
251
+ }
252
+ }
253
+ else if (diagnostics.selectors > 0 &&
254
+ diagnostics.discovery !== "skipped-too-large") {
255
+ // A clean scan ran (glob or selector-grep) and found zero overlap — distinct from
256
+ // "scoping wasn't attempted" (no selectors) or "scan was skipped" (repo too large).
257
+ scopedPoms = { tier1: [], tier2: [], scannedNoOverlap: true };
258
+ await recordNoPomLayer(params.testFile);
259
+ }
260
+ logger.info("POM pre-scoping", {
261
+ ...diagnostics,
262
+ tier1: tier1.length,
263
+ tier2: tier2.length,
264
+ ms: Date.now() - t0,
265
+ });
266
+ }
267
+ catch (err) {
268
+ logger.warning("POM pre-scoping failed — falling back to agent-driven detection", { error: String(err) });
269
+ }
270
+ }
271
+ const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework, scopedPoms, params.testType);
272
+ // Marked on the last line before the return, so the mark cannot be observed until the
273
+ // workflow it stands for has been built. An earlier mark let a verify arriving mid-scope
274
+ // check a spec that had not been handed the procedure yet.
275
+ //
276
+ // Every scoping outcome reaches this line — hit, empty, or thrown — because a discovery
277
+ // pass that could not clear the precondition would be refused forever.
278
+ if (pomAware && markKey !== undefined)
279
+ discoveredSpecs.add(markKey);
280
+ return codeReusePrompt;
281
+ }
36
282
  export function registerCodeReuseTool(server) {
37
283
  server.registerTool(TOOL_NAME, {
38
284
  description: `Analyzes code for reuse opportunities and enforces code reuse principles.
@@ -42,29 +288,28 @@ export function registerCodeReuseTool(server) {
42
288
  **CRITICAL RULES - VIOLATION WILL RESULT IN ERROR:**
43
289
  1. THIS TOOL MUST BE CALLED IF DURING THE TEST GENERATION THE CODE REUSE FLAG IS SET TO TRUE EXPLICITLY
44
290
  2. DO NOT create new helper functions - only find and reuse existing ones
45
- 3. ONLY create a utils file if ACTUAL helper functions (with signatures) exist in OTHER test files
291
+ 3. ONLY create a utils file when the returned steps say so: either ACTUAL helper functions (with signatures) exist in OTHER test files, or the returned steps include a seeding step that moves the CURRENT test's own helpers (created by skyramp_modularization) into the utils file
46
292
  4. DO NOT create utils files for repetitive patterns - that's refactoring, not reuse
47
293
  5. DO NOT extract patterns from current or other test files - only move existing functions
48
- 6. If other test files are identical/similar with no helper functions, DO NOTHING
294
+ 6. If other test files are identical/similar with no helper functions, DO NOTHING with them — unless the returned steps include a seeding step, in which case the CURRENT test's own helpers are still moved
49
295
  7. Helper functions must have explicit function definitions, not just patterns
50
- 8. If no existing helper functions found, test file remains unchanged
296
+ 8. If no existing helper functions are found AND the returned steps have no seeding step, the test file remains unchanged; with a seeding step, the current test's helpers move to the utils file and are imported back
51
297
 
52
298
 
53
299
  **WORKFLOW SUMMARY:**
54
300
  1. Check existing utils with header ${SKYRAMP_UTILS_HEADER} → reuse if found WITHOUT ANY OTHER CHANGES
55
301
  2. Search OTHER test files (not current test file) for local helpers with header ${SKYRAMP_UTILS_HEADER}
56
302
  3. **ONLY IF** reusable helpers are found in OTHER test files → move to utils and update imports
57
- 4. **IF NO** existing utils or reusable helpers are found in test files then reuse is complete, without creating any files or changes to the test files
303
+ 4. **IF NO** existing utils or reusable helpers are found in test files then reuse is complete, without creating any files or changes to the test files — unless the returned steps include a seeding step for the current test's own helpers
58
304
  5. Verify no duplicate code remains and tests work as expected
59
- 6. **MANDATORY**: After code reuse is complete, proceed to modularization by calling skyramp_modularization tool ONLY for UI, E2E, INTEGRATION or LOAD tests generated from traces.
305
+ 6. The returned steps include a MANDATORY line about skyramp_modularization for this test file follow it (it states whether modularization follows this pass or must not be run again).
60
306
 
61
- **WHEN TO SKIP (DO NOTHING):**
307
+ **WHEN TO SKIP (DO NOTHING) — applies to sibling-file reuse; a seeding step in the returned workflow still runs:**
62
308
  - Other test files only have repetitive patterns, no helper functions
63
309
  - Other test files are essentially identical to current file
64
310
  - No actual function definitions found in other test files
65
311
 
66
- **MANDATORY**: ONLY ALLOW CODE REUSE IF THE IS TRACE BASED FLAG IS SET TO TRUE ELSE DO NOT ALLOW CODE REUSE AND LEAVE THE TEST FILE AS IS.
67
- **CRITICAL**: NON TRACE BASED TESTS ARE ALREADY MODULARIZED AND DO NOT NEED CODE REUSE.
312
+ Applicability is decided by the caller: when a generation result was produced with \`codeReuse: true\`, it directs you here ALWAYS follow through (a no-op result is a normal outcome, never a failure). Multi-step tests (UI, E2E, INTEGRATION, LOAD) are the ones that benefit; simple single-request tests (smoke, contract, fuzz) do not request code reuse.
68
313
  The tool will provide step-by-step instructions that MUST be followed exactly.${isPomReuseEnabled() ? POM_AWARE_MODE_DESCRIPTION : ""}`,
69
314
  inputSchema: codeReuseSchema.shape,
70
315
  _meta: {
@@ -80,98 +325,37 @@ export function registerCodeReuseTool(server) {
80
325
  let errorResult;
81
326
  try {
82
327
  if (params.verify) {
83
- // The verify pass is POM-only, and it is the sole writer of the report's
84
- // reuse summary — so with the flag off a stale instruction to verify must
85
- // not run it, or a POM-less run would still emit POM reuse numbers.
86
- if (!isPomReuseEnabled()) {
328
+ const markKey = markKeyFor(params.testFile);
329
+ if (mustDiscoverFirst(params, markKey)) {
330
+ logger.info("Verify called before the discovery pass returning the discovery workflow", { testFile: params.testFile });
331
+ const workflow = await runDiscovery(params, markKey);
87
332
  return {
88
333
  content: [
89
334
  {
90
335
  type: "text",
91
- text: "VERIFICATION SKIPPED — POM-aware code reuse is disabled. Nothing to verify; continue.",
92
- },
93
- ],
94
- };
95
- }
96
- try {
97
- const r = await verifyReuse(params.testFile, params.language);
98
- const gate = (await infraGateFailure(params, r)) ?? (await zeroReuseGateFailure(params, r));
99
- // Record what this pass established before returning the text report:
100
- // every number in it is already in hand here, so the agent is never
101
- // asked to read one back out and retype it later.
102
- await recordVerifyOutcome(params.testFile, r, gate !== undefined, params.language);
103
- return { content: [{ type: "text", text: composeVerifyText(gate, r) }] };
104
- }
105
- catch (err) {
106
- return {
107
- content: [
108
- {
109
- type: "text",
110
- text: `VERIFICATION ERROR (treat as unverified, do not retry more than once): ${String(err)}`,
336
+ text: `${DISCOVER_FIRST_NOTICE}\n\n---\n\n${workflow}`,
111
337
  },
112
338
  ],
113
339
  };
114
340
  }
341
+ return await runVerify(params);
115
342
  }
116
- logger.info("Analyzing code for reuse opportunities", {
117
- testFile: params.testFile,
118
- language: params.language,
119
- framework: params.framework,
120
- });
121
- // Stage so testbot includes the generated files in its output commit.
122
- await stageGeneratedPaths(params.testFile);
123
- // Deterministic POM pre-scoping (TS/JS+Playwright only). Never blocks:
124
- // any error or empty result falls back to the agent-driven STEP 1.
125
- let scopedPoms;
126
- if (isPomAwareTarget(params.language, params.framework)) {
127
- try {
128
- const t0 = Date.now();
129
- const { tier1, tier2, diagnostics } = await selectScopedPoms({
130
- testFile: params.testFile,
131
- specContent: params.prompt,
132
- });
133
- if (tier1.length + tier2.length > 0) {
134
- scopedPoms = { tier1, tier2 };
135
- // Only report candidates found through the conventional POM globs.
136
- //
137
- // The selector-grep fallback also matches ordinary in-repo frontend
138
- // source — app components legitimately contain the spec's test-ids by
139
- // construction — so in a repo with no page-object layer at all it still
140
- // yields "candidates". Reporting those would render, on a POM-less repo,
141
- // "4 candidate POM files detected" next to zero reuse: each number true
142
- // in isolation, but together they assert reusable page objects existed
143
- // and were ignored. `zeroReuseGateFailure` already draws this exact line
144
- // for the same reason; the report has to draw it too.
145
- //
146
- // The agent is still handed the grep-discovered files to consider —
147
- // only the customer-facing count is withheld.
148
- if (diagnostics.discovery === "globs") {
149
- await recordCandidates(params.testFile, tier1.length);
150
- }
151
- }
152
- else if (diagnostics.selectors > 0 && diagnostics.discovery !== "skipped-too-large") {
153
- // A clean scan ran (glob or selector-grep) and found zero overlap — distinct from
154
- // "scoping wasn't attempted" (no selectors) or "scan was skipped" (repo too large).
155
- scopedPoms = { tier1: [], tier2: [], scannedNoOverlap: true };
156
- await recordNoPomLayer(params.testFile);
157
- }
158
- logger.info("POM pre-scoping", {
159
- ...diagnostics,
160
- tier1: tier1.length,
161
- tier2: tier2.length,
162
- ms: Date.now() - t0,
163
- });
164
- }
165
- catch (err) {
166
- logger.warning("POM pre-scoping failed — falling back to agent-driven detection", { error: String(err) });
343
+ // Modularize-first targets owe skyramp_modularization before reuse is issued —
344
+ // the other half of the pair the hand-off names. Only ever owed for a file
345
+ // generation handed off, so NL callers and POM targets are untouched.
346
+ if (isUtilsReuseEnabled() &&
347
+ !isPomAwareTarget(params.language, params.framework, params.testType)) {
348
+ const owed = await pendingModularization(params.testFile);
349
+ if (owed) {
350
+ errorResult = toolError(owed);
351
+ return errorResult;
167
352
  }
168
353
  }
169
- const codeReusePrompt = getCodeReusePrompt(params.testFile, params.language, params.framework, scopedPoms);
170
354
  return {
171
355
  content: [
172
356
  {
173
357
  type: "text",
174
- text: codeReusePrompt,
358
+ text: await runDiscovery(params, markKeyFor(params.testFile)),
175
359
  },
176
360
  ],
177
361
  };
@@ -195,7 +379,9 @@ export function registerCodeReuseTool(server) {
195
379
  testFile: params.testFile,
196
380
  language: params.language,
197
381
  framework: params.framework,
198
- }).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
382
+ }).catch((err) => {
383
+ logger.warning("Analytics event failed", { error: String(err) });
384
+ });
199
385
  }
200
386
  });
201
387
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * SKYR-4262 — enforcement for the assertion-enhancement step.
3
+ *
4
+ * `skyramp_enhance_assertions` is prompt-returning: it hands the agent
5
+ * instructions and relies on it to Read + Edit the spec. Nothing verified that
6
+ * the file actually changed, and one production run showed an agent calling the
7
+ * tool for 9 generated specs back-to-back, declaring the assertions adequate
8
+ * without reading a single file, and shipping specs whose named behavior
9
+ * (disabled-state checks) was asserted nowhere.
10
+ *
11
+ * Same enforcement shape as SKYR-4115's pendingReuseVerification: record an
12
+ * obligation when the instructions are handed out, check it at the next tool
13
+ * that touches the same spec, fail open on any state error, and make the
14
+ * clearing action one the agent can always perform:
15
+ * - edit the spec (any content change clears the debt), or
16
+ * - document a deliberate no-change decision with a
17
+ * `// assertions reviewed:` comment in the spec — which is itself an edit,
18
+ * so the hash check is the only gate and the marker is the documentation.
19
+ */
20
+ /** Snapshot of a spec at the moment enhancement instructions were handed out. */
21
+ export interface EnhanceRecord {
22
+ /** Absolute path, for basename-collision detection in multi-repo runs. */
23
+ testFilePath?: string;
24
+ /** sha256 of the file content at handout time. */
25
+ sha256?: string;
26
+ /** Number of `expect(` occurrences at handout time — for the block message. */
27
+ expectCount?: number;
28
+ }
29
+ /**
30
+ * Record that enhancement instructions were handed out for `testFile`.
31
+ * Best-effort: a missing run state (non-Testbot use) or unreadable file
32
+ * degrades to "no enforcement", never to a failed tool call.
33
+ */
34
+ export declare function recordEnhanceObligation(testFile: string, explicitStateFile?: string): Promise<void>;
35
+ /**
36
+ * Was this spec handed assertion-enhancement instructions it never acted on?
37
+ *
38
+ * Returns an actionable message when the file is byte-identical to its state at
39
+ * handout time, `undefined` otherwise. The debt clears through the file itself:
40
+ * any edit — adding the missing assertions, or documenting a deliberate
41
+ * no-change decision with a `// assertions reviewed: <reason>` comment —
42
+ * changes the hash. Enforcing "the file changed" rather than "the assertions
43
+ * are good" keeps this a checkpoint, not a judge: assertion quality stays where
44
+ * it belongs, in the enhancement instructions and the report.
45
+ *
46
+ * Fails open on every error path: an unreadable state file or spec is not
47
+ * evidence the agent skipped anything.
48
+ */
49
+ export declare function pendingAssertionEnhancement(testFile: string, explicitStateFile?: string): Promise<string | undefined>;
@@ -0,0 +1,109 @@
1
+ import * as path from "path";
2
+ import { createHash } from "crypto";
3
+ import { readFile } from "fs/promises";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { resolveRunStatePath, StateManager, } from "../../utils/AnalysisStateManager.js";
6
+ const REVIEW_MARKER = "// assertions reviewed:";
7
+ async function fileSnapshot(testFile) {
8
+ try {
9
+ const content = await readFile(testFile, "utf8");
10
+ return {
11
+ content,
12
+ sha256: createHash("sha256").update(content).digest("hex"),
13
+ expectCount: (content.match(/\bexpect\s*\(/g) ?? []).length,
14
+ };
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /**
21
+ * Record that enhancement instructions were handed out for `testFile`.
22
+ * Best-effort: a missing run state (non-Testbot use) or unreadable file
23
+ * degrades to "no enforcement", never to a failed tool call.
24
+ */
25
+ export async function recordEnhanceObligation(testFile, explicitStateFile) {
26
+ const stateFile = resolveRunStatePath(explicitStateFile);
27
+ if (!stateFile)
28
+ return;
29
+ const snapshot = await fileSnapshot(testFile);
30
+ if (!snapshot)
31
+ return;
32
+ try {
33
+ const manager = StateManager.fromStatePath(stateFile);
34
+ const data = await manager.readData();
35
+ if (!data)
36
+ return;
37
+ const key = path.basename(testFile);
38
+ await manager.writeData({
39
+ ...data,
40
+ enhanceOutcomes: {
41
+ ...data.enhanceOutcomes,
42
+ [key]: {
43
+ testFilePath: testFile,
44
+ sha256: snapshot.sha256,
45
+ expectCount: snapshot.expectCount,
46
+ },
47
+ },
48
+ });
49
+ }
50
+ catch (err) {
51
+ logger.warning("Could not record the assertion-enhancement obligation — proceeding", {
52
+ testFile,
53
+ error: String(err),
54
+ });
55
+ }
56
+ }
57
+ /**
58
+ * Was this spec handed assertion-enhancement instructions it never acted on?
59
+ *
60
+ * Returns an actionable message when the file is byte-identical to its state at
61
+ * handout time, `undefined` otherwise. The debt clears through the file itself:
62
+ * any edit — adding the missing assertions, or documenting a deliberate
63
+ * no-change decision with a `// assertions reviewed: <reason>` comment —
64
+ * changes the hash. Enforcing "the file changed" rather than "the assertions
65
+ * are good" keeps this a checkpoint, not a judge: assertion quality stays where
66
+ * it belongs, in the enhancement instructions and the report.
67
+ *
68
+ * Fails open on every error path: an unreadable state file or spec is not
69
+ * evidence the agent skipped anything.
70
+ */
71
+ export async function pendingAssertionEnhancement(testFile, explicitStateFile) {
72
+ try {
73
+ const stateFile = resolveRunStatePath(explicitStateFile);
74
+ if (!stateFile)
75
+ return undefined;
76
+ const data = await StateManager.fromStatePath(stateFile).readData();
77
+ const record = data?.enhanceOutcomes?.[path.basename(testFile)];
78
+ if (!record?.sha256)
79
+ return undefined;
80
+ // enhanceOutcomes is keyed by basename across the whole run; in a multi-repo
81
+ // run two specs can share one record. Blocking a tool on another spec's debt
82
+ // is worse than missing this one, so a mismatch fails open.
83
+ if (record.testFilePath && record.testFilePath !== testFile)
84
+ return undefined;
85
+ const snapshot = await fileSnapshot(testFile);
86
+ if (!snapshot)
87
+ return undefined;
88
+ if (snapshot.sha256 !== record.sha256)
89
+ return undefined;
90
+ if (snapshot.content.includes(REVIEW_MARKER))
91
+ return undefined;
92
+ const spec = path.basename(testFile);
93
+ const countNote = record.expectCount !== undefined
94
+ ? ` It still has the same ${record.expectCount} expect() call${record.expectCount === 1 ? "" : "s"} it was generated with.`
95
+ : "";
96
+ return (`skyramp_enhance_assertions handed out instructions for ${spec} and the file has not changed since.${countNote} ` +
97
+ `Read the file and apply the enhancement instructions — in particular, add expect() assertions for the specific ` +
98
+ `behavior this test is named for (state checks like toBeDisabled/toBeEnabled/toHaveCount, not just replayed text ` +
99
+ `assertions). If after actually reading the file you determine no change is needed, document that in the spec with ` +
100
+ `a \`${REVIEW_MARKER} <what you checked and why it is sufficient>\` comment near the top of the test.`);
101
+ }
102
+ catch (err) {
103
+ logger.warning("Could not check for a pending assertion enhancement — proceeding", {
104
+ testFile,
105
+ error: String(err),
106
+ });
107
+ return undefined;
108
+ }
109
+ }