@skyramp/mcp 0.2.8 → 0.2.150-rc.local.de

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 (93) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +18 -17
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +175 -0
  5. package/build/index.js +74 -13
  6. package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -0
  7. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
  8. package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
  9. package/build/prompts/local-dev/local-dev-plan.js +414 -0
  10. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  11. package/build/prompts/local-dev/local-dev-prompts.js +152 -0
  12. package/build/prompts/prompt-utils.d.ts +8 -0
  13. package/build/prompts/prompt-utils.js +33 -0
  14. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  15. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  16. package/build/prompts/sut-setup/shared.d.ts +20 -0
  17. package/build/prompts/sut-setup/shared.js +69 -7
  18. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +79 -16
  20. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +34 -1
  22. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  23. package/build/prompts/testbot/testbot-prompts.js +4 -34
  24. package/build/resources/testbotResource.js +2 -1
  25. package/build/services/TestExecutionService.d.ts +10 -2
  26. package/build/services/TestExecutionService.js +220 -28
  27. package/build/services/TestExecutionService.test.js +447 -16
  28. package/build/services/TestGenerationService.d.ts +2 -2
  29. package/build/tool-phases.js +4 -0
  30. package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -2
  31. package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
  32. package/build/tools/enrichTestWithMocksTool.js +414 -0
  33. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  34. package/build/tools/enrichTestWithMocksTool.test.js +397 -0
  35. package/build/tools/executeSkyrampTestTool.d.ts +3 -0
  36. package/build/tools/executeSkyrampTestTool.js +73 -18
  37. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  38. package/build/tools/executeSkyrampTestTool.test.js +111 -0
  39. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +72 -0
  40. package/build/tools/generate-tests/batchMockGenerationTool.js +382 -0
  41. package/build/tools/generate-tests/batchMockGenerationTool.test.d.ts +1 -0
  42. package/build/tools/generate-tests/batchMockGenerationTool.test.js +514 -0
  43. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  44. package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
  45. package/build/tools/generate-tests/generateMockRestTool.js +193 -21
  46. package/build/tools/generate-tests/generateMockRestTool.test.d.ts +1 -0
  47. package/build/tools/generate-tests/generateMockRestTool.test.js +340 -0
  48. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
  49. package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
  50. package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
  51. package/build/tools/generateEnrichedIntegrationTestTool.test.js +199 -0
  52. package/build/tools/one-click/oneClickTool.d.ts +10 -0
  53. package/build/tools/one-click/oneClickTool.js +182 -40
  54. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  55. package/build/tools/one-click/oneClickTool.test.js +204 -0
  56. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  57. package/build/tools/preflightMockCheckTool.js +91 -0
  58. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  59. package/build/tools/test-management/analyzeChangesTool.js +59 -38
  60. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  61. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  62. package/build/types/OneClickCommands.d.ts +1 -1
  63. package/build/types/RepositoryAnalysis.d.ts +117 -0
  64. package/build/types/RepositoryAnalysis.js +16 -2
  65. package/build/types/TestTypes.d.ts +11 -2
  66. package/build/types/TestTypes.js +11 -1
  67. package/build/utils/analyze-openapi.js +18 -1
  68. package/build/utils/analyze-openapi.test.d.ts +1 -0
  69. package/build/utils/analyze-openapi.test.js +19 -0
  70. package/build/utils/branchDiff.d.ts +17 -1
  71. package/build/utils/branchDiff.js +96 -14
  72. package/build/utils/branchDiff.test.d.ts +1 -0
  73. package/build/utils/branchDiff.test.js +107 -0
  74. package/build/utils/docker.test.js +1 -1
  75. package/build/utils/featureFlags.d.ts +15 -1
  76. package/build/utils/featureFlags.js +19 -2
  77. package/build/utils/featureFlags.test.js +57 -2
  78. package/build/utils/grpcMockValidation.d.ts +1 -0
  79. package/build/utils/grpcMockValidation.js +49 -0
  80. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  81. package/build/utils/grpcMockValidation.test.js +41 -0
  82. package/build/utils/httpMethodValidation.d.ts +4 -0
  83. package/build/utils/httpMethodValidation.js +13 -0
  84. package/build/utils/mockCompatibility.d.ts +49 -0
  85. package/build/utils/mockCompatibility.js +80 -0
  86. package/build/utils/mockCompatibility.test.d.ts +1 -0
  87. package/build/utils/mockCompatibility.test.js +88 -0
  88. package/build/utils/versions.d.ts +3 -3
  89. package/build/utils/versions.js +1 -1
  90. package/build/workspace/workspace.d.ts +35 -21
  91. package/build/workspace/workspace.js +8 -4
  92. package/build/workspace/workspace.test.js +65 -6
  93. package/package.json +2 -2
