@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
@@ -0,0 +1,26 @@
1
+ import { type UtilsVerifyResult } from "../../utils/utils-verify/index.js";
2
+ /** How many verify rounds the agent may spend before it must document what remains.
3
+ * SKYR-4059: an unbounded repair loop made six execution attempts against a cap of
4
+ * two and destroyed verified reuse in between. The bound is stated in the text the
5
+ * agent reads, not only here. */
6
+ export declare const UTILS_VERIFY_MAX_ROUNDS = 3;
7
+ /**
8
+ * The text the agent receives from a utils-path `verify: true` call. Precedence:
9
+ * violations (with the exact allow line per violation) → malformed markers → PASSED.
10
+ *
11
+ * Every remediation the agent could need is in the text: the file and line of each
12
+ * violation, what to change, the exact comment to paste when the violation is
13
+ * deliberate, and the round bound. A `<placeholder>` the agent must interpolate is
14
+ * how a gate never clears (mcp#716, run 30965653402), so the paste-ready lines carry
15
+ * the real kind and helper name and leave only the reason to fill in.
16
+ */
17
+ export interface UtilsVerifyContext {
18
+ /** Files the verify pass actually staged (empty outside a testbot run). */
19
+ staged?: string[];
20
+ /** Utils files `git add` refused in a testbot run — a failed verification on its own. */
21
+ unstaged?: string[];
22
+ /** Whether the verdict reached run state. When it did not, the checkpoints fail
23
+ * open on the unwritable state file — the agent is told rather than left to loop. */
24
+ recorded?: boolean;
25
+ }
26
+ export declare function formatUtilsVerifyText(r: UtilsVerifyResult, ctx?: UtilsVerifyContext): string;
@@ -0,0 +1,100 @@
1
+ import * as path from "path";
2
+ import { allowMarkerLine, } from "../../utils/utils-verify/index.js";
3
+ /** How many verify rounds the agent may spend before it must document what remains.
4
+ * SKYR-4059: an unbounded repair loop made six execution attempts against a cap of
5
+ * two and destroyed verified reuse in between. The bound is stated in the text the
6
+ * agent reads, not only here. */
7
+ export const UTILS_VERIFY_MAX_ROUNDS = 3;
8
+ /** Paths print as basenames only when every located utils file shares one directory
9
+ * with the file shown; otherwise in full. Two utils files both called
10
+ * `SkyrampUtils.py` under one repo is a designed-for state. */
11
+ function show(file, r) {
12
+ const dirs = new Set(r.utilsFiles.map((f) => path.dirname(f)));
13
+ return dirs.size <= 1 && dirs.has(path.dirname(file))
14
+ ? path.basename(file)
15
+ : file;
16
+ }
17
+ export function formatUtilsVerifyText(r, ctx = {}) {
18
+ const tail = verdictTail(ctx) + unstagedTail(r, ctx) + namingAdvisory(r);
19
+ const spec = r.spec;
20
+ if (r.utilsFiles.length === 0) {
21
+ return ("VERIFICATION PASSED — no shared utils file is written or imported for this test; nothing to verify. " +
22
+ "(If you did create one, it carries no `Generated by Skyramp on …` header line, so it cannot be recognised or staged — add the header and re-verify.)" +
23
+ tail);
24
+ }
25
+ const files = r.utilsFiles.map((f) => show(f, r)).join(", ");
26
+ if (r.ok) {
27
+ const allowed = r.allowed.length > 0
28
+ ? ` ${r.allowed.length} documented decline${r.allowed.length === 1 ? "" : "s"}.`
29
+ : "";
30
+ return (`VERIFICATION PASSED — ${files}: ${r.helpers} helper${r.helpers === 1 ? "" : "s"}, ${r.imported} imported by this test.${allowed} ` +
31
+ `${ctx.staged && ctx.staged.length > 0 ? "The utils file is staged for the output commit. " : ""}Continue.` +
32
+ tail +
33
+ inlineCallSitesAdvisory(r));
34
+ }
35
+ const lines = [];
36
+ if (r.violations.length > 0) {
37
+ lines.push(`VERIFICATION FAILED — the shared utils file violates ${r.violations.length} invariant${r.violations.length === 1 ? "" : "s"}:`);
38
+ for (const v of r.violations) {
39
+ lines.push(`- ${v.kind} · ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`);
40
+ }
41
+ lines.push("", `Fix each violation in the utils file (${r.family.family === "browser" ? "a shared browser helper holds actions and structural waits only; names are the user intent" : "a shared API helper asserts only the status code; one helper per method+path, extended with defaulted parameters; names are method + resource only"}), update any call site whose signature changed, then call skyramp_reuse_code again with the SAME arguments and \`verify: true\`. Re-verify after every fix pass; finish only on PASSED.`, `A violation that is deliberate may be documented instead of fixed. Copy the matching line below into the utils file as a comment, replacing only the <reason…> part — nothing else satisfies the gate:`);
42
+ if (spec) {
43
+ for (const v of r.violations)
44
+ lines.push(` ${allowMarkerLine(spec, v.kind, v.helper)}`);
45
+ }
46
+ lines.push(`Do not spend more than ${UTILS_VERIFY_MAX_ROUNDS} verify rounds on this file: if violations remain after round ${UTILS_VERIFY_MAX_ROUNDS}, document each remaining one with its line above and re-verify once more.`);
47
+ }
48
+ if (r.malformedAllows.length > 0) {
49
+ lines.push(r.violations.length > 0
50
+ ? ""
51
+ : "VERIFICATION FAILED — every violation is documented, but not every marker can be read back.", "Fix these `reuse-verify: allow` lines — the report cannot read them, so the declines they document would be dropped silently:", ...r.malformedAllows.map((m) => ` ${show(m.file, r)}: ${m.line}`), `The form is \`${spec?.commentPrefix ?? "#"} reuse-verify: allow <kind> <helper> — <reason>\` with the reason separated by an em dash or a hyphen with a space on BOTH sides, the kind one of duplicate-helper / body-assertion / scenario-name, and the reason written out (not the <reason…> placeholder).`);
52
+ }
53
+ return lines.join("\n") + tail + inlineCallSitesAdvisory(r);
54
+ }
55
+ /** SKYR-4219: the sibling call-site pass the prompt asks for and the agent skips (0/2
56
+ * seeded eval runs). Stated here because the verify call is the one point every run
57
+ * reaches. Advisory — the sibling is a pre-existing test the maintenance flow owns, so
58
+ * this informs and names the exact edit; it never blocks. */
59
+ function inlineCallSitesAdvisory(r) {
60
+ if (r.inlineCallSites.length === 0)
61
+ return "";
62
+ const utils = r.utilsFiles.length === 1
63
+ ? path.basename(r.utilsFiles[0])
64
+ : "the utils file";
65
+ const n = r.inlineCallSites.length;
66
+ return [
67
+ "",
68
+ "",
69
+ `ADVISORY — ${n} inline request call${n === 1 ? "" : "s"} in other Skyramp-generated tests that a shared helper already wraps:`,
70
+ ...r.inlineCallSites.map((c) => `- ${show(c.file, r)}:${c.line} · ${c.method} ${c.path} · ${utils}.${c.helper}`),
71
+ `Each is a candidate: the request's method, path and call shape match the helper's. Replace the block with a call to the helper only if the remaining differences are literals (they become the arguments; keep the block's response variable; import the helper into that file; change nothing else there), then re-verify. This does not block execution.`,
72
+ ].join("\n");
73
+ }
74
+ function unstagedTail(r, ctx) {
75
+ if (!ctx.unstaged || ctx.unstaged.length === 0)
76
+ return "";
77
+ return (`\n\nVERIFICATION FAILED — ${ctx.unstaged.length === 1 ? "this utils file" : "these utils files"} could not be staged for the output commit: ` +
78
+ ctx.unstaged.map((f) => show(f, r)).join(", ") +
79
+ `. The tests importing it will fail for everyone else. Usually the file is gitignored (check \`git check-ignore -v <path>\`) or lies outside the repository; fix that (move the file, or un-ignore it) and re-verify.`);
80
+ }
81
+ /** Naming is advised, never enforced: a heuristic that blocked execution on
82
+ * `create_test_case` or `list_expired_subscriptions` would be the worse outcome. */
83
+ function namingAdvisory(r) {
84
+ if (r.advisories.length === 0)
85
+ return "";
86
+ const n = r.advisories.length;
87
+ return [
88
+ "",
89
+ "",
90
+ `ADVISORY — ${n} helper name${n === 1 ? "" : "s"} look${n === 1 ? "s" : ""} scenario-flavoured (does not block):`,
91
+ ...r.advisories.map((v) => `- ${show(v.file, r)}:${v.line} · ${v.helper}: ${v.detail}`),
92
+ "Shared helpers are named by method + resource (API) or user intent (browser); scenario values and adjectives belong in arguments. Rename if the name would stop another test from reusing the helper; otherwise continue.",
93
+ ].join("\n");
94
+ }
95
+ function verdictTail(ctx) {
96
+ if (ctx.recorded !== false)
97
+ return "";
98
+ return ("\n\nNOTE — this verdict could not be recorded in the run state (the state file is missing or not writable). " +
99
+ "The checkpoints fail open when the state file cannot be written, so this does not block you; if skyramp_execute_test still refuses, report that rather than re-running verification.");
100
+ }
@@ -13,6 +13,7 @@ import { languageSchema } from "../../types/TestTypes.js";
13
13
  /** The spec a verify call targets — the subset of the tool params the gates need. */
