@skyramp/mcp 0.2.7 → 0.2.150-rc.mntnc

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 (67) 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/testbot/testbot-prompts.d.ts +1 -1
  12. package/build/prompts/testbot/testbot-prompts.js +23 -16
  13. package/build/prompts/testbot/testbot-prompts.test.js +0 -44
  14. package/build/resources/sutSetupResource.d.ts +2 -0
  15. package/build/resources/sutSetupResource.js +45 -0
  16. package/build/resources/testbotResource.js +1 -1
  17. package/build/services/TestDiscoveryService.d.ts +2 -2
  18. package/build/services/TestDiscoveryService.js +22 -12
  19. package/build/services/TestExecutionService.d.ts +9 -0
  20. package/build/services/TestExecutionService.js +158 -26
  21. package/build/services/TestExecutionService.test.js +306 -0
  22. package/build/tools/executeSkyrampTestTool.js +17 -4
  23. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +1 -1
  24. package/build/tools/submitReportTool.js +81 -35
  25. package/build/tools/submitReportTool.test.js +264 -50
  26. package/build/tools/test-management/actionsTool.js +10 -0
  27. package/build/tools/test-management/analyzeChangesTool.d.ts +0 -1
  28. package/build/tools/test-management/analyzeChangesTool.js +84 -41
  29. package/build/tools/test-management/analyzeChangesTool.test.js +4 -34
  30. package/build/tools/test-management/analyzeTestHealthTool.js +6 -3
  31. package/build/types/TestAnalysis.d.ts +2 -1
  32. package/build/types/TestExecution.d.ts +12 -2
  33. package/build/types/TestExecution.js +11 -1
  34. package/build/types/TestbotReport.d.ts +68 -0
  35. package/build/types/TestbotReport.js +1 -0
  36. package/build/types/index.d.ts +4 -0
  37. package/build/types/index.js +3 -0
  38. package/build/utils/AnalysisStateManager.js +10 -1
  39. package/build/utils/routeParsers.d.ts +0 -10
  40. package/build/utils/routeParsers.js +31 -38
  41. package/build/utils/scenarioDrafting.d.ts +1 -1
  42. package/build/utils/scenarioDrafting.js +23 -1
  43. package/build/utils/utils.d.ts +7 -1
  44. package/build/utils/utils.js +10 -0
  45. package/node_modules/playwright/LICENSE +202 -0
  46. package/node_modules/playwright/NOTICE +5 -0
  47. package/node_modules/playwright/README.md +168 -0
  48. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +13 -9
  49. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +4 -3
  50. package/node_modules/playwright/node_modules/playwright-core/LICENSE +202 -0
  51. package/node_modules/playwright/node_modules/playwright-core/NOTICE +5 -0
  52. package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +23 -126
  53. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-B0JOjboO.js → codeMirrorModule-B5kqh2EV.js} +1 -1
  54. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{defaultSettingsView-1anWeyDf.js → defaultSettingsView-CZ9npQ3N.js} +79 -79
  55. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.Cc5029a3.js → index.BCnxj-_b.js} +1 -1
  56. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
  57. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{uiMode.Wo5yvvVh.js → uiMode.1Ym0Ivn8.js} +1 -1
  58. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
  59. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  60. package/node_modules/playwright/package.json +1 -1
  61. package/package.json +9 -2
  62. package/node_modules/playwright/node_modules/playwright-core/.DS_Store +0 -0
  63. package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.bin/node-which +0 -52
  64. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/is-docker +0 -5
  65. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/mime +0 -46
  66. package/node_modules/playwright/node_modules/playwright-core/bundles/utils/node_modules/.bin/yaml +0 -11
  67. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.CP3Y8Zsb.js +0 -2
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Path of the Testbot workflow file relative to the repo root. Created by the
3
+ * Testbot installer and edited in-place by every SUT setup mode.
4
+ */
5
+ export const TESTBOT_WORKFLOW_PATH = ".github/workflows/skyramp-testbot.yml";
6
+ export var SutSetupMode;
7
+ (function (SutSetupMode) {
8
+ SutSetupMode["None"] = "none";
9
+ SutSetupMode["AdaptWorkflow"] = "adapt_workflow";
10
+ SutSetupMode["DockerCompose"] = "docker_compose";
11
+ })(SutSetupMode || (SutSetupMode = {}));
12
+ /**
13
+ * Glossary block rendered right after the first mention of "SUT" in each
14
+ * setup prompt. Shared across modes so the LLM gets a single explicit
15
+ * definition rather than inferring it from context.
16
+ */
17
+ export function buildSutDefinitionBlock() {
18
+ return `**System Under Test (SUT)** — the application services this workflow brings up so Testbot can exercise them end-to-end. The SUT is what runs the PR's source code: backend services (REST/gRPC APIs), their supporting infrastructure (databases, queues, caches), and any frontend UI under test. The SUT does NOT include the test runner itself or Testbot. Bringing up the SUT means building from PR source, starting all required services, seeding any data tests rely on, and exposing a health check that proves the system is ready for traffic.
19
+
20
+ `;
21
+ }
22
+ export function buildContextBlock(args) {
23
+ const lines = [];
24
+ lines.push(`<context>`);
25
+ lines.push(`Repository path: ${args.repositoryPath}`);
26
+ lines.push(`Setup mode: ${args.sutSetupMode}`);
27
+ if (args.sutSourceWorkflowFile)
28
+ lines.push(`Source workflow file: ${args.sutSourceWorkflowFile}`);
29
+ if (args.sutSourceDockerComposeFile)
30
+ lines.push(`Source docker-compose file: ${args.sutSourceDockerComposeFile}`);
31
+ lines.push(`</context>`);
32
+ return lines.join("\n");
33
+ }
34
+ export function buildLocalValidationSection() {
35
+ return `Before reporting success, exercise the adapted workflow locally. Testbot's external fix loop only retries when SUT lifecycle commands are set on the Testbot action. When the SUT is brought up by surrounding GHA steps (skipTargetSetup: 'true'), the fix loop is skipped — so the local check below is the only safety net before the workflow is committed.
36
+ Run commands one at a time in your shell — individual execution pinpoints failures far faster than running the whole workflow at once. Any non-zero exit is a fix-needed signal: adjust the workflow, re-run the failing command, and do not proceed until it passes.
37
+ 1. Validate build scripts and helper programs first — run these before anything else so build and auth failures surface cheaply:
38
+ a. Every helper script the workflow calls (e.g., \`./scripts/*.sh\`, \`make <target>\`, \`./gradlew <task>\`) — run each and confirm exit 0.
39
+ b. \`.skyramp/sut/get-auth-token.sh\` if present — run it and confirm it prints a non-empty token to stdout.
40
+ c. Validate any standalone Dockerfile referenced by GHA \`steps:\` (not by a compose service) with \`docker build\`. Do NOT also run \`docker build\` on Dockerfiles that belong to a compose service — \`docker compose build\` (next step) already builds them in one BuildKit graph; doubling up wastes ~5-10 GB of cache per service.
41
+ d. Every docker-compose file the workflow references — run \`docker compose -f <path> config\` to validate the YAML, then \`docker compose -f <path> build\`.
42
+ e. **Disk hygiene between heavy builds.** Between any two build commands (Dockerfile, compose, helper scripts that build), if the runner is under ~5 GB free or you see \`no space left on device\`, run \`docker builder prune -af -f && docker image prune -af\` before the next build. This frees BuildKit cache + dangling images without losing the running stack. Skipping this on heavy SUTs (medusa, calcom, mattermost, supabase) exhausts the ~14 GB \`ubuntu-latest\` disk mid-validation.
43
+ 2. Validate the SUT lifecycle — choose the branch that matches the chosen pattern:
44
+ a. If lifecycle commands are set on the Testbot action (skipTargetSetup is unset):
45
+ i. Run \`targetSetupCommand\` — confirm exit 0.
46
+ ii. Poll \`targetReadyCheckCommand\` until it exits 0 within \`targetReadyCheckTimeout\` seconds.
47
+ iii. Run \`targetTeardownCommand\` — confirm cleanup and exit 0.
48
+ b. If GHA steps wrap the Testbot action (skipTargetSetup: 'true'):
49
+ i. Extract the shell body of each SETUP step and run them in order. If a step depends entirely on a \${{ secrets.* }} value your shell cannot resolve, note it in <thinking> and skip — do not fabricate a value.
50
+ ii. Poll \`targetReadyCheckCommand\` until it exits 0.
51
+ iii. Extract and run each TEARDOWN step's body in order so the next iteration starts clean.
52
+ Only proceed to the success report once setup → health check → teardown all pass.`;
53
+ }
54
+ /**
55
+ * Returns the canonical "add to the Testbot workflow" block: every input that
56
+ * controls the SUT lifecycle, auth, and workflow-level setup, followed by the
57
+ * shared What to do / What not to do guidance (including the sutSetupMode
58
+ * bootstrap guard). Shared by every SUT setup mode (adapt_workflow and
59
+ * docker_compose) so the input semantics and rules never drift between prompts.
60
+ */
61
+ export function buildAddToTestbotWorkflowSection() {
62
+ return `#### Testbot action inputs - Add target lifecycle commands and auth to Testbot Workflow
63
+ For the full Testbot action input reference (defaults, edge cases, advanced options beyond the ones called out below), see the Testbot README: https://github.com/letsramp/testbot#inputs
64
+
65
+ Target lifecycle inputs (control how Testbot starts, validates, and stops the SUT):
66
+ 1. \`targetSetupCommand\` — shell command that builds and starts all SUT services. Testbot runs this before tests.
67
+ a. Default when unset: \`docker compose up -d\` (starts existing images, does NOT build).
68
+ b. For PR-source builds use \`docker compose up -d --build\` or the equivalent for helm/script-based SUTs.
69
+ c. With a dedicated compose file (reused or generated), point at it explicitly: \`docker compose -f .skyramp/sut/docker-compose.testbot.yml --project-directory . up -d --build\`.
70
+ d. Override whenever the source workflow's start sequence differs from the default.
71
+ 2. \`targetReadyCheckCommand\` — polling command (e.g., \`curl -sf http://localhost:8080/health\`) that Testbot runs repeatedly after setup until it exits 0 (service is ready) or the timeout is reached.
72
+ a. **CRITICAL: Always set this to a valid target readiness check command.** Required in every pattern: when \`targetSetupCommand\` is explicit, when relying on the \`docker compose up -d\` default, AND when surrounding GHA steps bring the SUT up before the Testbot action (\`skipTargetSetup: 'true'\`). Testbot's fail-early gate throws \`Unable to verify service readiness\` when no readiness signal is available (neither this nor a workspace service \`baseUrl\`) and the action exits non-zero. It is also what gates a backgrounded \`targetSetupCommand\` — without it Testbot can't tell when the service is actually listening.
73
+ 3. \`targetReadyCheckTimeout\` — seconds to wait for \`targetReadyCheckCommand\` to succeed.
74
+ a. Default: \`1800\` (30 min) — sized for cold \`docker compose --build\` runs on multi-service stacks. The loop exits on the first success, so fast services pay no extra cost. Override only if you want to fail faster on stuck setups (e.g. \`'300'\` for a small single-service SUT).
75
+ 4. \`targetTeardownCommand\` — shell command that stops and cleans up the SUT after tests (e.g., \`docker compose down -v\`, or \`docker compose -f .skyramp/sut/docker-compose.testbot.yml --project-directory . down -v\` for a dedicated compose file).
76
+ a. Always set this so each run starts from a clean state.
77
+ b. Omit only when teardown is handled by surrounding GHA steps with \`if: always()\`.
78
+ 5. \`skipTargetSetup\` — when \`'true'\`, Testbot skips running \`targetSetupCommand\` entirely.
79
+ a. Set this only when GHA steps surrounding the Testbot action already bring the SUT up.
80
+ b. Leave it unset (or \`'false'\`) when Testbot should run the full lifecycle via the inputs above.
81
+ Auth inputs (used by Testbot to authenticate against the running SUT during test recording and execution):
82
+ 6. \`authTokenCommand\` — shell command whose stdout is the auth credential for REST/gRPC API testing (e.g., a Bearer token, API key, or session cookie). Testbot captures the output and injects it as the \`Authorization\` header.
83
+ a. Set this when the SUT requires authentication for API calls.
84
+ b. If the source workflow exports a token via a step output or script, wire that same script here (e.g., \`bash .skyramp/sut/get-auth-token.sh\`).
85
+ c. If the workflow seeds a test user during setup, create \`.skyramp/sut/get-auth-token.sh\` that logs in with those credentials and prints the token.
86
+ d. Omit when the SUT APIs are unauthenticated.
87
+ 7. \`uiCredentials\` — \`username:password\` pair typed into the browser login form during UI test recording (format: \`myuser:mypassword\`).
88
+ a. Set for any frontend service that requires browser-based login.
89
+ b. Use \`\${{ secrets.SKYRAMP_UI_CREDENTIALS }}\` if the secret exists, otherwise use credentials seeded during SUT setup.
90
+ Workflow / job level settings (set on the workflow or job, not as \`skyramp/testbot\` action inputs):
91
+ 8. Secrets / environment variables — pass every secret/env var the setup needs into the workflow via an \`env:\` block or \`secrets: inherit\`, and add any dependency-resolution steps/jobs the SUT requires.
92
+ 9. \`runs-on\` runner label — switch to a larger runner when the SUT needs more disk or memory: heavy Docker builds, large dependency installs, or a multi-service (3+) compose stack. Standard runners run out of disk and crash with \`No space left on device\`. Runner names vary by org — do not hardcode \`large-ubuntu\`. Pick the label by (a) preserving whatever larger label the source workflow already uses (never downgrade to \`ubuntu-latest\`), (b) reusing the label adjacent heavy or integration jobs in \`.github/workflows/\` already use, or (c) falling back to a known GitHub-hosted large label such as \`ubuntu-latest-large\`.
93
+
94
+ #### What to do
95
+ Edit \`${TESTBOT_WORKFLOW_PATH}\` in place (already created by the Testbot installer) — do NOT recreate it. Add only what the SUT needs onto the existing \`skyramp/testbot\` action step, alongside the inputs the installer already added:
96
+ 1. The 4 target lifecycle inputs — \`targetSetupCommand\`, \`targetReadyCheckCommand\`, \`targetReadyCheckTimeout\`, \`targetTeardownCommand\`.
97
+ 2. The 2 auth inputs when the SUT needs them — \`authTokenCommand\` and/or \`uiCredentials\`.
98
+ 3. Any secrets / environment variables and dependency-resolution steps/jobs the setup needs (input 8).
99
+ 4. The \`runs-on\` runner label, if a larger runner is needed (input 9).
100
+ 5. Use a daemon/detached form for \`targetSetupCommand\` — \`docker compose up -d --build\`, \`nohup npm start &\`, \`./server &\`, \`pnpm --prefix ./apps/studio start &\`. The command should launch the service and exit; \`targetReadyCheckCommand\` is what gates readiness. Testbot backgrounds foreground-blocking commands (\`npm start\`, \`python app.py\`, \`./server\`) automatically after a 5-minute timeout as a safety net, so they will eventually work — but daemon mode is faster (no 5-minute wait) and surfaces startup crashes clearly via the command's exit code. If no daemon form is available and the repo has no \`docker-compose.yml\`, wrap the foreground command in compose (a \`Dockerfile\` + \`docker-compose.yml\` running \`npm start\`) so it can be launched with \`docker compose up -d --build\`. Only fall back to a GHA pre-step with \`&\` (e.g. \`run: npm start &\`) if Docker is not feasible.
101
+
102
+ #### What not to do
103
+ 1. IMPORTANT — do NOT remove or change the existing \`sutSetupMode\` input (nor \`sutSourceWorkflowFile\` / \`sutSourceDockerComposeFile\`). Leave \`sutSetupMode\` exactly as provided: Testbot flips it to \`none\` automatically after validation succeeds, and the committed \`sutSetupMode\` value is the signal that tells the next CI run to validate this adapted workflow rather than run tests. Deleting it or setting it to \`none\` breaks the bootstrap/validation cycle.
104
+ 2. Do not point the SUT at a prebuilt upstream image (e.g. \`image: org/app:latest\` or a fixed commit SHA from \`main\`) for any application service under test. The image will lag the PR and Testbot will validate stale code.
105
+ 3. Do not point the SUT at a remote staging or production environment for application services under test. Staging code drifts from PR source and turns Testbot's results into noise. External infra is acceptable only for sidecar dependencies the PR does not change (e.g. a managed test database).
106
+ 4. Do not use a foreground-blocking command as \`targetSetupCommand\` (e.g. \`npm start\`, \`pnpm --prefix ./apps/studio start\`, \`python app.py\`, \`prefect server start\`, \`./server\`) — see input 5 in "What to do" above for daemon forms and the compose-wrap fallback.
107
+ 5. Do not leave server processes running between fix-loop retries. If \`targetSetupCommand\` starts a direct process (not \`docker compose\`), always set \`targetTeardownCommand\` to kill it (e.g. \`pkill -f "node dist/index.js"\` or \`docker compose down\`). Without teardown, retried setup attempts will fail with \`EADDRINUSE\` because the previous process still holds the port.
108
+ `;
109
+ }
110
+ /**
111
+ * `buildAddToTestbotWorkflowSection()` output with its leading
112
+ * `#### Testbot action inputs ...` header stripped — for use as a PromptPlan
113
+ * step body where the PromptPlan step header replaces the inline `####`.
114
+ */
115
+ export function buildInputsBody() {
116
+ const text = buildAddToTestbotWorkflowSection();
117
+ const firstNewline = text.indexOf("\n");
118
+ return text.slice(firstNewline + 1);
119
+ }
120
+ export function buildCommonSutErrorsSection() {
121
+ return `Before finishing, verify the adapted Testbot workflow setup against common errors seen during SUT setup:
122
+ 1. port_conflict — another container is already using the port. Pick a free host port or stop the conflicting container.
123
+ 2. image_not_found / build_failed — the referenced image does not exist for this SHA, or the build step has the wrong context/Dockerfile path. Build from PR source, do not rely on upstream commit-SHA tags.
124
+ 3. healthcheck_timeout — the service is slow to come up. Use \`targetReadyCheckTimeout >= 120\` and ensure the health endpoint actually exists.
125
+ 4. connection_refused — the service is not listening on the expected host/port (often binding to 127.0.0.1 inside the container; bind to 0.0.0.0).
126
+ 5. dependency_error — required service (DB, cache, queue) is missing from compose or not healthy before the app starts. Add \`depends_on\` with \`condition: service_healthy\`.
127
+ 6. permission_denied — script is not executable (\`chmod +x\`) or volume mount has wrong ownership.
128
+ 7. auth_endpoint_404 / auth_credentials_invalid — the auth script hits a wrong path, or the database has no seeded user. The auth script must create the user before login when starting from a fresh DB.
129
+ 8. missing env vars / secrets — required env vars are unset on the runner. Either default them in the script or pass them through workflow \`env:\`.
130
+ 9. no_space_left — the runner ran out of disk space (\`No space left on device\`). This happens on standard runners when the SUT involves large Docker builds, many npm packages, or multiple compose services that pull large images. Switch \`runs-on\` to a larger runner — runner names vary by org, do not hardcode \`large-ubuntu\`. Pick the label by (a) preserving whatever larger label the source workflow already uses, (b) reusing the label adjacent heavy or integration jobs in \`.github/workflows/\` already use, or (c) falling back to a known GitHub-hosted large label such as \`ubuntu-latest-large\`. Indicator: source workflow already specifies a large runner, or the SUT has multi-stage Docker builds / a compose file with 3+ services.
131
+ Do not add an in-prompt fix loop — Testbot's external validator will retry with concrete error context if any of these occur.`;
132
+ }
@@ -136,7 +136,9 @@ Actions:
136
136
  **UPDATE vs REGENERATE — the deciding question is whether the root response wrapper changed:**
