@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,5 +1,12 @@
1
1
  import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
2
  export declare class AnalyticsService {
3
+ /**
4
+ * Only the fields named here reach the analytics backend. The tool's own
5
+ * params are NOT forwarded wholesale: they carry the user's prompt, which is
6
+ * free text about a private codebase and has no business leaving the machine.
7
+ * Anything added here is sent verbatim -- PushToolEvent on the Go side does no
8
+ * redaction of its own -- so add a field only when it is safe to publish.
9
+ */
3
10
  static pushTestGenerationToolEvent(toolName: string, result: CallToolResult, params: Record<string, any>): Promise<void>;
4
11
  static pushMCPToolEvent(toolName: string, result: CallToolResult | undefined, params: Record<string, any>): Promise<void>;
5
12
  /**
@@ -40,9 +40,15 @@ async function safePushToolEvent(entryPoint, toolName, errorMessage, params) {
40
40
  }
41
41
  }
42
42
  export class AnalyticsService {
43
+ /**
44
+ * Only the fields named here reach the analytics backend. The tool's own
45
+ * params are NOT forwarded wholesale: they carry the user's prompt, which is
46
+ * free text about a private codebase and has no business leaving the machine.
47
+ * Anything added here is sent verbatim -- PushToolEvent on the Go side does no
48
+ * redaction of its own -- so add a field only when it is safe to publish.
49
+ */
43
50
  static async pushTestGenerationToolEvent(toolName, result, params) {
44
51
  const analyticsResult = {};
45
- analyticsResult["prompt"] = params.prompt;
46
52
  analyticsResult["language"] = params.language;
47
53
  this.pushMCPToolEvent(toolName, result, analyticsResult);
48
54
  }
@@ -28,10 +28,8 @@ The test file \`${params.testFile}\` will remain unchanged. No further action is
28
28
  let prompt = "";
29
29
  switch (testType) {
30
30
  case TestType.UI:
31
- prompt = getModularizationPromptForUI(params.testFile);
32
- break;
33
31
  case TestType.E2E:
34
- prompt = getModularizationPromptForUI(params.testFile);
32
+ prompt = getModularizationPromptForUI(params.testFile, testType, params.language);
35
33
  break;
36
34
  case TestType.INTEGRATION:
37
35
  prompt = getModularizationPromptForIntegration(params.testFile);
@@ -45,8 +45,6 @@ export declare class TestDiscoveryService {
45
45
  private readonly SUPPORTED_EXTENSIONS;
46
46
  private readonly MAX_CONCURRENT_OPERATIONS;
47
47
  private readonly MAX_EXTERNAL_FULL_REPO;
48
- private readonly TEST_FILE_PATTERNS;
49
- private readonly TEST_DIR_PATTERNS;
50
48
  /**
51
49
  * Discover all tests under testDir — both Skyramp-generated and external (user-written).
52
50
  * Uses fast-glob for cross-platform file scanning, then classifies discovered files
@@ -5,6 +5,7 @@ import { TestSource } from "../types/TestAnalysis.js";
5
5
  import { TestType } from "../types/TestTypes.js";
6
6
  import { buildPathSignatures } from "../utils/pathSignatures.js";
7
7
  import fg from "fast-glob";
8
+ import { isDiscoveredTestFile } from "../utils/testFileClassification.js";
8
9
  export class TestDiscoveryService {
9
10
  EXCLUDED_DIRS = [
10
11
  "node_modules",
@@ -34,37 +35,6 @@ export class TestDiscoveryService {
34
35
  // In PR mode this hard limit is replaced by relevance-based partitioning which
35
36
  // naturally bounds the read set to files relevant to the changed endpoints.
36
37
  MAX_EXTERNAL_FULL_REPO = 100;
37
- // Test file naming patterns — match against basename.
38
- // Includes Skyramp-generated suffixes so that files like orders_smoke.py placed
39
- // outside a recognized test directory are still discovered.
40
- TEST_FILE_PATTERNS = [
41
- /^test_.*\.(py|js|ts|rb|go|php)$/, // test_*.py, test_*.rb, test_*.go
42
- /.*_test\.(py|ts|js|go|rs)$/, // *_test.py, *_test.go, *_test.rs
43
- /.*\.test\.(ts|js|tsx|jsx)$/, // *.test.ts, *.test.js, *.test.tsx
44
- /.*\..*spec\.(ts|js|tsx|jsx|rb)$/, // *.spec.ts, *.e2e-spec.ts, *.unit-spec.ts
45
- /.*Test\.(java|kt|kts|cs|scala|swift|m)$/, // *Test.java, *Test.kt, *Test.m (ObjC)
46
- /.*Tests\.(cs|swift|m)$/, // *Tests.cs, *Tests.swift, *Tests.m (ObjC)
47
- /.*_spec\.rb$/, // *_spec.rb (RSpec)
48
- /.*\.test\.php$/, // *.test.php
49
- /.*Test\.php$/, // *Test.php (PHPUnit)
50
- // Skyramp-generated test suffixes — must be matched even outside test dirs
51
- /.*_(?:smoke|contract|fuzz|integration|load|e2e|ui)\.(py|ts|js|java|rb|go|cs|kt|kts|scala|swift|php|rs)$/i,
52
- ];
53
- // Directory patterns that signal test-owned files — works with both / and \ separators.
54
- // Includes standard test dirs, snapshot dirs, and common helper/fixture dirs used by
55
- // Playwright, Cypress, and other frameworks for page objects and support code.
56
- TEST_DIR_PATTERNS = [
57
- /[\\/]tests?[\\/]/,
58
- /[\\/]__tests__[\\/]/,
59
- /[\\/]__snapshots__[\\/]/, // Jest/Vitest snapshot directories
60
- /[\\/]spec[\\/]/,
61
- /[\\/]e2e[\\/]/,
62
- /[\\/]cypress[\\/]/, // Cypress support, fixtures, plugins
63
- /[\\/]playwright[\\/]/, // Playwright fixtures and helpers
64
- /[\\/]page-?objects?[\\/]/, // page-objects/, page-object/, pageObjects/
65
- /[\\/]fixtures[\\/](?!db[\\/]|seed[\\/]|migrations[\\/])/, // test fixture files (not db/seed/migration fixtures)
66
- /[\\/]cypress[\\/]support[\\/]/, // Cypress support directory (scoped to avoid matching app support/ dirs)
67
- ];
68
38
  /**
69
39
  * Discover all tests under testDir — both Skyramp-generated and external (user-written).
70
40
  * Uses fast-glob for cross-platform file scanning, then classifies discovered files
@@ -478,12 +448,7 @@ export class TestDiscoveryService {
478
448
  * or directory placement.
479
449
  */
480
450
  isExternalTestFile(filePath) {
481
- const basename = path.basename(filePath);
482
- if (this.TEST_FILE_PATTERNS.some(p => p.test(basename)))
483
- return true;
484
- if (this.TEST_DIR_PATTERNS.some(p => p.test(filePath)))
485
- return true;
486
- return false;
451
+ return isDiscoveredTestFile(filePath);
487
452
  }
488
453
  /**
489
454
  * Extract metadata from a test file
@@ -44,6 +44,15 @@ export interface BaseTestParams {
44
44
  * empty-string edge (SKYR-3899).
45
45
  */
46
46
  export declare function deriveEffectiveFramework(language: string | undefined, framework: string | undefined): string;
47
+ /** The files codegen reports it wrote — the block every generation result carries:
48
+ *
49
+ * Successfully generated/updated tests files:
50
+ * \t/abs/path/to/orders_integration_test.py
51
+ *
52
+ * Read from the result rather than inferred from the filesystem, so a leftover from an
53
+ * earlier attempt or a sibling written by another call is never mistaken for this call's
54
+ * output. Empty when the block is absent (nothing to hand off). */
55
+ export declare function generatedFilesFrom(result: string): string[];
47
56
  export declare abstract class TestGenerationService {
48
57
  protected client: SkyrampClient;
49
58
  constructor();
@@ -51,6 +60,13 @@ export declare abstract class TestGenerationService {
51
60
  protected validateInputs(params: BaseTestParams): CallToolResult;
52
61
  protected abstract buildGenerationOptions(params: BaseTestParams & Record<string, any>): any;
53
62
  protected abstract getTestType(): TestType | typeof MOCK_TYPE;
63
+ /**
64
+ * Whether this generation's codeReuse hand-off sequences skyramp_modularization
65
+ * before skyramp_reuse_code (routing: isModularizeFirstTarget). Subclasses that
66
+ * add their own post-generation steps consult this so modularization is
67
+ * instructed exactly once.
68
+ */
69
+ protected emitsModularizeFirstHandOff(params: BaseTestParams): boolean;
54
70
  protected handleApiAnalysis(params: BaseTestParams): Promise<CallToolResult | null>;
55
71
  private static readonly STANDARD_HEADERS;
56
72
  private static simpleWildcardMatch;
@@ -3,7 +3,7 @@ import fs from "fs";
3
3
  import { SkyrampClient } from "@skyramp/skyramp";
4
4
  import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
5
5
  import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, resolveAuthFromWorkspace, getWorkspaceSkipTLSVerify, getWorkspaceScopedQueryParams, mergeQueryParamsString, } from "../utils/workspaceAuth.js";
6
- import { extractPathname, resolveQueryParamsForPath, contributesNothing } from "../workspace/queryParamResolution.js";
6
+ import { extractPathname, resolveQueryParamsForPath, contributesNothing, } from "../workspace/queryParamResolution.js";
7
7
  import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
8
8
  import { getEntryPoint } from "../utils/telemetry.js";
9
9
  import { getLanguageSteps } from "../utils/language-helper.js";
@@ -12,6 +12,8 @@ import { logger } from "../utils/logger.js";
12
12
  import { normalizeLanguageParams } from "../utils/normalizeParams.js";
13
13
  import { stageGeneratedPaths, resolveOutputDir } from "../utils/gitStaging.js";
14
14
  import { getTestsRepoDir } from "../utils/AnalysisStateManager.js";
15
+ import { recordReuseHandOff } from "../tools/code-refactor/reuse-state.js";
16
+ import { isModularizeFirstTarget } from "../utils/reuseRouting.js";
15
17
  /**
16
18
  * Derive the framework actually used for generation from the (normalized)
17
19
  * language + framework params. A blank/whitespace-only framework is treated as
@@ -35,10 +37,24 @@ export function deriveEffectiveFramework(language, framework) {
35
37
  * defaultQueryParams value into that path is not what the design intends
36
38
  * (SKYR-4050).
37
39
  */
38
- const SKIP_DEFAULT_QUERY_PARAMS_TEST_TYPES = new Set([
39
- TestType.UI,
40
- TestType.E2E,
41
- ]);
40
+ const SKIP_DEFAULT_QUERY_PARAMS_TEST_TYPES = new Set([TestType.UI, TestType.E2E]);
41
+ /** The files codegen reports it wrote — the block every generation result carries:
42
+ *
43
+ * Successfully generated/updated tests files:
44
+ * \t/abs/path/to/orders_integration_test.py
45
+ *
46
+ * Read from the result rather than inferred from the filesystem, so a leftover from an
47
+ * earlier attempt or a sibling written by another call is never mistaken for this call's
48
+ * output. Empty when the block is absent (nothing to hand off). */
49
+ export function generatedFilesFrom(result) {
50
+ const m = /Successfully generated\/updated tests? files?:\s*\n((?:[ \t]+\S[^\n]*\n?)+)/i.exec(result);
51
+ if (!m)
52
+ return [];
53
+ return m[1]
54
+ .split("\n")
55
+ .map((l) => l.trim())
56
+ .filter((l) => l.length > 0 && path.isAbsolute(l));
57
+ }
42
58
  export class TestGenerationService {
43
59
  client;
44
60
  constructor() {
@@ -74,6 +90,9 @@ export class TestGenerationService {
74
90
  return apiAnalysisResult;
75
91
  }
76
92
  const result = await this.executeGeneration(generateOptions);
93
+ // The files this call wrote, as codegen reports them — the reuse hand-off is
94
+ // recorded per FILE (SKYR-4220).
95
+ const written = generatedFilesFrom(result);
77
96
  const testType = this.getTestType();
78
97
  const languageSteps = getLanguageSteps({
79
98
  language: params.language || "",
@@ -83,12 +102,57 @@ export class TestGenerationService {
83
102
  // Orchestrate code optimization workflow based on flags
84
103
  let postGenerationMessage = "";
85
104
  if (params.codeReuse) {
86
- // Only code reuse
87
- postGenerationMessage += `
105
+ // Name the framework generation actually used, not the raw parameter:
106
+ // TS/JS derive to playwright when the caller omitted it, and
107
+ // skyramp_reuse_code requires framework to route correctly.
108
+ const effectiveFramework = deriveEffectiveFramework(params.language || "", params.framework);
109
+ const reuseArgs = `\`testFile\` set to the absolute path of the generated test file, \`language: "${params.language}"\`, \`testType: "${testType}"\`${effectiveFramework ? `, and \`framework: "${effectiveFramework}"\`` : ""}`;
110
+ // Modularize-first: fresh codegen carries no helper functions (each
111
+ // endpoint called once, each browser step inline), so modularization is
112
+ // what creates the named helpers that reuse then consolidates into the
113
+ // shared utils file. Emitted here, not in the testbot prompt, so the
114
+ // instruction exists only on runs that actually generated this test type
115
+ // and needs no hand-maintained step numbering.
116
+ if (this.emitsModularizeFirstHandOff(params)) {
117
+ // SKYR-4220: record the hand-off at the site that emits it, under the same
118
+ // condition, so skyramp_execute_test can refuse a test that skipped the pair
119
+ // and the report can mark it — without ever owing it for a test that was not
120
+ // told to run it. Keyed by the files this call wrote: a test generated into
121
+ // the same directory without codeReuse owes nothing.
122
+ await recordReuseHandOff(written, testType, params.language || "", effectiveFramework);
123
+ // Browser tests: the assertion enhancer anchors new assertions to
124
+ // selectors present in the test file, and after modularize → reuse
125
+ // those selectors live in the utils file. So for UI the enhancer runs
126
+ // FIRST, on the raw file; the extraction rules then keep every value
127
+ // assertion in the test body. API enhancers key off response paths,
128
+ // so integration keeps its validated modularize → reuse → enhance order.
129
+ const enhanceFirst = testType === TestType.UI && !!params.enhanceAssertions;
130
+ const steps = [
131
+ ...(enhanceFirst
132
+ ? [
133
+ `Call \`skyramp_enhance_assertions\` with \`testFile\` set to the absolute path of the generated test file, \`testType: "${testType}"\`, and \`enhanceType: "generation"\`, and apply every instruction returned to that file — BEFORE modularization, while the file's selectors are still inline.`,
134
+ ]
135
+ : []),
136
+ `Call \`skyramp_modularization\` with \`testFile\`, \`testType: "${testType}"\`, and \`isTraceBased: true\`, and apply the returned instructions — this extracts the test's steps into named helper functions, which is what makes cross-test reuse possible.`,
137
+ `Immediately after writing the modularized file — as your very NEXT skyramp tool call — call \`skyramp_reuse_code\` with ${reuseArgs}. The modularization call does NOT count as this step: reuse is a separate tool and MUST be invoked even when you expect it to find nothing. Follow the returned steps to completion. Sibling tests offering nothing to reuse is a normal outcome — the returned steps still move THIS test's own helpers into the shared utils file and import them back, so the test file IS expected to change; finish those steps, then continue (do NOT retry).`,
138
+ ];
139
+ const ordering = enhanceFirst
140
+ ? "three sub-steps, STRICTLY SEQUENTIAL (never in the same tool-call batch)"
141
+ : "two sub-steps, STRICTLY SEQUENTIAL (never in the same tool-call batch), BOTH BEFORE `skyramp_enhance_assertions`";
142
+ postGenerationMessage += `
143
+ Test generated successfully!
144
+
145
+ ⏭️**CRITICAL NEXT STEPS — ${ordering}**:
146
+ ${steps.map((text, i) => `${String.fromCharCode(97 + i)}. ${text}`).join("\n")}
147
+ `;
148
+ }
149
+ else {
150
+ postGenerationMessage += `
88
151
  Test generated successfully!
89
152
 
90
- ⏭️**CRITICAL NEXT STEP**: Running code reuse analysis with skyramp_reuse_code tool.
153
+ ⏭️**CRITICAL NEXT STEP**: Run code reuse analysis by calling the skyramp_reuse_code tool with ${reuseArgs}.
91
154
  `;
155
+ }
92
156
  }
