@skyramp/mcp 0.2.9 → 0.2.10-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/build/index.js +1 -1
  2. package/build/prompts/enhance-assertions/uiAssertionsPrompt.js +28 -1
  3. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +1 -0
  4. package/build/prompts/personas.d.ts +2 -2
  5. package/build/prompts/personas.js +2 -2
  6. package/build/prompts/sut-setup/modes/dockerComposePrompt.js +3 -0
  7. package/build/prompts/sut-setup/shared.js +1 -3
  8. package/build/prompts/test-maintenance/drift-analysis-prompt.d.ts +1 -1
  9. package/build/prompts/test-maintenance/drift-analysis-prompt.js +10 -5
  10. package/build/prompts/test-maintenance/driftAnalysisSections.js +1 -4
  11. package/build/prompts/test-maintenance/driftAnalysisShared.d.ts +7 -1
  12. package/build/prompts/test-maintenance/driftAnalysisShared.js +19 -1
  13. package/build/prompts/test-maintenance/uiDriftAnalysisSections.d.ts +0 -6
  14. package/build/prompts/test-maintenance/uiDriftAnalysisSections.js +0 -16
  15. package/build/prompts/test-recommendation/recommendationShared.js +1 -1
  16. package/build/prompts/testbot/testbot-prompts.js +8 -8
  17. package/build/resources/testbotResource.js +1 -1
  18. package/build/services/TestDiscoveryService.d.ts +0 -1
  19. package/build/services/TestDiscoveryService.js +13 -33
  20. package/build/services/TestDiscoveryService.test.js +136 -1
  21. package/build/services/TestExecutionService.d.ts +1 -1
  22. package/build/tools/executeSkyrampTestTool.js +40 -71
  23. package/build/tools/submitReportTool.d.ts +20 -20
  24. package/build/tools/submitReportTool.js +113 -31
  25. package/build/tools/submitReportTool.test.js +411 -94
  26. package/build/tools/test-management/actionsTool.js +135 -53
  27. package/build/tools/test-management/actionsTool.test.d.ts +1 -0
  28. package/build/tools/test-management/actionsTool.test.js +297 -0
  29. package/build/tools/test-management/analyzeChangesTool.d.ts +9 -0
  30. package/build/tools/test-management/analyzeChangesTool.js +63 -152
  31. package/build/tools/test-management/analyzeChangesTool.test.js +34 -3
  32. package/build/tools/test-management/analyzeTestHealthTool.js +24 -157
  33. package/build/tools/test-management/analyzeTestHealthTool.test.js +15 -215
  34. package/build/tools/workspace/initializeWorkspaceTool.js +35 -15
  35. package/build/types/FrontendIntegration.d.ts +25 -0
  36. package/build/types/FrontendIntegration.js +19 -0
  37. package/build/types/RepositoryAnalysis.d.ts +8 -8
  38. package/build/types/TestAnalysis.d.ts +16 -50
  39. package/build/types/TestAnalysis.js +0 -28
  40. package/build/types/TestTypes.d.ts +3 -1
  41. package/build/types/TestTypes.js +3 -1
  42. package/build/utils/AnalysisStateManager.d.ts +13 -1
  43. package/build/utils/docker.test.js +1 -1
  44. package/build/utils/featureFlags.d.ts +1 -1
  45. package/build/utils/featureFlags.js +1 -1
  46. package/build/utils/frontendIntegration.d.ts +9 -0
  47. package/build/utils/frontendIntegration.js +237 -0
  48. package/build/utils/frontendIntegration.test.d.ts +1 -0
  49. package/build/utils/frontendIntegration.test.js +229 -0
  50. package/build/utils/pr-comment-parser.d.ts +3 -3
  51. package/build/utils/pr-comment-parser.js +6 -6
  52. package/build/utils/pr-comment-parser.test.js +3 -3
  53. package/build/utils/repoScanner.d.ts +7 -0
  54. package/build/utils/repoScanner.js +14 -6
  55. package/build/utils/repoScanner.test.js +9 -9
  56. package/build/utils/routeParsers.d.ts +32 -0
  57. package/build/utils/routeParsers.js +205 -2
  58. package/build/utils/routeParsers.test.js +279 -29
  59. package/build/utils/sourceRouteExtractor.js +174 -5
  60. package/build/utils/sourceRouteExtractor.test.js +173 -0
  61. package/build/utils/utils.d.ts +11 -0
  62. package/build/utils/utils.js +19 -0
  63. package/build/utils/utils.test.js +23 -1
  64. package/build/utils/versions.d.ts +3 -3
  65. package/build/utils/versions.js +1 -1
  66. package/node_modules/playwright/lib/mcp/browser/tools/files.js +8 -3
  67. package/node_modules/playwright/lib/mcp/browser/tools/wait.js +1 -1
  68. package/node_modules/playwright/lib/mcp/skyramp/exportTool.js +10 -1
  69. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +33 -9
  70. package/node_modules/playwright/lib/mcp/test/skyRampExport.js +7 -1
  71. package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +44 -0
  72. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/{index.B_7ywgmr.js → index.d4gkVSou.js} +1 -1
  73. package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +1 -1
  74. package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
  75. package/node_modules/playwright/package.json +1 -1
  76. package/package.json +3 -3
