@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 (99) hide show
  1. package/build/commands/commandLibrary.d.ts +1 -0
  2. package/build/commands/commandLibrary.js +20 -14
  3. package/build/commands/commandLibrary.test.d.ts +1 -0
  4. package/build/commands/commandLibrary.test.js +59 -0
  5. package/build/commands/localDevTestChangesCommand.d.ts +15 -0
  6. package/build/commands/localDevTestChangesCommand.js +188 -0
  7. package/build/index.js +77 -13
  8. package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
  9. package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
  10. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +9 -8
  11. package/build/prompts/local-dev/local-dev-plan.d.ts +25 -0
  12. package/build/prompts/local-dev/local-dev-plan.js +419 -0
  13. package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
  14. package/build/prompts/local-dev/local-dev-prompts.js +157 -0
  15. package/build/prompts/prompt-utils.d.ts +8 -0
  16. package/build/prompts/prompt-utils.js +33 -0
  17. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  18. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -1
  19. package/build/prompts/sut-setup/shared.d.ts +20 -0
  20. package/build/prompts/sut-setup/shared.js +69 -7
  21. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
  22. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -28
  23. package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
  24. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +16 -1
  25. package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
  26. package/build/prompts/testbot/testbot-prompts.js +4 -34
  27. package/build/resources/testbotResource.js +2 -1
  28. package/build/services/AnalyticsService.d.ts +1 -1
  29. package/build/services/TestExecutionService.d.ts +2 -1
  30. package/build/services/TestExecutionService.js +15 -10
  31. package/build/services/TestExecutionService.test.js +75 -1
  32. package/build/services/TestGenerationService.d.ts +2 -2
  33. package/build/tool-phases.js +4 -0
  34. package/build/tools/code-refactor/enhanceAssertionsTool.js +47 -18
  35. package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
  36. package/build/tools/enrichTestWithMocksTool.js +726 -0
  37. package/build/tools/enrichTestWithMocksTool.test.d.ts +1 -0
  38. package/build/tools/enrichTestWithMocksTool.test.js +266 -0
  39. package/build/tools/executeSkyrampTestTool.d.ts +4 -0
  40. package/build/tools/executeSkyrampTestTool.js +48 -24
  41. package/build/tools/executeSkyrampTestTool.test.d.ts +1 -0
  42. package/build/tools/executeSkyrampTestTool.test.js +16 -0
  43. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +85 -0
  44. package/build/tools/generate-tests/batchMockGenerationTool.js +432 -0
  45. package/build/tools/generate-tests/generateContractRestTool.js +2 -2
  46. package/build/tools/generate-tests/generateMockRestTool.d.ts +129 -6
  47. package/build/tools/generate-tests/generateMockRestTool.js +234 -22
  48. package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
  49. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +24 -0
  50. package/build/tools/generateEnrichedIntegrationTestTool.js +225 -0
  51. package/build/tools/generateEnrichedIntegrationTestTool.test.d.ts +1 -0
  52. package/build/tools/generateEnrichedIntegrationTestTool.test.js +44 -0
  53. package/build/tools/localDevWorkflowFixes.test.d.ts +1 -0
  54. package/build/tools/localDevWorkflowFixes.test.js +255 -0
  55. package/build/tools/one-click/oneClickTool.d.ts +10 -0
  56. package/build/tools/one-click/oneClickTool.js +177 -21
  57. package/build/tools/one-click/oneClickTool.test.d.ts +1 -0
  58. package/build/tools/one-click/oneClickTool.test.js +172 -0
  59. package/build/tools/preflightMockCheckTool.d.ts +2 -0
  60. package/build/tools/preflightMockCheckTool.js +96 -0
  61. package/build/tools/submitReportTool.d.ts +14 -14
  62. package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
  63. package/build/tools/test-management/analyzeChangesTool.js +59 -38
  64. package/build/tools/trace/startTraceCollectionTool.js +3 -3
  65. package/build/tools/workspace/initializeWorkspaceTool.js +2 -9
  66. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  67. package/build/types/OneClickCommands.d.ts +1 -1
  68. package/build/types/RepositoryAnalysis.d.ts +117 -0
  69. package/build/types/RepositoryAnalysis.js +16 -2
  70. package/build/types/TestExecution.d.ts +1 -0
  71. package/build/types/TestTypes.d.ts +20 -8
  72. package/build/types/TestTypes.js +19 -5
  73. package/build/utils/analyze-openapi.js +18 -1
  74. package/build/utils/analyze-openapi.test.d.ts +1 -0
  75. package/build/utils/analyze-openapi.test.js +19 -0
  76. package/build/utils/branchDiff.d.ts +17 -1
  77. package/build/utils/branchDiff.js +96 -14
  78. package/build/utils/branchDiff.test.d.ts +1 -0
  79. package/build/utils/branchDiff.test.js +109 -0
  80. package/build/utils/docker.test.js +1 -1
  81. package/build/utils/featureFlags.d.ts +20 -1
  82. package/build/utils/featureFlags.js +26 -2
  83. package/build/utils/featureFlags.test.js +57 -2
  84. package/build/utils/grpcMockValidation.d.ts +1 -0
  85. package/build/utils/grpcMockValidation.js +49 -0
  86. package/build/utils/grpcMockValidation.test.d.ts +1 -0
  87. package/build/utils/grpcMockValidation.test.js +41 -0
  88. package/build/utils/httpMethodValidation.d.ts +4 -0
  89. package/build/utils/httpMethodValidation.js +13 -0
  90. package/build/utils/mockCompatibility.d.ts +49 -0
  91. package/build/utils/mockCompatibility.js +82 -0
  92. package/build/utils/mockCompatibility.test.d.ts +1 -0
  93. package/build/utils/mockCompatibility.test.js +79 -0
  94. package/build/utils/versions.d.ts +3 -3
  95. package/build/utils/versions.js +1 -1
  96. package/build/workspace/workspace.d.ts +49 -35
  97. package/build/workspace/workspace.js +10 -5
  98. package/build/workspace/workspace.test.js +65 -6
  99. package/package.json +2 -2
