@skyramp/mcp 0.4.4 → 0.5.0-rc.10

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 (144) hide show
  1. package/build/entitlements/entitlementGate.d.ts +8 -0
  2. package/build/entitlements/entitlementGate.js +57 -0
  3. package/build/entitlements/runCodes.d.ts +18 -0
  4. package/build/entitlements/runCodes.js +16 -0
  5. package/build/index.js +32 -6
  6. package/build/playwright/PlaywrightTraceService.d.ts +17 -0
  7. package/build/playwright/PlaywrightTraceService.js +29 -1
  8. package/build/playwright/dataSafetyConfig.d.ts +74 -0
  9. package/build/playwright/dataSafetyConfig.js +109 -0
  10. package/build/playwright/essentialTools.d.ts +1 -0
  11. package/build/playwright/essentialTools.js +44 -0
  12. package/build/playwright/registerPlaywrightTools.d.ts +36 -1
  13. package/build/playwright/registerPlaywrightTools.js +87 -43
  14. package/build/playwright/safeResolveDataSafety.d.ts +24 -0
  15. package/build/playwright/safeResolveDataSafety.js +33 -0
  16. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.d.ts +17 -0
  17. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +46 -177
  18. package/build/prompts/modularization/ui-test-modularization.d.ts +1 -0
  19. package/build/prompts/modularization/ui-test-modularization.js +9 -2
  20. package/build/prompts/test-maintenance/driftAnalysisShared.js +1 -1
  21. package/build/services/FleetExecutionService.d.ts +87 -16
  22. package/build/services/FleetExecutionService.js +487 -48
  23. package/build/services/FleetRunRegistry.d.ts +49 -0
  24. package/build/services/FleetRunRegistry.js +178 -0
  25. package/build/services/containerEnv.js +8 -2
  26. package/build/services/fleetLimitConfirmation.d.ts +19 -0
  27. package/build/services/fleetLimitConfirmation.js +126 -0
  28. package/build/tool-phases.d.ts +14 -8
  29. package/build/tool-phases.js +88 -43
  30. package/build/tools/code-refactor/assertion-state.js +3 -1
  31. package/build/tools/code-refactor/codeReuseTool.js +2 -1
  32. package/build/tools/code-refactor/utils-verify-gates.js +1 -1
  33. package/build/tools/executeFleetTool.d.ts +6 -4
  34. package/build/tools/executeFleetTool.js +67 -23
  35. package/build/tools/fixErrorTool.js +2 -1
  36. package/build/tools/fleetLifecycleTools.d.ts +20 -0
  37. package/build/tools/fleetLifecycleTools.js +124 -0
  38. package/build/tools/generate-tests/generateIntegrationRestTool.d.ts +38 -0
  39. package/build/tools/generate-tests/generateIntegrationRestTool.js +7 -1
  40. package/build/tools/generate-tests/generateUILoadRestTool.d.ts +19 -1
  41. package/build/tools/generate-tests/generateUILoadRestTool.js +316 -50
  42. package/build/tools/submitReportTool.js +4 -122
  43. package/build/tools/test-management/registerTestPlanTool.js +22 -6
  44. package/build/tools/workspace/initScanWorkspaceTool.d.ts +11 -1
  45. package/build/tools/workspace/initScanWorkspaceTool.js +111 -9
  46. package/build/tools/workspace/initializeWorkspaceTool.js +40 -4
  47. package/build/types/AssertionOutcome.d.ts +3 -0
  48. package/build/types/TestbotReport.d.ts +0 -37
  49. package/build/types/TestbotReport.js +0 -22
  50. package/build/types/index.d.ts +1 -1
  51. package/build/types/index.js +1 -1
  52. package/build/utils/assertion-verify/created-add-guard.d.ts +12 -0
  53. package/build/utils/assertion-verify/created-add-guard.js +173 -0
  54. package/build/utils/assertion-verify/index.d.ts +1 -0
  55. package/build/utils/assertion-verify/index.js +1 -0
  56. package/build/utils/assertion-verify/verify.d.ts +7 -3
  57. package/build/utils/assertion-verify/verify.js +9 -3
  58. package/build/utils/featureFlags.js +6 -3
  59. package/build/utils/repositorySlug.d.ts +14 -0
  60. package/build/utils/repositorySlug.js +46 -1
  61. package/build/utils/telemetry.d.ts +2 -0
  62. package/build/utils/telemetry.js +12 -0
  63. package/build/utils/utils-verify/action-key.d.ts +59 -2
  64. package/build/utils/utils-verify/action-key.js +162 -6
  65. package/build/utils/utils-verify/action-sites.d.ts +7 -0
  66. package/build/utils/utils-verify/action-sites.js +277 -35
  67. package/build/utils/utils-verify/call-sites.d.ts +8 -2
  68. package/build/utils/utils-verify/language-spec.js +1 -1
  69. package/build/utils/utils-verify/parse.d.ts +4 -0
  70. package/build/utils/utils-verify/parse.js +2 -1
  71. package/build/utils/versions.d.ts +3 -3
  72. package/build/utils/versions.js +1 -1
  73. package/build/workspace/workspace.d.ts +63 -121
  74. package/build/workspace/workspace.js +45 -48
  75. package/build/workspace/workspaceVerifier.d.ts +22 -0
  76. package/build/workspace/workspaceVerifier.js +65 -0
  77. package/node_modules/playwright/lib/dom-analyzer/blueprint.js +7 -6
  78. package/node_modules/playwright/lib/dom-analyzer/dynamicId.js +5 -4
  79. package/node_modules/playwright/lib/dom-analyzer/dynamicId.test.js +12 -0
  80. package/node_modules/playwright/lib/mcp/skyramp/dataSafetyGuard.js +822 -0
  81. package/node_modules/playwright/lib/mcp/skyramp/dataSafetySession.js +372 -0
  82. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +101 -8
  83. package/node_modules/playwright/lib/mcp/skyramp/finishTestSetupTool.js +45 -0
  84. package/node_modules/playwright/lib/mcp/skyramp/index.js +16 -0
  85. package/node_modules/playwright/lib/mcp/skyramp/recordingPhase.js +65 -0
  86. package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +1 -0
  87. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +598 -127
  88. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +85 -31
  89. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +132 -5
  90. package/node_modules/playwright/node_modules/playwright-core/lib/cli/program.js +2 -0
  91. package/node_modules/playwright/node_modules/playwright-core/lib/client/browserContext.js +4 -2
  92. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  93. package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
  94. package/node_modules/playwright/node_modules/playwright-core/lib/protocol/validator.js +2 -1
  95. package/node_modules/playwright/node_modules/playwright-core/lib/server/browserContext.js +17 -1
  96. package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +1 -0
  97. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harRecorder.js +1 -0
  98. package/node_modules/playwright/node_modules/playwright-core/lib/server/har/harTracer.js +1 -1
  99. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderRunner.js +2 -1
  100. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderSignalProcessor.js +2 -1
  101. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/clickExpandedState.js +75 -0
  102. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder.js +4 -2
  103. package/node_modules/playwright/node_modules/playwright-core/lib/utils/isomorphic/volatileDate.js +85 -0
  104. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bou8kKzE.js → codeMirrorModule-BuhAdckO.js} +1 -1
  105. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-COAHN430.js +809 -0
  106. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.0MJW0uG7.js → index.7AU8h0PZ.js} +1 -1
  107. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  108. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.GLqsx5cI.js → uiMode.Dl7smBJn.js} +1 -1
  109. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  110. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  111. package/node_modules/playwright/node_modules/playwright-core/src/cli/program.ts +5 -0
  112. package/node_modules/playwright/node_modules/playwright-core/src/client/browserContext.ts +3 -1
  113. package/node_modules/playwright/node_modules/playwright-core/src/client/types.ts +2 -0
  114. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  115. package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
  116. package/node_modules/playwright/node_modules/playwright-core/src/protocol/validator.ts +1 -0
  117. package/node_modules/playwright/node_modules/playwright-core/src/server/browserContext.ts +25 -1
  118. package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +2 -0
  119. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harRecorder.ts +1 -0
  120. package/node_modules/playwright/node_modules/playwright-core/src/server/har/harTracer.ts +3 -1
  121. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderRunner.ts +3 -1
  122. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderSignalProcessor.ts +1 -0
  123. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/clickExpandedState.ts +82 -0
  124. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder.ts +2 -0
  125. package/node_modules/playwright/node_modules/playwright-core/src/utils/isomorphic/volatileDate.ts +137 -1
  126. package/node_modules/playwright/package.json +1 -1
  127. package/package.json +4 -4
  128. package/plugin/prompts/plan-tests.md +2 -2
  129. package/plugin/prompts/testbot-task1.md +1 -1
  130. package/plugin/skills/enhance-assertions/reference/integration.md +1 -0
  131. package/plugin/skills/setup-testbot-target-sut/SKILL.md +60 -0
  132. package/plugin/skills/setup-testbot-target-sut/reference/ci-workflow.md +26 -0
  133. package/plugin/skills/setup-testbot-target-sut/reference/deployed-environment.md +105 -0
  134. package/plugin/skills/setup-testbot-target-sut/reference/existing-tests.md +14 -0
  135. package/plugin/skills/setup-testbot-target-sut/reference/troubleshooting.md +20 -0
  136. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +0 -2
  137. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +0 -178
  138. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +0 -2
  139. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +0 -311
  140. package/build/prompts/sut-setup/shared.d.ts +0 -62
  141. package/build/prompts/sut-setup/shared.js +0 -196
  142. package/build/resources/sutSetupResource.d.ts +0 -2
  143. package/build/resources/sutSetupResource.js +0 -63
  144. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DTp2-8Si.js +0 -809