137
137
  - **REGENERATE** only when a new top-level envelope object wraps the entire payload or the root key is renamed so that essentially every existing assertion must change.
138
138
  - **UPDATE** for everything else. If you can describe the fix as "patch these N assertion paths", it is UPDATE regardless of how many paths there are.
139
- - When every assertion in the file is invalid, it is REGENERATE. When you can still patch individual paths, it is UPDATE.`;
139
+ - When every assertion in the file is invalid, it is REGENERATE. When you can still patch individual paths, it is UPDATE.
140
+
141
+ **Non-test consumer check:** When the diff removes or renames response fields, also grep the repository for non-test source files that import or reference the changed schema type (e.g. \`grep -r "WorkQueueStatusDetail" src/\`). If any client model, SDK class, or serializer references the old field names, add a VERIFY entry for each such file in your recommendations so the developer is alerted — these are not test files but they will break at runtime.`;
140
142
  }
141
143
  export function buildCheckAuthAndAuthorization() {
142
144
  return `Has the authentication or authorization for this endpoint changed?
@@ -180,7 +182,7 @@ export function buildCheckAssignAction() {
180
182
  **Every action requires a specific rationale — including IGNORE:**
181
183
  - UPDATE / REGENERATE / DELETE: quote the specific diff line that triggered it.
182
184
  - VERIFY: name the uncertain element (e.g. "model-only change, cannot confirm field is exposed without checking the output layer").
183
- - IGNORE: name the specific reason the changed code cannot reach this test's endpoint (e.g. "diff only touches \`auth/session-service.js\` — this test targets \`/api/v1/orders\` which has no session dependency"). Generic "unrelated endpoint" or "service boundary" without a diff reference is not sufficient.
185
+ - IGNORE: name the changed file(s) from the diff AND explain why they cannot reach this test's endpoint (e.g. "diff only touches \`auth/session-service.js\` — this test targets \`/api/v1/orders\` which has no session dependency"). You must reference at least one specific changed file from the diff. Generic "unrelated endpoint" or "service boundary" without naming the changed file is not sufficient.
184
186
 
185
187
  - If the Additive Fields check flagged a new field with output layer signal confirmed in the diff → action is UPDATE. If the Additive Fields check returned VERIFY (model-only signal, no output layer change) → action remains VERIFY.
186
188
  - **Scope gate:** If the changed code is clearly not reachable through the service or base URL this test targets, assign IGNORE.
@@ -222,12 +224,16 @@ export function buildDriftOutputChecklist(stateFile, existingTests) {
222
224
  const noTestsNote = !hasTests
223
225
  ? `\nNo existing tests were found. Call \`skyramp_actions\` with \`recommendations: []\` and explain in your report why no maintenance was needed (e.g. "no existing tests cover the changed endpoints" or "PR adds a new endpoint with no prior coverage").\n`
224
226
  : "";
227
+ const symbolDiscoveryStep = hasTests
228
+ ? `**Symbol import discovery (before assessing tests):** If the diff adds or modifies class, function, or type definitions, grep the repository for test files that import those symbol names. Add any matched test files not already in the list below to your assessment — these files may need updating even if their path doesn't match the changed resource name. Include all discovered files in the \`recommendations[]\` passed to \`skyramp_actions\` — do NOT edit them directly without going through \`skyramp_actions\` first.`
229
+ : "";
225
230
  const existingTestSection = `**Existing tests (${existingTests?.length ?? 0} total) — assess ALL of the following:**
226
231
  ${testList}
227
232
  ${noTestsNote}
228
233
  **UPDATE scope notes:**
229
234
  - UPDATE is an in-place edit — do not recommend creating a new file for an already-tested endpoint.
230
235
  - If a new HTTP method is added to a resource, emit a separate UPDATE entry for every existing test file that covers that resource (contract, integration, UI).
236
+ - **When multiple test files cover the same changed resource, prefer the most resource-specific file** (e.g. a file whose name directly reflects the changed resource) over a generic shared test file. Only UPDATE both if the change affects base types shared across both files.
231
237
 
232
238
  For each test above, output one entry:
233
239
  - **IGNORE**: \`{testFile} — IGNORE: {reason the diff cannot reach this test's endpoint}\`
@@ -253,7 +259,7 @@ Rationale: DELETE because {quoted diff signal}; all covered endpoints removed
253
259
  \`\`\`
254
260
  Include ALL actions — IGNORE, VERIFY, UPDATE, REGENERATE, DELETE — in the \`recommendations[]\` passed to \`skyramp_actions\`. For \`[external]\` tests: \`skyramp_actions\` will apply UPDATE edits and surface REGENERATE/DELETE as report-only findings.
255
261
  Be concise — one line per IGNORE/VERIFY entry.`;
256
- const sections = [existingTestSection, finalStep];
262
+ const sections = [symbolDiscoveryStep, existingTestSection, finalStep].filter(Boolean);
257
263
  return `<output_format>
258
264
  Complete ALL of the following:
259
265
 
@@ -127,7 +127,7 @@ An unintegrated non-route component has no DOM node in the running app and canno
127
127
  return `${canInline ? "Read the \`<diff>\` above" : `Read the diff file at \`${p.diffFilePath}\``} and identify every new or modified API endpoint — route registrations, handler methods, controller annotations. Then use the **Router Mounting / Nesting** section above to reconstruct the full URL path for each endpoint by chaining all parent router prefixes down to the handler (e.g. a handler in a file with \`prefix="/reviews"\` that is mounted at \`/{product_id}\` under a router mounted at \`/api/v1/products\` → full path \`/api/v1/products/{product_id}/reviews\`).
128
128
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
129
129
  For each endpoint found: note the HTTP method, full path, and source file.
130
- ${preDetectedEndpoints ? "The endpoint catalog above already lists some changed endpoints — verify and augment with anything it missed." : "No endpoints were pre-detected in the changed files — extract them from the diff."}
130
+ ${preDetectedEndpoints ? "The endpoint catalog above already lists some changed endpoints — verify and augment with anything it missed. **If a changed file contains many endpoints (hub/routing files like api.go, routes.py, router.ts), cross-reference against the diff hunks to identify only the endpoints whose handler code actually changed — do not include neighboring routes whose code was untouched.**" : "No endpoints were pre-detected in the changed files — extract them from the diff."}
131
131
  **Also identify removed endpoints**: Look for deleted route annotations (lines starting with \`-\` in the diff) in modified files (files that still exist but had routes deleted). A removed endpoint is a route definition present in the base branch but absent in the current branch. Cross-reference against the scanned endpoint listing below — if a deleted route annotation's endpoint still appears there (e.g. moved to another file), it is NOT removed. Only flag endpoints that are truly gone from the codebase.
132
132
  **CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
133
133
  identify which parameters are URL query params vs request body. Look at framework-specific
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { type Service } from "../../workspace/workspace.js";
3
3
  export declare function getTestbotPrompt(prTitle: string, prDescription: string, summaryOutputFile: string, repositoryPath: string, baseBranch?: string, maxRecommendations?: number, maxGenerate?: number, _maxCritical?: number, // Reserved — accepted for API compat but not yet wired into prompt
4
- prNumber?: number, userPrompt?: string, services?: Service[], stateOutputFile?: string, uiCredentials?: string, testsRepoDir?: string): string;
4
+ prNumber?: number, userPrompt?: string, services?: Service[], uiCredentials?: string, testsRepoDir?: string): string;
5
5
  /**
6
6
  * Read services from .skyramp/workspace.yml. Returns empty array if
7
7
  * the workspace file doesn't exist or can't be parsed.
@@ -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";
@@ -19,7 +18,7 @@ const CONTRACT_MODE_GUIDANCE = CONSUMER_MODE_ENABLED
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.`;
21
20
  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) {
21
+ prNumber, userPrompt, services, uiCredentials, testsRepoDir) {
23
22
  maxGenerate = Math.min(Math.max(maxGenerate, 0), maxRecommendations);
24
23
  // For follow-up requests: emit the @skyramp-testbot header + guardrails + retrieve-recommendations step.
25
24
  // For first-run prompts: emit the full Task 1 analysis + maintenance section.
@@ -42,7 +41,7 @@ Verify the prompt inside <USER_PROMPT> is related to adding or removing tests fr
42
41
  - If the prompt matches one or more tests in the Additional Recommendations → proceed to Task 1 (Skip Analysis).
43
42
 
44
43
  ### 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}"` : ""}.
44
+ Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
46
45
  This will fetch the previous TestBot report from the PR comments and return deduplicated recommendations.
47
46
  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
47
  `
@@ -51,7 +50,7 @@ Use those recommendations as your baseline. Only add or remove tests that the us
51
50
 
52
51
  ## Task 1: Analyze & Maintain
53
52
 
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}"` : ""}.
53
+ 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
54
 
56
55
  **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
56
 
@@ -63,7 +62,13 @@ Use those recommendations as your baseline. Only add or remove tests that the us
63
62
 
64
63
  2. **Maintain existing tests:**
65
64
 
66
- ${buildDriftAnalysisPrompt()}
65
+ 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.
66
+
67
+ 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.
68
+
69
+ 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.
70
+
71
+ d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
67
72
 
68
73
  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
74
  - Computed fields not recalculated after mutation (e.g. \`total_amount\` unchanged after items are added/removed)
@@ -286,7 +291,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
286
291
  - 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
292
  - 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
293
  - **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.
294
+ **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
295
  **Skip only if one of these conditions is met:**
291
296
  - **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
292
297
  - **(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,9 +471,7 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
466
471
  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
472
  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
473
 
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).
474
+ **Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete.
472
475
  - Only report test results for files you actually ran.
473
476
  **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
477
 
@@ -494,16 +497,24 @@ In these cases:
494
497
 
495
498
  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
499
 
497
- Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}". Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
500
+ 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.
498
501
 
499
502
  - **additionalRecommendations**: AT MOST ${maxRecommendations - maxGenerate} items.
500
503
  - 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
504
  - 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.
505
+ - **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.
506
+ - **testResults**: For UI/E2E test entries, include the \`videoPath\` field if \`skyramp_execute_test\` returned one.
503
507
 
504
508
  ---
505
509
 
506
510
  ${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modularize: false })}`;
511
+ // TODO: merge stateFile and summaryOutputFile into a single file (follow-up PR).
512
+ // Currently the stateFile path is derived server-side from RUNNER_TEMP (GitHub Actions only);
513
+ // other CI systems (Jenkins, GitLab, Buildkite) require the caller to set RUNNER_TEMP.
514
+ // Once merged, the path flows through summaryOutputFile — already testbot-controlled and
515
+ // CI-agnostic — eliminating the RUNNER_TEMP dependency and the LLM confusion that motivated
516
+ // removing stateOutputFile from the prompt schema. Remove the RUNNER_TEMP branch in
517
+ // AnalysisStateManager.ts when this is done.
507
518
  }
508
519
  function escapeXml(value) {
509
520
  return value
@@ -578,10 +589,6 @@ export function registerTestbotPrompt(server) {
578
589
  .string()
579
590
  .optional()
580
591
  .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
592
  uiCredentials: z
586
593
  .string()
587
594
  .optional()
@@ -597,7 +604,7 @@ export function registerTestbotPrompt(server) {
597
604
  },
598
605
  }, async (args) => {
599
606
  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);
607
+ 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);
601
608
  if (args.workspaceValidationFailed) {
602
609
  prompt = buildWorkspaceRecoveryPrefix(args.repositoryPath) + prompt;
603
610
  }
@@ -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
  }
@@ -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
+ }
@@ -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);
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
  }
@@ -3,6 +3,15 @@ export declare const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.28";
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