@@ -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;
@@ -0,0 +1,96 @@
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
+ dockerLocalDev: z
28
+ .boolean()
29
+ .default(true)
30
+ .describe("When true, apply Docker local-dev checks such as REST loopback URL blockers. Set false for non-Docker/local runtimes."),
31
+ };
32
+ function formatWarning(warning) {
33
+ const icon = warning.blocking ? "🚫" : "⚠️ ";
34
+ return [
35
+ `${icon} [${warning.code}]${warning.blocking ? " BLOCKING — do not generate or deploy these mocks" : ""}`,
36
+ ` ${warning.message}`,
37
+ ` Suggestion: ${warning.suggestion}`,
38
+ ` Affected: ${warning.affectedMocks.join(", ")}`,
39
+ ].join("\n");
40
+ }
41
+ export function registerPreflightMockCheckTool(server) {
42
+ server.registerTool(TOOL_NAME, {
43
+ description: "Pre-flight compatibility check for mock deployments. Validates that a set of mocks " +
44
+ "can be deployed together without known protocol conflicts (e.g., REST loopback URLs in Docker local-dev " +
45
+ "and gRPC persistent connection caveats). Returns two classes of " +
46
+ "issues: advisory warnings (mocks are deployable but may exhibit runtime issues) and " +
47
+ "blocking warnings (mocks cannot intercept traffic and must not be generated or deployed). " +
48
+ "Check hasBlockers in the result and the blocking flag on each warning before proceeding.",
49
+ inputSchema,
50
+ outputSchema: preflightOutputSchema,
51
+ }, async (params) => {
52
+ const mocks = params.mocks;
53
+ const dockerLocalDev = params.dockerLocalDev !== false;
54
+ const result = validateMockCompatibility(mocks, { dockerLocalDev });
55
+ let text;
56
+ if (result.warnings.length === 0) {
57
+ text = [
58
+ "✅ Pre-flight check passed — all clear.",
59
+ "",
60
+ `${mocks.length} mock(s) validated. No known compatibility issues detected.`,
61
+ "Safe to deploy in a single apply_mock() call.",
62
+ ].join("\n");
63
+ }
64
+ else {
65
+ const blockingCount = result.warnings.filter((w) => w.blocking).length;
66
+ const advisoryCount = result.warnings.length - blockingCount;
67
+ const headerParts = [];
68
+ if (blockingCount > 0)
69
+ headerParts.push(`${blockingCount} blocking`);
70
+ if (advisoryCount > 0)
71
+ headerParts.push(`${advisoryCount} advisory`);
72
+ text = [
73
+ `⚠️ Pre-flight check: ${result.warnings.length} issue(s) detected (${headerParts.join(", ")}).`,
74
+ "",
75
+ ...result.warnings.map(formatWarning),
76
+ "",
77
+ result.hasBlockers
78
+ ? "🚫 Blocking mocks must be removed from the generation list — they cannot intercept traffic. Advisory mocks may still be deployed with the caveats noted above."
79
+ : "Advisory warnings only — mocks are deployable, but consider the suggestions above to avoid runtime issues.",
80
+ ].join("\n");
81
+ }
82
+ return {
83
+ structuredContent: {
84
+ hasBlockers: result.hasBlockers,
85
+ warnings: result.warnings.map((w) => ({
86
+ code: w.code,
87
+ message: w.message,
88
+ suggestion: w.suggestion,
89
+ affectedMocks: w.affectedMocks,
90
+ ...(w.blocking !== undefined ? { blocking: w.blocking } : {}),
91
+ })),
92
+ },
93
+ content: [{ type: "text", text }],
94
+ };
95
+ });
96
+ }
@@ -16,7 +16,7 @@ export declare const targetElementSchema: z.ZodObject<{
16
16
  stableId: string | null;
17
17
  contextText: string[] | null;
18
18
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
19
- widgetType?: "custom" | "unknown" | "native" | undefined;
19
+ widgetType?: "unknown" | "custom" | "native" | undefined;
20
20
  }, {
21
21
  role: string;
22
22
  accessibleName: string;
@@ -24,7 +24,7 @@ export declare const targetElementSchema: z.ZodObject<{
24
24
  stableId: string | null;
25
25
  contextText: string[] | null;
26
26
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
27
- widgetType?: "custom" | "unknown" | "native" | undefined;
27
+ widgetType?: "unknown" | "custom" | "native" | undefined;
28
28
  }>;
29
29
  export declare const pageContextSchema: z.ZodObject<{
30
30
  url: z.ZodString;
@@ -63,7 +63,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
63
63
  stableId: string | null;
64
64
  contextText: string[] | null;
65
65
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
66
- widgetType?: "custom" | "unknown" | "native" | undefined;
66
+ widgetType?: "unknown" | "custom" | "native" | undefined;
67
67
  }, {
68
68
  role: string;
69
69
  accessibleName: string;
@@ -71,7 +71,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
71
71
  stableId: string | null;
72
72
  contextText: string[] | null;
73
73
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
74
- widgetType?: "custom" | "unknown" | "native" | undefined;
74
+ widgetType?: "unknown" | "custom" | "native" | undefined;
75
75
  }>, "many">>>;
76
76
  pageContext: z.ZodOptional<z.ZodObject<{
77
77
  url: z.ZodString;
@@ -102,7 +102,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
102
102
  stableId: string | null;
103
103
  contextText: string[] | null;
104
104
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
105
- widgetType?: "custom" | "unknown" | "native" | undefined;
105
+ widgetType?: "unknown" | "custom" | "native" | undefined;
106
106
  }[] | null | undefined;
107
107
  pageContext?: {
108
108
  url: string;
@@ -127,7 +127,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
127
127
  stableId: string | null;
128
128
  contextText: string[] | null;
129
129
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
130
- widgetType?: "custom" | "unknown" | "native" | undefined;
130
+ widgetType?: "unknown" | "custom" | "native" | undefined;
131
131
  }[] | null | undefined;
132
132
  pageContext?: {
133
133
  url: string;
@@ -152,7 +152,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
152
152
  stableId: string | null;
153
153
  contextText: string[] | null;
154
154
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
155
- widgetType?: "custom" | "unknown" | "native" | undefined;
155
+ widgetType?: "unknown" | "custom" | "native" | undefined;
156
156
  }[] | null | undefined;
157
157
  pageContext?: {
158
158
  url: string;
@@ -177,7 +177,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
177
177
  stableId: string | null;
178
178
  contextText: string[] | null;
179
179
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
180
- widgetType?: "custom" | "unknown" | "native" | undefined;
180
+ widgetType?: "unknown" | "custom" | "native" | undefined;
181
181
  }[] | null | undefined;
182
182
  pageContext?: {
183
183
  url: string;
@@ -234,7 +234,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
234
234
  stableId: string | null;
235
235
  contextText: string[] | null;
236
236
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
237
- widgetType?: "custom" | "unknown" | "native" | undefined;
237
+ widgetType?: "unknown" | "custom" | "native" | undefined;
238
238
  }, {
239
239
  role: string;
240
240
  accessibleName: string;
@@ -242,7 +242,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
242
242
  stableId: string | null;
243
243
  contextText: string[] | null;
244
244
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
245
- widgetType?: "custom" | "unknown" | "native" | undefined;
245
+ widgetType?: "unknown" | "custom" | "native" | undefined;
246
246
  }>, "many">>>;
247
247
  pageContext: z.ZodOptional<z.ZodObject<{
248
248
  url: z.ZodString;
@@ -279,7 +279,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
279
279
  stableId: string | null;
280
280
  contextText: string[] | null;
281
281
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
282
- widgetType?: "custom" | "unknown" | "native" | undefined;
282
+ widgetType?: "unknown" | "custom" | "native" | undefined;
283
283
  }[] | null | undefined;
284
284
  pageContext?: {
285
285
  url: string;
@@ -313,7 +313,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
313
313
  stableId: string | null;
314
314
  contextText: string[] | null;
315
315
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
316
- widgetType?: "custom" | "unknown" | "native" | undefined;
316
+ widgetType?: "unknown" | "custom" | "native" | undefined;
317
317
  }[] | null | undefined;
318
318
  pageContext?: {
319
319
  url: string;
@@ -347,7 +347,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
347
347
  stableId: string | null;
348
348
  contextText: string[] | null;
349
349
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
350
- widgetType?: "custom" | "unknown" | "native" | undefined;
350
+ widgetType?: "unknown" | "custom" | "native" | undefined;
351
351
  }[] | null | undefined;
352
352
  pageContext?: {
353
353
  url: string;
@@ -381,7 +381,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
381
381
  stableId: string | null;
382
382
  contextText: string[] | null;
383
383
  mutability?: "unknown" | "mutable" | "immutable" | undefined;
384
- widgetType?: "custom" | "unknown" | "native" | undefined;
384
+ widgetType?: "unknown" | "custom" | "native" | undefined;
385
385
  }[] | null | undefined;
386
386
  pageContext?: {
387
387
  url: string;
@@ -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.ZodOptional<z.ZodDefault<z.ZodNumber>>;
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.";
@@ -352,8 +352,8 @@ export const analyzeChangesInputSchema = {
352
352
  .describe("Directory containing existing tests (auto-detected if omitted)"),
353
353
  topN: z
354
354
  .number()
355
- .default(MAX_RECOMMENDATIONS)
356
355
  .optional()
356
+ .default(MAX_RECOMMENDATIONS)
357
357
  .describe(`Number of ranked test recommendations to generate. Defaults to ${MAX_RECOMMENDATIONS}.`),
358
358
  maxGenerate: z
359
359
  .number()
@@ -374,6 +374,11 @@ export const analyzeChangesInputSchema = {
374
374
  .refine((v) => path.isAbsolute(v), { message: "testsRepoDir must be an absolute path" })
375
375
  .optional()
376
376
  .describe("Absolute path to a separate test repository clone. When set, existing test discovery scans this directory instead of repositoryPath. Used in cross-repo test delivery mode where tests live in a separate repo."),
377
+ includeUncommitted: z
378
+ .boolean()
379
+ .optional()
380
+ .default(false)
381
+ .describe("When true, diffs the base ref against the working tree (captures uncommitted and unstaged changes). Use for local-dev workflows. Defaults to false (CI mode — committed changes only)."),
377
382
  };
378
383
  // ── UI blueprint-capture instructions ──
379
384
  // Moved here from the former skyramp_ui_analyze_changes pre-flight tool. These
@@ -441,7 +446,7 @@ export function registerAnalyzeChangesTool(server) {
441
446
  if (analysisScope === AnalysisScope.CurrentBranchDiff) {
442
447
  await sendProgress(10, 100, "Computing branch diff...");
443
448
  try {
444
- diffData = await computeBranchDiff(params.repositoryPath, params.baseBranch);
449
+ diffData = await computeBranchDiff(params.repositoryPath, params.baseBranch, params.includeUncommitted ?? false);
445
450
  logger.info("Branch diff computed", {
446
451
  currentBranch: diffData.currentBranch,
447
452
  baseBranch: diffData.baseBranch,
@@ -1106,6 +1111,56 @@ export function registerAnalyzeChangesTool(server) {
1106
1111
  affectedServices: classifiedEndpoints.affectedServices,
1107
1112
  summary: "",
1108
1113
  } : undefined;
1114
+ // ── Route discovery context for LLM grounding and state persistence ──
1115
+ // fullAnalysis lives only in inMemorySessionStore (for MCP resources
1116
+ // and registerRecommendTestsPrompt). The disk state carries only the
1117
+ // slim fields that downstream tools (health, execute, actions) need.
1118
+ // routerMountContext and candidateRouteFiles are computed here so they
1119
+ // can be persisted to the state file for downstream tools (health, drift).
1120
+ // Without them, analyzeTestHealth would work only off the static catalog
1121
+ // which has wrong paths for nested resources and unsupported frameworks.
1122
+ const routeLikeUnmatchedFiles = [];
1123
+ for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
1124
+ const routeLike = SOURCE_EXTS.test(file) &&
1125
+ (ROUTE_FILE_PATTERN.test(file) || ROUTE_FILE_BASENAME_PATTERN.test(path.basename(file)));
1126
+ if (routeLike && !(await isGraphQLFile(file, params.repositoryPath))) {
1127
+ routeLikeUnmatchedFiles.push(file);
1128
+ }
1129
+ }
1130
+ const shouldIncludeCandidateRouteFiles = analysisScope !== AnalysisScope.CurrentBranchDiff ||
1131
+ rawRelatedEndpointCount === 0 ||
1132
+ scannedEndpoints.length === 0 ||
1133
+ routeLikeUnmatchedFiles.length > 0;
1134
+ let candidateRouteFiles;
1135
+ if (shouldIncludeCandidateRouteFiles) {
1136
+ candidateRouteFiles = [];
1137
+ for (const file of findCandidateRouteFiles(params.repositoryPath)) {
1138
+ if (!(await isGraphQLFile(file, params.repositoryPath))) {
1139
+ candidateRouteFiles.push(file);
1140
+ }
1141
+ }
1142
+ }
1143
+ // Write the full diff to a temp file before building state so the path
1144
+ // can be persisted and read by analyzeTestHealthTool for per-line detection.
1145
+ let diffFilePath;
1146
+ if (diffData?.diffContent) {
1147
+ diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
1148
+ await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
1149
+ }
1150
+ const routeDiscovery = {
1151
+ candidateFiles: [
1152
+ ...(diffData?.changedFiles ?? []),
1153
+ ...(candidateRouteFiles ?? []),
1154
+ ].filter((file, index, files) => files.indexOf(file) === index),
1155
+ staticHints: scannedEndpoints.map((ep) => ({
1156
+ path: ep.path,
1157
+ methods: ep.methods,
1158
+ sourceFile: ep.sourceFile,
1159
+ })),
1160
+ openApiPaths: specPaths ? [...specPaths] : [],
1161
+ routerMountContext,
1162
+ ...(diffFilePath ? { diffFilePath } : {}),
1163
+ };
1109
1164
  const fullAnalysis = {
1110
1165
  metadata: {
1111
1166
  repositoryName: path.basename(params.repositoryPath),
@@ -1162,6 +1217,7 @@ export function registerAnalyzeChangesTool(server) {
1162
1217
  hasCoverageReports: false,
1163
1218
  relevantExternalTestPaths,
1164
1219
  },
1220
+ routeDiscovery,
1165
1221
  ...(diffContext ? { branchDiffContext: diffContext } : {}),
1166
1222
  };
1167
1223
  // Store RecommendationState in memory so it's compatible with skyramp_recommend_tests if needed
@@ -1171,42 +1227,6 @@ export function registerAnalyzeChangesTool(server) {
1171
1227
  analysis: fullAnalysis,
1172
1228
  };
1173
1229
  storeSessionData(sessionId, recommendationState);
1174
- // ── Step 11: Build UnifiedAnalysisState and save ──
1175
- // fullAnalysis lives only in inMemorySessionStore (for MCP resources
1176
- // and registerRecommendTestsPrompt). The disk state carries only the
1177
- // slim fields that downstream tools (health, execute, actions) need.
1178
- // routerMountContext and candidateRouteFiles are computed here so they
1179
- // can be persisted to the state file for downstream tools (health, drift).
1180
- // Without them, analyzeTestHealth would work only off the static catalog
1181
- // which has wrong paths for nested resources and unsupported frameworks.
1182
- const routeLikeUnmatchedFiles = [];
1183
- for (const file of classifiedEndpoints?.unmatchedFiles ?? []) {
1184
- const routeLike = SOURCE_EXTS.test(file) &&
1185
- (ROUTE_FILE_PATTERN.test(file) || ROUTE_FILE_BASENAME_PATTERN.test(path.basename(file)));
1186
- if (routeLike && !(await isGraphQLFile(file, params.repositoryPath))) {
1187
- routeLikeUnmatchedFiles.push(file);
1188
- }
1189
- }
1190
- const shouldIncludeCandidateRouteFiles = analysisScope !== AnalysisScope.CurrentBranchDiff ||
1191
- rawRelatedEndpointCount === 0 ||
1192
- scannedEndpoints.length === 0 ||
1193
- routeLikeUnmatchedFiles.length > 0;
1194
- let candidateRouteFiles;
1195
- if (shouldIncludeCandidateRouteFiles) {
1196
- candidateRouteFiles = [];
1197
- for (const file of findCandidateRouteFiles(params.repositoryPath)) {
1198
- if (!(await isGraphQLFile(file, params.repositoryPath))) {
1199
- candidateRouteFiles.push(file);
1200
- }
1201
- }
1202
- }
1203
- // Write the full diff to a temp file before building state so the path
1204
- // can be persisted and read by analyzeTestHealthTool for per-line detection.
1205
- let diffFilePath;
1206
- if (diffData?.diffContent) {
1207
- diffFilePath = path.join(os.tmpdir(), `skyramp-diff-${sessionId}.diff`);
1208
- await fs.promises.writeFile(diffFilePath, diffData.diffContent, { encoding: "utf-8", mode: 0o600 });
1209
- }
1210
1230
  // Compute UI context from the diff's changed files using the shared
1211
1231
  // `isFrontendFile` classifier. Persisting this in the stateFile lets
1212
1232
  // skyramp_analyze_test_health and the recommendation prompt consume the
@@ -1299,6 +1319,7 @@ export function registerAnalyzeChangesTool(server) {
1299
1319
  sessionId,
1300
1320
  routerMountContext,
1301
1321
  candidateRouteFiles,
1322
+ routeDiscovery,
1302
1323
  relevantExternalTestPaths,
1303
1324
  },
1304
1325
  };
@@ -5,7 +5,7 @@ import { SkyrampClient } from "@skyramp/skyramp";
5
5
  import openProxyTerminalTracked from "../../utils/proxy-terminal.js";
6
6
  import { getEntryPoint } from "../../utils/telemetry.js";
7
7
  import { logger } from "../../utils/logger.js";
8
- import { basePlaywrightSchema, baseSchema, SESSION_STORAGE_FILENAME, } from "../../types/TestTypes.js";
8
+ import { basePlaywrightSchema, baseSchema, RuntimeEnvironment, SESSION_STORAGE_FILENAME, } from "../../types/TestTypes.js";
9
9
  import { AnalyticsService } from "../../services/AnalyticsService.js";
10
10
  import { resolveSessionPaths } from "./resolveSessionPaths.js";
11
11
  import { setSavedSessionPath } from "./sessionState.js";
@@ -66,8 +66,8 @@ For detailed documentation visit: https://www.skyramp.dev/docs/load-test/advance
66
66
  playwrightSaveStoragePath: basePlaywrightSchema.shape.playwrightSaveStoragePath,
67
67
  playwrightViewportSize: basePlaywrightSchema.shape.playwrightViewportSize,
68
68
  runtime: z
69
- .string()
70
- .default("docker")
69
+ .literal(RuntimeEnvironment.DOCKER)
70
+ .default(RuntimeEnvironment.DOCKER)
71
71
  .describe("Runtime environment for trace collection. Currently only 'docker' is supported and is used as the default."),
72
72
  include: z
73
73
  .array(z.string())
@@ -1,6 +1,5 @@
1
1
  import { z } from "zod";
2
- import { WorkspaceConfigManager, serviceSchema } from "../../workspace/workspace.js";
3
- import { createRequire } from "module";
2
+ import { WorkspaceConfigManager, serviceSchema, } from "../../workspace/workspace.js";
4
3
  import fs from "fs/promises";
5
4
  import yaml from "js-yaml";
6
5
  import { logger } from "../../utils/logger.js";
@@ -8,8 +7,6 @@ import { AnalyticsService } from "../../services/AnalyticsService.js";
8
7
  import { SKYRAMP_IMAGE_VERSION } from "../../utils/versions.js";
9
8
  import { validateAndConsumeScanToken } from "./initScanWorkspaceTool.js";
10
9
  import { upsertServicesByRepo } from "./serviceUpsert.js";
11
- const require = createRequire(import.meta.url);
12
- const MCP_VERSION = require("../../../package.json").version || "";
13
10
  function getExecutorVersion() {
14
11
  return SKYRAMP_IMAGE_VERSION;
15
12
  }
@@ -20,9 +17,7 @@ const TOOL_NAME = "skyramp_init_workspace";
20
17
  // set (incl. cookie/session/token) and the multi-repo `repository` field, so input
21
18
  // services are validated against it directly — no local schema extension needed.
22
19
  const initializeWorkspaceSchema = z.object({
23
- workspacePath: z
24
- .string()
25
- .describe("Path to workspace directory"),
20
+ workspacePath: z.string().describe("Path to workspace directory"),
26
21
  services: z
27
22
  .array(serviceSchema)
28
23
  .describe("Array of services discovered by skyramp_init_scan. Must contain at least one service."),
@@ -123,14 +118,12 @@ export function registerInitializeWorkspaceTool(server) {
123
118
  if (params.merge && alreadyExists) {
124
119
  config = await manager.read();
125
120
  config = await manager.updateMetadata({
126
- mcpVersion: MCP_VERSION,
127
121
  executorVersion: getExecutorVersion(),
128
122
  });
129
123
  }
130
124
  else {
131
125
  config = await manager.initialize();
132
126
  config = await manager.updateMetadata({
133
- mcpVersion: MCP_VERSION,
134
127
  executorVersion: getExecutorVersion(),
135
128
  });
136
129
  }