@skyramp/mcp 0.3.2-rc.pom-4 → 0.3.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 (132) hide show
  1. package/build/adapters/jestAdapter.d.ts +14 -0
  2. package/build/adapters/jestAdapter.js +113 -0
  3. package/build/adapters/mochaAdapter.d.ts +13 -0
  4. package/build/adapters/mochaAdapter.js +87 -0
  5. package/build/adapters/playwrightAdapter.d.ts +17 -0
  6. package/build/adapters/playwrightAdapter.js +182 -0
  7. package/build/adapters/pytestAdapter.d.ts +15 -0
  8. package/build/adapters/pytestAdapter.js +108 -0
  9. package/build/commands/commandLibrary.d.ts +1 -0
  10. package/build/commands/commandLibrary.js +19 -13
  11. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  12. package/build/commands/localDevTestChangesCommand.js +201 -0
  13. package/build/index.js +82 -6
  14. package/build/prompts/code-reuse.js +3 -0
  15. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  16. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  18. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  19. package/build/prompts/local-dev/local-dev-plan.js +429 -0
  20. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  21. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  22. package/build/prompts/prompt-utils.d.ts +8 -0
  23. package/build/prompts/prompt-utils.js +33 -0
  24. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +70 -7
  25. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +1 -1
  26. package/build/prompts/sut-setup/shared.js +19 -17
  27. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +10 -1
  28. package/build/prompts/test-maintenance/drift-analysis-prompt.js +54 -1
  29. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  30. package/build/prompts/test-recommendation/scopeAssessment.d.ts +5 -2
  31. package/build/prompts/test-recommendation/scopeAssessment.js +78 -6
  32. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  33. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  34. package/build/prompts/testbot/testbot-prompts.js +39 -55
  35. package/build/recommendation/planRanker.d.ts +15 -2
  36. package/build/recommendation/planRanker.js +76 -5
  37. package/build/resources/testbotResource.js +2 -1
  38. package/build/services/AnalyticsService.d.ts +1 -1
  39. package/build/services/TestExecutionService.d.ts +2 -1
  40. package/build/services/TestExecutionService.js +8 -3
  41. package/build/services/TestGenerationService.d.ts +2 -2
  42. package/build/services/TestGenerationService.js +39 -21
  43. package/build/services/containerEnv.js +3 -1
  44. package/build/tool-phases.js +7 -0
  45. package/build/tools/code-refactor/codeReuseTool.js +43 -4
  46. package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
  47. package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
  48. package/build/tools/code-refactor/reuse-outcome.js +158 -0
  49. package/build/tools/code-refactor/reuse-state.d.ts +45 -0
  50. package/build/tools/code-refactor/reuse-state.js +140 -0
  51. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  52. package/build/tools/enrichTestWithMocksTool.js +726 -0
  53. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  54. package/build/tools/executeSkyrampTestTool.js +62 -21
  55. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
  56. package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
  57. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
  58. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  59. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
  60. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  61. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
  62. package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
  63. package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
  64. package/build/tools/generate-tests/generateMockRestTool.js +391 -22
  65. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  66. package/build/tools/generate-tests/planGuard.js +2 -22
  67. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  68. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  69. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  70. package/build/tools/localDevWorkerComposeTool.js +264 -0
  71. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  72. package/build/tools/one-click/oneClickTool.js +195 -24
  73. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  74. package/build/tools/preflightMockCheckTool.js +96 -0
  75. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  76. package/build/tools/queryProxyMocksTool.js +522 -0
  77. package/build/tools/runExistingTestsTool.d.ts +138 -0
  78. package/build/tools/runExistingTestsTool.js +644 -0
  79. package/build/tools/submitReportTool.js +30 -1
  80. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  81. package/build/tools/test-management/analyzeChangesTool.js +63 -40
  82. package/build/tools/test-management/analyzeTestHealthTool.d.ts +11 -0
  83. package/build/tools/test-management/analyzeTestHealthTool.js +63 -1
  84. package/build/tools/test-management/registerTestPlanTool.js +55 -7
  85. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  86. package/build/types/ExternalTestExecution.d.ts +67 -0
  87. package/build/types/ExternalTestExecution.js +8 -0
  88. package/build/types/OneClickCommands.d.ts +1 -1
  89. package/build/types/Recommendation.d.ts +20 -0
  90. package/build/types/Recommendation.js +32 -6
  91. package/build/types/RepositoryAnalysis.d.ts +131 -14
  92. package/build/types/RepositoryAnalysis.js +16 -2
  93. package/build/types/ReuseOutcome.d.ts +63 -0
  94. package/build/types/ReuseOutcome.js +33 -0
  95. package/build/types/TestExecution.d.ts +1 -0
  96. package/build/types/TestTypes.d.ts +25 -7
  97. package/build/types/TestTypes.js +25 -7
  98. package/build/types/TestbotReport.d.ts +7 -0
  99. package/build/types/index.d.ts +2 -0
  100. package/build/types/index.js +1 -0
  101. package/build/utils/AnalysisStateManager.d.ts +33 -0
  102. package/build/utils/AnalysisStateManager.js +36 -2
  103. package/build/utils/analyze-openapi.js +18 -1
  104. package/build/utils/branchDiff.d.ts +17 -1
  105. package/build/utils/branchDiff.js +99 -14
  106. package/build/utils/featureFlags.d.ts +31 -0
  107. package/build/utils/featureFlags.js +37 -0
  108. package/build/utils/grpcMockValidation.d.ts +1 -0
  109. package/build/utils/grpcMockValidation.js +49 -0
  110. package/build/utils/httpMethodValidation.d.ts +4 -0
  111. package/build/utils/httpMethodValidation.js +15 -0
  112. package/build/utils/logger.js +1 -1
  113. package/build/utils/mockCompatibility.d.ts +49 -0
  114. package/build/utils/mockCompatibility.js +82 -0
  115. package/build/utils/pom-verify/verify.d.ts +5 -0
  116. package/build/utils/pom-verify/verify.js +1 -0
  117. package/build/utils/progress.js +10 -5
  118. package/build/utils/proxy-terminal.js +3 -3
  119. package/build/utils/routeParsers.d.ts +3 -9
  120. package/build/utils/routeParsers.js +79 -4
  121. package/build/utils/utils.js +2 -2
  122. package/build/utils/versions.d.ts +4 -3
  123. package/build/utils/versions.js +3 -1
  124. package/build/utils/workspaceAuth.d.ts +46 -0
  125. package/build/utils/workspaceAuth.js +156 -1
  126. package/build/workspace/frameworks.d.ts +11 -0
  127. package/build/workspace/frameworks.js +22 -0
  128. package/build/workspace/testSuites.d.ts +20 -0
  129. package/build/workspace/testSuites.js +17 -0
  130. package/build/workspace/workspace.d.ts +206 -24
  131. package/build/workspace/workspace.js +52 -2
  132. package/package.json +5 -2
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Mock Compatibility Pre-flight Check
3
+ *
4
+ * Validates mock protocol combinations BEFORE deployment to surface
5
+ * known incompatibilities early rather than at test execution time.
6
+ */
7
+ import { MockProtocol } from "../types/TestTypes.js";
8
+ export { MockProtocol };
9
+ function parseRestHostname(endpointURL) {
10
+ try {
11
+ const hostname = new URL(endpointURL).hostname.toLowerCase();
12
+ return hostname.startsWith("[") && hostname.endsWith("]")
13
+ ? hostname.slice(1, -1)
14
+ : hostname;
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ function isLoopbackRestEndpoint(endpointURL) {
21
+ const hostname = parseRestHostname(endpointURL);
22
+ return (hostname === "localhost" ||
23
+ hostname === "127.0.0.1" ||
24
+ hostname === "0.0.0.0" ||
25
+ hostname === "::1" ||
26
+ hostname === "host.docker.internal");
27
+ }
28
+ /**
29
+ * Validates a set of mock specs for known protocol compatibility issues.
30
+ *
31
+ * Rules:
32
+ * - Any gRPC mock present → GRPC_PERSISTENT_CONN warning (DNS hijacking skips persistent conns)
33
+ * - Docker local-dev REST mock using loopback/host.docker.internal → REST_LOOPBACK_URL blocking warning
34
+ *
35
+ * REST mocks work correctly in Docker local-dev mode only when the mock URL uses
36
+ * the real Docker service hostname (e.g. "billing" from "http://billing:5001").
37
+ * The worker adds that hostname as a Docker network alias so DNS resolves to the mock server.
38
+ *
39
+ * `compatible` is false when any warning is blocking and the mock cannot intercept traffic.
40
+ * `hasBlockers` is true when at least one warning is blocking (mock must not be generated/deployed).
41
+ */
42
+ export function validateMockCompatibility(mocks, options = {}) {
43
+ const warnings = [];
44
+ const restMocks = mocks.filter((m) => m.protocol === MockProtocol.Rest);
45
+ const grpcMocks = mocks.filter((m) => m.protocol === MockProtocol.Grpc);
46
+ const loopbackRestMocks = options.dockerLocalDev
47
+ ? restMocks.filter((m) => isLoopbackRestEndpoint(m.endpointURL))
48
+ : [];
49
+ if (loopbackRestMocks.length > 0) {
50
+ warnings.push({
51
+ code: "REST_LOOPBACK_URL",
52
+ message: "REST mocks for Docker local-dev must use the original Docker service hostname. " +
53
+ "Loopback hosts such as localhost, 127.0.0.1, 0.0.0.0, ::1, or host.docker.internal " +
54
+ "do not give the Skyramp worker the downstream service DNS alias it needs to intercept traffic.",
55
+ suggestion: "Use the real Docker service origin in endpointURL, for example http://ams:4000 or " +
56
+ "http://billing:5001. Do not use localhost, host.docker.internal, or the Skyramp worker address " +
57
+ "for deployable REST mocks.",
58
+ affectedMocks: loopbackRestMocks.map((m) => m.endpointURL),
59
+ blocking: true,
60
+ });
61
+ }
62
+ // Rule: gRPC mocks use DNS hijacking that doesn't intercept persistent connections
63
+ if (grpcMocks.length > 0) {
64
+ warnings.push({
65
+ code: "GRPC_PERSISTENT_CONN",
66
+ message: "gRPC mocks use DNS hijacking which does not intercept persistent connections. " +
67
+ "If the service under test establishes a gRPC connection before mocks are applied, " +
68
+ "the existing connection will bypass the mock.",
69
+ suggestion: "Apply gRPC mocks before the service under test starts, or restart " +
70
+ "the service after applying mocks so it establishes a new connection through the mock DNS. " +
71
+ "For Docker local-dev, also verify the worker has the original downstream hostname alias " +
72
+ "from endpointURL (for example, partner-accounts for partner-accounts:50051).",
73
+ affectedMocks: grpcMocks.map((m) => m.grpcServiceName || m.endpointURL),
74
+ });
75
+ }
76
+ const hasBlockers = warnings.some((w) => w.blocking);
77
+ return {
78
+ compatible: !hasBlockers,
79
+ hasBlockers,
80
+ warnings,
81
+ };
82
+ }
@@ -10,5 +10,10 @@ export type VerifyResult = {
10
10
  unverifiable: string[];
11
11
  parseError?: string;
12
12
  checkedCalls: number;
13
+ /** Every POM call found in the spec, verifiable or not. NOT checkedCalls +
14
+ * unverifiable.length: `unverifiable` also absorbs `unrecognized` bindings,
15
+ * which are not calls at all, so that sum overcounts. This is the count of
16
+ * POM members the delivered spec actually reuses. */
17
+ reusedCalls: number;
13
18
  };
14
19
  export declare function verifyReuse(testFile: string, language: string): Promise<VerifyResult>;
@@ -182,5 +182,6 @@ export async function verifyReuse(testFile, language) {
182
182
  unverifiable,
183
183
  parseError,
184
184
  checkedCalls,
185
+ reusedCalls: calls.length,
185
186
  };
186
187
  }
@@ -10,11 +10,16 @@ export function makeProgressReporter(extra) {
10
10
  return async (progress, total, message) => {
11
11
  const progressToken = extra._meta?.progressToken;
12
12
  if (progressToken !== undefined) {
13
- const notification = {
14
- method: "notifications/progress",
15
- params: { progressToken, progress, total, message },
16
- };
17
- await extra.sendNotification(notification);
13
+ try {
14
+ const notification = {
15
+ method: "notifications/progress",
16
+ params: { progressToken, progress, total, message },
17
+ };
18
+ await extra.sendNotification(notification);
19
+ }
20
+ catch {
21
+ // Progress is best-effort; a disconnected client should not fail the tool.
22
+ }
18
23
  }
19
24
  };
20
25
  }
@@ -334,7 +334,7 @@ export async function closeProxyTerminal() {
334
334
  terminalPID,
335
335
  });