@@ -0,0 +1,397 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import { buildMockImportBlock, injectMockImports, } from "./enrichTestWithMocksTool.js";
5
+ import { ProgrammingLanguage } from "../types/TestTypes.js";
6
+ describe("enrichTestWithMocksTool", () => {
7
+ function withTempTestFile(content, callback) {
8
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-enrich-"));
9
+ try {
10
+ const testDir = path.join(tempDir, "tests", "skyramp");
11
+ const mocksDir = path.join(testDir, "mocks");
12
+ fs.mkdirSync(mocksDir, { recursive: true });
13
+ const testFile = path.join(testDir, "partner_happy_path_integration_test.py");
14
+ fs.writeFileSync(testFile, content);
15
+ callback(testFile, mocksDir);
16
+ }
17
+ finally {
18
+ fs.rmSync(tempDir, { recursive: true, force: true });
19
+ }
20
+ }
21
+ it("builds an apply_all_mocks helper that reuses the integration test client", () => {
22
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
23
+ const mockFiles = [
24
+ path.join(mocksDir, "ams_validate_POST_mock.py"),
25
+ path.join(mocksDir, "billing_preferences_PUT_mock.py"),
26
+ ];
27
+ const block = buildMockImportBlock(testFile, "partner-happy-path", mockFiles);
28
+ expect(block).toContain("def apply_all_mocks(client):");
29
+ expect(block).toContain("client.remove_all_mocks()");
30
+ expect(block).toContain("all_mocks = []");
31
+ expect(block).toContain("all_mocks.extend(mocks)");
32
+ expect(block).toContain("client.apply_mock(all_mocks)");
33
+ // MOCK_TARGET_URLS always emitted (empty when no mock files have parseable URLs)
34
+ expect(block).toContain("MOCK_TARGET_URLS = {");
35
+ expect(block).toContain("target_url = MOCK_TARGET_URLS.get(service_name)");
36
+ // No globals() hack — MOCK_TARGET_URLS is always defined
37
+ expect(block).not.toContain("globals()");
38
+ // No Kafka mocks — normalization helper omitted
39
+ expect(block).not.toContain("_normalize_kafka_mocks");
40
+ expect(block).not.toContain("from collections import defaultdict");
41
+ expect(block).not.toContain("by_url[mock.url].append(mock)");
42
+ expect(block).not.toContain('for protocol in ("rest", "grpc", "kafka"):');
43
+ expect(block).not.toContain('runtime="docker"');
44
+ });
45
+ });
46
+ it("supports explicit Python language", () => {
47
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
48
+ const block = buildMockImportBlock(testFile, "partner-happy-path", [path.join(mocksDir, "billing_preferences_PUT_mock.py")], undefined, ProgrammingLanguage.PYTHON);
49
+ expect(block).toContain("def apply_all_mocks(client):");
50
+ expect(block).toContain("import importlib.util");
51
+ });
52
+ });
53
+ it("rejects explicit non-Python languages", () => {
54
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
55
+ expect(() => buildMockImportBlock(testFile, "partner-happy-path", [path.join(mocksDir, "billing_preferences_PUT_mock.py")], undefined, ProgrammingLanguage.TYPESCRIPT)).toThrow('Mock enrichment is currently supported only for Python tests; received language "typescript"');
56
+ });
57
+ });
58
+ it("rejects inferred non-Python test files", () => {
59
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-enrich-ts-"));
60
+ try {
61
+ const testDir = path.join(tempDir, "tests", "skyramp");
62
+ const mocksDir = path.join(testDir, "mocks");
63
+ fs.mkdirSync(mocksDir, { recursive: true });
64
+ const testFile = path.join(testDir, "partner_happy_path_integration_test.ts");
65
+ expect(() => injectMockImports("import skyramp from '@skyramp/skyramp';\n", testFile, "partner-happy-path", [path.join(mocksDir, "billing_preferences_PUT_mock.py")])).toThrow('Mock enrichment is currently supported only for Python tests; received language "typescript"');
66
+ }
67
+ finally {
68
+ fs.rmSync(tempDir, { recursive: true, force: true });
69
+ }
70
+ });
71
+ it("does not duplicate an existing mock block on retry", () => {
72
+ const original = [
73
+ "import skyramp",
74
+ "",
75
+ "# ─── Mock Scenario: existing ──────────────────────────────────────",
76
+ 'SCENARIO_NAME = "existing"',
77
+ "",
78
+ "from mocks.billing_preferences_PUT_mock import get_all_mocks as get_billing_preferences_PUT_mock_mocks",
79
+ "",
80
+ "MOCK_SERVICES = {",
81
+ ' "billing-preferences-PUT": get_billing_preferences_PUT_mock_mocks,',
82
+ "}",
83
+ "",
84
+ "def apply_all_mocks(client):",
85
+ ' """Apply all mocks collectively in one atomic operation."""',
86
+ " client.remove_all_mocks()",
87
+ "",
88
+ "def test_integration():",
89
+ " client = skyramp.Client(",
90
+ ' runtime="local"',
91
+ " )",
92
+ " response = client.send_request(",
93
+ ' url="http://localhost:3000",',
94
+ ' path="/api/v1/accounts",',
95
+ ' method="POST"',
96
+ " )",
97
+ " assert response.status_code == 201",
98
+ "",
99
+ ].join("\n");
100
+ withTempTestFile(original, (testFile, mocksDir) => {
101
+ const enriched = injectMockImports(original, testFile, "existing", [
102
+ path.join(mocksDir, "billing_preferences_PUT_mock.py"),
103
+ ]);
104
+ expect(enriched.match(/MOCK_SERVICES = \{/g)).toHaveLength(1);
105
+ expect(enriched.match(/def apply_all_mocks\(client\):/g)).toHaveLength(1);
106
+ expect(enriched).toContain("apply_all_mocks(client)");
107
+ expect(enriched).toContain('"billing-preferences-PUT": {"factory": _load_mock_factory("mocks/billing_preferences_PUT_mock.py", "billing_preferences_PUT_mock"), "protocol": "rest"}');
108
+ });
109
+ });
110
+ it("normalizes generated localhost mock URLs back to original service origins", () => {
111
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
112
+ const mockFile = path.join(mocksDir, "customer_configurations_GET_mock.py");
113
+ fs.writeFileSync(mockFile, [
114
+ "# Generated by Skyramp",
115
+ "# Command: skyramp generate mock rest http://partner-accounts:50052/api/v1/applications/app_customer_config_test/customer_configurations \\",
116
+ 'URL = skyramp.get_base_url("SKYRAMP_TEST_BASE_URL", "http://localhost:8080")',
117
+ ].join("\n"));
118
+ const block = buildMockImportBlock(testFile, "partner-happy-path", [
119
+ mockFile,
120
+ ]);
121
+ expect(block).toContain("MOCK_TARGET_URLS = {");
122
+ expect(block).toContain('"customer-configurations-GET": "http://partner-accounts:50052"');
123
+ expect(block).toContain("target_url = MOCK_TARGET_URLS.get(service_name)");
124
+ expect(block).toContain("mock.url = target_url");
125
+ // No globals() hack
126
+ expect(block).not.toContain("globals()");
127
+ });
128
+ });
129
+ it("normalizes multiple REST mock command targets to service origins and ignores generated URL defaults", () => {
130
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
131
+ const amsMock = path.join(mocksDir, "validate_POST_mock.py");
132
+ const billingMock = path.join(mocksDir, "billing_preferences_PUT_mock.py");
133
+ fs.writeFileSync(amsMock, [
134
+ "# Generated by Skyramp",
135
+ "# Command: skyramp generate mock rest http://ams:4000/api/internal/v1/users/validate?trace=abc",
136
+ 'URL = skyramp.get_base_url("SKYRAMP_TEST_BASE_URL", "http://localhost:8080")',
137
+ ].join("\n"));
138
+ fs.writeFileSync(billingMock, [
139
+ "# Generated by Skyramp",
140
+ "# Command: skyramp generate mock rest http://billing:5001/accounts/:id/billing_preferences",
141
+ 'URL = skyramp.get_base_url("SKYRAMP_TEST_BASE_URL", "http://localhost:8080")',
142
+ ].join("\n"));
143
+ const block = buildMockImportBlock(testFile, "rest-routing", [
144
+ amsMock,
145
+ billingMock,
146
+ ]);
147
+ expect(block).toContain('"validate-POST": "http://ams:4000"');
148
+ expect(block).toContain('"billing-preferences-PUT": "http://billing:5001"');
149
+ expect(block).toContain("mock.url = target_url");
150
+ expect(block).not.toContain('"http://localhost:8080"');
151
+ expect(block).not.toContain('"http://ams:4000/api/internal/v1/users/validate?trace=abc"');
152
+ });
153
+ });
154
+ it("normalizes generated gRPC mock host:port targets back to original service origins", () => {
155
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
156
+ const mockFile = path.join(mocksDir, "partner_accounts_GET_mock.py");
157
+ fs.writeFileSync(mockFile, [
158
+ "# Generated by Skyramp",
159
+ "# Command: skyramp generate mock grpc partner-accounts:50051 \\",
160
+ "# \\t\\t--proto-path app/proto/partner_accounts.proto \\",
161
+ "# \\t\\t--service PartnerAccountsService",
162
+ 'URL = skyramp.get_base_url("SKYRAMP_TEST_BASE_URL", "http://localhost:8080")',
163
+ ].join("\n"));
164
+ const block = buildMockImportBlock(testFile, "partner-happy-path", [
165
+ mockFile,
166
+ ]);
167
+ expect(block).toContain('"partner-accounts-GET": "partner-accounts:50051"');
168
+ expect(block).toContain("mock.url = target_url");
169
+ });
170
+ });
171
+ it("normalizes generated Kafka mock host:port targets back to original broker origins", () => {
172
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
173
+ const mockFile = path.join(mocksDir, "kafka_events_account_created_mock.py");
174
+ fs.writeFileSync(mockFile, [
175
+ "# Generated by Skyramp",
176
+ "# Command: skyramp generate mock kafka redpanda:9092",
177
+ "# \t\t--topic events",
178
+ 'URL = skyramp.get_base_url("SKYRAMP_TEST_BASE_URL", "http://localhost:8080")',
179
+ ].join("\n"));
180
+ const block = buildMockImportBlock(testFile, "partner-happy-path", [
181
+ mockFile,
182
+ ]);
183
+ expect(block).toContain('"kafka-events-account-created": "redpanda:9092"');
184
+ expect(block).toContain('"kafka-events-account-created": {"factory": _load_mock_factory("mocks/kafka_events_account_created_mock.py", "kafka_events_account_created_mock"), "protocol": "kafka"}');
185
+ expect(block).toContain("mock.url = target_url");
186
+ expect(block).not.toContain('if mock_config.get("protocol") == "kafka":');
187
+ expect(block).not.toContain(" continue");
188
+ expect(block).toContain("client.apply_mock(all_mocks)");
189
+ });
190
+ });
191
+ it("injects apply_all_mocks after the generated skyramp client is constructed", () => {
192
+ const original = [
193
+ "import skyramp",
194
+ "",
195
+ "def test_integration():",
196
+ " # Invocation of Skyramp Client",
197
+ " client = skyramp.Client(",
198
+ ' runtime="local"',
199
+ " )",
200
+ "",
201
+ " response = client.send_request(",
202
+ ' url="http://localhost:3000",',
203
+ ' path="/api/v1/accounts",',
204
+ ' method="POST"',
205
+ " )",
206
+ " assert response.status_code == 201",
207
+ "",
208
+ ].join("\n");
209
+ withTempTestFile(original, (testFile, mocksDir) => {
210
+ const enriched = injectMockImports(original, testFile, "partner-happy-path", [path.join(mocksDir, "billing_preferences_PUT_mock.py")]);
211
+ const applyCallIndex = enriched.lastIndexOf("apply_all_mocks(client)");
212
+ expect(applyCallIndex).toBeGreaterThan(-1);
213
+ expect(enriched.indexOf('runtime="local"')).toBeLessThan(applyCallIndex);
214
+ expect(applyCallIndex).toBeLessThan(enriched.indexOf("client.send_request"));
215
+ expect(enriched).not.toContain("SKYRAMP_WORKER_IMAGE");
216
+ expect(enriched).not.toContain("client.worker_image");
217
+ expect(enriched).not.toContain("client.local_image");
218
+ });
219
+ });
220
+ it("replaces an existing generated mock block on retry", () => {
221
+ const original = [
222
+ "import skyramp",
223
+ "",
224
+ "# ─── Mock Scenario: stale ──────────────────────────────────────",
225
+ 'SCENARIO_NAME = "stale"',
226
+ "",
227
+ "from mocks.old_mock import get_all_mocks as get_old_mock_mocks",
228
+ "",
229
+ "MOCK_SERVICES = {",
230
+ ' "old": get_old_mock_mocks,',
231
+ "}",
232
+ "",
233
+ "def apply_all_mocks(client):",
234
+ ' """Apply all mocks collectively in one atomic operation."""',
235
+ " client.remove_all_mocks()",
236
+ " client.apply_mock([])",
237
+ "",
238
+ "def test_integration():",
239
+ " client = skyramp.Client(",
240
+ ' runtime="local"',
241
+ " )",
242
+ " apply_all_mocks(client)",
243
+ " assert True",
244
+ "",
245
+ ].join("\n");
246
+ withTempTestFile(original, (testFile, mocksDir) => {
247
+ const enriched = injectMockImports(original, testFile, "fresh", [
248
+ path.join(mocksDir, "billing_preferences_PUT_mock.py"),
249
+ ]);
250
+ expect(enriched.match(/MOCK_SERVICES = \{/g)).toHaveLength(1);
251
+ expect(enriched.match(/def apply_all_mocks\(client\):/g)).toHaveLength(1);
252
+ expect(enriched).toContain('SCENARIO_NAME = "fresh"');
253
+ expect(enriched).toContain("billing_preferences_PUT_mock");
254
+ expect(enriched).not.toContain("SKYRAMP_WORKER_IMAGE");
255
+ expect(enriched).not.toContain("client.worker_image");
256
+ expect(enriched).not.toContain("client.local_image");
257
+ expect(enriched).not.toContain("old_mock");
258
+ expect(enriched).not.toContain('"old": get_old_mock_mocks');
259
+ });
260
+ });
261
+ it("sanitizes dotted mock filename aliases", () => {
262
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
263
+ const mockFile = path.join(mocksDir, "idl.partner_accounts.customerconfigurationsservice_mock.py");
264
+ fs.writeFileSync(mockFile, [
265
+ "# Generated by Skyramp",
266
+ "# Command: skyramp generate mock grpc partner-accounts:50051 \\",
267
+ "# \t\t--service CustomerConfigurationsService",
268
+ ].join("\n"));
269
+ const block = buildMockImportBlock(testFile, "partner-happy-path", [
270
+ mockFile,
271
+ ]);
272
+ expect(block).toContain("import importlib.util");
273
+ expect(block).toContain('"idl.partner-accounts.customerconfigurationsservice": {"factory": _load_mock_factory("mocks/idl.partner_accounts.customerconfigurationsservice_mock.py", "idl_partner_accounts_customerconfigurationsservice_mock"), "protocol": "grpc"}');
274
+ });
275
+ });
276
+ it("normalizes generated Python Kafka mocks before applying all protocols together", () => {
277
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
278
+ const restMock = path.join(mocksDir, "billing_preferences_PUT_mock.py");
279
+ const grpcMock = path.join(mocksDir, "partner_accounts_mock.py");
280
+ const kafkaMock = path.join(mocksDir, "kafka_events_mock.py");
281
+ fs.writeFileSync(restMock, "# Command: skyramp generate mock rest http://billing:5001/accounts/:id/billing_preferences");
282
+ fs.writeFileSync(grpcMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
283
+ fs.writeFileSync(kafkaMock, "# Command: skyramp generate mock kafka redpanda:9092");
284
+ const block = buildMockImportBlock(testFile, "mixed", [
285
+ restMock,
286
+ grpcMock,
287
+ kafkaMock,
288
+ ]);
289
+ expect(block).toContain('"billing-preferences-PUT": {"factory": _load_mock_factory("mocks/billing_preferences_PUT_mock.py", "billing_preferences_PUT_mock"), "protocol": "rest"}');
290
+ expect(block).toContain('"partner-accounts": {"factory": _load_mock_factory("mocks/partner_accounts_mock.py", "partner_accounts_mock"), "protocol": "grpc"}');
291
+ expect(block).toContain('"kafka-events": {"factory": _load_mock_factory("mocks/kafka_events_mock.py", "kafka_events_mock"), "protocol": "kafka"}');
292
+ expect(block).not.toContain("def _normalize_kafka_mocks(service_name, mocks):");
293
+ expect(block).toContain('"kafka-events": "redpanda:9092"');
294
+ expect(block).not.toContain('if mock_config.get("protocol") == "kafka":');
295
+ expect(block).not.toContain(" continue");
296
+ expect(block).toContain("all_mocks.extend(mocks)");
297
+ expect(block.match(/client\.apply_mock\(all_mocks\)/g)).toHaveLength(1);
298
+ });
299
+ });
300
+ it("omits _normalize_kafka_mocks when no Kafka mocks are present (billing-rejects scenario)", () => {
301
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
302
+ // Mirrors the Checkr billing_rejects_rollback scenario: REST + gRPC only, no Kafka
303
+ const validateMock = path.join(mocksDir, "validate_POST_mock.py");
304
+ const partnerAppsMock = path.join(mocksDir, "partnerapplicationsservice_mock.py");
305
+ const customerConfigsMock = path.join(mocksDir, "customerconfigurationsservice_mock.py");
306
+ const billingMock = path.join(mocksDir, "billing_preferences_PUT_mock.py");
307
+ fs.writeFileSync(validateMock, "# Command: skyramp generate mock rest http://ams:4000/api/internal/v1/users/validate");
308
+ fs.writeFileSync(partnerAppsMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
309
+ fs.writeFileSync(customerConfigsMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
310
+ fs.writeFileSync(billingMock, "# Command: skyramp generate mock rest http://billing:5001/accounts/:id/billing_preferences");
311
+ const block = buildMockImportBlock(testFile, "billing-rejects-rollback", [
312
+ validateMock, partnerAppsMock, customerConfigsMock, billingMock,
313
+ ]);
314
+ expect(block).toContain('SCENARIO_NAME = "billing-rejects-rollback"');
315
+ expect(block).toContain("MOCK_TARGET_URLS = {");
316
+ expect(block).toContain('"validate-POST": "http://ams:4000"');
317
+ expect(block).toContain('"billing-preferences-PUT": "http://billing:5001"');
318
+ expect(block).toContain('"partnerapplicationsservice": "partner-accounts:50051"');
319
+ // No Kafka → normalization helper must be absent
320
+ expect(block).not.toContain("_normalize_kafka_mocks");
321
+ expect(block).not.toContain("globals()");
322
+ // apply_all_mocks still present and complete
323
+ expect(block).toContain("def apply_all_mocks(client):");
324
+ expect(block).toContain("target_url = MOCK_TARGET_URLS.get(service_name)");
325
+ });
326
+ });
327
+ it("emits _normalize_kafka_mocks for Checkr partner-account-with-customer-configs (REST + gRPC + Kafka)", () => {
328
+ withTempTestFile("import skyramp\n", (testFile, mocksDir) => {
329
+ // Mirrors the real partner_account_with_customer_configs test in checkr-demo
330
+ const validateMock = path.join(mocksDir, "validate_POST_mock.py");
331
+ const usersMock = path.join(mocksDir, "users_POST_mock.py");
332
+ const billingMock = path.join(mocksDir, "billing_preferences_PUT_success_mock.py");
333
+ const partnerAppsMock = path.join(mocksDir, "partnerapplicationsservice_mock.py");
334
+ const customerConfigsMock = path.join(mocksDir, "customerconfigurationsservice_mock.py");
335
+ const recacheMock = path.join(mocksDir, "recachepartnermetricsservice_mock.py");
336
+ const kafkaEventsMock = path.join(mocksDir, "kafka_events_mock.py");
337
+ const kafkaConsentMock = path.join(mocksDir, "kafka_consent_customer_mock.py");
338
+ fs.writeFileSync(validateMock, "# Command: skyramp generate mock rest http://ams:4000/api/internal/v1/users/validate");
339
+ fs.writeFileSync(usersMock, "# Command: skyramp generate mock rest http://ams:4000/api/internal/v1/users");
340
+ fs.writeFileSync(billingMock, "# Command: skyramp generate mock rest http://billing:5001/accounts/:id/billing_preferences");
341
+ fs.writeFileSync(partnerAppsMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
342
+ fs.writeFileSync(customerConfigsMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
343
+ fs.writeFileSync(recacheMock, "# Command: skyramp generate mock grpc partner-accounts:50051");
344
+ fs.writeFileSync(kafkaEventsMock, "# Command: skyramp generate mock kafka redpanda:9092");
345
+ fs.writeFileSync(kafkaConsentMock, "# Command: skyramp generate mock kafka redpanda:9092");
346
+ const block = buildMockImportBlock(testFile, "partner-account-with-customer-configs", [
347
+ validateMock, usersMock, billingMock,
348
+ partnerAppsMock, customerConfigsMock, recacheMock,
349
+ kafkaEventsMock, kafkaConsentMock,
350
+ ]);
351
+ expect(block).toContain('SCENARIO_NAME = "partner-account-with-customer-configs"');
352
+ expect(block).not.toContain("def _normalize_kafka_mocks(service_name, mocks):");
353
+ expect(block).not.toContain('if mock_config.get("protocol") == "kafka":');
354
+ expect(block).not.toContain(" continue");
355
+ // REST, gRPC, and Kafka targets are all represented in MOCK_TARGET_URLS.
356
+ expect(block).toContain('"validate-POST": "http://ams:4000"');
357
+ expect(block).toContain('"billing-preferences-PUT-success": "http://billing:5001"');
358
+ expect(block).toContain('"partnerapplicationsservice": "partner-accounts:50051"');
359
+ expect(block).toContain('"kafka-events": "redpanda:9092"');
360
+ expect(block).toContain('"kafka-consent-customer": "redpanda:9092"');
361
+ // No globals() hack
362
+ expect(block).not.toContain("globals()");
363
+ expect(block).toContain("target_url = MOCK_TARGET_URLS.get(service_name)");
364
+ });
365
+ });
366
+ it("preserves helper code after a generated block on retry", () => {
367
+ const original = [
368
+ "import skyramp",
369
+ "",
370
+ "# ─── Mock Scenario: stale ──────────────────────────────────────",
371
+ 'SCENARIO_NAME = "stale"',
372
+ "",
373
+ "MOCK_SERVICES = {",
374
+ "}",
375
+ "",
376
+ "def apply_all_mocks(client):",
377
+ " client.remove_all_mocks()",
378
+ "",
379
+ "def make_payload():",
380
+ ' return {"ok": True}',
381
+ "",
382
+ "def test_integration():",
383
+ " client = skyramp.Client()",
384
+ " apply_all_mocks(client)",
385
+ ' assert make_payload()["ok"]',
386
+ "",
387
+ ].join("\n");
388
+ withTempTestFile(original, (testFile, mocksDir) => {
389
+ const enriched = injectMockImports(original, testFile, "fresh", [
390
+ path.join(mocksDir, "billing_preferences_PUT_mock.py"),
391
+ ]);
392
+ expect(enriched).toContain("\ndef make_payload():");
393
+ expect(enriched).toContain('return {"ok": True}');
394
+ expect(enriched).toContain('SCENARIO_NAME = "fresh"');
395
+ });
396
+ });
397
+ });
@@ -1,2 +1,5 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
2
3
  export declare function registerExecuteSkyrampTestTool(server: McpServer): void;