14
14
  export type VerifyTarget = Pick<z.infer<typeof languageSchema>, "language" | "framework"> & {
15
15
  testFile: string;
16
+ testType?: string;
16
17
  };
17
18
  /** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
18
19
  export declare function formatVerifyReport(r: VerifyResult): string;
@@ -22,7 +23,7 @@ export declare function formatVerifyReport(r: VerifyResult): string;
22
23
  export declare function formatInfraGateFailure(): string;
23
24
  /** Renders the zero-reuse compliance-gate failure: a spec with 0 verified POM calls whose raw
24
25
  * selectors nonetheless overlap tier-1 POM candidates — i.e. reuse was skipped, not unavailable. */
25
- export declare function formatZeroReuseGateFailure(tier1: ScopedPom[]): string;
26
+ export declare function formatZeroReuseGateFailure(tier1: ScopedPom[], unparsable?: string[]): string;
26
27
  /** Infrastructure gate: fires regardless of reuse/binding counts — a POM-aware spec whose Skyramp
27
28
  * runtime imports (or the `newSkyrampPlaywrightPage` wrapper) were stripped — e.g. replaced by the
28
29
  * repo's own fixtures under rule 3d's convention-matching — is a hard failure even when its POM
@@ -1,16 +1,18 @@
1
1
  import { readFile } from "fs/promises";
2
2
  import * as path from "path";
3
3
  import { logger } from "../../utils/logger.js";
4
- import { isPomAwareTarget } from "../../prompts/code-reuse.js";
5
- import { selectScopedPoms } from "../../utils/pom-scope/index.js";
4
+ import { isPomAwareTarget } from "../../utils/reuseRouting.js";
5
+ import { selectScopedPoms, } from "../../utils/pom-scope/index.js";
6
6
  import { stripComments, stripStrings } from "../../utils/pom-scope/strip.js";
7
7
  import { escapeRegExp } from "../../utils/regex.js";
8
+ import { findUnparsableKeptInline } from "./reuse-outcome.js";
8
9
  /** Renders a `VerifyResult` (from `verifyReuse`) as the text report returned to the agent. */
