@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
@@ -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;
@@ -39,10 +39,10 @@ export declare const pageContextSchema: z.ZodObject<{
39
39
  export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
40
40
  testId: z.ZodString;
41
41
  testType: z.ZodNativeEnum<typeof TestType>;
42
- category: z.ZodEffects<z.ZodEnum<["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>, "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud", unknown>;
42
+ category: z.ZodEffects<z.ZodEnum<["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>, "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud", unknown>;
43
43
  endpoint: z.ZodString;
44
44
  fileName: z.ZodString;
45
- description: z.ZodOptional<z.ZodString>;
45
+ description: z.ZodString;
46
46
  scenarioFile: z.ZodOptional<z.ZodString>;
47
47
  traceFile: z.ZodOptional<z.ZodString>;
48
48
  frontendTrace: z.ZodOptional<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;
@@ -84,13 +84,13 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
84
84
  pageHash?: string | undefined;
85
85
  }>>;
86
86
  }, "strip", z.ZodTypeAny, {
87
+ description: string;
87
88
  testType: TestType;
88
- category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
89
+ category: "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
89
90
  fileName: string;
90
91
  endpoint: string;
91
92
  testId: string;
92
93
  reasoning: string;
93
- description?: string | undefined;
94
94
  repository?: string | undefined;
95
95
  traceFile?: string | undefined;
96
96
  scenarioFile?: string | undefined;
@@ -102,19 +102,19 @@ 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;
109
109
  pageHash?: string | undefined;
110
110
  } | undefined;
111
111
  }, {
112
+ description: string;
112
113
  testType: TestType;
113
114
  fileName: string;
114
115
  endpoint: string;
115
116
  testId: string;
116
117
  reasoning: string;
117
- description?: string | undefined;
118
118
  repository?: string | undefined;
119
119
  category?: unknown;
120
120
  traceFile?: string | undefined;
@@ -127,20 +127,20 @@ 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;
134
134
  pageHash?: string | undefined;
135
135
  } | undefined;
136
136
  }>, {
137
+ description: string;
137
138
  testType: TestType;
138
- category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
139
+ category: "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
139
140
  fileName: string;
140
141
  endpoint: string;
141
142
  testId: string;
142
143
  reasoning: string;
143
- description?: string | undefined;
144
144
  repository?: string | undefined;
145
145
  traceFile?: string | undefined;
146
146
  scenarioFile?: string | undefined;
@@ -152,19 +152,19 @@ 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;
159
159
  pageHash?: string | undefined;
160
160
  } | undefined;