@@ -9,7 +9,7 @@ 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 { EnvironmentFamily, FailureCauseKind, IssueFoundCategory, } from "../types/TestbotReport.js";
12
+ import { 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";
@@ -115,46 +115,17 @@ const repositoryField = z
115
115
  .trim()
116
116
  .min(1)
117
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.");
144
118
  // videoPath is deliberately absent from this input contract: it is attached server-side
145
119
  // from the run's execution records (see attachVideoPath), and zod strips any the model
146
120
  // supplies anyway. SKYR-4156 is what happens when the agent owns that field instead.
147
- const testResultSchema = z
148
- .object({
121
+ const testResultSchema = z.object({
149
122
  testType: z
150
123
  .enum(TestType)
151
124
  .describe("Type of test. Do not include priority or other metadata in this field."),
152
125
  endpoint: z
153
126
  .string()
154
127
  .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'."),
155
- status: z
156
- .enum(["Pass", "Fail", "Skipped"])
157
- .describe("Test execution result"),
128
+ status: z.enum(["Pass", "Fail", "Skipped"]).describe("Test execution result"),
158
129
  details: z
159
130
  .string()
160
131
  .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'"),
@@ -174,76 +145,7 @@ const testResultSchema = z
174
145
  .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. " +
175
146
  "`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. " +
176
147
  "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(),
178
148
  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
- }
247
149
  });
248
150
  // Structured grounding for UI recommendations. Lifted verbatim from a
249
151
  // `browser_blueprint` element captured during this run. Source of truth for
@@ -486,25 +388,8 @@ const issueFoundSchema = z
486
388
  .describe("1-based line number in sourceFile. Optional and advisory. " +
487
389
  "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. " +
488
390
  "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),
494
391
  })
495
392
  .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
- }
508
393
  // A `bug` entry asserts that product code is wrong, so it has to name the code:
509
394
  // without a file the entry is prose a reviewer cannot act on, and nothing
510
395
  // downstream can place it in the diff. Only the file is required — a config file
@@ -2865,10 +2750,7 @@ export function registerSubmitReportTool(server) {
2865
2750
  // these objects and don't expect it).
2866
2751
  testResults: params.testResults.map((row) => {
2867
2752
  const { testFilePath: _tfp, ...wire } = attachVideoPath(row, stateData.executionVideos);
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) };
2753
+ return wire;
2872
2754
  }),
2873
2755
  issuesFound,
2874
2756
  executionCoverage,
@@ -6,6 +6,7 @@ import { logger } from "../../utils/logger.js";
6
6
  import { canonicalJson, isPlainObject } from "../../utils/canonicalJson.js";
7
7
  import { StateManager, resolveOwnRunStatePath, } from "../../utils/AnalysisStateManager.js";
8
8
  import { toolError } from "../../utils/utils.js";
9
+ import { AZURE_REPOSITORY_SLUG, isRepositorySlug, } from "../../utils/repositorySlug.js";
9
10
  import { TESTBOT_SUPPORTED_TEST_TYPES } from "../../types/TestTypes.js";
10
11
  import { changedFileNames, computeBranchDiff, } from "../../utils/branchDiff.js";
11
12
  import { collectChangedRuns } from "../../utils/changedRuns.js";
@@ -95,16 +96,27 @@ const registerStepSchema = z
95
96
  });
96
97
  }
97
98
  });
