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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/build/index.js +2 -1
  2. package/build/prompts/code-reuse.d.ts +7 -1
  3. package/build/prompts/code-reuse.js +8 -5
  4. package/build/prompts/code-reuse.test.d.ts +1 -0
  5. package/build/prompts/code-reuse.test.js +62 -0
  6. package/build/prompts/pom-aware-code-reuse.d.ts +6 -1
  7. package/build/prompts/pom-aware-code-reuse.js +100 -53
  8. package/build/prompts/pom-aware-code-reuse.test.d.ts +1 -0
  9. package/build/prompts/pom-aware-code-reuse.test.js +11 -0
  10. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +4 -2
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +11 -65
  12. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +2 -2
  13. package/build/prompts/test-recommendation/recommendationSections.js +5 -2
  14. package/build/prompts/test-recommendation/scopeAssessment.js +1 -1
  15. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +26 -1
  16. package/build/prompts/test-recommendation/test-recommendation-prompt.js +68 -56
  17. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +48 -11
  18. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  19. package/build/prompts/testbot/testbot-prompts.js +67 -21
  20. package/build/prompts/testbot/testbot-prompts.test.js +44 -0
  21. package/build/recommendation/budgeters/diversityBalancedBudgeter.d.ts +7 -0
  22. package/build/recommendation/budgeters/diversityBalancedBudgeter.js +71 -0
  23. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +1 -0
  24. package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +75 -0
  25. package/build/recommendation/budgeters/fixedNBudgeter.d.ts +7 -0
  26. package/build/recommendation/budgeters/fixedNBudgeter.js +11 -0
  27. package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +1 -0
  28. package/build/recommendation/budgeters/fixedNBudgeter.test.js +66 -0
  29. package/build/recommendation/budgeters/shared.d.ts +19 -0
  30. package/build/recommendation/budgeters/shared.js +66 -0
  31. package/build/recommendation/discriminators.d.ts +31 -0
  32. package/build/recommendation/discriminators.js +355 -0
  33. package/build/recommendation/discriminators.test.d.ts +1 -0
  34. package/build/recommendation/discriminators.test.js +324 -0
  35. package/build/recommendation/diversity.d.ts +47 -0
  36. package/build/recommendation/diversity.js +101 -0
  37. package/build/recommendation/diversity.test.d.ts +1 -0
  38. package/build/recommendation/diversity.test.js +77 -0
  39. package/build/recommendation/planRanker.d.ts +50 -0
  40. package/build/recommendation/planRanker.js +67 -0
  41. package/build/recommendation/planRanker.test.d.ts +1 -0
  42. package/build/recommendation/planRanker.test.js +110 -0
  43. package/build/recommendation/testFixtures.d.ts +25 -0
  44. package/build/recommendation/testFixtures.js +45 -0
  45. package/build/resources/testbotResource.js +4 -1
  46. package/build/services/ScenarioGenerationService.d.ts +5 -0
  47. package/build/services/ScenarioGenerationService.js +16 -1
  48. package/build/services/ScenarioGenerationService.test.js +44 -0
  49. package/build/services/TestExecutionService.d.ts +15 -1
  50. package/build/services/TestExecutionService.js +210 -55
  51. package/build/services/TestExecutionService.test.js +397 -0
  52. package/build/services/TestGenerationService.js +19 -1
  53. package/build/services/TestGenerationService.test.js +58 -0
  54. package/build/tool-phases.js +1 -0
  55. package/build/toolNames.d.ts +19 -0
  56. package/build/toolNames.js +19 -0
  57. package/build/tools/code-refactor/codeReuseTool.d.ts +7 -0
  58. package/build/tools/code-refactor/codeReuseTool.js +130 -4
  59. package/build/tools/code-refactor/codeReuseTool.test.d.ts +1 -0
  60. package/build/tools/code-refactor/codeReuseTool.test.js +290 -0
  61. package/build/tools/executeSkyrampTestTool.js +8 -2
  62. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +6 -1
  63. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +110 -17
  64. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +147 -0
  65. package/build/tools/generate-tests/generateContractRestTool.js +11 -1
  66. package/build/tools/generate-tests/generateIntegrationRestTool.js +24 -1
  67. package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +1 -0
  68. package/build/tools/generate-tests/generateIntegrationRestTool.test.js +159 -0
  69. package/build/tools/generate-tests/planGuard.d.ts +13 -0
  70. package/build/tools/generate-tests/planGuard.js +78 -0
  71. package/build/tools/generate-tests/planGuard.test.d.ts +1 -0
  72. package/build/tools/generate-tests/planGuard.test.js +185 -0
  73. package/build/tools/generate-tests/scenarioFileIdentity.d.ts +10 -0
  74. package/build/tools/generate-tests/scenarioFileIdentity.js +46 -0
  75. package/build/tools/generate-tests/scenarioLint.d.ts +30 -0
  76. package/build/tools/generate-tests/scenarioLint.js +150 -0
  77. package/build/tools/generate-tests/scenarioLint.test.d.ts +1 -0
  78. package/build/tools/generate-tests/scenarioLint.test.js +100 -0
  79. package/build/tools/submitReportTool.js +78 -0
  80. package/build/tools/submitReportTool.test.js +255 -0
  81. package/build/tools/test-management/analyzeChangesTool.js +55 -2
  82. package/build/tools/test-management/analyzeChangesTool.test.js +12 -0
  83. package/build/tools/test-management/index.d.ts +1 -0
  84. package/build/tools/test-management/index.js +1 -0
  85. package/build/tools/test-management/registerTestPlanTool.d.ts +2 -0
  86. package/build/tools/test-management/registerTestPlanTool.js +329 -0
  87. package/build/tools/test-management/registerTestPlanTool.test.d.ts +1 -0
  88. package/build/tools/test-management/registerTestPlanTool.test.js +296 -0
  89. package/build/types/Recommendation.d.ts +97 -0
  90. package/build/types/Recommendation.js +48 -0
  91. package/build/types/RepositoryAnalysis.d.ts +14 -14
  92. package/build/types/TestExecution.d.ts +2 -0
  93. package/build/types/TestRecommendation.d.ts +12 -1
  94. package/build/types/TestRecommendation.js +26 -11
  95. package/build/types/TestTypes.js +1 -1
  96. package/build/utils/AnalysisStateManager.d.ts +47 -0
  97. package/build/utils/docker.test.js +1 -1
  98. package/build/utils/planMatchKeys.d.ts +61 -0
  99. package/build/utils/planMatchKeys.js +125 -0
  100. package/build/utils/pom-scope/import-expansion.d.ts +5 -0
  101. package/build/utils/pom-scope/import-expansion.js +32 -0
  102. package/build/utils/pom-scope/index.d.ts +39 -0
  103. package/build/utils/pom-scope/index.js +120 -0
  104. package/build/utils/pom-scope/index.test.d.ts +1 -0
  105. package/build/utils/pom-scope/index.test.js +239 -0
  106. package/build/utils/pom-scope/pom-files.d.ts +3 -0
  107. package/build/utils/pom-scope/pom-files.js +48 -0
  108. package/build/utils/pom-scope/pom-files.test.d.ts +1 -0
  109. package/build/utils/pom-scope/pom-files.test.js +29 -0
  110. package/build/utils/pom-scope/scoring.d.ts +20 -0
  111. package/build/utils/pom-scope/scoring.js +45 -0
  112. package/build/utils/pom-scope/scoring.test.d.ts +1 -0
  113. package/build/utils/pom-scope/scoring.test.js +39 -0
  114. package/build/utils/pom-scope/selector-extractor.d.ts +7 -0
  115. package/build/utils/pom-scope/selector-extractor.js +57 -0
  116. package/build/utils/pom-scope/selector-extractor.test.d.ts +1 -0
  117. package/build/utils/pom-scope/selector-extractor.test.js +67 -0
  118. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.d.ts +5 -0
  119. package/build/utils/pom-verify/__fixtures__/af-style/asset-list.page.js +5 -0
  120. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.d.ts +5 -0
  121. package/build/utils/pom-verify/__fixtures__/af-style/pageobjects/asset-list-page.js +9 -0
  122. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.d.ts +4 -0
  123. package/build/utils/pom-verify/__fixtures__/af-style/report.iframe.page.js +4 -0
  124. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.d.ts +4 -0
  125. package/build/utils/pom-verify/__fixtures__/af-style/workflow-footer.page.js +4 -0
  126. package/build/utils/pom-verify/bindings.d.ts +19 -0
  127. package/build/utils/pom-verify/bindings.js +161 -0
  128. package/build/utils/pom-verify/bindings.test.d.ts +1 -0
  129. package/build/utils/pom-verify/bindings.test.js +164 -0
  130. package/build/utils/pom-verify/calls.d.ts +16 -0
  131. package/build/utils/pom-verify/calls.js +42 -0
  132. package/build/utils/pom-verify/calls.test.d.ts +1 -0
  133. package/build/utils/pom-verify/calls.test.js +61 -0
  134. package/build/utils/pom-verify/index.d.ts +4 -0
  135. package/build/utils/pom-verify/index.js +4 -0
  136. package/build/utils/pom-verify/resolve.d.ts +7 -0
  137. package/build/utils/pom-verify/resolve.js +27 -0
  138. package/build/utils/pom-verify/resolve.test.d.ts +1 -0
  139. package/build/utils/pom-verify/resolve.test.js +68 -0
  140. package/build/utils/pom-verify/strip.d.ts +9 -0
  141. package/build/utils/pom-verify/strip.js +89 -0
  142. package/build/utils/pom-verify/verify.d.ts +14 -0
  143. package/build/utils/pom-verify/verify.js +158 -0
  144. package/build/utils/pom-verify/verify.test.d.ts +1 -0
  145. package/build/utils/pom-verify/verify.test.js +325 -0
  146. package/build/utils/reportVerification.d.ts +61 -0
  147. package/build/utils/reportVerification.js +104 -0
  148. package/build/utils/reportVerification.test.d.ts +1 -0
  149. package/build/utils/reportVerification.test.js +185 -0
  150. package/build/utils/scenarioDrafting.js +5 -5
  151. package/build/utils/versions.d.ts +3 -3
  152. package/build/utils/versions.js +1 -1
  153. package/build/utils/workspaceAuth.d.ts +9 -1
  154. package/build/utils/workspaceAuth.js +25 -5
  155. package/build/utils/workspaceAuth.test.js +48 -0
  156. package/build/workspace/workspace.d.ts +20 -0
  157. package/build/workspace/workspace.js +4 -0
  158. package/build/workspace/workspace.test.js +10 -0
  159. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +77 -8
  160. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  161. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  162. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +101 -0
  163. package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
  164. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-aszq5EdG.js → codeMirrorModule-Bzd72-bG.js} +1 -1
  165. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-BxS7Jm4s.js → defaultSettingsView-DzxTioTK.js} +101 -101
  166. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.D4JTTy4R.js → index.BGc30U3S.js} +1 -1
  167. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  168. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.DaRMQKOI.js → uiMode.IaDrb29A.js} +1 -1
  169. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  170. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  171. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  172. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  173. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +131 -0
  174. package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
  175. package/node_modules/playwright/package.json +1 -1
  176. package/package.json +3 -3
  177. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
