@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.
Files changed (90) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +17 -14
  3. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  4. package/build/commands/localDevTestChangesCommand.js +188 -0
  5. package/build/index.js +74 -13
  6. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  7. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  8. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +7 -6
  9. package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
  10. package/build/prompts/local-dev/local-dev-plan.js +413 -0
  11. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  12. package/build/prompts/local-dev/local-dev-prompts.js +155 -0
  13. package/build/prompts/prompt-utils.d.ts +8 -0
  14. package/build/prompts/prompt-utils.js +33 -0
  15. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  16. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  17. package/build/prompts/sut-setup/shared.d.ts +20 -0
  18. package/build/prompts/sut-setup/shared.js +69 -7
  19. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  20. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
  21. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  22. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
  23. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  24. package/build/prompts/testbot/testbot-prompts.js +4 -34
  25. package/build/resources/testbotResource.js +2 -1
  26. package/build/services/TestExecutionService.d.ts +2 -1
  27. package/build/services/TestExecutionService.js +9 -9
  28. package/build/services/TestExecutionService.test.js +42 -1
  29. package/build/services/TestGenerationService.d.ts +2 -2
  30. package/build/tool-phases.js +4 -0
  31. package/build/tools/code-refactor/enhanceAssertionsTool.js +42 -18
  32. package/build/tools/enrichTestWithMocksTool.d.ts +15 -0
  33. package/build/tools/enrichTestWithMocksTool.js +413 -0
  34. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  35. package/build/tools/enrichTestWithMocksTool.test.js +60 -0
  36. package/build/tools/executeSkyrampTestTool.d.ts +4 -0
  37. package/build/tools/executeSkyrampTestTool.js +39 -18
  38. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  39. package/build/tools/executeSkyrampTestTool.test.js +16 -0
  40. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +79 -0
  41. package/build/tools/generate-tests/batchMockGenerationTool.js +395 -0
  42. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  43. package/build/tools/generate-tests/generateMockRestTool.d.ts +56 -6
  44. package/build/tools/generate-tests/generateMockRestTool.js +193 -21
  45. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
  46. package/build/tools/generateEnrichedIntegrationTestTool.js +209 -0
  47. package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
  48. package/build/tools/localDevWorkflowFixes.test.js +144 -0
  49. package/build/tools/one-click/oneClickTool.d.ts +10 -0
  50. package/build/tools/one-click/oneClickTool.js +177 -21
  51. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  52. package/build/tools/one-click/oneClickTool.test.js +172 -0
  53. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  54. package/build/tools/preflightMockCheckTool.js +91 -0
  55. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  56. package/build/tools/test-management/analyzeChangesTool.js +59 -38
  57. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  58. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  59. package/build/types/OneClickCommands.d.ts +1 -1
  60. package/build/types/RepositoryAnalysis.d.ts +117 -0
  61. package/build/types/RepositoryAnalysis.js +16 -2
  62. package/build/types/TestTypes.d.ts +11 -2
  63. package/build/types/TestTypes.js +11 -1
  64. package/build/utils/analyze-openapi.js +18 -1
  65. package/build/utils/analyze-openapi.test.d.ts +1 -0
  66. package/build/utils/analyze-openapi.test.js +19 -0
  67. package/build/utils/branchDiff.d.ts +17 -1
  68. package/build/utils/branchDiff.js +96 -14
  69. package/build/utils/branchDiff.test.d.ts +1 -0
  70. package/build/utils/branchDiff.test.js +107 -0
  71. package/build/utils/docker.test.js +1 -1
  72. package/build/utils/featureFlags.d.ts +15 -1
  73. package/build/utils/featureFlags.js +19 -2
  74. package/build/utils/featureFlags.test.js +57 -2
  75. package/build/utils/grpcMockValidation.d.ts +1 -0
  76. package/build/utils/grpcMockValidation.js +49 -0
  77. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  78. package/build/utils/grpcMockValidation.test.js +41 -0
  79. package/build/utils/httpMethodValidation.d.ts +4 -0
  80. package/build/utils/httpMethodValidation.js +13 -0
  81. package/build/utils/mockCompatibility.d.ts +49 -0
  82. package/build/utils/mockCompatibility.js +80 -0
  83. package/build/utils/mockCompatibility.test.d.ts +1 -0
  84. package/build/utils/mockCompatibility.test.js +88 -0
  85. package/build/utils/versions.d.ts +3 -3
  86. package/build/utils/versions.js +1 -1
  87. package/build/workspace/workspace.d.ts +35 -21
  88. package/build/workspace/workspace.js +8 -4
  89. package/build/workspace/workspace.test.js +65 -6
  90. package/package.json +2 -2
