@skyramp/mcp 0.2.8 → 0.2.150-rc.ldw
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 +20 -14
- package/build/commands/commandLibrary.test.d.ts +1 -0
- package/build/commands/commandLibrary.test.js +59 -0
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +188 -0
- package/build/index.js +77 -13
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +9 -8
- package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
- package/build/prompts/local-dev/local-dev-plan.js +419 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +157 -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 +0 -28
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -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/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +15 -10
- package/build/services/TestExecutionService.test.js +75 -1
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/tool-phases.js +4 -0
- package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
- package/build/tools/enrichTestWithMocksTool.test.js +266 -0
- package/build/tools/executeSkyrampTestTool.d.ts +4 -0
- package/build/tools/executeSkyrampTestTool.js +48 -24
- package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
- package/build/tools/executeSkyrampTestTool.test.js +16 -0
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
- package/build/tools/generate-tests/generateMockRestTool.js +234 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +225 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
- package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
- package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
- package/build/tools/localDevWorkflowFixes.test.js +255 -0
- package/build/tools/one-click/oneClickTool.d.ts +10 -0
- package/build/tools/one-click/oneClickTool.js +177 -21
- package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
- package/build/tools/one-click/oneClickTool.test.js +172 -0
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/submitReportTool.d.ts +14 -14
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +59 -38
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- 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/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +20 -8
- package/build/types/TestTypes.js +19 -5
- 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 +109 -0
- package/build/utils/docker.test.js +1 -1
- package/build/utils/featureFlags.d.ts +20 -1
- package/build/utils/featureFlags.js +26 -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 +82 -0
- package/build/utils/mockCompatibility.test.d.ts +1 -0
- package/build/utils/mockCompatibility.test.js +79 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/workspace/workspace.d.ts +49 -35
- package/build/workspace/workspace.js +10 -5
- package/build/workspace/workspace.test.js +65 -6
- package/package.json +2 -2
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as path from "path";
|
|
5
|
+
import { ProgrammingLanguage, MockProtocol } from "../types/TestTypes.js";
|
|
6
|
+
const TOOL_NAME = "skyramp_enrich_test_with_mocks";
|
|
7
|
+
const enrichSchema = {
|
|
8
|
+
testFile: z
|
|
9
|
+
.string()
|
|
10
|
+
.describe("Absolute path to the generated test file to enrich (e.g., /repo/tests/skyramp/order_happy_path_integration_test.py)"),
|
|
11
|
+
scenarioName: z
|
|
12
|
+
.string()
|
|
13
|
+
.describe("Human-readable scenario name (e.g., order-happy-path, payment-timeout). Used as the SCENARIO_NAME constant."),
|
|
14
|
+
mockFiles: z
|
|
15
|
+
.array(z.string())
|
|
16
|
+
.describe("Absolute paths to the generated mock files (from skyramp_mock_generation) that this test needs. " +
|
|
17
|
+
"The tool will inject import statements and apply_mock() calls for each file. " +
|
|
18
|
+
'Example: ["/repo/tests/skyramp/mocks/validate_POST_mock.py", "/repo/tests/skyramp/mocks/payment_authorizations_POST_mock.py"]'),
|
|
19
|
+
language: z
|
|
20
|
+
.nativeEnum(ProgrammingLanguage)
|
|
21
|
+
.optional()
|
|
22
|
+
.describe("Programming language of the generated test. Optional for backward compatibility; inferred from testFile when omitted. Mock enrichment supports Python, TypeScript, JavaScript, and Java tests."),
|
|
23
|
+
traceComments: z
|
|
24
|
+
.array(z.string())
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional trace comments showing how mock values flow to assertions (e.g., 'pricing-service returns discount=true → creator sets order.discount_applied=true → assert True')"),
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* skyramp_enrich_test_with_mocks
|
|
30
|
+
*
|
|
31
|
+
* Injects mock imports and apply_mock() calls into a generated test file.
|
|
32
|
+
* The mock files are generated by skyramp_mock_generation — this tool wires
|
|
33
|
+
* them into the test so the mocks are applied before the test runs.
|
|
34
|
+
*
|
|
35
|
+
* No data duplication — the test imports from the generated mock files.
|
|
36
|
+
*/
|
|
37
|
+
export function registerEnrichTestWithMocksTool(server) {
|
|
38
|
+
server.registerTool(TOOL_NAME, {
|
|
39
|
+
description: "Enrich a generated INTEGRATION test file by injecting imports from generated mock files. " +
|
|
40
|
+
"Call this after skyramp_mock_generation and skyramp_integration_test_generation. " +
|
|
41
|
+
"The tool adds import statements and apply_mock() calls so the test uses the " +
|
|
42
|
+
"generated mocks directly — no data duplication. " +
|
|
43
|
+
"NOTE: Do NOT call this for contract tests — consumer contract tests already have mocks wired inline by the generation tool.",
|
|
44
|
+
inputSchema: enrichSchema,
|
|
45
|
+
}, async (params) => {
|
|
46
|
+
try {
|
|
47
|
+
const { testFile, scenarioName, mockFiles, traceComments, language } = params;
|
|
48
|
+
if (!fs.existsSync(testFile)) {
|
|
49
|
+
return {
|
|
50
|
+
content: [
|
|
51
|
+
{
|
|
52
|
+
type: "text",
|
|
53
|
+
text: `**Error:** Test file not found: ${testFile}`,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
isError: true,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
// Validate mock files exist
|
|
60
|
+
const missingFiles = mockFiles.filter((f) => !fs.existsSync(f));
|
|
61
|
+
if (missingFiles.length > 0) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: "text",
|
|
66
|
+
text: `**Error:** Mock files not found:\n` +
|
|
67
|
+
missingFiles.map((f) => ` - ${f}`).join("\n") +
|
|
68
|
+
`\n\nGenerate them first with \`skyramp_mock_generation\`.`,
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const resolvedLanguage = resolveMockEnrichmentLanguage(testFile, language);
|
|
75
|
+
const original = fs.readFileSync(testFile, "utf-8");
|
|
76
|
+
const enriched = injectMockImports(original, testFile, scenarioName, mockFiles, traceComments, resolvedLanguage);
|
|
77
|
+
fs.writeFileSync(testFile, enriched);
|
|
78
|
+
logger.info(`Enriched test file with mock imports: ${testFile}`, {
|
|
79
|
+
scenarioName,
|
|
80
|
+
mockFileCount: mockFiles.length,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
content: [
|
|
84
|
+
{
|
|
85
|
+
type: "text",
|
|
86
|
+
text: [
|
|
87
|
+
`**Test enriched with mock imports for scenario "${scenarioName}".**\n`,
|
|
88
|
+
`**File:** ${testFile}`,
|
|
89
|
+
`**Mock files imported:** ${mockFiles.length}`,
|
|
90
|
+
...mockFiles.map((f) => ` • ${path.basename(f)}`),
|
|
91
|
+
`\nThe test now imports and applies mocks from the generated files.`,
|
|
92
|
+
"Before execution, stop real services selected for mocking, then execute the test with `skyramp_execute_test`.",
|
|
93
|
+
].join("\n"),
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
logger.error(`Failed to enrich test: ${error.message}`);
|
|
100
|
+
return {
|
|
101
|
+
content: [
|
|
102
|
+
{
|
|
103
|
+
type: "text",
|
|
104
|
+
text: `**Failed to enrich test:** ${error.message}`,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
isError: true,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
113
|
+
function injectMockImports(original, testFile, scenarioName, mockFiles, traceComments, language) {
|
|
114
|
+
const resolvedLanguage = resolveMockEnrichmentLanguage(testFile, language);
|
|
115
|
+
assertSupportedMockEnrichmentLanguage(testFile, resolvedLanguage);
|
|
116
|
+
const mockEntries = buildMockEntries(testFile, mockFiles, resolvedLanguage);
|
|
117
|
+
const generatedBlock = buildMockImportBlock(testFile, scenarioName, mockFiles, traceComments, resolvedLanguage, mockEntries);
|
|
118
|
+
const withoutExistingBlock = replaceGeneratedMockBlock(original, generatedBlock);
|
|
119
|
+
const withBlock = withoutExistingBlock.replaced
|
|
120
|
+
? withoutExistingBlock.source
|
|
121
|
+
: insertGeneratedMockBlock(withoutExistingBlock.source, generatedBlock, resolvedLanguage);
|
|
122
|
+
switch (resolvedLanguage) {
|
|
123
|
+
case ProgrammingLanguage.PYTHON:
|
|
124
|
+
return wirePythonApplyCalls(withBlock);
|
|
125
|
+
case ProgrammingLanguage.TYPESCRIPT:
|
|
126
|
+
case ProgrammingLanguage.JAVASCRIPT:
|
|
127
|
+
return wireJavascriptApplyCalls(withBlock);
|
|
128
|
+
case ProgrammingLanguage.JAVA:
|
|
129
|
+
return appendJavaMockClasses(wireJavaApplyCalls(withBlock), mockEntries);
|
|
130
|
+
default:
|
|
131
|
+
assertSupportedMockEnrichmentLanguage(testFile, resolvedLanguage);
|
|
132
|
+
return withBlock;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function resolveMockEnrichmentLanguage(testFile, language) {
|
|
136
|
+
if (language)
|
|
137
|
+
return language;
|
|
138
|
+
const extension = path.extname(testFile).toLowerCase();
|
|
139
|
+
switch (extension) {
|
|
140
|
+
case ".py":
|
|
141
|
+
return ProgrammingLanguage.PYTHON;
|
|
142
|
+
case ".ts":
|
|
143
|
+
case ".tsx":
|
|
144
|
+
return ProgrammingLanguage.TYPESCRIPT;
|
|
145
|
+
case ".js":
|
|
146
|
+
case ".jsx":
|
|
147
|
+
return ProgrammingLanguage.JAVASCRIPT;
|
|
148
|
+
case ".java":
|
|
149
|
+
return ProgrammingLanguage.JAVA;
|
|
150
|
+
default:
|
|
151
|
+
throw new Error(`Cannot infer mock enrichment language from test file extension: ${testFile}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function assertSupportedMockEnrichmentLanguage(testFile, language) {
|
|
155
|
+
if (isSupportedMockEnrichmentLanguage(language))
|
|
156
|
+
return;
|
|
157
|
+
throw new Error(unsupportedLanguageMessage(testFile, language));
|
|
158
|
+
}
|
|
159
|
+
function isSupportedMockEnrichmentLanguage(language) {
|
|
160
|
+
return SUPPORTED_MOCK_ENRICHMENT_LANGUAGES.has(language);
|
|
161
|
+
}
|
|
162
|
+
function unsupportedLanguageMessage(testFile, language) {
|
|
163
|
+
return `Mock enrichment supports Python, TypeScript, JavaScript, and Java tests; received language "${language}" for test file "${testFile}".`;
|
|
164
|
+
}
|
|
165
|
+
const SUPPORTED_MOCK_ENRICHMENT_LANGUAGES = new Set([
|
|
166
|
+
ProgrammingLanguage.PYTHON,
|
|
167
|
+
ProgrammingLanguage.TYPESCRIPT,
|
|
168
|
+
ProgrammingLanguage.JAVASCRIPT,
|
|
169
|
+
ProgrammingLanguage.JAVA,
|
|
170
|
+
]);
|
|
171
|
+
const PYTHON_APPLY_FUNCTION = "def apply_all_mocks(client):";
|
|
172
|
+
function buildMockEntries(testFile, mockFiles, language) {
|
|
173
|
+
const testDir = path.dirname(testFile);
|
|
174
|
+
const usedModuleNames = new Set();
|
|
175
|
+
return mockFiles.map((mockFile) => {
|
|
176
|
+
const relativePath = path.relative(testDir, mockFile).replace(/\\/g, "/");
|
|
177
|
+
const extension = path.extname(mockFile);
|
|
178
|
+
const baseName = path.basename(mockFile, extension);
|
|
179
|
+
const moduleName = uniqueIdentifier(baseName, usedModuleNames);
|
|
180
|
+
const className = toPascalCase(baseName);
|
|
181
|
+
const classReference = prepareMockFileForLanguageImport(mockFile, relativePath, language, className);
|
|
182
|
+
const serviceName = baseName.replace(/_mock$/i, "").replace(/_/g, "-");
|
|
183
|
+
const commandLine = extractMockCommandLine(mockFile);
|
|
184
|
+
return {
|
|
185
|
+
serviceName,
|
|
186
|
+
moduleName,
|
|
187
|
+
relativePath,
|
|
188
|
+
importPath: buildImportPath(relativePath, language),
|
|
189
|
+
importLabel: buildImportPath(relativePath, language),
|
|
190
|
+
mockFile,
|
|
191
|
+
className,
|
|
192
|
+
classReference: classReference ?? className,
|
|
193
|
+
targetUrl: extractBaseUrlFromCommandLine(commandLine),
|
|
194
|
+
protocol: extractProtocolFromCommandLine(commandLine, mockFile),
|
|
195
|
+
};
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
function buildImportPath(relativePath, language) {
|
|
199
|
+
const normalized = relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
200
|
+
if (language === ProgrammingLanguage.TYPESCRIPT) {
|
|
201
|
+
return normalized.replace(/\.(ts|tsx|js|jsx)$/i, "");
|
|
202
|
+
}
|
|
203
|
+
if (language === ProgrammingLanguage.JAVASCRIPT) {
|
|
204
|
+
return normalized.replace(/\.(ts|tsx)$/i, ".js");
|
|
205
|
+
}
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
function prepareMockFileForLanguageImport(mockFile, relativePath, language, className) {
|
|
209
|
+
if (language === ProgrammingLanguage.JAVA) {
|
|
210
|
+
return className;
|
|
211
|
+
}
|
|
212
|
+
if (language !== ProgrammingLanguage.TYPESCRIPT &&
|
|
213
|
+
language !== ProgrammingLanguage.JAVASCRIPT) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
const source = fs.readFileSync(mockFile, "utf-8");
|
|
217
|
+
let next = disableNodeMockAutoApply(source);
|
|
218
|
+
if (language === ProgrammingLanguage.TYPESCRIPT) {
|
|
219
|
+
if (!/export\s+function\s+getAllMocks\s*\(/.test(next)) {
|
|
220
|
+
next = next.replace(/function\s+getAllMocks\s*\(/, "export function getAllMocks(");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
else if (!/module\.exports\.getAllMocks\s*=/.test(next)) {
|
|
224
|
+
next = `${next.trimEnd()}\n\nmodule.exports.getAllMocks = getAllMocks;\n`;
|
|
225
|
+
}
|
|
226
|
+
if (next !== source) {
|
|
227
|
+
fs.writeFileSync(mockFile, next);
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
function disableNodeMockAutoApply(source) {
|
|
232
|
+
if (source.includes("skyramp-enrich disabled auto-apply"))
|
|
233
|
+
return source;
|
|
234
|
+
return source.replace(/\nmain\(\)\.catch\(\s*\(?\s*\w+\s*\)?\s*=>\s*\{[\s\S]*?\n\}\);\s*$/, '\n// skyramp-enrich disabled auto-apply: imported integration tests call getAllMocks() directly.\n');
|
|
235
|
+
}
|
|
236
|
+
function replaceGeneratedMockBlock(source, generatedBlock) {
|
|
237
|
+
const existingBlockRange = findGeneratedMockBlockRange(source);
|
|
238
|
+
if (!existingBlockRange)
|
|
239
|
+
return { source, replaced: false };
|
|
240
|
+
const suffix = source.slice(existingBlockRange.end);
|
|
241
|
+
const separator = suffix.length > 0 && !suffix.startsWith("\n") ? "\n" : "";
|
|
242
|
+
return {
|
|
243
|
+
source: source.slice(0, existingBlockRange.start) +
|
|
244
|
+
generatedBlock +
|
|
245
|
+
separator +
|
|
246
|
+
suffix,
|
|
247
|
+
replaced: true,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function insertGeneratedMockBlock(source, generatedBlock, language) {
|
|
251
|
+
if (language === ProgrammingLanguage.JAVA) {
|
|
252
|
+
return insertJavaMockBlock(source, generatedBlock);
|
|
253
|
+
}
|
|
254
|
+
const lines = source.split("\n");
|
|
255
|
+
const insertIdx = findTopLevelBlockInsertionIndex(lines, language);
|
|
256
|
+
lines.splice(insertIdx, 0, "", generatedBlock, "");
|
|
257
|
+
return lines.join("\n");
|
|
258
|
+
}
|
|
259
|
+
function findTopLevelBlockInsertionIndex(lines, language) {
|
|
260
|
+
let insertIdx = 0;
|
|
261
|
+
for (let i = 0; i < lines.length; i++) {
|
|
262
|
+
const line = lines[i].trim();
|
|
263
|
+
const isImportLine = language === ProgrammingLanguage.PYTHON
|
|
264
|
+
? line.startsWith("import ") || line.startsWith("from ") || line.startsWith("# Import")
|
|
265
|
+
: line.startsWith("import ") ||
|
|
266
|
+
line.startsWith("const ") && line.includes("require(") ||
|
|
267
|
+
line.startsWith("// Import");
|
|
268
|
+
if (isImportLine ||
|
|
269
|
+
(line === "" &&
|
|
270
|
+
i > 0 &&
|
|
271
|
+
(lines[i - 1].trim().startsWith("import") ||
|
|
272
|
+
lines[i - 1].trim().startsWith("from") ||
|
|
273
|
+
lines[i - 1].trim().includes("require(")))) {
|
|
274
|
+
insertIdx = i + 1;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
if (insertIdx === 0) {
|
|
278
|
+
const commentPrefix = language === ProgrammingLanguage.PYTHON ? "#" : "//";
|
|
279
|
+
for (let i = 0; i < lines.length; i++) {
|
|
280
|
+
if (!lines[i].startsWith(commentPrefix) && lines[i].trim() !== "") {
|
|
281
|
+
insertIdx = i;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return insertIdx;
|
|
287
|
+
}
|
|
288
|
+
function insertJavaMockBlock(source, generatedBlock) {
|
|
289
|
+
const lines = source.split("\n");
|
|
290
|
+
const classIdx = lines.findIndex((line) => /\bclass\s+\w+/.test(line));
|
|
291
|
+
if (classIdx === -1) {
|
|
292
|
+
throw new Error("Could not find Java test class to insert mock wiring block.");
|
|
293
|
+
}
|
|
294
|
+
let insertIdx = classIdx + 1;
|
|
295
|
+
if (!lines[classIdx].includes("{")) {
|
|
296
|
+
const braceIdx = lines.findIndex((line, idx) => idx > classIdx && line.includes("{"));
|
|
297
|
+
if (braceIdx !== -1)
|
|
298
|
+
insertIdx = braceIdx + 1;
|
|
299
|
+
}
|
|
300
|
+
lines.splice(insertIdx, 0, generatedBlock, "");
|
|
301
|
+
return lines.join("\n");
|
|
302
|
+
}
|
|
303
|
+
function findGeneratedMockBlockRange(source) {
|
|
304
|
+
const startMatch = /(^|\n)([ \t]*(?:#|\/\/)\s*.*Mock Scenario:.*)/.exec(source);
|
|
305
|
+
if (!startMatch)
|
|
306
|
+
return null;
|
|
307
|
+
const start = startMatch.index + startMatch[1].length;
|
|
308
|
+
const afterStart = source.slice(start);
|
|
309
|
+
const endMarkerMatch = /\n[ \t]*(?:#|\/\/)\s*--- End Mock Scenario ---/.exec(afterStart);
|
|
310
|
+
if (endMarkerMatch) {
|
|
311
|
+
const markerEnd = start + endMarkerMatch.index + endMarkerMatch[0].length;
|
|
312
|
+
return { start, end: markerEnd };
|
|
313
|
+
}
|
|
314
|
+
return findLegacyPythonGeneratedMockBlockRange(source, start);
|
|
315
|
+
}
|
|
316
|
+
function findLegacyPythonGeneratedMockBlockRange(source, start) {
|
|
317
|
+
const segment = source.slice(start);
|
|
318
|
+
const lines = segment.split("\n");
|
|
319
|
+
const applyIdx = lines.findIndex((line) => line.trim() === PYTHON_APPLY_FUNCTION);
|
|
320
|
+
if (applyIdx === -1)
|
|
321
|
+
return null;
|
|
322
|
+
let endLine = lines.length;
|
|
323
|
+
for (let i = applyIdx + 1; i < lines.length; i++) {
|
|
324
|
+
const line = lines[i];
|
|
325
|
+
if (line.trim() === "" || /^\s/.test(line))
|
|
326
|
+
continue;
|
|
327
|
+
endLine = i;
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
while (endLine < lines.length) {
|
|
331
|
+
const line = lines[endLine].trim();
|
|
332
|
+
if (line === "" || line.startsWith("# Mock trace:")) {
|
|
333
|
+
endLine += 1;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
const replacementText = lines.slice(0, endLine).join("\n");
|
|
339
|
+
return { start, end: start + replacementText.length };
|
|
340
|
+
}
|
|
341
|
+
function wirePythonApplyCalls(source) {
|
|
342
|
+
const lines = source.split("\n");
|
|
343
|
+
for (let i = 0; i < lines.length; i++) {
|
|
344
|
+
const line = lines[i];
|
|
345
|
+
if (/^def test_\w+\(/.test(line.trim())) {
|
|
346
|
+
const functionEnd = findPythonFunctionEnd(lines, i + 1);
|
|
347
|
+
const applyCallIndex = lines.findIndex((l, idx) => idx >= i &&
|
|
348
|
+
idx < functionEnd &&
|
|
349
|
+
(l.trim() === "apply_all_mocks(client)" ||
|
|
350
|
+
l.trim() === "apply_all_mocks()"));
|
|
351
|
+
if (applyCallIndex !== -1)
|
|
352
|
+
continue;
|
|
353
|
+
const clientEnd = findPythonSkyrampClientConstructionEnd(lines, i + 1, functionEnd);
|
|
354
|
+
if (clientEnd !== -1) {
|
|
355
|
+
lines.splice(clientEnd + 1, 0, " apply_all_mocks(client)", "");
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
function wireJavascriptApplyCalls(source) {
|
|
362
|
+
const lines = source.split("\n");
|
|
363
|
+
const applyCall = "await applyAllMocks(client);";
|
|
364
|
+
for (let i = 0; i < lines.length; i++) {
|
|
365
|
+
if (!/\b(?:const|let|var)\s+client\s*=\s*(?:new\s+)?SkyrampClient\b/.test(lines[i])) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
const statementEnd = findStatementEnd(lines, i);
|
|
369
|
+
const scopeEnd = findJavascriptScopeEnd(lines, statementEnd + 1);
|
|
370
|
+
const existing = lines.findIndex((line, idx) => idx > i && idx < scopeEnd && line.trim() === applyCall);
|
|
371
|
+
if (existing !== -1)
|
|
372
|
+
continue;
|
|
373
|
+
const indent = lines[i].match(/^\s*/)?.[0] ?? "";
|
|
374
|
+
lines.splice(statementEnd + 1, 0, `${indent}${applyCall}`);
|
|
375
|
+
i = statementEnd + 1;
|
|
376
|
+
}
|
|
377
|
+
return lines.join("\n");
|
|
378
|
+
}
|
|
379
|
+
function wireJavaApplyCalls(source) {
|
|
380
|
+
const lines = source.split("\n");
|
|
381
|
+
const applyCall = "applyAllMocks(client);";
|
|
382
|
+
for (let i = 0; i < lines.length; i++) {
|
|
383
|
+
if (!/\b(?:var|SkyrampClient)\s+client\s*=\s*(?:new\s+SkyrampClient\b|SkyrampClient\.builder\b)/.test(lines[i])) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const statementEnd = findStatementEnd(lines, i);
|
|
387
|
+
const methodEnd = findJavaMethodEnd(lines, statementEnd + 1);
|
|
388
|
+
const existing = lines.findIndex((line, idx) => idx > i && idx < methodEnd && line.trim() === applyCall);
|
|
389
|
+
if (existing !== -1)
|
|
390
|
+
continue;
|
|
391
|
+
const indent = lines[i].match(/^\s*/)?.[0] ?? "";
|
|
392
|
+
lines.splice(statementEnd + 1, 0, `${indent}${applyCall}`);
|
|
393
|
+
i = statementEnd + 1;
|
|
394
|
+
}
|
|
395
|
+
return lines.join("\n");
|
|
396
|
+
}
|
|
397
|
+
function appendJavaMockClasses(source, mockEntries) {
|
|
398
|
+
const withoutExisting = source.replace(/\n\n \/\/ --- Mock Classes ---[\s\S]*?\n \/\/ --- End Mock Classes ---\s*$/m, "");
|
|
399
|
+
const classes = mockEntries.map((entry) => {
|
|
400
|
+
const mockSource = normalizeJavaMockSource(fs.readFileSync(entry.mockFile, "utf-8"), entry.className);
|
|
401
|
+
return ` // --- Mock Class: ${entry.className} ---\n${mockSource}`;
|
|
402
|
+
});
|
|
403
|
+
if (classes.length === 0)
|
|
404
|
+
return withoutExisting;
|
|
405
|
+
return `${withoutExisting.trimEnd()}\n\n // --- Mock Classes ---\n${classes.join("\n\n")}\n // --- End Mock Classes ---\n`;
|
|
406
|
+
}
|
|
407
|
+
function normalizeJavaMockSource(source, className) {
|
|
408
|
+
return source
|
|
409
|
+
.replace(/^\s*package\s+[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*;\s*/m, "")
|
|
410
|
+
.replace(/^\s*import(?:\s+static)?\s+[^;]+;\s*$/gm, "")
|
|
411
|
+
.replace(/\.(protobufExtraFiles|protobufExtraImportPath)\(\[([^\]]*)\]\)/g, (_match, method, rawValues) => {
|
|
412
|
+
const values = rawValues
|
|
413
|
+
.split(",")
|
|
414
|
+
.map((value) => value.trim())
|
|
415
|
+
.filter(Boolean)
|
|
416
|
+
.map((value) => JSON.stringify(value));
|
|
417
|
+
return `.${method}(Arrays.asList(${values.join(", ")}))`;
|
|
418
|
+
})
|
|
419
|
+
.replace(/\n\s*\/\/ Apply all mocks to the system[\s\S]*?\n\s*public static void main\(String\[\] args\) throws Exception \{[\s\S]*?\n\s*\}\s*(?=\n\s*\}\s*$)/m, "")
|
|
420
|
+
.replace(new RegExp(`public\\s+final\\s+class\\s+${className}\\b`), `final class ${className}`)
|
|
421
|
+
.replace(new RegExp(`public\\s+class\\s+${className}\\b`), `class ${className}`)
|
|
422
|
+
.trim();
|
|
423
|
+
}
|
|
424
|
+
function findPythonFunctionEnd(lines, bodyStart) {
|
|
425
|
+
for (let i = bodyStart; i < lines.length; i++) {
|
|
426
|
+
if (/^def\s+\w+\(/.test(lines[i]) || /^class\s+\w+/.test(lines[i])) {
|
|
427
|
+
return i;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return lines.length;
|
|
431
|
+
}
|
|
432
|
+
function findPythonSkyrampClientConstructionEnd(lines, start, end) {
|
|
433
|
+
for (let i = start; i < end; i++) {
|
|
434
|
+
if (!/^\s*client\s*=\s*skyramp\.Client\s*\(/.test(lines[i])) {
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
let depth = 0;
|
|
438
|
+
for (let j = i; j < end; j++) {
|
|
439
|
+
depth += countChar(lines[j], "(") - countChar(lines[j], ")");
|
|
440
|
+
if (depth <= 0) {
|
|
441
|
+
return j;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
return i;
|
|
445
|
+
}
|
|
446
|
+
return -1;
|
|
447
|
+
}
|
|
448
|
+
function findStatementEnd(lines, start) {
|
|
449
|
+
let parenDepth = 0;
|
|
450
|
+
let braceDepth = 0;
|
|
451
|
+
let bracketDepth = 0;
|
|
452
|
+
for (let i = start; i < lines.length; i++) {
|
|
453
|
+
const line = lines[i];
|
|
454
|
+
parenDepth += countChar(line, "(") - countChar(line, ")");
|
|
455
|
+
braceDepth += countChar(line, "{") - countChar(line, "}");
|
|
456
|
+
bracketDepth += countChar(line, "[") - countChar(line, "]");
|
|
457
|
+
if (parenDepth <= 0 &&
|
|
458
|
+
braceDepth <= 0 &&
|
|
459
|
+
bracketDepth <= 0 &&
|
|
460
|
+
line.trim().endsWith(";")) {
|
|
461
|
+
return i;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return start;
|
|
465
|
+
}
|
|
466
|
+
function findJavascriptScopeEnd(lines, start) {
|
|
467
|
+
let depth = 1;
|
|
468
|
+
for (let i = start; i < lines.length; i++) {
|
|
469
|
+
depth += countChar(lines[i], "{") - countChar(lines[i], "}");
|
|
470
|
+
if (depth <= 0)
|
|
471
|
+
return i;
|
|
472
|
+
}
|
|
473
|
+
return lines.length;
|
|
474
|
+
}
|
|
475
|
+
function findJavaMethodEnd(lines, start) {
|
|
476
|
+
let depth = 1;
|
|
477
|
+
for (let i = start; i < lines.length; i++) {
|
|
478
|
+
depth += countChar(lines[i], "{") - countChar(lines[i], "}");
|
|
479
|
+
if (depth <= 0)
|
|
480
|
+
return i;
|
|
481
|
+
}
|
|
482
|
+
return lines.length;
|
|
483
|
+
}
|
|
484
|
+
function countChar(value, char) {
|
|
485
|
+
return [...value].filter((current) => current === char).length;
|
|
486
|
+
}
|
|
487
|
+
function buildMockImportBlock(testFile, scenarioName, mockFiles, traceComments, language, mockEntries) {
|
|
488
|
+
const resolvedLanguage = resolveMockEnrichmentLanguage(testFile, language);
|
|
489
|
+
assertSupportedMockEnrichmentLanguage(testFile, resolvedLanguage);
|
|
490
|
+
const entries = mockEntries ?? buildMockEntries(testFile, mockFiles, resolvedLanguage);
|
|
491
|
+
switch (resolvedLanguage) {
|
|
492
|
+
case ProgrammingLanguage.PYTHON:
|
|
493
|
+
return buildPythonMockImportBlock(scenarioName, entries, traceComments);
|
|
494
|
+
case ProgrammingLanguage.TYPESCRIPT:
|
|
495
|
+
return buildNodeMockImportBlock(scenarioName, entries, traceComments, true);
|
|
496
|
+
case ProgrammingLanguage.JAVASCRIPT:
|
|
497
|
+
return buildNodeMockImportBlock(scenarioName, entries, traceComments, false);
|
|
498
|
+
case ProgrammingLanguage.JAVA:
|
|
499
|
+
return buildJavaMockImportBlock(scenarioName, entries, traceComments);
|
|
500
|
+
default:
|
|
501
|
+
assertSupportedMockEnrichmentLanguage(testFile, resolvedLanguage);
|
|
502
|
+
return "";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
function buildPythonMockImportBlock(scenarioName, mockEntries, traceComments) {
|
|
506
|
+
const lines = [];
|
|
507
|
+
lines.push(`# --- Mock Scenario: ${scenarioName} ---`);
|
|
508
|
+
lines.push(`SCENARIO_NAME = ${JSON.stringify(scenarioName)}`);
|
|
509
|
+
lines.push(``);
|
|
510
|
+
lines.push(`import importlib.util`);
|
|
511
|
+
lines.push(`from pathlib import Path`);
|
|
512
|
+
lines.push(``);
|
|
513
|
+
lines.push(`def _load_mock_factory(relative_path, module_name):`);
|
|
514
|
+
lines.push(` mock_path = Path(__file__).parent / relative_path`);
|
|
515
|
+
lines.push(` spec = importlib.util.spec_from_file_location(module_name, mock_path)`);
|
|
516
|
+
lines.push(` if spec is None or spec.loader is None:`);
|
|
517
|
+
lines.push(` raise ImportError(f"Could not load mock module: {mock_path}")`);
|
|
518
|
+
lines.push(` module = importlib.util.module_from_spec(spec)`);
|
|
519
|
+
lines.push(` spec.loader.exec_module(module)`);
|
|
520
|
+
lines.push(` return module.get_all_mocks`);
|
|
521
|
+
lines.push(``);
|
|
522
|
+
lines.push(`MOCK_SERVICES = {`);
|
|
523
|
+
for (const entry of mockEntries) {
|
|
524
|
+
lines.push(` ${JSON.stringify(entry.serviceName)}: {"factory": _load_mock_factory(${JSON.stringify(entry.relativePath)}, ${JSON.stringify(entry.moduleName)}), "protocol": ${JSON.stringify(entry.protocol)}},`);
|
|
525
|
+
}
|
|
526
|
+
lines.push(`}`);
|
|
527
|
+
appendPythonTargetUrls(lines, mockEntries);
|
|
528
|
+
lines.push(``);
|
|
529
|
+
lines.push(PYTHON_APPLY_FUNCTION);
|
|
530
|
+
lines.push(` """Apply all generated mocks together after protocol-specific normalization."""`);
|
|
531
|
+
lines.push(` client.remove_all_mocks()`);
|
|
532
|
+
lines.push(``);
|
|
533
|
+
lines.push(` all_mocks = []`);
|
|
534
|
+
lines.push(` for service_name, mock_config in MOCK_SERVICES.items():`);
|
|
535
|
+
lines.push(` get_mocks_fn = mock_config["factory"]`);
|
|
536
|
+
lines.push(` mocks = get_mocks_fn()`);
|
|
537
|
+
lines.push(` target_url = MOCK_TARGET_URLS.get(service_name)`);
|
|
538
|
+
lines.push(` if target_url:`);
|
|
539
|
+
lines.push(` for mock in mocks:`);
|
|
540
|
+
lines.push(` mock.url = target_url`);
|
|
541
|
+
lines.push(` all_mocks.extend(mocks)`);
|
|
542
|
+
lines.push(``);
|
|
543
|
+
lines.push(` if all_mocks:`);
|
|
544
|
+
lines.push(` client.apply_mock(all_mocks)`);
|
|
545
|
+
appendTraceComments(lines, traceComments, "#");
|
|
546
|
+
lines.push(`# --- End Mock Scenario ---`);
|
|
547
|
+
return lines.join("\n");
|
|
548
|
+
}
|
|
549
|
+
function appendPythonTargetUrls(lines, mockEntries) {
|
|
550
|
+
lines.push(``);
|
|
551
|
+
lines.push(`MOCK_TARGET_URLS = {`);
|
|
552
|
+
for (const entry of mockEntries) {
|
|
553
|
+
if (entry.targetUrl) {
|
|
554
|
+
lines.push(` ${JSON.stringify(entry.serviceName)}: ${JSON.stringify(entry.targetUrl)},`);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
lines.push(`}`);
|
|
558
|
+
}
|
|
559
|
+
function buildNodeMockImportBlock(scenarioName, mockEntries, traceComments, typed) {
|
|
560
|
+
const lines = [];
|
|
561
|
+
const anyType = typed ? ": any" : "";
|
|
562
|
+
const anyArrayType = typed ? ": any[]" : "";
|
|
563
|
+
const stringRecordType = typed ? ": Record<string, string>" : "";
|
|
564
|
+
lines.push(`// --- Mock Scenario: ${scenarioName} ---`);
|
|
565
|
+
if (typed) {
|
|
566
|
+
for (const entry of mockEntries) {
|
|
567
|
+
lines.push(`import * as ${entry.moduleName} from ${JSON.stringify(entry.importPath)};`);
|
|
568
|
+
}
|
|
569
|
+
lines.push(``);
|
|
570
|
+
}
|
|
571
|
+
lines.push(`const SCENARIO_NAME = ${JSON.stringify(scenarioName)};`);
|
|
572
|
+
lines.push(``);
|
|
573
|
+
lines.push(`const MOCK_SERVICES = [`);
|
|
574
|
+
for (const entry of mockEntries) {
|
|
575
|
+
const moduleRef = typed
|
|
576
|
+
? `module: ${entry.moduleName}`
|
|
577
|
+
: `modulePath: ${JSON.stringify(entry.importPath)}`;
|
|
578
|
+
lines.push(` { name: ${JSON.stringify(entry.serviceName)}, ${moduleRef}, importLabel: ${JSON.stringify(entry.importLabel)}, protocol: ${JSON.stringify(entry.protocol)} },`);
|
|
579
|
+
}
|
|
580
|
+
lines.push(`];`);
|
|
581
|
+
lines.push(``);
|
|
582
|
+
lines.push(`const MOCK_TARGET_URLS${stringRecordType} = {`);
|
|
583
|
+
for (const entry of mockEntries) {
|
|
584
|
+
if (entry.targetUrl) {
|
|
585
|
+
lines.push(` ${JSON.stringify(entry.serviceName)}: ${JSON.stringify(entry.targetUrl)},`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
lines.push(`};`);
|
|
589
|
+
lines.push(``);
|
|
590
|
+
lines.push(`async function applyAllMocks(client${anyType}) {`);
|
|
591
|
+
lines.push(` await client.removeAllMocks();`);
|
|
592
|
+
lines.push(``);
|
|
593
|
+
lines.push(` const allMocks${anyArrayType} = [];`);
|
|
594
|
+
lines.push(` for (const service of MOCK_SERVICES) {`);
|
|
595
|
+
if (typed) {
|
|
596
|
+
lines.push(` const getAllMocks = service.module.getAllMocks ?? service.module.default?.getAllMocks;`);
|
|
597
|
+
}
|
|
598
|
+
else {
|
|
599
|
+
lines.push(` const mockModule = await import(service.modulePath);`);
|
|
600
|
+
lines.push(` const getAllMocks = mockModule.getAllMocks ?? mockModule.default?.getAllMocks;`);
|
|
601
|
+
}
|
|
602
|
+
lines.push(` if (!getAllMocks) {`);
|
|
603
|
+
lines.push(` throw new Error(\`Mock module \${service.importLabel ?? service.name} does not export getAllMocks()\`);`);
|
|
604
|
+
lines.push(` }`);
|
|
605
|
+
lines.push(` const mocks = await getAllMocks();`);
|
|
606
|
+
lines.push(` const targetUrl = MOCK_TARGET_URLS[service.name];`);
|
|
607
|
+
lines.push(` if (targetUrl) {`);
|
|
608
|
+
lines.push(` for (const mock of mocks) {`);
|
|
609
|
+
lines.push(` mock.url = targetUrl;`);
|
|
610
|
+
lines.push(` }`);
|
|
611
|
+
lines.push(` }`);
|
|
612
|
+
lines.push(` allMocks.push(...mocks);`);
|
|
613
|
+
lines.push(` }`);
|
|
614
|
+
lines.push(``);
|
|
615
|
+
lines.push(` if (allMocks.length > 0) {`);
|
|
616
|
+
lines.push(` await client.applyMock(allMocks);`);
|
|
617
|
+
lines.push(` }`);
|
|
618
|
+
lines.push(`}`);
|
|
619
|
+
appendTraceComments(lines, traceComments, "//");
|
|
620
|
+
lines.push(`// --- End Mock Scenario ---`);
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
}
|
|
623
|
+
function buildJavaMockImportBlock(scenarioName, mockEntries, traceComments) {
|
|
624
|
+
const lines = [];
|
|
625
|
+
lines.push(` // --- Mock Scenario: ${scenarioName} ---`);
|
|
626
|
+
lines.push(` private static final String SCENARIO_NAME = ${JSON.stringify(scenarioName)};`);
|
|
627
|
+
lines.push(` private static final List<String> MOCK_SERVICES = Arrays.asList(`);
|
|
628
|
+
mockEntries.forEach((entry, index) => {
|
|
629
|
+
const suffix = index === mockEntries.length - 1 ? "" : ",";
|
|
630
|
+
lines.push(` ${JSON.stringify(entry.serviceName)}${suffix}`);
|
|
631
|
+
});
|
|
632
|
+
lines.push(` );`);
|
|
633
|
+
lines.push(``);
|
|
634
|
+
lines.push(` private void applyAllMocks(SkyrampClient client) throws SkyrampException {`);
|
|
635
|
+
lines.push(` client.removeAllMocks();`);
|
|
636
|
+
lines.push(``);
|
|
637
|
+
lines.push(` List<MockV2> allMocks = new ArrayList<>();`);
|
|
638
|
+
mockEntries.forEach((entry, index) => {
|
|
639
|
+
lines.push(` List<MockV2> mocks${index} = new ${entry.classReference}().getAllMocks();`);
|
|
640
|
+
if (entry.targetUrl) {
|
|
641
|
+
lines.push(` for (MockV2 mock : mocks${index}) {`);
|
|
642
|
+
lines.push(` mock.setUrl(${JSON.stringify(entry.targetUrl)});`);
|
|
643
|
+
lines.push(` }`);
|
|
644
|
+
}
|
|
645
|
+
lines.push(` allMocks.addAll(mocks${index});`);
|
|
646
|
+
});
|
|
647
|
+
lines.push(``);
|
|
648
|
+
lines.push(` if (!allMocks.isEmpty()) {`);
|
|
649
|
+
lines.push(` client.applyMock(allMocks);`);
|
|
650
|
+
lines.push(` }`);
|
|
651
|
+
lines.push(` }`);
|
|
652
|
+
appendTraceComments(lines, traceComments, " //");
|
|
653
|
+
lines.push(` // --- End Mock Scenario ---`);
|
|
654
|
+
return lines.join("\n");
|
|
655
|
+
}
|
|
656
|
+
function appendTraceComments(lines, traceComments, prefix) {
|
|
657
|
+
if (!traceComments?.length)
|
|
658
|
+
return;
|
|
659
|
+
lines.push("");
|
|
660
|
+
for (const comment of traceComments) {
|
|
661
|
+
lines.push(`${prefix} Mock trace: ${comment}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
function extractMockCommandLine(mockFile) {
|
|
665
|
+
if (!fs.existsSync(mockFile))
|
|
666
|
+
return undefined;
|
|
667
|
+
return fs
|
|
668
|
+
.readFileSync(mockFile, "utf-8")
|
|
669
|
+
.split("\n")
|
|
670
|
+
.map((line) => line.replace(/^\s*(?:#|\/)\/?\s?/, "").trim())
|
|
671
|
+
.find((line) => line.startsWith("Command: skyramp generate mock "));
|
|
672
|
+
}
|
|
673
|
+
function extractProtocolFromCommandLine(commandLine, mockFile) {
|
|
674
|
+
const protocolMatch = commandLine?.match(/skyramp generate mock (rest|grpc|kafka)\b/);
|
|
675
|
+
if (protocolMatch?.[1])
|
|
676
|
+
return protocolMatch[1];
|
|
677
|
+
const baseName = path.basename(mockFile).toLowerCase();
|
|
678
|
+
if (baseName.includes("kafka"))
|
|
679
|
+
return MockProtocol.Kafka;
|
|
680
|
+
if (baseName.includes("grpc"))
|
|
681
|
+
return MockProtocol.Grpc;
|
|
682
|
+
return MockProtocol.Rest;
|
|
683
|
+
}
|
|
684
|
+
function extractBaseUrlFromCommandLine(commandLine) {
|
|
685
|
+
if (!commandLine)
|
|
686
|
+
return undefined;
|
|
687
|
+
const commandMatch = commandLine.match(/skyramp generate mock (?:rest|grpc|kafka)\s+([^\s\\]+)/);
|
|
688
|
+
if (!commandMatch || commandMatch[1].startsWith("--"))
|
|
689
|
+
return undefined;
|
|
690
|
+
return normalizeMockCommandTarget(commandMatch[1]);
|
|
691
|
+
}
|
|
692
|
+
function uniqueIdentifier(raw, used) {
|
|
693
|
+
const base = raw.replace(/\W|^(?=\d)/g, "_");
|
|
694
|
+
let candidate = base;
|
|
695
|
+
let suffix = 2;
|
|
696
|
+
while (used.has(candidate)) {
|
|
697
|
+
candidate = `${base}_${suffix}`;
|
|
698
|
+
suffix += 1;
|
|
699
|
+
}
|
|
700
|
+
used.add(candidate);
|
|
701
|
+
return candidate;
|
|
702
|
+
}
|
|
703
|
+
function toPascalCase(raw) {
|
|
704
|
+
return raw
|
|
705
|
+
.replace(/\.[^.]+$/, "")
|
|
706
|
+
.split(/[^a-zA-Z0-9]+/)
|
|
707
|
+
.filter(Boolean)
|
|
708
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
709
|
+
.join("");
|
|
710
|
+
}
|
|
711
|
+
function normalizeMockCommandTarget(target) {
|
|
712
|
+
if (!target)
|
|
713
|
+
return undefined;
|
|
714
|
+
try {
|
|
715
|
+
const endpointUrl = new URL(target);
|
|
716
|
+
if (endpointUrl.origin !== "null")
|
|
717
|
+
return endpointUrl.origin;
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
// Fall through to host:port handling below.
|
|
721
|
+
}
|
|
722
|
+
// gRPC and Kafka mock targets are usually service host:port values without a URL scheme.
|
|
723
|
+
return /^[A-Za-z0-9_.-]+:\d+$/.test(target) ? target : undefined;
|
|
724
|
+
}
|
|
725
|
+
// Exported for testing
|
|
726
|
+
export { injectMockImports, buildMockImportBlock, isSupportedMockEnrichmentLanguage };
|