package/build/index.js CHANGED
@@ -96,7 +96,7 @@ Before calling ANY test generation tool, you MUST follow this flow:
96
96
  4. **CRITICAL — endpointURL**: The \`endpointURL\` parameter MUST be the full URL to the specific endpoint being tested, NOT just the base URL. Construct it by combining \`api.baseUrl\` with the endpoint path. Example: if \`api.baseUrl\` is \`http://localhost:8000\` and the endpoint is \`/api/v1/products\`, pass \`endpointURL: "http://localhost:8000/api/v1/products"\`. NEVER pass just the base URL (e.g. \`http://localhost:8000\`) as \`endpointURL\`.
97
97
  5. **CRITICAL — scenario generation**: When calling \`skyramp_batch_scenario_test_generation\`, ALWAYS pass:
98
98
  - \`baseURL\`: The full base URL from \`api.baseUrl\` (e.g., \`http://localhost:3000\`). This determines the scheme, host, and port in the generated trace. Without it, the trace defaults to https:443 which is almost always wrong for local development.
99
- - \`authHeader\`: Which HTTP header carries the auth credential. Get it from \`api.authHeader\` in workspace config. Examples: \`Authorization\` (Bearer/Token auth), \`X-Api-Key\` (API key auth), \`Cookie\` (session/cookie auth like NextAuth). Pass \`""\` to skip auth entirely (unauthenticated endpoints or \`api.authType: "none"\`).
99
+ - \`authHeader\`: Which HTTP header carries the auth credential. Get it from \`api.authHeader\` in workspace config. Examples: \`Authorization\` (Bearer/Token auth), \`X-Api-Key\` (API key auth), \`Cookie\` (session/cookie auth like NextAuth). Pass \`""\` to document the endpoint as unauthenticated in the generated test's assertions (unauthenticated endpoints or \`api.authType: "none"\`).
100
100
  - \`authScheme\`: Only when \`authHeader\` is \`Authorization\`. The prefix before the token (e.g., \`"Bearer"\` → \`Authorization: Bearer <token>\`). **Derive from**: (1) OpenAPI spec \`securitySchemes\`/\`securityDefinitions\`, (2) source code auth middleware, (3) workspace \`api.authType\`. **Do NOT guess.**
101
101
  - \`authToken\`: The full header value, used verbatim. When omitted, \`SKYRAMP_PLACEHOLDER_TOKEN\` is auto-generated. Only provide when the header needs a specific format (e.g., \`"session=${AUTH_PLACEHOLDER_TOKEN}"\` for Cookie). **Do NOT fabricate token values.**
102
102
  - \`apiSchema\` is OPTIONAL — omit it for code-first apps without OpenAPI specs.
@@ -4,6 +4,33 @@ const UI_ASSERTION_CATEGORIES = [
4
4
  {
5
5
  name: "Critical UI Assertions",
6
6
  rules: [
7
+ {
8
+ title: "Exercise the changed interaction (dynamic behavior)",
9
+ description: "When the PR introduces or changes interactive behavior (toggle, theme switch, expand/collapse, dialog open/close, focus management, optimistic update), the test MUST perform the triggering action and assert BOTH the state before AND after the action — not only the default or resting state. WHY: the PR's change IS the transition; a test that asserts only the initial/resting state would still pass even if the interaction were completely broken, so it gives zero coverage of what changed. If the test asserts only the resting state, add the action that triggers the change and assert the resulting transition — never mark this not applicable merely because the generated test lacks an action.",
10
+ subPoints: [
11
+ "Toggle / switch: click the control and assert the observable flips (e.g. data-theme 'light' -> 'dark'), then click again and assert it reverses.",
12
+ "Dialog / modal: open it and assert focus moves into the dialog; close via ESC or Cancel and assert focus returns to the trigger.",
13
+ "The after-state must differ from the before-state — a single assertion of the unchanged default value does not verify the change.",
14
+ ],
15
+ examples: [
16
+ {
17
+ language: "javascript",
18
+ code: `await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
19
+ await page.getByTestId('theme-toggle').click();
20
+ await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');`,
21
+ },
22
+ {
23
+ // Python (playwright-python) — same before/after transition pattern
24
+ language: "python",
25
+ code: `trigger = page.get_by_test_id('delete-recipe')
26
+ trigger.click()
27
+ expect(page.get_by_role('dialog')).to_be_focused()
28
+ page.keyboard.press('Escape')
29
+ # focus returns to the trigger
30
+ expect(trigger).to_be_focused()`,
31
+ },
32
+ ],
33
+ },
7
34
  {
8
35
  title: "Selector constraints",
9
36
  description: "Every assertion uses a selector already in the file. Never invent `data-testid`, role names, or classes.",
@@ -240,7 +267,7 @@ Before editing the given test file, you must output a \`<thinking>\` block. The
240
267
  3. Classify each in-scope action / selector / captured response by its applicable assertion category, marking each category APPLICABLE or NOT APPLICABLE.
241
268
  - Critical UI Assertions applies when there is a collection / repeated element (\`toHaveCount\`), a pageerror handler, a captured network response, a negative-only or URL-only test needing a positive-path companion, or a tautological locator-by-text + assert-text to replace.
242
269
  - Computed Values applies when there is a knowable exact text / value / attribute, including a definition-page displayed value or an image \`src\`.
243
- - Post-edit State applies only when the test contains a state-changing action (form fill+submit, save / delete / create / toggle, checkbox click, hover / mouseout, refresh, reload, JS update, form edit); otherwise NOT APPLICABLE.
270
+ - Post-edit State applies whenever EITHER (a) the test contains a state-changing action (form fill+submit, save / delete / create / toggle, checkbox click, hover / mouseout, refresh, reload, JS update, form edit), OR (b) the PR changes interactive behavior — in case (b), add the triggering action and assert the before/after transition (see "Exercise the changed interaction"). Mark NOT APPLICABLE only when the PR changes no interactive behavior and the test has no state-changing action.
244
271
  4. For each in-scope item, output one JSON object using the template below. The output is an array — repeat the object template below once per in-scope item.
245
272
  - \`action_or_selector_or_response\`: the selector, action, or captured network response this entry covers.
246
273
  - \`assertion_categories\`: an object that MUST contain every category name below as a key. The value of each category is itself an object that MUST contain every rule title under that category as a key. For each rule, the value is an array of assertion lines you will add for this item under that rule. Use \`[]\` only when the rule does not apply to this item — every category key and every rule key must still be present. This forces you to consider every rule for every item.
@@ -97,6 +97,7 @@ Create one service entry per deployable unit. You MUST include every backend/API
97
97
  <api_fields>
98
98
  1. api.schemaPath: Path or URL to an OpenAPI or Swagger schema. Search for openapi.json, openapi.yaml, swagger.json, or swagger.yaml files. Framework defaults are: FastAPI serves /openapi.json, Express serves /api-docs, and Spring serves /v3/api-docs. For locally-run services, use a localhost URL. For cloud or externally hosted services (such as Salesforce, Vercel, or Cloudflare), use the actual deployment URL found in config or documentation.
99
99
  2. api.baseUrl (required): The base URL where the service is reachable, such as "http://localhost:3000" or "https://api.example.com". Derive from docker-compose ports, app config, README, or environment variables. Use localhost for services run locally and the actual deployment URL for cloud or externally hosted services. NEVER fabricate a URL. Only use URLs found in config files, README, or environment variables.
100
+ Include the API path prefix (e.g. \`/api\`, \`/api/v1\`, \`/v1\`) when one applies — not just \`host:port\`. A bare host means every test request hits the root and 404s. Derive the prefix from the strongest available signal: OpenAPI \`servers[].url\` or \`basePath\`; the app's routing config (FastAPI \`root_path\`, Express \`app.use('/api/v1', ...)\`, Rails routes prefix, Django \`urlpatterns\` mount); or the longest common leading path across the existing test suite's endpoints. If none of those yield a prefix, leave it bare AND flag it in your summary so the user can review.
100
101
  3. api.authType (required): The authentication type. Valid values are: ${AUTH_TYPES_PROMPT_LIST}
101
102
  Detect by checking in the following order (language-agnostic, apply whichever signals match):
102
103
  a. Dependencies and packages (package.json, requirements.txt, go.mod, Gemfile, composer.json, pom.xml, build.gradle):
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Skyramp personas injected into tool descriptions and prompts.
3
3
  *
4
- * In TestBot environments (SKYRAMP_FEATURE_TESTBOT=1), the persona is injected
4
+ * In Testbot environments (SKYRAMP_FEATURE_TESTBOT=1), the persona is injected
5
5
  * once via `claude --append-system-prompt` from the testbot GitHub Action
6
6
  * (see testbot.git `src/agents/claude.ts` `loadQaPersona()`) rather than
7
7
  * repeating it in every tool description. In that case getPersonaPrefix()
@@ -14,7 +14,7 @@
14
14
  export declare const SKYRAMP_QA_PERSONA = "You are acting as a Skyramp QA Automation Engineer. Your responsibility is to translate user test intent into precise, deterministic test artifacts \u2014 whether generating API tests from specs, recording browser interactions for UI flows, or maintaining existing test suites. Derive all parameters strictly from the codebase, workspace config, API schemas, and page snapshots. Never guess or hallucinate values.";
15
15
  /**
16
16
  * Returns the persona prefix for use in tool descriptions.
17
- * Returns an empty string when running inside TestBot — the testbot action
17
+ * Returns an empty string when running inside Testbot — the testbot action
18
18
  * appends the persona via `claude --append-system-prompt` instead, so we
19
19
  * avoid duplicating it in every tool description.
20
20
  */
@@ -2,7 +2,7 @@ import { isTestbotEnabled } from "../utils/featureFlags.js";
2
2
  /**
3
3
  * Skyramp personas injected into tool descriptions and prompts.
4
4
  *
5
- * In TestBot environments (SKYRAMP_FEATURE_TESTBOT=1), the persona is injected
5
+ * In Testbot environments (SKYRAMP_FEATURE_TESTBOT=1), the persona is injected
6
6
  * once via `claude --append-system-prompt` from the testbot GitHub Action
7
7
  * (see testbot.git `src/agents/claude.ts` `loadQaPersona()`) rather than
8
8
  * repeating it in every tool description. In that case getPersonaPrefix()
@@ -15,7 +15,7 @@ import { isTestbotEnabled } from "../utils/featureFlags.js";
15
15
  export const SKYRAMP_QA_PERSONA = `You are acting as a Skyramp QA Automation Engineer. Your responsibility is to translate user test intent into precise, deterministic test artifacts — whether generating API tests from specs, recording browser interactions for UI flows, or maintaining existing test suites. Derive all parameters strictly from the codebase, workspace config, API schemas, and page snapshots. Never guess or hallucinate values.`;
16
16
  /**
17
17
  * Returns the persona prefix for use in tool descriptions.
18
- * Returns an empty string when running inside TestBot — the testbot action
18
+ * Returns an empty string when running inside Testbot — the testbot action
19
19
  * appends the persona via `claude --append-system-prompt` instead, so we
20
20
  * avoid duplicating it in every tool description.
21
21
  */
@@ -93,6 +93,9 @@ Steps:
93
93
  - The \`--project-directory .\` means build context is the repository root (NOT .skyramp/sut/)
94
94
  - Healthchecks for ALL services (testbot relies on \`--wait\`)
95
95
  - Simple testbot credentials in environment (admin@testbot.com / testbot)
96
+ - Never paste cryptographic material into the \`environment:\` section of the compose file. This rule covers RSA and EC private keys, PEM blobs, JWK JSON, OAuth client secrets, and any other signing keypair. In practice, refuse anything that looks like a PEM block (text starting with \`-----BEGIN \` followed by \`PRIVATE KEY\`, \`RSA PRIVATE KEY\`, \`EC PRIVATE KEY\`, \`OPENSSH PRIVATE KEY\`, or \`ENCRYPTED PRIVATE KEY\` and the matching \`-----\`), a JWK in base64url-encoded JSON form (strings starting with \`eyJrdHkiOi\`, which decodes to \`{"kty":\`), or any base64 string longer than about 200 characters that looks like a key. The compose file is committed to the customer's repository, so anything written here lands in their PR diff and stays in git history — including the private half of any keypair. Two cases to handle:
97
+ 1. **Symmetric secrets** — when the app accepts a plain string (\`JWT_SECRET\`, \`SESSION_SECRET\`, signing salts, HMAC keys), use a clearly-marked dummy literal so a code reviewer immediately recognizes it as fake. For example: \`JWT_SECRET: "testbot-dummy-do-not-use-in-production"\`.
98
+ 2. **Asymmetric keys** — when the app requires a real keypair (RS256 JWT signing, gRPC TLS, signing keypairs), do not commit the keys. Instead, generate them inside the container at startup so they exist only for the container's lifetime. First look at the app's source or config to determine what format it expects: PEM, JWK, or raw base64. Then override the service's \`entrypoint\` with a short script that generates a keypair in that format, exports the key bodies into the env vars the app reads, and finally \`exec\`s the app's original command. Pick the tool that matches the format: \`openssl genpkey\` for PEM, \`python -m jwcrypto\` / \`node -e "require('jose')..."\` / \`step crypto jwk create\` for JWK, \`openssl rand -base64\` for a raw secret. Prefer a tool that already exists in the app's runtime image — for instance, a Django service usually has \`jwcrypto\` or \`josepy\` installed already — rather than installing one at startup. Testbot fetches its bearer token via \`get-auth-token.sh\`, which calls the app's login endpoint; the app signs the token with its in-container private key and returns it as an opaque string. The test client never reads the private key directly, so per-container ephemeral keys are enough — no GitHub repo secrets, no out-of-band key sharing.
96
99
  - Generous \`start_period\` (30-120s) for CI cold builds
97
100
  - For monorepos: may need a builder service that runs first
98
101
 
@@ -80,9 +80,7 @@ export function buildWorkspaceInitSection() {
80
80
  1. Call \`skyramp_init_scan\` with \`workspacePath\` set to that repository root → follow the returned instructions to discover all services.
81
81
  2. Call \`skyramp_init_workspace\` with the same \`workspacePath\`, \`services\`, and the \`scanToken\` from step 1.
82
82
 
83
- This produces a baseline \`.skyramp/workspace.yml\` listing the services Testbot will test. Later steps in this SUT bootstrap run write \`.skyramp/sut/*\` files alongside it. You might have to update the per-service \`runtimeDetails.serverStartCommand\` to match the new SUT files created, so Testbot brings up the services correctly.
84
-
85
- Also verify each service's \`api.baseUrl\` includes the API path prefix (e.g. \`/api\`, \`/api/v1\`, \`/v1\`) — not just \`host:port\`. A bare host means every test request hits the root and 404s. Derive the prefix from the strongest available signal: OpenAPI \`servers[].url\` or \`basePath\`; the app's routing config (FastAPI \`root_path\`, Express \`app.use('/api/v1', ...)\`, Rails routes prefix, Django \`urlpatterns\` mount); or the longest common leading path across the existing test suite's endpoints. If none of those yield a prefix, leave it bare AND flag it in your summary so the user can review.`;
83
+ Later steps in this SUT bootstrap run write \`.skyramp/sut/*\` files alongside the workspace file. If you need to update per-service \`runtimeDetails.serverStartCommand\` to match the new SUT files, call \`skyramp_init_workspace\` again with \`force: true\` and the full updated services array — never use Edit/Write directly on \`.skyramp/workspace.yml\`. See the \`skyramp_init_workspace\` tool description for the edit-mode contract.`;
86
84
  }