4
+ export declare function testFileHasDeployableMockTargets(testFile: string): boolean;
5
+ export declare function validateMockIsolationConfirmation(testFile: string, mockIsolationConfirmed?: boolean): CallToolResult | undefined;
@@ -1,11 +1,12 @@
1
1
  import { z } from "zod";
2
2
  import { stripVTControlCharacters } from "util";
3
+ import * as fs from "fs";
3
4
  import { TestExecutionService } from "../services/TestExecutionService.js";
4
5
  import { AnalyticsService } from "../services/AnalyticsService.js";
5
6
  import { TestExecutionStatus } from "../types/TestExecution.js";
6
7
  import { getWorkspaceBaseUrl } from "../utils/workspaceAuth.js";
7
8
  import { ProgrammingLanguage, TestType } from "../types/TestTypes.js";
8
- import { StateManager } from "../utils/AnalysisStateManager.js";
9
+ import { StateManager, } from "../utils/AnalysisStateManager.js";
9
10
  import { logger } from "../utils/logger.js";
10
11
  const TOOL_NAME = "skyramp_execute_test";
11
12
  export function registerExecuteSkyrampTestTool(server) {
@@ -28,6 +29,7 @@ REQUIRED PARAMETERS:
28
29
 
29
30
  OPTIONAL PARAMETERS:
30
31
  - stateFile: Path to state file from skyramp_analyze_changes. When provided, execution results (passed/failed, errors, duration) will be written back to enrich test health analysis.
32
+ - mockIsolationConfirmed: Required for enriched integration tests with MOCK_SERVICES. Set true only after confirming every real service selected for mocking is stopped and will not be started during execution.
31
33
 
32
34
  AUTHENTICATION:
33
35
  Pass an empty string as the token parameter — the server automatically injects SKYRAMP_TEST_TOKEN from the environment. The executor applies it with the correct header defined in workspace.yml:
@@ -53,7 +55,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
53
55
  .describe("Programming language of the test file to execute (e.g., python, javascript, typescript, java)"),
54
56
  testType: z
55
57
  .nativeEnum(TestType)
56
- .describe("Type of the test to execute."),
58
+ .describe("Type of the test to execute. Note: 'mock' is NOT a valid test type — mock files are deployed via their apply_mock() function, not executed as tests."),
57
59
  testFile: z
58
60
  .string()
59
61
  .describe("ALWAYS USE ABSOLUTE PATH to the test file to execute"),
@@ -76,28 +78,42 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
76
78
  .string()
77
79
  .optional()
78
80
  .describe("The owner/repo whose analysis section to write execution results into (e.g. 'letsramp/api-insight'). In a multi-repo run, ALWAYS set it — the primary's owner/repo for a primary-repo test, or a related repo's owner/repo for that repo's section of the run-scoped stateFile. Omit only for a single-repo run."),
81
+ mockIsolationConfirmed: z
82
+ .boolean()
83
+ .optional()
84
+ .describe("For enriched integration tests with MOCK_SERVICES, set true only after verifying every real downstream service selected for mocking is stopped and only the Skyramp worker will own those service aliases during execution."),
79
85
  },