@@ -0,0 +1,144 @@
1
+ import { jest, describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import fs from "fs";
3
+ import os from "os";
4
+ import path from "path";
5
+ import { TestType } from "../types/TestTypes.js";
6
+ const generateRestMock = jest.fn();
7
+ jest.unstable_mockModule("../services/AnalyticsService.js", () => ({
8
+ AnalyticsService: {
9
+ pushMCPToolEvent: jest.fn(() => Promise.resolve(undefined)),
10
+ pushTestGenerationToolEvent: jest.fn(() => Promise.resolve(undefined)),
11
+ },
12
+ }));
13
+ jest.unstable_mockModule("../utils/gitStaging.js", () => ({
14
+ stageGeneratedPaths: jest.fn(),
15
+ }));
16
+ jest.unstable_mockModule("@skyramp/skyramp", () => ({
17
+ SkyrampClient: jest.fn().mockImplementation(() => ({
18
+ generateRestMock,
19
+ generateGrpcMock: jest.fn(),
20
+ generateKafkaMock: jest.fn(),
21
+ })),
22
+ }));
23
+ const { executeBatchMockGeneration, MockProtocol } = await import("./generate-tests/batchMockGenerationTool.js");
24
+ const { registerEnhanceAssertionsTool } = await import("./code-refactor/enhanceAssertionsTool.js");
25
+ function registerEnhanceToolForTest() {
26
+ let registered;
27
+ const server = {
28
+ registerTool: jest.fn((_name, _config, handler) => {
29
+ registered = { handler };
30
+ }),
31
+ };
32
+ registerEnhanceAssertionsTool(server);
33
+ if (!registered)
34
+ throw new Error("tool was not registered");
35
+ return registered;
36
+ }
37
+ describe("executeBatchMockGeneration", () => {
38
+ let tmpDir;
39
+ beforeEach(() => {
40
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-batch-mocks-"));
41
+ generateRestMock.mockReset();
42
+ });
43
+ afterEach(() => {
44
+ fs.rmSync(tmpDir, { recursive: true, force: true });
45
+ });
46
+ it("passes per-mock output names and directories to the generator", async () => {
47
+ const alternateDir = path.join(tmpDir, "alternate");
48
+ generateRestMock.mockResolvedValue("success: generated mock");
49
+ const result = await executeBatchMockGeneration({
50
+ outputDir: tmpDir,
51
+ mocks: [
52
+ {
53
+ protocol: MockProtocol.Rest,
54
+ endpointURL: "http://billing-service:4000/v1/preferences",
55
+ method: "PUT",
56
+ output: "billing_preferences_success_mock.py",
57
+ },
58
+ {
59
+ protocol: MockProtocol.Rest,
60
+ endpointURL: "http://billing-service:4000/v1/preferences",
61
+ method: "PUT",
62
+ outputDir: alternateDir,
63
+ output: "billing_preferences_error_mock.py",
64
+ },
65
+ ],
66
+ });
67
+ expect(result.isError).toBe(false);
68
+ expect(generateRestMock).toHaveBeenCalledTimes(2);
69
+ expect(generateRestMock.mock.calls[0][0]).toMatchObject({
70
+ outputDir: tmpDir,
71
+ output: "billing_preferences_success_mock.py",
72
+ });
73
+ expect(generateRestMock.mock.calls[1][0]).toMatchObject({
74
+ outputDir: alternateDir,
75
+ output: "billing_preferences_error_mock.py",
76
+ });
77
+ expect(fs.existsSync(alternateDir)).toBe(true);
78
+ const text = result.content[0].text;
79
+ expect(text).toContain("billing_preferences_error_mock.py");
80
+ });
81
+ it("marks partial failures as tool errors", async () => {
82
+ generateRestMock.mockResolvedValue("success: generated mock");
83
+ const result = await executeBatchMockGeneration({
84
+ outputDir: tmpDir,
85
+ mocks: [
86
+ {
87
+ protocol: MockProtocol.Rest,
88
+ endpointURL: "http://billing-service:4000/v1/preferences",
89
+ method: "PUT",
90
+ output: "billing_preferences_success_mock.py",
91
+ },
92
+ {
93
+ protocol: MockProtocol.Rest,
94
+ endpointURL: "http://billing-service:4000/v1/preferences",
95
+ method: "",
96
+ output: "billing_preferences_error_mock.py",
97
+ },
98
+ ],
99
+ });
100
+ expect(result.isError).toBe(true);
101
+ const text = result.content[0].text;
102
+ expect(text).toContain("Succeeded:** 1");
103
+ expect(text).toContain("Failed:** 1");
104
+ });
105
+ });
106
+ describe("skyramp_enhance_assertions autoApply", () => {
107
+ beforeEach(() => {
108
+ delete process.env.SKYRAMP_FEATURE_TESTBOT;
109
+ });
110
+ it("includes integration-specific assertion guidance", async () => {
111
+ const tool = registerEnhanceToolForTest();
112
+ const result = await tool.handler({
113
+ testFile: "/repo/tests/account_integration_test.py",
114
+ testType: TestType.INTEGRATION,
115
+ enhanceType: "generation",
116
+ autoApply: true,
117
+ });
118
+ expect(result.content[0].text).toContain("Type-specific assertion guidance");
119
+ expect(result.content[0].text).toContain("Chained values across steps");
120
+ });
121
+ it("includes provider contract-specific assertion guidance", async () => {
122
+ const tool = registerEnhanceToolForTest();
123
+ const result = await tool.handler({
124
+ testFile: "/repo/tests/account_contract_test.py",
125
+ testType: TestType.CONTRACT,
126
+ enhanceType: "generation",
127
+ autoApply: true,
128
+ });
129
+ expect(result.content[0].text).toContain("Chained values from inline request data");
130
+ expect(result.content[0].text).toContain("Do not reference `beforeAll` / `afterAll`");
131
+ });
132
+ it("includes UI-specific and Testbot-specific assertion guidance", async () => {
133
+ process.env.SKYRAMP_FEATURE_TESTBOT = "1";
134
+ const tool = registerEnhanceToolForTest();
135
+ const result = await tool.handler({
136
+ testFile: "/repo/tests/account_ui.spec.ts",
137
+ testType: TestType.UI,
138
+ enhanceType: "generation",
139
+ autoApply: true,
140
+ });
141
+ expect(result.content[0].text).toContain("Critical UI Assertions");
142
+ expect(result.content[0].text).toContain("Additional Testbot-Specific Checks");
143
+ });
144
+ });
@@ -1,4 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare enum CallingAgentIde {
3
+ Cursor = "cursor",
4
+ Claude = "claude"
5
+ }
2
6
  /** User params passed to workflow builder (optional fields may be undefined) */
3
7
  export type OneClickParams = {
4
8
  workflow: string;
@@ -7,5 +11,11 @@ export type OneClickParams = {
7
11
  apiSchema?: string;
8
12
  topN?: number;
9
13
  token?: string;
14
+ baseBranch?: string;
15
+ servicesToMock?: string;
16
+ realServices?: string;
17
+ mockProtocols?: string;
18
+ callingAgent?: CallingAgentIde;
19
+ rawCallingAgent?: string;
10
20
  };
11
21
  export declare function registerOneClickTool(server: McpServer): void;
@@ -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,92 @@ 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
+ skyramp_local_dev baseCommit: ${params.baseBranch ?? "main"}
98
+ Services to mock: ${params.servicesToMock ?? "not specified"}
99
+ Real services: ${params.realServices ?? "not specified"}
100
+ Mock protocols: ${params.mockProtocols ?? "not specified"}
101
+ Calling agent: ${params.rawCallingAgent ?? params.callingAgent ?? "not specified"}
102
+
103
+ Workspace config: ${workspaceExists ? `found at ${workspacePath}` : `missing at ${workspacePath}`}
104
+
105
+ `;
106
+ }
13
107
  function buildWorkflowParamDescription() {
14
108
  const commands = getAllCommands();
15
109
  const sections = commands.map((cmd) => {
@@ -29,7 +123,31 @@ function buildWorkflowParamDescription() {
29
123
  return (`The workflow to execute. Match the user's intent against the available workflows below and pass the matching workflow ID.\n\n` +
30
124
  `Available workflows:\n${sections.join("\n\n")}`);
31
125
  }
