@skyramp/mcp 0.2.7 → 0.2.8

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 (90) hide show
  1. package/build/index.js +5 -2
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +24 -5
  3. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.d.ts +2 -0
  4. package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +114 -0
  5. package/build/prompts/sut-setup/modes/dockerComposePrompt.d.ts +2 -0
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +292 -0
  7. package/build/prompts/sut-setup/shared.d.ts +39 -0
  8. package/build/prompts/sut-setup/shared.js +132 -0
  9. package/build/prompts/test-maintenance/driftAnalysisSections.js +9 -3
  10. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -1
  11. package/build/prompts/test-recommendation/diffExecutionPlan.js +76 -2
  12. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +91 -0
  13. package/build/prompts/testbot/testbot-prompts.d.ts +16 -1
  14. package/build/prompts/testbot/testbot-prompts.js +113 -22
  15. package/build/prompts/testbot/testbot-prompts.test.js +95 -45
  16. package/build/resources/sutSetupResource.d.ts +2 -0
  17. package/build/resources/sutSetupResource.js +45 -0
  18. package/build/resources/testbotResource.js +2 -2
  19. package/build/services/TestDiscoveryService.d.ts +2 -2
  20. package/build/services/TestDiscoveryService.js +22 -12
  21. package/build/services/TestExecutionService.d.ts +10 -1
  22. package/build/services/TestExecutionService.js +158 -26
  23. package/build/services/TestExecutionService.test.js +306 -0
  24. package/build/tools/executeSkyrampTestTool.js +26 -7
  25. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  26. package/build/tools/submitReportTool.d.ts +10 -0
  27. package/build/tools/submitReportTool.js +106 -39
  28. package/build/tools/submitReportTool.test.js +296 -50
  29. package/build/tools/test-management/actionsTool.js +21 -7
  30. package/build/tools/test-management/analyzeChangesTool.d.ts +1 -1
  31. package/build/tools/test-management/analyzeChangesTool.js +106 -41
  32. package/build/tools/test-management/analyzeChangesTool.test.js +21 -41
  33. package/build/tools/test-management/analyzeTestHealthTool.js +16 -9
  34. package/build/tools/workspace/initializeWorkspaceTool.js +39 -22
  35. package/build/tools/workspace/serviceUpsert.d.ts +12 -0
  36. package/build/tools/workspace/serviceUpsert.js +23 -0
  37. package/build/tools/workspace/serviceUpsert.test.d.ts +1 -0
  38. package/build/tools/workspace/serviceUpsert.test.js +50 -0
  39. package/build/types/TestAnalysis.d.ts +2 -1
  40. package/build/types/TestExecution.d.ts +12 -2
  41. package/build/types/TestExecution.js +11 -1
  42. package/build/types/TestbotReport.d.ts +68 -0
  43. package/build/types/TestbotReport.js +1 -0
  44. package/build/types/index.d.ts +4 -0
  45. package/build/types/index.js +3 -0
  46. package/build/utils/AnalysisStateManager.d.ts +61 -1
  47. package/build/utils/AnalysisStateManager.js +144 -3
  48. package/build/utils/AnalysisStateManager.test.js +98 -0
  49. package/build/utils/docker.test.js +1 -1
  50. package/build/utils/routeParsers.d.ts +0 -10
  51. package/build/utils/routeParsers.js +31 -38
  52. package/build/utils/scenarioDrafting.d.ts +1 -1
  53. package/build/utils/scenarioDrafting.js +23 -1
  54. package/build/utils/utils.d.ts +7 -1
  55. package/build/utils/utils.js +10 -0
  56. package/build/utils/versions.d.ts +3 -3
  57. package/build/utils/versions.js +1 -1
  58. package/build/workspace/workspace.d.ts +10 -10
  59. package/build/workspace/workspace.js +7 -7
  60. package/build/workspace/workspace.test.js +6 -6
  61. package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +31 -13
  62. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +88 -15
  63. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  64. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  65. package/node_modules/playwright/node_modules/playwright-core/lib/generated/injectedScriptSource.js +1 -1
  66. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +79 -19
  67. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/aliasRemapper.js +84 -0
  68. package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +15 -4
  69. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-CPZKmjFR.js → codeMirrorModule-D0BjbCb7.js} +1 -1
  70. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +193 -0
  71. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-Cqp3cwEJ.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CYf9adZh.js} +79 -79
  74. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.B_7ywgmr.js} +1 -1
  75. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  76. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  77. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.u4_8VnCV.js} +1 -1
  78. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  79. package/node_modules/playwright/node_modules/playwright-core/src/generated/injectedScriptSource.ts +1 -1
  80. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +129 -43
  81. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/aliasRemapper.ts +119 -0
  82. package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +33 -10
  83. package/node_modules/playwright/package.json +1 -1
  84. package/package.json +10 -3
  85. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  86. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  87. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  88. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  89. package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-CsgPR2wy.js +0 -193
  90. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { AnalyticsService } from "../../services/AnalyticsService.js";
4
4
  import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PARAM_UUID_GUIDANCE, AUTH_CONFLICT_ERROR_MSG, } from "../test-recommendation/recommendationSections.js";
5
- import { buildDriftAnalysisPrompt } from "../test-maintenance/drift-analysis-prompt.js";
6
5
  import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
7
6
  import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
8
7
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
@@ -18,9 +17,53 @@ const CONTRACT_MODE_GUIDANCE = CONSUMER_MODE_ENABLED
18
17
  For client-facing APIs consumed by frontend: add \`consumerMode: true\`.
19
18
  Both modes (\`providerMode: true, consumerMode: true\`): For diff that contains BOTH provider signals (such as new/modified endpoint handlers, route changes this service owns) AND consumer signals (outbound HTTP client calls to another service, no new endpoint handlers).`
20
19
  : ` Always add \`providerMode: true\` — the tool generates provider-side contract tests only.`;
