@skyramp/mcp 0.2.8 → 0.2.10-rc.1

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 (88) hide show
  1. package/build/index.js +1 -8
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
  4. package/build/prompts/personas.d.ts +2 -2
  5. package/build/prompts/personas.js +2 -2
  6. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +3 -1
  7. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +6 -1
  8. package/build/prompts/sut-setup/shared.d.ts +20 -0
  9. package/build/prompts/sut-setup/shared.js +67 -7
  10. package/build/prompts/test-maintenance/actionsInstructions.js +5 -1
  11. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +15 -2
  12. package/build/prompts/test-maintenance/drift-analysis-prompt.js +63 -3
  13. package/build/prompts/test-maintenance/driftAnalysisSections.js +7 -25
  14. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +20 -0
  15. package/build/prompts/test-maintenance/driftAnalysisShared.js +80 -0
  16. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +32 -0
  17. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +212 -0
  18. package/build/prompts/test-recommendation/recommendationShared.js +1 -1
  19. package/build/prompts/test-recommendation/scopeAssessment.js +12 -2
  20. package/build/prompts/testbot/testbot-prompts.js +10 -9
  21. package/build/resources/testbotResource.js +1 -1
  22. package/build/services/TestDiscoveryService.d.ts +12 -10
  23. package/build/services/TestDiscoveryService.js +137 -83
  24. package/build/services/TestDiscoveryService.test.js +370 -15
  25. package/build/services/TestExecutionService.d.ts +1 -1
  26. package/build/services/TestExecutionService.js +7 -7
  27. package/build/services/TestExecutionService.test.js +4 -1
  28. package/build/tools/executeSkyrampTestTool.js +40 -71
  29. package/build/tools/submitReportTool.d.ts +25 -25
  30. package/build/tools/submitReportTool.js +123 -34
  31. package/build/tools/submitReportTool.test.js +413 -94
  32. package/build/tools/test-management/actionsTool.js +177 -77
  33. package/build/tools/test-management/actionsTool.test.d.ts +1 -0
  34. package/build/tools/test-management/actionsTool.test.js +297 -0
  35. package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
  36. package/build/tools/test-management/analyzeChangesTool.js +74 -169
  37. package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
  38. package/build/tools/test-management/analyzeTestHealthTool.js +63 -22
  39. package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +1 -0
  40. package/build/tools/test-management/analyzeTestHealthTool.test.js +268 -0
  41. package/build/tools/workspace/initializeWorkspaceTool.js +37 -24
  42. package/build/tools/workspace/initializeWorkspaceTool.test.js +9 -4
  43. package/build/types/FrontendIntegration.d.ts +25 -0
  44. package/build/types/FrontendIntegration.js +19 -0
  45. package/build/types/RepositoryAnalysis.d.ts +8 -8
  46. package/build/types/TestAnalysis.d.ts +18 -49
  47. package/build/types/TestAnalysis.js +0 -28
  48. package/build/types/TestTypes.d.ts +3 -1
  49. package/build/types/TestTypes.js +3 -1
  50. package/build/types/TestbotReport.d.ts +1 -1
  51. package/build/utils/AnalysisStateManager.d.ts +16 -22
  52. package/build/utils/docker.test.js +1 -1
  53. package/build/utils/featureFlags.d.ts +1 -1
  54. package/build/utils/featureFlags.js +1 -1
  55. package/build/utils/frontendIntegration.d.ts +9 -0
  56. package/build/utils/frontendIntegration.js +237 -0
  57. package/build/utils/frontendIntegration.test.d.ts +1 -0
  58. package/build/utils/frontendIntegration.test.js +229 -0
  59. package/build/utils/pr-comment-parser.d.ts +3 -3
  60. package/build/utils/pr-comment-parser.js +6 -6
  61. package/build/utils/pr-comment-parser.test.js +3 -3
  62. package/build/utils/repoScanner.d.ts +7 -0
  63. package/build/utils/repoScanner.js +14 -6
  64. package/build/utils/repoScanner.test.js +9 -9
  65. package/build/utils/routeParsers.d.ts +32 -0
  66. package/build/utils/routeParsers.js +205 -2
  67. package/build/utils/routeParsers.test.js +279 -29
  68. package/build/utils/sourceRouteExtractor.js +174 -5
  69. package/build/utils/sourceRouteExtractor.test.js +173 -0
  70. package/build/utils/utils.d.ts +11 -0
  71. package/build/utils/utils.js +19 -0
  72. package/build/utils/utils.test.js +23 -1
  73. package/build/utils/versions.d.ts +3 -3
  74. package/build/utils/versions.js +1 -1
  75. package/build/workspace/workspace.d.ts +21 -21
  76. package/build/workspace/workspace.js +7 -4
  77. package/build/workspace/workspace.test.js +65 -6
  78. package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
  79. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  80. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
  81. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
  82. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
  83. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
  84. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
  85. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  86. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  87. package/node_modules/playwright/package.json +1 -1
  88. package/package.json +3 -3
