@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
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { LOCAL_DEV_SKILL_CONTENT } from "../../commands/localDevTestChangesCommand.js";
|
|
2
5
|
import { lookupCommand, getAllCommands } from "../../commands/commandLibrary.js";
|
|
3
6
|
import { logger } from "../../utils/logger.js";
|
|
4
7
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
8
|
const TOOL_NAME = "skyramp_one_click_tool";
|
|
9
|
+
export var CallingAgentIde;
|
|
10
|
+
(function (CallingAgentIde) {
|
|
11
|
+
CallingAgentIde["Cursor"] = "cursor";
|
|
12
|
+
CallingAgentIde["Claude"] = "claude";
|
|
13
|
+
})(CallingAgentIde || (CallingAgentIde = {}));
|
|
6
14
|
/**
|
|
7
15
|
* Generates the `workflow` parameter description from the live command library.
|
|
8
16
|
* The LLM reads this when filling the tool call, matches the user's intent against
|
|
@@ -10,6 +18,91 @@ const TOOL_NAME = "skyramp_one_click_tool";
|
|
|
10
18
|
* matching workflow ID. No hardcoded enum — adding a new workflow to the library
|
|
11
19
|
* automatically updates this description.
|
|
12
20
|
*/
|
|
21
|
+
async function pathExists(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.access(filePath);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function assertRepositoryPathIsSafe(repositoryPath) {
|
|
31
|
+
const stat = await fs.stat(repositoryPath).catch(() => undefined);
|
|
32
|
+
if (!stat?.isDirectory()) {
|
|
33
|
+
throw new Error(`repositoryPath must be an existing directory: ${repositoryPath}`);
|
|
34
|
+
}
|
|
35
|
+
if (!(await pathExists(path.join(repositoryPath, ".git")))) {
|
|
36
|
+
throw new Error(`repositoryPath must point to a git repository: ${repositoryPath}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function normalizeCallingAgentIde(callingAgent) {
|
|
40
|
+
switch (callingAgent?.trim().toLowerCase()) {
|
|
41
|
+
case CallingAgentIde.Cursor:
|
|
42
|
+
return CallingAgentIde.Cursor;
|
|
43
|
+
case CallingAgentIde.Claude:
|
|
44
|
+
return CallingAgentIde.Claude;
|
|
45
|
+
default:
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
function resolveLocalDevSkillTarget(repositoryPath, callingAgent) {
|
|
50
|
+
switch (callingAgent) {
|
|
51
|
+
case CallingAgentIde.Cursor:
|
|
52
|
+
return {
|
|
53
|
+
agent: CallingAgentIde.Cursor,
|
|
54
|
+
skillPath: path.join(repositoryPath, ".cursor", "skills", "local-dev", "SKILL.md"),
|
|
55
|
+
};
|
|
56
|
+
case CallingAgentIde.Claude:
|
|
57
|
+
return {
|
|
58
|
+
agent: CallingAgentIde.Claude,
|
|
59
|
+
skillPath: path.join(repositoryPath, ".claude", "skills", "local-dev", "SKILL.md"),
|
|
60
|
+
};
|
|
61
|
+
default:
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function ensureLocalDevSkillFile(repositoryPath, callingAgent) {
|
|
66
|
+
await assertRepositoryPathIsSafe(repositoryPath);
|
|
67
|
+
const target = resolveLocalDevSkillTarget(repositoryPath, callingAgent);
|
|
68
|
+
if (!target)
|
|
69
|
+
return {};
|
|
70
|
+
if (await pathExists(target.skillPath))
|
|
71
|
+
return { target };
|
|
72
|
+
await fs.mkdir(path.dirname(target.skillPath), { recursive: true });
|
|
73
|
+
await fs.writeFile(target.skillPath, LOCAL_DEV_SKILL_CONTENT, "utf8");
|
|
74
|
+
return { target, installed: target.skillPath };
|
|
75
|
+
}
|
|
76
|
+
async function buildLocalDevBootstrapStatus(params) {
|
|
77
|
+
if (params.workflow !== "local_dev_test_changes")
|
|
78
|
+
return "";
|
|
79
|
+
const skillResult = await ensureLocalDevSkillFile(params.repositoryPath, params.callingAgent);
|
|
80
|
+
const workspacePath = path.join(params.repositoryPath, ".skyramp", "workspace.yml");
|
|
81
|
+
const workspaceExists = await pathExists(workspacePath);
|
|
82
|
+
const hasUnsupportedCallingAgent = params.rawCallingAgent !== undefined && params.callingAgent === undefined;
|
|
83
|
+
const skillStatus = skillResult.installed
|
|
84
|
+
? `Installed local-dev skill file for ${skillResult.target?.agent}:
|
|
85
|
+
- ${skillResult.installed}`
|
|
86
|
+
: skillResult.target
|
|
87
|
+
? `Local-dev skill file already installed for ${skillResult.target.agent}: ${skillResult.target.skillPath}`
|
|
88
|
+
: hasUnsupportedCallingAgent
|
|
89
|
+
? "Local-dev skill file not installed: this agent does not support repo-local skills, so skill creation was skipped."
|
|
90
|
+
: "Local-dev skill file not installed: pass `callingAgent` as `cursor` or `claude` to install a repo-local skill. Agents without repo-local skill support can ignore this step.";
|
|
91
|
+
return `
|
|
92
|
+
## Bootstrap Status
|
|
93
|
+
|
|
94
|
+
${skillStatus}
|
|
95
|
+
|
|
96
|
+
Base branch: ${params.baseBranch ?? "main"}
|
|
97
|
+
Services to mock: ${params.servicesToMock ?? "not specified"}
|
|
98
|
+
Real services: ${params.realServices ?? "not specified"}
|
|
99
|
+
Mock protocols: ${params.mockProtocols ?? "not specified"}
|
|
100
|
+
Calling agent: ${params.rawCallingAgent ?? params.callingAgent ?? "not specified"}
|
|
101
|
+
|
|
102
|
+
Workspace config: ${workspaceExists ? `found at ${workspacePath}` : `missing at ${workspacePath}`}
|
|
103
|
+
|
|
104
|
+
`;
|
|
105
|
+
}
|
|
13
106
|
function buildWorkflowParamDescription() {
|
|
14
107
|
const commands = getAllCommands();
|
|
15
108
|
const sections = commands.map((cmd) => {
|
|
@@ -29,32 +122,86 @@ function buildWorkflowParamDescription() {
|
|
|
29
122
|
return (`The workflow to execute. Match the user's intent against the available workflows below and pass the matching workflow ID.\n\n` +
|
|
30
123
|
`Available workflows:\n${sections.join("\n\n")}`);
|
|
31
124
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
125
|
+
function isLocalDevWorkflowAvailable() {
|
|
126
|
+
return getAllCommands().some((cmd) => cmd.id === "local_dev_test_changes");
|
|
127
|
+
}
|
|
128
|
+
function buildToolDescription() {
|
|
129
|
+
const localDevInstructions = isLocalDevWorkflowAvailable()
|
|
130
|
+
? `- When the user asks to run Skyramp local-dev, test local changes, validate a feature branch, test a branch against a base/prod/main branch, or mock downstream services for local testing: MUST call this tool with workflow \`local_dev_test_changes\`, \`callingAgent\` set to the current MCP client (\`cursor\` or \`claude\` for skill installation; VS Code does not support repo-local Agent Skills), \`repositoryPath\`, and \`baseBranch\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.\n`
|
|
131
|
+
: "";
|
|
132
|
+
return `Execute predefined, multi-step testing workflows with a single tool call.
|
|
133
|
+
|
|
134
|
+
**Purpose**: The One-Click Commands tool provides a mechanism to define and execute complex, multi-step testing workflows by orchestrating multiple MCP tools in sequence. Commands enable users to accomplish high-level testing goals (e.g., "comprehensively test this endpoint") through a single invocation, without requiring new MCP tool implementations for each workflow.
|
|
135
|
+
|
|
136
|
+
**When to use**:
|
|
137
|
+
${localDevInstructions}- When the user names a specific endpoint and explicitly asks for comprehensive, thorough, or deep testing (e.g. "comprehensively test this endpoint", "thoroughly test the products endpoint"): MUST call this tool with workflow \`test_given_endpoint_comprehensively\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.
|
|
138
|
+
- When the user wants to scan the entire repo (no specific endpoint, no PR diff) to get ranked recommendations, generate top N types, and execute: MUST call this tool with workflow \`full_repo_scan_recommend_generate_and_execute_top_n_tests\`.
|
|
139
|
+
- Do NOT invoke these workflows for simple single-tool requests such as "generate a smoke test" or "run this test" — call the relevant tool directly instead.
|
|
140
|
+
|
|
141
|
+
**How it works**: Pass the \`workflow\` ID that matches the user's intent (see the \`workflow\` parameter description for available workflows and their trigger phrases). The tool returns a **workflow specification** (step-by-step instructions). You must then **execute that workflow** by calling the listed MCP tools in order, passing outputs (e.g. state file paths) from one step as inputs to the next. The tool does not execute other tools itself; it provides the declarative workflow for you to run.
|
|
142
|
+
|
|
143
|
+
**Design**:
|
|
144
|
+
- **MCP-only**: Workflows orchestrate only MCP tools; no CLI execution.
|
|
145
|
+
- **Declarative**: Workflows are sequences of tool calls with dependencies.
|
|
146
|
+
- **State-efficient**: State files are created only by recommendation/map tools (in system temp, e.g. /tmp); do not create any state files in the user repository.
|
|
147
|
+
- **Composable**: Complex workflows are built from existing Skyramp tools.`;
|
|
148
|
+
}
|
|
149
|
+
function buildOneClickSchema() {
|
|
150
|
+
const schema = {
|
|
151
|
+
workflow: z.string().describe(buildWorkflowParamDescription()),
|
|
152
|
+
repositoryPath: z
|
|
153
|
+
.string()
|
|
154
|
+
.describe("Absolute path to the repository (e.g., /Users/dev/my-repo). Required for all commands."),
|
|
155
|
+
endpointURL: z
|
|
156
|
+
.string()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("Full URL of the endpoint to test. Used by test_given_endpoint_comprehensively and full_repo_scan_recommend_generate_and_execute_top_n_tests. If omitted, use api.baseUrl + path from workspace or recommendation."),
|
|
159
|
+
apiSchema: z
|
|
160
|
+
.string()
|
|
161
|
+
.optional()
|
|
162
|
+
.describe("Path or URL to OpenAPI/Swagger schema. If omitted, use schema from workspace or analysis."),
|
|
163
|
+
topN: z
|
|
164
|
+
.number()
|
|
165
|
+
.optional()
|
|
166
|
+
.describe("Number of top recommended test types to generate (e.g. 3–5). Default is 3. Used by full_repo_scan_recommend_generate_and_execute_top_n_tests."),
|
|
167
|
+
token: z
|
|
168
|
+
.string()
|
|
169
|
+
.optional()
|
|
170
|
+
.describe("Authentication/session token for test execution. Always pass this to skyramp_execute_test to avoid auth errors; use empty string '' if no auth required."),
|
|
171
|
+
};
|
|
172
|
+
if (!isLocalDevWorkflowAvailable()) {
|
|
173
|
+
return z.object(schema);
|
|
174
|
+
}
|
|
175
|
+
return z.object({
|
|
176
|
+
...schema,
|
|
177
|
+
baseBranch: z
|
|
178
|
+
.string()
|
|
179
|
+
.optional()
|
|
180
|
+
.default("main")
|
|
181
|
+
.describe("Base branch, commit SHA, or tag to diff against. Used by local_dev_test_changes workflow. Default: 'main'. If the user names a branch such as 'prod branch', pass that exact branch only when it exists or ask for clarification; do not silently substitute main."),
|
|
182
|
+
servicesToMock: z
|
|
183
|
+
.string()
|
|
184
|
+
.optional()
|
|
185
|
+
.describe("Comma-separated downstream service names the user explicitly wants mocked in the local_dev_test_changes workflow. When the user says 'mock all', 'mock everything', 'mock all downstream', or 'mock all except X', pass the sentinel value \"all\" (not an empty string and not omitted). Omit only when the user did not express any intent to mock services."),
|
|
186
|
+
realServices: z
|
|
187
|
+
.string()
|
|
188
|
+
.optional()
|
|
189
|
+
.describe("Comma-separated service names that should receive real traffic in the local_dev_test_changes workflow, such as infrastructure, the service under test, or first-party local dependencies."),
|
|
190
|
+
mockProtocols: z
|
|
191
|
+
.string()
|
|
192
|
+
.optional()
|
|
193
|
+
.describe("Comma-separated protocols the user explicitly wants mocked in local_dev_test_changes, such as 'rest', 'grpc', or 'kafka'. Omit to use the workflow default of mocking only true third-party downstream services."),
|
|
194
|
+
callingAgent: z
|
|
195
|
+
.string()
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Calling MCP client for local_dev_test_changes. Pass 'cursor' or 'claude' to install only that agent's local-dev skill file under .cursor or .claude. Pass 'vscode' if calling from VS Code; skill creation will be skipped."),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
54
200
|
/**
|
|
55
201
|
* Build workflow instruction text from any command definition and params.
|
|
202
|
+
* Optional `stepsPreamble` is inserted between the intro header and Step 0.
|
|
56
203
|
*/
|
|
57
|
-
function buildWorkflowFromCommand(cmd, params) {
|
|
204
|
+
function buildWorkflowFromCommand(cmd, params, stepsPreamble = "") {
|
|
58
205
|
const getParamDisplay = (key) => {
|
|
59
206
|
const v = params[key];
|
|
60
207
|
if (v === undefined || v === null)
|
|
@@ -108,6 +255,7 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
|
|
|
108
255
|
|
|
109
256
|
`;
|
|
110
257
|
return (intro +
|
|
258
|
+
stepsPreamble +
|
|
111
259
|
stepLines.join("\n\n---\n\n") +
|
|
112
260
|
`
|
|
113
261
|
|
|
@@ -122,23 +270,9 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
|
|
|
122
270
|
`);
|
|
123
271
|
}
|
|
124
272
|
export function registerOneClickTool(server) {
|
|
273
|
+
const oneClickSchema = buildOneClickSchema();
|
|
125
274
|
server.registerTool(TOOL_NAME, {
|
|
126
|
-
description:
|
|
127
|
-
|
|
128
|
-
**Purpose**: The One-Click Commands tool provides a mechanism to define and execute complex, multi-step testing workflows by orchestrating multiple MCP tools in sequence. Commands enable users to accomplish high-level testing goals (e.g., "comprehensively test this endpoint") through a single invocation, without requiring new MCP tool implementations for each workflow.
|
|
129
|
-
|
|
130
|
-
**When to use**:
|
|
131
|
-
- When the user names a specific endpoint and explicitly asks for comprehensive, thorough, or deep testing (e.g. "comprehensively test this endpoint", "thoroughly test the products endpoint"): MUST call this tool with workflow \`test_given_endpoint_comprehensively\`. Do NOT self-orchestrate — call this tool first and follow the returned instructions.
|
|
132
|
-
- When the user wants to scan the entire repo (no specific endpoint, no PR diff) to get ranked recommendations, generate top N types, and execute: MUST call this tool with workflow \`full_repo_scan_recommend_generate_and_execute_top_n_tests\`.
|
|
133
|
-
- Do NOT invoke these workflows for simple single-tool requests such as "generate a smoke test" or "run this test" — call the relevant tool directly instead.
|
|
134
|
-
|
|
135
|
-
**How it works**: Pass the \`workflow\` ID that matches the user's intent (see the \`workflow\` parameter description for available workflows and their trigger phrases). The tool returns a **workflow specification** (step-by-step instructions). You must then **execute that workflow** by calling the listed MCP tools in order, passing outputs (e.g. state file paths) from one step as inputs to the next. The tool does not execute other tools itself; it provides the declarative workflow for you to run.
|
|
136
|
-
|
|
137
|
-
**Design**:
|
|
138
|
-
- **MCP-only**: Workflows orchestrate only MCP tools; no CLI execution.
|
|
139
|
-
- **Declarative**: Workflows are sequences of tool calls with dependencies.
|
|
140
|
-
- **State-efficient**: State files are created only by recommendation/map tools (in system temp, e.g. /tmp); do not create any state files in the user repository.
|
|
141
|
-
- **Composable**: Complex workflows are built from existing Skyramp tools.`,
|
|
275
|
+
description: buildToolDescription(),
|
|
142
276
|
inputSchema: oneClickSchema.shape,
|
|
143
277
|
}, async (params) => {
|
|
144
278
|
let errorResult;
|
|
@@ -148,6 +282,7 @@ export function registerOneClickTool(server) {
|
|
|
148
282
|
repositoryPath: params.repositoryPath,
|
|
149
283
|
});
|
|
150
284
|
const cmd = lookupCommand(params.workflow);
|
|
285
|
+
const normalizedCallingAgent = normalizeCallingAgentIde(params.callingAgent);
|
|
151
286
|
const oneClickParams = {
|
|
152
287
|
workflow: params.workflow,
|
|
153
288
|
repositoryPath: params.repositoryPath,
|
|
@@ -155,8 +290,15 @@ export function registerOneClickTool(server) {
|
|
|
155
290
|
apiSchema: params.apiSchema,
|
|
156
291
|
topN: params.topN ?? 3,
|
|
157
292
|
token: params.token ?? "",
|
|
293
|
+
baseBranch: params.baseBranch,
|
|
294
|
+
servicesToMock: params.servicesToMock,
|
|
295
|
+
realServices: params.realServices,
|
|
296
|
+
mockProtocols: params.mockProtocols,
|
|
297
|
+
callingAgent: normalizedCallingAgent,
|
|
298
|
+
rawCallingAgent: params.callingAgent,
|
|
158
299
|
};
|
|
159
|
-
const
|
|
300
|
+
const bootstrapStatus = await buildLocalDevBootstrapStatus(oneClickParams);
|
|
301
|
+
const workflowText = buildWorkflowFromCommand(cmd, oneClickParams, bootstrapStatus);
|
|
160
302
|
return {
|
|
161
303
|
content: [
|
|
162
304
|
{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
|
|
2
|
+
import { execFileSync } from "child_process";
|
|
3
|
+
import * as fs from "fs";
|
|
4
|
+
import * as os from "os";
|
|
5
|
+
import * as path from "path";
|
|
6
|
+
jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
|
|
7
|
+
AnalyticsService: { pushMCPToolEvent: jest.fn() },
|
|
8
|
+
}));
|
|
9
|
+
const { registerOneClickTool } = await import("./oneClickTool.js");
|
|
10
|
+
function registerToolForTest() {
|
|
11
|
+
let registered;
|
|
12
|
+
const server = {
|
|
13
|
+
registerTool: jest.fn((name, config, handler) => {
|
|
14
|
+
registered = { name, config, handler };
|
|
15
|
+
}),
|
|
16
|
+
};
|
|
17
|
+
registerOneClickTool(server);
|
|
18
|
+
if (!registered)
|
|
19
|
+
throw new Error("tool was not registered");
|
|
20
|
+
return registered;
|
|
21
|
+
}
|
|
22
|
+
describe("skyramp_one_click_tool", () => {
|
|
23
|
+
let tmpDir;
|
|
24
|
+
let originalLocalDevEnv;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
originalLocalDevEnv = process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
27
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = "1";
|
|
28
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-"));
|
|
29
|
+
execFileSync("git", ["init"], { cwd: tmpDir, stdio: "ignore" });
|
|
30
|
+
});
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
if (originalLocalDevEnv === undefined) {
|
|
33
|
+
delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
process.env.SKYRAMP_FEATURE_LOCAL_DEV = originalLocalDevEnv;
|
|
37
|
+
}
|
|
38
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
it("routes local-dev requests and installs local-dev skill files", async () => {
|
|
41
|
+
const tool = registerToolForTest();
|
|
42
|
+
const result = await tool.handler({
|
|
43
|
+
workflow: "local_dev_test_changes",
|
|
44
|
+
repositoryPath: tmpDir,
|
|
45
|
+
callingAgent: "cursor",
|
|
46
|
+
baseBranch: "checkr-prod-flow",
|
|
47
|
+
});
|
|
48
|
+
const text = result.content[0].text;
|
|
49
|
+
expect(result.isError).toBe(false);
|
|
50
|
+
expect(text).toContain("Bootstrap Status");
|
|
51
|
+
expect(text).toContain("Installed local-dev skill file for cursor");
|
|
52
|
+
expect(text).toContain("Workspace config: missing");
|
|
53
|
+
expect(text).toContain('baseCommit`: from user input');
|
|
54
|
+
expect(text).toContain('Base branch: checkr-prod-flow');
|
|
55
|
+
expect(text).toContain('Services to mock: not specified');
|
|
56
|
+
expect(text).toContain('Real services: not specified');
|
|
57
|
+
expect(text).toContain('Mock protocols: not specified');
|
|
58
|
+
expect(text).toContain('Calling agent: cursor');
|
|
59
|
+
const cursorSkill = fs.readFileSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"), "utf-8");
|
|
60
|
+
expect(cursorSkill).toContain("mock only true third-party downstream services");
|
|
61
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(true);
|
|
62
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
63
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
it("installs only the Claude local-dev skill file", async () => {
|
|
66
|
+
const tool = registerToolForTest();
|
|
67
|
+
const result = await tool.handler({
|
|
68
|
+
workflow: "local_dev_test_changes",
|
|
69
|
+
repositoryPath: tmpDir,
|
|
70
|
+
callingAgent: "claude",
|
|
71
|
+
});
|
|
72
|
+
const text = result.content[0].text;
|
|
73
|
+
expect(result.isError).toBe(false);
|
|
74
|
+
expect(text).toContain("Installed local-dev skill file for claude");
|
|
75
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(true);
|
|
76
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
77
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
78
|
+
});
|
|
79
|
+
it("skips skill creation for agents without repo-local skill support", async () => {
|
|
80
|
+
const tool = registerToolForTest();
|
|
81
|
+
const result = await tool.handler({
|
|
82
|
+
workflow: "local_dev_test_changes",
|
|
83
|
+
repositoryPath: tmpDir,
|
|
84
|
+
callingAgent: "vscode",
|
|
85
|
+
});
|
|
86
|
+
const text = result.content[0].text;
|
|
87
|
+
expect(result.isError).toBe(false);
|
|
88
|
+
expect(text).toContain("this agent does not support repo-local skills, so skill creation was skipped");
|
|
89
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
90
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
91
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
it("does not install a skill file without a recognized calling agent", async () => {
|
|
94
|
+
const tool = registerToolForTest();
|
|
95
|
+
const result = await tool.handler({
|
|
96
|
+
workflow: "local_dev_test_changes",
|
|
97
|
+
repositoryPath: tmpDir,
|
|
98
|
+
callingAgent: "unknown",
|
|
99
|
+
});
|
|
100
|
+
const text = result.content[0].text;
|
|
101
|
+
expect(result.isError).toBe(false);
|
|
102
|
+
expect(text).toContain("this agent does not support repo-local skills, so skill creation was skipped");
|
|
103
|
+
expect(text).toContain("Calling agent: unknown");
|
|
104
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
105
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
106
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
it("asks for a supported calling agent when callingAgent is omitted", async () => {
|
|
109
|
+
const tool = registerToolForTest();
|
|
110
|
+
const result = await tool.handler({
|
|
111
|
+
workflow: "local_dev_test_changes",
|
|
112
|
+
repositoryPath: tmpDir,
|
|
113
|
+
});
|
|
114
|
+
const text = result.content[0].text;
|
|
115
|
+
expect(result.isError).toBe(false);
|
|
116
|
+
expect(text).toContain("pass `callingAgent` as `cursor` or `claude`");
|
|
117
|
+
expect(text).toContain("Calling agent: not specified");
|
|
118
|
+
expect(text).not.toContain("this agent does not support repo-local skills");
|
|
119
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
120
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
121
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
122
|
+
});
|
|
123
|
+
it("surfaces local-dev mock routing parameters in workflow text", async () => {
|
|
124
|
+
const tool = registerToolForTest();
|
|
125
|
+
const result = await tool.handler({
|
|
126
|
+
workflow: "local_dev_test_changes",
|
|
127
|
+
repositoryPath: tmpDir,
|
|
128
|
+
callingAgent: "cursor",
|
|
129
|
+
baseBranch: "checkr-prod-flow",
|
|
130
|
+
servicesToMock: "stripe",
|
|
131
|
+
realServices: "partner-accounts,redpanda",
|
|
132
|
+
mockProtocols: "rest",
|
|
133
|
+
});
|
|
134
|
+
const text = result.content[0].text;
|
|
135
|
+
expect(result.isError).toBe(false);
|
|
136
|
+
expect(text).toContain("Services to mock: stripe");
|
|
137
|
+
expect(text).toContain("Real services: partner-accounts,redpanda");
|
|
138
|
+
expect(text).toContain("Mock protocols: rest");
|
|
139
|
+
});
|
|
140
|
+
it("does not install local-dev skill files outside an existing git repository", async () => {
|
|
141
|
+
const tool = registerToolForTest();
|
|
142
|
+
const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
|
|
143
|
+
try {
|
|
144
|
+
const result = await tool.handler({
|
|
145
|
+
workflow: "local_dev_test_changes",
|
|
146
|
+
repositoryPath: nonRepo,
|
|
147
|
+
callingAgent: "cursor",
|
|
148
|
+
});
|
|
149
|
+
expect(result.isError).toBe(true);
|
|
150
|
+
expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
|
|
151
|
+
expect(result.content[0].text).not.toContain("before installing local-dev skills");
|
|
152
|
+
expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
153
|
+
expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
154
|
+
expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
fs.rmSync(nonRepo, { recursive: true, force: true });
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
it("validates repositoryPath even when skill creation is skipped", async () => {
|
|
161
|
+
const tool = registerToolForTest();
|
|
162
|
+
const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
|
|
163
|
+
try {
|
|
164
|
+
const result = await tool.handler({
|
|
165
|
+
workflow: "local_dev_test_changes",
|
|
166
|
+
repositoryPath: nonRepo,
|
|
167
|
+
callingAgent: "vscode",
|
|
168
|
+
});
|
|
169
|
+
expect(result.isError).toBe(true);
|
|
170
|
+
expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
|
|
171
|
+
expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
172
|
+
expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
173
|
+
expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
fs.rmSync(nonRepo, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
it("advertises local-dev as a one-click workflow instead of self-orchestration", () => {
|
|
180
|
+
const tool = registerToolForTest();
|
|
181
|
+
expect(String(tool.config.description)).toContain("workflow `local_dev_test_changes`");
|
|
182
|
+
expect(String(tool.config.description)).toContain("Do NOT self-orchestrate");
|
|
183
|
+
});
|
|
184
|
+
it("hides local-dev workflow when the feature flag is disabled", async () => {
|
|
185
|
+
delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
|
|
186
|
+
const tool = registerToolForTest();
|
|
187
|
+
expect(String(tool.config.description)).not.toContain("local_dev_test_changes");
|
|
188
|
+
expect(String(tool.config.inputSchema)).not.toContain("local_dev_test_changes");
|
|
189
|
+
expect(Object.keys(tool.config.inputSchema)).not.toContain("baseBranch");
|
|
190
|
+
expect(Object.keys(tool.config.inputSchema)).not.toContain("servicesToMock");
|
|
191
|
+
expect(Object.keys(tool.config.inputSchema)).not.toContain("realServices");
|
|
192
|
+
expect(Object.keys(tool.config.inputSchema)).not.toContain("mockProtocols");
|
|
193
|
+
expect(Object.keys(tool.config.inputSchema)).not.toContain("callingAgent");
|
|
194
|
+
const result = await tool.handler({
|
|
195
|
+
workflow: "local_dev_test_changes",
|
|
196
|
+
repositoryPath: tmpDir,
|
|
197
|
+
});
|
|
198
|
+
expect(result.isError).toBe(true);
|
|
199
|
+
expect(result.content[0].text).toContain('Unknown workflow: "local_dev_test_changes"');
|
|
200
|
+
expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
|
|
201
|
+
expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
|
|
202
|
+
expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { validateMockCompatibility, MockProtocol, } from "../utils/mockCompatibility.js";
|
|
3
|
+
const warningSchema = z.object({
|
|
4
|
+
code: z.string().describe("Warning code identifier (e.g. REST_LOOPBACK_URL, GRPC_PERSISTENT_CONN)."),
|
|
5
|
+
message: z.string().describe("Human-readable description of the issue."),
|
|
6
|
+
suggestion: z.string().describe("Recommended action to resolve or work around the issue."),
|
|
7
|
+
affectedMocks: z.array(z.string()).describe("Mock identifiers (service name or endpoint URL) affected by this warning."),
|
|
8
|
+
blocking: z.boolean().optional().describe("When true, the affected mocks cannot intercept traffic and must not be generated or deployed."),
|
|
9
|
+
});
|
|
10
|
+
const preflightOutputSchema = {
|
|
11
|
+
hasBlockers: z.boolean().describe("True when at least one warning is blocking. Blocking mocks must be removed from the generation list before proceeding."),
|
|
12
|
+
warnings: z.array(warningSchema).describe("All compatibility warnings. Inspect the blocking field on each entry; blocking ones must be skipped, advisory ones may proceed with caution."),
|
|
13
|
+
};
|
|
14
|
+
const TOOL_NAME = "skyramp_preflight_mock_check";
|
|
15
|
+
const mockSpecSchema = z.object({
|
|
16
|
+
protocol: z.nativeEnum(MockProtocol).describe("The protocol of the mock"),
|
|
17
|
+
endpointURL: z.string().describe("The endpoint URL for the mock"),
|
|
18
|
+
method: z.string().optional().describe("HTTP method (for REST mocks)"),
|
|
19
|
+
grpcServiceName: z.string().optional().describe("gRPC service name (for gRPC mocks)"),
|
|
20
|
+
kafkaTopic: z.string().optional().describe("Kafka topic (for Kafka mocks)"),
|
|
21
|
+
});
|
|
22
|
+
const inputSchema = {
|
|
23
|
+
mocks: z
|
|
24
|
+
.array(mockSpecSchema)
|
|
25
|
+
.min(1)
|
|
26
|
+
.describe("Array of mock specifications to validate for compatibility"),
|
|
27
|
+
};
|
|
28
|
+
function formatWarning(warning) {
|
|
29
|
+
const icon = warning.blocking ? "🚫" : "⚠️ ";
|
|
30
|
+
return [
|
|
31
|
+
`${icon} [${warning.code}]${warning.blocking ? " BLOCKING — do not generate or deploy these mocks" : ""}`,
|
|
32
|
+
` ${warning.message}`,
|
|
33
|
+
` Suggestion: ${warning.suggestion}`,
|
|
34
|
+
` Affected: ${warning.affectedMocks.join(", ")}`,
|
|
35
|
+
].join("\n");
|
|
36
|
+
}
|
|
37
|
+
export function registerPreflightMockCheckTool(server) {
|
|
38
|
+
server.registerTool(TOOL_NAME, {
|
|
39
|
+
description: "Pre-flight compatibility check for mock deployments. Validates that a set of mocks " +
|
|
40
|
+
"can be deployed together without known protocol conflicts (e.g., REST loopback URLs in Docker local-dev " +
|
|
41
|
+
"and gRPC persistent connection caveats). Returns two classes of " +
|
|
42
|
+
"issues: advisory warnings (mocks are deployable but may exhibit runtime issues) and " +
|
|
43
|
+
"blocking warnings (mocks cannot intercept traffic and must not be generated or deployed). " +
|
|
44
|
+
"Check hasBlockers in the result and the blocking flag on each warning before proceeding.",
|
|
45
|
+
inputSchema,
|
|
46
|
+
outputSchema: preflightOutputSchema,
|
|
47
|
+
}, async (params) => {
|
|
48
|
+
const mocks = params.mocks;
|
|
49
|
+
const result = validateMockCompatibility(mocks, { dockerLocalDev: true });
|
|
50
|
+
let text;
|
|
51
|
+
if (result.warnings.length === 0) {
|
|
52
|
+
text = [
|
|
53
|
+
"✅ Pre-flight check passed — all clear.",
|
|
54
|
+
"",
|
|
55
|
+
`${mocks.length} mock(s) validated. No known compatibility issues detected.`,
|
|
56
|
+
"Safe to deploy in a single apply_mock() call.",
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const blockingCount = result.warnings.filter((w) => w.blocking).length;
|
|
61
|
+
const advisoryCount = result.warnings.length - blockingCount;
|
|
62
|
+
const headerParts = [];
|
|
63
|
+
if (blockingCount > 0)
|
|
64
|
+
headerParts.push(`${blockingCount} blocking`);
|
|
65
|
+
if (advisoryCount > 0)
|
|
66
|
+
headerParts.push(`${advisoryCount} advisory`);
|
|
67
|
+
text = [
|
|
68
|
+
`⚠️ Pre-flight check: ${result.warnings.length} issue(s) detected (${headerParts.join(", ")}).`,
|
|
69
|
+
"",
|
|
70
|
+
...result.warnings.map(formatWarning),
|
|
71
|
+
"",
|
|
72
|
+
result.hasBlockers
|
|
73
|
+
? "🚫 Blocking mocks must be removed from the generation list — they cannot intercept traffic. Advisory mocks may still be deployed with the caveats noted above."
|
|
74
|
+
: "Advisory warnings only — mocks are deployable, but consider the suggestions above to avoid runtime issues.",
|
|
75
|
+
].join("\n");
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
structuredContent: {
|
|
79
|
+
hasBlockers: result.hasBlockers,
|
|
80
|
+
warnings: result.warnings.map((w) => ({
|
|
81
|
+
code: w.code,
|
|
82
|
+
message: w.message,
|
|
83
|
+
suggestion: w.suggestion,
|
|
84
|
+
affectedMocks: w.affectedMocks,
|
|
85
|
+
...(w.blocking !== undefined ? { blocking: w.blocking } : {}),
|
|
86
|
+
})),
|
|
87
|
+
},
|
|
88
|
+
content: [{ type: "text", text }],
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -15,11 +15,12 @@ export declare const analyzeChangesInputSchema: {
|
|
|
15
15
|
scope: z.ZodOptional<z.ZodDefault<z.ZodEnum<["full_repo", "branch_diff"]>>>;
|
|
16
16
|
baseBranch: z.ZodOptional<z.ZodString>;
|
|
17
17
|
testDirectory: z.ZodOptional<z.ZodString>;
|
|
18
|
-
topN: z.
|
|
18
|
+
topN: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
|
|
19
19
|
maxGenerate: z.ZodOptional<z.ZodNumber>;
|
|
20
20
|
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
21
21
|
repository: z.ZodOptional<z.ZodString>;
|
|
22
22
|
testsRepoDir: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
23
|
+
includeUncommitted: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
|
|
23
24
|
};
|
|
24
25
|
export declare const NO_UI_INSTRUCTIONS = "No UI changes detected \u2014 no blueprint capture needed.";
|
|
25
26
|
export declare const NO_RESOLVABLE_URLS_INSTRUCTIONS = "Frontend changes detected but no candidate URLs could be resolved (workspace baseUrl missing or no router files matched). UI recommendations will be source-grounded only.";
|