20
+ /**
21
+ * Parse the JSON-encoded `relatedRepositories` argument passed via the testbot
22
+ * prompt/resource. Returns undefined for missing/blank input or malformed JSON so the
23
+ * caller cleanly falls back to single-repo behavior.
24
+ */
25
+ export function parseRelatedRepositories(raw) {
26
+ if (!raw)
27
+ return undefined;
28
+ try {
29
+ const parsed = JSON.parse(raw);
30
+ if (!Array.isArray(parsed) || parsed.length === 0)
31
+ return undefined;
32
+ return parsed
33
+ .filter((r) => !!r &&
34
+ typeof r.repositoryPath === "string" &&
35
+ r.repositoryPath.length > 0 &&
36
+ typeof r.repo === "string" &&
37
+ r.repo.length > 0)
38
+ .map((r) => ({
39
+ repositoryPath: r.repositoryPath,
40
+ baseBranch: typeof r.baseBranch === "string" ? r.baseBranch : undefined,
41
+ repo: r.repo,
42
+ }));
43
+ }
44
+ catch {
45
+ return undefined;
46
+ }
47
+ }
21
48
  export function getTestbotPrompt(prTitle, prDescription, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, _maxCritical = MAX_CRITICAL_TESTS, // Reserved — accepted for API compat but not yet wired into prompt
22
- prNumber, userPrompt, services, stateOutputFile, uiCredentials, testsRepoDir) {
49
+ prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo) {
23
50
  maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
51
+ // TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
52
+ // (src/prompts/test-recommendation/promptPlan.ts) so step numbers don't have
53
+ // to be hand-maintained when steps are added or reordered.
54
+ // Multi-repo mode: when related repos are present, the agent analyzes each one
55
+ // (primary + related) and correlates cross-repo changes. The configured checkout
56
+ // HEAD of each related repo is its feature ref; `baseBranch` is that repo's
57
+ // default branch, so skyramp_analyze_changes computes a real default…feature
58
+ // diff. Every repo carries an explicit `repo` (owner/repo) resolved action-side,
59
+ // so the agent never infers it. GENERATE candidates are pooled across the primary
60
+ // + related repos into one shared ranked budget; each generated test executes
61
+ // against its own repo's service when reachable. Two distinct roots: the unified
62
+ // .skyramp/workspace.yml ALWAYS lives in the primary repo (where the workflow is
63
+ // installed), while test FILES are delivered into each service's declared
64
+ // testDirectory, relative to the delivery root (the configured test repo if set,
65
+ // otherwise the primary repo).
66
+ const hasRelatedRepos = !!relatedRepositories?.length;
24
67
  // For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
25
68
  // For first-run prompts: emit the full Task 1 analysis + maintenance section.
26
69
  const task1Section = userPrompt
@@ -42,7 +85,7 @@ Verify the prompt inside <USER_PROMPT> is related to adding or removing tests fr
42
85
  - If the prompt matches one or more tests in the Additional Recommendations → proceed to Task 1 (Skip Analysis).
43
86
 
44
87
  ### Task 1: Retrieve Previous Recommendations
45
- Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${stateOutputFile ? `, \`stateOutputFile\`: "${stateOutputFile}"` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
88
+ Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
46
89
  This will fetch the previous TestBot report from the PR comments and return deduplicated recommendations.
47
90
  Use those recommendations as your baseline. Only add or remove tests that the user requested AND that appear in the Additional Recommendations. Then proceed straight to Task 2: Generate New Tests.
48
91
  `
@@ -51,7 +94,7 @@ Use those recommendations as your baseline. Only add or remove tests that the us
51
94
 
52
95
  ## Task 1: Analyze & Maintain
53
96
 
54
- 1. Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff", \`topN\`: ${maxRecommendations}, \`maxGenerate\`: ${maxGenerate}${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${stateOutputFile ? `, \`stateOutputFile\`: "${stateOutputFile}"` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
97
+ 1. Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff", \`topN\`: ${maxRecommendations}, \`maxGenerate\`: ${maxGenerate}${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
55
98
 
56
99
  **Follow all instructions returned by \`skyramp_analyze_changes\`** — its response opens with a **UI Blueprint Capture** section that tells you which candidate URLs to capture (and how) before you write any UI recommendation \`reasoning\`.${uiCredentials ? " If the section lists candidate URLs, log in once via the credentials in your <ui-credentials> context before navigating to them." : ""}
57
100
 
@@ -60,10 +103,36 @@ Use those recommendations as your baseline. Only add or remove tests that the us
60
103
 
61
104
  **If \`skyramp_analyze_changes\` returns an error:** retry once only if the error is transient (timeout, network blip, temporary unavailability) — do NOT retry for permanent errors (invalid repository path, missing required parameter, authentication failure). If it fails again, call \`skyramp_submit_report\` with a minimal valid payload: leave all test arrays empty and add the error to \`issuesFound\`. Refer to the \`skyramp_submit_report\` schema for required fields. Do NOT attempt Task 2 without a valid stateFile.
62
105
  **If all changed files are non-application** (CI/CD, docs, lock files, config) → skip to Task 3 (Submit Report) with empty arrays and a single \`issuesFound\` entry explaining why (same format as the zero-test path below).
106
+ ${hasRelatedRepos ? `
107
+ **MULTI-REPO CONTEXT (MANDATORY).** This run includes ${relatedRepositories.length} related ${relatedRepositories.length === 1 ? "repository" : "repositories"} listed in the \`<related_repositories>\` block below, each with an explicit \`repository\` (\`owner/repo\`), \`path\`, and \`base_branch\`. Use the \`repository\` value verbatim — do NOT infer it from git remotes or paths. You MUST analyze EACH related repository — exactly one \`skyramp_analyze_changes\` call per listed repo (${relatedRepositories.length} ${relatedRepositories.length === 1 ? "call" : "calls"}), in addition to the primary call in step 2.
108
+
109
+ **Run the primary call (step 2) FIRST, then the related repos in listed order — not in parallel.** All calls in this run automatically share ONE run-scoped state file — you do NOT pass a state-file path; setting \`repository\` is enough. The primary writes its root section; each related repo's call upserts its own section into that same file. Concurrent calls would race on the shared file, so they must be sequential. For each related repo:
110
+ - **(a)** Register its services into the **single unified workspace** — do NOT write a \`.skyramp/workspace.yml\` into the related repo (it is read-only context, and a per-repo file would need its own PR). The unified workspace always lives in the **primary repo** (where the testbot workflow is installed): \`${repositoryPath}\`${testsRepoDir ? ` — NOT in the configured test repo, even though generated test files are delivered there` : ""}. Steps: call \`skyramp_init_scan\` with \`workspacePath\`: the related repo's \`path\` to DISCOVER its services, then call \`skyramp_init_workspace\` with \`workspacePath\`: "${repositoryPath}" (the PRIMARY repo, NOT the related repo's path${testsRepoDir ? " and NOT the test repo" : ""}), \`merge\`: true, the \`scanToken\`, and the discovered services — each with its \`repository\` field set to that repo's \`repository\` value from the block. This upserts the related repo's services into the one unified \`.skyramp/workspace.yml\` (services are keyed by the composite of \`repository\` + \`serviceName\`).
111
+ - **(b)** Then call \`skyramp_analyze_changes\` with that repo's \`path\` (as \`repositoryPath\`), \`scope\`: "branch_diff", and \`repository\`: that repo's \`repository\` value. Passing \`repository\` stores this analysis as a section within the one run-scoped state file (the file is reused automatically across calls — do NOT pass any state-file path). Pass \`baseBranch\` only when the repo's \`base_branch\` attribute names a concrete branch; when \`base_branch\` is \`auto-detect\`, OMIT \`baseBranch\` and let the tool detect the repo's default branch.
112
+ - **(c)** If analysis of a related repo fails (e.g. workspace init or diff error), record a one-line \`issuesFound\` note and CONTINUE — a missing related repo must not block the primary analysis.
113
+ Keep ALL returned diffs and recommendations from every repo in your working context. **Correlate** cross-repo changes when forming recommendations and the business case — e.g. an API contract changed in one repo and consumed by another, a request/response shape that must stay compatible across the boundary.
114
+
115
+ **One state file, per-repo sections.** There is exactly ONE state file for the whole run (created automatically and reused across calls). Each \`skyramp_analyze_changes\` call writes its repo's section into it (keyed by \`repository\`). When you maintain/act on any repo's tests — \`skyramp_analyze_test_health\`, \`skyramp_actions\`, \`skyramp_execute_test\` — pass that repo's \`repository\` (\`owner/repo\`) as the \`repository\` argument alongside the \`stateFile\` path returned by \`skyramp_analyze_changes\`, so the tool reads/writes that repo's section. The primary repo's \`repository\` is \`${primaryRepo || "<the primary repo's owner/repo>"}\`; pass it for the primary repo's tests just as you do for related repos.
116
+
117
+ **Cross-repo test generation (Task 2 spans all repos).** Pool the GENERATE **and** ADDITIONAL recommendations from the primary AND every related repo into ONE candidate set — treat each repo's per-repo GENERATE list as **candidates, not a fixed selection**. (This OVERRIDES the "use the pre-ranked GENERATE list as-is" mandate in Task 2 below, which applies only to single-repo runs.) To fill the shared budget of ${maxGenerate} tests, distribute slots EVENLY across test types with spillover:
118
+ 1. **Bucket** the pooled candidates by test type (UI/E2E, contract, integration), including ONLY the types that actually have candidates.
119
+ 2. **Protected items first:** a promoted \`bug_caught\` (CRITICAL) test or an attack-surface \`security_boundary\` test always takes a GENERATE slot before round-robin fills the rest.
120
+ 3. **Round-robin** one slot per type in priority order — the highest-scored candidate of the highest-ranked type first — taking one per type per round.
121
+ 4. **Spillover:** when a type's candidates run out, its freed slots go to the next-highest-scored candidate of any remaining type.
122
+ Within a type, higher score wins regardless of which repo it came from. Everything not selected becomes an ADDITIONAL recommendation. This guarantees a frontend-only primary repo cannot starve a related backend repo's contract/integration tests of GENERATE slots (and vice versa). When you generate a test for a related repo's endpoint:
123
+ - **Execute it only if that repo's service is already running and reachable.** The workflow's setup may have started multiple services; before generating an API test for a related repo, confirm its \`base_url\` (from that repo's workspace/Execution Plan) responds. If the service is unreachable, still GENERATE the test but mark its \`testResults\` status as \`Skipped\` with details "service not running in this run" — do NOT count an unreachable service as a failure.
124
+ - **Write the test file into that service's own \`testDirectory\`** — the one declared for the service in the unified workspace.yml (the related repo's services were registered there in step (a), each with its \`repository\`). The \`testDirectory\` is interpreted relative to the **single delivery root** (the configured test repo if set, otherwise the primary repo), so all generated tests are delivered together by the existing single-target delivery. Do NOT invent a per-source-repo subdirectory, and do NOT write into the related repo's own checkout — it is read-only context. (If two repos happen to declare the same \`testDirectory\`, their files coexist there; the \`repository\` field on each report item — below — is what attributes ownership, not the path.)
125
+ - **Set the \`repository\` field** (\`owner/repo\`) on every such \`newTestsCreated\` / \`testResults\` item so the report attributes it to the originating repo (see Report Guidelines).` : ""}
63
126
 
64
127
  2. **Maintain existing tests:**
65
128
 
66
- ${buildDriftAnalysisPrompt()}
129
+ a. Call \`skyramp_analyze_test_health\` with \`stateFile\` (from \`skyramp_analyze_changes\` output). **Do NOT read application source files** (routes, models, controllers) — all change information you need is in the \`skyramp_analyze_changes\` output and the diff.
130
+
131
+ b. Write \`updateInstructions\` for each UPDATE or REGENERATE test before calling \`skyramp_actions\` — articulating the change first prevents file content from overriding diff-based reasoning.
132
+
133
+ c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every test whose action is UPDATE or REGENERATE. Run them in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.
134
+
135
+ d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
67
136
 
68
137
  3. **Code review:** From the \`skyramp_analyze_changes\` output and the existing test files you read for maintenance, note any logic bugs. Do NOT read additional source files just for code review — use what is already available from the analysis and test file reads. Common patterns to flag:
69
138
  - Computed fields not recalculated after mutation (e.g. \`total_amount\` unchanged after items are added/removed)
@@ -260,13 +329,25 @@ ${buildDriftAnalysisPrompt()}
260
329
  ? `<ui-credentials>\n${trimmedCredentials}\n</ui-credentials>`
261
330
  : '';
262
331
  const testsRepoDirBlock = testsRepoDir ? `<TESTS REPO DIR>${testsRepoDir}</TESTS REPO DIR>\n` : '';
332
+ // Multi-repo context block. Each entry's `repositoryPath` is checked out at its
333
+ // FEATURE ref; `baseBranch` is that repo's default branch (the diff base), so
334
+ // skyramp_analyze_changes computes a real default…feature diff for the related repo.
335
+ // `repository` is the owner/repo resolved action-side (no agent inference).
336
+ const relatedReposBlock = hasRelatedRepos
337
+ ? `<related_repositories>\n` +
338
+ relatedRepositories
339
+ .map((r) => ` <repository repository="${r.repo}" path="${r.repositoryPath}" base_branch="${r.baseBranch || "auto-detect"}" />`)
340
+ .join("\n") +
341
+ `\n</related_repositories>\n`
342
+ : '';
263
343
  const testDirInstruction = testsRepoDir
264
344
  ? `the \`<output_dir>\` from the \`<services>\` block, rooted under the test repository at \`${testsRepoDir}\` (i.e. \`${testsRepoDir}/<output_dir>\`). Write ALL test output files to paths under \`${testsRepoDir}\`, not under \`${repositoryPath}\`. Do NOT write any test files to the app repository.`
265
345
  : `${SERVICE_REFS.testDirRef}. Do NOT create a new \`tests/\` directory at the repo root — use that path. If no \`testDirectory\` is configured, default to the language-conventional location (e.g. \`src/test/java/...\` for Java, \`tests/\` for Python).`;
346
+ const primaryRepoBlock = primaryRepo ? `<REPOSITORY>${primaryRepo}</REPOSITORY>\n` : '';
266
347
  return `<TITLE>${prTitle}</TITLE>
267
348
  <DESCRIPTION>${prDescription}</DESCRIPTION>
268
- <REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
269
- ${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
349
+ ${primaryRepoBlock}<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
350
+ ${relatedReposBlock}${testsRepoDirBlock}${serviceContext ? serviceContext + '\n' : ''}${uiCredentialsBlock ? uiCredentialsBlock + '\n' : ''}Use the Skyramp MCP server tools for all tasks below.
270
351
 
271
352
  ${task1Section}
272
353
 
@@ -274,7 +355,7 @@ ${task1Section}
274
355
 
275
356
  ${userPrompt ? "Generate only the tests that the user requested from the Additional Recommendations. The rules below still apply." : "Drift-based maintenance (Task 1) is complete. This step only processes the GENERATE list. Exception: if a GENERATE item targets a resource with an existing `[skyramp]` contract test, UPDATE that test file (see covered-resource handling below) — a new test case added to an existing file counts toward the budget and is reported in `newTestsCreated`."}
276
357
 
277
- - **MANDATORY — use the pre-ranked GENERATE list as-is**: The Execution Plan's GENERATE section governs ADD actions. You MUST generate exactly those scenarios in the exact order listed. Do NOT substitute, rename, or replace a GENERATE item. If parameter grounding uncovers a distinct bug-catching scenario not already in the GENERATE or ADDITIONAL list, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.
358
+ - **MANDATORY — use the pre-ranked GENERATE list as-is**: The Execution Plan's GENERATE section governs ADD actions. You MUST generate exactly those scenarios in the exact order listed. Do NOT substitute, rename, or replace a GENERATE item. If parameter grounding uncovers a distinct bug-catching scenario not already in the GENERATE or ADDITIONAL list, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Follow that pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs the GENERATE list IS final — generate it exactly as-is.)` : ""}
278
359
  - Scenario JSON files are always new files — always generate them for new methods. Every generated scenario JSON must have a corresponding new integration test generated from it via \`skyramp_integration_test_generation\`.
279
360
  - Covered-resource handling (aligns with Execution Plan Step 0): When a GENERATE item targets a resource that already has an existing test file covering the same endpoint:
280
361
  - If the existing test source is \`[external]\`, skip the resource entirely — the external test already provides coverage. Do NOT UPDATE, REGENERATE, or DELETE external tests.
@@ -286,7 +367,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
286
367
  - Example: If enrichment reveals that sending \`discount_value\` without \`discount_type\` silently orphans the value (a concrete bug), complete all planned GENERATE items first, then generate this discovered scenario as an extra test and report it in \`newTestsCreated\`.
287
368
  - Total generated: Follow the "Budget: N generate" line in the Execution Plan. Process every GENERATE-tagged item in order. Backfill from ADDITIONAL candidates (highest-ranked first) until \`newTestsCreated\` reaches ${maxGenerate} or all candidates are exhausted.
288
369
  - **UI test priority**: If the PR scope assessment shows any UI/E2E budget OR \`uiContext.changedFrontendFiles\` is non-empty (the deterministic server signal — populated for all supported frontend file types including \`.tsx\`/\`.jsx\`/\`.vue\`/\`.svelte\`/\`.dart\`), you MUST attempt to generate at least one UI test. Use \`browser_navigate\` to the app's base URL — if the app responds, record a trace and generate the test.
289
- **Flutter web apps:** Skyramp's Playwright tools automatically enable Flutter's accessibility semantics tree on every \`browser_navigate\` call — you do NOT need to manually click \`flt-semantics-placeholder\` or add any activation step to the trace. Do NOT log an \`issuesFound\` entry about Flutter canvas rendering or accessibility activation — this is handled transparently. **Do NOT skip test generation or abstain from recording based on what you see in the Flutter source code** (e.g. \`SemanticsBinding.ensureSemantics()\` commented out, \`IS_TESTING\` flag absent, or similar) — Skyramp enables accessibility from the browser side regardless of the app's Dart code. Proceed with \`browser_navigate\` and test recording as normal.
370
+ **Flutter web apps:** Skyramp's Playwright tools automatically enable Flutter's accessibility semantics tree on every \`browser_navigate\` call — you do NOT need to manually click \`flt-semantics-placeholder\` or add any activation step to the trace. Do NOT log an \`issuesFound\` entry about Flutter canvas rendering or accessibility activation — this is handled transparently. **Do NOT skip test generation or abstain from recording based on what you see in the Flutter source code** (e.g. \`SemanticsBinding.ensureSemantics()\` commented out, \`IS_TESTING\` flag absent, or similar) — Skyramp enables accessibility from the browser side regardless of the app's Dart code. Proceed with \`browser_navigate\` and test recording as normal. **Start at the app's root URL** (e.g. \`{baseUrl}/\`) — do NOT \`browser_navigate\` straight to a deep sub-route (e.g. \`/authors\`, \`/orders/13\`). Flutter \`go_router\` SPAs route from the root: deep-linking on a cold page load often fails to render the expected screen (the route's widgets never mount, so the trace captures the wrong page). Load the root, let the app's own routing/auth-redirect render, then reach target screens by interaction. **After the initial login, navigate using in-app controls only** (tab buttons, links, back buttons) — do NOT call \`browser_navigate\` to a different URL after login. Flutter web apps are SPAs: a \`browser_navigate\` to a new URL after login triggers a full page reload which clears the auth session, causing redundant re-login cycles in the generated test. Use button clicks to reach target screens instead.
290
371
  **Skip only if one of these conditions is met:**
291
372
  - **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
292
373
  - **(b) Unintegrated non-route component** — the changed file is a leaf component (not a framework route/entrypoint) that has no integration point in the running app. To confirm:
@@ -466,14 +547,10 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
466
547
  4. **Wait**: Do NOT proceed to test execution until steps 1–3 are complete and the verification checklist in the \`skyramp_enhance_assertions\` tool result has been validated for EVERY generated test file.
467
548
  Do not make any changes other than the assertion enhancements described above. For example: do not modify auth headers, cookies, tokens, env vars, or imports that the generation tool already set correctly — those are correct by construction and changing them breaks auth or execution.
468
549
 
469
- **Execution timing:**
470
- - **beforeStatus** (maintained tests only): execute each maintained test file **once at the start** (before any edits) to capture \`beforeStatus\`. This is the only execution allowed before edits.
471
- - **Final execution**: Do NOT call \`skyramp_execute_test\` again until ALL maintenance edits AND ALL new test generation/enhancement are complete. Then execute every test file once — maintained files (for \`afterStatus\`) and new files together. Run independent files in parallel (same tool call batch).
550
+ **Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete.
472
551
  - Only report test results for files you actually ran.
473
552
  **Auth**: If \`skyramp_analyze_changes\` reports an auth token or \`SKYRAMP_TEST_TOKEN\` is set, pass it in **every** \`skyramp_execute_test\` call from the first attempt — do NOT wait for a 401/403 to discover auth is needed.
474
553
 
475
- ---
476
-
477
554
  ## Task 3: Submit Report
478
555
 
479
556
  **Before calling \`skyramp_submit_report\` — mandatory count check:**
@@ -494,16 +571,26 @@ In these cases:
494
571
 
495
572
  Otherwise: in \`newTestsCreated\`, you must have exactly ${maxGenerate} budget-counting new tests for the planned GENERATE items. Only new files (ADD) created for those planned GENERATE items count toward this ${maxGenerate} target — GENERATE items converted to UPDATE do not. You may also include at most one additional discovered-scenario file in \`newTestsCreated\` (the bug-catching test generated after all planned items); that extra test does **not** count against the ${maxGenerate} budget. If you have fewer than ${maxGenerate} budget-counting new tests, backfill from the remaining ADDITIONAL candidates before proceeding. Only proceed with fewer than ${maxGenerate} budget-counting new tests if all candidates failed after retry AND the fallback single-contract test also failed.
496
573
 
497
- Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}". Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
498
-
574
+ Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
575
+ ${hasRelatedRepos ? `
576
+ - **MULTI-REPO attribution**: Set the \`repository\` field (\`owner/repo\`) on EVERY \`newTestsCreated\`, \`testResults\`, \`issuesFound\`, and \`additionalRecommendations\` item — including items about the PRIMARY repo — so each finding is unambiguously attributed. The primary repo's \`repository\` is \`${primaryRepo || "<the primary repo's owner/repo>"}\`; items derived from a related repo's diff (from the \`<related_repositories>\` analysis) carry that repo's \`repository\` value. In \`businessCaseAnalysis\`, include a short per-repo subsection and call out any cross-repo correlations you found.
577
+ ` : ""}
499
578
  - **additionalRecommendations**: AT MOST ${maxRecommendations - maxGenerate} items.