93
157
  else if (params.modularizeCode) {
94
158
  // Check if test type should be modularized
@@ -210,6 +274,16 @@ The generated test file remains unchanged and ready to use as-is.
210
274
  ? { content: [], isError: false }
211
275
  : errList;
212
276
  }
277
+ /**
278
+ * Whether this generation's codeReuse hand-off sequences skyramp_modularization
279
+ * before skyramp_reuse_code (routing: isModularizeFirstTarget). Subclasses that
280
+ * add their own post-generation steps consult this so modularization is
281
+ * instructed exactly once.
282
+ */
283
+ emitsModularizeFirstHandOff(params) {
284
+ return (!!params.codeReuse &&
285
+ isModularizeFirstTarget(this.getTestType(), params.language));
286
+ }
213
287
  async handleApiAnalysis(params) {
214
288
  if (params.apiSchema && params.endpointURL && params.method === "") {
215
289
  try {
@@ -274,7 +348,9 @@ The generated test file remains unchanged and ready to use as-is.
274
348
  // KNOWN_AUTH_HEADERS is imported from workspaceAuth.ts — single source of truth
275
349
  // shared with trace-parser.ts for redaction. See workspaceAuth.ts for the full list.
276
350
  static simpleWildcardMatch(pattern, value) {
277
- const regex = new RegExp("^" + pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$");
351
+ const regex = new RegExp("^" +
352
+ pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") +
353
+ "$");
278
354
  return regex.test(value);
279
355
  }
280
356
  static traceMatchesFilters(entry, include, exclude) {
@@ -421,7 +497,7 @@ The generated test file remains unchanged and ready to use as-is.
421
497
  // authType handles the Authorization header implicitly — the Go CLI treats
422
498
  // --auth-type and --auth-header as mutually exclusive, so clear authHeader
423
499
  // when it's the standard Authorization header to avoid the conflict.
424
- if (isAuthorizationHeaderName(generateOptions.authHeader || '')) {
500
+ if (isAuthorizationHeaderName(generateOptions.authHeader || "")) {
425
501
  delete generateOptions.authHeader;
426
502
  }
427
503
  }
@@ -106,23 +106,24 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
106
106
  // carrying as literals (SKYR-3862). Without this the container never sees
107
107
  // them and every generated login fails its guard.
108
108
  //
109
+ // Matched by prefix, like the service URLs above: a multi-credential
110
+ // `uiCredentials` input exports one keyed pair per credential —
111
+ // SKYRAMP_UI_USERNAME_<KEY> / SKYRAMP_UI_PASSWORD_<KEY> — alongside the
112
+ // unkeyed first-credential pair, and codegen binds each generated test to
113
+ // the pair it was recorded with (SKYR-4110). Dropping a keyed pair here
114
+ // would fail that test's guard even though the run provided the credential.
115
+ //
109
116
  // Forwarded on presence, not truthiness: the generated guard deliberately
110
117
  // distinguishes an unset variable (a misconfiguration, fails fast) from one
111
118
  // set to "" (valid on forms that accept a blank password). A truthy check —
112
119
  // as used for the URL vars above — would drop the empty case and collapse it
113
120
  // into the failure case.
114
- //
115
- // TODO(SKYR-4110): only ONE credential pair is forwarded, because testbot
116
- // exports only the first credential from its `uiCredentials` input. That input
117
- // was designed to carry several, so the agent can pick per test case by
118
- // authorization role — so a multi-role suite can get a credential its test did
119
- // not expect, surfacing as a false 403. The fix is a keyed form
120
- // (SKYRAMP_UI_PASSWORD_<KEY>), which needs no new forwarding logic here: the
121
- // loop above already forwards SKYRAMP_TEST_SERVICE_URL_* by prefix, so the
122
- // same approach applies. Keep the presence-not-truthiness rule when it lands.
123
- for (const key of ["SKYRAMP_UI_USERNAME", "SKYRAMP_UI_PASSWORD"]) {
124
- const val = hostEnv[key];
125
- if (val !== undefined) {
121
+ for (const [key, val] of Object.entries(hostEnv)) {
122
+ const isCredential = key === "SKYRAMP_UI_USERNAME" ||
123
+ key === "SKYRAMP_UI_PASSWORD" ||
124
+ key.startsWith("SKYRAMP_UI_USERNAME_") ||
125
+ key.startsWith("SKYRAMP_UI_PASSWORD_");
126
+ if (isCredential && val !== undefined) {
126
127
  env.push(`${key}=${val}`);
127
128
  }
128
129
  }