9
10
  export function formatVerifyReport(r) {
10
11
  if (r.ok) {
11
12
  const lines = [`VERIFICATION PASSED — ${r.checkedCalls} calls checked.`];
12
- if (r.unverifiable.length > 0) {
13
- lines.push(`Unverifiable (informational): ${r.unverifiable.join(", ")}`);
13
+ const informational = [...r.unverifiableCalls, ...r.unrecognizedBindings];
14
+ if (informational.length > 0) {
15
+ lines.push(`Unverifiable (informational): ${informational.join(", ")}`);
14
16
  }
15
17
  return lines.join("\n");
16
18
  }
@@ -36,35 +38,85 @@ export function formatInfraGateFailure() {
36
38
  }
37
39
  /** Renders the zero-reuse compliance-gate failure: a spec with 0 verified POM calls whose raw
38
40
  * selectors nonetheless overlap tier-1 POM candidates — i.e. reuse was skipped, not unavailable. */
39
- export function formatZeroReuseGateFailure(tier1) {
41
+ export function formatZeroReuseGateFailure(tier1, unparsable = []) {
42
+ if (tier1.length === 0)
43
+ return formatMalformedMarkerFailure(unparsable);
40
44
  const lines = [
41
45
  "VERIFICATION FAILED — this test contains no page-object reuse, but detection found reusable POM candidates for its selectors:",
42
46
  ...tier1.map((p) => `- ${p.file}${p.matchedTokens.length > 0 ? ` (matched: ${p.matchedTokens.join(", ")})` : " (imported by a matched page-object file)"}`),
43
- "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.",
47
+ "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.",
48
+ // The exact marker per candidate rather than a `<file basename>` placeholder: the
49
+ // gate matches on the name it prints here, and an agent asked to interpolate
50
+ // "basename" writes the stem (`channels`) about as often as `channels.ts`, which
51
+ // read as undocumented and left the gate unclearable (run 30965653402).
52
+ "Only finish with raw locators if you document EVERY file above. Copy these lines verbatim into the spec, replacing each <reason...> — nothing else satisfies the gate:",
53
+ ...tier1.map((p) => ` // kept inline: ${path.basename(p.file)} — <reason it cannot cover this test>`),
54
+ "Then re-run this verification, which will accept the documented files.",
44
55
  ];
56
+ if (unparsable.length > 0)
57
+ lines.push(malformedMarkerLines(unparsable).join("\n"));
45
58
  return lines.join("\n");
46
59
  }
60
+ /** The remediation block for markers the gate accepts but `parseKeptInline` cannot read.
61
+ * Shared by both entry points so the corrected form is spelled the same way in each. */
62
+ function malformedMarkerLines(unparsable) {
63
+ return [
64
+ "Also fix these `kept inline:` lines — they satisfy this gate but the report cannot read them, so their declines would be dropped silently:",
65
+ ...unparsable.map((line) => ` ${line}`),
66
+ "The reason must be separated by an em dash, or by a hyphen with a space on BOTH sides: `// kept inline: <file basename> — <reason>`.",
67
+ ];
68
+ }
69
+ /** Renders the malformed-marker failure on its own — every candidate is documented, so the
70
+ * zero-reuse gate itself would clear, and the only remaining defect is a line the report
71
+ * cannot parse. Reported to the agent, which can still fix it, rather than to a log line at
72
+ * report time, which nobody reads in time. */
73
+ function formatMalformedMarkerFailure(unparsable) {
74
+ return [
75
+ "VERIFICATION FAILED — every POM candidate is documented, but not every decline can be read back.",
76
+ ...malformedMarkerLines(unparsable),
77
+ ].join("\n");
78
+ }
47
79
  /** True when `specContent` already carries a `// kept inline: <basename> — <reason>` comment line
48
80
  * naming this candidate's file. This is the one canonical marker format: both instruction sites —
49
81
  * `formatZeroReuseGateFailure` above and `formatVerifyReport`'s demote flow — spell out exactly
50
- * this shape, so a demoted call's comment also satisfies the gate on the re-verify pass. */
82
+ * this shape, so a demoted call's comment also satisfies the gate on the re-verify pass.
83
+ *
84
+ * The extension is optional. `parseKeptInline` — which builds the report's `skipped` list from
85
+ * these same markers — accepts the stem, so requiring it here made the two disagree: a spec
86
+ * documenting `channels` reported all six declines correctly while the gate read every one as
87
+ * undocumented and blocked test execution outright (run 30965653402). A stem is unambiguous
88
+ * anyway; two POM files differing only by extension are not a case worth failing closed on. */
51
89
  function isKeptInlineDocumented(specContent, file) {
52
- const basename = escapeRegExp(path.basename(file));
90
+ const base = path.basename(file);
91
+ const stem = base.slice(0, base.length - path.extname(base).length);
92
+ const name = escapeRegExp(base) +
93
+ (stem && stem !== base ? `|${escapeRegExp(stem)}` : "");
53
94
  // Anchored full-format match: `// kept inline: <basename> — <non-empty reason>`.
54
- // The basename must start right after the marker (no substring collisions with
55
- // longer filenames) and a dash-separated reason is required (a bare basename is
56
- // not documentation — the reason is what makes the escape PR-reviewable).
57
- const re = new RegExp(`//\\s*kept inline:\\s*${basename}\\s*[—–-]+\\s*\\S`);
95
+ // The name must start right after the marker and be followed by the separator, so a
96
+ // stem cannot match a longer filename sharing its prefix. A dash-separated reason is
97
+ // required (a bare basename is not documentation — the reason is what makes the
98
+ // escape PR-reviewable).
99
+ // `(?!<reason)` rejects the placeholder from formatZeroReuseGateFailure's paste-ready
100
+ // line when it was pasted unedited. Accepting it would publish "<reason it cannot cover
101
+ // this test>" to the PR as the stated decline, which is worse than no marker at all.
102
+ const re = new RegExp(`//\\s*kept inline:\\s*(?:${name})\\s*[—–-]+\\s*(?!<reason)\\S`);
58
103
  // String literals are blanked first so a marker embedded in quoted text can't satisfy the
59
104
  // gate — only a real comment counts (standalone or trailing after code; both are legitimate).
60
- return stripStrings(specContent).split("\n").some((line) => re.test(line));
105
+ return stripStrings(specContent)
106
+ .split("\n")
107
+ .some((line) => re.test(line));
61
108
  }
62
- /** Verification ran clean but checked nothing: no verified POM calls, no violations, no deep-chain
63
- * (unverifiable) entries, no parse error. Such a spec still carries all of its raw selectors
64
- * verbatim, so running POM detection against it is meaningful — any other outcome (real bindings,
65
- * deep-chain-only reuse, broken syntax) needs its own remediation, not the mapping-instructions gate. */
109
+ /** Verification ran clean but checked nothing: no verified POM calls, no violations, no unverifiable
110
+ * calls and no unrecognized bindings, no parse error. Such a spec still carries all of its raw
111
+ * selectors verbatim, so running POM detection against it is meaningful — any other outcome (real
112
+ * bindings, deep-chain-only reuse, broken syntax) needs its own remediation, not the
113
+ * mapping-instructions gate. */
66
114
  function isCleanButEmpty(r) {
67
- return r.checkedCalls === 0 && r.violations.length === 0 && r.unverifiable.length === 0 && !r.parseError;
115
+ return (r.checkedCalls === 0 &&
116
+ r.violations.length === 0 &&
117
+ r.unverifiableCalls.length === 0 &&
118
+ r.unrecognizedBindings.length === 0 &&
119
+ !r.parseError);
68
120
  }
69
121
  /** Infrastructure gate: fires regardless of reuse/binding counts — a POM-aware spec whose Skyramp
70
122
  * runtime imports (or the `newSkyrampPlaywrightPage` wrapper) were stripped — e.g. replaced by the
@@ -73,14 +125,17 @@ function isCleanButEmpty(r) {
73
125
  * fire. Broken syntax (parseError) never reaches it: that needs its own remediation
74
126
  * (formatVerifyReport surfaces it), not an infra compliance failure. */
75
127
  export async function infraGateFailure(params, r) {
76
- if (!isPomAwareTarget(params.language, params.framework) || r.parseError)
128
+ if (!isPomAwareTarget(params.language, params.framework, params.testType) ||
129
+ r.parseError)
77
130
  return undefined;
78
131
  let specContent;
79
132
  try {
80
133
  specContent = await readFile(params.testFile, "utf8");
81
134
  }
82
135
  catch (err) {
83
- logger.warning("Verify-mode infra gate skipped — spec unreadable", { error: String(err) });
136
+ logger.warning("Verify-mode infra gate skipped — spec unreadable", {
137
+ error: String(err),
138
+ });
84
139
  return undefined;
85
140
  }
86
141
  // A commented-out import or wrapper call is not live infrastructure, and quoted text mentioning
@@ -88,7 +143,8 @@ export async function infraGateFailure(params, r) {
88
143
  // `'@skyramp/skyramp'` is itself a string literal (must survive), while the wrapper check wants
89
144
  // strings blanked too so `"newSkyrampPlaywrightPage("` in prose can't satisfy it.
90
145
  const commentless = stripComments(specContent);
91
- if (!commentless.includes("@skyramp/skyramp") || !stripStrings(commentless).includes("newSkyrampPlaywrightPage(")) {
146
+ if (!commentless.includes("@skyramp/skyramp") ||
147
+ !stripStrings(commentless).includes("newSkyrampPlaywrightPage(")) {
92
148
  return formatInfraGateFailure();
93
149
  }
94
150
  return undefined;
@@ -99,19 +155,28 @@ export async function infraGateFailure(params, r) {
99
155
  * scoping errors (or a legitimately POM-less repo, tier1 === []) also return undefined: this gate
100
156
  * must never turn an infra error into a false compliance failure. */
101
157
  export async function zeroReuseGateFailure(params, r) {
102
- if (!isCleanButEmpty(r) || !isPomAwareTarget(params.language, params.framework))
158
+ if (!isCleanButEmpty(r) ||
159
+ !isPomAwareTarget(params.language, params.framework, params.testType))
103
160
  return undefined;
104
161
  try {
105
162
  const specContent = await readFile(params.testFile, "utf8");
106
- const { tier1, diagnostics } = await selectScopedPoms({ testFile: params.testFile, specContent });
163
+ const { tier1, diagnostics } = await selectScopedPoms({
164
+ testFile: params.testFile,
165
+ specContent,
166
+ });
107
167
  // Only fire on candidates found via conventional POM dirs (globs) — the selector-grep
108
168
  // discovery fallback also matches ordinary in-repo frontend source (app components
109
169
  // legitimately contain the spec's test-ids by construction), which is not evidence of
110
170
  // skipped reuse in a POM-less repo.
111
171
  if (diagnostics.discovery === "globs" && tier1.length > 0) {
112
172
  const undocumented = tier1.filter((p) => !isKeptInlineDocumented(specContent, p.file));
113
- if (undocumented.length > 0) {
114
- return formatZeroReuseGateFailure(undocumented);
173
+ // A marker this gate accepts but `parseKeptInline` cannot read clears the escape
174
+ // hatch while its decline never reaches the report — the silent loss this feature
175
+ // exists to close. Caught here rather than only logged at report time: the agent is
176
+ // still running and can fix the line; the log reader is not.
177
+ const unparsable = findUnparsableKeptInline(specContent);
178
+ if (undocumented.length > 0 || unparsable.length > 0) {
179
+ return formatZeroReuseGateFailure(undocumented, unparsable);
115
180
  }
116
181
  }
117
182
  }
@@ -1,4 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { TestExecutionResult } from "../types/TestExecution.js";
2
3
  import { TestType } from "../types/TestTypes.js";
3
4
  export declare const CONTRACT_EXECUTION_MODES: readonly ["provider", "consumer"];
4
5
  export type ContractExecutionMode = (typeof CONTRACT_EXECUTION_MODES)[number];
@@ -19,4 +20,22 @@ export declare function shouldInjectSkyrampBaseUrl(testType: TestType, contractM
19
20
  * its own attachment line.
20
21
  */
21
22
  export declare function withVideoInfo(output: string, videoPath?: string): string;
23
+ /**
24
+ * The failure text the agent receives. Everything it needs has to be in here:
25
+ * only a tool's return value reaches the transcript, and Claude Code does not
26
+ * capture an MCP server's stderr, so anything this omits is unrecoverable once
27
+ * the run ends.
28
+ *
29
+ * `output` alone was not enough. When the executor produced none, the message
30
+ * read "Test execution failed:" and stopped — so the agent inferred a cause and
31
+ * reported it as fact. Measured in eval run 32220788735: it blamed an
32
+ * unreachable backend while the app was answering 200 on both the host and the
33
+ * docker bridge, and that invented cause reached `issuesFound`.
34
+ *
35
+ * The 401 hint is emitted only when the output carries a 401 in a status-shaped
36
+ * position — see HTTP_401_SHAPES. It used to be unconditional, which pointed the
37
+ * agent at authentication on a run whose output was empty, and then it keyed off
38
+ * the bare number, which pointed it there on a test count or a line number.
39
+ */
40
+ export declare function buildExecutionFailureText(result: TestExecutionResult): string;
22
41
  export declare function registerExecuteSkyrampTestTool(server: McpServer): void;
@@ -1,13 +1,16 @@
1
1
  import { z } from "zod";
2
+ import { pendingReuseDebt } from "./code-refactor/reuse-state.js";
3
+ import { pendingAssertionEnhancement } from "./code-refactor/enhance-state.js";
4
+ import { stageUtilsArtifacts } from "../utils/utils-verify/index.js";
2
5
  import path from "path";
3
6
  import { stripVTControlCharacters } from "util";
4
7
  import { TestExecutionService } from "../services/TestExecutionService.js";
5
8
  import { AnalyticsService } from "../services/AnalyticsService.js";
6
9
  import { makeProgressReporter } from "../utils/progress.js";
7
- import { TestExecutionStatus } from "../types/TestExecution.js";
10
+ import { TestExecutionStatus, } from "../types/TestExecution.js";
8
11
  import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
9
12
  import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
10
- import { StateManager, getTestsRepoDir } from "../utils/AnalysisStateManager.js";
13
+ import { StateManager, getTestsRepoDir, } from "../utils/AnalysisStateManager.js";
11
14
  import { TestSource } from "../types/TestAnalysis.js";
12
15
  import { logger } from "../utils/logger.js";
13
16
  import { toolError } from "../utils/utils.js";
@@ -39,6 +42,119 @@ export function shouldInjectSkyrampBaseUrl(testType, contractMode) {
39
42
  export function withVideoInfo(output, videoPath) {
40
43
  return videoPath ? `${output}\n\nVideo recording: ${videoPath}` : output;
41
44
  }
45
+ /**
46
+ * Where a real HTTP 401 shows up in runner output. Every entry is a shape taken from
47
+ * actual skyramp_execute_test output in the eval logs, not from guesswork:
48
+ *
49
+ * "status_code": 401, the Skyramp client's per-request JSON block
50
+ * statusCode":401 (backslash-escaped) the same, inside a "response_body" string
51
+ * Received: 401 Playwright's expect diff
52
+ * assert 401 == 201 / assert 404 == 401 pytest assertion rewriting
53
+ * where 401 = ResponseV2(...) pytest's explanation of the value above
54
+ * .status_code == 401 / .toBe(401) the test source echoed in a code frame
55
+ * HTTP/1.1 401 / 401 Unauthorized the status line and the reason phrase
56
+ *
57
+ * The reason phrase counts only next to the number. On its own, "Unauthorized" is
58
+ * as often the name of a test as the answer to one — a Playwright case titled
59
+ * "should return Unauthorized for an expired token" prints that word on a run
60
+ * whose received status was 500.
61
+ *
62
+ * Matching the bare number instead was the defect: `collected 401 items`,
63
+ * `Running 401 tests using 4 workers`, `foo_test.py:401: AssertionError`,
64
+ * `duration: 401 ms` and a path holding `pr-401` all satisfied it, and each one then
65
+ * told the agent the run had failed on auth and to go and ask the user for a token.
66
+ */
67
+ const HTTP_401_SHAPES = [
68
+ // Reason phrase, bound to the number it explains. A serialized error body
69
+ // naming the phrase without a status is covered by the status-field pattern
70
+ // below, which reads the code the response actually carried.
71
+ // Anchored to the start of a line: a status line or a bare reason phrase sits
72
+ // there, while a test TITLE ("should return 401 Unauthorized for an expired
73
+ // token") always has words in front of it and is not evidence of a response.
74
+ /^\s*401\s+unauthori[sz]ed\b/im,
75
+ // Status line, as curl -i and Go's httputil print it.
76
+ /\bHTTP\/[\d.]+\s+401\b/i,
77
+ // A status FIELD set to 401 — the value the response carried. Only `:` is
78
+ // accepted: `== 401` and `= 401` are an assertion or echoed test source, which
79
+ // state what the test WANTED, and a failing test means it did not get it.
80
+ // A line naming `expected` is what the test WANTED, so an expected object
81
+ // (`Expected: {"status": 401}`) alongside `Received: {"status": 500}` is not a
82
+ // 401 the app sent. Reject the line rather than the value.
83
+ /^(?!.*\bexpected\b).*\b(?:status|status[_-]?code|statuscode|code|errorcode)\\?"?\s*:\s*401\b/im,
84
+ // Playwright prints both compared values. `Received` is what the app sent;
85
+ // `Expected` is what the test wanted, so it is not evidence of a 401.
86
+ /^\s*Received:\s*401\b/im,
87
+ // pytest assertion rewriting. A Skyramp-generated test reads
88
+ // `assert response.status_code == N`, so the OBSERVED value is on the left and
89
+ // `assert 404 == 401` means the test wanted 401 and the app sent 404.
90
+ //
91
+ // `skyramp_execute_test` also runs user-authored files, and a suite written
92
+ // the other way round (`assert 401 == resp.status_code`) fails as
93
+ // `assert 401 == 500` — a 500 the app sent, matching these two shapes. The
94
+ // two conventions are textually identical, so nothing here can separate them;
95
+ // the hint's wording carries the uncertainty instead of asserting a 401.
96
+ /\bassert\s+401\s*==/i,
97
+ /\bwhere\s+401\s*=/i,
98
+ ];
99
+ /**
100
+ * The failure text the agent receives. Everything it needs has to be in here:
101
+ * only a tool's return value reaches the transcript, and Claude Code does not
102
+ * capture an MCP server's stderr, so anything this omits is unrecoverable once
103
+ * the run ends.
104
+ *
105
+ * `output` alone was not enough. When the executor produced none, the message
106
+ * read "Test execution failed:" and stopped — so the agent inferred a cause and
107
+ * reported it as fact. Measured in eval run 32220788735: it blamed an
108
+ * unreachable backend while the app was answering 200 on both the host and the
109
+ * docker bridge, and that invented cause reached `issuesFound`.
110
+ *
111
+ * The 401 hint is emitted only when the output carries a 401 in a status-shaped
112
+ * position — see HTTP_401_SHAPES. It used to be unconditional, which pointed the
113
+ * agent at authentication on a run whose output was empty, and then it keyed off
114
+ * the bare number, which pointed it there on a test count or a line number.
115
+ */
116
+ export function buildExecutionFailureText(result) {
117
+ const output = stripVTControlCharacters(result.output || "").trim();
118
+ const facts = [`status=${result.status}`];
119
+ if (typeof result.exitCode === "number")
120
+ facts.push(`exitCode=${result.exitCode}`);
121
+ if (typeof result.duration === "number")
122
+ facts.push(`duration=${result.duration}ms`);
123
+ const sections = [`Test execution failed (${facts.join(", ")}).`];
124
+ const errors = (result.errors ?? []).filter((e) => e && e.trim());
125
+ if (output) {
126
+ sections.push(output);
127
+ }
128
+ else if (errors.length > 0) {
129
+ // The executor reported a cause of its own (e.g. "Docker image setup
130
+ // failed"). Saying the cause cannot be determined would contradict the
131
+ // Errors line below and throw away the only thing known about the failure.
132
+ sections.push("The executor captured no test output, so there are no per-test diagnostics. " +
133
+ "It did report the error below, which is the cause on record — use it. Do " +
134
+ "NOT infer anything the Errors line does not say, and leave the generated " +
135
+ "test unchanged.");
136
+ }
137
+ else {
138
+ sections.push("The executor captured no output, so this failure carries no diagnostics of " +
139
+ "its own and the cause cannot be determined from it. The test runner, the " +
140
+ "test process, or the application could each have died silently. Do NOT " +
141
+ "report the app as unreachable or misconfigured on this basis — nothing " +
142
+ "here shows that. Check any Errors line below and that the test runner " +
143
+ "started, then retry once; if it fails the same way, report the execution " +
144
+ "failure with its cause unresolved and leave the generated test unchanged.");
145
+ }
146
+ if (errors.length > 0)
147
+ sections.push(`Errors: ${errors.join("; ")}`);
148
+ if (HTTP_401_SHAPES.some((shape) => shape.test(output))) {
149
+ sections.push("The output carries a 401 in a status position. If the app returned it, ask " +
150
+ "the user for the authentication token (or where to read it from) and " +
151
+ "retry. Check which side of the comparison the 401 sits on first: a suite " +
152
+ "that writes the expected value on the left fails as `assert 401 == 500`, " +
153
+ "and there the 401 is what the test wanted, not what it got. Leave the " +
154
+ "generated test file unchanged either way.");
155
+ }
156
+ return sections.join("\n\n");
157
+ }
42
158
  export function registerExecuteSkyrampTestTool(server) {
43
159
  server.registerTool(TOOL_NAME, {
44
160
  description: `Execute a Skyramp-generated test in isolated containerized environments for reliable, deterministic testing. Call this once a test file exists on disk (from a skyramp_*_test_generation tool). First-time execution may take longer while Docker images download — this is expected, not a failure.`,
@@ -111,6 +227,17 @@ export function registerExecuteSkyrampTestTool(server) {
111
227
  errorResult = toolError(`testFile must be an absolute path, got: ${params.testFile}`);
112
228
  return errorResult;
113
229
  }
230
+ // SKYR-4262: block execution when skyramp_enhance_assertions handed out
231
+ // instructions for this spec and the file has not changed since — the
232
+ // agent acknowledged the enhancement step without acting on it. The debt
233
+ // clears through the file itself (any edit, including a documented
234
+ // `// assertions reviewed:` no-change decision), so the agent can never
235
+ // be stuck here. Same enforcement shape as the reuse checkpoint above.
236
+ const owedEnhancement = await pendingAssertionEnhancement(params.testFile, params.stateFile);
237
+ if (owedEnhancement) {
238
+ errorResult = toolError(owedEnhancement);
239
+ return errorResult;
240
+ }
114
241
  // Deterministic external-test guard (SKYR-3924): this tool runs Skyramp-generated
115
242
  // tests in the executor and cannot run a repo's native (user-written) suite, so a
116
243
  // run on an external test only errors (e.g. pytest import/collection failure).
@@ -139,6 +266,25 @@ export function registerExecuteSkyrampTestTool(server) {
139
266
  logger.warning(`External-test guard could not read stateFile (${err.message}); proceeding with execution`);
140
267
  }
141
268
  }
269
+ // SKYR-4220: stage the shared utils file (see stageUtilsArtifacts) — execution is
270
+ // the last step before delivery, and a utils file left unstaged here ships a
271
+ // test importing a module the PR lacks. After the external-test skip: a native
272
+ // suite this tool will not run gets no scan and no staging.
273
+ await stageUtilsArtifacts(params.testFile);
274
+ // SKYR-4115 backstop: enhance_assertions carries the same check, but nothing
275
+ // guarantees the agent calls it, and execution is the last step that still
276
+ // precedes reporting. See pendingReuseVerification for why enforcing the verify
277
+ // CALL needs no retry budget.
278
+ //
279
+ // Pass params.stateFile through: without it the state path resolves from the
280
+ // CI/Testbot anchor alone and the check returns early — failing OPEN — for a
281
+ // caller that supplies a valid stateFile outside CI. The external-test guard
282
+ // below already reads params.stateFile, so this one honours the same input.
283
+ const owedReuseVerification = await pendingReuseDebt(params.testFile, params.stateFile, params.testType);
284
+ if (owedReuseVerification) {
285
+ errorResult = toolError(owedReuseVerification);
286
+ return errorResult;
287
+ }
142
288
  // Send initial progress
143
289
  await sendProgress(5, 100, "Starting test execution...");
144
290
  // Cross-repo run (SKYR-3819): tests delivered under the run's testsRepoDir
@@ -172,7 +318,9 @@ export function registerExecuteSkyrampTestTool(server) {
172
318
  }
173
319
  const executionService = new TestExecutionService();
174
320
  const effectiveToken = resolveEffectiveToken(params.unauthenticated, params.token, process.env.SKYRAMP_TEST_TOKEN);
175
- if (!effectiveToken && !params.unauthenticated && params.token === undefined) {
321
+ if (!effectiveToken &&
322
+ !params.unauthenticated &&
323
+ params.token === undefined) {
176
324
  logger.warning("No auth token available — authenticated endpoints will likely return 401. Set SKYRAMP_TEST_TOKEN or pass token/unauthenticated.");
177
325
  }
178
326
  // Execute test with progress callback - reports Docker cache/pull status.
@@ -223,7 +371,9 @@ export function registerExecuteSkyrampTestTool(server) {
223
371
  else {
224
372
  stateData.existingTests[testIndex].executionAfter = result;
225
373
  }
226
- await stateManager.writeRepoData(stateData, { repo: params.repository });
374
+ await stateManager.writeRepoData(stateData, {
375
+ repo: params.repository,
376
+ });
227
377
  logger.info(`Updated stateFile with execution results for ${params.testFile}`);
228
378
  }
229
379
  else {
@@ -244,9 +394,7 @@ export function registerExecuteSkyrampTestTool(server) {
244
394
  // Progress is already reported by TestExecutionService
245
395
  // Only report final status if not already at 100%
246
396
  if (result.status !== TestExecutionStatus.Pass) {
247
- errorResult = toolError(withVideoInfo(`Test execution failed: ${stripVTControlCharacters(result.output || "")}
248
-
249
- If this failed with a 401 status code, ask the user for the authentication token (or where to read it from) and retry — leave the generated test file unchanged.`, result.videoPath));
397
+ errorResult = toolError(withVideoInfo(buildExecutionFailureText(result), result.videoPath));
250
398
  return errorResult;
251
399
  }
252
400
  // Success - progress already reported by TestExecutionService
@@ -277,7 +425,9 @@ If this failed with a 401 status code, ask the user for the authentication token
277
425
  workspacePath: params.workspacePath,
278
426
  language: params.language,
279
427
  testType: params.testType,
280
- }).catch((err) => { logger.warning("Analytics event failed", { error: String(err) }); });
428
+ }).catch((err) => {
429
+ logger.warning("Analytics event failed", { error: String(err) });
430
+ });
281
431
  }
282
432
  });
283
433
  }
@@ -481,9 +481,9 @@ Call \`skyramp_integration_test_generation\` with the returned \`scenarioFile\`
481
481
  responseBody: t.ResponseBody,
482
482
  })));
483
483
  if (chainLint.errors.length > 0) {
484
- return toolError(`**Scenario step-ordering errors**\n\n`
484
+ return toolError(`**Scenario chaining errors**\n\n`
485
485
  + chainLint.errors.map((e) => `- ${e}`).join("\n")
486
- + `\n\nReorder or fix the steps and resubmit the full batch.`);
486
+ + `\n\nApply the remedy each error names and resubmit the full batch.`);
487
487
  }
488
488
  const chainLintWarning = chainLint.warnings.length > 0
489
489
  ? `\n\n⚠️ **Chaining warnings** — the generated test may hardcode these instead of chaining:\n`
@@ -2,11 +2,20 @@ import { z } from "zod";
2
2
  import { baseSchema, baseTraceSchema, TestType, codeRefactoringSchema, } from "../../types/TestTypes.js";
3
3
  import { TestGenerationService, } from "../../services/TestGenerationService.js";
4
4
  import { AnalyticsService } from "../../services/AnalyticsService.js";
5
+ import { toolError } from "../../utils/utils.js";
6
+ import { checkTraceReuse, recordTraceOutput } from "./trace-reuse-guard.js";
5
7
  const e2eTestSchema = {
6
8
  ...baseTraceSchema.shape,
7
9
  playwrightInput: z
8
10
  .string()
9
11
  .describe("MUST be absolute path to the playwright input file like /path/to/playwright-ui-test.zip and MUST be a zip file captured using start_trace_collection tool"),
12
+ allowTraceReuse: z
13
+ .boolean()
14
+ .default(false)
15
+ .describe("Explicit override for the trace-reuse guard. By default, generating a second differently-named test " +
16
+ "from a Playwright zip that already produced one is rejected — one recording covers one scenario, and reusing " +
17
+ "it manufactures identical tests under names the recording never covered. Set true only when the same " +
18
+ "recording genuinely covers the new scenario end-to-end."),
10
19
  ...codeRefactoringSchema.shape,
11
20
  ...baseSchema.shape,
12
21
  };
@@ -42,8 +51,15 @@ If \`trace\` parameter is provided (path to a trace file), DO NOT pass \`apiSche
42
51
  keywords: ["e2e test", "end-to-end test"],
43
52
  },
44
53
  }, async (params) => {
54
+ // SKYR-4262: same trace-reuse guard as skyramp_ui_test_generation — this
55
+ // tool consumes the same Playwright zips, so the same fan-out applies.
56
+ const traceReuse = await checkTraceReuse(params.playwrightInput, params.output, params.outputDir, !!params.allowTraceReuse, "e2e");
57
+ if (traceReuse.rejection)
58
+ return toolError(traceReuse.rejection);
45
59
  const service = new E2ETestService();
46
60
  const result = await service.generateTest(params);
61
+ if (!result.isError && traceReuse.key && traceReuse.identity)
62
+ recordTraceOutput(traceReuse.key, traceReuse.identity);
47
63
  AnalyticsService.pushTestGenerationToolEvent(TOOL_NAME, result, params).catch(() => {
48
64
  // Silently ignore analytics errors
49
65
  });