87
85
  export function buildLocalValidationSection() {
88
86
  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.
@@ -14,4 +14,4 @@ export type UiDriftParams = {
14
14
  export declare function buildDriftAnalysisPrompt(stateFile: string | undefined, apiTests: Array<{
15
15
  testFile: string;
16
16
  source?: string;
17
- }>, ui?: UiDriftParams): string;
17
+ }>, ui?: UiDriftParams, repoPaths?: string[]): string;
@@ -1,6 +1,6 @@
1
1
  import { buildActionDecisionTree, buildCheckAdditiveFields, buildCheckEndpointExistence, buildCheckResponseShape, buildCheckAuthAndAuthorization, buildCheckBehavioralContract, buildCheckAssignAction, buildDriftOutputChecklist, } from "./driftAnalysisSections.js";
2
- import { buildUiActionDecisionTree, buildUiCheckRouteExistence, buildUiCheckSelectors, buildUiCheckPageObjects, buildUiCheckBehavioralChanges, buildUiCheckAssignAction, buildUiCheckExportedSymbols, buildUiDriftOutputChecklist, } from "./uiDriftAnalysisSections.js";
3
- import { RECOMMENDATIONS_INSTRUCTION } from "./driftAnalysisShared.js";
2
+ import { buildUiActionDecisionTree, buildUiCheckRouteExistence, buildUiCheckSelectors, buildUiCheckPageObjects, buildUiCheckBehavioralChanges, buildUiCheckAssignAction, buildUiDriftOutputChecklist, } from "./uiDriftAnalysisSections.js";
3
+ import { RECOMMENDATIONS_INSTRUCTION, buildSymbolDiscoveryStep } from "./driftAnalysisShared.js";
4
4
  import { PromptPlan } from "../test-recommendation/promptPlan.js";