500
579
  - For \`testType: "contract"\` entries: **\`primaryEndpoint\` is required** (e.g. \`"GET /api/v1/users/{user_id}"\`). The tool will reject the submission without it — do not omit it or you will be forced to resubmit.
501
580
  - For \`testType: "integration"\` or \`"e2e"\` entries: omit \`primaryEndpoint\` — use \`description\` to list the endpoints involved instead.
502
- - **testMaintenance**: Use \`[]\` **only** if no existing Skyramp tests were found in the repository. If existing tests were found (any score), include one entry per test. Set \`action\` to the exact drift action you chose from the Action Decision Matrix (\`UPDATE\`, \`REGENERATE\`, \`DELETE\`, \`VERIFY\`, or \`IGNORE\`). For UPDATE/REGENERATE/DELETE tests that were modified and executed, populate all fields from real before/after execution results. For VERIFY/IGNORE tests (not modified), derive \`beforeStatus\` from the \`skyramp_analyze_test_health\` health score (typically \`"Pass"\` if drift score is 0 and no health issues were flagged), set \`afterStatus\` to \`"Skipped"\`, and use \`afterDetails\` to explain why (e.g. "IGNORE: drift score 0 endpoint not modified in this PR"). Do **not** add entries for tests that were not returned by the health analysis.
581
+ - **testMaintenance**: Use \`[]\` **only** if no existing Skyramp tests were found in the repository. If existing tests were found (any score), include one entry per test. Set \`action\` to the exact drift action you chose from the Action Decision Matrix (\`UPDATE\`, \`REGENERATE\`, \`DELETE\`, \`VERIFY\`, or \`IGNORE\`). For UPDATE/REGENERATE/DELETE tests that were modified and executed, populate \`beforeDetails\` and \`afterDetails\` from what you observed in \`skyramp_execute_test\` output one line each, no newlines. For passing runs write the count and timing (e.g. \`"4 passed in 15.09s"\`). For failing runs write the test name and failure reason (e.g. \`"1 failed: test_foo - assert X == Y shared-state pollution"\`). For VERIFY/IGNORE tests (not modified), omit \`beforeDetails\` and \`afterDetails\`. Do **not** add entries for tests that were not returned by the health analysis.
582
+ - **testResults**: For UI/E2E test entries, include the \`videoPath\` field if \`skyramp_execute_test\` returned one.
503
583
 
504
584
  ---
505
585
 
506
586
  ${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modularize: false })}`;