98
- /** The `owner/repo` a plan entry names. The SHAPE is checked, not just "not blank":
99
- * a run keys each repository by the slug read off its origin remote. */
99
+ /** An `org/project/repo` slug is right only on a run whose own repository is one:
100
+ * on a GitHub run it is a typo, which the shape alone cannot tell. */
101
+ function threePartSlugOffRun(params, runRepository) {
102
+ if (runRepository && AZURE_REPOSITORY_SLUG.test(runRepository)) {
103
+ return undefined;
104
+ }
105
+ return [...(params.plannedTests ?? []), ...(params.maintains ?? [])]
106
+ .map((entry) => entry?.repository)
107
+ .find((repository) => typeof repository === "string" && repository.split("/").length === 3);
108
+ }
109
+ /** The `owner/repo` (or Azure DevOps `org/project/repo`) a plan entry names. The
110
+ * SHAPE is checked, not just "not blank": a run keys each repository by the slug
111
+ * read off its origin remote. */
100
112
  function repositorySlugField(subject, purpose) {
101
113
  return z
102
114
  .string()
103
115
  .trim()
104
- .regex(/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/, {
105
- message: `${subject}'s repository must be an \`owner/repo\` slug`,
116
+ .refine(isRepositorySlug, {
117
+ message: `${subject}'s repository must be an \`owner/repo\` slug, or \`org/project/repo\` on Azure DevOps`,
106
118
  })
107
- .describe(`The github repository slug, \`owner/repo\`, ${purpose}.`);
119
+ .describe(`The repository slug, \`owner/repo\` (\`org/project/repo\` on Azure DevOps), ${purpose}.`);
108
120
  }
109
121
  /** The `changes` and `defects` id lists a plan entry cites, written once. The two
110
122
  * copies drifted whenever either was edited. */
@@ -902,7 +914,7 @@ export function registerRegisterTestPlanTool(server) {
902
914
  },
903
915
  description: "Register your complete test plan for this run, with the changes that make each planned test checkable. " +
904
916
  "The planning procedure in your prompt says how to decide and declare the plan. " +
905
- "Every registration the schema accepts is stored, and no check ever refuses one. " +
917
+ "Every registration the schema accepts is stored, and no check ever refuses one, except an Azure DevOps `org/project/repo` on a run that is not on Azure DevOps. " +
906
918
  "The schema refuses the whole call, and names the field and the rule that refused it. " +
907
919
  "An undeclared key is refused by name on a planned test and on `declarations`, so a declaration written one level too high is corrected rather than dropped in silence. " +
908
920
  "Checks run against the stored plan and return objections. Fix one and register it again as often as you like, or answer it by id through `answers`. " +
@@ -926,6 +938,10 @@ export function registerRegisterTestPlanTool(server) {
926
938
  if (!fullState) {
927
939
  return toolError(`State file is empty or invalid: ${stateFile}. Call skyramp_analyze_changes first to generate a valid state file.`);
928
940
  }
941
+ const offRun = threePartSlugOffRun(params, fullState.metadata?.repository);
942
+ if (offRun) {
943
+ return toolError(`\`${offRun}\`: this run's repository must be an \`owner/repo\` slug; \`org/project/repo\` names an Azure DevOps repository, and this run is not on Azure DevOps.`);
944
+ }
929
945
  const registrationNumber = (fullState.registrationCount ?? 0) + 1;
930
946
  const registration = buildRegistration(params, registrationNumber);
931
947
  // The previous registration's answers are already on the stored plan, which
@@ -1,4 +1,14 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
- export declare function issueScanToken(): string;
2
+ import { type ScanRelatedRepository } from "../../prompts/initialize-workspace/initializeWorkspacePrompt.js";
3
+ /** What a scan token was issued for: when it expires, and the related
4
+ * repositories its scan named, which skyramp_init_workspace verifies the
5
+ * services against. */
6
+ export interface ScanGrant {
7
+ expiry: number;
8
+ relatedRepositories: ScanRelatedRepository[];
9
+ }
10
+ export declare function issueScanToken(relatedRepositories?: ScanRelatedRepository[]): string;
11
+ /** The grant of a valid, unexpired token, without consuming it. */
12
+ export declare function scanTokenGrant(token: string): ScanGrant | undefined;
3
13
  export declare function validateAndConsumeScanToken(token: string): boolean;
4
14
  export declare function registerInitScanWorkspaceTool(server: McpServer): void;
@@ -1,24 +1,33 @@
1
1
  import { z } from "zod";
2
2
  import { randomUUID } from "crypto";
3
+ import fs from "fs";
3
4
  import { WorkspaceConfigManager } from "../../workspace/workspace.js";
5
+ import { AZURE_REPOSITORY_SLUG, resolveRealPath, tryRepositorySlug, } from "../../utils/repositorySlug.js";
4
6
  import { logger } from "../../utils/logger.js";
5
7
  import { AnalyticsService } from "../../services/AnalyticsService.js";
6
- import { INIT_WORKSPACE_INSTRUCTIONS } from "../../prompts/initialize-workspace/initializeWorkspacePrompt.js";
8
+ import { buildInitWorkspaceInstructions, } from "../../prompts/initialize-workspace/initializeWorkspacePrompt.js";
7
9
  const TOOL_NAME = "skyramp_init_scan";
8
- // Token store: token -> expiry timestamp (ms)
9
10
  const scanTokens = new Map();
10
11
  const TOKEN_TTL_MS = 10 * 60 * 1000; // 10 minutes
11
- export function issueScanToken() {
12
+ export function issueScanToken(relatedRepositories = []) {
12
13
  const token = randomUUID();
13
- scanTokens.set(token, Date.now() + TOKEN_TTL_MS);
14
+ scanTokens.set(token, {
15
+ expiry: Date.now() + TOKEN_TTL_MS,
16
+ relatedRepositories,
17
+ });
14
18
  return token;
15
19
  }
20
+ /** The grant of a valid, unexpired token, without consuming it. */
21
+ export function scanTokenGrant(token) {
22
+ const grant = scanTokens.get(token);
23
+ return grant && Date.now() < grant.expiry ? grant : undefined;
24
+ }
16
25
  export function validateAndConsumeScanToken(token) {
17
- const expiry = scanTokens.get(token);
18
- if (!expiry)
26
+ const grant = scanTokens.get(token);
27
+ if (!grant)
19
28
  return false;
20
29
  scanTokens.delete(token);
21
- return Date.now() < expiry;
30
+ return Date.now() < grant.expiry;
22
31
  }
23
32
  const initScanWorkspaceSchema = z.object({
24
33
  workspacePath: z.string().describe("Path to the workspace/repository root"),
@@ -26,12 +35,77 @@ const initScanWorkspaceSchema = z.object({
26
35
  .boolean()
27
36
  .default(false)
28
37
  .describe("Set to true to force a re-scan even if .skyramp/workspace.yml already exists. Default is false."),
38
+ relatedRepositories: z
39
+ .array(z.object({
40
+ repositoryPath: z
41
+ .string()
42
+ .describe("Path to the related repository's checkout"),
43
+ repository: z
44
+ .string()
45
+ .optional()
46
+ .describe("The related repository's owner/repo, or org/project/repo when workspacePath is on Azure DevOps. Defaults to the one its checkout's origin remote names."),
47
+ }))
48
+ .optional()
49
+ .describe("The other repositories of a multi-repository Target. Each is scanned into the one workspace.yml at workspacePath, its services carrying its owner/repo (org/project/repo on Azure DevOps) as `repository`; none gets a workspace.yml of its own. Omit for a single repository."),
50
+ testRepositoryPath: z
51
+ .string()
52
+ .optional()
53
+ .describe("Path to the separate test repository generated tests are delivered into, when there is one. Each service's testDirectory is then read from, and relative to, that checkout."),
29
54
  });
55
+ const OWNER_REPO = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
56
+ /** `owner/repo`, or `org/project/repo` when the workspace itself is on Azure
57
+ * DevOps: on any other workspace a three-part name is a typo. */
58
+ function isRelatedRepositorySlug(repository, workspaceRepository) {
59
+ if (OWNER_REPO.test(repository))
60
+ return true;
61
+ return (!!workspaceRepository &&
62
+ AZURE_REPOSITORY_SLUG.test(workspaceRepository) &&
63
+ AZURE_REPOSITORY_SLUG.test(repository));
64
+ }
65
+ function isDirectory(p) {
66
+ try {
67
+ return fs.statSync(p).isDirectory();
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Each related repository with its owner/repo, or the first problem with them:
75
+ * a checkout that is not a directory, is the workspace itself, or names no
76
+ * owner/repo, and a repository listed twice. Paths compare with symlinks
77
+ * resolved, and owner/repo case-insensitively, as GitHub does.
78
+ */
79
+ async function resolveRelatedRepositories(workspacePath, entries) {
80
+ const repos = [];
81
+ const workspaceReal = await resolveRealPath(workspacePath);
82
+ const workspaceRepository = await tryRepositorySlug(workspacePath);
83
+ for (const entry of entries) {
84
+ const at = entry.repositoryPath;
85
+ if (!isDirectory(at))
86
+ return { error: `relatedRepositories: ${at} is not a directory` };
87
+ if ((await resolveRealPath(at)) === workspaceReal)
88
+ return {
89
+ error: `relatedRepositories: ${at} is workspacePath itself; list only the other repositories`,
90
+ };
91
+ const repository = entry.repository?.trim() || (await tryRepositorySlug(at));
92
+ if (!repository ||
93
+ !isRelatedRepositorySlug(repository, workspaceRepository))
94
+ return {
95
+ error: `relatedRepositories: no owner/repo for ${at}${repository ? ` ("${repository}" is not owner/repo, or org/project/repo on an Azure DevOps workspace)` : " (its checkout has no origin remote)"}; pass its repository`,
96
+ };
97
+ if (repos.some((r) => r.repository.toLowerCase() === repository.toLowerCase()))
98
+ return { error: `relatedRepositories: ${repository} is listed twice` };
99
+ repos.push({ repository, repositoryPath: at });
100
+ }
101
+ return { repos };
102
+ }
30
103
  export function registerInitScanWorkspaceTool(server) {
31
104
  server.registerTool(TOOL_NAME, {
32
105
  description: "Call this FIRST, before any other Skyramp tool (test generation, execution, analysis), whenever workspacePath is a git repository and .skyramp/workspace.yml does not exist at its root. " +
33
106
  "PREREQUISITE for skyramp_init_workspace. " +
34
107
  "Scans a repository and returns instructions for discovering all services, along with a scanToken. " +
108
+ "For a Target spanning several repositories, call it ONCE with the others in relatedRepositories, so all their services go into one workspace.yml. " +
35
109
  "Follow the instructions to build the services array, then pass both to skyramp_init_workspace to create the workspace config.",
36
110
  inputSchema: initScanWorkspaceSchema,
37
111
  }, async (params) => {
@@ -39,6 +113,8 @@ export function registerInitScanWorkspaceTool(server) {
39
113
  const recordParams = {
40
114
  workspacePath: params.workspacePath,
41
115
  force: String(params.force || false),
116
+ relatedRepositories: String(params.relatedRepositories?.length ?? 0),
117
+ testRepository: String(Boolean(params.testRepositoryPath)),
42
118
  };
43
119
  try {
44
120
  const manager = new WorkspaceConfigManager(params.workspacePath);
@@ -53,12 +129,38 @@ export function registerInitScanWorkspaceTool(server) {
53
129
  isError: false,
54
130
  };
55
131
  }
56
- const token = issueScanToken();
132
+ const related = await resolveRelatedRepositories(params.workspacePath, params.relatedRepositories ?? []);
133
+ if ("error" in related) {
134
+ errorResult = {
135
+ content: [{ type: "text", text: related.error }],
136
+ isError: true,
137
+ };
138
+ return errorResult;
139
+ }
140
+ if (params.testRepositoryPath &&
141
+ !isDirectory(params.testRepositoryPath)) {
142
+ errorResult = {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: `testRepositoryPath: ${params.testRepositoryPath} is not a directory`,
147
+ },
148
+ ],
149
+ isError: true,
150
+ };
151
+ return errorResult;
152
+ }
153
+ const token = issueScanToken(related.repos);
154
+ const instructions = buildInitWorkspaceInstructions({
155
+ workspacePath: params.workspacePath,
156
+ relatedRepositories: related.repos,
157
+ testRepositoryPath: params.testRepositoryPath,
158
+ });
57
159
  return {
58
160
  content: [
59
161
  {
60
162
  type: "text",
61
- text: `SCAN TOKEN: ${token}\n\nSave this token — you MUST pass it as scanToken when calling skyramp_init_workspace.\n\n${INIT_WORKSPACE_INSTRUCTIONS}`,
163
+ text: `SCAN TOKEN: ${token}\n\nSave this token — you MUST pass it as scanToken when calling skyramp_init_workspace.\n\n${instructions}`,
62
164
  },
63
165
  ],
64
166
  isError: false,
@@ -7,7 +7,8 @@ import yaml from "js-yaml";
7
7
  import { logger } from "../../utils/logger.js";
8
8
  import { AnalyticsService } from "../../services/AnalyticsService.js";
9
9
  import { SKYRAMP_IMAGE_VERSION } from "../../utils/versions.js";
10
- import { validateAndConsumeScanToken } from "./initScanWorkspaceTool.js";
10
+ import { scanTokenGrant, validateAndConsumeScanToken, } from "./initScanWorkspaceTool.js";
11
+ import { verifyWorkspaceServices } from "../../workspace/workspaceVerifier.js";
11
12
  import { upsertServicesByRepo } from "./serviceUpsert.js";
12
13
  import { isInsideDir } from "../../utils/gitStaging.js";
13
14
  import { EXECUTOR_VIDEOS_DIR, EXECUTOR_WORK_DIR, isInsideExecutorWorkDir, } from "../../utils/executorWorkDir.js";
@@ -110,7 +111,7 @@ export function registerInitializeWorkspaceTool(server) {
110
111
  "1. Fresh init: PREREQUISITE: Call skyramp_init_scan first to get a scanToken and discover services. " +
111
112
  "Then call this tool to write .skyramp/workspace.yml with the discovered services and scanToken. " +
112
113
  "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). " +
113
- "The services schema is strict: any field not in the schema (e.g. inventing `serverStopCommand` on `runtimeDetails`) fails validation and the call is rejected.",
114
+ "The services schema is strict: any field not in the schema (e.g. inventing `healthUrl` on `runtimeDetails`) fails validation and the call is rejected.",
114
115
  inputSchema: initializeWorkspaceSchema,
115
116
  _meta: {
116
117
  keywords: [
@@ -182,11 +183,46 @@ export function registerInitializeWorkspaceTool(server) {
182
183
  if (badMerged)
183
184
  return badTestDirectoryResult(badMerged);
184
185
  }
186
+ // The verifier, before the token is spent: a refusal asks for a
187
+ // corrected retry, which must be able to reuse the same token. Only the
188
+ // incoming services: a merge must not be blocked by an older entry.
189
+ const grant = params.scanToken
190
+ ? scanTokenGrant(params.scanToken)
191
+ : undefined;
192
+ const issues = verifyWorkspaceServices(params.services, {
193
+ workspacePath,
194
+ relatedRepositories: grant?.relatedRepositories,
195
+ });
196
+ if (issues.length > 0) {
197
+ return {
198
+ content: [
199
+ {
200
+ type: "text",
201
+ text: `${manager.getConfigPath()} was not written: ${issues.length} problem(s) with the services.\n${issues.map((i) => `- ${i}`).join("\n")}\nFix each one and call skyramp_init_workspace again with the full corrected services array${grant ? " and the same scanToken, which is still valid" : ""}.`,
202
+ },
203
+ ],
204
+ isError: true,
205
+ };
206
+ }
185
207
  // scanToken is required for fresh init; for edits (force:true on an
186
208
  // existing workspace.yml) the caller supplies the full services array
187
- // directly and we skip the token check.
209
+ // directly and no token is needed. A token passed with an edit is still
210
+ // validated and spent, so a successful write never leaves it reusable.
188
211
  const isEdit = alreadyExists && params.force;
189
- if (!isEdit) {
212
+ if (isEdit && params.scanToken) {
213
+ if (!validateAndConsumeScanToken(params.scanToken)) {
214
+ return {
215
+ content: [
216
+ {
217
+ type: "text",
218
+ text: "Invalid or expired scanToken. An edit of an existing workspace.yml needs none: call again with force: true and no scanToken.",
219
+ },
220
+ ],
221
+ isError: true,
222
+ };
223
+ }
224
+ }
225
+ else if (!isEdit) {
190
226
  if (!params.scanToken) {
191
227
  return {
192
228
  content: [
@@ -45,6 +45,9 @@ export interface AssertionRecord {
45
45
  baselineCount?: number;
46
46
  baselineStrength?: number;
47
47
  baselineFingerprints?: string[];
48
+ /** Integration-teardown `tracker.track_for_cleanup(` lines at hand-out — must survive
49
+ * enhancement verbatim (verify hard-fails otherwise). */
50
+ baselineCreatedAddLines?: string[];
48
51
  issuedAt?: string;
49
52
  /** Verdict of the last verify pass; absent until one runs. */
50
53
  verification?: AssertionVerdict;
@@ -13,26 +13,6 @@ export declare enum IssueFoundCategory {
13
13
  Type = "type",
14
14
  Config = "config"
15
15
  }
16
- /** Why a Fail or Skipped testResults row did not pass. Application is the code
17
- * under test (or a test written to prove a defect, failing as intended);
18
- * Environment is the test environment stopping the test before it reached the
19
- * behaviour; TestDefect is the generated test itself being wrong. */
20
- export declare enum FailureCauseKind {
21
- Application = "application",
22
- Environment = "environment",
23
- TestDefect = "test_defect"
24
- }
25
- /** Which test-environment gap stopped a test: the `family` of an Environment
26
- * failureCause, and the `environmentFamily` of a config issuesFound entry. */
27
- export declare enum EnvironmentFamily {
28
- AuthToken = "auth_token",
29
- Secret = "secret",
30
- DataConnection = "data_connection",
31
- AppNotStarted = "app_not_started",
32
- Login = "login",
33
- ExternalDependency = "external_dependency",
34
- RecordingReplay = "recording_replay"
35
- }
36
16
  /**
37
17
  * Shape of the JSON report written by skyramp_submit_report and read by testbot
38
18
  * for rendering as Markdown. All fields mirror the corresponding Zod schemas in
@@ -124,19 +104,6 @@ export interface TestbotReport {
124
104
  endpoint: string;
125
105
  status: "Pass" | "Fail" | "Skipped";
126
106
  details: string;
127
- /** Server-derived: the basename of the `testFilePath` the agent submitted,
128
- * which never reaches the report. The join key to `newTestsCreated` and
129
- * `testMaintenance`: match it against the basename of their `fileName`
130
- * (a `newTestsCreated` row may carry a repository-relative path), plus
131
- * `repository`. */
132
- fileName: string;
133
- /** Why a Fail or Skipped row did not pass. The submit schema requires it on
134
- * those rows and refuses it on a Pass row; `family` is set exactly when
135
- * `kind` is Environment. Absent in reports from MCP versions predating it. */
136
- failureCause?: {
137
- kind: FailureCauseKind;
138
- family?: EnvironmentFamily;
139
- };
140
107
  videoPath?: string;
141
108
  /** See newTestsCreated[].repository. */
142
109
  repository: string;
@@ -191,10 +158,6 @@ export interface TestbotReport {
191
158
  sourceFile?: string;
192
159
  sourceSymbol?: string;
193
160
  sourceLine?: number;
194
- /** On a `config` entry that is a test-environment gap, which gap. The submit
195
- * schema refuses it on any other category. Optional: a config entry need
196
- * not be an environment gap, and older MCP versions never set it. */
197
- environmentFamily?: EnvironmentFamily;
198
161
  /** See newTestsCreated[].repository. */
199
162
  repository: string;
200
163
  }[];
@@ -8,25 +8,3 @@ export var IssueFoundCategory;
8
8
  IssueFoundCategory["Type"] = "type";
9
9
  IssueFoundCategory["Config"] = "config";
10
10
  })(IssueFoundCategory || (IssueFoundCategory = {}));
11
- /** Why a Fail or Skipped testResults row did not pass. Application is the code
12
- * under test (or a test written to prove a defect, failing as intended);
13
- * Environment is the test environment stopping the test before it reached the
14
- * behaviour; TestDefect is the generated test itself being wrong. */
15
- export var FailureCauseKind;
16
- (function (FailureCauseKind) {
17
- FailureCauseKind["Application"] = "application";
18
- FailureCauseKind["Environment"] = "environment";
19
- FailureCauseKind["TestDefect"] = "test_defect";
20
- })(FailureCauseKind || (FailureCauseKind = {}));
21
- /** Which test-environment gap stopped a test: the `family` of an Environment
22
- * failureCause, and the `environmentFamily` of a config issuesFound entry. */
23
- export var EnvironmentFamily;
24
- (function (EnvironmentFamily) {
25
- EnvironmentFamily["AuthToken"] = "auth_token";
26
- EnvironmentFamily["Secret"] = "secret";
27
- EnvironmentFamily["DataConnection"] = "data_connection";
28
- EnvironmentFamily["AppNotStarted"] = "app_not_started";
29
- EnvironmentFamily["Login"] = "login";
30
- EnvironmentFamily["ExternalDependency"] = "external_dependency";
31
- EnvironmentFamily["RecordingReplay"] = "recording_replay";
32
- })(EnvironmentFamily || (EnvironmentFamily = {}));
@@ -2,7 +2,7 @@ export { TestExecutionStatus } from "./TestExecution.js";
2
2
  export { DriftAction } from "./TestAnalysis.js";
3
3
  export { TestType, HttpMethod } from "./TestTypes.js";
4
4
  export type { TestbotReport } from "./TestbotReport.js";
5
- export { IssueFoundCategory, FailureCauseKind, EnvironmentFamily, } from "./TestbotReport.js";
5
+ export { IssueFoundCategory } from "./TestbotReport.js";
6
6
  export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
7
7
  export type { ReuseOutcome, ReuseSkippedEntry, ReuseMissedEntry, HelperReuseOutcome, RetrofitOutcome, ChangedHelperEntry, ChangedHelperCaller, OrphanedHelper, } from "./ReuseOutcome.js";
8
8
  export type { RelatedRepository, TestbotPromptOptions, } from "./TestbotPromptOptions.js";
@@ -1,5 +1,5 @@
1
1
  export { TestExecutionStatus } from "./TestExecution.js";
2
2
  export { DriftAction } from "./TestAnalysis.js";
3
3
  export { TestType, HttpMethod } from "./TestTypes.js";
4
- export { IssueFoundCategory, FailureCauseKind, EnvironmentFamily, } from "./TestbotReport.js";
4
+ export { IssueFoundCategory } from "./TestbotReport.js";
5
5
  export { ReuseDeclinedBy, ReuseVerificationOutcome, HelperVerificationOutcome, } from "./ReuseOutcome.js";
@@ -0,0 +1,12 @@
1
+ import type { LintFinding } from "./lint-types.js";
2
+ /** Each whole `tracker.track_for_cleanup(...)` / `tracker.trackForCleanup(...)`
3
+ * call of a file, in order — the baseline the enhanced file is compared
4
+ * against. */
5
+ export declare function collectCreatedAddLines(content: string): string[];
6
+ /**
7
+ * Hard findings when the enhanced file's registration lines differ from the
8
+ * baseline's (sequence must match exactly: same lines, same count, same order)
9
+ * or when anything but blank or comment lines sits between an id capture and
10
+ * its registration. With no baseline only the adjacency check runs.
11
+ */
12
+ export declare function lintCreatedAddLines(content: string, baselineLines?: string[]): LintFinding[];