5
5
  const _apiPlan = new PromptPlan()
6
6
  .addPhase("maintenance", "Test Maintenance Assessment", {
@@ -21,7 +21,6 @@ const _uiPlan = new PromptPlan()
21
21
  headerLevel: "##",
22
22
  stepFormat: "hash",
23
23
  })
24
- .step("UI_SYMBOL_PRESCAN", "Exported symbol pre-scan — find additional impacted tests via grep", (p) => buildUiCheckExportedSymbols(p.changedFrontendFiles))
25
24
  .step("UI_ASSESS", "UI Action Decision Tree — assess each UI test against the diff and blueprints", () => buildUiActionDecisionTree())
26
25
  .subStep("UI_ROUTE_EXISTENCE", "Route existence", () => buildUiCheckRouteExistence())
27
26
  .subStep("UI_SELECTORS", "Selector validity", () => buildUiCheckSelectors())
@@ -36,8 +35,14 @@ const _uiPlan = new PromptPlan()
36
35
  * - ui: when provided, appends a UI drift section for component/browser tests.
37
36
  * Omit when no frontend files changed or no UI tests exist.
38
37
  */
39
- export function buildDriftAnalysisPrompt(stateFile, apiTests, ui) {
38
+ export function buildDriftAnalysisPrompt(stateFile, apiTests, ui, repoPaths) {
40
39
  const parts = [];
40
+ // Emit symbol discovery once at the top regardless of how many sections follow.
41
+ // Placing it inside each plan's checklist caused duplication on mixed diffs.
42
+ const hasAnyTests = apiTests.length > 0 || (ui?.tests?.length ?? 0) > 0;
43
+ const discovery = buildSymbolDiscoveryStep(hasAnyTests, repoPaths);
44
+ if (discovery)
45
+ parts.push(discovery);
41
46
  // Include API drift when there are API tests, or when UI drift is not running
42
47
  // (ensures skyramp_actions is always reachable even if apiTests is empty).
43
48
  if (apiTests.length > 0 || !ui) {
@@ -67,7 +72,7 @@ For each test above, read it to determine its type, then apply the scope gate fr
67
72
  </ui_analysis_context>
68
73
 
69
74
  `;
70
- parts.push(contextHeader + `<ui_drift_analysis_rules>\n${_uiPlan.render({ stateFile, tests: ui.tests, changedFrontendFiles: ui.changedFrontendFiles, blueprintCaptured: ui.blueprintCaptured })}\n</ui_drift_analysis_rules>`);
75
+ parts.push(contextHeader + `<ui_drift_analysis_rules>\n${_uiPlan.render({ stateFile, tests: ui.tests, blueprintCaptured: ui.blueprintCaptured })}\n</ui_drift_analysis_rules>`);
71
76
  }
72
77
  parts.push(RECOMMENDATIONS_INSTRUCTION);
73
78
  return parts.join("\n\n");
@@ -210,9 +210,6 @@ export function buildDriftOutputChecklist(stateFile, existingTests) {
210
210
  const noTestsNote = !hasTests
211
211
  ? `\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`
212
212
  : "";
213
- const symbolDiscoveryStep = hasTests
214
- ? `**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\`.`
215
- : "";
216
213
  const existingTestSection = `**Existing tests (${existingTests?.length ?? 0} total) — assess ALL of the following:**
217
214
  ${testList}
218
215
  ${noTestsNote}
@@ -244,7 +241,7 @@ Action: DELETE
244
241
  Rationale: DELETE because {quoted diff signal}; all covered endpoints removed
245
242
  \`\`\`
246
243
  Be concise — one line per VERIFY/IGNORE entry.`;
247
- const sections = [symbolDiscoveryStep, existingTestSection, finalStep].filter(Boolean);
244
+ const sections = [existingTestSection, finalStep];
248
245
  return `<output_format>
249
246
  Complete ALL of the following:
250
247
 
@@ -1,4 +1,10 @@
1
- /** Shared drift prompt sections (assign-action, external test policy) for API and UI prompts. */
1
+ /** Shared drift prompt sections (assign-action, external test policy, symbol discovery) for API and UI prompts. */
2
+ /**
3
+ * Build the symbol-discovery grep instruction — unified for API and UI tests.
4
+ * Two-pass: hunk-context symbols (what changed) + full file exports (what exists),
5
+ * covering aliases, indirect renders, and shared hooks the diff doesn't mention.
6
+ */
7
+ export declare function buildSymbolDiscoveryStep(hasTests: boolean, repoPaths?: string[]): string;
2
8
  /** One source of truth for the recommendations[] contract passed to skyramp_actions. */
3
9
  export declare const RECOMMENDATIONS_INSTRUCTION = "Pass VERIFY, UPDATE, REGENERATE, and DELETE entries to `recommendations[]`. Omit IGNORE.";
4
10
  export declare function buildSharedExternalTestPolicy(): string;
@@ -1,4 +1,22 @@
1
- /** Shared drift prompt sections (assign-action, external test policy) for API and UI prompts. */
1
+ /** Shared drift prompt sections (assign-action, external test policy, symbol discovery) for API and UI prompts. */
2
+ /**
3
+ * Build the symbol-discovery grep instruction — unified for API and UI tests.
4
+ * Two-pass: hunk-context symbols (what changed) + full file exports (what exists),
5
+ * covering aliases, indirect renders, and shared hooks the diff doesn't mention.
6
+ */
7
+ export function buildSymbolDiscoveryStep(hasTests, repoPaths) {
8
+ if (!hasTests)
9
+ return "";
10
+ const repoList = repoPaths && repoPaths.length > 1
11
+ ? repoPaths.map(p => `\`${p}\``).join(", ")
12
+ : repoPaths?.[0] ? `\`${repoPaths[0]}\`` : "the repository";
13
+ return `**Symbol discovery (before assessing):** Find additional test files that reference changed code:
14
+
15
+ 1. **Hunk context:** Extract function and class names from \`@@\` context lines and changed endpoint URL path segments. Grep ${repoList} for each.
16
+ 2. **Full file exports:** For each changed source file in the diff, read it and extract all exported symbols (component names, function names, hooks, class names). Grep ${repoList} for each (case-insensitive). This catches tests using aliased imports, indirect renders, or shared hooks not visible in the diff hunk.
17
+
18
+ Add any matched test files not already in your assessment, and include them in \`recommendations[]\`.`;
19
+ }
2
20
  /** One source of truth for the recommendations[] contract passed to skyramp_actions. */
3
21
  export const RECOMMENDATIONS_INSTRUCTION = `Pass VERIFY, UPDATE, REGENERATE, and DELETE entries to \`recommendations[]\`. Omit IGNORE.`;
4
22
  export function buildSharedExternalTestPolicy() {
@@ -3,12 +3,6 @@
3
3
  * This is the main entry point for UI drift analysis.
4
4
  */
5
5
  export declare function buildUiActionDecisionTree(): string;
6
- /**
7
- * Pre-scan step: agent greps changed files' exported symbols across the test
8
- * directory to catch tests that server-side discovery missed — aliased imports,
9
- * indirect renders, or any test the blueprint diff doesn't directly reference.
10
- */
11
- export declare function buildUiCheckExportedSymbols(changedFrontendFiles?: string[]): string;
12
6
  /**
13
7
  * Check if routes targeted by the UI test still exist.
14
8
  */
@@ -57,22 +57,6 @@ Build a detection list first: for each matched diff line, write one line: \`{pat
57
57
  - Primary matching signal: read the test to check if it directly renders a changed component — if so, it is impacted.
58
58
  </ui_decision_rules>`;
59
59
  }
60
- /**
61
- * Pre-scan step: agent greps changed files' exported symbols across the test
62
- * directory to catch tests that server-side discovery missed — aliased imports,
63
- * indirect renders, or any test the blueprint diff doesn't directly reference.
64
- */
65
- export function buildUiCheckExportedSymbols(changedFrontendFiles) {
66
- const files = (changedFrontendFiles ?? []).join(", ") || "the changed frontend files";
67
- return `The server pre-discovered the tests listed above using exported symbols and UI signals (testids, aria-labels, i18n keys, CSS classes) from the diff. Before assessing, do a quick complementary grep to catch any tests the server may have missed — aliased imports, indirect renders, or tests using symbols not present in the diff:
68
-
69
- 1. For each changed frontend file (${files}):
70
- a. Read the file and extract exported symbols — component names, function names, hook names, class names.
71
- b. Grep the test directory for each symbol name (case-insensitive).
72
- c. For any matching test file not already in the \`<ui_analysis_context>\` list — read it and apply the decision tree.
73
-
74
- This catches tests that import a changed symbol under an alias, exercise a shared hook, or render the changed component indirectly via a parent.`;
75
- }
76
60
  /**
77
61
  * Check if routes targeted by the UI test still exist.
78
62
  */
@@ -76,7 +76,7 @@ export function buildExternalCoverageSet(testLocations) {
76
76
  }
77
77
  return coverage;
78
78
  }
