@skyramp/mcp 0.2.5 → 0.2.7

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 (186) hide show
  1. package/build/commands/commandLibrary.d.ts +12 -0
  2. package/build/commands/recommendTestsAndExecuteCommand.d.ts +2 -0
  3. package/build/commands/testThisEndpointCommand.d.ts +2 -0
  4. package/build/index.d.ts +2 -0
  5. package/build/index.js +1 -2
  6. package/build/playwright/PlaywrightTraceService.d.ts +32 -0
  7. package/build/playwright/index.d.ts +3 -0
  8. package/build/playwright/registerPlaywrightTools.d.ts +8 -0
  9. package/build/playwright/registerPlaywrightTools.js +12 -11
  10. package/build/playwright/traceRecordingPrompt.d.ts +13 -0
  11. package/build/playwright/traceRecordingPrompt.js +3 -11
  12. package/build/prompts/code-reuse.d.ts +1 -0
  13. package/build/prompts/enhance-assertions/contractProviderAssertionsPrompt.d.ts +2 -0
  14. package/build/prompts/enhance-assertions/integrationAssertionsPrompt.d.ts +2 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +15 -0
  16. package/build/prompts/enhance-assertions/uiAssertionsPrompt.d.ts +2 -0
  17. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +14 -3
  18. package/build/prompts/fix-error-prompt.d.ts +1 -0
  19. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +1 -0
  20. package/build/prompts/modularization/integration-test-modularization.d.ts +1 -0
  21. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  22. package/build/prompts/personas.d.ts +21 -0
  23. package/build/prompts/pom-aware-code-reuse.d.ts +1 -0
  24. package/build/prompts/startTraceCollectionPrompts.d.ts +2 -0
  25. package/build/prompts/test-maintenance/actionsInstructions.d.ts +35 -0
  26. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +4 -0
  27. package/build/prompts/test-maintenance/driftAnalysisSections.d.ts +13 -0
  28. package/build/prompts/test-recommendation/analysisOutputPrompt.d.ts +84 -0
  29. package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +1 -0
  30. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +1 -0
  31. package/build/prompts/test-recommendation/diffExecutionPlan.d.ts +17 -0
  32. package/build/prompts/test-recommendation/diffExecutionPlan.js +7 -16
  33. package/build/prompts/test-recommendation/fullRepoCatalog.d.ts +7 -0
  34. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +1 -0
  35. package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +1 -0
  36. package/build/prompts/test-recommendation/promptPlan.d.ts +203 -0
  37. package/build/prompts/test-recommendation/promptPlan.test.d.ts +1 -0
  38. package/build/prompts/test-recommendation/recommendationSections.d.ts +35 -0
  39. package/build/prompts/test-recommendation/recommendationShared.d.ts +31 -0
  40. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +7 -0
  41. package/build/prompts/test-recommendation/scopeAssessment.d.ts +55 -0
  42. package/build/prompts/test-recommendation/scopeAssessment.js +2 -2
  43. package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +1 -0
  44. package/build/prompts/test-recommendation/scopeAssessment.test.js +1 -0
  45. package/build/prompts/test-recommendation/test-recommendation-prompt.d.ts +6 -0
  46. package/build/prompts/test-recommendation/test-recommendation-prompt.js +4 -4
  47. package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +1 -0
  48. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -99
  49. package/build/prompts/testbot/testbot-prompts.d.ts +11 -0
  50. package/build/prompts/testbot/testbot-prompts.js +198 -34
  51. package/build/prompts/testbot/testbot-prompts.test.d.ts +1 -0
  52. package/build/prompts/testbot/testbot-prompts.test.js +15 -73
  53. package/build/resources/analysisResources.d.ts +17 -0
  54. package/build/resources/progressResource.d.ts +2 -0
  55. package/build/resources/testbotResource.d.ts +2 -0
  56. package/build/services/AnalyticsService.d.ts +13 -0
  57. package/build/services/AnalyticsService.test.d.ts +1 -0
  58. package/build/services/AnalyticsService.test.js +86 -0
  59. package/build/services/ModularizationService.d.ts +11 -0
  60. package/build/services/ScenarioGenerationService.d.ts +37 -0
  61. package/build/services/ScenarioGenerationService.integration.test.d.ts +1 -0
  62. package/build/services/ScenarioGenerationService.integration.test.js +4 -0
  63. package/build/services/ScenarioGenerationService.test.d.ts +1 -0
  64. package/build/services/TestDiscoveryService.d.ts +128 -0
  65. package/build/services/TestDiscoveryService.test.d.ts +1 -0
  66. package/build/services/TestDiscoveryService.test.js +4 -3
  67. package/build/services/TestExecutionService.d.ts +46 -0
  68. package/build/services/TestExecutionService.test.d.ts +1 -0
  69. package/build/services/TestExecutionService.test.js +33 -25
  70. package/build/services/TestGenerationService.d.ts +56 -0
  71. package/build/services/TestGenerationService.test.d.ts +1 -0
  72. package/build/services/TestGenerationService.test.js +10 -8
  73. package/build/services/containerEnv.d.ts +14 -0
  74. package/build/tool-phase-coverage.test.d.ts +1 -0
  75. package/build/tool-phase-coverage.test.js +2 -0
  76. package/build/tool-phases.d.ts +38 -0
  77. package/build/tools/auth/loginTool.d.ts +2 -0
  78. package/build/tools/auth/logoutTool.d.ts +2 -0
  79. package/build/tools/code-refactor/codeReuseTool.d.ts +2 -0
  80. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +2 -0
  81. package/build/tools/code-refactor/modularizationTool.d.ts +2 -0
  82. package/build/tools/executeSkyrampTestTool.d.ts +2 -0
  83. package/build/tools/fixErrorTool.d.ts +2 -0
  84. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +45 -0
  85. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +1 -0
  86. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +11 -9
  87. package/build/tools/generate-tests/generateContractRestTool.d.ts +31 -0
  88. package/build/tools/generate-tests/generateE2ERestTool.d.ts +13 -0
  89. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +10 -0
  90. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +16 -0
  91. package/build/tools/generate-tests/generateLoadRestTool.d.ts +18 -0
  92. package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +1 -0
  93. package/build/tools/generate-tests/generateMockRestTool.d.ts +64 -0
  94. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +8 -0
  95. package/build/tools/generate-tests/generateUIRestTool.d.ts +14 -0
  96. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  97. package/build/tools/generate-tests/loadTestSchema.d.ts +39 -0
  98. package/build/tools/one-click/oneClickTool.d.ts +11 -0
  99. package/build/tools/submitReportTool.d.ts +384 -0
  100. package/build/tools/submitReportTool.js +144 -2
  101. package/build/tools/submitReportTool.test.d.ts +1 -0
  102. package/build/tools/submitReportTool.test.js +215 -5
  103. package/build/tools/test-management/actionsTool.d.ts +10 -0
  104. package/build/tools/test-management/analyzeChangesTool.d.ts +27 -0
  105. package/build/tools/test-management/analyzeChangesTool.js +44 -7
  106. package/build/tools/test-management/analyzeChangesTool.test.d.ts +1 -0
  107. package/build/tools/test-management/analyzeChangesTool.test.js +76 -39
  108. package/build/tools/test-management/analyzeTestHealthTool.d.ts +2 -0
  109. package/build/tools/test-management/index.d.ts +3 -0
  110. package/build/tools/test-management/index.js +0 -1
  111. package/build/tools/trace/resolveSaveStoragePath.d.ts +7 -0
  112. package/build/tools/trace/resolveSaveStoragePath.test.d.ts +1 -0
  113. package/build/tools/trace/resolveSessionPaths.d.ts +40 -0
  114. package/build/tools/trace/resolveSessionPaths.test.d.ts +1 -0
  115. package/build/tools/trace/sessionState.d.ts +2 -0
  116. package/build/tools/trace/sessionState.test.d.ts +1 -0
  117. package/build/tools/trace/startTraceCollectionTool.d.ts +2 -0
  118. package/build/tools/trace/stopTraceCollectionTool.d.ts +2 -0
  119. package/build/tools/workspace/initScanWorkspaceTool.d.ts +4 -0
  120. package/build/tools/workspace/initializeWorkspaceTool.d.ts +2 -0
  121. package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +1 -0
  122. package/build/tools/workspace/initializeWorkspaceTool.test.js +134 -0
  123. package/build/types/OneClickCommands.d.ts +68 -0
  124. package/build/types/RepositoryAnalysis.d.ts +3004 -0
  125. package/build/types/TestAnalysis.d.ts +96 -0
  126. package/build/types/TestExecution.d.ts +53 -0
  127. package/build/types/TestRecommendation.d.ts +12 -0
  128. package/build/types/TestTypes.d.ts +234 -0
  129. package/build/utils/AnalysisStateManager.d.ts +182 -0
  130. package/build/utils/AnalysisStateManager.test.d.ts +1 -0
  131. package/build/utils/analyze-openapi.d.ts +1 -0
  132. package/build/utils/branchDiff.d.ts +18 -0
  133. package/build/utils/dartRouteExtractor.d.ts +45 -0
  134. package/build/utils/dartRouteExtractor.test.d.ts +1 -0
  135. package/build/utils/docker.d.ts +19 -0
  136. package/build/utils/docker.test.d.ts +1 -0
  137. package/build/utils/docker.test.js +10 -9
  138. package/build/utils/featureFlags.d.ts +37 -0
  139. package/build/utils/featureFlags.test.d.ts +1 -0
  140. package/build/utils/gitStaging.d.ts +24 -0
  141. package/build/utils/gitStaging.test.d.ts +1 -0
  142. package/build/utils/gitStaging.test.js +13 -7
  143. package/build/utils/httpDefaults.d.ts +10 -0
  144. package/build/utils/httpDefaults.test.d.ts +1 -0
  145. package/build/utils/initAgent.d.ts +36 -0
  146. package/build/utils/language-helper.d.ts +6 -0
  147. package/build/utils/logger.d.ts +11 -0
  148. package/build/utils/normalizeParams.d.ts +14 -0
  149. package/build/utils/normalizeSkyrampImports.d.ts +13 -0
  150. package/build/utils/pr-comment-parser.d.ts +31 -0
  151. package/build/utils/pr-comment-parser.test.d.ts +1 -0
  152. package/build/utils/pr-comment-parser.test.js +6 -5
  153. package/build/utils/projectMetadata.d.ts +15 -0
  154. package/build/utils/projectMetadata.test.d.ts +1 -0
  155. package/build/utils/proxy-terminal.d.ts +2 -0
  156. package/build/utils/repoScanner.d.ts +27 -0
  157. package/build/utils/repoScanner.test.d.ts +1 -0
  158. package/build/utils/routeParsers.d.ts +63 -0
  159. package/build/utils/routeParsers.test.d.ts +1 -0
  160. package/build/utils/scenarioDrafting.d.ts +71 -0
  161. package/build/utils/scenarioDrafting.test.d.ts +1 -0
  162. package/build/utils/skyrampMdContent.d.ts +5 -0
  163. package/build/utils/sourceRouteExtractor.d.ts +48 -0
  164. package/build/utils/sourceRouteExtractor.test.d.ts +1 -0
  165. package/build/utils/telemetry.d.ts +32 -0
  166. package/build/utils/telemetry.test.d.ts +1 -0
  167. package/build/utils/trace-parser.d.ts +32 -0
  168. package/build/utils/trace-parser.test.d.ts +6 -0
  169. package/build/utils/uiPageEnumerator.d.ts +146 -0
  170. package/build/utils/uiPageEnumerator.js +58 -14
  171. package/build/utils/uiPageEnumerator.test.d.ts +1 -0
  172. package/build/utils/uiPageEnumerator.test.js +223 -43
  173. package/build/utils/utils.d.ts +39 -0
  174. package/build/utils/utils.test.d.ts +1 -0
  175. package/build/utils/versions.d.ts +3 -0
  176. package/build/utils/versions.js +1 -1
  177. package/build/utils/workspaceAuth.d.ts +127 -0
  178. package/build/utils/workspaceAuth.test.d.ts +1 -0
  179. package/build/utils/workspaceAuth.test.js +25 -25
  180. package/build/workspace/index.d.ts +1 -0
  181. package/build/workspace/workspace.d.ts +390 -0
  182. package/build/workspace/workspace.test.d.ts +1 -0
  183. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -2
  184. package/package.json +8 -3
  185. package/build/tools/test-management/uiAnalyzeChangesTool.js +0 -155
  186. package/build/tools/test-management/uiAnalyzeChangesTool.test.js +0 -147