336
336
  }
337
- catch (killErr) {
337
+ catch (_killErr) {
338
338
  // Process is already dead, which is what we want
339
339
  logger.debug("Terminal process already terminated", {
340
340
  terminalPID,
@@ -384,14 +384,14 @@ export async function closeProxyTerminal() {
384
384
  terminalPID,
385
385
  });
386
386
  }
387
- catch (killErr) {
387
+ catch (_killErr) {
388
388
  // Process is already dead, which is what we want
389
389
  logger.debug("Terminal process already terminated", {
390
390
  terminalPID,
391
391
  });
392
392
  }
393
393
  }
394
- catch (pidErr) {
394
+ catch (_pidErr) {
395
395
  // Fallback to regular process kill
396
396
  try {
397
397
  process.kill(terminalPID, "SIGTERM");
@@ -59,15 +59,9 @@ export declare function isRouteLikeFile(filePath: string): boolean;
59
59
  * route that existed in the base branch.
60
60
  */
61
61
  export declare function selectRemovalCandidateFiles(diffData: BranchDiffData): string[];
62
- /**
63
- * Recover endpoints as they existed in the base branch, for the given candidate
64
- * files. `fetchBaseContent` returns a file's base-branch content (e.g. via
65
- * `git show base:<file>`); files that throw are skipped. Returns every endpoint
66
- * found in the base versions — callers pass the result to
67
- * `classifyEndpointsByChangedFiles`, whose moved/still-present filter drops any
68
- * endpoint that survives in the current catalog, leaving only genuine removals.
69
- */
70
- export declare function recoverRemovedEndpointsFromBase(candidateFiles: string[], fetchBaseContent: (file: string) => Promise<string>): Promise<ScannedEndpoint[]>;
62
+ export declare function recoverRemovedEndpointsFromBase(candidateFiles: string[], fetchBaseContent: (file: string) => Promise<string>, repositoryPath?: string, baseBranch?: string): Promise<ScannedEndpoint[]>;
63
+ /** Strip git's `a/` / `b/` diff-path prefix and normalize separators. */
64
+ export declare function normalizeDiffPath(spec: string): string;
71
65
  /**
72
66
  * Parse a unified diff into the set of new-side (post-change) line numbers that
73
67
  * were touched, keyed by normalized file path.
@@ -1,4 +1,8 @@
1
- import { joinRoutePrefix } from "./pythonMountPrefixes.js";
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { execFileSync } from "child_process";
5
+ import { joinRoutePrefix, resolvePythonMountPrefixes } from "./pythonMountPrefixes.js";
2
6
  import { logger } from "./logger.js";
3
7
  export function nextjsFileToApiPath(filePath) {
4
8
  const pagesMatch = filePath.match(/(?:^|\/)pages\/(api\/.+)\.[jt]sx?$/);
@@ -577,9 +581,80 @@ export function selectRemovalCandidateFiles(diffData) {
577
581
  * `classifyEndpointsByChangedFiles`, whose moved/still-present filter drops any
578
582
  * endpoint that survives in the current catalog, leaving only genuine removals.
579
583
  */
580
- export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseContent) {
584
+ /**
585
+ * Materialize just the `.py` files of a git ref into a fresh temp directory
586
+ * via `git archive` (no full checkout/worktree) — lets `resolvePythonMountPrefixes`
587
+ * run against the BASE tree unmodified. Best-effort: returns null on any
588
+ * failure (shallow clone missing the ref, git/tar unavailable, etc.) so the
589
+ * caller can fall back rather than abort removal recovery.
590
+ */
591
+ function materializePythonFilesFromRef(repositoryPath, ref) {
592
+ let tmpDir;
593
+ try {
594
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-base-py-"));
595
+ }
596
+ catch {
597
+ return null;
598
+ }
599
+ const archivePath = path.join(tmpDir, "archive.tar");
600
+ try {
601
+ execFileSync("git", ["-C", repositoryPath, "archive", "--format=tar", "-o", archivePath, ref, "--", "*.py"], {
602
+ stdio: ["ignore", "ignore", "ignore"],
603
+ });
604
+ execFileSync("tar", ["-xf", archivePath, "-C", tmpDir], { stdio: ["ignore", "ignore", "ignore"] });
605
+ fs.rmSync(archivePath, { force: true });
606
+ return tmpDir;
607
+ }
608
+ catch (err) {
609
+ logger.warning("Failed to materialize base-branch Python files for mount-prefix resolution — falling back to current working tree", {
610
+ ref,
611
+ error: err instanceof Error ? err.message : String(err),
612
+ });
613
+ try {
614
+ fs.rmSync(tmpDir, { recursive: true, force: true });
615
+ }
616
+ catch {
617
+ // best-effort cleanup
618
+ }
619
+ return null;
620
+ }
621
+ }
622
+ export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseContent, repositoryPath, baseBranch) {
581
623
  const routeFiles = candidateFiles.filter(isRouteLikeFile);
582
624
  const endpointMap = new Map();
625
+ // Cross-file FastAPI mount-prefix resolution, mirroring the main scan
626
+ // (repoScanner.ts). Without it, a route file whose prefix is applied
627
+ // externally (app.include_router(router, prefix=...)) yields unmounted
628
+ // paths here that can never match the fully-mounted current-catalog
629
+ // paths in classifyEndpointsByChangedFiles's "still exists" check — every
630
+ // endpoint in that file gets misreported as removed, not just the one
631
+ // actually deleted (SKYR-4026).
632
+ //
633
+ // Resolved from the BASE tree when possible (via a `.py`-only git-archive
634
+ // extraction), not the current working tree — if a PR also renames the
635
+ // mount prefix itself (not just the recovered route), head-state prefixes
636
+ // would rewrite the recovered base endpoint onto the NEW prefix, which
637
+ // could mask a real removal or misattribute the path. Falls back to the
638
+ // current working tree if base-tree materialization isn't possible (e.g.
639
+ // shallow clone, git/tar unavailable) — still strictly better than no
640
+ // mount-prefix resolution at all.
641
+ let mountPrefixesByFile;
642
+ if (repositoryPath && routeFiles.some((f) => f.endsWith(".py"))) {
643
+ const baseTreeDir = baseBranch ? materializePythonFilesFromRef(repositoryPath, baseBranch) : null;
644
+ try {
645
+ mountPrefixesByFile = resolvePythonMountPrefixes(baseTreeDir ?? repositoryPath);
646
+ }
647
+ finally {
648
+ if (baseTreeDir) {
649
+ try {
650
+ fs.rmSync(baseTreeDir, { recursive: true, force: true });
651
+ }
652
+ catch {
653
+ // best-effort cleanup
654
+ }
655
+ }
656
+ }
657
+ }
583
658
  for (const file of routeFiles) {
584
659
  let content;
585
660
  try {
@@ -597,7 +672,7 @@ export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseC
597
672
  });
598
673
  continue;
599
674
  }
600
- for (const ep of parseFileEndpoints(content, file)) {
675
+ for (const ep of parseFileEndpoints(content, file, mountPrefixesByFile?.get(file))) {
601
676
  // parseFileEndpoints already normalizes paths to a leading slash.
602
677
  const key = `${file}::${ep.path}`;
603
678
  const existing = endpointMap.get(key);
@@ -620,7 +695,7 @@ export async function recoverRemovedEndpointsFromBase(candidateFiles, fetchBaseC
620
695
  }));
621
696
  }
622
697
  /** Strip git's `a/` / `b/` diff-path prefix and normalize separators. */
623
- function normalizeDiffPath(spec) {
698
+ export function normalizeDiffPath(spec) {
624
699
  let p = spec.replace(/\\/g, "/");
625
700
  if (p.startsWith("a/") || p.startsWith("b/"))
626
701
  p = p.slice(2);
@@ -69,7 +69,7 @@ export function getPathParameterValidationError(requiredPathParams) {
69
69
  return (`Please specify value for the following required path parameter:\n` +
70
70
  `${requiredPathParams
71
71
  .split(",")
72
- .map((param, index) => `- \`${param.trim()}\``)
72
+ .map((param) => `- \`${param.trim()}\``)
73
73
  .join("\n")}\n\n`);
74
74
  }
75
75
  }
@@ -150,7 +150,7 @@ export function validateRequestData(value) {
150
150
  JSON.parse(value);
151
151
  return value;
152
152
  }
153
- catch (e) {
153
+ catch (_e) {
154
154
  logger.warning("Request data is not a valid JSON string", { value });
155
155
  }
156
156
  const isAbsolute = path.isAbsolute(value);
@@ -1,3 +1,4 @@
1
- export declare const SKYRAMP_IMAGE_VERSION = "v1.3.36";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.36";
3
- export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.36";
1
+ export declare const SKYRAMP_IMAGE_VERSION = "v1.3.37";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.37";
3
+ export declare const WORKER_DOCKER_IMAGE = "skyramp/worker:v1.3.37";
4
+ export declare const WORKER_CONTROL_PORT = 35142;
@@ -1,3 +1,5 @@
1
- export const SKYRAMP_IMAGE_VERSION = "v1.3.36";
1
+ export const SKYRAMP_IMAGE_VERSION = "v1.3.37";
2
2
  export const EXECUTOR_DOCKER_IMAGE = `skyramp/executor:${SKYRAMP_IMAGE_VERSION}`;
3
3
  export const WORKER_DOCKER_IMAGE = `skyramp/worker:${SKYRAMP_IMAGE_VERSION}`;
4
+ // Control port the Skyramp worker listens on (SDK `CONTAINER_PORT`).
5
+ export const WORKER_CONTROL_PORT = 35142;
@@ -133,3 +133,49 @@ export declare function getWorkspaceAuthConfig(repositoryPath: string): Promise<
133
133
  /** True when any workspace service declares api.skipTLSVerify — the SUT serves
134
134
  * self-signed TLS and generated tests must skip cert verification (SKYR-3961). */
135
135
  export declare function getWorkspaceSkipTLSVerify(repositoryPath: string): Promise<boolean>;
136
+ /**
137
+ * Resolve api.defaultQueryParams for test generation — query params always
138
+ * attached to every generated request for this service (e.g. RBAC context
139
+ * an authorization layer requires beyond the auth header). Same
140
+ * first-matching-service convention as getWorkspaceAuthConfig (SKYR-4050).
141
+ *
142
+ * Unlike readWorkspaceConfigRaw (which delegates to WorkspaceConfigManager and
143
+ * resolves EXACTLY `<repositoryPath>/.skyramp/workspace.yml`), this walks up
144
+ * from `repositoryPath` to find the nearest `.skyramp/workspace.yml` — both
145
+ * call sites pass a nested test-output directory (e.g. `<repo>/tests/skyramp`),
146
+ * not the repo root, so an exact-match lookup would silently never find the
147
+ * config. Mirrors the sync fs.existsSync walk-up in
148
+ * generateBatchScenarioRestTool.ts.
149
+ */
150
+ /**
151
+ * Walk up from `startDir` to find the nearest `.skyramp/workspace.yml`, checking
152
+ * `startDir` itself first and continuing through every ancestor INCLUDING the
153
+ * filesystem root — a naive `while (dir !== path.dirname(dir))` loop exits
154
+ * before ever checking the root candidate, since that equality only becomes
155
+ * true once `dir` already IS the root. Returns the full config path, or null.
156
+ * `existsSync` is injectable for testing; defaults to the real filesystem.
157
+ */
158
+ export declare function findWorkspaceConfigPath(startDir: string, existsSync?: (p: string) => boolean): string | null;
159
+ export declare function getWorkspaceDefaultQueryParams(repositoryPath: string): Promise<Record<string, string> | undefined>;
160
+ /**
161
+ * Merge workspace-declared default query params into a comma-separated
162
+ * "key=value,key2=value2" queryParams string (the format used by
163
+ * baseTestSchema.queryParams). Existing keys always win on collision.
164
+ */
165
+ export declare function mergeQueryParamsString(existing: string | undefined, defaults: Record<string, string>): string;
166
+ /**
167
+ * Merge workspace-declared default query params into a JSON-object queryParams
168
+ * string (the format used by batch-scenario step queryParams). Existing keys
169
+ * always win on collision. A malformed existingJson is left untouched — the
170
+ * caller's own JSON.parse warning path still fires downstream.
171
+ */
172
+ export declare function mergeQueryParamsJson(existingJson: string | undefined, defaults: Record<string, string>): string;
173
+ /**
174
+ * Resolve auth header and scheme from workspace config for test generation.
175
+ * Returns null when auth should be skipped (authType absent or "none").
176
+ * Single source of truth — used by both TestGenerationService and batch scenario tool.
177
+ */
178
+ export declare function resolveAuthFromWorkspace(repoPath: string, existingAuthHeader?: string, existingAuthScheme?: string): Promise<{
179
+ authHeader: string;
180
+ authScheme?: string;
181
+ } | null>;
@@ -89,7 +89,20 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language, te
89
89
  return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
90
90
  });
91
91
  });
92
- // Step 2: narrow by language if still ambiguous
92
+ // Step 2: longest-prefix match — when multiple services match (e.g.
93
+ // tests/skyramp is a prefix of tests/skyramp/cart), pick the service
94
+ // whose testDirectory is the most specific (longest) prefix of the test file.
95
+ if (matches.length > 1) {
96
+ matches.sort((a, b) => {
97
+ const aLen = path.resolve(repositoryPath, a.testDirectory).length;
98
+ const bLen = path.resolve(repositoryPath, b.testDirectory).length;
99
+ return bLen - aLen; // longest first
100
+ });
101
+ // Keep only services with the longest matching testDirectory length.
102
+ const longestLen = path.resolve(repositoryPath, matches[0].testDirectory).length;
103
+ matches = matches.filter((s) => path.resolve(repositoryPath, s.testDirectory).length === longestLen);
104
+ }
105
+ // Step 3: narrow by language if still ambiguous, but don't eliminate all matches
93
106
  if (matches.length > 1 && language) {
94
107
  const byLanguage = matches.filter((s) => s.language?.toLowerCase() === language.toLowerCase());
95
108
  if (byLanguage.length > 0)
@@ -350,3 +363,145 @@ export async function getWorkspaceSkipTLSVerify(repositoryPath) {
350
363
  return false;
351
364
  return (rawConfig.services ?? []).some((s) => s.api?.skipTLSVerify === true);
352
365
  }
366
+ /**
367
+ * Resolve api.defaultQueryParams for test generation — query params always
368
+ * attached to every generated request for this service (e.g. RBAC context
369
+ * an authorization layer requires beyond the auth header). Same
370
+ * first-matching-service convention as getWorkspaceAuthConfig (SKYR-4050).
371
+ *
372
+ * Unlike readWorkspaceConfigRaw (which delegates to WorkspaceConfigManager and
373
+ * resolves EXACTLY `<repositoryPath>/.skyramp/workspace.yml`), this walks up
374
+ * from `repositoryPath` to find the nearest `.skyramp/workspace.yml` — both
375
+ * call sites pass a nested test-output directory (e.g. `<repo>/tests/skyramp`),
376
+ * not the repo root, so an exact-match lookup would silently never find the
377
+ * config. Mirrors the sync fs.existsSync walk-up in
378
+ * generateBatchScenarioRestTool.ts.
379
+ */
380
+ /**
381
+ * Walk up from `startDir` to find the nearest `.skyramp/workspace.yml`, checking
382
+ * `startDir` itself first and continuing through every ancestor INCLUDING the
383
+ * filesystem root — a naive `while (dir !== path.dirname(dir))` loop exits
384
+ * before ever checking the root candidate, since that equality only becomes
385
+ * true once `dir` already IS the root. Returns the full config path, or null.
386
+ * `existsSync` is injectable for testing; defaults to the real filesystem.
387
+ */
388
+ export function findWorkspaceConfigPath(startDir, existsSync = fs.existsSync) {
389
+ const WS_SUBPATH = path.join(".skyramp", "workspace.yml");
390
+ let searchDir = path.resolve(startDir);
391
+ while (true) {
392
+ const candidate = path.join(searchDir, WS_SUBPATH);
393
+ if (existsSync(candidate))
394
+ return candidate;
395
+ const parentDir = path.dirname(searchDir);
396
+ if (parentDir === searchDir)
397
+ return null; // reached the filesystem root; already checked above
398
+ searchDir = parentDir;
399
+ }
400
+ }
401
+ export async function getWorkspaceDefaultQueryParams(repositoryPath) {
402
+ const wsConfigPath = findWorkspaceConfigPath(repositoryPath);
403
+ if (!wsConfigPath)
404
+ return undefined;
405
+ try {
406
+ const raw = fs.readFileSync(wsConfigPath, "utf-8");
407
+ const parsed = yaml.load(raw);
408
+ const svc = parsed?.services?.find((s) => s.api?.defaultQueryParams !== undefined);
409
+ return svc?.api?.defaultQueryParams;
410
+ }
411
+ catch {
412
+ return undefined;
413
+ }
414
+ }
415
+ /**
416
+ * Merge workspace-declared default query params into a comma-separated
417
+ * "key=value,key2=value2" queryParams string (the format used by
418
+ * baseTestSchema.queryParams). Existing keys always win on collision.
419
+ */
420
+ export function mergeQueryParamsString(existing, defaults) {
421
+ const pairs = new Map();
422
+ if (existing) {
423
+ for (const pair of existing.split(",")) {
424
+ const eqIndex = pair.indexOf("=");
425
+ if (eqIndex === -1)
426
+ continue;
427
+ const key = pair.slice(0, eqIndex).trim();
428
+ const value = pair.slice(eqIndex + 1).trim();
429
+ if (key)
430
+ pairs.set(key, value);
431
+ }
432
+ }
433
+ for (const [key, value] of Object.entries(defaults)) {
434
+ if (typeof value !== "string") {
435
+ logger.warning("Skipping workspace defaultQueryParams entry with a non-string value", { key });
436
+ continue;
437
+ }
438
+ if (key.includes(",") || key.includes("=") || value.includes(",") || value.includes("=")) {
439
+ logger.warning("Skipping workspace defaultQueryParams entry with invalid characters", { key });
440
+ continue;
441
+ }
442
+ if (!pairs.has(key))
443
+ pairs.set(key, value);
444
+ }
445
+ return Array.from(pairs.entries())
446
+ .map(([key, value]) => `${key}=${value}`)
447
+ .join(",");
448
+ }
449
+ /**
450
+ * Merge workspace-declared default query params into a JSON-object queryParams
451
+ * string (the format used by batch-scenario step queryParams). Existing keys
452
+ * always win on collision. A malformed existingJson is left untouched — the
453
+ * caller's own JSON.parse warning path still fires downstream.
454
+ */
455
+ export function mergeQueryParamsJson(existingJson, defaults) {
456
+ let parsed = {};
457
+ if (existingJson) {
458
+ try {
459
+ const candidate = JSON.parse(existingJson);
460
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
461
+ return existingJson;
462
+ }
463
+ parsed = candidate;
464
+ }
465
+ catch {
466
+ return existingJson;
467
+ }
468
+ }
469
+ return JSON.stringify({ ...defaults, ...parsed });
470
+ }
471
+ /**
472
+ * Resolve auth header and scheme from workspace config for test generation.
473
+ * Returns null when auth should be skipped (authType absent or "none").
474
+ * Single source of truth — used by both TestGenerationService and batch scenario tool.
475
+ */
476
+ export async function resolveAuthFromWorkspace(repoPath, existingAuthHeader, existingAuthScheme) {
477
+ if (existingAuthHeader !== undefined)
478
+ return null; // caller already specified
479
+ const wsAuth = await getWorkspaceAuthConfig(repoPath);
480
+ // Skip auth when explicitly none. When authType is absent, still resolve if
481
+ // authHeader or authScheme is set (some workspaces specify only the header).
482
+ if (wsAuth.authType === WorkspaceAuthType.None)
483
+ return null;
484
+ if (!wsAuth.authType && !wsAuth.authHeader && !wsAuth.authScheme)
485
+ return null;
486
+ let resolvedHeader = wsAuth.authHeader ?? getDefaultAuthHeader(wsAuth.authType);
487
+ if (!resolvedHeader && wsAuth.authScheme !== undefined) {
488
+ resolvedHeader = "Authorization";
489
+ }
490
+ if (wsAuth.authType === WorkspaceAuthType.ApiKey && !resolvedHeader) {
491
+ logger.warning("workspace.yml has authType: apiKey but no authHeader is set — " +
492
+ "requests will be sent without an auth header and will likely receive 401/403. " +
493
+ "Add authHeader: <X-Your-Key-Header> to .skyramp/workspace.yml.");
494
+ return null;
495
+ }
496
+ if (!resolvedHeader)
497
+ return null;
498
+ logger.info("authHeader resolved from workspace config", {
499
+ authHeader: resolvedHeader,
500
+ authType: wsAuth.authType,
501
+ });
502
+ let authScheme = existingAuthScheme;
503
+ if (isAuthorizationHeaderName(resolvedHeader)) {
504
+ authScheme = authScheme ?? wsAuth.authScheme ?? getAuthScheme(wsAuth.authType);
505
+ }
506
+ return { authHeader: resolvedHeader, authScheme };
507
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Canonical set of supported test frameworks — the single source of truth for
3
+ * both the workspace `framework` enum and the `framework` tool-param description
4
+ * in TestTypes.ts. `robot` is Robot Framework (Python). Extend this list when a
5
+ * new framework is genuinely supported; do not fork it.
6
+ *
7
+ * Kept in its own dependency-free module so lightweight consumers (e.g.
8
+ * TestTypes.ts) can read the list without importing workspace.ts and pulling in
9
+ * its Node-only dependencies (fs / child_process / js-yaml).
10
+ */
11
+ export declare const SUPPORTED_FRAMEWORKS: readonly ["playwright", "pytest", "robot", "junit", "vitest", "jest", "mocha", "go", "rspec", "jasmine"];
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Canonical set of supported test frameworks — the single source of truth for
3
+ * both the workspace `framework` enum and the `framework` tool-param description
4
+ * in TestTypes.ts. `robot` is Robot Framework (Python). Extend this list when a
5
+ * new framework is genuinely supported; do not fork it.
6
+ *
7
+ * Kept in its own dependency-free module so lightweight consumers (e.g.
8
+ * TestTypes.ts) can read the list without importing workspace.ts and pulling in
9
+ * its Node-only dependencies (fs / child_process / js-yaml).
10
+ */
11
+ export const SUPPORTED_FRAMEWORKS = [
12
+ "playwright",
13
+ "pytest",
14
+ "robot",
15
+ "junit",
16
+ "vitest",
17
+ "jest",
18
+ "mocha",
19
+ "go",
20
+ "rspec",
21
+ "jasmine",
22
+ ];
@@ -0,0 +1,20 @@
1
+ import type { Service } from "./workspace.js";
2
+ /**
3
+ * A single test suite (one runtimeDetails.testSuites[] entry) flattened to one
4
+ * uniform shape, carrying its owning serviceName so routing/aggregation can label
5
+ * results per suite, and a framework resolved against the service's.
6
+ */
7
+ export interface NormalizedSuite {
8
+ serviceName: string;
9
+ framework?: string;
10
+ testRunCommand: string;
11
+ testEnv?: Record<string, string>;
12
+ /** Repo-relative globs this suite owns, for selector→suite routing. */
13
+ pathGlobs?: string[];
14
+ }
15
+ /**
16
+ * Normalizes a service's runtimeDetails.testSuites into a flat list of suites
17
+ * (SKYR-4002), each carrying its owning serviceName and a framework that falls
18
+ * back to the service's. Returns [] when the service records no testSuites.
19
+ */
20
+ export declare function resolveSuites(service: Service): NormalizedSuite[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Normalizes a service's runtimeDetails.testSuites into a flat list of suites
3
+ * (SKYR-4002), each carrying its owning serviceName and a framework that falls
4
+ * back to the service's. Returns [] when the service records no testSuites.
5
+ */
6
+ export function resolveSuites(service) {
7
+ const suites = service.runtimeDetails?.testSuites;
8
+ if (!suites)
9
+ return [];
10
+ return suites.map((suite) => ({
11
+ serviceName: service.serviceName,
12
+ framework: suite.framework ?? service.framework,
13
+ testRunCommand: suite.testRunCommand,
14
+ testEnv: suite.testEnv,
15
+ pathGlobs: suite.pathGlobs,
16
+ }));
17
+ }