@@ -0,0 +1,268 @@
1
+ // @ts-nocheck - Jest ESM type inference issues
2
+ import { jest, describe, it, expect, beforeEach } from "@jest/globals";
3
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/server/mcp.js", () => ({
4
+ McpServer: jest.fn(),
5
+ }));
6
+ jest.unstable_mockModule("@modelcontextprotocol/sdk/types.js", () => ({
7
+ CallToolResult: {},
8
+ }));
9
+ jest.unstable_mockModule("../../utils/logger.js", () => ({
10
+ logger: {
11
+ info: jest.fn(),
12
+ debug: jest.fn(),
13
+ error: jest.fn(),
14
+ warning: jest.fn(),
15
+ },
16
+ }));
17
+ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
18
+ AnalyticsService: { pushMCPToolEvent: jest.fn() },
19
+ }));
20
+ const mockBuildDriftAnalysisPrompt = jest.fn().mockImplementation((_stateFile, apiTests, ui) => {
21
+ const parts = [];
22
+ if ((apiTests?.length ?? 0) > 0 || !ui)
23
+ parts.push("<drift_analysis_rules>API drift</drift_analysis_rules>");
24
+ if (ui)
25
+ parts.push("<ui_drift_analysis_rules>UI drift</ui_drift_analysis_rules>");
26
+ return parts.join("\n\n");
27
+ });
28
+ jest.unstable_mockModule("../../prompts/test-maintenance/drift-analysis-prompt.js", () => ({
29
+ buildDriftAnalysisPrompt: mockBuildDriftAnalysisPrompt,
30
+ }));
31
+ const mockFromStatePath = jest.fn();
32
+ const mockCleanupOldFiles = jest.fn().mockImplementation(() => Promise.resolve());
33
+ jest.unstable_mockModule("../../utils/AnalysisStateManager.js", () => ({
34
+ StateManager: {
35
+ fromStatePath: mockFromStatePath,
36
+ cleanupOldFiles: mockCleanupOldFiles,
37
+ },
38
+ TestSource: { Skyramp: "skyramp", External: "external" },
39
+ }));
40
+ jest.unstable_mockModule("../../utils/utils.js", () => ({
41
+ toolError: (msg) => ({ content: [{ type: "text", text: `Error: ${msg}` }] }),
42
+ }));
43
+ const { registerAnalyzeTestHealthTool } = await import("./analyzeTestHealthTool.js");
44
+ // ---------------------------------------------------------------------------
45
+ // Helpers
46
+ // ---------------------------------------------------------------------------
47
+ function makeFakeServer() {
48
+ let capturedHandler = null;
49
+ let capturedSchema = null;
50
+ const server = {
51
+ registerTool: jest.fn((_name, config, handler) => {
52
+ capturedSchema = config.inputSchema;
53
+ capturedHandler = handler;
54
+ }),
55
+ getHandler: () => capturedHandler,
56
+ getSchema: () => capturedSchema,
57
+ };
58
+ return server;
59
+ }
60
+ function makeStateManager(stateData) {
61
+ return {
62
+ getStatePath: jest.fn().mockReturnValue("/tmp/skyramp-analysis-test.json"),
63
+ readData: jest.fn().mockResolvedValue(stateData),
64
+ readFullState: jest.fn().mockResolvedValue({ metadata: { repositoryPath: "/repo" } }),
65
+ // Multi-repo API added in SKYR-3786 — analyzeTestHealthTool now uses these.
66
+ readRepoData: jest.fn().mockResolvedValue(stateData),
67
+ getRepoRepositoryPath: jest.fn().mockResolvedValue("/repo"),
68
+ listRelatedRepos: jest.fn().mockResolvedValue([]),
69
+ };
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // Tests
73
+ // ---------------------------------------------------------------------------
74
+ describe("analyzeTestHealthTool — blueprintCaptured schema", () => {
75
+ it("registers blueprintCaptured as an optional boolean parameter", () => {
76
+ const server = makeFakeServer();
77
+ registerAnalyzeTestHealthTool(server);
78
+ const schema = server.getSchema();
79
+ expect(schema).toHaveProperty("blueprintCaptured");
80
+ expect(schema.blueprintCaptured.safeParse(undefined).success).toBe(true);
81
+ expect(schema.blueprintCaptured.safeParse(true).success).toBe(true);
82
+ });
83
+ });
84
+ describe("analyzeTestHealthTool — UI drift gate logic", () => {
85
+ let server;
86
+ beforeEach(() => {
87
+ jest.clearAllMocks();
88
+ server = makeFakeServer();
89
+ registerAnalyzeTestHealthTool(server);
90
+ });
91
+ it("includes UI drift when uiContext has changedFrontendFiles and a UI test", async () => {
92
+ const mockStateManager = makeStateManager({
93
+ existingTests: [
94
+ { testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
95
+ ],
96
+ uiContext: {
97
+ changedFrontendFiles: ["/repo/src/components/Cart.tsx"],
98
+ candidateUiPages: [],
99
+ },
100
+ });
101
+ mockFromStatePath.mockReturnValue(mockStateManager);
102
+ const result = await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
103
+ expect(result).not.toHaveProperty("isError", true);
104
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeDefined();
105
+ const testsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][2].tests;
106
+ expect(testsArg).toContainEqual(expect.objectContaining({ testFile: "/repo/tests/cart.spec.ts" }));
107
+ });
108
+ it("passes blueprintCaptured=true to buildUiDriftAnalysisPrompt", async () => {
109
+ const mockStateManager = makeStateManager({
110
+ existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" }],
111
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
112
+ });
113
+ mockFromStatePath.mockReturnValue(mockStateManager);
114
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
115
+ const blueprintArg = mockBuildDriftAnalysisPrompt.mock.calls[0][2].blueprintCaptured;
116
+ expect(blueprintArg).toBe(true);
117
+ });
118
+ it("passes blueprintCaptured=false to buildUiDriftAnalysisPrompt", async () => {
119
+ const mockStateManager = makeStateManager({
120
+ existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" }],
121
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
122
+ });
123
+ mockFromStatePath.mockReturnValue(mockStateManager);
124
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: false });
125
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeDefined();
126
+ const blueprintArg = mockBuildDriftAnalysisPrompt.mock.calls[0][2].blueprintCaptured;
127
+ expect(blueprintArg).toBe(false);
128
+ });
129
+ it("excludes UI drift when uiContext is absent", async () => {
130
+ const mockStateManager = makeStateManager({ existingTests: [] });
131
+ mockFromStatePath.mockReturnValue(mockStateManager);
132
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
133
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeUndefined();
134
+ });
135
+ it("excludes UI drift when changedFrontendFiles is empty", async () => {
136
+ const mockStateManager = makeStateManager({
137
+ existingTests: [{ testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui" }],
138
+ uiContext: { changedFrontendFiles: [], candidateUiPages: [] },
139
+ });
140
+ mockFromStatePath.mockReturnValue(mockStateManager);
141
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
142
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeUndefined();
143
+ });
144
+ it("includes UI drift (empty test list) when frontend changed but no UI tests found — grep pre-scan will discover them", async () => {
145
+ const mockStateManager = makeStateManager({
146
+ existingTests: [
147
+ { testFile: "/repo/tests/products_contract_test.py", source: "skyramp", testType: "contract", framework: "pytest" },
148
+ { testFile: "/repo/tests/orders_smoke_test.py", source: "skyramp", testType: "smoke", framework: "pytest" },
149
+ ],
150
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
151
+ });
152
+ mockFromStatePath.mockReturnValue(mockStateManager);
153
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
154
+ // UI drift fires even with 0 UI tests — grep pre-scan discovers component tests dynamically
155
+ const uiParam = mockBuildDriftAnalysisPrompt.mock.calls[0][2];
156
+ expect(uiParam).toBeDefined();
157
+ expect(uiParam.tests).toHaveLength(0);
158
+ expect(uiParam.changedFrontendFiles).toEqual(["/repo/src/Cart.tsx"]);
159
+ });
160
+ it("includes UI drift for RTL tests (framework=rtl, testType=unknown)", async () => {
161
+ const mockStateManager = makeStateManager({
162
+ existingTests: [
163
+ { testFile: "/repo/src/components/Cart.test.tsx", source: "external", testType: "unknown", framework: "rtl" },
164
+ { testFile: "/repo/tests/orders_smoke_test.py", source: "skyramp", testType: "smoke", framework: "pytest" },
165
+ ],
166
+ repositoryAnalysis: { relevantExternalTestPaths: ["src/components/Cart.test.tsx"] },
167
+ uiContext: { changedFrontendFiles: ["/repo/src/components/Cart.tsx"], candidateUiPages: [] },
168
+ });
169
+ mockFromStatePath.mockReturnValue(mockStateManager);
170
+ mockFromStatePath.mock.results[0]?.value?.readFullState?.mockResolvedValue({
171
+ metadata: { repositoryPath: "/repo" },
172
+ });
173
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: false });
174
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeDefined();
175
+ const testsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][2].tests;
176
+ expect(testsArg).toHaveLength(1);
177
+ expect(testsArg[0]).toMatchObject({ testFile: "/repo/src/components/Cart.test.tsx" });
178
+ });
179
+ it("excludes RTL test from API drift prompt when UI drift is running", async () => {
180
+ const mockStateManager = makeStateManager({
181
+ existingTests: [
182
+ { testFile: "/repo/src/components/Cart.test.tsx", source: "external", testType: "unknown", framework: "rtl" },
183
+ { testFile: "/repo/tests/orders_smoke_test.py", source: "skyramp", testType: "smoke", framework: "pytest" },
184
+ ],
185
+ repositoryAnalysis: { relevantExternalTestPaths: ["src/components/Cart.test.tsx"] },
186
+ uiContext: { changedFrontendFiles: ["/repo/src/components/Cart.tsx"], candidateUiPages: [] },
187
+ });
188
+ mockFromStatePath.mockReturnValue(mockStateManager);
189
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: false });
190
+ const apiTestsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][1];
191
+ expect(apiTestsArg).toHaveLength(1);
192
+ expect(apiTestsArg[0]).toMatchObject({ testFile: "/repo/tests/orders_smoke_test.py" });
193
+ });
194
+ it("passes only UI tests to buildUiDriftAnalysisPrompt when mixed test types exist", async () => {
195
+ const mockStateManager = makeStateManager({
196
+ existingTests: [
197
+ { testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
198
+ { testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
199
+ { testFile: "/repo/tests/smoke.py", source: "skyramp", testType: "smoke", framework: "pytest" },
200
+ ],
201
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
202
+ });
203
+ mockFromStatePath.mockReturnValue(mockStateManager);
204
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
205
+ expect(mockBuildDriftAnalysisPrompt.mock.calls[0][2]).toBeDefined();
206
+ const testsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][2].tests;
207
+ expect(testsArg).toHaveLength(1);
208
+ expect(testsArg[0]).toMatchObject({ testFile: "/repo/tests/cart.spec.ts" });
209
+ });
210
+ it("excludes UI tests from API drift prompt when UI drift is also running", async () => {
211
+ const mockStateManager = makeStateManager({
212
+ existingTests: [
213
+ { testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
214
+ { testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
215
+ ],
216
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
217
+ });
218
+ mockFromStatePath.mockReturnValue(mockStateManager);
219
+ await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
220
+ const apiTestsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][1];
221
+ expect(apiTestsArg).toHaveLength(1);
222
+ expect(apiTestsArg[0]).toMatchObject({ testFile: "/repo/tests/products_contract.py" });
223
+ });
224
+ it("includes UI tests in API drift prompt when no frontend changes", async () => {
225
+ const mockStateManager = makeStateManager({
226
+ existingTests: [
227
+ { testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
228
+ { testFile: "/repo/tests/products_contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
229
+ ],
230
+ });
231
+ mockFromStatePath.mockReturnValue(mockStateManager);
232
+ await server.getHandler()({ stateFile: "/tmp/state.json" });
233
+ // No frontend changes → no UI drift section → UI tests fall through to API drift
234
+ // so they are not silently dropped on API-only PRs.
235
+ const apiTestsArg = mockBuildDriftAnalysisPrompt.mock.calls[0][1];
236
+ expect(apiTestsArg).toHaveLength(2);
237
+ expect(apiTestsArg).toContainEqual(expect.objectContaining({ testFile: "/repo/tests/cart.spec.ts" }));
238
+ expect(apiTestsArg).toContainEqual(expect.objectContaining({ testFile: "/repo/tests/products_contract.py" }));
239
+ });
240
+ });
241
+ describe("analyzeTestHealthTool — prompt concatenation", () => {
242
+ let server;
243
+ beforeEach(() => {
244
+ jest.clearAllMocks();
245
+ server = makeFakeServer();
246
+ registerAnalyzeTestHealthTool(server);
247
+ });
248
+ it("concatenates API and UI drift prompts when both are included", async () => {
249
+ const mockStateManager = makeStateManager({
250
+ existingTests: [
251
+ { testFile: "/repo/tests/cart.spec.ts", source: "skyramp", testType: "ui", framework: "playwright-ui" },
252
+ { testFile: "/repo/tests/contract.py", source: "skyramp", testType: "contract", framework: "pytest" },
253
+ ],
254
+ uiContext: { changedFrontendFiles: ["/repo/src/Cart.tsx"], candidateUiPages: [] },
255
+ });
256
+ mockFromStatePath.mockReturnValue(mockStateManager);
257
+ const result = await server.getHandler()({ stateFile: "/tmp/state.json", blueprintCaptured: true });
258
+ expect(result.structuredContent?.prompt).toContain("API drift");
259
+ expect(result.structuredContent?.prompt).toContain("UI drift");
260
+ });
261
+ it("returns only API drift prompt when uiContext is absent", async () => {
262
+ const mockStateManager = makeStateManager({ existingTests: [] });
263
+ mockFromStatePath.mockReturnValue(mockStateManager);
264
+ const result = await server.getHandler()({ stateFile: "/tmp/state.json" });
265
+ expect(result.structuredContent?.prompt).toContain("API drift");
266
+ expect(result.structuredContent?.prompt).not.toContain("UI drift");
267
+ });
268
+ });
@@ -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,19 +17,18 @@ 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."),
29
24
  scanToken: z
30
25
  .string()
31
- .describe("Token returned by skyramp_init_scan. Required to confirm the repo was scanned before initializing."),
26
+ .optional()
27
+ .describe("Token returned by skyramp_init_scan. Required on a fresh init (no existing .skyramp/workspace.yml) to confirm the repo was scanned before initializing. Omit when editing an existing workspace.yml — pair `force: true` with the full updated `services` array instead."),
32
28
  force: z
33
29
  .boolean()
34
30
  .default(false)
35
- .describe("Set to true to overwrite an existing workspace file. Use when the user explicitly requests it, or when recovering from a workspace validation failure (schema mismatch, unknown fields). Default is false."),
31
+ .describe("Set to true to overwrite an existing workspace.yml. Use for edits (e.g. updating `runtimeDetails.serverStartCommand` after generating SUT files): pass the full updated `services` array — this is an overwrite, not a merge, so any field not present is dropped. Also use when recovering from a validation failure. Required when `scanToken` is omitted. Default is false."),
36
32
  merge: z
37
33
  .boolean()
38
34
  .default(false)
@@ -54,8 +50,11 @@ async function writeWorkspaceConfig(manager, config) {
54
50
  }
55
51
  export function registerInitializeWorkspaceTool(server) {
56
52
  server.registerTool(TOOL_NAME, {
57
- description: "PREREQUISITE: Call skyramp_init_scan first to get a scanToken and discover services. " +
58
- "Then call this tool to write .skyramp/workspace.yml with the discovered services and scanToken.",
53
+ description: "Write or update .skyramp/workspace.yml. Always use this tool for any change to that file do NOT use Edit/Write directly. " +
54
+ "1. Fresh init: PREREQUISITE: Call skyramp_init_scan first to get a scanToken and discover services. " +
55
+ "Then call this tool to write .skyramp/workspace.yml with the discovered services and scanToken. " +
56
+ "2. Edit existing workspace.yml: omit scanToken, set force: true, and pass the FULL updated services array (overwrite, not merge — fields not in the array are dropped). " +
57
+ "The services schema is strict: any field not in the schema (e.g. inventing `serverStopCommand` on `runtimeDetails`) fails validation and the call is rejected.",
59
58
  inputSchema: initializeWorkspaceSchema.shape,
60
59
  _meta: {
61
60
  keywords: [
@@ -93,17 +92,33 @@ export function registerInitializeWorkspaceTool(server) {
93
92
  isError: false,
94
93
  };
95
94
  }
96
- // Validate scan token
97
- if (!validateAndConsumeScanToken(params.scanToken)) {
98
- return {
99
- content: [
100
- {
101
- type: "text",
102
- text: "Invalid or expired scanToken. Call skyramp_init_scan first to get a valid scanToken and scanning instructions.",
103
- },
104
- ],
105
- isError: true,
106
- };
95
+ // scanToken is required for fresh init; for edits (force:true on an
96
+ // existing workspace.yml) the caller supplies the full services array
97
+ // directly and we skip the token check.
98
+ const isEdit = alreadyExists && params.force;
99
+ if (!isEdit) {
100
+ if (!params.scanToken) {
101
+ return {
102
+ content: [
103
+ {
104
+ type: "text",
105
+ text: "scanToken is required for a fresh init. Call skyramp_init_scan first to get a scanToken and scanning instructions. To edit an existing workspace.yml, set force: true and omit scanToken.",
106
+ },
107
+ ],
108
+ isError: true,
109
+ };
110
+ }
111
+ if (!validateAndConsumeScanToken(params.scanToken)) {
112
+ return {
113
+ content: [
114
+ {
115
+ type: "text",
116
+ text: "Invalid or expired scanToken. Call skyramp_init_scan first to get a valid scanToken and scanning instructions.",
117
+ },
118
+ ],
119
+ isError: true,
120
+ };
121
+ }
107
122
  }
108
123
  if (!params.services || params.services.length === 0) {
109
124
  return {
@@ -123,14 +138,12 @@ export function registerInitializeWorkspaceTool(server) {
123
138
  if (params.merge && alreadyExists) {
124
139
  config = await manager.read();
125
140
  config = await manager.updateMetadata({
126
- mcpVersion: MCP_VERSION,
127
141
  executorVersion: getExecutorVersion(),
128
142
  });
129
143
  }
130
144
  else {
131
145
  config = await manager.initialize();
132
146
  config = await manager.updateMetadata({
133
- mcpVersion: MCP_VERSION,
134
147
  executorVersion: getExecutorVersion(),
135
148
  });
136
149
  }
@@ -36,7 +36,9 @@ describe("initializeWorkspaceTool", () => {
36
36
  // Build a mock WorkspaceConfigManager
37
37
  mockManager = {
38
38
  exists: jest.fn().mockResolvedValue(false),
39
- getConfigPath: jest.fn().mockReturnValue(path.join(tmpDir, ".skyramp/workspace.yml")),
39
+ getConfigPath: jest
40
+ .fn()
41
+ .mockReturnValue(path.join(tmpDir, ".skyramp/workspace.yml")),
40
42
  initialize: jest.fn().mockResolvedValue({ workspace: {}, services: [] }),
41
43
  updateMetadata: jest.fn().mockImplementation(async (data) => ({
42
44
  workspace: {},
@@ -95,7 +97,6 @@ describe("initializeWorkspaceTool", () => {
95
97
  expect(result.content[0].text).toContain("Workspace initialized");
96
98
  expect(mockManager.initialize).toHaveBeenCalled();
97
99
  expect(mockManager.updateMetadata).toHaveBeenCalledWith(expect.objectContaining({
98
- mcpVersion: expect.any(String),
99
100
  executorVersion: expect.any(String),
100
101
  }));
101
102
  });
@@ -103,7 +104,9 @@ describe("initializeWorkspaceTool", () => {
103
104
  mockManager.exists.mockResolvedValue(true);
104
105
  const result = await handler({
105
106
  workspacePath: tmpDir,
106
- services: [{ serviceName: "svc", language: "python", testDirectory: "tests" }],
107
+ services: [
108
+ { serviceName: "svc", language: "python", testDirectory: "tests" },
109
+ ],
107
110
  scanToken: "valid-token",
108
111
  force: false,
109
112
  });
@@ -114,7 +117,9 @@ describe("initializeWorkspaceTool", () => {
114
117
  validateAndConsumeScanToken.mockReturnValue(false);
115
118
  const result = await handler({
116
119
  workspacePath: tmpDir,
117
- services: [{ serviceName: "svc", language: "python", testDirectory: "tests" }],
120
+ services: [
121
+ { serviceName: "svc", language: "python", testDirectory: "tests" },
122
+ ],
118
123
  scanToken: "bad-token",
119
124
  force: false,
120
125
  });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Frontend component integration check types — SKYR-3855.
3
+ *
4
+ * Produced by `checkFrontendFileIntegration` (src/utils/frontendIntegration.ts)
5
+ * and surfaced to the agent as `uiContext.frontendFileIntegration` in the
6
+ * `skyramp_analyze_changes` output and state file.
7
+ */
8
+ /** String enum so the JSON wire format (state file, tool output) stays unchanged. */
9
+ export declare enum IntegrationReason {
10
+ /** Framework route/page/entrypoint file — reachable by convention, grep skipped. */
11
+ RouteOrEntrypoint = "route-or-entrypoint",
12
+ /** At least one production file imports/references the component's module path. */
13
+ Imported = "imported",
14
+ /** No production importer found — the component has no DOM presence in the running app. */
15
+ NoImporters = "no-importers",
16
+ /** The check could not run (missing file, scan failure) — reported integrated (fail-open). */
17
+ ScanError = "scan-error"
18
+ }
19
+ export interface FrontendFileIntegration {
20
+ file: string;
21
+ integrated: boolean;
22
+ /** Production files that import/reference the component, relative to repositoryPath. Capped at 10. */
23
+ importers: string[];
24
+ reason: IntegrationReason;
25
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Frontend component integration check types — SKYR-3855.
3
+ *
4
+ * Produced by `checkFrontendFileIntegration` (src/utils/frontendIntegration.ts)
5
+ * and surfaced to the agent as `uiContext.frontendFileIntegration` in the
6
+ * `skyramp_analyze_changes` output and state file.
7
+ */
8
+ /** String enum so the JSON wire format (state file, tool output) stays unchanged. */
9
+ export var IntegrationReason;
10
+ (function (IntegrationReason) {
11
+ /** Framework route/page/entrypoint file — reachable by convention, grep skipped. */
12
+ IntegrationReason["RouteOrEntrypoint"] = "route-or-entrypoint";
13
+ /** At least one production file imports/references the component's module path. */
14
+ IntegrationReason["Imported"] = "imported";
15
+ /** No production importer found — the component has no DOM presence in the running app. */
16
+ IntegrationReason["NoImporters"] = "no-importers";
17
+ /** The check could not run (missing file, scan failure) — reported integrated (fail-open). */
18
+ IntegrationReason["ScanError"] = "scan-error";
19
+ })(IntegrationReason || (IntegrationReason = {}));
@@ -1195,7 +1195,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1195
1195
  description: string;
1196
1196
  priority: "high" | "medium" | "low";
1197
1197
  scenarioName: string;
1198
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1198
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1199
1199
  steps: {
1200
1200
  path: string;
1201
1201
  method: string;
@@ -1232,7 +1232,7 @@ export declare const draftedScenarioSchema: z.ZodObject<{
1232
1232
  description: string;
1233
1233
  priority: "high" | "medium" | "low";
1234
1234
  scenarioName: string;
1235
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1235
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1236
1236
  steps: {
1237
1237
  path: string;
1238
1238
  method: string;
@@ -1644,7 +1644,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1644
1644
  description: string;
1645
1645
  priority: "high" | "medium" | "low";
1646
1646
  scenarioName: string;
1647
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1647
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1648
1648
  steps: {
1649
1649
  path: string;
1650
1650
  method: string;
@@ -1681,7 +1681,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1681
1681
  description: string;
1682
1682
  priority: "high" | "medium" | "low";
1683
1683
  scenarioName: string;
1684
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1684
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1685
1685
  steps: {
1686
1686
  path: string;
1687
1687
  method: string;
@@ -1724,7 +1724,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1724
1724
  description: string;
1725
1725
  priority: "high" | "medium" | "low";
1726
1726
  scenarioName: string;
1727
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1727
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1728
1728
  steps: {
1729
1729
  path: string;
1730
1730
  method: string;
@@ -1767,7 +1767,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
1767
1767
  description: string;
1768
1768
  priority: "high" | "medium" | "low";
1769
1769
  scenarioName: string;
1770
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1770
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
1771
1771
  steps: {
1772
1772
  path: string;
1773
1773
  method: string;
@@ -2629,7 +2629,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2629
2629
  description: string;
2630
2630
  priority: "high" | "medium" | "low";
2631
2631
  scenarioName: string;
2632
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2632
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2633
2633
  steps: {
2634
2634
  path: string;
2635
2635
  method: string;
@@ -2831,7 +2831,7 @@ export declare const repositoryAnalysisSchema: z.ZodObject<{
2831
2831
  description: string;
2832
2832
  priority: "high" | "medium" | "low";
2833
2833
  scenarioName: string;
2834
- category: "breaking_change" | "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2834
+ category: "new_endpoint" | "bug_caught" | "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
2835
2835
  steps: {
2836
2836
  path: string;
2837
2837
  method: string;
@@ -1,45 +1,10 @@
1
1
  import { TestExecutionResult } from "./TestExecution.js";
2
+ import { TestType } from "./TestTypes.js";
2
3
  export declare enum RecommendationPriority {
3
4
  High = "high",
4
5
  Medium = "medium",
5
6
  Low = "low"
6
7
  }
7
- export declare enum IssueSeverity {
8
- Low = "low",
9
- Medium = "medium",
10
- High = "high",
11
- Critical = "critical"
12
- }
13
- export declare enum DriftChangeType {
14
- EndpointAdded = "endpoint_added",
15
- EndpointRemoved = "endpoint_removed",
16
- EndpointRenamed = "endpoint_renamed",
17
- EndpointModified = "endpoint_modified",
18
- AuthenticationChanged = "authentication_changed",
19
- SchemaChanges = "schema_changes",
20
- RouteChanged = "route_changed",
21
- RouteAdded = "route_added",
22
- RouteRemoved = "route_removed",
23
- UiComponentAdded = "ui_component_added",
24
- UiComponentRemoved = "ui_component_removed",
25
- UiComponentModified = "ui_component_modified",
26
- UiComponentRestructured = "ui_component_restructured",
27
- DependencyChanged = "dependency_changed",
28
- FunctionChanged = "function_changed",
29
- ClassChanged = "class_changed",
30
- BreakingChange = "breaking_change",
31
- CodeChange = "code_change"
32
- }
33
- export interface DriftChange {
34
- type: DriftChangeType;
35
- file: string;
36
- description: string;
37
- severity: IssueSeverity;
38
- details?: string;
39
- }
40
- export interface AffectedFiles {
41
- files: string[];
42
- }
43
8
  /** Origin of a test file — whether it was generated by Skyramp or is user/third-party maintained. */
44
9
  export declare enum TestSource {
45
10
  Skyramp = "skyramp",
@@ -59,12 +24,23 @@ export declare enum EstimatedWork {
59
24
  Medium = "Medium",
60
25
  Large = "Large"
61
26
  }
62
- /** Normalized internal recommendation built from LLM-supplied args.recommendations. */
63
- export interface DriftRecommendation {
64
- testFile: string;
27
+ /** The maintenance-decision fields shared by every stage of the test-maintenance
28
+ * pipeline (skyramp_actions' recommendations, the state's maintenanceVerdicts, and
29
+ * the report's testMaintenance rows) — one canonical shape instead of each stage
30
+ * redeclaring the same five fields under its own name. */
31
+ export interface MaintenanceActionCore {
32
+ testFilePath: string;
65
33
  action: DriftAction;
66
- priority: RecommendationPriority;
34
+ testType: TestType;
35
+ endpoint: string;
67
36
  rationale: string;
37
+ }
38
+ /** Normalized internal recommendation built from LLM-supplied args.recommendations. */
39
+ export interface DriftRecommendation extends MaintenanceActionCore {
40
+ /** When selectors are abstracted into a page object, pomFile is the POM path to edit.
41
+ * testFilePath remains the spec (for catalog lookup and report); pomFile is what gets patched. */
42
+ pomFile?: string;
43
+ priority: RecommendationPriority;
68
44
  estimatedWork: EstimatedWork;
69
45
  updateInstructions: string;
70
46
  renamedEndpoints: Array<{
@@ -80,18 +56,11 @@ export interface DriftRecommendation {
80
56
  */
81
57
  export interface TestAnalysisResult {
82
58
  testFile: string;
83
- testType: string;
59
+ testType: TestType;
84
60
  language: string;
85
61
  framework: string;
86
- apiSchema?: string;
87
62
  apiEndpoint?: string;
88
- /** Origin of this test. Defaults to TestSource.Skyramp for backwards compatibility
89
- * with existing state files. */
90
- source?: TestSource;
91
- drift?: {
92
- changes: DriftChange[];
93
- affectedFiles: AffectedFiles;
94
- };
63
+ source: TestSource;
95
64
  executionBefore?: TestExecutionResult;
96
65
  executionAfter?: TestExecutionResult;
97
66
  }