80
86
  _meta: {
81
87
  keywords: ["run test", "execute test"],
82
88
  },
83
89
  }, async (params, extra) => {
84
- let errorResult;
85
- // Helper to send progress notifications to the MCP client
90
+ // Helper to send progress notifications to the MCP client.
91
+ // Wrapped in try-catch so a broken/disconnected transport never crashes
92
+ // the tool handler or causes an unhandled rejection.
86
93
  const sendProgress = async (progress, total, message) => {
87
94
  const progressToken = extra._meta?.progressToken;
88
95
  if (progressToken !== undefined) {
89
- const notification = {
90
- method: "notifications/progress",
91
- params: {
92
- progressToken,
93
- progress,
94
- total,
95
- message,
96
- },
97
- };
98
- await extra.sendNotification(notification);
96
+ try {
97
+ const notification = {
98
+ method: "notifications/progress",
99
+ params: {
100
+ progressToken,
101
+ progress,
102
+ total,
103
+ message,
104
+ },
105
+ };
106
+ await extra.sendNotification(notification);
107
+ }
108
+ catch {
109
+ // Progress notifications are best-effort — swallow failures so
110
+ // a flaky or disconnected transport doesn't kill the execution.
111
+ }
99
112
  }
100
113
  };
114
+ // Send immediate acknowledgment
115
+ await sendProgress(0, 100, "Starting execution...");
116
+ let errorResult;
101
117
  // Progress callback adapter for TestExecutionService
102
118
  const onExecutionProgress = async (progress) => {
103
119
  await sendProgress(progress.percent, 100, progress.message);
@@ -107,20 +123,27 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
107
123
  let useHostNetwork = false;
108
124
  try {
109
125
  // Send initial progress
110
- await sendProgress(0, 100, "Starting test execution...");
126
+ await sendProgress(5, 100, "Starting test execution...");
127
+ const mockIsolationError = validateMockIsolationConfirmation(params.testFile, params.mockIsolationConfirmed);
128
+ if (mockIsolationError) {
129
+ errorResult = mockIsolationError;
130
+ return mockIsolationError;
131
+ }
111
132
  // Always resolve workspace config for dockerNetwork (host networking)
112
133
  // and optionally inject SKYRAMP_TEST_BASE_URL if not already set.
113
134
  if (params.workspacePath) {
114
135
  const { baseUrl, dockerNetwork, candidates } = await getWorkspaceBaseUrl(params.workspacePath, params.testFile, params.language);
115
136
  useHostNetwork = !!dockerNetwork;
116
- if (!process.env.SKYRAMP_TEST_BASE_URL) {
137
+ const shouldInjectBaseUrl = params.testType !== TestType.CONTRACT;
138
+ if (shouldInjectBaseUrl && !process.env.SKYRAMP_TEST_BASE_URL) {
117
139
  if (baseUrl) {
118
140
  process.env.SKYRAMP_TEST_BASE_URL = baseUrl;
119
141
  didSetSkyrampBaseUrl = true;
120
142
  }
121
143
  else if (candidates.length > 0) {
122
144
  return {
123
- content: [{
145
+ content: [
146
+ {
124
147
  type: "text",
125
148
  text: [
126
149
  `Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
@@ -128,7 +151,8 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
128
151
  ``,
129
152
  `Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
130
153
  ].join("\n"),
131
- }],
154
+ },
155
+ ],
132
156
  isError: true,
133
157
  };
134
158
  }
@@ -235,3 +259,34 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
235
259
  }
236
260
  });
237
261
  }
262
+ export function testFileHasDeployableMockTargets(testFile) {
263
+ if (!fs.existsSync(testFile))
264
+ return false;
265
+ const source = fs.readFileSync(testFile, "utf-8");
266
+ return (/MOCK_TARGET_URLS\s*=\s*{\s*["']/s.test(source) ||
267
+ /"protocol"\s*:\s*"(?:rest|grpc|kafka)"/s.test(source));
268
+ }
269
+ export function validateMockIsolationConfirmation(testFile, mockIsolationConfirmed) {
270
+ if (!testFileHasDeployableMockTargets(testFile) || mockIsolationConfirmed) {
271
+ return undefined;
272
+ }
273
+ return {
274
+ content: [
275
+ {
276
+ type: "text",
277
+ text: [
278
+ "**Blocked: mock isolation confirmation is required before execution.**",
279
+ "",
280
+ "This test contains deployable REST/gRPC/Kafka mock targets, so selected downstream services or brokers will be mocked by Skyramp. Before running it, stop every real service container selected for mocking and ensure the SUT will connect after mocks are applied.",
281
+ "",
282
+ "Required checklist:",
283
+ "1. Identify the services in `MOCK_SERVICES` / `MOCK_TARGET_URLS`.",
284
+ "2. Stop the matching real docker-compose services, e.g. `docker compose stop <mocked-service>`.",
285
+ "3. Start or restart only the SUT and real dependencies; do not start mocked downstream services.",
286
+ "4. Re-run `skyramp_execute_test` with `mockIsolationConfirmed: true`.",
287
+ ].join("\n"),
288
+ },
289
+ ],
290
+ isError: true,
291
+ };
292
+ }
@@ -0,0 +1 @@
1
+ export {};