@skyramp/mcp 0.4.4-rc.1 → 0.4.4

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 (148) hide show
  1. package/build/index.js +239 -210
  2. package/build/playwright/PlaywrightTraceService.d.ts +1 -1
  3. package/build/playwright/blueprintDigestStore.d.ts +1 -1
  4. package/build/playwright/registerPlaywrightTools.d.ts +1 -1
  5. package/build/playwright/traceRecordingPrompt.d.ts +1 -1
  6. package/build/playwright/traceRecordingPrompt.js +2 -2
  7. package/build/prompts/local-dev/local-dev-prompts.d.ts +1 -1
  8. package/build/prompts/local-dev/local-dev-prompts.js +2 -2
  9. package/build/prompts/startTraceCollectionPrompts.d.ts +1 -1
  10. package/build/prompts/startTraceCollectionPrompts.js +2 -3
  11. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.d.ts +1 -1
  12. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +2 -2
  13. package/build/prompts/testbot/testbot-prompts.d.ts +1 -1
  14. package/build/prompts/testbot/testbot-prompts.js +4 -7
  15. package/build/recommendation/answers.d.ts +17 -52
  16. package/build/recommendation/answers.js +29 -99
  17. package/build/recommendation/pullRequestText.d.ts +0 -2
  18. package/build/recommendation/pullRequestText.js +0 -16
  19. package/build/recommendation/registerPlan.d.ts +4 -7
  20. package/build/recommendation/registerPlan.js +11 -37
  21. package/build/recommendation/types.d.ts +4 -21
  22. package/build/recommendation/verifierContracts.d.ts +4 -4
  23. package/build/recommendation/verifierContracts.js +4 -4
  24. package/build/recommendation/verifiers/citedPath.d.ts +2 -2
  25. package/build/recommendation/verifiers/citedPath.js +2 -2
  26. package/build/recommendation/verifiers/issueTraceability.d.ts +5 -1
  27. package/build/recommendation/verifiers/issueTraceability.js +35 -4
  28. package/build/resources/analysisResources.d.ts +1 -1
  29. package/build/resources/analysisResources.js +1 -1
  30. package/build/resources/progressResource.d.ts +1 -1
  31. package/build/resources/sutSetupResource.d.ts +1 -1
  32. package/build/resources/sutSetupResource.js +1 -1
  33. package/build/resources/testbotResource.d.ts +1 -1
  34. package/build/resources/testbotResource.js +1 -1
  35. package/build/services/AnalyticsService.d.ts +1 -1
  36. package/build/services/FleetExecutionService.d.ts +130 -0
  37. package/build/services/FleetExecutionService.js +391 -0
  38. package/build/services/ModularizationService.d.ts +1 -1
  39. package/build/services/TestExecutionService.d.ts +7 -0
  40. package/build/services/TestExecutionService.js +21 -0
  41. package/build/services/TestGenerationService.d.ts +1 -1
  42. package/build/tool-phases.js +2 -0
  43. package/build/tools/auth/loginTool.d.ts +1 -1
  44. package/build/tools/auth/loginTool.js +2 -2
  45. package/build/tools/auth/logoutTool.d.ts +1 -1
  46. package/build/tools/auth/logoutTool.js +2 -2
  47. package/build/tools/code-refactor/alignAssertionsTool.d.ts +1 -1
  48. package/build/tools/code-refactor/alignAssertionsTool.js +1 -1
  49. package/build/tools/code-refactor/assertionSkillTools.d.ts +1 -1
  50. package/build/tools/code-refactor/assertionSkillTools.js +2 -2
  51. package/build/tools/code-refactor/codeReuseTool.d.ts +1 -1
  52. package/build/tools/code-refactor/codeReuseTool.js +2 -2
  53. package/build/tools/code-refactor/enhanceAssertionsTool.d.ts +1 -1
  54. package/build/tools/code-refactor/enhanceAssertionsTool.js +1 -1
  55. package/build/tools/code-refactor/modularizationTool.d.ts +1 -1
  56. package/build/tools/code-refactor/modularizationTool.js +2 -2
  57. package/build/tools/enrichTestWithMocksTool.d.ts +1 -1
  58. package/build/tools/enrichTestWithMocksTool.js +2 -2
  59. package/build/tools/executeFleetTool.d.ts +28 -0
  60. package/build/tools/executeFleetTool.js +107 -0
  61. package/build/tools/executeSkyrampTestTool.d.ts +8 -5
  62. package/build/tools/executeSkyrampTestTool.js +5 -5
  63. package/build/tools/fixErrorTool.d.ts +1 -1
  64. package/build/tools/fixErrorTool.js +1 -1
  65. package/build/tools/generate-tests/batchMockGenerationTool.d.ts +6 -57
  66. package/build/tools/generate-tests/batchMockGenerationTool.js +3 -3
  67. package/build/tools/generate-tests/generateBatchScenarioRestTool.d.ts +9 -45
  68. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +29 -11
  69. package/build/tools/generate-tests/generateContractRestTool.d.ts +1 -2
  70. package/build/tools/generate-tests/generateContractRestTool.js +1 -1
  71. package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -2
  72. package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
  73. package/build/tools/generate-tests/generateFuzzRestTool.d.ts +1 -1
  74. package/build/tools/generate-tests/generateFuzzRestTool.js +1 -1
  75. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +1 -2
  76. package/build/tools/generate-tests/generateIntegrationRestTool.js +1 -1
  77. package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -2
  78. package/build/tools/generate-tests/generateLoadRestTool.js +2 -1
  79. package/build/tools/generate-tests/generateMockRestTool.d.ts +1 -2
  80. package/build/tools/generate-tests/generateMockRestTool.js +3 -3
  81. package/build/tools/generate-tests/generateSmokeRestTool.d.ts +1 -1
  82. package/build/tools/generate-tests/generateSmokeRestTool.js +1 -1
  83. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +64 -0
  84. package/build/tools/generate-tests/generateUILoadRestTool.js +512 -0
  85. package/build/tools/generate-tests/generateUIRestTool.d.ts +6 -7
  86. package/build/tools/generate-tests/generateUIRestTool.js +1 -1
  87. package/build/tools/generate-tests/loadTestSchema.d.ts +15 -15
  88. package/build/tools/generate-tests/trace-reuse-guard.d.ts +1 -1
  89. package/build/tools/generateEnrichedIntegrationTestTool.d.ts +1 -2
  90. package/build/tools/generateEnrichedIntegrationTestTool.js +1 -1
  91. package/build/tools/localDevWorkerComposeTool.d.ts +1 -1
  92. package/build/tools/localDevWorkerComposeTool.js +2 -2
  93. package/build/tools/one-click/oneClickTool.d.ts +1 -1
  94. package/build/tools/one-click/oneClickTool.js +1 -1
  95. package/build/tools/preflightMockCheckTool.d.ts +1 -1
  96. package/build/tools/preflightMockCheckTool.js +3 -3
  97. package/build/tools/queryProxyMocksTool.d.ts +1 -1
  98. package/build/tools/queryProxyMocksTool.js +3 -3
  99. package/build/tools/submitReportTool.d.ts +111 -414
  100. package/build/tools/submitReportTool.js +154 -138
  101. package/build/tools/test-management/actionsTool.d.ts +1 -1
  102. package/build/tools/test-management/actionsTool.js +7 -7
  103. package/build/tools/test-management/analyzeChangesTool.d.ts +22 -118
  104. package/build/tools/test-management/analyzeChangesTool.js +4 -4
  105. package/build/tools/test-management/analyzeTestHealthTool.d.ts +1 -1
  106. package/build/tools/test-management/analyzeTestHealthTool.js +4 -4
  107. package/build/tools/test-management/registerTestPlanTool.d.ts +111 -743
  108. package/build/tools/test-management/registerTestPlanTool.js +20 -36
  109. package/build/tools/test-management/resolveScreenTool.d.ts +1 -1
  110. package/build/tools/test-management/resolveScreenTool.js +6 -2
  111. package/build/tools/trace/startTraceCollectionTool.d.ts +1 -1
  112. package/build/tools/trace/startTraceCollectionTool.js +4 -4
  113. package/build/tools/trace/stopTraceCollectionTool.d.ts +1 -1
  114. package/build/tools/trace/stopTraceCollectionTool.js +2 -2
  115. package/build/tools/verifyTestDependenciesTool.d.ts +1 -1
  116. package/build/tools/verifyTestDependenciesTool.js +1 -1
  117. package/build/tools/workspace/initScanWorkspaceTool.d.ts +1 -1
  118. package/build/tools/workspace/initScanWorkspaceTool.js +1 -1
  119. package/build/tools/workspace/initializeWorkspaceTool.d.ts +1 -1
  120. package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
  121. package/build/types/RepositoryAnalysis.d.ts +164 -1170
  122. package/build/types/RepositoryAnalysis.js +12 -10
  123. package/build/types/StepMethod.d.ts +24 -1
  124. package/build/types/StepMethod.js +1 -1
  125. package/build/types/TestTypes.d.ts +34 -133
  126. package/build/types/TestTypes.js +3 -3
  127. package/build/types/TestbotReport.d.ts +37 -0
  128. package/build/types/TestbotReport.js +22 -0
  129. package/build/types/index.d.ts +1 -1
  130. package/build/types/index.js +1 -1
  131. package/build/utils/docker.d.ts +29 -7
  132. package/build/utils/docker.js +83 -27
  133. package/build/utils/initAgent.d.ts +3 -3
  134. package/build/utils/initAgent.js +58 -26
  135. package/build/utils/progress.d.ts +3 -4
  136. package/build/utils/progress.js +4 -4
  137. package/build/utils/utils.d.ts +1 -1
  138. package/build/utils/versions.d.ts +3 -3
  139. package/build/utils/versions.js +1 -1
  140. package/build/workspace/workspace.d.ts +117 -538
  141. package/build/workspace/workspace.js +2 -2
  142. package/package.json +6 -5
  143. package/plugin/prompts/testbot-task1.md +1 -1
  144. package/plugin/skills/repair-staged-files/SKILL.md +17 -0
  145. package/build/recommendation/blockerProbes.d.ts +0 -13
  146. package/build/recommendation/blockerProbes.js +0 -91
  147. package/build/recommendation/blockers.d.ts +0 -65
  148. package/build/recommendation/blockers.js +0 -135