@@ -1,13 +1,20 @@
1
- // Mock @skyramp/skyramp so workspaceAuth's import resolves; readWorkspaceConfigRaw
2
- // itself is mocked per-test below.
3
- jest.mock("@skyramp/skyramp", () => ({
4
- WorkspaceConfigManager: { create: jest.fn() },
5
- }));
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest } from "@jest/globals";
6
3
  import * as fs from "fs";
7
4
  import * as os from "os";
8
5
  import * as path from "path";
9
- import { frameworkFileToUrlPath, findCandidatePagesByFrameworkRoute, findCandidatePagesBySourceRoute, findRootFallbackPage, enumerateCandidateUiPages, pickFrontendBaseUrl, detectsFilesystemRouting, } from "./uiPageEnumerator.js";
10
- import * as workspaceAuth from "./workspaceAuth.js";
6
+ // Under native ESM, imported module namespaces are read-only, so jest.spyOn on
7
+ // workspaceAuth fails. Partial-mock the module: keep every real export the
8
+ // subject's import graph needs and override only readWorkspaceConfigRaw.
9
+ // jest.requireActual can't load an ESM module synchronously, so import the real
10
+ // module first (unstable_mockModule is not hoisted) and spread its namespace.
11
+ const realWorkspaceAuth = await import("./workspaceAuth.js");
12
+ const readWorkspaceConfigRaw = jest.fn();
13
+ jest.unstable_mockModule("./workspaceAuth.js", () => ({
14
+ ...realWorkspaceAuth,
15
+ readWorkspaceConfigRaw,
16
+ }));
17
+ const { frameworkFileToUrlPath, findCandidatePagesByFrameworkRoute, findCandidatePagesBySourceRoute, findRootFallbackPage, enumerateCandidateUiPages, pickFrontendBaseUrl, detectsFilesystemRouting, } = await import("./uiPageEnumerator.js");
11
18
  // Helper to create a tmp repo dir with optional config files; returns the path.