587
+ // TODO: merge stateFile and summaryOutputFile into a single file (follow-up PR).
588
+ // Currently the stateFile path is derived server-side from RUNNER_TEMP (GitHub Actions only);
589
+ // other CI systems (Jenkins, GitLab, Buildkite) require the caller to set RUNNER_TEMP.
590
+ // Once merged, the path flows through summaryOutputFile — already testbot-controlled and
591
+ // CI-agnostic — eliminating the RUNNER_TEMP dependency and the LLM confusion that motivated
592
+ // removing stateOutputFile from the prompt schema. Remove the RUNNER_TEMP branch in
593
+ // AnalysisStateManager.ts when this is done.
507
594
  }
508
595
  function escapeXml(value) {
509
596
  return value
@@ -578,10 +665,6 @@ export function registerTestbotPrompt(server) {
578
665
  .string()
579
666
  .optional()
580
667
  .describe("Natural language prompt from the user (via @skyramp-testbot comment) to add or remove specific recommendations."),
581
- stateOutputFile: z
582
- .string()
583
- .optional()
584
- .describe("Absolute path where skyramp_analyze_changes should write its state file. When provided, the caller can locate the file without log parsing."),
585
668
  uiCredentials: z
586
669
  .string()
587
670
  .optional()
@@ -594,10 +677,18 @@ export function registerTestbotPrompt(server) {
594
677
  .string()
595
678
  .optional()
596
679
  .describe("Absolute path to a cloned test repository. When set, the agent writes generated test files there instead of the app repository (cross-repo test delivery)."),
680
+ relatedRepositories: z
681
+ .string()
682
+ .optional()
683
+ .describe("JSON-encoded array of {repo, repositoryPath, baseBranch?} for related repositories to analyze (multi-repo mode). `repo` is the owner/repo slug (resolved by the action; used verbatim, never inferred). Each is analyzed with its own skyramp_analyze_changes call; the agent correlates cross-repo changes and draws GENERATE candidates from all repos into one shared ranked budget. Generated test files are delivered into each service's declared testDirectory, relative to the single delivery root (the primary repo, or the configured test repo)."),
684
+ primaryRepo: z
685
+ .string()
686
+ .optional()
687
+ .describe("The primary repository's owner/repo slug (where the testbot workflow runs). Used verbatim as the `repository` attribution for the primary repo's analysis, tests, and report items in multi-repo runs."),
597
688
  },
598
689
  }, async (args) => {
599
690
  const services = await readWorkspaceServices(args.repositoryPath);
600
- let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.stateOutputFile, args.uiCredentials, args.testsRepoDir);
691
+ let prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.maxCritical, args.prNumber, args.userPrompt, services.length ? services : undefined, args.uiCredentials, args.testsRepoDir, parseRelatedRepositories(args.relatedRepositories), args.primaryRepo);
601
692
  if (args.workspaceValidationFailed) {
602
693
  prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
603
694
  }