@@ -9,13 +9,11 @@ import { ProgrammingLanguage, TESTBOT_SUPPORTED_TEST_TYPES, TestType, } from "..
9
9
  import { STEP_METHOD_DESCRIPTION, stepMethodSchema, } from "../types/StepMethod.js";
10
10
  import { DriftAction } from "../types/TestAnalysis.js";
11
11
  import { isResult, TestExecutionStatus } from "../types/TestExecution.js";
12
- import { IssueFoundCategory } from "../types/TestbotReport.js";
12
+ import { EnvironmentFamily, FailureCauseKind, IssueFoundCategory, } from "../types/TestbotReport.js";
13
13
  import { StateManager, runArtifactDir, getTestsRepoDir, resolveOwnRunStatePath, } from "../utils/AnalysisStateManager.js";
14
14
  import { toolError, testFileMatches } from "../utils/utils.js";
15
15
  import { isTestbotEnabled, isUtilsReuseEnabled, } from "../utils/featureFlags.js";
16
- import { BLOCKER_DESCRIPTION, closingAnswerFor, needsBlocker, REFUSED_ANSWER_PREFIX, refusedAnswerObjections, unknownAnswerObjections, } from "../recommendation/answers.js";
17
- import { blockersIn, probeBlockers } from "../recommendation/blockerProbes.js";
18
- import { blockerSchema, blockerText, hasBlocker, refutedEvidence, verifyBlocker, } from "../recommendation/blockers.js";
16
+ import { answerFor, unknownAnswerObjections, } from "../recommendation/answers.js";
19
17
  import { checkDeliveredMatchesPlan, checkMaintenanceDelivered, } from "../recommendation/verifiers/deliveredMatchesPlan.js";