161
161
  }, {
162
+ description: string;
162
163
  testType: TestType;
163
164
  fileName: string;
164
165
  endpoint: string;
165
166
  testId: string;
166
167
  reasoning: string;
167
- description?: string | undefined;
168
168
  repository?: string | undefined;
169
169
  category?: unknown;
170
170
  traceFile?: string | undefined;
@@ -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;
@@ -187,7 +187,7 @@ export declare const newTestSchema: z.ZodEffects<z.ZodObject<{
187
187
  export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
188
188
  testId: z.ZodString;
189
189
  testType: z.ZodNativeEnum<typeof TestType>;
190
- category: z.ZodEffects<z.ZodEnum<["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>, "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud", unknown>;
190
+ category: z.ZodEffects<z.ZodEnum<["business_rule", "security_boundary", "data_integrity", "breaking_change", "auth", "error_handling", "workflow", "data_validation", "crud"]>, "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud", unknown>;
191
191
  primaryEndpoint: z.ZodOptional<z.ZodString>;
192
192
  scenarioName: z.ZodOptional<z.ZodString>;
193
193
  steps: z.ZodArray<z.ZodObject<{
@@ -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;
@@ -258,7 +258,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
258
258
  description: string;
259
259
  priority: "high" | "medium" | "low";
260
260
  testType: TestType;
261
- category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
261
+ category: "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
262
262
  steps: {
263
263
  description: string;
264
264
  path?: string | undefined;
@@ -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;
@@ -326,7 +326,7 @@ export declare const additionalRecommendationSchema: z.ZodEffects<z.ZodObject<{
326
326
  description: string;
327
327
  priority: "high" | "medium" | "low";
328
328
  testType: TestType;
329
- category: "breaking_change" | "business_rule" | "security_boundary" | "data_integrity" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
329
+ category: "business_rule" | "security_boundary" | "data_integrity" | "breaking_change" | "auth" | "error_handling" | "workflow" | "data_validation" | "crud";
330
330
  steps: {
331
331
  description: string;
332
332
  path?: string | undefined;
@@ -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;
@@ -8,7 +8,7 @@ import { TestType, HttpMethod } from "../types/TestTypes.js";
8
8
  import { DriftAction } from "../types/TestAnalysis.js";
9
9
  import { TestExecutionStatus } from "../types/TestExecution.js";
10
10
  import { StateManager } from "../utils/AnalysisStateManager.js";
11
- import { toolError } from "../utils/utils.js";
11
+ import { toolError, testFileMatches } from "../utils/utils.js";
12
12
  // Drift actions that actually modify a test file. VERIFY and IGNORE are
13
13
  // no-ops (the test was assessed but left unchanged), so they must not count
14
14
  // toward "tests maintained" telemetry.
@@ -77,7 +77,7 @@ export const newTestSchema = z.object({
77
77
  category: z.preprocess((val) => externalCategory(val), z.enum(TEST_CATEGORIES)).describe("Test category — critical categories (security_boundary, business_rule, data_integrity, breaking_change) get generation priority over workflow"),
78
78
  endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
79
79
  fileName: z.string().describe("Name of the generated test file"),
80
- description: z.string().optional().describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
80
+ description: z.string().trim().min(1).describe("What the test does — the steps and assertions, not the bugs it finds. e.g. 'Creates a collection, adds a link, then verifies the link exists'. Do NOT describe expected failures or bugs here — those belong in issuesFound."),
81
81
  scenarioFile: z.string().optional().describe("Path to the scenario JSON file if one was generated (e.g. 'tests/scenario_collections-links.json')"),
82
82
  traceFile: z.string().optional().describe("Path to the backend trace file if used or created"),
83
83
  frontendTrace: z.string().optional().describe("Path to the Playwright/UI trace file if used or created"),
@@ -149,7 +149,7 @@ export const newTestSchema = z.object({
149
149
  }
150
150
  }
151
151
  });
152
- const descriptionSchema = z.object({
152
+ const issueFoundSchema = z.object({
153
153
  description: z.string().describe("One-line description. Do NOT prefix with the severity level — severity is a separate field. Include code logic bugs from the diff, test generation/execution failures, and environment misconfiguration."),
154
154
  severity: z
155
155
  .enum(["critical", "high", "medium", "low"])
@@ -244,10 +244,14 @@ export const additionalRecommendationSchema = z.object({
244
244
  }
245
245
  }
246
246
  });
247
+ // TODO(multi-repo maintenance): no `repository` field yet — see readData() TODO below.
247
248
  const testMaintenanceSchema = z.object({
248
249
  testType: z.nativeEnum(TestType).describe("Type of test."),
249
250
  endpoint: z.string().describe("HTTP verb and path, e.g. 'GET /api/v1/products'"),
250
- fileName: z.string().describe("Test file that was maintained, e.g. 'products_smoke_test.py'"),
251
+ testFilePath: z
252
+ .string()
253
+ .refine((p) => path.isAbsolute(p), { message: "testFilePath must be an absolute path" })
254
+ .describe("Absolute path of the test file that was maintained, e.g. '/repo/tests/products_smoke_test.py' — the same path you passed to skyramp_execute_test's testFile param. Consumers should basename this for display."),
251
255
  action: z.nativeEnum(DriftAction).describe("The drift action assigned to this test during maintenance triage."),
252
256
  description: z.string().describe("What was changed and why"),
253
257
  beforeDetails: z.string().describe("One line only — no embedded newlines, no raw HTTP headers or JSON blobs. " +
@@ -269,6 +273,14 @@ const testMaintenanceLLMSchema = testMaintenanceSchema.omit({
269
273
  beforeStatus: true,
270
274
  afterStatus: true,
271
275
  });
276
+ // LLM only drafts beforeDetails/afterDetails per file — action/testType/endpoint/description
277
+ // come from stateData.maintenanceVerdicts. Picked from testMaintenanceLLMSchema to avoid
278
+ // redeclaring those fields.
279
+ const testMaintenanceDetailSchema = testMaintenanceLLMSchema.pick({
280
+ testFilePath: true,
281
+ beforeDetails: true,
282
+ afterDetails: true,
283
+ });
272
284
  /**
273
285
  * Derive per-run analytics counts from a submitted report. These power the
274
286
  * alpha-launch dashboards (tests generated/maintained, suite growth, bugs vs
@@ -327,6 +339,12 @@ function deduplicateById(items) {
327
339
  }
328
340
  export function registerSubmitReportTool(server) {
329
341
  server.registerTool(TOOL_NAME, {
342
+ annotations: {
343
+ readOnlyHint: false,
344
+ destructiveHint: true, // overwrites summaryOutputFile on disk
345
+ idempotentHint: false,
346
+ openWorldHint: false,
347
+ },
330
348
  description: "Submit the final testbot report. Call this tool once after completing all test analysis, generation, and execution. " +
331
349
  "This is the ONLY way to submit the report — do NOT write the report to a file manually.",
332
350
  inputSchema: {
@@ -335,7 +353,8 @@ export function registerSubmitReportTool(server) {
335
353
  .describe("The file path where the report should be written (provided in the task instructions)"),
336
354
  businessCaseAnalysis: z
337
355
  .string()
338
- .describe("1-2 sentences describing what user-facing interactions this PR enables or changes (e.g. 'customers can now leave and view product reviews'). Focus on the user journey, not technical implementation. Flag backend-only or frontend-only gaps."),
356
+ .min(1, "businessCaseAnalysis is required describe what the PR does, even for no-test runs (config-only, non-application, Testbot onboarding). It is never optional and must not be empty.")
357
+ .describe("1-2 sentences describing what user-facing interactions this PR enables or changes (e.g. 'customers can now leave and view product reviews'). Focus on the user journey, not technical implementation. Flag backend-only or frontend-only gaps. Always required — even when no tests apply (config-only diffs, Testbot onboarding PRs); describe the PR's purpose and why no tests apply."),
339
358
  newTestsCreated: z
340
359
  .array(newTestSchema)
341
360
  .describe("List of new tests created. Use empty array [] if none."),
@@ -344,15 +363,15 @@ export function registerSubmitReportTool(server) {
344
363
  .optional()
345
364
  .default([])
346
365
  .describe("Recommended tests that were not generated (lower priority). Only include recommendations that add distinct coverage beyond generated tests — do not pad with variants testing the same endpoint and flow."),
347
- testMaintenance: z
348
- .array(testMaintenanceLLMSchema)
366
+ testMaintenanceDetails: z
367
+ .array(testMaintenanceDetailSchema)
349
368
  .optional()
350
- .describe("One entry per test assessed in the drift analysis step, including IGNORE and VERIFY entries these provide triage transparency even when no edits were needed. Omit this field entirely when no existing tests were found."),
369
+ .describe("One entry per maintained test that had a before/after execution recorded, with beforeDetails/afterDetails drafted from what you actually saw when you ran it. Omit an entry entirely when no execution was recorded for that test."),
351
370
  testResults: z
352
371
  .array(testResultSchema)
353
372
  .describe("List of ALL test execution results. One entry per test executed."),
354
373
  issuesFound: z
355
- .array(descriptionSchema)
374
+ .array(issueFoundSchema)
356
375
  .describe("List of issues, failures, or bugs found. Use empty array [] if none."),
357
376
  nextSteps: z
358
377
  .array(z.string())
@@ -360,6 +379,7 @@ export function registerSubmitReportTool(server) {
360
379
  .default([])
361
380
  .describe("Actionable follow-ups for the PR author. Each entry must be a single-line string (no embedded newlines). " +
362
381
  "Include a next step for every critical/high severity issue in issuesFound. No next steps for low-severity issues. " +
382
+ "Also include a next step for every external test (Playwright, Cypress, RTL) assigned REGENERATE or DELETE in testMaintenance — the developer must act on these manually since they cannot be auto-applied (e.g. 'Regenerate frontend/tests/cart_pom.spec.ts — CartLine structure changed, all selectors need re-recording' or 'Delete frontend/tests/homepage.spec.ts — /cart route removed'). " +
363
383
  "If multiple tests fail with 404 or connection refused: suggest checking targetSetupCommand/targetReadyCheckCommand. " +
364
384
  "If 401/403 on auth endpoints: suggest authTokenCommand. " +
365
385
  "When referencing code, use file name and relevant code pattern — no line numbers unless certain."),
@@ -385,31 +405,93 @@ export function registerSubmitReportTool(server) {
385
405
  let errorResult;
386
406
  const dedupedNewTests = deduplicateById([...params.newTestsCreated]);
387
407
  const dedupedRecommendations = deduplicateById([...(params.additionalRecommendations ?? [])]);
408
+ const stateManager = StateManager.fromStatePath(params.stateFile);
409
+ let stateData;
410
+ try {
411
+ // Only reads the primary repo — see the related-repo check below for why related
412
+ // repos' maintenanceVerdicts are never folded in here.
413
+ stateData = await stateManager.readData();
414
+ }
415
+ catch (err) {
416
+ errorResult = toolError(`Failed to load stateFile: ${err.message}`);
417
+ return errorResult;
418
+ }
419
+ if (!stateData) {
420
+ errorResult = toolError(`stateFile not found or empty: ${params.stateFile}`);
421
+ return errorResult;
422
+ }
423
+ // undefined + existing tests present means skyramp_actions was never called this
424
+ // run — not "no maintenance needed" (that case writes an empty array). Reject rather
425
+ // than silently reporting an empty testMaintenance section indistinguishable from a
426
+ // real "nothing to do". With zero existing tests there was nothing to lose, so
427
+ // undefined is safe there — no round-trip through skyramp_actions required.
428
+ if (stateData.maintenanceVerdicts === undefined && (stateData.existingTests?.length ?? 0) > 0) {
429
+ errorResult = toolError("stateFile has existingTests but no maintenanceVerdicts — skyramp_actions was not called for this run. " +
430
+ "Call skyramp_actions (with recommendations: [] if no existing tests needed action) before skyramp_submit_report.");
431
+ return errorResult;
432
+ }
433
+ // TestbotReport.testMaintenance has no repository field, so a related repo's
434
+ // reconciled verdicts have nowhere to go in this report. Fail loud instead of
435
+ // silently omitting real maintenance work skyramp_actions already did and persisted.
436
+ // Multi-repo maintenance reporting is a separate, unscoped feature (would also need
437
+ // testbot prompt / drift-analysis / test-bot.git renderer changes) — not done here.
438
+ const relatedReposWithVerdicts = [];
439
+ for (const repo of await stateManager.listRelatedRepos()) {
440
+ const relatedData = await stateManager.readRepoData(repo);
441
+ if ((relatedData?.maintenanceVerdicts?.length ?? 0) > 0)
442
+ relatedReposWithVerdicts.push(repo);
443
+ }
444
+ if (relatedReposWithVerdicts.length > 0) {
445
+ errorResult = toolError(`Related repo(s) have reconciled maintenance verdicts that cannot be included in this report yet ` +
446
+ `(multi-repo maintenance reporting is not supported — testMaintenance has no repository field): ${relatedReposWithVerdicts.join(", ")}.`);
447
+ return errorResult;
448
+ }
449
+ // action/testType/endpoint/description come entirely from stateData.maintenanceVerdicts
450
+ // (written by skyramp_actions right after drift analysis) — the LLM never supplies
451
+ // testMaintenance directly, so there's nothing to re-transcribe here.
452
+ const rawMaintenance = (stateData.maintenanceVerdicts ?? []).map((v) => ({
453
+ testFilePath: v.testFilePath,
454
+ testType: v.testType,
455
+ endpoint: v.endpoint,
456
+ action: v.action,
457
+ description: v.rationale,
458
+ beforeDetails: "",
459
+ afterDetails: "",
460
+ }));
388
461
  let testMaintenance;
389
- if (params.testMaintenance) {
390
- try {
391
- const stateData = await StateManager.fromStatePath(params.stateFile).readData();
392
- if (!stateData) {
393
- errorResult = toolError(`stateFile not found or empty: ${params.stateFile}`);
394
- return errorResult;
395
- }
396
- // Status fields are stateFile-authoritative. Details are LLM-supplied when present
397
- // (single-line, with root-cause context) and fall back to server-side extraction.
398
- testMaintenance = params.testMaintenance.map((m) => {
399
- const recorded = stateData.existingTests?.find((t) => t.testFile.endsWith(m.fileName) || path.basename(t.testFile) === path.basename(m.fileName));
400
- const defaultBeforeStatus = MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
401
- const defaultAfterStatus = m.action === DriftAction.Delete ? TestExecutionStatus.Skipped
402
- : MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
403
- const beforeStatus = recorded?.executionBefore?.status ?? defaultBeforeStatus;
404
- const afterStatus = recorded?.executionAfter?.status ?? defaultAfterStatus;
405
- logger.info(`${m.fileName}: before=${beforeStatus} after=${afterStatus}`);
406
- return { ...m, beforeStatus, afterStatus };
407
- });
408
- }
409
- catch (err) {
410
- errorResult = toolError(`Failed to load stateFile for maintenance enrichment: ${err.message}`);
411
- return errorResult;
412
- }
462
+ // Rows missing a required detail (a real execution happened but the LLM didn't
463
+ // draft a summary for it) — collected here and rejected below, rather than
464
+ // silently shipping a blank field.
465
+ const missingDetails = [];
466
+ if (rawMaintenance.length > 0) {
467
+ // beforeStatus/afterStatus are always stateFile-authoritative. beforeDetails/
468
+ // afterDetails come from the LLM's drafted summary (testMaintenanceDetails) —
469
+ // required whenever the stateFile shows a matching execution actually ran.
470
+ testMaintenance = rawMaintenance.map((m) => {
471
+ const recorded = stateData.existingTests?.find((t) => testFileMatches(t.testFile, m.testFilePath));
472
+ const detail = params.testMaintenanceDetails?.find((d) => d.testFilePath === m.testFilePath);
473
+ const displayName = path.basename(m.testFilePath);
474
+ const defaultBeforeStatus = MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
475
+ const defaultAfterStatus = m.action === DriftAction.Delete ? TestExecutionStatus.Skipped
476
+ : MAINTENANCE_CHANGE_ACTIONS.has(m.action) ? TestExecutionStatus.Unknown : TestExecutionStatus.Skipped;
477
+ const beforeStatus = recorded?.executionBefore?.status ?? defaultBeforeStatus;
478
+ const afterStatus = recorded?.executionAfter?.status ?? defaultAfterStatus;
479
+ // Trim before checking a whitespace-only string is semantically blank and
480
+ // must not satisfy the "drafted a summary" requirement.
481
+ const beforeDetails = detail?.beforeDetails?.trim() ?? "";
482
+ const afterDetails = detail?.afterDetails?.trim() ?? "";
483
+ if (recorded?.executionBefore && !beforeDetails)
484
+ missingDetails.push(`${displayName} (beforeDetails)`);
485
+ if (recorded?.executionAfter && !afterDetails)
486
+ missingDetails.push(`${displayName} (afterDetails)`);
487
+ logger.info(`${displayName}: before=${beforeStatus} after=${afterStatus}`);
488
+ return { ...m, beforeDetails, afterDetails, beforeStatus, afterStatus };
489
+ });
490
+ }
491
+ if (missingDetails.length > 0) {
492
+ errorResult = toolError(`${missingDetails.length} maintenance row(s) have a recorded execution but no drafted summary: ${missingDetails.join(", ")}. ` +
493
+ "Add a testMaintenanceDetails entry with the missing field(s), drafted from the execution output you already saw.");
494
+ return errorResult;
413
495
  }
414
496
  // Strip generation-artifact fields from newTestsCreated before writing.
415
497
  // scenarioFile, traceFile, frontendTrace are internal paths used during
@@ -421,7 +503,14 @@ export function registerSubmitReportTool(server) {
421
503
  businessCaseAnalysis: params.businessCaseAnalysis,
422
504
  newTestsCreated: sanitizedNewTests,
423
505
  additionalRecommendations: dedupedRecommendations.map(normalizeRepository),
424
- testMaintenance,
506
+ // Report wire format keeps main's original `fileName` (basename) — testFilePath is
507
+ // an internal-only field, needed for matching but never meant to reach the report.
508
+ // TODO(multi-repo maintenance): map(normalizeRepository) once testMaintenanceSchema
509
+ // has a repository field (see TODO above).
510
+ testMaintenance: testMaintenance?.map(({ testFilePath, ...row }) => ({
511
+ ...row,
512
+ fileName: path.basename(testFilePath),
513
+ })),
425
514
  testResults: params.testResults.map(normalizeRepository),
426
515
  issuesFound: params.issuesFound.map(normalizeRepository),
427
516
  nextSteps: params.nextSteps ?? [],