@@ -1,6 +1,6 @@
1
1
  import { ScenarioSource } from "../types/RepositoryAnalysis.js";
2
2
  import { TestType } from "../types/TestTypes.js";
3
- import { CATEGORY_PRIORITY } from "../types/TestRecommendation.js";
3
+ import { CATEGORY_PRIORITY, PriorityTier } from "../types/TestRecommendation.js";
4
4
  import { inferExpectedStatus } from "./httpDefaults.js";
5
5
  import { WorkspaceAuthType } from "./workspaceAuth.js";
6
6
  import { deriveResourceToken } from "./importerHop.js";
@@ -229,8 +229,8 @@ const TIER_ORDER = { CRITICAL: 4, HIGH: 3, MEDIUM: 2, LOW: 1 };
229
229
  export function capScenarios(scenarios) {
230
230
  if (scenarios.length <= MAX_TOTAL_SCENARIOS)
231
231
  return scenarios;
232
- const critical = scenarios.filter(s => CATEGORY_PRIORITY[s.category] === "CRITICAL");
233
- const rest = scenarios.filter(s => CATEGORY_PRIORITY[s.category] !== "CRITICAL");
232
+ const critical = scenarios.filter(s => CATEGORY_PRIORITY[s.category] === PriorityTier.CRITICAL);
233
+ const rest = scenarios.filter(s => CATEGORY_PRIORITY[s.category] !== PriorityTier.CRITICAL);
234
234
  const breadthPicks = [];
235
235
  const seenCategories = new Set();
236
236
  for (const s of rest) {
@@ -242,8 +242,8 @@ export function capScenarios(scenarios) {
242
242
  const breadthSet = new Set(breadthPicks);
243
243
  const remaining = rest.filter(s => !breadthSet.has(s));
244
244
  remaining.sort((a, b) => {
245
- const ta = TIER_ORDER[CATEGORY_PRIORITY[a.category] ?? "LOW"] ?? 1;
246
- const tb = TIER_ORDER[CATEGORY_PRIORITY[b.category] ?? "LOW"] ?? 1;
245
+ const ta = TIER_ORDER[CATEGORY_PRIORITY[a.category] ?? PriorityTier.LOW] ?? 1;
246
+ const tb = TIER_ORDER[CATEGORY_PRIORITY[b.category] ?? PriorityTier.LOW] ?? 1;
247
247
  return tb - ta;
248
248
  });
249
249
  const budget = MAX_TOTAL_SCENARIOS - critical.length - breadthPicks.length;
@@ -1,3 +1,3 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.34";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.34";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.34";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.35";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.35";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.35";
@@ -1,3 +1,3 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.34";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.35";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
@@ -40,8 +40,13 @@ export interface WorkspaceBaseUrlResult {
40
40
  *
41
41
  * e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
42
42
  * → { baseUrl: "http://localhost:8000", candidates: [] }
43
+ *
44
+ * `testRepoPath` (SKYR-3819): in a cross-repo run, delivered test files live under
45
+ * the run's testsRepoDir but mirror the service's testDirectory structure. When
46
+ * provided, each service's testDirectory is also resolved against this root so
47
+ * cross-repo test files still resolve the SUT service's baseUrl and dockerNetwork.
43
48
  */
44
- export declare function getWorkspaceBaseUrl(repositoryPath: string, testFile?: string, language?: string): Promise<WorkspaceBaseUrlResult>;
49
+ export declare function getWorkspaceBaseUrl(repositoryPath: string, testFile?: string, language?: string, testRepoPath?: string): Promise<WorkspaceBaseUrlResult>;
45
50
  /**
46
51
  * Known auth-related HTTP header names (lowercase).
47
52
  * Single source of truth used for both:
@@ -125,3 +130,6 @@ export interface WorkspaceAuthConfig {
125
130
  authScheme?: string;
126
131
  }
127
132
  export declare function getWorkspaceAuthConfig(repositoryPath: string): Promise<WorkspaceAuthConfig>;
133
+ /** True when any workspace service declares api.skipTLSVerify — the SUT serves
134
+ * self-signed TLS and generated tests must skip cert verification (SKYR-3961). */
135
+ export declare function getWorkspaceSkipTLSVerify(repositoryPath: string): Promise<boolean>;
@@ -56,8 +56,13 @@ export async function readWorkspaceConfigRaw(workspacePath) {
56
56
  *
57
57
  * e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
58
58
  * → { baseUrl: "http://localhost:8000", candidates: [] }
59
+ *
60
+ * `testRepoPath` (SKYR-3819): in a cross-repo run, delivered test files live under
61
+ * the run's testsRepoDir but mirror the service's testDirectory structure. When
62
+ * provided, each service's testDirectory is also resolved against this root so
63
+ * cross-repo test files still resolve the SUT service's baseUrl and dockerNetwork.
59
64
  */
60
- export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
65
+ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language, testRepoPath) {
61
66
  const noMatch = { baseUrl: undefined, dockerNetwork: undefined, candidates: [] };
62
67
  if (!testFile)
63
68
  return noMatch;
@@ -67,15 +72,22 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
67
72
  return noMatch;
68
73
  const services = rawConfig.services ?? [];
69
74
  // Normalise testFile to an absolute path for reliable prefix matching
75
+ // (path.resolve also collapses any ".." segments in an absolute input)
70
76
  const absTestFile = path.isAbsolute(testFile)
71
- ? testFile
77
+ ? path.resolve(testFile)
72
78
  : path.resolve(repositoryPath, testFile);
73
- // Step 1: filter by testDirectory prefix
79
+ // Step 1: filter by testDirectory prefix — resolved against the SUT repo,
80
+ // and additionally against the cross-repo test root when one is known.
81
+ const testDirRoots = [repositoryPath];
82
+ if (testRepoPath)
83
+ testDirRoots.push(testRepoPath);
74
84
  let matches = services.filter((s) => {
75
85
  if (!s.testDirectory || !s.api?.baseUrl)
76
86
  return false;
77
- const absTestDirectory = path.resolve(repositoryPath, s.testDirectory);
78
- return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
87
+ return testDirRoots.some((root) => {
88
+ const absTestDirectory = path.resolve(root, s.testDirectory);
89
+ return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
90
+ });
79
91
  });
80
92
  // Step 2: narrow by language if still ambiguous
81
93
  if (matches.length > 1 && language) {
@@ -330,3 +342,11 @@ export async function getWorkspaceAuthConfig(repositoryPath) {
330
342
  const authScheme = svc?.api?.authScheme;
331
343
  return { authHeader, authType, authScheme };
332
344
  }
345
+ /** True when any workspace service declares api.skipTLSVerify — the SUT serves
346
+ * self-signed TLS and generated tests must skip cert verification (SKYR-3961). */
347
+ export async function getWorkspaceSkipTLSVerify(repositoryPath) {
348
+ const rawConfig = await readWorkspaceConfigRaw(repositoryPath);
349
+ if (!rawConfig)
350
+ return false;
351
+ return (rawConfig.services ?? []).some((s) => s.api?.skipTLSVerify === true);
352
+ }
@@ -210,3 +210,51 @@ describe("getWorkspaceBaseUrl", () => {
210
210
  expect(result.baseUrl).toBeUndefined();
211
211
  });
212
212
  });
213
+ // SKYR-3819: in a cross-repo run, delivered test files live under testsRepoDir
214
+ // (a sibling of the SUT repo) but mirror the service's testDirectory structure.
215
+ // getWorkspaceBaseUrl must also match testDirectory against the test-repo root,
216
+ // or cross-repo executions lose SKYRAMP_TEST_BASE_URL and dockerNetwork.
217
+ describe("getWorkspaceBaseUrl with testRepoPath (cross-repo)", () => {
218
+ const TEST_REPO = "/some/test-repo";
219
+ it("matches a cross-repo test file against testDirectory resolved from testRepoPath", async () => {
220
+ const result = await getWorkspaceBaseUrl(REPO, `${TEST_REPO}/backend/tests/orders_test.py`, "python", TEST_REPO);
221
+ expect(result.baseUrl).toBe("http://localhost:8000");
222
+ expect(result.candidates).toHaveLength(0);
223
+ });
224
+ it("returns dockerNetwork for a cross-repo match", async () => {
225
+ const { WorkspaceConfigManager } = await import("../workspace/workspace.js");
226
+ WorkspaceConfigManager.mockImplementationOnce(() => ({
227
+ exists: jest.fn().mockResolvedValue(true),
228
+ read: jest.fn().mockResolvedValue({
229
+ services: [
230
+ {
231
+ serviceName: "backend",
232
+ testDirectory: "tests",
233
+ api: { baseUrl: "http://localhost:8000" },
234
+ runtimeDetails: { dockerNetwork: "myapp_default" },
235
+ },
236
+ ],
237
+ }),
238
+ }));
239
+ const result = await getWorkspaceBaseUrl(REPO, `${TEST_REPO}/tests/orders_test.py`, undefined, TEST_REPO);
240
+ expect(result.baseUrl).toBe("http://localhost:8000");
241
+ expect(result.dockerNetwork).toBe("myapp_default");
242
+ });
243
+ it("still matches SUT-repo test files when testRepoPath is provided", async () => {
244
+ const result = await getWorkspaceBaseUrl(REPO, `${REPO}/backend/tests/orders_test.py`, "python", TEST_REPO);
245
+ expect(result.baseUrl).toBe("http://localhost:8000");
246
+ });
247
+ it("does not match a cross-repo file that is outside every testDirectory", async () => {
248
+ const result = await getWorkspaceBaseUrl(REPO, `${TEST_REPO}/unrelated/orders_test.py`, "python", TEST_REPO);
249
+ expect(result.baseUrl).toBeUndefined();
250
+ expect(result.candidates).toHaveLength(0);
251
+ });
252
+ it("does not match a cross-repo file without testRepoPath (pre-existing behavior)", async () => {
253
+ const result = await getWorkspaceBaseUrl(REPO, `${TEST_REPO}/backend/tests/orders_test.py`, "python");
254
+ expect(result.baseUrl).toBeUndefined();
255
+ });
256
+ it("normalizes '..' segments in an absolute testFile before prefix matching", async () => {
257
+ const result = await getWorkspaceBaseUrl(REPO, `${TEST_REPO}/somewhere/../backend/tests/orders_test.py`, "python", TEST_REPO);
258
+ expect(result.baseUrl).toBe("http://localhost:8000");
259
+ });
260
+ });
@@ -13,18 +13,24 @@ export declare const serviceSchema: z.ZodObject<{
13
13
  authHeader: z.ZodOptional<z.ZodString>;
14
14
  authScheme: z.ZodOptional<z.ZodString>;
15
15
  baseUrl: z.ZodOptional<z.ZodString>;
16
+ /** SUT serves self-signed TLS (e.g. a kind kube-apiserver) — generated
17
+ * tests should skip certificate verification for this service (SKYR-3961).
18
+ * Maps to the generation tools' `insecure` parameter / CLI `--insecure`. */
19
+ skipTLSVerify: z.ZodOptional<z.ZodBoolean>;
16
20
  }, "strict", z.ZodTypeAny, {
17
21
  authHeader?: string | undefined;
18
22
  authScheme?: string | undefined;
19
23
  schemaPath?: string | undefined;
20
24
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
21
25
  baseUrl?: string | undefined;
26
+ skipTLSVerify?: boolean | undefined;
22
27
  }, {
23
28
  authHeader?: string | undefined;
24
29
  authScheme?: string | undefined;
25
30
  schemaPath?: string | undefined;
26
31
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
27
32
  baseUrl?: string | undefined;
33
+ skipTLSVerify?: boolean | undefined;
28
34
  }>>;
29
35
  runtimeDetails: z.ZodOptional<z.ZodObject<{
30
36
  serverStartCommand: z.ZodOptional<z.ZodString>;
@@ -57,6 +63,7 @@ export declare const serviceSchema: z.ZodObject<{
57
63
  schemaPath?: string | undefined;
58
64
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
59
65
  baseUrl?: string | undefined;
66
+ skipTLSVerify?: boolean | undefined;
60
67
  } | undefined;
61
68
  runtimeDetails?: {
62
69
  runtime: "local" | "docker" | "k8s";
@@ -77,6 +84,7 @@ export declare const serviceSchema: z.ZodObject<{
77
84
  schemaPath?: string | undefined;
78
85
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
79
86
  baseUrl?: string | undefined;
87
+ skipTLSVerify?: boolean | undefined;
80
88
  } | undefined;
81
89
  runtimeDetails?: {
82
90
  runtime: "local" | "docker" | "k8s";
@@ -128,18 +136,24 @@ export declare const workspaceConfigSchema: z.ZodObject<{
128
136
  authHeader: z.ZodOptional<z.ZodString>;
129
137
  authScheme: z.ZodOptional<z.ZodString>;
130
138
  baseUrl: z.ZodOptional<z.ZodString>;
139
+ /** SUT serves self-signed TLS (e.g. a kind kube-apiserver) — generated
140
+ * tests should skip certificate verification for this service (SKYR-3961).
141
+ * Maps to the generation tools' `insecure` parameter / CLI `--insecure`. */
142
+ skipTLSVerify: z.ZodOptional<z.ZodBoolean>;
131
143
  }, "strict", z.ZodTypeAny, {
132
144
  authHeader?: string | undefined;
133
145
  authScheme?: string | undefined;
134
146
  schemaPath?: string | undefined;
135
147
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
136
148
  baseUrl?: string | undefined;
149
+ skipTLSVerify?: boolean | undefined;
137
150
  }, {
138
151
  authHeader?: string | undefined;
139
152
  authScheme?: string | undefined;
140
153
  schemaPath?: string | undefined;
141
154
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
142
155
  baseUrl?: string | undefined;
156
+ skipTLSVerify?: boolean | undefined;
143
157
  }>>;
144
158
  runtimeDetails: z.ZodOptional<z.ZodObject<{
145
159
  serverStartCommand: z.ZodOptional<z.ZodString>;
@@ -172,6 +186,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
172
186
  schemaPath?: string | undefined;
173
187
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
174
188
  baseUrl?: string | undefined;
189
+ skipTLSVerify?: boolean | undefined;
175
190
  } | undefined;
176
191
  runtimeDetails?: {
177
192
  runtime: "local" | "docker" | "k8s";
@@ -192,6 +207,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
192
207
  schemaPath?: string | undefined;
193
208
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
194
209
  baseUrl?: string | undefined;
210
+ skipTLSVerify?: boolean | undefined;
195
211
  } | undefined;
196
212
  runtimeDetails?: {
197
213
  runtime: "local" | "docker" | "k8s";
@@ -225,6 +241,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
225
241
  schemaPath?: string | undefined;
226
242
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
227
243
  baseUrl?: string | undefined;
244
+ skipTLSVerify?: boolean | undefined;
228
245
  } | undefined;
229
246
  runtimeDetails?: {
230
247
  runtime: "local" | "docker" | "k8s";
@@ -258,6 +275,7 @@ export declare const workspaceConfigSchema: z.ZodObject<{
258
275
  schemaPath?: string | undefined;
259
276
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
260
277
  baseUrl?: string | undefined;
278
+ skipTLSVerify?: boolean | undefined;
261
279
  } | undefined;
262
280
  runtimeDetails?: {
263
281
  runtime: "local" | "docker" | "k8s";
@@ -299,6 +317,7 @@ export declare function validateWorkspaceConfig(config: unknown): z.SafeParseRet
299
317
  schemaPath?: string | undefined;
300
318
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
301
319
  baseUrl?: string | undefined;
320
+ skipTLSVerify?: boolean | undefined;
302
321
  } | undefined;
303
322
  runtimeDetails?: {
304
323
  runtime: "local" | "docker" | "k8s";
@@ -332,6 +351,7 @@ export declare function validateWorkspaceConfig(config: unknown): z.SafeParseRet
332
351
  schemaPath?: string | undefined;
333
352
  authType?: "none" | "token" | "bearer" | "basic" | "oauth" | "apiKey" | "cookie" | "session" | undefined;
334
353
  baseUrl?: string | undefined;
354
+ skipTLSVerify?: boolean | undefined;
335
355
  } | undefined;
336
356
  runtimeDetails?: {
337
357
  runtime: "local" | "docker" | "k8s";
@@ -51,6 +51,10 @@ export const serviceSchema = z
51
51
  authHeader: z.string().optional(),
52
52
  authScheme: z.string().optional(),
53
53
  baseUrl: z.string().optional(),
54
+ /** SUT serves self-signed TLS (e.g. a kind kube-apiserver) — generated
55
+ * tests should skip certificate verification for this service (SKYR-3961).
56
+ * Maps to the generation tools' `insecure` parameter / CLI `--insecure`. */
57
+ skipTLSVerify: z.boolean().optional(),
54
58
  })
55
59
  .strict()
56
60
  .optional(),
@@ -16,6 +16,16 @@ describe("workspace module (ported from @skyramp/skyramp, SKYR-3794)", () => {
16
16
  it("rejects unknown fields (strict)", () => {
17
17
  expect(serviceSchema.safeParse({ serviceName: "api", bogus: true }).success).toBe(false);
18
18
  });
19
+ it("accepts api.skipTLSVerify for self-signed-TLS SUTs and rejects non-boolean (SKYR-3961)", () => {
20
+ expect(serviceSchema.safeParse({
21
+ serviceName: "api",
22
+ api: { baseUrl: "https://127.0.0.1:45643", skipTLSVerify: true },
23
+ }).success).toBe(true);
24
+ expect(serviceSchema.safeParse({
25
+ serviceName: "api",
26
+ api: { baseUrl: "https://127.0.0.1:45643", skipTLSVerify: "yes" },
27
+ }).success).toBe(false);
28
+ });
19
29
  });
20
30
  describe("validateWorkspaceConfig / createDefaultConfig", () => {
21
31
  it("a default config validates", () => {
@@ -1311,9 +1311,10 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
1311
1311
  details = passed ? `Value assertion passed: "${params.element}" has value "${params.expected}".` : `Value assertion FAILED: "${params.element}" has value "${elementValue}", expected "${params.expected}".`;
1312
1312
  }
1313
1313
  if (passed) {
1314
+ const recordedExpected = params.type === "text" && params.substring !== false ? (0, import_utils.stableTextLead)(params.expected) : params.expected;
1314
1315
  const selectorText = parsed.locator.kind === "text" ? parsed.locator.body : parsed.locator.next?.kind === "text" ? parsed.locator.next.body : parsed.locator.options?.name ?? null;
1315
- if (selectorText && selectorText === params.expected) {
1316
- traceDebug(`Skipped tautological assertion: selector already matches "${params.expected}"`);
1316
+ if (selectorText && selectorText === recordedExpected) {
1317
+ traceDebug(`Skipped tautological assertion: selector already matches "${recordedExpected}"`);
1317
1318
  } else {
1318
1319
  const assertName = params.type === "value" ? "assertValue" : "assertText";
1319
1320
  this._trackedActions.push({
@@ -1321,8 +1322,8 @@ Could not parse locator: ${locatorExpr}` }], isError: true };
1321
1322
  // Pass selector/expected as structured args so assertActionToJsonl can
1322
1323
  // decode unambiguously (the colon-encoded code below corrupts values
1323
1324
  // that contain ':', e.g. URLs). The code string is a fallback only.
1324
- args: { type: params.type, ref: params.ref, selector: parsed.selector, expected: params.expected, ...params.type === "text" ? { substring: params.substring } : {} },
1325
- code: `${assertName}:${parsed.selector}:${params.expected}${params.type === "text" ? ":" + params.substring : ""}`,
1325
+ args: { type: params.type, ref: params.ref, selector: parsed.selector, expected: recordedExpected, ...params.type === "text" ? { substring: params.substring } : {} },
1326
+ code: `${assertName}:${parsed.selector}:${recordedExpected}${params.type === "text" ? ":" + params.substring : ""}`,
1326
1327
  timestamp
1327
1328
  });
1328
1329
  traceDebug(`Assert: ${assertName} with selector ${parsed.selector}`);
@@ -1571,14 +1572,14 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1571
1572
  const roleMatch = refLine.match(/^\s*-\s*(\w+)\s+"([^"]*)"/);
1572
1573
  if (roleMatch && TraceRecordingBackend.ASSERTABLE_ROLES.has(roleMatch[1])) {
1573
1574
  const role = roleMatch[1];
1574
- const name = roleMatch[2];
1575
+ const name = (0, import_utils.stableTextLead)(roleMatch[2]);
1575
1576
  return {
1576
1577
  selector: `internal:role=${role}[name="${name}"i]`,
1577
1578
  locator: { kind: "role", body: role, options: { attrs: [], exact: false, name } }
1578
1579
  };
1579
1580
  }
1580
1581
  if (roleMatch && roleMatch[2]) {
1581
- const text = roleMatch[2];
1582
+ const text = (0, import_utils.stableTextLead)(roleMatch[2]);
1582
1583
  return {
1583
1584
  selector: `internal:text="${text}"i`,
1584
1585
  locator: { kind: "text", body: text, options: { exact: false } }
@@ -1586,7 +1587,7 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1586
1587
  }
1587
1588
  const colonTextMatch = refLine.match(/\[ref=\w+\](?:\s*\[[^\]]*\])*\s*:\s*(.+?)\s*$/);
1588
1589
  if (colonTextMatch && colonTextMatch[1].trim()) {
1589
- const text = colonTextMatch[1].trim();
1590
+ const text = (0, import_utils.stableTextLead)(colonTextMatch[1].trim());
1590
1591
  return {
1591
1592
  selector: `internal:text="${text}"i`,
1592
1593
  locator: { kind: "text", body: text, options: { exact: false } }
@@ -1686,6 +1687,71 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1686
1687
  target += `.locator('${frame.replace(/'/g, "\\'")}').contentFrame()`;
1687
1688
  return `await ${target}.${expr}.${verb}(${callArgs});`;
1688
1689
  }
1690
+ /**
1691
+ * SKYR-3840: re-anchor a fragile / index-suffixed testid LEAF onto a stable
1692
+ * anchor. Upstream generateSelector prefers a data-testid because it is unique
1693
+ * at record time, but an index-suffixed testid (choice-card-6, package-name-1)
1694
+ * is a positional counter that shifts when the list reorders or grows. When the
1695
+ * clicked element carries a stable name — its own, or an inner heading title for
1696
+ * card/tile widgets — anchor on role+name / text instead. Mirrors the manual
1697
+ * recorder's ScopingHandler fix (_isFragileTestId + h1-h6→heading) for the
1698
+ * tool-recording path, which codegen passes through verbatim.
1699
+ *
1700
+ * Only no-argument click-family verbs qualify: re-anchoring onto a role+name /
1701
+ * text locator is meaningful for clicking a card/label, but not for
1702
+ * fill/type/selectOption (their target is an input, never a heading/text node,
1703
+ * and the rewrite must not drop the call's value argument).
1704
+ *
1705
+ * Returns the rewritten result, or null to leave the selector unchanged.
1706
+ */
1707
+ async _reanchorFragileTestidLeaf(code, args, result) {
1708
+ const testidLeafMatch = code.match(/^await\s+page\.getByTestId\(\s*(['"])([^'"]+)\1\s*\)\.(click|dblclick|check|uncheck|hover)\(/);
1709
+ if (!testidLeafMatch || !(0, import_dynamicId.isDynamicId)(testidLeafMatch[2]) || !args.ref)
1710
+ return null;
1711
+ const fragileTestid = testidLeafMatch[2];
1712
+ const action = testidLeafMatch[3];
1713
+ const ref = args.ref;
1714
+ const page = this._browserBackend.context?.currentTab()?.page;
1715
+ if (!page)
1716
+ return null;
1717
+ let anchor = null;
1718
+ try {
1719
+ anchor = await page.locator(`aria-ref=${ref}`).evaluate((el) => {
1720
+ const stableLabel = (t) => {
1721
+ if (!t) return null;
1722
+ const s = t.trim().replace(/\s+/g, " ");
1723
+ if (s.length < 2 || s.length > 50) return null;
1724
+ if (/[$£€]\s?\d|\d{4,}|\b\d{1,2}\/\d{1,2}\b/.test(s)) return null;
1725
+ return s;
1726
+ };
1727
+ const heading = el.querySelector('h1,h2,h3,h4,h5,h6,[role="heading"]');
1728
+ const hname = heading ? stableLabel(heading.textContent) : null;
1729
+ if (hname) return { kind: "heading", name: hname };
1730
+ const own = stableLabel([...el.childNodes].filter((n) => n.nodeType === Node.TEXT_NODE).map((n) => n.textContent).join(""));
1731
+ if (own) return { kind: "text", name: own };
1732
+ return null;
1733
+ });
1734
+ } catch (e) {
1735
+ traceDebug(`[reanchor] aria-ref evaluate failed for ref ${ref}: ${e}`);
1736
+ return null;
1737
+ }
1738
+ if (!anchor)
1739
+ return null;
1740
+ let count = 0;
1741
+ try {
1742
+ count = anchor.kind === "heading" ? await page.getByRole("heading", { name: anchor.name }).count() : await page.getByText(anchor.name).count();
1743
+ } catch (e) {
1744
+ traceDebug(`[reanchor] uniqueness count failed for "${anchor.name}": ${e}`);
1745
+ return null;
1746
+ }
1747
+ if (count !== 1) {
1748
+ traceDebug(`[reanchor] anchor "${anchor.name}" not unique (${count}); keeping testid`);
1749
+ return null;
1750
+ }
1751
+ const expr = anchor.kind === "heading" ? `getByRole('heading', { name: ${JSON.stringify(anchor.name)} })` : `getByText(${JSON.stringify(anchor.name)})`;
1752
+ traceDebug(`[reanchor] fragile testid "${fragileTestid}" -> ${expr}`);
1753
+ return this._modifyResultCode(result, `await page.${expr}.${action}();`);
1754
+ }
1689
1755
  async _improveActionSelector(toolName, args, result, preClickHoverCode = null, preClickSnapshot = null, preClickLocatorCount = 0, preClickParentChain = null) {
1690
1756
  if (result.isError)
1691
1757
  return result;
@@ -1696,6 +1762,9 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1696
1762
  const code = parsed?.code ?? "";
1697
1763
  if (!code)
1698
1764
  return result;
1765
+ const reanchored = await this._reanchorFragileTestidLeaf(code, args, result);
1766
+ if (reanchored)
1767
+ return reanchored;
1699
1768
  const cssLocatorMatch = code.match(/await\s+page\.locator\(\s*(['"])([.#][^'"]+)\1\s*\)\./);
1700
1769
  if (cssLocatorMatch) {
1701
1770
  const cssSelector = cssLocatorMatch[2];
@@ -1769,7 +1838,7 @@ Cell row ${params.row}, column ${params.column} has "${actual}", expected "${par
1769
1838
  if (!roleName && ambiguousCount > 1) {
1770
1839
  const textMatch = refLine.match(/^\s*-\s*\w+\s+"([^"]*)"/);
1771
1840
  if (textMatch && textMatch[1]) {
1772
- const visibleText = textMatch[1];
1841
+ const visibleText = (0, import_utils.stableTextLead)(textMatch[1]);
1773
1842
  const textPattern = new RegExp(`^\\s*-\\s*${role}\\s+"[^"]*${visibleText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "i");
1774
1843
  const textCount = snapLines.filter((l) => textPattern.test(l)).length;
1775
1844
  if (textCount === 1) {