20
18
  import { checkReportedCategoryMatchesPlan, checkRequirementConflictReported, } from "../recommendation/verifiers/reportedCategory.js";
21
19
  import { checkDefectsReported, checkIssueTraceability, } from "../recommendation/verifiers/issueTraceability.js";
@@ -117,17 +115,46 @@ const repositoryField = z
117
115
  .trim()
118
116
  .min(1)
119
117
  .describe("The 'owner/repo' this item pertains to (e.g. 'letsramp/api-insight'). Set it on every item — the primary's owner/repo for primary-repo objections, or a related repo's owner/repo for objections derived from that repository's diff.");
118
+ // One home for the family meanings: a `.describe()` on the base enum is replaced
119
+ // by the `.optional().describe()` at each use site, so the meanings go there.
120
+ const ENVIRONMENT_FAMILY_MEANINGS = "auth_token = an API token is missing or expired. " +
121
+ "secret = another secret or credential is missing. " +
122
+ "data_connection = the database or data store is not reachable. " +
123
+ "app_not_started = the app under test did not start or is not reachable. " +
124
+ "login = the sign-in step failed. " +
125
+ "external_dependency = an external service is not configured. " +
126
+ "recording_replay = recording or replay of the trace failed.";
127
+ const environmentFamilyField = z.enum(EnvironmentFamily);
128
+ // SKYR-4555. Why a Fail or Skipped row did not pass; the row-level refinement on
129
+ // testResultSchema decides when it is required and which shapes are consistent.
130
+ const failureCauseSchema = z
131
+ .object({
132
+ kind: z
133
+ .enum(FailureCauseKind)
134
+ .describe("application = the code under test misbehaved, or a test written to prove a defect failed as intended. " +
135
+ "environment = the test could not exercise the behaviour because of the test environment: a missing token or secret, no data connection, the app did not start, sign-in failed, an external service is not configured, or recording or replay failed. Name the gap in `family`. " +
136
+ "test_defect = the generated test itself is wrong, not the app: an ambiguous locator, a strict-mode clash, a bad assertion setup."),
137
+ family: environmentFamilyField
138
+ .optional()
139
+ .describe("The test-environment gap. Required when kind is 'environment'. Not allowed for the other kinds. " +
140
+ ENVIRONMENT_FAMILY_MEANINGS),
141
+ })
142
+ .describe("Why this test did not pass. REQUIRED when status is 'Fail' or 'Skipped'. Not allowed when status is 'Pass'. " +
143
+ "It must agree with reachedBehavior: 'application' needs reachedBehavior true or omitted; 'environment' needs reachedBehavior false or omitted.");
120
144
  // videoPath is deliberately absent from this input contract: it is attached server-side
121
145
  // from the run's execution records (see attachVideoPath), and zod strips any the model
122
146
  // supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