@@ -8,7 +8,7 @@ jest.unstable_mockModule("../../services/AnalyticsService.js", () => ({
8
8
  jest.unstable_mockModule("../../playwright/traceRecordingPrompt.js", () => ({
9
9
  getTraceRecordingPromptText: () => "",
10
10
  }));
11
- const { getTestbotPrompt, buildWorkspaceRecoveryPrefix } = await import("./testbot-prompts.js");
11
+ const { getTestbotPrompt, buildWorkspaceRecoveryPrefix, parseRelatedRepositories } = await import("./testbot-prompts.js");
12
12
  // Minimal args to invoke getTestbotPrompt — only services matter for these tests
13
13
  const baseArgs = {
14
14
  prTitle: "Test PR",
@@ -25,26 +25,6 @@ function callWithServices(services) {
25
25
  undefined, // userPrompt
26
26
  services);
27
27
  }
28
- function callWithStateOutputFile(stateOutputFile) {
29
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
30
- undefined, // maxRecommendations
31
- undefined, // maxGenerate
32
- undefined, // maxCritical
33
- undefined, // prNumber
34
- undefined, // userPrompt
35
- undefined, // services
36
- stateOutputFile);
37
- }
38
- function callFollowUpWithStateOutputFile(stateOutputFile) {
39
- return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
40
- undefined, // maxRecommendations
41
- undefined, // maxGenerate
42
- undefined, // maxCritical
43
- undefined, // prNumber
44
- "add more tests", // userPrompt — triggers follow-up path
45
- undefined, // services
46
- stateOutputFile);
47
- }
48
28
  describe("buildServiceContext (via getTestbotPrompt)", () => {
49
29
  it("renders full service with all fields", () => {
50
30
  const prompt = callWithServices([
@@ -122,27 +102,6 @@ describe("buildServiceContext (via getTestbotPrompt)", () => {
122
102
  expect(prompt).toContain("</REPOSITORY PATH>\nUse the Skyramp MCP server tools");
123
103
  });
124
104
  });
125
- describe("stateOutputFile in getTestbotPrompt", () => {
126
- it("includes stateOutputFile in skyramp_analyze_changes call for first-run prompt", () => {
127
- const stateFile = "/tmp/skyramp/analyze-changes-state.json";
128
- const prompt = callWithStateOutputFile(stateFile);
129
- // The prompt must pass stateOutputFile to skyramp_analyze_changes
130
- expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
131
- });
132
- it("includes stateOutputFile in skyramp_analyze_changes call for follow-up prompt", () => {
133
- const stateFile = "/tmp/skyramp/analyze-changes-state.json";
134
- const prompt = callFollowUpWithStateOutputFile(stateFile);
135
- expect(prompt).toContain(`\`stateOutputFile\`: "${stateFile}"`);
136
- });
137
- it("omits stateOutputFile from skyramp_analyze_changes call when not provided", () => {
138
- const prompt = callWithStateOutputFile(undefined);
139
- expect(prompt).not.toContain("stateOutputFile");
140
- });
141
- it("omits stateOutputFile from follow-up prompt when not provided", () => {
142
- const prompt = callFollowUpWithStateOutputFile(undefined);
143
- expect(prompt).not.toContain("stateOutputFile");
144
- });
145
- });
146
105
  describe("uiCredentials in getTestbotPrompt", () => {
147
106
  function callWithUiCredentials(uiCredentials) {
148
107
  return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
@@ -152,7 +111,6 @@ describe("uiCredentials in getTestbotPrompt", () => {
152
111
  undefined, // prNumber
153
112
  undefined, // userPrompt
154
113
  undefined, // services
155
- undefined, // stateOutputFile
156
114
  uiCredentials);
157
115
  }
158
116
  // The instructions text references `<ui-credentials>` in prose (backticked);
@@ -261,7 +219,6 @@ describe("testsRepoDir in getTestbotPrompt", () => {
261
219
  undefined, // prNumber
262
220
  undefined, // userPrompt
263
221
  undefined, // services
264
- undefined, // stateOutputFile
265
222
  undefined, // uiCredentials
266
223
  testsRepoDir);
267
224
  }
@@ -273,7 +230,6 @@ describe("testsRepoDir in getTestbotPrompt", () => {
273
230
  undefined, // prNumber
274
231
  "add more tests", // userPrompt — triggers follow-up path
275
232
  undefined, // services
276
- undefined, // stateOutputFile
277
233
  undefined, // uiCredentials
278
234
  testsRepoDir);
279
235
  }
@@ -328,3 +284,97 @@ describe("testbot prompt blueprint-grounded recommendations (slice 4)", () => {
328
284
  expect(exceptionBlock).toContain(".vue");
329
285
  });
330
286
  });
287
+ describe("relatedRepositories in getTestbotPrompt (multi-repo mode)", () => {
288
+ const PRIMARY_REPO = "letsramp/primary-app";
289
+ const related = [
290
+ { repo: "letsramp/backend", repositoryPath: "/tmp/skyramp/related-repo-0-backend", baseBranch: "main" },
291
+ { repo: "letsramp/shared", repositoryPath: "/tmp/skyramp/related-repo-1-shared", baseBranch: "master" },
292
+ ];
293
+ function callWithRelated(repos, opts) {
294
+ return getTestbotPrompt(baseArgs.prTitle, baseArgs.prDescription, baseArgs.summaryOutputFile, baseArgs.repositoryPath, undefined, // baseBranch
295
+ undefined, // maxRecommendations
296
+ undefined, // maxGenerate
297
+ undefined, // maxCritical
298
+ undefined, // prNumber
299
+ undefined, // userPrompt
300
+ undefined, // services
301
+ undefined, // uiCredentials
302
+ opts?.testsRepoDir, // testsRepoDir
303
+ repos, opts?.primaryRepo ?? PRIMARY_REPO);
304
+ }
305
+ it("emits a <related_repositories> XML block with repository + path + base_branch per repo", () => {
306
+ const prompt = callWithRelated(related);
307
+ expect(prompt).toContain("<related_repositories>");
308
+ expect(prompt).toContain("</related_repositories>");
309
+ expect(prompt).toContain('<repository repository="letsramp/backend" path="/tmp/skyramp/related-repo-0-backend" base_branch="main" />');
310
+ expect(prompt).toContain('<repository repository="letsramp/shared" path="/tmp/skyramp/related-repo-1-shared" base_branch="master" />');
311
+ // Pure XML — no free-form "repositoryPath:" / "baseBranch:" colon syntax.
312
+ expect(prompt).not.toContain("repositoryPath:");
313
+ expect(prompt).not.toMatch(/baseBranch:/);
314
+ });
315
+ it("emits the primary repo's owner/repo in a <REPOSITORY> block", () => {
316
+ const prompt = callWithRelated(related);
317
+ expect(prompt).toContain(`<REPOSITORY>${PRIMARY_REPO}</REPOSITORY>`);
318
+ });
319
+ it("always emits a base_branch attribute, even when no explicit baseBranch is provided", () => {
320
+ const prompt = callWithRelated([
321
+ { repo: "letsramp/backend", repositoryPath: "/tmp/skyramp/related-repo-0-backend" },
322
+ ]);
323
+ expect(prompt).toContain("<related_repositories>");
324
+ // Absent value → explicit auto-detect marker in the attribute.
325
+ expect(prompt).toContain('<repository repository="letsramp/backend" path="/tmp/skyramp/related-repo-0-backend" base_branch="auto-detect" />');
326
+ });
327
+ it("keeps the unified workspace in the PRIMARY repo even when testsRepoDir is set", () => {
328
+ const prompt = callWithRelated(related, { testsRepoDir: "/tmp/skyramp/test-repo" });
329
+ // workspace.yml is registered in the primary repo regardless of the test repo.
330
+ expect(prompt).toContain(`\`workspacePath\`: "${baseArgs.repositoryPath}"`);
331
+ expect(prompt).not.toContain('`workspacePath`: "/tmp/skyramp/test-repo"');
332
+ // And it makes the primary-vs-test-repo distinction explicit to the agent.
333
+ expect(prompt).toMatch(/NOT in the configured test repo/);
334
+ });
335
+ it("leaves the single-repo prompt unchanged when no related repos are provided", () => {
336
+ const prompt = callWithRelated(undefined);
337
+ expect(prompt).not.toContain("<related_repositories>");
338
+ expect(prompt).not.toContain("MULTI-REPO CONTEXT");
339
+ expect(prompt).not.toContain("MULTI-REPO attribution");
340
+ // The cross-repo round-robin / candidate-pooling language must not leak into
341
+ // single-repo runs (the as-is GENERATE mandate stays authoritative there).
342
+ expect(prompt).not.toMatch(/round-robin/i);
343
+ expect(prompt).not.toContain("Multi-repo exception");
344
+ expect(prompt).not.toMatch(/candidates, not a fixed selection/i);
345
+ });
346
+ it("treats an empty array as single-repo", () => {
347
+ const prompt = callWithRelated([]);
348
+ expect(prompt).not.toContain("<related_repositories>");
349
+ });
350
+ });
351
+ describe("parseRelatedRepositories", () => {
352
+ it("parses a valid JSON array", () => {
353
+ const parsed = parseRelatedRepositories(JSON.stringify([
354
+ { repo: "o/a", repositoryPath: "/a", baseBranch: "main" },
355
+ { repo: "o/b", repositoryPath: "/b" },
356
+ ]));
357
+ expect(parsed).toEqual([
358
+ { repo: "o/a", repositoryPath: "/a", baseBranch: "main" },
359
+ { repo: "o/b", repositoryPath: "/b", baseBranch: undefined },
360
+ ]);
361
+ });
362
+ it("returns undefined for missing input", () => {
363
+ expect(parseRelatedRepositories(undefined)).toBeUndefined();
364
+ expect(parseRelatedRepositories("")).toBeUndefined();
365
+ });
366
+ it("returns undefined for malformed JSON", () => {
367
+ expect(parseRelatedRepositories("{not json")).toBeUndefined();
368
+ });
369
+ it("returns undefined for an empty array", () => {
370
+ expect(parseRelatedRepositories("[]")).toBeUndefined();
371
+ });
372
+ it("drops entries missing a repositoryPath or repo", () => {
373
+ const parsed = parseRelatedRepositories(JSON.stringify([
374
+ { repo: "o/x", baseBranch: "main" }, // no repositoryPath
375
+ { repositoryPath: "/no-repo" }, // no repo
376
+ { repo: "o/ok", repositoryPath: "/ok" },
377
+ ]));
378
+ expect(parsed).toEqual([{ repo: "o/ok", repositoryPath: "/ok", baseBranch: undefined }]);
379
+ });
380
+ });
@@ -0,0 +1,2 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function registerSutSetupResource(server: McpServer): void;
@@ -0,0 +1,45 @@
1
+ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { logger } from "../utils/logger.js";
3
+ import { resourceText } from "../utils/utils.js";
4
+ import { AnalyticsService } from "../services/AnalyticsService.js";
5
+ import { getAdaptWorkflowPrompt } from "../prompts/sut-setup/modes/adaptWorkflowPrompt.js";
6
+ import { getDockerComposePrompt } from "../prompts/sut-setup/modes/dockerComposePrompt.js";
7
+ import { SutSetupMode } from "../prompts/sut-setup/shared.js";
8
+ export function registerSutSetupResource(server) {
9
+ logger.info("Registering SUT setup resource");
10
+ // RFC 6570 {+rest} (reserved expansion) captures the entire query string
11
+ // including the leading "?". This avoids the SDK's per-param regex which
12
+ // fails on empty query-param values.
13
+ const template = new ResourceTemplate("skyramp://prompts/skyramp_testbot_sut{+rest}", { list: undefined });
14
+ server.registerResource("skyramp_testbot_sut", template, {
15
+ title: "Skyramp Testbot SUT Setup",
16
+ description: "Returns mode-specific instructions for SUT (System Under Test) setup. " +
17
+ "Modes: none, adapt_workflow, docker_compose.",
18
+ mimeType: "text/plain",
19
+ }, async (uri) => {
20
+ const param = (name, fallback) => uri.searchParams.get(name) ?? fallback;
21
+ const mode = param("sutSetupMode", SutSetupMode.None);
22
+ const args = {
23
+ repositoryPath: param("repositoryPath", "."),
24
+ sutSetupMode: mode,
25
+ sutSourceWorkflowFile: param("sutSourceWorkflowFile", ""),
26
+ sutSourceDockerComposeFile: param("sutSourceDockerComposeFile", ""),
27
+ };
28
+ let prompt;
29
+ switch (mode) {
30
+ case SutSetupMode.AdaptWorkflow:
31
+ prompt = getAdaptWorkflowPrompt(args);
32
+ break;
33
+ case SutSetupMode.DockerCompose:
34
+ prompt = getDockerComposePrompt(args);
35
+ break;
36
+ case SutSetupMode.None:
37
+ prompt = "sutSetupMode is 'none'. No SUT setup required.";
38
+ break;
39
+ default:
40
+ prompt = `Unknown SUT setup mode: '${mode}'. Valid modes: none, adapt_workflow, docker_compose.`;
41
+ }
42
+ AnalyticsService.pushMCPToolEvent("skyramp_testbot_sut_resource", undefined, { mode }).catch(() => { });
43
+ return resourceText(uri.toString(), prompt);
44
+ });
45
+ }
@@ -2,7 +2,7 @@ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { AnalyticsService } from "../services/AnalyticsService.js";
4
4
  import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, } from "../prompts/test-recommendation/recommendationSections.js";
5
- import { getTestbotPrompt, readWorkspaceServices } from "../prompts/testbot/testbot-prompts.js";
5
+ import { getTestbotPrompt, readWorkspaceServices, parseRelatedRepositories, } from "../prompts/testbot/testbot-prompts.js";
6
6
  export function registerTestbotResource(server) {
7
7
  logger.info("Registering testbot resource");
8
8
  // RFC 6570 {+rest} (reserved expansion) captures the entire query string
@@ -25,7 +25,7 @@ export function registerTestbotResource(server) {
25
25
  const maxCrit = parseInt(uri.searchParams.get("maxCritical") || "", 10);
26
26
  const repositoryPath = param("repositoryPath", ".");
27
27
  const services = await readWorkspaceServices(repositoryPath);
28
- const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("stateOutputFile") || undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined);
28
+ const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("summaryOutputFile", ""), repositoryPath, uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(maxCrit) ? MAX_CRITICAL_TESTS : maxCrit, isNaN(prNum) ? undefined : prNum, uri.searchParams.get("userPrompt") || undefined, services.length ? services : undefined, uri.searchParams.get("uiCredentials") || undefined, uri.searchParams.get("testsRepoDir") || undefined, parseRelatedRepositories(uri.searchParams.get("relatedRepositories") || undefined), uri.searchParams.get("primaryRepo") || undefined);
29
29
  AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
30
30
  // Return the original URI — clients may use it to re-fetch the resource,
31
31
  // and the caller already has these params. Credentials never appear in
@@ -46,8 +46,8 @@ export declare class TestDiscoveryService {
46
46
  */
47
47
  private scoreRelevance;
48
48
  /**
49
- * Partition external test files into relevant (score > 0) and low-relevance (score = 0)
50
- * based on file path/name overlap with the changed resource names from the PR diff.
49
+ * Partition external test files into relevant (score > 0) and low-relevance (score = 0).
50
+ * Signal: path/name token overlap with changedResources.
51
51
  */
52
52
  private partitionByRelevance;
53
53
  /**
@@ -102,9 +102,7 @@ export class TestDiscoveryService {
102
102
  this.partitionByRelevance(classified.external, changedResources));
103
103
  }
104
104
  else if (changedResources !== undefined) {
105
- // PR mode with an explicit empty endpoint list from diff parsing — don't flood
106
- // context with irrelevant external tests. The LLM will work from Skyramp tests
107
- // and scanned endpoints only.
105
+ // PR mode with no path-resolvable endpoints no path-based scoring available.
108
106
  relevantExternal = [];
109
107
  otherExternal = [];
110
108
  }
@@ -151,27 +149,39 @@ export class TestDiscoveryService {
151
149
  // Use the last two path segments to avoid false matches from deeply-nested dirs
152
150
  const segments = normalized.split("/").slice(-2).join("/");
153
151
  const tokens = new Set(segments.split(/[/\-_.]+/).filter(Boolean));
152
+ // Expand tokens with plural/singular variants so "deployment" matches
153
+ // "deployments" and vice versa. Only strip trailing "s" when the result
154
+ // is ≥4 chars (avoids "pas"→"pa") and doesn't end in "ss" (avoids "pass"→"pas").
155
+ const expanded = new Set(tokens);
156
+ for (const t of tokens) {
157
+ if (t.endsWith("s") && !t.endsWith("ss") && t.length > 4)
158
+ expanded.add(t.slice(0, -1));
159
+ else if (!t.endsWith("s"))
160
+ expanded.add(t + "s");
161
+ }
154
162
  return changedResources.filter(r => {
155
163
  const rLower = r.toLowerCase();
156
- // Direct single-token match (e.g. "orders" in file path tokens)
157
- if (tokens.has(rLower))
164
+ if (expanded.has(rLower))
158
165
  return true;
159
- // Compound resource names (e.g. "order-items" from /api/order-items) — check
160
- // that ALL hyphen/underscore-separated parts appear as individual tokens.
161
- // This handles test_order_items.py correctly matching resource "order-items".
166
+ // Compound resource names (e.g. "order-items") — all parts must appear as tokens.
162
167
  const parts = rLower.split(/[-_]/);
163
- return parts.length > 1 && parts.every(p => p.length >= 3 && tokens.has(p));
168
+ return parts.length > 1 && parts.every(p => p.length >= 3 && expanded.has(p));
164
169
  }).length;
165
170
  }
166
171
  /**
167
- * Partition external test files into relevant (score > 0) and low-relevance (score = 0)
168
- * based on file path/name overlap with the changed resource names from the PR diff.
172
+ * Partition external test files into relevant (score > 0) and low-relevance (score = 0).
173
+ * Signal: path/name token overlap with changedResources.
169
174
  */
170
175
  partitionByRelevance(files, changedResources) {
171
176
  const relevant = [];
172
177
  const other = [];
173
178
  for (const f of files) {
174
- (this.scoreRelevance(f, changedResources) > 0 ? relevant : other).push(f);
179
+ if (this.scoreRelevance(f, changedResources) > 0) {
180
+ relevant.push(f);
181
+ }
182
+ else {
183
+ other.push(f);
184
+ }
175
185
  }
176
186
  return { relevant, other };
177
187
  }
@@ -1,8 +1,17 @@
1
1
  import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, ProgressCallback } from "../types/TestExecution.js";
2
- export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.28";
2
+ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.29";
3
3
  export declare const PLAYWRIGHT_CONFIG_FILES: string[];
4
4
  export declare const EXCLUDED_MOUNT_ITEMS: string[];
5
5
  export declare const MOUNT_NULL_ITEMS: string[];
6
+ /**
7
+ * Check if a test type requires browser execution (UI/E2E)
8
+ */
9
+ export declare function isBrowserTest(testType: string): boolean;
10
+ /**
11
+ * Generate a deterministic unique subdirectory name for video output
12
+ * based on the test file path
13
+ */
14
+ export declare function getVideoSubdir(testFile: string): string;
6
15
  /**
7
16
  * Detect session file paths referenced in test files
8
17
  * Looks for storageState patterns in TypeScript/JavaScript/Python/Java/C# test files