@skyramp/mcp 0.3.0 → 0.3.1-ldw-beta

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 (112) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +19 -13
  3. package/build/commands/commandLibrary.test.d.ts +1 -0
  4. package/build/commands/commandLibrary.test.js +59 -0
  5. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  6. package/build/commands/localDevTestChangesCommand.js +201 -0
  7. package/build/index.js +80 -6
  8. package/build/playwright/traceRecordingPrompt.js +2 -0
  9. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  10. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  11. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
  12. package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
  13. package/build/prompts/local-dev/local-dev-plan.js +466 -0
  14. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  15. package/build/prompts/local-dev/local-dev-prompts.js +190 -0
  16. package/build/prompts/prompt-utils.d.ts +8 -0
  17. package/build/prompts/prompt-utils.js +33 -0
  18. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
  22. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  23. package/build/prompts/testbot/testbot-prompts.js +5 -34
  24. package/build/resources/testbotResource.js +2 -1
  25. package/build/services/AnalyticsService.d.ts +1 -1
  26. package/build/services/TestExecutionService.d.ts +2 -1
  27. package/build/services/TestExecutionService.js +8 -3
  28. package/build/services/TestExecutionService.test.js +83 -3
  29. package/build/services/TestGenerationService.d.ts +2 -2
  30. package/build/services/TestGenerationService.js +7 -21
  31. package/build/services/containerEnv.js +3 -1
  32. package/build/tool-phases.js +6 -0
  33. package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
  34. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  35. package/build/tools/enrichTestWithMocksTool.js +726 -0
  36. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  37. package/build/tools/enrichTestWithMocksTool.test.js +266 -0
  38. package/build/tools/executeSkyrampTestTool.d.ts +11 -0
  39. package/build/tools/executeSkyrampTestTool.js +63 -22
  40. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  41. package/build/tools/executeSkyrampTestTool.test.js +36 -0
  42. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
  43. package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
  44. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +6 -21
  45. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +1 -0
  46. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  47. package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
  48. package/build/tools/generate-tests/generateMockRestTool.js +234 -22
  49. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  50. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
  51. package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
  52. package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
  53. package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
  54. package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
  55. package/build/tools/localDevWorkerComposeTool.js +224 -0
  56. package/build/tools/localDevWorkerComposeTool.test.d.ts +1 -0
  57. package/build/tools/localDevWorkerComposeTool.test.js +129 -0
  58. package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
  59. package/build/tools/localDevWorkflowFixes.test.js +255 -0
  60. package/build/tools/one-click/oneClickTool.d.ts +13 -0
  61. package/build/tools/one-click/oneClickTool.js +195 -24
  62. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  63. package/build/tools/one-click/oneClickTool.test.js +172 -0
  64. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  65. package/build/tools/preflightMockCheckTool.js +96 -0
  66. package/build/tools/queryProxyMocksTool.d.ts +70 -0
  67. package/build/tools/queryProxyMocksTool.js +522 -0
  68. package/build/tools/queryProxyMocksTool.test.d.ts +1 -0
  69. package/build/tools/queryProxyMocksTool.test.js +164 -0
  70. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  71. package/build/tools/test-management/analyzeChangesTool.js +61 -38
  72. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  73. package/build/types/OneClickCommands.d.ts +1 -1
  74. package/build/types/RepositoryAnalysis.d.ts +117 -0
  75. package/build/types/RepositoryAnalysis.js +16 -2
  76. package/build/types/TestExecution.d.ts +1 -0
  77. package/build/types/TestTypes.d.ts +25 -7
  78. package/build/types/TestTypes.js +19 -5
  79. package/build/utils/analyze-openapi.js +18 -1
  80. package/build/utils/analyze-openapi.test.d.ts +1 -0
  81. package/build/utils/analyze-openapi.test.js +19 -0
  82. package/build/utils/branchDiff.d.ts +17 -1
  83. package/build/utils/branchDiff.js +99 -14
  84. package/build/utils/branchDiff.test.d.ts +1 -0
  85. package/build/utils/branchDiff.test.js +109 -0
  86. package/build/utils/featureFlags.d.ts +16 -0
  87. package/build/utils/featureFlags.js +20 -0
  88. package/build/utils/featureFlags.test.js +57 -2
  89. package/build/utils/grpcMockValidation.d.ts +1 -0
  90. package/build/utils/grpcMockValidation.js +49 -0
  91. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  92. package/build/utils/grpcMockValidation.test.js +41 -0
  93. package/build/utils/httpMethodValidation.d.ts +4 -0
  94. package/build/utils/httpMethodValidation.js +15 -0
  95. package/build/utils/mockCompatibility.d.ts +49 -0
  96. package/build/utils/mockCompatibility.js +82 -0
  97. package/build/utils/mockCompatibility.test.d.ts +1 -0
  98. package/build/utils/mockCompatibility.test.js +79 -0
  99. package/build/utils/progress.js +10 -5
  100. package/build/utils/versions.d.ts +1 -0
  101. package/build/utils/versions.js +2 -0
  102. package/build/utils/workspaceAuth.d.ts +9 -0
  103. package/build/utils/workspaceAuth.js +51 -1
  104. package/build/workspace/workspace.d.ts +28 -14
  105. package/build/workspace/workspace.js +3 -1
  106. package/node_modules/playwright/lib/mcp/browser/tools/screenshot.js +1 -1
  107. package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +45 -0
  108. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +6 -22
  109. package/node_modules/playwright/lib/mcp/test/resultCode.test.js +48 -0
  110. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  111. package/node_modules/playwright/package.json +1 -1
  112. package/package.json +2 -2