123
- const testResultSchema = z.object({
147
+ const testResultSchema = z
148
+ .object({
124
149
  testType: z
125
- .nativeEnum(TestType)
150
+ .enum(TestType)
126
151
  .describe("Type of test. Do not include priority or other metadata in this field."),
127
152
  endpoint: z
128
153
  .string()
129
154
  .describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
130
- status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
155
+ status: z
156
+ .enum(["Pass", "Fail", "Skipped"])
157
+ .describe("Test execution result"),
131
158
  details: z
132
159
  .string()
133
160
  .describe("One sentence — no embedded newlines, no markdown. e.g. '10.8s, products_contract_test.py' or 'failed: <one-line error summary>, products_contract_test.py'"),
@@ -147,7 +174,76 @@ const testResultSchema = z.object({
147
174
  .describe("For a failing test only. `true` when the request that carries the behaviour under test was sent and answered by the code under test, and the assertion that failed is about that behaviour. " +
148
175
  "`false` when the failure came earlier: a 404, 401, 403 or 5xx on a route the change adds; an expired token or a sign-in page; a control or setup step that failed; a generator or SDK error. " +
149
176
  "If your own `details` text says the request was not sent or an earlier step tripped, this is `false`. Omit only if you cannot tell."),
177
+ failureCause: failureCauseSchema.optional(),
150
178
  repository: repositoryField,
179
+ })
180
+ .superRefine((row, ctx) => {
181
+ // SKYR-4555: the business-user report sorts failures by cause, so every row
182
+ // that did not pass must name one and a passing row must not. The kind must
183
+ // agree with reachedBehavior where the agent set it: the two answer the same
184
+ // question — did the test reach the behaviour under test — from two sides.
185
+ const cause = row.failureCause;
186
+ if (row.status === "Pass") {
187
+ if (cause === undefined)
188
+ return;
189
+ ctx.addIssue({
190
+ code: "custom",
191
+ path: ["failureCause"],
192
+ message: `failureCause is not allowed when status is 'Pass'. A passing test has no failure cause. ` +
193
+ `Remove failureCause from this row.`,
194
+ });
195
+ return;
196
+ }
197
+ if (cause === undefined) {
198
+ ctx.addIssue({
199
+ code: "custom",
200
+ path: ["failureCause"],
201
+ message: `failureCause is required when status is '${row.status}'. Set failureCause.kind to one of: ` +
202
+ `'application' (the code under test misbehaved, or a test written to prove a defect failed as intended); ` +
203
+ `'environment' (the test environment stopped the test before it reached the behaviour — also set failureCause.family); ` +
204
+ `'test_defect' (the generated test itself is wrong, not the app).`,
205
+ });
206
+ return;
207
+ }
208
+ if (cause.kind === FailureCauseKind.Environment) {
209
+ if (cause.family === undefined) {
210
+ ctx.addIssue({
211
+ code: "custom",
212
+ path: ["failureCause", "family"],
213
+ message: `failureCause.family is required when failureCause.kind is 'environment'. ` +
214
+ `Set it to the gap that stopped the test: ${Object.values(EnvironmentFamily).join(", ")}.`,
215
+ });
216
+ }
217
+ }
218
+ else if (cause.family !== undefined) {
219
+ ctx.addIssue({
220
+ code: "custom",
221
+ path: ["failureCause", "family"],
222
+ message: `failureCause.family is allowed only when failureCause.kind is 'environment'. ` +
223
+ `Remove failureCause.family, or set kind to 'environment' if the test environment stopped the test.`,
224
+ });
225
+ }
226
+ if (cause.kind === FailureCauseKind.Application) {
227
+ if (row.reachedBehavior === false) {
228
+ ctx.addIssue({
229
+ code: "custom",
230
+ path: ["failureCause", "kind"],
231
+ message: `failureCause.kind 'application' contradicts reachedBehavior false. 'application' means the test reached the behaviour under test and the code misbehaved. ` +
232
+ `If the failure came earlier, set kind to 'environment' or 'test_defect'. If the test did reach the behaviour, set reachedBehavior to true.`,
233
+ });
234
+ }
235
+ }
236
+ else if (cause.kind === FailureCauseKind.Environment &&
237
+ row.reachedBehavior === true) {
238
+ // test_defect is not checked: a wrong assertion can fail after the test
239
+ // reached the behaviour, or a bad setup step can fail before it.
240
+ ctx.addIssue({
241
+ code: "custom",
242
+ path: ["failureCause", "kind"],
243
+ message: `failureCause.kind 'environment' contradicts reachedBehavior true. reachedBehavior true means the test reached the behaviour under test, so the environment did not stop it. ` +
244
+ `Set kind to 'application' or 'test_defect', or set reachedBehavior to false if the failure came earlier.`,
245
+ });
246
+ }
151
247
  });
152
248
  // Structured grounding for UI recommendations. Lifted verbatim from a
153
249
  // `browser_blueprint` element captured during this run. Source of truth for
@@ -237,7 +333,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
237
333
  "Required on EVERY entry, UI included. That is deliberately wider than the approved-plan generation gate, which stays contract and integration only: " +
238
334
  "this id is the exact key the report is joined back to the plan on, and an entry without one falls back to guessing from the test name and endpoint."),
239
335
  testType: z
240
- .nativeEnum(TestType)
336
+ .enum(TestType)
241
337
  .describe("Type of test created. Do not include priority or other metadata in this field."),
242
338
  category: z
243
339
  .enum(SCENARIO_CATEGORIES)
@@ -284,7 +380,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
284
380
  // optional above and required here so the message says what to do.
285
381
  if ((rec.plannedTestId ?? "").trim().length === 0) {
286
382
  ctx.addIssue({
287
- code: z.ZodIssueCode.custom,
383
+ code: "custom",
288
384
  path: ["plannedTestId"],
289
385
  message: "plannedTestId is required on every newTestsCreated entry. Use the plannedTestId of the plan entry this test was approved under, from skyramp_register_test_plan.",
290
386
  });
@@ -294,7 +390,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
294
390
  for (const field of ["targetElements", "pageContext"]) {
295
391
  if (rec[field] !== undefined) {
296
392
  ctx.addIssue({
297
- code: z.ZodIssueCode.custom,
393
+ code: "custom",
298
394
  path: [field],
299
395
  message: `${field} is only valid for testType: 'ui'`,
300
396
  });
@@ -305,7 +401,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
305
401
  // description is required for UI tests (walkthrough of steps/assertions).
306
402
  if (!rec.description) {
307
403
  ctx.addIssue({
308
- code: z.ZodIssueCode.custom,
404
+ code: "custom",
309
405
  path: ["description"],
310
406
  message: "description is required for testType: 'ui'.",
311
407
  });
@@ -313,7 +409,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
313
409
  // targetElements must be present (array or null — not omitted entirely).
314
410
  if (rec.targetElements === undefined) {
315
411
  ctx.addIssue({
316
- code: z.ZodIssueCode.custom,
412
+ code: "custom",
317
413
  path: ["targetElements"],
318
414
  message: "targetElements is required for testType: 'ui'. Use null when blueprint capture failed; the null is the signal and needs no marker in description or reasoning.",
319
415
  });
@@ -324,7 +420,7 @@ export const newTestSchema = z.preprocess(stripNullGroundingFields, z
324
420
  if (Array.isArray(rec.targetElements) &&
325
421
  rec.pageContext === undefined) {
326
422
  ctx.addIssue({
327
- code: z.ZodIssueCode.custom,
423
+ code: "custom",
328
424
  path: ["pageContext"],
329
425
  message: "pageContext is required for testType: 'ui' when targetElements is an array.",
330
426
  });
@@ -355,7 +451,7 @@ const issueFoundSchema = z
355
451
  "report an issue here at 'critical' or 'high'. A stated requirement the code does not meet is not a " +
356
452
  "medium. The check raises an objection, never a rejection."),
357
453
  category: z
358
- .nativeEnum(IssueFoundCategory)
454
+ .enum(IssueFoundCategory)
359
455
  .describe("Issue classification. bug = a product/code defect, e.g. found by a test or in the diff. " +
360
456
  "lint = a linter or formatter objection (eslint, flake8, prettier). " +
361
457
  "type = a type-check failure (tsc, mypy). " +
@@ -374,7 +470,7 @@ const issueFoundSchema = z
374
470
  .optional()
375
471
  .describe("The `id` of the plan defect this issue reports, from the `defects` list you registered with skyramp_register_test_plan. Spell it as that list spells it: an id the plan does not declare traces nothing. " +
376
472
  "Set it on the `category: bug` entry that reports the defect. A `defectId` on a `lint`, `type` or `config` entry does not report the defect — the report renders those in its own Configuration Errors section. " +
377
- "CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it, or when a `maintains` entry you registered cites it. Leave it out for an issue the plan did not declare."),
473
+ "CHECKED: every plan defect has a `bug` issue that names it, and a `bug` entry with no `plannedTestId` traces through this defect when a delivered test that expects to fail cites it, when a `maintains` entry you registered cites it, or when you answered `defects:untested:<id>` at plan time. Leave it out for an issue the plan did not declare."),
378
474
  sourceFile: citationString.describe("Path of the application file whose code is missing or wrong, relative to the repository root (e.g. 'src/crud/products.py'). " +
379
475
  "REQUIRED when category is 'bug'. " +
380
476
  "For code that is MISSING — an unmounted router, an unregistered route, an import never added — name the file where the line should be, not the file that defines what is unmounted. " +
@@ -390,8 +486,25 @@ const issueFoundSchema = z
390
486
  .describe("1-based line number in sourceFile. Optional and advisory. " +
391
487
  "Give the line of the offending statement itself, not the line of the enclosing function's signature — review views place the objection on exactly this line. " +
392
488
  "The tool does not check that the line still holds the cited code, because line numbers drift."),
489
+ environmentFamily: environmentFamilyField
490
+ .optional()
491
+ .describe("Only for category 'config'. Not allowed for the other categories. " +
492
+ "When this config issue is a test-environment gap, name the gap. Omit it for a config issue that is not a test-environment gap. " +
493
+ ENVIRONMENT_FAMILY_MEANINGS),
393
494
  })
394
495
  .superRefine((issue, ctx) => {
496
+ // SKYR-4555: the family names a test-environment gap, which only a config
497
+ // entry reports. On a bug, lint or type entry it would put the issue in two
498
+ // places at once.
499
+ if (issue.environmentFamily !== undefined &&
500
+ issue.category !== IssueFoundCategory.Config) {
501
+ ctx.addIssue({
502
+ code: "custom",
503
+ path: ["environmentFamily"],
504
+ message: `environmentFamily is allowed only when category is 'config'. It names the test-environment gap behind a config issue. ` +
505
+ `Remove environmentFamily from this '${issue.category}' entry, or set category to 'config' if the issue is a test-environment gap.`,
506
+ });
507
+ }
395
508
  // A `bug` entry asserts that product code is wrong, so it has to name the code:
396
509
  // without a file the entry is prose a reviewer cannot act on, and nothing
397
510
  // downstream can place it in the diff. Only the file is required — a config file
@@ -408,7 +521,7 @@ const issueFoundSchema = z
408
521
  if (issue.sourceFile !== undefined)
409
522
  return;
410
523
  ctx.addIssue({
411
- code: z.ZodIssueCode.custom,
524
+ code: "custom",
412
525
  path: ["sourceFile"],
413
526
  message: `sourceFile is required when category is 'bug' — a bug entry must name the code it is about. ` +
414
527
  `Open the file, then set sourceFile (path relative to the repository root). Also set sourceSymbol (the function or identifier inside it) when the file has one, and sourceLine (the line of the offending statement) when you have it — both are optional. ` +
@@ -431,11 +544,11 @@ const scenarioStepSchema = z.object({
431
544
  .optional()
432
545
  .describe("Expected HTTP status code, e.g. 200, 201, 404"),
433
546
  requestBody: z
434
- .record(z.any())
547
+ .record(z.string(), z.any())
435
548
  .optional()
436
549
  .describe("Example request body with realistic field values"),
437
550
  responseBody: z
438
- .union([z.record(z.any()), z.array(z.any())])
551
+ .union([z.record(z.string(), z.any()), z.array(z.any())])
439
552
  .optional()
440
553
  .describe("Key response fields to verify, e.g. { id: 'number', name: 'string', in_stock: 'boolean?' }. An array for a collection/list endpoint that returns a JSON array."),
441
554
  });
@@ -445,7 +558,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
445
558
  .string()
446
559
  .describe("Human-readable kebab-case identifier, e.g. 'integration-products-orders-workflow' or 'e2e-checkout-flow'. Format: '<testType>-<scenario-slug>'. Must be unique within the report."),
447
560
  testType: z
448
- .nativeEnum(TestType)
561
+ .enum(TestType)
449
562
  .describe("Type of test. Do not include priority or other metadata in this field."),
450
563
  category: z
451
564
  .enum(SCENARIO_CATEGORIES)
@@ -499,7 +612,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
499
612
  .superRefine((rec, ctx) => {
500
613
  if (rec.testType === TestType.CONTRACT && !rec.primaryEndpoint) {
501
614
  ctx.addIssue({
502
- code: z.ZodIssueCode.custom,
615
+ code: "custom",
503
616
  path: ["primaryEndpoint"],
504
617
  message: "primaryEndpoint is required for contract tests — provide the HTTP verb and path, e.g. 'PATCH /api/v1/orders/{order_id}'.",
505
618
  });
@@ -509,7 +622,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
509
622
  for (const field of ["targetElements", "pageContext"]) {
510
623
  if (rec[field] !== undefined) {
511
624
  ctx.addIssue({
512
- code: z.ZodIssueCode.custom,
625
+ code: "custom",
513
626
  path: [field],
514
627
  message: `${field} is only valid for testType: 'ui'`,
515
628
  });
@@ -519,7 +632,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
519
632
  else {
520
633
  if (rec.targetElements === undefined) {
521
634
  ctx.addIssue({
522
- code: z.ZodIssueCode.custom,
635
+ code: "custom",
523
636
  path: ["targetElements"],
524
637
  message: "targetElements is required for testType: 'ui'. Use null when no capture covers the screen.",
525
638
  });
@@ -527,7 +640,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
527
640
  if (Array.isArray(rec.targetElements) &&
528
641
  rec.pageContext === undefined) {
529
642
  ctx.addIssue({
530
- code: z.ZodIssueCode.custom,
643
+ code: "custom",
531
644
  path: ["pageContext"],
532
645
  message: "pageContext is required for testType: 'ui' when targetElements is an array.",
533
646
  });
@@ -537,7 +650,7 @@ export const additionalRecommendationSchema = z.preprocess(stripNullGroundingFie
537
650
  }
538
651
  }));
539
652
  const testMaintenanceSchema = z.object({
540
- testType: z.nativeEnum(TestType).describe("Type of test."),
653
+ testType: z.enum(TestType).describe("Type of test."),
541
654
  endpoint: z
542
655
  .string()
543
656
  .describe("For contract and integration tests, the HTTP verb and path, e.g. 'GET /api/v1/products'. For ui and e2e tests, the page path the test opens, e.g. '/notifications'."),
@@ -548,7 +661,7 @@ const testMaintenanceSchema = z.object({
548
661
  })
549
662
  .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."),
550
663
  action: z
551
- .nativeEnum(DriftAction)
664
+ .enum(DriftAction)
552
665
  .describe("The drift action assigned to this test during maintenance triage."),
553
666
  description: z.string().describe("What was changed and why"),
554
667
  beforeDetails: z
@@ -576,8 +689,8 @@ const testMaintenanceSchema = z.object({
576
689
  // "probably the primary" cannot tell a single-repo row from a mis-stamped one.
577
690
  repository: z.string(),
578
691
  // Server-populated from stateFile execution records — never supplied by the LLM.
579
- beforeStatus: z.nativeEnum(TestExecutionStatus),
580
- afterStatus: z.nativeEnum(TestExecutionStatus),
692
+ beforeStatus: z.enum(TestExecutionStatus),
693
+ afterStatus: z.enum(TestExecutionStatus),
581
694
  });
582
695
  // LLM input contract: status fields are server-populated from stateFile, and
583
696
  // `defects` is attached from the per-file detail the agent drafts, not from the
@@ -1361,7 +1474,7 @@ export function registerSubmitReportTool(server) {
1361
1474
  "This is the ONLY way to submit the report — do NOT write the report to a file manually. " +
1362
1475
  "If a check objects to what you reported, the objection names an id: fix what you can, and answer the rest through `answers` on a second call. " +
1363
1476
  "On an ACCEPTED call this tool also edits the files this run changed: a comment addressed to a Skyramp gate — `reuse-verify: allow …` or `// kept inline: …` — is removed from each of them, because it speaks to a gate and not to the customer who owns the file. Nothing else in those files is touched. A REFUSED call leaves every source file exactly as it found it; it still records its own run state, such as the refusal count and the objections it raised.",
1364
- inputSchema: {
1477
+ inputSchema: z.object({
1365
1478
  businessCaseAnalysis: z
1366
1479
  .string()
1367
1480
  .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.")
@@ -1412,12 +1525,11 @@ export function registerSubmitReportTool(server) {
1412
1525
  answer: z
1413
1526
  .string()
1414
1527
  .describe("Why the objection does not apply, or what you decided instead. A blank or whitespace-only answer closes nothing and the objection stays open."),
1415
- blocker: blockerSchema.optional().describe(BLOCKER_DESCRIPTION),
1416
1528
  })
1417
1529
  .strict())
1418
1530
  .optional()
1419
1531
  .default([])
1420
- .describe("One answer per check that objected to your previous call and that you do not intend to fix. An answered objection is published with your answer beside it and no longer counted open; an unanswered one is published on its own. An objection about a bug with no test is the exception: it closes only when the answer also sets `blocker`, and a sentence on its own leaves it open and published. Answers persist across calls, so an id answered once stays answered — with the blocker it carried. There is no call limit."),
1532
+ .describe("One answer per check that objected to your previous call and that you do not intend to fix. An answered objection is published with your answer beside it and no longer counted open; an unanswered one is published on its own. Answers persist across calls, so an id answered once stays answered. There is no call limit."),
1421
1533
  stateFile: z
1422
1534
  .string()
1423
1535
  .optional()
@@ -1426,7 +1538,7 @@ export function registerSubmitReportTool(server) {
1426
1538
  "another run's execution results, and the call is refused. " +
1427
1539
  "The report is written beside it, and it holds the execution results, the " +
1428
1540
  "page captures and the videos this report needs."),
1429
- },
1541
+ }),
1430
1542
  _meta: {
1431
1543
  keywords: ["report", "summary", "testbot", "submit"],
1432
1544
  },
@@ -2513,14 +2625,6 @@ export function registerSubmitReportTool(server) {
2513
2625
  typeof entry?.answer === "string" &&
2514
2626
  entry.answer.trim().length > 0);
2515
2627
  const carriedByKey = new Map(carried.map((entry) => [answerKey(entry.objection), entry]));
2516
- // Probed here for the same reason the plan tool probes before building its
2517
- // context: the checks below are synchronous. Both this call's blockers and
2518
- // the carried ones, so a carried close is re-judged rather than trusted.
2519
- const blockerChecks = await probeBlockers([
2520
- ...blockersIn(acknowledged),
2521
- ...blockersIn(carried),
2522
- ]);
2523
- const judge = (blocker) => verifyBlocker(blocker, blockerChecks);
2524
2628
  // What the LAST call raised. Without it there is nothing to notice a
2525
2629
  // fixed objection's absence against.
2526
2630
  const previouslyRaised = (Array.isArray(stateData.reportObjections?.raisedObjections)
@@ -2547,35 +2651,12 @@ export function registerSubmitReportTool(server) {
2547
2651
  ...raised,
2548
2652
  ...previouslyRaised.map((entry) => entry.objection),
2549
2653
  ];
2550
- const raisedKeys = new Set(raised.map(answerKey));
2551
2654
  const reportStageObjections = [
2552
2655
  ...raised,
2553
2656
  ...unknownAnswerObjections(answerable, acknowledged, {
2554
2657
  scope: "this run raised",
2555
2658
  suggestion: "Answer only the ids this tool has listed on this run. A plan-time objection is answered through skyramp_register_test_plan's own answers, not here.",
2556
2659
  }),
2557
- // The same refusal plan time raises: a sentence sent for an objection that
2558
- // takes a blocker leaves that objection open, and this says so rather than
2559
- // letting the answer pass in silence. Against this call's answers only,
2560
- // like `registerPlan` — a carried one that closed nothing simply stays
2561
- // carried, and its objection stays published open.
2562
- ...refusedAnswerObjections(raised, acknowledged, judge, (id) =>
2563
- // The same rule the loop below applies to a carried answer. Without
2564
- // it a second, weaker answer for an objection an earlier call already
2565
- // closed raised `answers:refused:` beside the close this call still
2566
- // publishes as answered. Matched by id AND message, as `carriedByKey`
2567
- // is: one id can name a different bug on a later call, and a close
2568
- // carried for the first must not silence a refusal for the second.
2569
- carried.some((entry) => {
2570
- if (entry.objection?.objectionId !== id)
2571
- return false;
2572
- if (!raisedKeys.has(answerKey(entry.objection)))
2573
- return false;
2574
- const blocker = hasBlocker(entry.blocker)
2575
- ? entry.blocker
2576
- : undefined;
2577
- return (!needsBlocker(id) || (!!blocker && judge(blocker) !== "refuted"));
2578
- })),
2579
2660
  ];
2580
2661
  const raisedNow = new Set(reportStageObjections.map(answerKey));
2581
2662
  // A call with no execution result gives these checks nothing to judge, so
@@ -2605,57 +2686,12 @@ export function registerSubmitReportTool(server) {
2605
2686
  const answeredReportObjections = [];
2606
2687
  const openReportObjections = [];
2607
2688
  for (const objection of reportStageObjections) {
2608
- // `closingAnswerFor` applies the blocker rule to THIS call's answers. A
2609
- // carried answer was accepted on an earlier call and is re-checked against
2610
- // the same rule here, because the objection it closed is recomputed every
2611
- // call. The answer and the blocker come from the ONE entry that closed it:
2612
- // searching for the blocker apart from the answer published the first
2613
- // blocker sent, refuted or belonging to no answer at all.
2614
- const wantsBlocker = needsBlocker(objection.objectionId);
2615
- const closingNow = closingAnswerFor(objection, acknowledged, judge);
2616
- const blockerNow = hasBlocker(closingNow?.blocker)
2617
- ? closingNow.blocker
2618
- : undefined;
2619
- const carriedEntry = carriedByKey.get(answerKey(objection));
2620
- const carriedBlocker = hasBlocker(carriedEntry?.blocker)
2621
- ? carriedEntry?.blocker
2622
- : undefined;
2623
- const answerNow = closingNow?.answer;
2624
- // A carried answer meets the same rule as the call that sent it: a
2625
- // blocker, and one this call's probe does not refute.
2626
- const carriedCloses = carriedEntry &&
2627
- (!wantsBlocker ||
2628
- (!!carriedBlocker && judge(carriedBlocker) !== "refuted"));
2629
- const answer = answerNow ?? (carriedCloses ? carriedEntry?.answer : undefined);
2630
- if (!answer) {
2689
+ const answer = answerFor(objection, acknowledged) ??
2690
+ carriedByKey.get(answerKey(objection))?.answer;
2691
+ if (answer)
2692
+ answeredReportObjections.push({ objection, answer });
2693
+ else
2631
2694
  openReportObjections.push(objection);
2632
- // A carried answer that closed this objection on an earlier call and
2633
- // does not now is the probe changing its mind — the service came back
2634
- // up. Without this the objection simply reappears with no reason.
2635
- if (carriedEntry &&
2636
- carriedBlocker &&
2637
- judge(carriedBlocker) === "refuted") {
2638
- openReportObjections.push({
2639
- objectionId: `${REFUSED_ANSWER_PREFIX}${objection.objectionId}`,
2640
- verifier: "answers",
2641
- message: "The server checked the blocker this answer names and found the opposite, so the objection stays open.",
2642
- evidence: `answered "${objection.objectionId}" on an earlier call with \`${carriedBlocker.kind}\`; ${refutedEvidence(carriedBlocker)}`,
2643
- suggestion: "Write the test: what you named as the blocker is not in the way. If something else is, send the blocker that names it — the server checks each kind against the run it can see.",
2644
- });
2645
- }
2646
- continue;
2647
- }
2648
- // A refuted blocker is not published, as at plan time: on an objection any
2649
- // answer closes, the sentence closes it and the untrue claim is dropped.
2650
- const sent = answerNow ? blockerNow : carriedBlocker;
2651
- const blocker = sent && judge(sent) !== "refuted" ? sent : undefined;
2652
- answeredReportObjections.push({
2653
- objection,
2654
- answer,
2655
- ...(blocker
2656
- ? { blocker, blockerVerified: judge(blocker) === "verified" }
2657
- : {}),
2658
- });
2659
2695
  }
2660
2696
  objections = [
2661
2697
  ...stillOpen.map((objection) => ({
@@ -2668,16 +2704,6 @@ export function registerSubmitReportTool(server) {
2668
2704
  ...entry.objection,
2669
2705
  stage: "plan",
2670
2706
  answer: typeof entry.answer === "string" ? entry.answer : "",
2671
- // A plan-time close that a blocker carried says so here, exactly as
2672
- // the post-execution closes below do. Without this a probed close
2673
- // was published as an ordinary sentence and the report lost the one
2674
- // fact that distinguishes them.
2675
- ...(hasBlocker(entry.blocker)
2676
- ? {
2677
- unverifiedBlocker: blockerText(entry.blocker),
2678
- blockerVerified: !!entry.blockerVerified,
2679
- }
2680
- : {}),
2681
2707
  })),
2682
2708
  ...unverified
2683
2709
  .filter((entry) => entry?.objection)
@@ -2685,29 +2711,16 @@ export function registerSubmitReportTool(server) {
2685
2711
  ...entry.objection,
2686
2712
  stage: "plan",
2687
2713
  answer: typeof entry.answer === "string" ? entry.answer : "",
2688
- // `unverifiedCloses[].blocker` is a `PlanBlocker`; reading it as a
2689
- // string published an empty one on every plan-time close.
2690
- ...(hasBlocker(entry.blocker)
2691
- ? { unverifiedBlocker: blockerText(entry.blocker) }
2692
- : {}),
2714
+ unverifiedBlocker: typeof entry.blocker === "string" ? entry.blocker : "",
2693
2715
  })),
2694
2716
  ...openReportObjections.map((objection) => ({
2695
2717
  ...objection,
2696
2718
  stage: "post-execution",
2697
2719
  })),
2698
- // The blocker is published beside the answer exactly as the plan-time
2699
- // closes above are, and `blockerVerified` says whether a probe backed it
2700
- // or the server could only take it as given.
2701
- ...answeredReportObjections.map(({ objection, answer, blocker, blockerVerified }) => ({
2720
+ ...answeredReportObjections.map(({ objection, answer }) => ({
2702
2721
  ...objection,
2703
2722
  stage: "post-execution",
2704
2723
  answer,
2705
- ...(blocker
2706
- ? {
2707
- unverifiedBlocker: blockerText(blocker),
2708
- blockerVerified: !!blockerVerified,
2709
- }
2710
- : {}),
2711
2724
  })),
2712
2725
  ...resolvedObjections.map(({ objection, resolvedBy }) => ({
2713
2726
  ...objection,
@@ -2852,7 +2865,10 @@ export function registerSubmitReportTool(server) {
2852
2865
  // these objects and don't expect it).
2853
2866
  testResults: params.testResults.map((row) => {
2854
2867
  const { testFilePath: _tfp, ...wire } = attachVideoPath(row, stateData.executionVideos);
2855
- return wire;
2868
+ // SKYR-4555: the basename is the join key to newTestsCreated and
2869
+ // testMaintenance (consumers compare it with the basename of their
2870
+ // fileName, which may be a relative path); the path stays internal.
2871
+ return { ...wire, fileName: path.basename(row.testFilePath) };
2856
2872
  }),
2857
2873
  issuesFound,
2858
2874
  executionCoverage,
@@ -1,4 +1,4 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
1
+ import { McpServer } from "@modelcontextprotocol/server";
2
2
  /**
3
3
  * Compute a suggested new filename when an endpoint is renamed.
4
4
  */
@@ -69,19 +69,19 @@ const recommendationSchema = z.object({
69
69
  })
70
70
  .describe("Absolute path to the page object (POM) file to edit, when the spec delegates selectors to a page object. When set, skyramp_actions edits this file instead of testFilePath. testFilePath (the spec) is still used for catalog validation and the maintenance report."),
71
71
  action: z
72
- .nativeEnum(DriftAction)
72
+ .enum(DriftAction)
73
73
  .refine((a) => a !== DriftAction.Ignore, {
74
74
  message: "Omit IGNORE entries entirely — a discovered test you don't recommend is automatically treated as needing no action.",
75
75
  })
76
76
  .describe("The maintenance action for this test (UPDATE, REGENERATE, DELETE, or VERIFY). Do not send IGNORE — omit no-action tests from `recommendations` entirely; anything you leave out is recorded as needing no action."),
77
77
  testType: z
78
- .nativeEnum(TestType)
78
+ .enum(TestType)
79
79
  .describe("Type of this test — carried through to the maintenance report."),
80
80
  endpoint: z
81
81
  .string()
82
82
  .describe("HTTP verb and path this test covers, e.g. 'GET /api/v1/products'."),
83
83
  priority: z
84
- .nativeEnum(RecommendationPriority)
84
+ .enum(RecommendationPriority)
85
85
  .optional()
86
86
  .describe("Update priority"),
87
87
  rationale: z
@@ -90,7 +90,7 @@ const recommendationSchema = z.object({
90
90
  .min(1)
91
91
  .describe("1-2 sentence explanation of why this action is needed — becomes the maintenance report's description, so it must not be blank."),
92
92
  estimatedWork: z
93
- .nativeEnum(EstimatedWork)
93
+ .enum(EstimatedWork)
94
94
  .optional()
95
95
  .describe("Estimated effort to apply the update"),
96
96
  updateInstructions: z
@@ -169,15 +169,15 @@ export function registerActionsTool(server) {
169
169
  description: `Execute test maintenance actions — final step of the unified Test Health Analysis Flow. Call this once per run even when no tests need action (pass \`recommendations: []\`) — it is where the maintenance verdicts are persisted before the run moves on. Supply only the tests that need an action; any discovered test you omit from \`recommendations\` is treated as needing no action and recorded automatically, so you never enumerate the no-action tests yourself.`,
170
170
  // SKYR-4194: the payload is returned via structuredContent, which requires a
171
171
  // declared outputSchema — see dualChannelResult.
172
- outputSchema: {
172
+ outputSchema: z.object({
173
173
  summary: z
174
174
  .string()
175
175
  .describe("Human-readable report of the maintenance actions to take."),
176
176
  instructions: z
177
177
  .string()
178
178
  .describe("JSON instructions for carrying out those actions — file lists, per-file edit context, and the update/regenerate/delete strategies. Read and follow this field."),
179
- },
180
- inputSchema: actionsSchema,
179
+ }),
180
+ inputSchema: z.object(actionsSchema),
181
181
  }, async (args) => {
182
182
  let errorResult;
183
183
  try {