12
19
  function makeTmpRepo(configFiles = []) {
13
20
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-test-repo-"));
@@ -191,25 +198,21 @@ export default function App() {
191
198
  // pickFrontendBaseUrl
192
199
  // ---------------------------------------------------------------------------
193
200
  describe("pickFrontendBaseUrl", () => {
194
- let readSpy;
195
- beforeEach(() => {
196
- readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
197
- });
198
201
  afterEach(() => {
199
- readSpy.mockRestore();
202
+ readWorkspaceConfigRaw.mockReset();
200
203
  });
201
204
  it("returns null when workspace config is missing", async () => {
202
- readSpy.mockResolvedValue(null);
205
+ readWorkspaceConfigRaw.mockResolvedValue(null);
203
206
  expect(await pickFrontendBaseUrl("/repo")).toBeNull();
204
207
  });
205
208
  it("returns null when no services have baseUrl", async () => {
206
- readSpy.mockResolvedValue({
209
+ readWorkspaceConfigRaw.mockResolvedValue({
207
210
  services: [{ serviceName: "backend", language: "python" }],
208
211
  });
209
212
  expect(await pickFrontendBaseUrl("/repo")).toBeNull();
210
213
  });
211
214
  it("picks the frontend service by name suffix", async () => {
212
- readSpy.mockResolvedValue({
215
+ readWorkspaceConfigRaw.mockResolvedValue({
213
216
  services: [
214
217
  { serviceName: "demoshop-backend", api: { baseUrl: "http://localhost:8000" } },
215
218
  { serviceName: "demoshop-frontend", api: { baseUrl: "http://localhost:5173" } },
@@ -218,7 +221,7 @@ describe("pickFrontendBaseUrl", () => {
218
221
  expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:5173");
219
222
  });
220
223
  it("picks the frontend service by framework when name doesn't signal it", async () => {
221
- readSpy.mockResolvedValue({
224
+ readWorkspaceConfigRaw.mockResolvedValue({
222
225
  services: [
223
226
  { serviceName: "api", language: "python", api: { baseUrl: "http://localhost:8000" } },
224
227
  {
@@ -232,7 +235,7 @@ describe("pickFrontendBaseUrl", () => {
232
235
  expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
233
236
  });
234
237
  it("falls back to first service with baseUrl when no frontend signal", async () => {
235
- readSpy.mockResolvedValue({
238
+ readWorkspaceConfigRaw.mockResolvedValue({
236
239
  services: [
237
240
  { serviceName: "svc-a", language: "go", api: { baseUrl: "http://localhost:8000" } },
238
241
  { serviceName: "svc-b", language: "go", api: { baseUrl: "http://localhost:8001" } },
@@ -247,7 +250,7 @@ describe("pickFrontendBaseUrl", () => {
247
250
  // frameworks. Without this, the agent never reaches the browser regardless
248
251
  // of the .dart classifier fix.
249
252
  it("picks the Flutter playwright-framework service by name suffix", async () => {
250
- readSpy.mockResolvedValue({
253
+ readWorkspaceConfigRaw.mockResolvedValue({
251
254
  services: [
252
255
  {
253
256
  serviceName: "birdle-frontend",
@@ -264,7 +267,7 @@ describe("pickFrontendBaseUrl", () => {
264
267
  // allow-list. The chosen URL must come from H3 (first service with baseUrl).
265
268
  // We add a second service to prove H3 picked the first, not H1 by accident.
266
269
  it("falls back to first service when name+framework don't match H1 or H2", async () => {
267
- readSpy.mockResolvedValue({
270
+ readWorkspaceConfigRaw.mockResolvedValue({
268
271
  services: [
269
272
  {
270
273
  serviceName: "birdle",
@@ -282,24 +285,205 @@ describe("pickFrontendBaseUrl", () => {
282
285
  });
283
286
  expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:8080");
284
287
  });
288
+ // SKYR-3812: Environment variable override tests
289
+ describe("env var overrides (SKYR-3812)", () => {
290
+ let originalGlobal;
291
+ let originalPerSvc = {};
292
+ beforeEach(() => {
293
+ // Snapshot env vars this suite may touch so we can restore them exactly.
294
+ originalGlobal = process.env.SKYRAMP_TEST_BASE_URL;
295
+ originalPerSvc = {};
296
+ for (const key of Object.keys(process.env)) {
297
+ if (key.startsWith("SKYRAMP_TEST_SERVICE_URL_")) {
298
+ originalPerSvc[key] = process.env[key];
299
+ }
300
+ }
301
+ });
302
+ afterEach(() => {
303
+ // Restore the global override.
304
+ if (originalGlobal === undefined) {
305
+ delete process.env.SKYRAMP_TEST_BASE_URL;
306
+ }
307
+ else {
308
+ process.env.SKYRAMP_TEST_BASE_URL = originalGlobal;
309
+ }
310
+ // Restore per-service vars: reset the union of keys that existed before
311
+ // the test and keys the test set, so neither pre-existing values nor
312
+ // test-set values leak across tests or out of the suite.
313
+ const keys = new Set([
314
+ ...Object.keys(originalPerSvc),
315
+ ...Object.keys(process.env).filter((k) => k.startsWith("SKYRAMP_TEST_SERVICE_URL_")),
316
+ ]);
317
+ for (const key of keys) {
318
+ if (originalPerSvc[key] === undefined) {
319
+ delete process.env[key];
320
+ }
321
+ else {
322
+ process.env[key] = originalPerSvc[key];
323
+ }
324
+ }
325
+ readWorkspaceConfigRaw.mockReset();
326
+ });
327
+ it("SKYRAMP_TEST_BASE_URL (global) overrides workspace.yml baseUrl", async () => {
328
+ readWorkspaceConfigRaw.mockResolvedValue({
329
+ services: [
330
+ {
331
+ serviceName: "frontend",
332
+ api: { baseUrl: "http://localhost:3000" },
333
+ },
334
+ ],
335
+ });
336
+ process.env.SKYRAMP_TEST_BASE_URL = "https://preview.example.com";
337
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://preview.example.com");
338
+ });
339
+ it("per-service SKYRAMP_TEST_SERVICE_URL_<NAME> overrides for the picked frontend service", async () => {
340
+ readWorkspaceConfigRaw.mockResolvedValue({
341
+ services: [
342
+ { serviceName: "backend", api: { baseUrl: "http://localhost:8000" } },
343
+ {
344
+ serviceName: "demoshop-frontend",
345
+ api: { baseUrl: "http://localhost:5173" },
346
+ },
347
+ ],
348
+ });
349
+ process.env.SKYRAMP_TEST_SERVICE_URL_DEMOSHOP_FRONTEND =
350
+ "https://frontend-preview.example.com";
351
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://frontend-preview.example.com");
352
+ });
353
+ it("sanitizes service name: hyphens, dots, colons, slashes to underscores", async () => {
354
+ readWorkspaceConfigRaw.mockResolvedValue({
355
+ services: [
356
+ {
357
+ serviceName: "supabase-nextjs",
358
+ api: { baseUrl: "http://localhost:3000" },
359
+ },
360
+ ],
361
+ });
362
+ process.env.SKYRAMP_TEST_SERVICE_URL_SUPABASE_NEXTJS =
363
+ "https://supabase-preview.example.com";
364
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://supabase-preview.example.com");
365
+ });
366
+ it("sanitizes service name with dot: my.app -> MY_APP", async () => {
367
+ readWorkspaceConfigRaw.mockResolvedValue({
368
+ services: [
369
+ { serviceName: "my.app", api: { baseUrl: "http://localhost:3000" } },
370
+ ],
371
+ });
372
+ process.env.SKYRAMP_TEST_SERVICE_URL_MY_APP =
373
+ "https://myapp-preview.example.com";
374
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://myapp-preview.example.com");
375
+ });
376
+ it("sanitizes service name with colon: proto://app -> PROTO___APP", async () => {
377
+ readWorkspaceConfigRaw.mockResolvedValue({
378
+ services: [
379
+ {
380
+ serviceName: "proto://app",
381
+ api: { baseUrl: "http://localhost:3000" },
382
+ },
383
+ ],
384
+ });
385
+ process.env.SKYRAMP_TEST_SERVICE_URL_PROTO___APP =
386
+ "https://proto-preview.example.com";
387
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://proto-preview.example.com");
388
+ });
389
+ it("per-service takes precedence over global when both are set", async () => {
390
+ readWorkspaceConfigRaw.mockResolvedValue({
391
+ services: [
392
+ {
393
+ serviceName: "frontend",
394
+ api: { baseUrl: "http://localhost:3000" },
395
+ },
396
+ ],
397
+ });
398
+ process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
399
+ process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND =
400
+ "https://per-service-preview.example.com";
401
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://per-service-preview.example.com");
402
+ });
403
+ it("empty/whitespace global env var is ignored, falls back to workspace.yml", async () => {
404
+ readWorkspaceConfigRaw.mockResolvedValue({
405
+ services: [
406
+ {
407
+ serviceName: "frontend",
408
+ api: { baseUrl: "http://localhost:3000" },
409
+ },
410
+ ],
411
+ });
412
+ process.env.SKYRAMP_TEST_BASE_URL = " ";
413
+ expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
414
+ });
415
+ it("empty/whitespace per-service env var is ignored, falls back to global", async () => {
416
+ readWorkspaceConfigRaw.mockResolvedValue({
417
+ services: [
418
+ {
419
+ serviceName: "frontend",
420
+ api: { baseUrl: "http://localhost:3000" },
421
+ },
422
+ ],
423
+ });
424
+ process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
425
+ process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = " ";
426
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
427
+ });
428
+ it("global override returned even with no matching service", async () => {
429
+ readWorkspaceConfigRaw.mockResolvedValue({
430
+ services: [
431
+ {
432
+ serviceName: "backend",
433
+ language: "python",
434
+ api: { baseUrl: "http://localhost:8000" },
435
+ },
436
+ ],
437
+ });
438
+ process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
439
+ // pickFrontendBaseUrl will select the first service with baseUrl (H3), but the override takes precedence
440
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
441
+ });
442
+ it("existing no-override behavior still returns workspace.yml value", async () => {
443
+ readWorkspaceConfigRaw.mockResolvedValue({
444
+ services: [
445
+ {
446
+ serviceName: "frontend",
447
+ api: { baseUrl: "http://localhost:3000" },
448
+ },
449
+ ],
450
+ });
451
+ // No env vars set
452
+ expect(await pickFrontendBaseUrl("/repo")).toBe("http://localhost:3000");
453
+ });
454
+ it("does not throw when picked service has a non-string serviceName (unvalidated YAML)", async () => {
455
+ // readWorkspaceConfigRaw can fall back to an unvalidated YAML parse where
456
+ // serviceName is e.g. a number. The per-service guard must skip it rather
457
+ // than calling sanitizeServiceName on a non-string and throwing.
458
+ readWorkspaceConfigRaw.mockResolvedValue({
459
+ services: [
460
+ {
461
+ serviceName: 123,
462
+ language: "typescript",
463
+ framework: "next",
464
+ api: { baseUrl: "http://localhost:3000" },
465
+ },
466
+ ],
467
+ });
468
+ process.env.SKYRAMP_TEST_BASE_URL = "https://global-preview.example.com";
469
+ // Falls through the per-service branch to the global override.
470
+ expect(await pickFrontendBaseUrl("/repo")).toBe("https://global-preview.example.com");
471
+ });
472
+ });
285
473
  });
286
474
  // ---------------------------------------------------------------------------
287
475
  // findRootFallbackPage
288
476
  // ---------------------------------------------------------------------------
289
477
  describe("findRootFallbackPage", () => {
290
- let readSpy;
291
- beforeEach(() => {
292
- readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
293
- });
294
478
  afterEach(() => {
295
- readSpy.mockRestore();
479
+ readWorkspaceConfigRaw.mockReset();
296
480
  });
297
481
  it("returns null when no frontend baseUrl resolvable", async () => {
298
- readSpy.mockResolvedValue(null);
482
+ readWorkspaceConfigRaw.mockResolvedValue(null);
299
483
  expect(await findRootFallbackPage("/repo")).toBeNull();
300
484
  });
301
485
  it("returns root-fallback page when frontend baseUrl resolves", async () => {
302
- readSpy.mockResolvedValue({
486
+ readWorkspaceConfigRaw.mockResolvedValue({
303
487
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
304
488
  });
305
489
  const page = await findRootFallbackPage("/repo");
@@ -314,22 +498,18 @@ describe("findRootFallbackPage", () => {
314
498
  // enumerateCandidateUiPages
315
499
  // ---------------------------------------------------------------------------
316
500
  describe("enumerateCandidateUiPages", () => {
317
- let readSpy;
318
- beforeEach(() => {
319
- readSpy = jest.spyOn(workspaceAuth, "readWorkspaceConfigRaw");
320
- });
321
501
  afterEach(() => {
322
- readSpy.mockRestore();
502
+ readWorkspaceConfigRaw.mockReset();
323
503
  });
324
504
  it("returns empty for empty file list", async () => {
325
505
  expect(await enumerateCandidateUiPages("/repo", [])).toEqual([]);
326
506
  });
327
507
  it("returns empty when workspace has no resolvable baseUrl", async () => {
328
- readSpy.mockResolvedValue(null);
508
+ readWorkspaceConfigRaw.mockResolvedValue(null);
329
509
  expect(await enumerateCandidateUiPages("/repo", ["app/orders/page.tsx"])).toEqual([]);
330
510
  });
331
511
  it("uses strategy 1 (framework-route-grep) when route files match", async () => {
332
- readSpy.mockResolvedValue({
512
+ readWorkspaceConfigRaw.mockResolvedValue({
333
513
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
334
514
  });
335
515
  const repo = makeTmpRepo(["next.config.js"]);
@@ -345,7 +525,7 @@ describe("enumerateCandidateUiPages", () => {
345
525
  fs.rmSync(repo, { recursive: true, force: true });
346
526
  });
347
527
  it("falls back to strategy 3 (root-fallback) when no route files match", async () => {
348
- readSpy.mockResolvedValue({
528
+ readWorkspaceConfigRaw.mockResolvedValue({
349
529
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
350
530
  });
351
531
  const pages = await enumerateCandidateUiPages("/repo", [
@@ -359,7 +539,7 @@ describe("enumerateCandidateUiPages", () => {
359
539
  });
360
540
  });
361
541
  it("emits multiple route pages when several match strategy 1", async () => {
362
- readSpy.mockResolvedValue({
542
+ readWorkspaceConfigRaw.mockResolvedValue({
363
543
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
364
544
  });
365
545
  // Need a real repoPath with framework config so detectsFilesystemRouting passes.
@@ -377,7 +557,7 @@ describe("enumerateCandidateUiPages", () => {
377
557
  fs.rmSync(repo, { recursive: true, force: true });
378
558
  });
379
559
  it("uses strategy 2 (source-grounded-routes) when strategy 1 doesn't match but routes are declared in source", async () => {
380
- readSpy.mockResolvedValue({
560
+ readWorkspaceConfigRaw.mockResolvedValue({
381
561
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:5173" } }],
382
562
  });
383
563
  // Repo with no framework config (so strategy 1 won't fire) but with a
@@ -401,7 +581,7 @@ export default function App() { return <Routes><Route path="/cart" element={<Car
401
581
  fs.rmSync(repo, { recursive: true, force: true });
402
582
  });
403
583
  it("falls back to root when repo lacks a framework config (no false positives)", async () => {
404
- readSpy.mockResolvedValue({
584
+ readWorkspaceConfigRaw.mockResolvedValue({
405
585
  services: [{ serviceName: "frontend", api: { baseUrl: "http://localhost:3000" } }],
406
586
  });
407
587
  // Repo has files matching the Pages Router pattern but no next.config —
@@ -423,7 +603,7 @@ export default function App() { return <Routes><Route path="/cart" element={<Car
423
603
  // (no framework config; TS extractor doesn't see .dart; no GoRoute calls).
424
604
  // Result: root-fallback with the workspace baseUrl.
425
605
  it("returns root-fallback for a Flutter repo with no GoRoute declarations", async () => {
426
- readSpy.mockResolvedValue({
606
+ readWorkspaceConfigRaw.mockResolvedValue({
427
607
  services: [
428
608
  {
429
609
  serviceName: "birdle-frontend",
@@ -449,7 +629,7 @@ export default function App() { return <Routes><Route path="/cart" element={<Car
449
629
  // surfaces every absolute route as a candidate URL via the all-routes
450
630
  // fallback. Diff-match narrowing is exercised by the next test.
451
631
  it("returns dart-go-router URLs for a Flutter repo with GoRoute declarations", async () => {
452
- readSpy.mockResolvedValue({
632
+ readWorkspaceConfigRaw.mockResolvedValue({
453
633
  services: [
454
634
  {
455
635
  serviceName: "books-frontend",
@@ -482,7 +662,7 @@ final router = GoRouter(routes: [
482
662
  // whose screen file is in frontendFiles. The fixture has 3 routes; only
483
663
  // /authors's screen file (lib/src/screens/authors.dart) is in the diff.
484
664
  it("narrows GoRouter candidates to routes whose screen file is in the diff", async () => {
485
- readSpy.mockResolvedValue({
665
+ readWorkspaceConfigRaw.mockResolvedValue({
486
666
  services: [
487
667
  {
488
668
  serviceName: "books-frontend",
@@ -519,7 +699,7 @@ final router = GoRouter(routes: [
519
699
  // route is redirect-only, so we should fall back to the non-redirect-only
520
700
  // routes.
521
701
  it("filters out redirect-only GoRoutes", async () => {
522
- readSpy.mockResolvedValue({
702
+ readWorkspaceConfigRaw.mockResolvedValue({
523
703
  services: [
524
704
  {
525
705
  serviceName: "frontend",
@@ -547,7 +727,7 @@ final router = GoRouter(routes: [
547
727
  // file edited but our import-resolution heuristic missed the screen), surface
548
728
  // ALL renderable routes rather than collapsing to empty.
549
729
  it("falls back to all renderable routes when no screen file matches the diff", async () => {
550
- readSpy.mockResolvedValue({
730
+ readWorkspaceConfigRaw.mockResolvedValue({
551
731
  services: [
552
732
  {
553
733
  serviceName: "frontend",
@@ -580,7 +760,7 @@ final router = GoRouter(routes: [
580
760
  // (e.g. shelf_router uses similar names in tests) must NOT trigger Strategy
581
761
  // 2.5 — pubspec.yaml lacks `sdk: flutter`, hasFlutterSdkDep returns false.
582
762
  it("does not invoke Dart extractor when pubspec.yaml lacks sdk: flutter", async () => {
583
- readSpy.mockResolvedValue({
763
+ readWorkspaceConfigRaw.mockResolvedValue({
584
764
  services: [
585
765
  {
586
766
  serviceName: "server-frontend",
@@ -0,0 +1,39 @@
1
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
+ export declare function readDiffFile(diffFilePath: string | undefined): string | undefined;
3
+ export declare function toolError(message: string): CallToolResult;
4
+ /**
5
+ * Happy-path counterpart to `toolError`. Wraps a single text payload in the
6
+ * MCP tool-result shape so handlers don't have to construct
7
+ * `{ content: [{ type: "text", text }] }` boilerplate themselves.
8
+ */
9
+ export declare function toolText(text: string): CallToolResult;
10
+ export declare const OUTPUT_DIR_FIELD_NAME = "outputDir";
11
+ export declare const TRACE_OUTPUT_FILE_FIELD_NAME = "traceOutputFile";
12
+ export declare const PLAYWRIGHT_OUTPUT_FILE_FIELD_NAME = "playwrightOutput";
13
+ export declare const PATH_PARAMS_FIELD_NAME = "pathParams";
14
+ export declare const QUERY_PARAMS_FIELD_NAME = "queryParams";
15
+ export declare const FORM_PARAMS_FIELD_NAME = "formParams";
16
+ export declare function IsValidKeyValueList(input: string): boolean;
17
+ export declare const SKYRAMP_UTILS_HEADER = "Generated by Skyramp";
18
+ export declare function getPathParameterValidationError(requiredPathParams: string): string;
19
+ export declare function validatePath(dir: string, fieldName: string): CallToolResult | null;
20
+ export declare function validateParams(params: string, fieldName: string): CallToolResult | null;
21
+ export declare function validateRequestData(value: string): string | null;
22
+ /**
23
+ * Generates a Skyramp header for generated files
24
+ * @param language The programming language for the file
25
+ * @returns The formatted header string
26
+ */
27
+ export declare function generateSkyrampHeader(language: string): string;
28
+ /**
29
+ * Returns prompt phrasing for where the agent should find service details.
30
+ *
31
+ * - Testbot mode: agent receives a `<services>` XML block — no workspace.yml exists.
32
+ * - Normal MCP mode: agent reads `.skyramp/workspace.yml`.
33
+ */
34
+ export declare function resolveServiceDetailsRef(): {
35
+ testDirRef: string;
36
+ frontendTestDirRef: string;
37
+ baseUrlRef: string;
38
+ authSourceRef: string;
39
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.28";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.28";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.28";
@@ -1,3 +1,3 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.27";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.28";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Reads `.skyramp/workspace.yml`.
3
+ *
4
+ * Tries the library's Zod-validated `WorkspaceConfigManager.read()` first so
5
+ * well-formed workspaces produce a normalized object. When validation fails
6
+ * (e.g. a hand-rolled fixture that omits `metadata.createdAt` /`updatedAt`,
7
+ * or any new field added by a future schema), falls back to a raw YAML parse.
8
+ *
9
+ * Callers are expected to read only the fields they need (`services`,
10
+ * `services[].api.baseUrl`, etc.) — they should not depend on the strict
11
+ * shape across both paths.
12
+ *
13
+ * Returns null if the file does not exist OR cannot be parsed as YAML at all.
14
+ */
15
+ export declare function readWorkspaceConfigRaw(workspacePath: string): Promise<any | null>;
16
+ export interface WorkspaceServiceCandidate {
17
+ serviceName: string;
18
+ baseUrl: string;
19
+ }
20
+ export interface WorkspaceBaseUrlResult {
21
+ /** Resolved scheme+host+port when exactly one service matched. */
22
+ baseUrl: string | undefined;
23
+ /** Docker network from the matched service's runtimeDetails. */
24
+ dockerNetwork: string | undefined;
25
+ /**
26
+ * When baseUrl is undefined and candidates is non-empty, multiple services
27
+ * matched the test file but could not be disambiguated. Surface these to the
28
+ * LLM so it can ask the user or re-invoke with SKYRAMP_TEST_BASE_URL set.
29
+ */
30
+ candidates: WorkspaceServiceCandidate[];
31
+ }
32
+ /**
33
+ * Returns scheme+host+port for the service that owns the given test file.
34
+ *
35
+ * Matching strategy (in order):
36
+ * 1. Filter services whose `testDirectory` is a path prefix of `testFile`.
37
+ * 2. If multiple services match (shared testDirectory), narrow by `language`.
38
+ * 3. If still ambiguous, return all candidates so the LLM can resolve it —
39
+ * never guess when multiple services are plausible owners.
40
+ *
41
+ * e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
42
+ * → { baseUrl: "http://localhost:8000", candidates: [] }
43
+ */
44
+ export declare function getWorkspaceBaseUrl(repositoryPath: string, testFile?: string, language?: string): Promise<WorkspaceBaseUrlResult>;
45
+ /**
46
+ * Known auth-related HTTP header names (lowercase).
47
+ * Single source of truth used for both:
48
+ * - trace redaction (trace-parser.ts — headers to scrub from recorded traces)
49
+ * - auth extraction (TestGenerationService — headers to pull out as auth credentials)
50
+ */
51
+ export declare const KNOWN_AUTH_HEADERS: Set<string>;
52
+ /**
53
+ * Cross-framework auth middleware signal patterns used by both the testbot and
54
+ * recommendation prompts to detect auth when workspace config says authType: none.
55
+ *
56
+ * Format: plain strings (e.g. "HTTPBearer", "@PreAuthorize")
57
+ *
58
+ * This is the single source of truth — prompts must import and use this list
59
+ * instead of hardcoding pattern names inline.
60
+ */
61
+ export declare const AUTH_MIDDLEWARE_PATTERNS: string[];
62
+ /** Rendered as a comma-separated string for use in prompts. */
63
+ export declare const AUTH_MIDDLEWARE_PATTERNS_STR: string;
64
+ export declare enum WorkspaceAuthType {
65
+ Bearer = "bearer",
66
+ Basic = "basic",
67
+ OAuth = "oauth",
68
+ /** Generic custom-header auth (X-API-Key, X-Admin-Key, etc.) */
69
+ ApiKey = "apiKey",
70
+ /** Session cookie auth — header is typically Cookie: <name>=<value> */
71
+ Cookie = "cookie",
72
+ /** Alias for Cookie; some workspace configs write authType: session */
73
+ Session = "session",
74
+ /** Django REST Framework / token-based: Authorization: Token <value> */
75
+ Token = "token",
76
+ None = "none"
77
+ }
78
+ /**
79
+ * All valid authType values as a tuple for use in zod enums.
80
+ * Single source of truth — tools and prompts should use this instead of hardcoding.
81
+ */
82
+ export declare const ALL_AUTH_TYPES: [string, ...string[]];
83
+ /**
84
+ * Formatted list of authType values for use in prompt text.
85
+ * Example: "`bearer`, `basic`, `oauth`, `token`, `cookie`, `session`, `apiKey`, or `none`"
86
+ */
87
+ export declare const AUTH_TYPES_PROMPT_LIST: string;
88
+ export declare function parseWorkspaceAuthType(value: string | undefined): WorkspaceAuthType | undefined;
89
+ /**
90
+ * Returns the conventional HTTP header name for an auth type when authHeader
91
+ * is not explicitly configured in workspace.yml.
92
+ *
93
+ * This is the single source of truth for the authType → authHeader mapping.
94
+ * All prompt builders and tools must use this instead of hardcoding header names.
95
+ *
96
+ * - cookie / session → "Cookie" (always goes in Cookie header)
97
+ * - apiKey → "" (custom header; caller must read source)
98
+ * - none → ""
99
+ * - "" / undefined → "" (do not infer a header when authType is missing)
100
+ * - bearer / basic / oauth / token → "Authorization"
101
+ */
102
+ export declare function getDefaultAuthHeader(authType: WorkspaceAuthType | "" | undefined): string;
103
+ /**
104
+ * Returns true when the given header NAME string is the Authorization header.
105
+ *
106
+ * Use this instead of sprinkling /^authorization$/i regexes across prompts and tools.
107
+ */
108
+ export declare function isAuthorizationHeaderName(headerName: string): boolean;
109
+ /**
110
+ * Maps a WorkspaceAuthType to the canonical HTTP Authorization scheme prefix
111
+ * (the word that appears before the token in "Authorization: <scheme> <token>").
112
+ *
113
+ * If `explicitScheme` is provided (from workspace.yml api.authScheme), it takes
114
+ * precedence over the inferred scheme. This allows custom schemes like "Ghost",
115
+ * "Hasura", or any internal API without hardcoding them in the codebase.
116
+ *
117
+ * Returns undefined for auth types that do NOT use the Authorization header
118
+ * (Cookie, ApiKey, None) — callers should not pass authScheme in those cases.
119
+ */
120
+ export declare function getAuthScheme(authType: WorkspaceAuthType | undefined, explicitScheme?: string): string | undefined;
121
+ export interface WorkspaceAuthConfig {
122
+ authHeader?: string;
123
+ authType?: WorkspaceAuthType;
124
+ /** Explicit Authorization scheme from workspace.yml (e.g., "Ghost", "Hasura", custom). */
125
+ authScheme?: string;
126
+ }
127
+ export declare function getWorkspaceAuthConfig(repositoryPath: string): Promise<WorkspaceAuthConfig>;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,29 +1,29 @@
1
- import { getWorkspaceBaseUrl } from "./workspaceAuth.js";
1
+ // @ts-nocheck - Jest ESM mock type inference issues
2
+ import { jest } from "@jest/globals";
2
3
  const REPO = "/some/repo";
3
- jest.mock("../workspace/workspace.js", () => {
4
- const twoServiceConfig = {
5
- services: [
6
- {
7
- serviceName: "backend",
8
- language: "python",
9
- testDirectory: "backend/tests",
10
- api: { baseUrl: "http://localhost:8000" },
11
- },
12
- {
13
- serviceName: "frontend",
14
- language: "typescript",
15
- testDirectory: "frontend/tests",
16
- api: { baseUrl: "http://localhost:5173" },
17
- },
18
- ],
19
- };
20
- return {
21
- WorkspaceConfigManager: jest.fn().mockImplementation(() => ({
22
- exists: jest.fn().mockResolvedValue(true),
23
- read: jest.fn().mockResolvedValue(twoServiceConfig),
24
- })),
25
- };
26
- });
4
+ const twoServiceConfig = {
5
+ services: [
6
+ {
7
+ serviceName: "backend",
8
+ language: "python",
9
+ testDirectory: "backend/tests",
10
+ api: { baseUrl: "http://localhost:8000" },
11
+ },
12
+ {
13
+ serviceName: "frontend",
14
+ language: "typescript",
15
+ testDirectory: "frontend/tests",
16
+ api: { baseUrl: "http://localhost:5173" },
17
+ },
18
+ ],
19
+ };
20
+ jest.unstable_mockModule("../workspace/workspace.js", () => ({
21
+ WorkspaceConfigManager: jest.fn().mockImplementation(() => ({
22
+ exists: jest.fn().mockResolvedValue(true),
23
+ read: jest.fn().mockResolvedValue(twoServiceConfig),
24
+ })),
25
+ }));
26
+ const { getWorkspaceBaseUrl } = await import("./workspaceAuth.js");
27
27
  describe("getWorkspaceBaseUrl", () => {
28
28
  it("should return no match when testFile is not provided", async () => {
29
29
  const result = await getWorkspaceBaseUrl(REPO);
@@ -0,0 +1 @@
1
+ export * from "./workspace.js";