79
- // Shared TestBot task and step labels used by prompt modules that cannot import
79
+ // Shared Testbot task and step labels used by prompt modules that cannot import
80
80
  // testbot-prompts.ts directly without creating a circular dependency.
81
81
  export const TASK_UI_PRESCAN = "0";
82
82
  export const TASK_ANALYZE_MAINTAIN = "1";
@@ -86,7 +86,7 @@ Verify the prompt inside <USER_PROMPT> is related to adding or removing tests fr
86
86
 
87
87
  ### Task 1: Retrieve Previous Recommendations
88
88
  Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff"${baseBranch ? `, \`baseBranch\`: "${baseBranch}"` : ""}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""}${testsRepoDir ? `, \`testsRepoDir\`: "${testsRepoDir}"` : ""}.
89
- This will fetch the previous TestBot report from the PR comments and return deduplicated recommendations.
89
+ This will fetch the previous Testbot report from the PR comments and return deduplicated recommendations.
90
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.
91
91
  `
92
92
  : `
@@ -102,7 +102,7 @@ Use those recommendations as your baseline. Only add or remove tests that the us
102
102
  **Feature context check:** After capturing a blueprint per the UI Blueprint Capture instructions, cross-check whether it actually shows the changed feature. Look at the diff — what component, field, or UI element was changed? If that component only renders in a specific context (a particular collection type, a non-empty list, a specific user role, a form that only appears after a prior action), and the blueprint you captured doesn't contain it, navigate to the right context before using this blueprint for recommendations. A blueprint that doesn't show the changed feature produces recommendations that test the wrong thing — for example, recommending tests against a system collection when the PR changed behavior on custom collections. If you can't determine the right context from the diff and workspace config, note it in \`issuesFound\` and fall back to source-grounded recommendations for that URL.
