@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.
- package/build/commands/commandLibrary.d.ts +1 -0
- package/build/commands/commandLibrary.js +18 -17
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +175 -0
- package/build/index.js +74 -13
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +1 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
- package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
- package/build/prompts/local-dev/local-dev-plan.js +414 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +152 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
- package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
- package/build/prompts/sut-setup/shared.d.ts +20 -0
- package/build/prompts/sut-setup/shared.js +69 -7
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +79 -16
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +34 -1
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +4 -34
- package/build/resources/testbotResource.js +2 -1
- package/build/services/TestExecutionService.d.ts +10 -2
- package/build/services/TestExecutionService.js +220 -28
- package/build/services/TestExecutionService.test.js +447 -16
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/tool-phases.js +4 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +34 -2
- package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
- package/build/tools/enrichTestWithMocksTool.js +414 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +397 -0
- package/build/tools/executeSkyrampTestTool.d.ts +3 -0
- package/build/tools/executeSkyrampTestTool.js +73 -18
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +111 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +72 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +382 -0
- package/build/tools/generate-tests/batchMockGenerationTool.test.d.ts +1 -0
- package/build/tools/generate-tests/batchMockGenerationTool.test.js +514 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
- package/build/tools/generate-tests/generateMockRestTool.js +193 -21
- package/build/tools/generate-tests/generateMockRestTool.test.d.ts +1 -0
- package/build/tools/generate-tests/generateMockRestTool.test.js +340 -0
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +199 -0
- package/build/tools/one-click/oneClickTool.d.ts +10 -0
- package/build/tools/one-click/oneClickTool.js +182 -40
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +204 -0
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +91 -0
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +59 -38
- package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
- package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/RepositoryAnalysis.d.ts +117 -0
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/TestTypes.d.ts +11 -2
- package/build/types/TestTypes.js +11 -1
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/analyze-openapi.test.d.ts +1 -0
- package/build/utils/analyze-openapi.test.js +19 -0
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +96 -14
- package/build/utils/branchDiff.test.d.ts +1 -0
- package/build/utils/branchDiff.test.js +107 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +15 -1
- package/build/utils/featureFlags.js +19 -2
- package/build/utils/featureFlags.test.js +57 -2
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/grpcMockValidation.test.d.ts +1 -0
- package/build/utils/grpcMockValidation.test.js +41 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +13 -0
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +80 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +88 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +35 -21
- package/build/workspace/workspace.js +8 -4
- package/build/workspace/workspace.test.js +65 -6
- 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
|
-
|
|
85
|
-
//
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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(
|
|
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
|
-
|
|
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 {};
|