32
- const oneClickSchema = z.object({
126
+ function isLocalDevWorkflowAvailable() {
127
+ return getAllCommands().some((cmd) => cmd.id === "local_dev_test_changes");
128
+ }
129
+ function buildToolDescription() {
130
+ const localDevInstructions = isLocalDevWorkflowAvailable()
131
+ ? `- 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`
132
+ : "";
133
+ return `Execute predefined, multi-step testing workflows with a single tool call.
134
+
135
+ **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.
136
+
137
+ **When to use**:
138
+ ${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.
139
+ - 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\`.
140
+ - 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.
141
+
142
+ **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.
143
+
144
+ **Design**:
145
+ - **MCP-only**: Workflows orchestrate only MCP tools; no CLI execution.
146
+ - **Declarative**: Workflows are sequences of tool calls with dependencies.
147
+ - **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.
148
+ - **Composable**: Complex workflows are built from existing Skyramp tools.`;
149
+ }
150
+ const baseOneClickSchema = z.object({
33
151
  workflow: z.string().describe(buildWorkflowParamDescription()),
34
152
  repositoryPath: z
35
153
  .string()
@@ -51,10 +169,52 @@ const oneClickSchema = z.object({
51
169
  .optional()
52
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."),
53
171
  });
172
+ function buildOneClickSchema() {
173
+ if (!isLocalDevWorkflowAvailable()) {
174
+ return baseOneClickSchema;
175
+ }
176
+ return baseOneClickSchema.extend({
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
+ }
200
+ function renderStepGuidance(step, cmd, params) {
201
+ let guidance = step.conditionalGuidance ?? "";
202
+ if (!guidance || cmd.id !== "local_dev_test_changes")
203
+ return guidance;
204
+ const baseCommit = params.baseBranch ?? "main";
205
+ guidance = guidance
206
+ .replaceAll("<BASE_BRANCH from one-click baseBranch, default main>", baseCommit)
207
+ .replaceAll("<BASE_BRANCH>", baseCommit)
208
+ .replaceAll("<servicesToMock from one-click params, if provided; use all when the user requested mock all>", params.servicesToMock ?? "not specified")
209
+ .replaceAll("<realServices from one-click params, if provided>", params.realServices ?? "not specified")
210
+ .replaceAll("<mockProtocols from one-click params, if provided>", params.mockProtocols ?? "not specified");
211
+ return guidance;
212
+ }
54
213
  /**
55
214
  * Build workflow instruction text from any command definition and params.
215
+ * Optional `stepsPreamble` is inserted between the intro header and Step 0.
56
216
  */
57
- function buildWorkflowFromCommand(cmd, params) {
217
+ function buildWorkflowFromCommand(cmd, params, stepsPreamble = "") {
58
218
  const getParamDisplay = (key) => {
59
219
  const v = params[key];
60
220
  if (v === undefined || v === null)
@@ -90,8 +250,9 @@ function buildWorkflowFromCommand(cmd, params) {
90
250
  block += `\n\nCapture for later steps: ${tc.outputs.join(", ")}.`;
91
251
  }
92
252
  }
93
- if (step.conditionalGuidance) {
94
- block += `\n\n**Note**: ${step.conditionalGuidance}`;
253
+ const conditionalGuidance = renderStepGuidance(step, cmd, params);
254
+ if (conditionalGuidance) {
255
+ block += `\n\n**Note**: ${conditionalGuidance}`;
95
256
  }
96
257
  stepLines.push(block);
97
258
  }
@@ -108,6 +269,7 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
108
269
 
109
270
  `;
110
271
  return (intro +
272
+ stepsPreamble +
111
273
  stepLines.join("\n\n---\n\n") +
112
274
  `
113
275
 
@@ -122,23 +284,9 @@ Execute the following steps in order. Pass outputs (e.g. state file path) from o
122
284
  `);
123
285
  }
124
286
  export function registerOneClickTool(server) {
287
+ const oneClickSchema = buildOneClickSchema();
125
288
  server.registerTool(TOOL_NAME, {
126
- description: `Execute predefined, multi-step testing workflows with a single tool call.
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.`,
289
+ description: buildToolDescription(),
142
290
  inputSchema: oneClickSchema.shape,
143
291
  }, async (params) => {
144
292
  let errorResult;
@@ -148,6 +296,7 @@ export function registerOneClickTool(server) {
148
296
  repositoryPath: params.repositoryPath,
149
297
  });
150
298
  const cmd = lookupCommand(params.workflow);
299
+ const normalizedCallingAgent = normalizeCallingAgentIde(params.callingAgent);
151
300
  const oneClickParams = {
152
301
  workflow: params.workflow,
153
302
  repositoryPath: params.repositoryPath,
@@ -155,8 +304,15 @@ export function registerOneClickTool(server) {
155
304
  apiSchema: params.apiSchema,
156
305
  topN: params.topN ?? 3,
157
306
  token: params.token ?? "",
307
+ baseBranch: params.baseBranch,
308
+ servicesToMock: params.servicesToMock,
309
+ realServices: params.realServices,
310
+ mockProtocols: params.mockProtocols,
311
+ callingAgent: normalizedCallingAgent,
312
+ rawCallingAgent: params.callingAgent,
158
313
  };
159
- const workflowText = buildWorkflowFromCommand(cmd, oneClickParams);
314
+ const bootstrapStatus = await buildLocalDevBootstrapStatus(oneClickParams);
315
+ const workflowText = buildWorkflowFromCommand(cmd, oneClickParams, bootstrapStatus);
160
316
  return {
161
317
  content: [
162
318
  {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
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('skyramp_local_dev baseCommit: checkr-prod-flow');
51
+ const cursorSkill = fs.readFileSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"), "utf-8");
52
+ expect(cursorSkill).toContain("mock only true third-party downstream services");
53
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(true);
54
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
55
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
56
+ });
57
+ it("installs only the Claude local-dev skill file", async () => {
58
+ const tool = registerToolForTest();
59
+ const result = await tool.handler({
60
+ workflow: "local_dev_test_changes",
61
+ repositoryPath: tmpDir,
62
+ callingAgent: "claude",
63
+ });
64
+ expect(result.isError).toBe(false);
65
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(true);
66
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
67
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
68
+ });
69
+ it("skips skill creation for agents without repo-local skill support", async () => {
70
+ const tool = registerToolForTest();
71
+ const result = await tool.handler({
72
+ workflow: "local_dev_test_changes",
73
+ repositoryPath: tmpDir,
74
+ callingAgent: "vscode",
75
+ });
76
+ expect(result.isError).toBe(false);
77
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
78
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
79
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
80
+ });
81
+ it("does not install a skill file without a recognized calling agent", async () => {
82
+ const tool = registerToolForTest();
83
+ const result = await tool.handler({
84
+ workflow: "local_dev_test_changes",
85
+ repositoryPath: tmpDir,
86
+ callingAgent: "unknown",
87
+ });
88
+ expect(result.isError).toBe(false);
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("surfaces local-dev mock routing parameters in workflow text", async () => {
94
+ const tool = registerToolForTest();
95
+ const result = await tool.handler({
96
+ workflow: "local_dev_test_changes",
97
+ repositoryPath: tmpDir,
98
+ callingAgent: "cursor",
99
+ baseBranch: "checkr-prod-flow",
100
+ servicesToMock: "stripe",
101
+ realServices: "partner-accounts,redpanda",
102
+ mockProtocols: "rest",
103
+ });
104
+ const text = result.content[0].text;
105
+ expect(result.isError).toBe(false);
106
+ expect(text).toContain("Services to mock: stripe");
107
+ expect(text).toContain("Real services: partner-accounts,redpanda");
108
+ expect(text).toContain("Mock protocols: rest");
109
+ expect(text).toContain("<SERVICE_ROUTING>");
110
+ expect(text).toContain("<MOCK_SERVICES>stripe</MOCK_SERVICES>");
111
+ expect(text).toContain("<REAL_SERVICES>partner-accounts,redpanda</REAL_SERVICES>");
112
+ expect(text).toContain("<MOCK_PROTOCOLS>rest</MOCK_PROTOCOLS>");
113
+ });
114
+ it("does not install local-dev skill files outside an existing git repository", async () => {
115
+ const tool = registerToolForTest();
116
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
117
+ try {
118
+ const result = await tool.handler({
119
+ workflow: "local_dev_test_changes",
120
+ repositoryPath: nonRepo,
121
+ callingAgent: "cursor",
122
+ });
123
+ expect(result.isError).toBe(true);
124
+ expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
125
+ expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
126
+ expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
127
+ expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
128
+ }
129
+ finally {
130
+ fs.rmSync(nonRepo, { recursive: true, force: true });
131
+ }
132
+ });
133
+ it("validates repositoryPath even when skill creation is skipped", async () => {
134
+ const tool = registerToolForTest();
135
+ const nonRepo = fs.mkdtempSync(path.join(os.tmpdir(), "skyramp-one-click-nonrepo-"));
136
+ try {
137
+ const result = await tool.handler({
138
+ workflow: "local_dev_test_changes",
139
+ repositoryPath: nonRepo,
140
+ callingAgent: "vscode",
141
+ });
142
+ expect(result.isError).toBe(true);
143
+ expect(result.content[0].text).toContain("repositoryPath must point to a git repository");
144
+ expect(fs.existsSync(path.join(nonRepo, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
145
+ expect(fs.existsSync(path.join(nonRepo, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
146
+ expect(fs.existsSync(path.join(nonRepo, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
147
+ }
148
+ finally {
149
+ fs.rmSync(nonRepo, { recursive: true, force: true });
150
+ }
151
+ });
152
+ it("hides local-dev workflow when the feature flag is disabled", async () => {
153
+ delete process.env.SKYRAMP_FEATURE_LOCAL_DEV;
154
+ const tool = registerToolForTest();
155
+ expect(String(tool.config.description)).not.toContain("local_dev_test_changes");
156
+ expect(String(tool.config.inputSchema)).not.toContain("local_dev_test_changes");
157
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("baseBranch");
158
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("servicesToMock");
159
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("realServices");
160
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("mockProtocols");
161
+ expect(Object.keys(tool.config.inputSchema)).not.toContain("callingAgent");
162
+ const result = await tool.handler({
163
+ workflow: "local_dev_test_changes",
164
+ repositoryPath: tmpDir,
165
+ });
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0].text).toContain('Unknown workflow: "local_dev_test_changes"');
168
+ expect(fs.existsSync(path.join(tmpDir, ".cursor/skills/local-dev/SKILL.md"))).toBe(false);
169
+ expect(fs.existsSync(path.join(tmpDir, ".claude/skills/local-dev/SKILL.md"))).toBe(false);
170
+ expect(fs.existsSync(path.join(tmpDir, ".vscode/skills/local-dev/SKILL.md"))).toBe(false);
171
+ });
172
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerPreflightMockCheckTool(server: McpServer): void;