@@ -0,0 +1,24 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export interface WorkerComposeResult {
3
+ baseComposeFile: string;
4
+ overrideFile: string;
5
+ dockerNetwork: string;
6
+ workerPort: number;
7
+ composeFilePrefix: string;
8
+ }
9
+ /**
10
+ * Generate the worker compose override and return the resolved network name and
11
+ * the `-f` prefix for `docker compose` commands. Throws on a missing compose file
12
+ * or if Docker is not running.
13
+ */
14
+ export declare function setupLocalDevWorker(repositoryPath: string, composeFile?: string): WorkerComposeResult;
15
+ /**
16
+ * Deterministically set up the Skyramp worker as a Docker Compose service for
17
+ * the local-dev workflow.
18
+ *
19
+ * This replaces the SDK's imperative worker spawn (which raced onto the wrong
20
+ * Docker network and leaked on teardown — Local-Dev Skill Findings #1, #9, #10).
21
+ * The worker becomes a compose service via a generated override file, so it joins
22
+ * the SUT's network declaratively and is removed by `docker compose down`.
23
+ */
24
+ export declare function registerLocalDevWorkerComposeTool(server: McpServer): void;
@@ -0,0 +1,224 @@
1
+ import { z } from "zod";
2
+ import * as path from "path";
3
+ import * as fs from "fs";
4
+ import { execSync } from "child_process";
5
+ import yaml from "js-yaml";
6
+ import { AnalyticsService } from "../services/AnalyticsService.js";
7
+ import { WORKER_DOCKER_IMAGE, WORKER_CONTROL_PORT } from "../utils/versions.js";
8
+ import { logger } from "../utils/logger.js";
9
+ const TOOL_NAME = "skyramp_setup_local_dev_worker";
10
+ // Relative path (under the repo) where the generated worker override is written.
11
+ const OVERRIDE_REL_PATH = ".skyramp/skyramp.worker.compose.yml";
12
+ const BASE_COMPOSE_CANDIDATES = [
13
+ "docker-compose.yml",
14
+ "docker-compose.yaml",
15
+ "compose.yml",
16
+ "compose.yaml",
17
+ ];
18
+ /**
19
+ * Docker Compose project name derivation: lowercase the directory basename and
20
+ * strip characters outside [a-z0-9_-] (Compose's normalization). COMPOSE_PROJECT_NAME
21
+ * overrides it when set. The default network is then `<project>_<networkKey>`, or
22
+ * `<project>_default` when the compose file declares no named networks.
23
+ */
24
+ function deriveProjectName(composeDir, env = process.env) {
25
+ const override = env.COMPOSE_PROJECT_NAME?.trim();
26
+ if (override)
27
+ return override;
28
+ return path
29
+ .basename(composeDir)
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9_-]/g, "");
32
+ }
33
+ /** Resolve the base compose file: explicit path wins, else discover at repo root. */
34
+ function resolveBaseCompose(repositoryPath, composeFile) {
35
+ if (composeFile) {
36
+ const abs = path.isAbsolute(composeFile)
37
+ ? composeFile
38
+ : path.join(repositoryPath, composeFile);
39
+ return fs.existsSync(abs) ? abs : undefined;
40
+ }
41
+ for (const candidate of BASE_COMPOSE_CANDIDATES) {
42
+ const abs = path.join(repositoryPath, candidate);
43
+ if (fs.existsSync(abs))
44
+ return abs;
45
+ }
46
+ return undefined;
47
+ }
48
+ /**
49
+ * Build the worker override compose document. The worker joins the same named
50
+ * network(s) the base declares (so it shares the SUT's network for DNS-alias
51
+ * hijacking at mock-apply time); when the base declares none, the worker joins
52
+ * the implicit `default` network like every other service. Only the control
53
+ * port (35142) is published to the host — mock ports stay internal to the
54
+ * docker network.
55
+ */
56
+ function buildOverrideDoc(networkKeys) {
57
+ const workerService = {
58
+ image: WORKER_DOCKER_IMAGE,
59
+ volumes: [
60
+ "skyramp:/etc/skyramp",
61
+ "/var/run/docker.sock:/var/run/docker.sock",
62
+ ],
63
+ ports: [`127.0.0.1:${WORKER_CONTROL_PORT}:${WORKER_CONTROL_PORT}`],
64
+ restart: "always",
65
+ };
66
+ const doc = {
67
+ services: { skyramp: workerService },
68
+ volumes: { skyramp: null },
69
+ };
70
+ if (networkKeys.length > 0) {
71
+ workerService.networks = networkKeys;
72
+ // Reference each base network by key so Compose merges (does not redefine) it.
73
+ doc.networks = Object.fromEntries(networkKeys.map((k) => [k, {}]));
74
+ }
75
+ return doc;
76
+ }
77
+ /**
78
+ * Resolve the actual Docker network name Compose uses for `key`:
79
+ * - an explicit `name:` is used verbatim (NOT project-scoped),
80
+ * - `external: true` networks are pre-existing and not project-scoped
81
+ * (use `external.name` for the legacy form, else the key),
82
+ * - otherwise Compose prefixes the project: `<project>_<key>`.
83
+ */
84
+ function resolveNetworkName(project, key, def) {
85
+ const d = def ?? {};
86
+ if (typeof d.name === "string" && d.name.length > 0)
87
+ return d.name;
88
+ if (d.external) {
89
+ if (typeof d.external === "object" && d.external.name) {
90
+ return d.external.name;
91
+ }
92
+ return key;
93
+ }
94
+ return `${project}_${key}`;
95
+ }
96
+ /**
97
+ * Verify that the Docker daemon is reachable.
98
+ * Throws with a clear, actionable message so the agent stops immediately
99
+ * instead of spending turns manually debugging Docker connectivity.
100
+ *
101
+ * Does NOT switch Docker contexts — that can break a working setup when the
102
+ * active context differs from `default` but Docker is accessible via symlink.
103
+ */
104
+ function checkDockerReady() {
105
+ try {
106
+ execSync("docker info", { stdio: "pipe", timeout: 15_000 });
107
+ return; // Docker is reachable.
108
+ }
109
+ catch {
110
+ // Docker not reachable on current context/config.
111
+ }
112
+ throw new Error("Docker daemon is not running or not reachable. " +
113
+ "Start Docker Desktop and retry. " +
114
+ "Do NOT attempt to debug Docker connectivity, try alternative socket paths, " +
115
+ "switch Docker contexts, or run `open -a Docker` — " +
116
+ "report this as a blocking environment error and stop.");
117
+ }
118
+ /**
119
+ * Generate the worker compose override and return the resolved network name and
120
+ * the `-f` prefix for `docker compose` commands. Throws on a missing compose file
121
+ * or if Docker is not running.
122
+ */
123
+ export function setupLocalDevWorker(repositoryPath, composeFile) {
124
+ // Fail fast if Docker is not available — prevents the agent from proceeding
125
+ // with false confidence and then spending turns debugging Docker manually.
126
+ // Skipped when SKIP_DOCKER_CHECK=1 (unit tests that only validate YAML generation).
127
+ if (process.env.SKIP_DOCKER_CHECK !== "1") {
128
+ checkDockerReady();
129
+ }
130
+ const baseComposeFile = resolveBaseCompose(repositoryPath, composeFile);
131
+ if (!baseComposeFile) {
132
+ throw new Error(composeFile
133
+ ? `Compose file not found: ${composeFile}`
134
+ : `No docker-compose file found at ${repositoryPath} (looked for: ${BASE_COMPOSE_CANDIDATES.join(", ")}). Pass composeFile for a non-default location.`);
135
+ }
136
+ const parsed = yaml.load(fs.readFileSync(baseComposeFile, "utf-8"), {
137
+ schema: yaml.JSON_SCHEMA,
138
+ });
139
+ const networksObj = parsed?.networks ?? {};
140
+ const networkKeys = Object.keys(networksObj);
141
+ // Compose derives project name from the working directory (typically repo root),
142
+ // not from the compose file's directory. The plan instructs agents to run
143
+ // `docker compose` from repositoryPath.
144
+ const project = deriveProjectName(repositoryPath);
145
+ // The network the SDK client + executor target. First declared named network,
146
+ // else the implicit default network. Honor explicit `name:`/`external:` so the
147
+ // returned name matches the network Compose actually creates/uses.
148
+ const primaryKey = networkKeys[0] ?? "default";
149
+ const dockerNetwork = resolveNetworkName(project, primaryKey, networksObj[primaryKey]);
150
+ const overrideFile = path.join(repositoryPath, OVERRIDE_REL_PATH);
151
+ fs.mkdirSync(path.dirname(overrideFile), { recursive: true });
152
+ const overrideDoc = buildOverrideDoc(networkKeys);
153
+ const header = "# Generated by skyramp_setup_local_dev_worker — runs the Skyramp mock worker\n" +
154
+ "# as a compose service so it shares the SUT's network and is torn down with\n" +
155
+ "# the project. Layer it: docker compose -f <base> -f this up. Safe to delete.\n";
156
+ fs.writeFileSync(overrideFile, header + yaml.dump(overrideDoc, { lineWidth: -1 }), "utf-8");
157
+ return {
158
+ baseComposeFile,
159
+ overrideFile,
160
+ dockerNetwork,
161
+ workerPort: WORKER_CONTROL_PORT,
162
+ composeFilePrefix: `-f ${baseComposeFile} -f ${overrideFile}`,
163
+ };
164
+ }
165
+ /**
166
+ * Deterministically set up the Skyramp worker as a Docker Compose service for
167
+ * the local-dev workflow.
168
+ *
169
+ * This replaces the SDK's imperative worker spawn (which raced onto the wrong
170
+ * Docker network and leaked on teardown — Local-Dev Skill Findings #1, #9, #10).
171
+ * The worker becomes a compose service via a generated override file, so it joins
172
+ * the SUT's network declaratively and is removed by `docker compose down`.
173
+ */
174
+ export function registerLocalDevWorkerComposeTool(server) {
175
+ server.registerTool(TOOL_NAME, {
176
+ description: `Set up the Skyramp mock worker as a Docker Compose service for the local-dev workflow (run this in the Deploy phase, before bringing the stack up).
177
+
178
+ Writes a compose override file (.skyramp/skyramp.worker.compose.yml) that declares the worker as a service on the SUT's docker network, then returns the docker-compose \`-f\` prefix and the resolved docker network name.
179
+
180
+ Why: running the worker as a compose service makes it join the SUT's network declaratively (so the app can reach its mocks) and tears it down with \`docker compose down\` — replacing the fragile runtime worker spawn that landed on the wrong network and leaked on teardown.
181
+
182
+ Use the returned values like this:
183
+ - Bring up: \`docker compose <composeFilePrefix> up -d --build <sut-service> <real-deps...> skyramp\` (stop mocked-service containers first).
184
+ - Tear down: \`docker compose <composeFilePrefix> down -v\`.
185
+ - Pass \`dockerNetwork\` (and \`dockerWorkerPort\`=workerPort) to every test generation call so the generated client — emitted with runtime="docker", docker_network, docker_skyramp_port — connects to this worker.`,
186
+ inputSchema: {
187
+ repositoryPath: z
188
+ .string()
189
+ .refine((value) => path.isAbsolute(value), {
190
+ message: "repositoryPath must be an absolute path",
191
+ })
192
+ .describe("Absolute path to the repository (where docker-compose runs)."),
193
+ composeFile: z
194
+ .string()
195
+ .optional()
196
+ .describe("Path to the base docker-compose file (absolute, or relative to repositoryPath). Omit to auto-discover docker-compose.yml/.yaml or compose.yml/.yaml at the repo root. Provide this when the compose file lives in a subdirectory or has a non-default name."),
197
+ },
198
+ }, async (args) => {
199
+ try {
200
+ const result = setupLocalDevWorker(args.repositoryPath, args.composeFile);
201
+ AnalyticsService.pushMCPToolEvent(TOOL_NAME, undefined, {}).catch(() => { });
202
+ logger.info("Generated local-dev worker compose override", {
203
+ overrideFile: result.overrideFile,
204
+ dockerNetwork: result.dockerNetwork,
205
+ });
206
+ return {
207
+ content: [
208
+ {
209
+ type: "text",
210
+ text: JSON.stringify(result, null, 2),
211
+ },
212
+ ],
213
+ structuredContent: { ...result },
214
+ };
215
+ }
216
+ catch (err) {
217
+ const message = err instanceof Error ? err.message : String(err);
218
+ return {
219
+ isError: true,
220
+ content: [{ type: "text", text: message }],
221
+ };
222
+ }
223
+ });
224
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,129 @@
1
+ import { describe, it, expect, afterEach } from "@jest/globals";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import yaml from "js-yaml";
6
+ import { setupLocalDevWorker } from "./localDevWorkerComposeTool.js";
7
+ import { WORKER_DOCKER_IMAGE } from "../utils/versions.js";
8
+ /** Create a project dir (named, for deterministic project-name derivation) with a compose file. */
9
+ function makeRepo(projectName, composeBody, fileName = "docker-compose.yml") {
10
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-worker-"));
11
+ const repo = path.join(root, projectName);
12
+ fs.mkdirSync(repo, { recursive: true });
13
+ fs.writeFileSync(path.join(repo, fileName), composeBody);
14
+ return { root, repo };
15
+ }
16
+ const NAMED_NETWORK_COMPOSE = `services:
17
+ monolith:
18
+ image: monolith
19
+ networks: [checkr_default]
20
+ networks:
21
+ checkr_default:
22
+ driver: bridge
23
+ `;
24
+ const DEFAULT_NETWORK_COMPOSE = `services:
25
+ monolith:
26
+ image: monolith
27
+ `;
28
+ // Network with an explicit `name:` — Compose uses that verbatim (not project-scoped).
29
+ const EXPLICIT_NAME_COMPOSE = `services:
30
+ monolith:
31
+ image: monolith
32
+ networks: [app]
33
+ networks:
34
+ app:
35
+ name: my_custom_net
36
+ `;
37
+ // External network — pre-existing, not project-scoped.
38
+ const EXTERNAL_NETWORK_COMPOSE = `services:
39
+ monolith:
40
+ image: monolith
41
+ networks: [shared]
42
+ networks:
43
+ shared:
44
+ external: true
45
+ `;
46
+ describe("setupLocalDevWorker", () => {
47
+ const created = [];
48
+ beforeAll(() => {
49
+ // Skip Docker daemon check in unit tests — these tests validate YAML
50
+ // generation logic, not Docker connectivity.
51
+ process.env.SKIP_DOCKER_CHECK = "1";
52
+ });
53
+ afterAll(() => {
54
+ delete process.env.SKIP_DOCKER_CHECK;
55
+ });
56
+ afterEach(() => {
57
+ while (created.length)
58
+ fs.rmSync(created.pop(), { recursive: true, force: true });
59
+ delete process.env.COMPOSE_PROJECT_NAME;
60
+ });
61
+ function loadOverride(file) {
62
+ return yaml.load(fs.readFileSync(file, "utf-8"));
63
+ }
64
+ it("attaches the worker to the base's named network and resolves <project>_<key>", () => {
65
+ const { root, repo } = makeRepo("demoapp", NAMED_NETWORK_COMPOSE);
66
+ created.push(root);
67
+ const result = setupLocalDevWorker(repo);
68
+ expect(result.dockerNetwork).toBe("demoapp_checkr_default");
69
+ expect(result.workerPort).toBe(35142);
70
+ expect(result.composeFilePrefix).toBe(`-f ${result.baseComposeFile} -f ${result.overrideFile}`);
71
+ const doc = loadOverride(result.overrideFile);
72
+ const worker = doc.services.skyramp;
73
+ expect(worker.image).toBe(WORKER_DOCKER_IMAGE);
74
+ expect(worker.ports).toEqual(["127.0.0.1:35142:35142"]); // control port bound to localhost only
75
+ expect(worker.volumes).toContain("/var/run/docker.sock:/var/run/docker.sock");
76
+ expect(worker.networks).toEqual(["checkr_default"]);
77
+ expect(doc.networks).toHaveProperty("checkr_default");
78
+ expect(doc.volumes).toHaveProperty("skyramp");
79
+ });
80
+ it("uses an explicit network name: verbatim (not project-scoped)", () => {
81
+ const { root, repo } = makeRepo("demoapp", EXPLICIT_NAME_COMPOSE);
82
+ created.push(root);
83
+ const result = setupLocalDevWorker(repo);
84
+ expect(result.dockerNetwork).toBe("my_custom_net");
85
+ expect(loadOverride(result.overrideFile).services.skyramp.networks).toEqual(["app"]);
86
+ });
87
+ it("uses the key (not project-scoped) for an external network", () => {
88
+ const { root, repo } = makeRepo("demoapp", EXTERNAL_NETWORK_COMPOSE);
89
+ created.push(root);
90
+ const result = setupLocalDevWorker(repo);
91
+ expect(result.dockerNetwork).toBe("shared");
92
+ });
93
+ it("joins the implicit default network when the base declares none", () => {
94
+ const { root, repo } = makeRepo("plainapp", DEFAULT_NETWORK_COMPOSE);
95
+ created.push(root);
96
+ const result = setupLocalDevWorker(repo);
97
+ expect(result.dockerNetwork).toBe("plainapp_default");
98
+ const doc = loadOverride(result.overrideFile);
99
+ // No named networks → worker has no explicit networks list (joins default).
100
+ expect(doc.services.skyramp.networks).toBeUndefined();
101
+ expect(doc.networks).toBeUndefined();
102
+ });
103
+ it("honors COMPOSE_PROJECT_NAME for the network name", () => {
104
+ const { root, repo } = makeRepo("ignored-basename", NAMED_NETWORK_COMPOSE);
105
+ created.push(root);
106
+ process.env.COMPOSE_PROJECT_NAME = "custom";
107
+ const result = setupLocalDevWorker(repo);
108
+ expect(result.dockerNetwork).toBe("custom_checkr_default");
109
+ });
110
+ it("resolves a non-root composeFile and builds the right -f prefix", () => {
111
+ const { root, repo } = makeRepo("subdirapp", NAMED_NETWORK_COMPOSE, "docker-compose.yml");
112
+ created.push(root);
113
+ // Move the compose into a subdir to simulate a non-default location.
114
+ const subdir = path.join(repo, "infra");
115
+ fs.mkdirSync(subdir);
116
+ const moved = path.join(subdir, "compose.yml");
117
+ fs.renameSync(path.join(repo, "docker-compose.yml"), moved);
118
+ const result = setupLocalDevWorker(repo, "infra/compose.yml");
119
+ expect(result.baseComposeFile).toBe(moved);
120
+ expect(result.composeFilePrefix).toContain(`-f ${moved}`);
121
+ // Project name comes from the working directory (repo root), not the compose file's directory.
122
+ expect(result.dockerNetwork).toBe("subdirapp_checkr_default");
123
+ });
124
+ it("throws a clear error when no compose file is found", () => {
125
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-worker-"));
126
+ created.push(root);
127
+ expect(() => setupLocalDevWorker(root)).toThrow(/No docker-compose file found/);
128
+ });
129
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,255 @@
1
+ import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { RuntimeEnvironment, TestType } from "../types/TestTypes.js";
6
+ const generateRestMock = jest.fn();
7
+ const generateGrpcMock = jest.fn();
8
+ jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
9
+ AnalyticsService: {
10
+ pushMCPToolEvent: jest.fn(() => Promise.resolve(undefined)),
11
+ pushTestGenerationToolEvent: jest.fn(() => Promise.resolve(undefined)),
12
+ },
13
+ }));
14
+ jest.unstable_mockModule("../utils/gitStaging.js", () => ({
15
+ resolveOutputDir: jest.fn((outputDir) => outputDir),
16
+ stageGeneratedPaths: jest.fn(),
17
+ }));
18
+ jest.unstable_mockModule("@skyramp/skyramp", () => ({
19
+ SkyrampClient: jest.fn().mockImplementation(() => ({
20
+ generateRestMock,
21
+ generateGrpcMock,
22
+ generateKafkaMock: jest.fn(),
23
+ })),
24
+ }));
25
+ const { executeBatchMockGeneration, MockProtocol, registerBatchMockTool } = await import("./generate-tests/batchMockGenerationTool.js");
26
+ const { MockGenerationService } = await import("./generate-tests/generateMockRestTool.js");
27
+ const { registerEnhanceAssertionsTool } = await import("./code-refactor/enhanceAssertionsTool.js");
28
+ const { registerPreflightMockCheckTool } = await import("./preflightMockCheckTool.js");
29
+ const { AnalyticsService } = await import("../services/AnalyticsService.js");
30
+ function registerToolForTest(register) {
31
+ let registered;
32
+ const server = {
33
+ registerTool: jest.fn((_name, config, handler) => {
34
+ registered = { config, handler };
35
+ }),
36
+ };
37
+ register(server);
38
+ if (!registered)
39
+ throw new Error("tool was not registered");
40
+ return registered;
41
+ }
42
+ function registerEnhanceToolForTest() {
43
+ return registerToolForTest(registerEnhanceAssertionsTool);
44
+ }
45
+ function registerPreflightToolForTest() {
46
+ return registerToolForTest(registerPreflightMockCheckTool);
47
+ }
48
+ function registerBatchMockToolForTest() {
49
+ return registerToolForTest(registerBatchMockTool);
50
+ }
51
+ describe("MockGenerationService", () => {
52
+ let tmpDir;
53
+ beforeEach(() => {
54
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-single-mock-"));
55
+ generateGrpcMock.mockReset();
56
+ });
57
+ afterEach(() => {
58
+ fs.rmSync(tmpDir, { recursive: true, force: true });
59
+ });
60
+ it("omits responseData for single gRPC status error mocks", async () => {
61
+ generateGrpcMock.mockResolvedValue("success: generated mock");
62
+ const result = await new MockGenerationService().generateTest({
63
+ outputDir: tmpDir,
64
+ force: true,
65
+ protocol: MockProtocol.Grpc,
66
+ endpointURL: "profile-service:50051",
67
+ protoPath: "/repo/proto/profile.proto",
68
+ grpcServiceName: "ProfileService",
69
+ method: "GetProfile",
70
+ grpcStatusCode: 5,
71
+ grpcStatusMessage: "profile not found",
72
+ });
73
+ expect(result.isError).toBe(false);
74
+ expect(generateGrpcMock).toHaveBeenCalledWith(expect.objectContaining({
75
+ grpcStatusCode: 5,
76
+ grpcStatusMessage: "profile not found",
77
+ }));
78
+ expect(generateGrpcMock.mock.calls[0][0]).not.toHaveProperty("responseData");
79
+ });
80
+ });
81
+ describe("executeBatchMockGeneration", () => {
82
+ let tmpDir;
83
+ beforeEach(() => {
84
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-batch-mocks-"));
85
+ generateRestMock.mockReset();
86
+ generateGrpcMock.mockReset();
87
+ });
88
+ afterEach(() => {
89
+ fs.rmSync(tmpDir, { recursive: true, force: true });
90
+ });
91
+ it("passes per-mock output names and directories to the generator", async () => {
92
+ const alternateDir = path.join(tmpDir, "alternate");
93
+ generateRestMock.mockResolvedValue("success: generated mock");
94
+ const result = await executeBatchMockGeneration({
95
+ outputDir: tmpDir,
96
+ mocks: [
97
+ {
98
+ protocol: MockProtocol.Rest,
99
+ endpointURL: "http://billing-service:4000/v1/preferences",
100
+ method: "PUT",
101
+ output: "billing_preferences_success_mock.py",
102
+ },
103
+ {
104
+ protocol: MockProtocol.Rest,
105
+ endpointURL: "http://billing-service:4000/v1/preferences",
106
+ method: "PUT",
107
+ outputDir: alternateDir,
108
+ output: "billing_preferences_error_mock.py",
109
+ },
110
+ ],
111
+ });
112
+ expect(result.isError).toBe(false);
113
+ expect(generateRestMock).toHaveBeenCalledTimes(2);
114
+ expect(generateRestMock.mock.calls[0][0]).toMatchObject({
115
+ outputDir: tmpDir,
116
+ output: "billing_preferences_success_mock.py",
117
+ });
118
+ expect(generateRestMock.mock.calls[1][0]).toMatchObject({
119
+ outputDir: alternateDir,
120
+ output: "billing_preferences_error_mock.py",
121
+ });
122
+ expect(fs.existsSync(alternateDir)).toBe(true);
123
+ const text = result.content[0].text;
124
+ expect(text).toContain("billing_preferences_error_mock.py");
125
+ });
126
+ it("passes gRPC status error fields to the generator", async () => {
127
+ generateGrpcMock.mockResolvedValue("success: generated mock");
128
+ const result = await executeBatchMockGeneration({
129
+ outputDir: tmpDir,
130
+ mocks: [
131
+ {
132
+ protocol: MockProtocol.Grpc,
133
+ endpointURL: "profile-service:50051",
134
+ protoPath: "/repo/proto/profile.proto",
135
+ grpcServiceName: "ProfileService",
136
+ method: "GetProfile",
137
+ grpcStatusCode: 5,
138
+ grpcStatusMessage: "profile not found",
139
+ },
140
+ ],
141
+ });
142
+ expect(result.isError).toBe(false);
143
+ expect(generateGrpcMock).toHaveBeenCalledWith(expect.objectContaining({
144
+ grpcStatusCode: 5,
145
+ grpcStatusMessage: "profile not found",
146
+ }));
147
+ expect(generateGrpcMock.mock.calls[0][0]).not.toHaveProperty("responseData");
148
+ });
149
+ it("marks partial failures as tool errors", async () => {
150
+ generateRestMock.mockResolvedValue("success: generated mock");
151
+ const result = await executeBatchMockGeneration({
152
+ outputDir: tmpDir,
153
+ mocks: [
154
+ {
155
+ protocol: MockProtocol.Rest,
156
+ endpointURL: "http://billing-service:4000/v1/preferences",
157
+ method: "PUT",
158
+ output: "billing_preferences_success_mock.py",
159
+ },
160
+ {
161
+ protocol: MockProtocol.Rest,
162
+ endpointURL: "http://billing-service:4000/v1/preferences",
163
+ method: "",
164
+ output: "billing_preferences_error_mock.py",
165
+ },
166
+ ],
167
+ });
168
+ expect(result.isError).toBe(true);
169
+ const text = result.content[0].text;
170
+ expect(text).toContain("Succeeded:** 1");
171
+ expect(text).toContain("Failed:** 1");
172
+ });
173
+ });
174
+ describe("skyramp_preflight_mock_check", () => {
175
+ const loopbackRestMock = {
176
+ protocol: MockProtocol.Rest,
177
+ endpointURL: "http://localhost:4000/v1/users",
178
+ method: "GET",
179
+ };
180
+ it("defaults to Docker local-dev blockers", async () => {
181
+ const tool = registerPreflightToolForTest();
182
+ const result = await tool.handler({ mocks: [loopbackRestMock] });
183
+ expect(result.structuredContent.hasBlockers).toBe(true);
184
+ expect(result.structuredContent.warnings[0].code).toBe("REST_LOOPBACK_URL");
185
+ });
186
+ it("allows callers to disable Docker local-dev checks", async () => {
187
+ const tool = registerPreflightToolForTest();
188
+ const result = await tool.handler({
189
+ mocks: [loopbackRestMock],
190
+ dockerLocalDev: false,
191
+ });
192
+ expect(result.structuredContent.hasBlockers).toBe(false);
193
+ expect(result.structuredContent.warnings).toHaveLength(0);
194
+ });
195
+ });
196
+ describe("skyramp_batch_mock_generation schema", () => {
197
+ it("uses the shared runtime schema and normalizes aliases", () => {
198
+ const tool = registerBatchMockToolForTest();
199
+ expect(tool.config.inputSchema.runtime.safeParse("local").data).toBe(RuntimeEnvironment.LOCAL);
200
+ expect(tool.config.inputSchema.runtime.safeParse("docker").data).toBe(RuntimeEnvironment.DOCKER);
201
+ expect(tool.config.inputSchema.runtime.safeParse("k8s").data).toBe(RuntimeEnvironment.KUBERNETES);
202
+ expect(tool.config.inputSchema.runtime.safeParse("kubernetes").data).toBe(RuntimeEnvironment.KUBERNETES);
203
+ expect(tool.config.inputSchema.runtime.safeParse("dockre").success).toBe(false);
204
+ });
205
+ });
206
+ describe("skyramp_enhance_assertions autoApply", () => {
207
+ beforeEach(() => {
208
+ delete process.env.SKYRAMP_FEATURE_TESTBOT;
209
+ jest.clearAllMocks();
210
+ });
211
+ it("includes integration-specific assertion guidance", async () => {
212
+ const tool = registerEnhanceToolForTest();
213
+ const result = await tool.handler({
214
+ testFile: "/repo/tests/account_integration_test.py",
215
+ testType: TestType.INTEGRATION,
216
+ enhanceType: "generation",
217
+ autoApply: true,
218
+ });
219
+ expect(result.content[0].text).toContain("Type-specific assertion guidance");
220
+ expect(result.content[0].text).toContain("Chained values across steps");
221
+ });
222
+ it("records autoApply analytics with native boolean value", async () => {
223
+ const tool = registerEnhanceToolForTest();
224
+ await tool.handler({
225
+ testFile: "/repo/tests/account_integration_test.py",
226
+ testType: TestType.INTEGRATION,
227
+ enhanceType: "generation",
228
+ autoApply: true,
229
+ });
230
+ expect(AnalyticsService.pushMCPToolEvent).toHaveBeenCalledWith("skyramp_enhance_assertions", undefined, expect.objectContaining({ autoApply: true }));
231
+ });
232
+ it("includes provider contract-specific assertion guidance", async () => {
233
+ const tool = registerEnhanceToolForTest();
234
+ const result = await tool.handler({
235
+ testFile: "/repo/tests/account_contract_test.py",
236
+ testType: TestType.CONTRACT,
237
+ enhanceType: "generation",
238
+ autoApply: true,
239
+ });
240
+ expect(result.content[0].text).toContain("Chained values from inline request data");
241
+ expect(result.content[0].text).toContain("Do not reference `beforeAll` / `afterAll`");
242
+ });
243
+ it("includes UI-specific and Testbot-specific assertion guidance", async () => {
244
+ process.env.SKYRAMP_FEATURE_TESTBOT = "1";
245
+ const tool = registerEnhanceToolForTest();
246
+ const result = await tool.handler({
247
+ testFile: "/repo/tests/account_ui.spec.ts",
248
+ testType: TestType.UI,
249
+ enhanceType: "generation",
250
+ autoApply: true,
251
+ });
252
+ expect(result.content[0].text).toContain("Critical UI Assertions");
253
+ expect(result.content[0].text).toContain("Additional Testbot-Specific Checks");
254
+ });
255
+ });
@@ -1,4 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare enum CallingAgentIde {
3
+ Cursor = "cursor",
4
+ Claude = "claude"
5
+ }
2
6
  /** User params passed to workflow builder (optional fields may be undefined) */
3
7
  export type OneClickParams = {
4
8
  workflow: string;
@@ -7,5 +11,14 @@ export type OneClickParams = {
7
11
  apiSchema?: string;
8
12
  topN?: number;
9
13
  token?: string;
14
+ baseBranch?: string;
15
+ servicesToMock?: string;
16
+ realServices?: string;
17
+ mockProtocols?: string;
18
+ proxyUrl?: string;
19
+ proxyKubeconfig?: string;
20
+ proxyNamespace?: string;
21
+ callingAgent?: CallingAgentIde;
22
+ rawCallingAgent?: string;
10
23
  };
11
24
  export declare function registerOneClickTool(server: McpServer): void;