103
103
 
104
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.
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).
105
+ **If all changed files are non-application** (CI/CD, docs, lock files, config) → skip to Task 3 (Submit Report) with empty arrays. Put the one-paragraph summary in \`businessCaseAnalysis\` (always populated; that's where end-state narration belongs); leave \`issuesFound\` empty a non-application diff is not an issue. Example narration for a Testbot onboarding PR (\`.github/workflows/skyramp-testbot.yml\` and/or files under \`.skyramp/\`): "This PR adds Skyramp Testbot GitHub Actions workflow configuration to enable automated test generation on every pull request. It also adds System Under Test (SUT) setup files under \`.skyramp/sut/\` required for the testbot workflow, to bring up services for testing. It contains no application code changes and has no testable behavioral surface."
106
106
  ${hasRelatedRepos ? `
107
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
108
 
@@ -130,7 +130,7 @@ ${hasRelatedRepos ? `
130
130
 
131
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
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.
133
+ c. Call \`skyramp_execute_test\` with \`phase: "before"\` and \`stateFile\` for every test whose action is UPDATE, REGENERATE, or DELETE. Run them sequentially, not in parallel. This captures the pre-edit baseline — do not skip even if you expect the test to fail.
134
134
 
135
135
  d. Call \`skyramp_actions\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) and apply the edits it returns.
136
136
 
@@ -370,7 +370,7 @@ ${userPrompt ? "Generate only the tests that the user requested from the Additio
370
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.
371
371
  **Skip only if one of these conditions is met:**
372
372
  - **(a) App is unreachable** — \`browser_navigate\` fails or connection is refused.
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:
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. **The server already computes this** — check \`uiContext.frontendFileIntegration\` in the \`skyramp_analyze_changes\` output: if it marks the changed file \`integrated: false\`, treat the component as unintegrated WITHOUT re-running the grep below (the tool output's accompanying instruction block already tells you what to do — do not substitute another page or trace). Only fall back to the manual grep procedure when \`frontendFileIntegration\` is absent (older MCP versions) or doesn't cover the changed file:
374
374
  1. Grep for the component's exported name AND its module path/filename across all production source files (excluding \`*.test.*\`, \`*.spec.*\`, \`*.stories.*\`, \`__tests__/\` directories — only production code imports count).
375
375
  2. If no production file imports, re-exports, or renders it, the component has no DOM node in the running app → unintegrated.
376
376
  3. **Exception**: if the same PR also adds a route/page file (e.g. under Next.js \`pages/\` or \`app/\`) that imports the component, the route IS the integration point — test through it.
@@ -547,7 +547,7 @@ Do NOT use \`page.waitForTimeout()\` with fixed delays. Do NOT retry more than o
547
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.
548
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.
549
549
 
550
- **Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete.
550
+ **Final execution (mandatory):** Do NOT call \`skyramp_execute_test\` until ALL maintenance edits AND ALL new test generation/enhancement are complete. Run these calls sequentially, not in parallel.
551
551
  - Only report test results for files you actually ran.
552
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.
553
553
 
@@ -579,7 +579,7 @@ ${hasRelatedRepos ? `
579
579
  - **additionalRecommendations**: AT MOST ${maxRecommendations - maxGenerate} items.
580
580
  - 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.
581
581
  - For \`testType: "integration"\` or \`"e2e"\` entries: omit \`primaryEndpoint\` — use \`description\` to list the endpoints involved instead.
582
- - **\`testMaintenance\`**: include one entry per test that received UPDATE, REGENERATE, DELETE, or VERIFY. Do NOT include IGNORE — a test assessed and confirmed clean is not a maintenance action and adds noise to the report.
582
+ - **\`testMaintenanceDetails\`**: include one entry per test that received UPDATE, REGENERATE, DELETE, or VERIFY. Do NOT include IGNORE — a test assessed and confirmed clean is not a maintenance action and adds noise to the report.
583
583
  - **testResults**: For UI/E2E test entries, include the \`videoPath\` field if \`skyramp_execute_test\` returned one.
584
584
 
585
585
  ---
@@ -631,7 +631,7 @@ export function buildWorkspaceRecoveryPrefix(repositoryPath) {
631
631
  export function registerTestbotPrompt(server) {
632
632
  logger.info("Registering testbot prompt");
633
633
  server.registerPrompt("skyramp_testbot", {
634
- description: "Run Skyramp TestBot to generate test recommendations and perform test maintenance for a pull request.",
634
+ description: "Run Skyramp Testbot to generate test recommendations and perform test maintenance for a pull request.",
635
635
  argsSchema: {
636
636
  prTitle: z.string().describe("Pull request title"),
637
637
  prDescription: z.string().describe("Pull request description/body"),
@@ -661,7 +661,7 @@ export function registerTestbotPrompt(server) {
661
661
  prNumber: z
662
662
  .number()
663
663
  .optional()
664
- .describe("GitHub PR number. Passed to skyramp_analyze_changes to fetch previous TestBot comments for recommendation consistency across commits."),
664
+ .describe("GitHub PR number. Passed to skyramp_analyze_changes to fetch previous Testbot comments for recommendation consistency across commits."),
665
665
  userPrompt: z
666
666
  .string()
667
667
  .optional()
@@ -14,7 +14,7 @@ export function registerTestbotResource(server) {
14
14
  list: undefined,
15
15
  });
16
16
  server.registerResource("skyramp_testbot", template, {
17
- title: "Skyramp TestBot Prompt",
17
+ title: "Skyramp Testbot Prompt",
18
18
  description: "Returns task instructions for PR test analysis, generation, and maintenance.",
19
19
  mimeType: "text/plain",
20
20
  }, async (uri) => {
@@ -121,7 +121,6 @@ export declare class TestDiscoveryService {
121
121
  * Extract API schema path from test content
122
122
  * Looks for Skyramp command line first, then fallback to other patterns
123
123
  */
124
- private extractApiSchema;
125
124
  /**
126
125
  * Extract Framework from test content
127
126
  * Looks for Skyramp command line first, then fallback to other patterns
@@ -2,6 +2,7 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import { logger } from "../utils/logger.js";
4
4
  import { TestSource } from "../types/TestAnalysis.js";
5
+ import { TestType } from "../types/TestTypes.js";
5
6
  import fg from "fast-glob";
6
7
  export class TestDiscoveryService {
7
8
  EXCLUDED_DIRS = [
@@ -329,7 +330,6 @@ export class TestDiscoveryService {
329
330
  }
330
331
  const language = this.detectLanguage(testFile);
331
332
  const testType = this.detectTestType(content);
332
- const apiSchema = this.extractApiSchema(content);
333
333
  const framework = this.extractFramework(content);
334
334
  const apiEndpoint = this.extractCoveredEndpoints(content);
335
335
  return {
@@ -337,8 +337,8 @@ export class TestDiscoveryService {
337
337
  testType,
338
338
  language,
339
339
  framework,
340
- apiSchema,
341
340
  apiEndpoint,
341
+ source: TestSource.Skyramp,
342
342
  };
343
343
  }
344
344
  /**
@@ -498,24 +498,24 @@ export class TestDiscoveryService {
498
498
  detectExternalTestType(filePath, content) {
499
499
  const lowerPath = filePath.toLowerCase();
500
500
  if (/integration|_integration/.test(lowerPath))
501
- return "integration";
501
+ return TestType.INTEGRATION;
502
502
  if (/e2e|end[_-]to[_-]end/.test(lowerPath))
503
- return "e2e";
503
+ return TestType.E2E;
504
504
  // Playwright/Cypress directory paths → e2e even without "e2e" in the filename.
505
505
  // Ensures correct testType for tests where content wasn't read (framework stays "").
506
506
  if (/[\\/]playwright[\\/]|[\\/]cypress[\\/]/.test(lowerPath))
507
- return "e2e";
507
+ return TestType.E2E;
508
508
  if (/contract/.test(lowerPath))
509
- return "contract";
509
+ return TestType.CONTRACT;
510
510
  if (/smoke/.test(lowerPath))
511
- return "smoke";
511
+ return TestType.SMOKE;
512
512
  if (/load|performance/.test(lowerPath))
513
- return "load";
513
+ return TestType.LOAD;
514
514
  if (/[\\/]unit[\\/]|_unit/.test(lowerPath))
515
- return "unit";
515
+ return TestType.UNIT;
516
516
  if (/(?:requests|fetch|axios|supertest|RestAssured)/i.test(content))
517
- return "integration";
518
- return "unknown";
517
+ return TestType.INTEGRATION;
518
+ return TestType.UNKNOWN;
519
519
  }
520
520
  detectExternalFramework(content) {
521
521
  if (/from selenium\b|import selenium\b|from appium\b|import appium\b/.test(content))
@@ -637,39 +637,19 @@ export class TestDiscoveryService {
637
637
  * Checks Skyramp command line, filename, and content patterns
638
638
  */
639
639
  detectTestType(content) {
640
- const testTypes = [
641
- "smoke",
642
- "integration",
643
- "load",
644
- "contract",
645
- "fuzz",
646
- "e2e",
647
- "ui",
648
- ];
649
- // First, try to extract from Skyramp command line
650
- // Pattern: skyramp generate smoke rest ...
651
640
  const commandMatch = content.match(/skyramp generate (\w+)/i);
652
641
  if (commandMatch && commandMatch[1]) {
653
642
  const type = commandMatch[1].toLowerCase();
654
- if (testTypes.includes(type)) {
643
+ if (Object.values(TestType).includes(type)) {
655
644
  return type;
656
645
  }
657
646
  }
658
- return "";
647
+ return TestType.UNKNOWN;
659
648
  }
660
649
  /**
661
650
  * Extract API schema path from test content
662
651
  * Looks for Skyramp command line first, then fallback to other patterns
663
652
  */
664
- extractApiSchema(content) {
665
- // First, try to extract from Skyramp command line
666
- // Pattern: --api-schema http://localhost:8000/openapi.json
667
- const apiSchemaMatch = content.match(/--api-schema\s+([^\s\\]+)/);
668
- if (apiSchemaMatch && apiSchemaMatch[1]) {
669
- return apiSchemaMatch[1];
670
- }
671
- return "";
672
- }
673
653
  /**
674
654
  * Extract Framework from test content
675
655
  * Looks for Skyramp command line first, then fallback to other patterns