@skyramp/mcp 0.1.5 → 0.1.7

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 (46) hide show
  1. package/build/index.js +6 -5
  2. package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +150 -149
  3. package/build/prompts/personas.js +2 -1
  4. package/build/prompts/test-maintenance/drift-analysis-prompt.js +2 -1
  5. package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +28 -0
  6. package/build/prompts/test-recommendation/analysisOutputPrompt.js +72 -14
  7. package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +154 -0
  8. package/build/prompts/test-recommendation/diffExecutionPlan.js +290 -0
  9. package/build/prompts/test-recommendation/fullRepoCatalog.js +271 -0
  10. package/build/prompts/test-recommendation/recommendationSections.js +4 -2
  11. package/build/prompts/test-recommendation/recommendationShared.js +68 -0
  12. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +20 -4
  13. package/build/prompts/test-recommendation/test-recommendation-prompt.js +11 -640
  14. package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +6 -6
  15. package/build/prompts/testbot/testbot-prompts.js +19 -7
  16. package/build/prompts/testbot/testbot-prompts.test.js +22 -5
  17. package/build/resources/analysisResources.js +1 -0
  18. package/build/services/ScenarioGenerationService.js +5 -1
  19. package/build/services/TestGenerationService.js +3 -0
  20. package/build/tools/code-refactor/codeReuseTool.js +3 -0
  21. package/build/tools/code-refactor/enhanceAssertionsTool.js +5 -1
  22. package/build/tools/code-refactor/modularizationTool.js +3 -0
  23. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +123 -1
  24. package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +205 -9
  25. package/build/tools/generate-tests/generateContractRestTool.js +19 -19
  26. package/build/tools/generate-tests/generateIntegrationRestTool.js +9 -2
  27. package/build/tools/generate-tests/generateUIRestTool.js +23 -8
  28. package/build/tools/test-management/analyzeChangesTool.js +218 -2
  29. package/build/tools/test-management/analyzeChangesTool.test.js +233 -1
  30. package/build/tools/workspace/initializeWorkspaceTool.js +1 -1
  31. package/build/utils/docker.test.js +1 -1
  32. package/build/utils/featureFlags.js +7 -0
  33. package/build/utils/featureFlags.test.js +81 -0
  34. package/build/utils/gitStaging.js +18 -0
  35. package/build/utils/gitStaging.test.js +87 -0
  36. package/build/utils/httpDefaults.js +17 -0
  37. package/build/utils/httpDefaults.test.js +21 -0
  38. package/build/utils/scenarioDrafting.js +37 -15
  39. package/build/utils/scenarioDrafting.test.js +66 -0
  40. package/build/utils/telemetry.js +2 -1
  41. package/build/utils/utils.js +23 -0
  42. package/build/utils/versions.js +1 -1
  43. package/node_modules/playwright/lib/mcp/browser/context.js +2 -0
  44. package/node_modules/playwright/lib/mcp/browser/tools/keyboard.js +2 -2
  45. package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +17 -26
  46. package/package.json +2 -2
package/build/index.js CHANGED
@@ -35,6 +35,7 @@ import { registerAnalysisResources } from "./resources/analysisResources.js";
35
35
  import { registerProgressResource } from "./resources/progressResource.js";
36
36
  import { AnalyticsService } from "./services/AnalyticsService.js";
37
37
  import { registerInitTriggerOnMCPInitialized } from "./utils/initAgent.js";
38
+ import { isTestbotEnabled } from "./utils/featureFlags.js";
38
39
  import { registerPlaywrightTools, registerTraceRecordingPrompt, getPlaywrightTraceService, } from "./playwright/index.js";
39
40
  const oneClickEnabled = process.env.SKYRAMP_FEATURE_ONE_CLICK === "1";
40
41
  const oneClickInstructions = oneClickEnabled
@@ -95,8 +96,8 @@ After \`skyramp_analyze_changes\`, inspect enriched data via MCP Resources (use
95
96
  Before calling ANY test generation tool, you MUST follow this flow:
96
97
 
97
98
  1. **Read** the .skyramp/workspace.yml file to get the configured defaults.
98
- 2. **Extract** the \`language\`, \`framework\`, \`outputDir\`, \`api.baseUrl\`, \`api.authHeader\`, and \`api.authType\` from the services section.
99
- 3. **Use those values** as defaults for the test generation tool call. Do NOT ask the user for these values if they are already configured in the workspace file.
99
+ 2. **Extract** the \`language\`, \`framework\`, \`testDirectory\`, \`api.baseUrl\`, \`api.authHeader\`, and \`api.authType\` from the matching service in the services section.
100
+ 3. **Use those values** as defaults for the test generation tool call. Pass the service \`testDirectory\` as the generation tool \`outputDir\`. Do NOT ask the user for these values if they are already configured in the workspace file.
100
101
  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\`.
101
102
  5. **CRITICAL — scenario generation**: When calling \`skyramp_batch_scenario_test_generation\`, ALWAYS pass:
102
103
  - \`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.
@@ -107,7 +108,7 @@ Before calling ANY test generation tool, you MUST follow this flow:
107
108
  6. **CRITICAL — integration test from scenario**: When calling \`skyramp_integration_test_generation\` with a \`scenarioFile\`:
108
109
  - If workspace has \`api.authType\` set: omit auth params entirely — passing auth here alongside workspace \`authType\` causes "${AUTH_CONFLICT_ERROR_MSG}".
109
110
  - If workspace has no \`api.authType\`: pass \`authHeader\` only (no \`authScheme\`).
110
- 7. **If the workspace file does not exist**, or the needed values (language, framework, outputDir) are missing from the workspace config, ASK the user which language and framework they want before calling the tool.
111
+ 7. **If the workspace file does not exist**, or the needed values (language, framework, testDirectory) are missing from the workspace config, ASK the user which language, framework, and outputDir they want before calling the tool.
111
112
  8. The user can always override workspace defaults by explicitly specifying values in their request.
112
113
  `,
113
114
  });
@@ -118,7 +119,7 @@ const prompts = [
118
119
  registerRecommendTestsPrompt,
119
120
  registerTraceRecordingPrompt,
120
121
  ];
121
- if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
122
+ if (isTestbotEnabled()) {
122
123
  prompts.push(registerTestbotPrompt);
123
124
  registerTestbotResource(server);
124
125
  logger.info("TestBot prompt enabled via SKYRAMP_FEATURE_TESTBOT");
@@ -169,7 +170,7 @@ const infrastructureTools = [
169
170
  registerTraceTool,
170
171
  registerTraceStopTool,
171
172
  ];
172
- if (process.env.SKYRAMP_FEATURE_TESTBOT === "1") {
173
+ if (isTestbotEnabled()) {
173
174
  infrastructureTools.push(registerSubmitReportTool);
174
175
  logger.info("TestBot tools enabled via SKYRAMP_FEATURE_TESTBOT");
175
176
  }
@@ -1,13 +1,10 @@
1
1
  import { getPersonaPrefix } from "../personas.js";
2
2
  import { AUTH_TYPES_PROMPT_LIST } from "../../utils/workspaceAuth.js";
3
- export const INIT_WORKSPACE_INSTRUCTIONS = `${getPersonaPrefix()}Your task is to scan this repository, discover ALL services, and call the \`skyramp_init_workspace\` tool with the discovered services array and the scanToken.
4
-
5
- After scanning the workspace, before calling the \`skyramp_init_workspace\` tool, you MUST:
6
-
7
- **1. Output a \`<thinking>\` block** to justify the reasoning behind each field mapping for every discovered service.
8
-
9
- **2. Then output a Discovery Summary** with the exact services array you will pass to the tool:
3
+ export const INIT_WORKSPACE_INSTRUCTIONS = `${getPersonaPrefix()}Your task is to scan this repository, discover ALL services, and call the skyramp_init_workspace tool with the discovered services array and the scanToken.
10
4
 
5
+ After scanning the workspace, before calling the skyramp_init_workspace tool, you MUST:
6
+ 1. Output a <thinking> block to justify the reasoning behind each field mapping for every discovered service. Follow the scanning sections first, then field definitions, then verification.
7
+ 2. Then output a Discovery Summary with the exact services array you will pass to the tool:
11
8
  \`\`\`json
12
9
  [
13
10
  {
@@ -18,156 +15,160 @@ After scanning the workspace, before calling the \`skyramp_init_workspace\` tool
18
15
  "api": { "schemaPath": "<path-or-url>", "baseUrl": "<url>", "authType": "<type>", "authHeader": "<header>" },
19
16
  "runtimeDetails": { "runtime": "<runtime>", "serverStartCommand": "<command>", "dockerNetwork": "<network>" }
20
17
  }
21
- // ... one entry per discovered service
22
18
  ]
23
19
  \`\`\`
24
20
 
25
- ## Step 1 — List ALL Top-Level Directories
26
-
21
+ ### List all top-level directories
22
+ <list_directories>
27
23
  Run a directory listing of the workspace root. Every top-level directory is a potential service. Common layouts:
28
-
29
24
  | Layout | Example dirs | Expect |
30
25
  |--------|-------------|--------|
31
26
  | Monorepo | apps/web, apps/api, packages/shared | 1 service per app |
32
27
  | Microservices | services/auth, services/orders | 1 service per service dir |
33
28
  | Single service | src/, lib/ | 1 service (the root) |
34
29
 
35
- ## Step 2 Inspect EVERY Candidate Directory
36
-
37
- For **each** top-level directory, check for service indicator files:
38
-
39
- **Language indicators** (presence of ANY = independent service):
40
- - package.json typescript / javascript
41
- - requirements.txt, pyproject.toml, Pipfile python
42
- - pom.xml, build.gradle → java
43
-
44
- **Test framework** (look inside the service dir):
45
- - playwright.config.* playwright
46
- - pytest.ini, conftest.py, pyproject.toml [tool.pytest] pytest
47
- - junit in pom.xml junit
48
-
49
- **API schemas** (look inside the service dir AND check known framework defaults):
50
- - openapi.json/yaml, swagger.json/yaml → schema file path
51
- - FastAPI projects http://localhost:{port}/openapi.json
52
- - Express with swagger-ui → http://localhost:{port}/api-docs
53
- - Spring Boot http://localhost:{port}/v3/api-docs
54
-
55
- ## Step 3 Check Root-Level Runtime Config
56
-
30
+ If the repo is a monorepo, check for workspace manifests BEFORE scanning individual directories:
31
+ 1. JavaScript/TypeScript monorepos:
32
+ - If pnpm-workspace.yaml exists, read the packages array to find all package directories (for example, ["apps/*", "packages/*"]).
33
+ - If turbo.json exists, read the root package.json workspaces field to enumerate all packages.
34
+ - If lerna.json exists, read the packages array in lerna.json (for example, ["packages/*"]).
35
+ - If the root package.json has a "workspaces" field, use the glob patterns to identify all packages.
36
+ - If nx.json exists, scan the projects in nx.json or project.json files in subdirectories.
37
+ 2. Java monorepos:
38
+ - If a parent pom.xml has a <modules> section, each listed module is a separate service (Maven multi-module project).
39
+ - If settings.gradle or settings.gradle.kts has include statements, each included project is a separate service (Gradle multi-project).
40
+ 3. Python monorepos:
41
+ - If a pyproject.toml has a [tool.poetry.packages] or [tool.setuptools.packages] section with multiple paths, each is a potential service.
42
+ - If multiple directories each contain their own pyproject.toml or requirements.txt, treat each as an independent service.
43
+ When any of these exist, use the workspace manifest to enumerate ALL packages and apps. Each package with its own server entry point, API routes, or UI framework is a separate service. Do NOT skip frontend packages. A Next.js app, React SPA, or Vue app in the monorepo is a service.
44
+ </list_directories>
45
+
46
+ ### Inspect every candidate directory
47
+ <inspect_directories>
48
+ For each top-level directory, check for service indicator files.
49
+
50
+ Language indicators (presence of any means an independent service):
51
+ 1. package.json indicates typescript or javascript
52
+ 2. requirements.txt, pyproject.toml, or Pipfile indicates python
53
+ 3. pom.xml or build.gradle indicates java
54
+
55
+ Test framework (look inside the service directory):
56
+ 1. playwright.config.* indicates playwright
57
+ 2. pytest.ini, conftest.py, or pyproject.toml [tool.pytest] indicates pytest
58
+ 3. junit in pom.xml indicates junit
59
+
60
+ API schemas (look inside the service directory and check known framework defaults):
61
+ 1. openapi.json/yaml or swagger.json/yaml provides the schema file path
62
+ 2. FastAPI projects serve the schema at http://localhost:{port}/openapi.json
63
+ 3. Express with swagger-ui serves at http://localhost:{port}/api-docs
64
+ 4. Spring Boot serves at http://localhost:{port}/v3/api-docs
65
+ </inspect_directories>
66
+
67
+ ### Check root-level runtime config
68
+ <runtime_config>
57
69
  Inspect the repo root (and subdirectories like .devcontainer/) for shared runtime configuration:
58
- - docker-compose.yml extract service names, ports, start commands
59
- Docker Compose ALWAYS prefixes the network name with "<project-name>_".
60
- If compose has "networks: { my-net: ... }" → actual network = "<project-name>_my-net".
61
- If no explicit networks section default network = "<project-name>_default".
62
- Project name = basename of the CWD where docker compose runs.
63
- - Makefile → extract start/dev targets
64
- - Root package.json scripts workspace-level commands
65
-
66
- ## Step 4 Build the Complete Services Array
67
-
68
- Create one service entry per deployable unit. You MUST include:
69
- - Every backend/API service (Python, Java, Go, Node.js)
70
- - Every frontend service (React, Vue, Angular, Next.js)
71
- - Set runtime fields from docker-compose.yml if present
72
-
73
- **Basic fields:**
74
- - \`serviceName\` *(required)* unique identifier, e.g. "api-gateway", "user-service"
75
- - \`language\` \`python\` | \`typescript\` | \`javascript\` | \`java\`
76
- Detect from: package.json → typescript/javascript | requirements.txt/pyproject.toml python | pom.xml/build.gradle java
77
- - \`framework\` \`playwright\` | \`pytest\` | \`robot\` | \`junit\`
78
- Detect from: pytest.ini/playwright.config/jest.config/junit in pom.xml
79
- MUST match the language: python pytest or robot | typescript/javascript → playwright | java → junit
80
- - \`testDirectory\` path relative to repo root where generated tests will be placed. **MUST match the test framework's configured test directory**:
81
- - **Playwright**: Read \`playwright.config.ts\` (or \`.js\`/\`.mjs\`) and extract the \`testDir\` value. If no \`testDir\` is specified, common defaults: "tests/", "test/".
82
- - **pytest**: Read \`pytest.ini\`, \`pyproject.toml [tool.pytest.ini_options]\`, or \`setup.cfg [tool:pytest]\` for \`testpaths\`. Common defaults: "tests/", "test/".
83
- - **JUnit**: Usually "src/test/java" — check \`pom.xml\` or \`build.gradle\` for custom test source directories.
84
- ⚠️ **CRITICAL**: If the framework config specifies a test directory, you MUST use that exact path
85
-
86
- **API fields:**
87
- - \`api.schemaPath\` path or URL to OpenAPI/Protobuf/GraphQL schema
88
- Search for: openapi.json, swagger.yaml, *.proto, *.graphql
89
- Framework defaults: FastAPI /openapi.json | Express → /api-docs | Spring /v3/api-docs
90
- For locally-run services, use a localhost URL. For cloud/externally hosted services (e.g. Salesforce, Vercel, Cloudflare), use the actual deployment URL found in config or documentation.
91
- - \`api.baseUrl\` *(required)* the base URL where the service is reachable, e.g. "http://localhost:3000" or "https://api.example.com"
92
- Derive from docker-compose ports, app config, README, or environment variables.
93
- Use localhost for services run locally; use the actual deployment URL for cloud/externally hosted services.
94
- ⚠️ NEVER fabricate a URL only use URLs found in config files, README, or environment variables.
95
- - \`api.authType\` auth type: ${AUTH_TYPES_PROMPT_LIST}
96
-
97
- Detect by checking in order (language-agnostic — apply whichever signals match):
98
- 1. **Dependencies / packages** (package.json, requirements.txt, go.mod, Gemfile, composer.json, pom.xml, build.gradle):
99
- - \`jsonwebtoken\`, \`passport-jwt\`, \`@nestjs/jwt\`, \`jose\`, \`fastapi[security]\`, \`PyJWT\`, \`python-jose\`, \`github.com/golang-jwt/jwt\`, \`jjwt\`, \`spring-security-oauth2\` \`bearer\`
100
- ⚠️ **Spring exception**: if \`spring-security\` is present BUT the security config uses \`HttpSecurity.formLogin()\` or \`sessionManagement()\` without a \`JwtDecoder\` bean, the actual auth is session cookies → \`cookie\`. Check the \`SecurityConfig\` / \`WebSecurityConfigurerAdapter\` class before assigning \`bearer\` to any Spring service.
101
- - \`passport-http\`, Spring basic auth \`basic\`
102
- - \`passport-oauth2\`, \`openid-client\`, \`doorkeeper\`, \`keycloak-connect\`, \`spring-security-oauth2-resource-server\`, \`github.com/coreos/go-oidc\` → \`oauth\`
103
- - \`rest_framework\` with \`TokenAuthentication\`, \`djangorestframework-simplejwt\` (Token scheme) \`token\`
104
- - \`express-session\`, \`cookie-session\`, \`iron-session\`, \`next-auth\`, \`gorilla/sessions\`, \`laravel/session\` \`cookie\`
105
- - \`laravel/sanctum\`, \`laravel/passport\` (API routes) \`bearer\`; frontend web routes → \`cookie\`
106
- 2. **Environment variables** (.env, docker-compose, README):
107
- - \`JWT_SECRET\`, \`ACCESS_TOKEN_SECRET\`, \`FIREBASE_SECRET\`, \`SUPABASE_JWT_SECRET\` \`bearer\`
108
- - \`API_KEY\`, \`X_API_KEY\`, \`ADMIN_KEY\`, \`SERVICE_KEY\` (used as header value, not JWT signing) \`apiKey\`
109
- - \`CLIENT_ID\` + \`CLIENT_SECRET\`, \`OAUTH_CLIENT_*\`, \`OIDC_*\` \`oauth\`
110
- - \`SESSION_SECRET\`, \`COOKIE_SECRET\`, \`NEXTAUTH_SECRET\` \`cookie\`
111
- 3. **Source code / middleware patterns** (auth, middleware, or security config files):
112
- - Node/Express: \`req.headers.authorization\` split \`"Bearer"\` \`bearer\` | \`Authorization: Token\` \`token\` | custom header check \`apiKey\` | session/cookie middleware → \`cookie\`
113
- - FastAPI/Starlette: \`HTTPBearer\`, \`OAuth2PasswordBearer\`, \`Depends(get_current_user)\` \`bearer\` | \`SessionMiddleware\` → \`cookie\`
114
- - Django/DRF: \`TokenAuthentication\` \`token\` | \`JWTAuthentication\` \`bearer\` | \`SessionAuthentication\` \`cookie\`
115
- - Spring: \`JwtDecoder\` / \`JwtAuthenticationFilter\` \`bearer\` | \`HttpSecurity.formLogin()\` + sessions → \`cookie\`
116
- - Go: \`ParseWithClaims\` / \`GetHeader("Authorization")\` \`bearer\` | \`gorilla/sessions\` \`cookie\`
117
- - Rails: \`authenticate_or_request_with_http_token\` \`bearer\` | \`before_action :authenticate_user!\` (Devise) → \`cookie\`
118
- - Laravel: \`auth:sanctum\` (API) \`bearer\` | \`middleware("auth")\` web \`cookie\`
119
- - Rust/Axum: \`Authorization<Bearer>\` extraction → \`bearer\`
120
- 4. **Query-parameter auth** — some APIs pass credentials as a URL query param rather than a header (e.g. \`?key=<key>\`, \`?api_key=<key>\`, \`?access_token=<token>\`). Signals:
121
- - Source code reads credentials from \`req.query.key\`, \`request.query_params["api_key"]\`, \`@RequestParam("token")\`, etc.
122
- - API docs / README show auth examples like \`/endpoint?key=<your-key>\`
123
- 5. Fallback: frontend/UI service \`none\` | backend API with no header-based auth signals → \`bearer\`
124
- - \`api.authHeader\` the exact HTTP header name carrying the credential:
125
- - \`bearer\` / \`basic\` / \`oauth\` / \`token\`: always \`"Authorization"\` (inferred automatically, so you may omit it)
126
- - \`cookie\` / \`session\`: always \`"Cookie"\` (also inferred automatically)
127
- - \`apiKey\`: **required** set to the actual custom header name (e.g. \`"X-API-Key"\`, \`"X-Admin-Key"\`)
128
- - \`none\`: omit or \`""\`
129
- - \`api.authScheme\` *(optional)* the Authorization header prefix (the word before the token):
130
- - Standard types derive this automatically: \`bearer\` \`"Bearer"\`, \`token\` \`"Token"\`, \`basic\` \`"Basic"\`
131
- - **Set explicitly for custom/non-standard schemes** (e.g. Hawk uses \`"Hawk"\`, Digest uses \`"Digest"\`)
132
- - Example: \`authType: bearer\`, \`authScheme: "Hawk"\` produces \`Authorization: Hawk <token>\`
133
- - Omit for cookie/session/apiKey (they don't use Authorization header)
134
-
135
- **Runtime fields:**
136
- - \`runtimeDetails.runtime\` \`local\` | \`docker\` | \`k8s\`
137
- Detect per service:
138
- - Service listed in docker-compose.yml → \`"docker"\`
139
- - Service has only a Dockerfile (no compose entry) → \`"local"\` or \`"docker"\`
140
- - k8s manifests exist (charts/, k8s/, deploy/) → \`"k8s"\`
141
- ⚠️ A repo may have MIXED runtimes a backend in docker-compose.yml uses "docker" while a frontend run with pnpm/npm locally uses "local". Include ALL services regardless of runtime.
142
-
143
- - \`runtimeDetails.serverStartCommand\` command to start the service. MUST match runtime:
144
- - \`"local"\` → application command: "uvicorn main:app", "npm run dev", "java -jar app.jar"
145
- - \`"docker"\` Docker command: "docker compose up -d \<service-name\>" ← prefer service-scoped
146
- - \`"k8s"\` → k8s command: "kubectl apply -f deploy/", "helm install myrelease ."
147
- ⚠️ NEVER mix (e.g. "uvicorn …" with runtime "docker" will cause errors).
148
-
149
- - \`runtimeDetails.dockerNetwork\` Docker network name. ONLY set when runtime is \`"docker"\`. NEVER set for "local" or "k8s".
150
- - \`runtimeDetails.k8sNamespace\` Kubernetes namespace. ONLY set when runtime is \`"k8s"\`. NEVER set for "local" or "docker".
151
- - \`runtimeDetails.k8sContext\` Kubernetes context. ONLY set when runtime is \`"k8s"\`. NEVER set for "local" or "docker".
152
-
153
- ## Verification Steps
154
-
155
- Before calling \`skyramp_init_workspace\`, confirm all of the following:
156
- - ALWAYS SCAN REPO AND FIND SERVICES. A REPO SHOULD HAVE AT LEAST ONE SERVICE.
157
- - **CRITICAL**: ALL services are included — backend AND frontend. The workspace config is a complete registry of the entire repo, not just the service relevant to your current task. A fullstack or monorepo MUST have multiple services — if you found only one, re-scan every top-level directory before proceeding.
158
- - Services NOT in docker-compose.yml (e.g. a frontend run with pnpm/npm locally) MUST still be included with runtime "local".
159
- - Every service has \`api.baseUrl\` set to a valid, discoverable URL — localhost for local services, or the actual deployment URL for cloud/external services. Never fabricate a URL.
160
- - Every service with \`authType: apiKey\` has \`authHeader\` explicitly set to the actual custom header name (e.g. \`"X-API-Key"\`, \`"X-Admin-Key"\`). If you cannot find the header name in the source code, env vars, or README, do NOT use \`authType: apiKey\` — use \`authType: none\` and add a YAML comment explaining auth is unresolved.
161
- - \`framework\` matches \`language\` (python pytest/robot | typescript/javascript playwright | java → junit)
162
- - \`testDirectory\` matches the framework's config file (Playwright: \`testDir\` in playwright.config.ts | pytest: \`testpaths\` in pytest.ini/pyproject.toml | JUnit: test source dir in pom.xml/build.gradle). If no config file is found, use the common defaults: "tests/", "test/".
163
- - \`serverStartCommand\` matches \`runtime\`
164
- - For services in docker-compose.yml: runtime MUST be "docker" and command MUST be a docker command (e.g. "docker compose up -d <service-name>").
165
- - NEVER use application-level commands (uvicorn, npm, node, python, java, etc.) with runtime "docker".
166
- - \`dockerNetwork\` is set only when runtime is "docker"
167
- - \`k8sNamespace\` and \`k8sContext\` are set only when runtime is "k8s"
168
-
169
- Once verified, call \`skyramp_init_workspace\` with:
170
- - \`workspacePath\`: the repository root path
171
- - \`services\`: the array built above
172
- - \`scanToken\`: the token returned by the first call to \`skyramp_init_workspace\` (called with only workspacePath)
173
- - \`force\`: defaults to false — only set to true if the user explicitly asks to overwrite an existing \`.skyramp/workspace.yml\``;
70
+ 1. CLAUDE.md or AGENTS.md: if either file exists at the repo root, check it for dev/test/CI setup instructions including how to start services, required environment variables, and runtime configuration.
71
+ 2. Docker Compose files: scan for ALL compose files including docker-compose.yml, docker-compose.yaml, docker-compose.*.yml (such as docker-compose.testbot.yml or docker-compose.dev.yml), compose.yml, and compose.*.yaml in the repo root and subdirectories. Distinguish between application compose files and infrastructure-only compose files. Infrastructure compose files contain only supporting services like databases, Redis, Minio, mail servers, or message queues and do NOT run the application itself. Application compose files contain the actual application services that build from the repo source code (look for "build:" directives pointing to the repo, or services that map to discovered application directories). Only use application compose files for determining runtime and serverStartCommand. When a non-default compose file is found, the serverStartCommand must reference it explicitly (such as "docker compose -f docker-compose.testbot.yml up -d <service-name>"). Docker Compose ALWAYS prefixes the network name with the project name. If compose has "networks: { my-net: ... }", the actual network name is "<project-name>_my-net". If there is no explicit networks section, the default network is "<project-name>_default". The project name is the basename of the working directory where docker compose runs.
72
+ 3. Makefile: extract start and dev targets.
73
+ 4. Root package.json scripts: extract workspace-level commands.
74
+ </runtime_config>
75
+
76
+ ### Build the complete services array
77
+ <build_services>
78
+ Create one service entry per deployable unit. You MUST include every backend/API service (Python, Java, Go, Node.js) and every frontend service (React, Vue, Angular, Next.js). Set runtime fields from docker-compose.yml if present.
79
+ </build_services>
80
+
81
+ ### Basic fields
82
+ <basic_fields>
83
+ 1. serviceName (required): A unique identifier for the service. Use the format <repoName>-<serviceName>-<frontend or backend> for consistency. For example: directus-api-backend, directus-app-frontend. For single-service repos where the service directory is the root, omit the directory portion such as onlineBoutique-frontend.
84
+ 2. language (required): One of python, typescript, javascript, or java. Detect from package.json (typescript or javascript), requirements.txt or pyproject.toml (python), or pom.xml or build.gradle (java).
85
+ 3. framework (required): One of playwright, pytest, robot, or junit. Detect from pytest.ini, playwright.config, jest.config, or junit in pom.xml. The framework MUST match the language: python uses pytest or robot, typescript or javascript uses playwright, and java uses junit.
86
+ 4. testDirectory (required): A stable path relative to the repo root where generated tests for this service will be placed. For each service, use the test directory configured by that service's test framework when one is discoverable:
87
+ - Playwright: Read playwright.config.ts (or .js/.mjs) and extract the testDir value.
88
+ - pytest: Read pytest.ini, pyproject.toml [tool.pytest.ini_options], or setup.cfg [tool:pytest] for testpaths.
89
+ - JUnit: Usually src/test/java. Check pom.xml or build.gradle for custom test source directories.
90
+ If no framework-configured test directory is available, use the Skyramp deterministic fallback:
91
+ - Single service: set testDirectory to tests/skyramp.
92
+ - Multiple services or monorepos: set testDirectory to tests/skyramp/<serviceDirName>, where <serviceDirName> is the service directory name with path separators and whitespace replaced by hyphens.
93
+ Framework config takes precedence. Use the Skyramp deterministic fallback only when no framework-configured test directory is available.
94
+ </basic_fields>
95
+
96
+ ### API fields
97
+ <api_fields>
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
+ 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
+ 3. api.authType (required): The authentication type. Valid values are: ${AUTH_TYPES_PROMPT_LIST}
101
+ Detect by checking in the following order (language-agnostic, apply whichever signals match):
102
+ a. Dependencies and packages (package.json, requirements.txt, go.mod, Gemfile, composer.json, pom.xml, build.gradle):
103
+ - jsonwebtoken, passport-jwt, @nestjs/jwt, jose, fastapi[security], PyJWT, python-jose, github.com/golang-jwt/jwt, jjwt, or spring-security-oauth2 indicates bearer. Spring exception: if spring-security is present but the security config uses HttpSecurity.formLogin() or sessionManagement() without a JwtDecoder bean, the actual auth is session cookies so use cookie. Check the SecurityConfig or WebSecurityConfigurerAdapter class before assigning bearer to any Spring service.
104
+ - passport-http or Spring basic auth indicates basic.
105
+ - passport-oauth2, openid-client, doorkeeper, keycloak-connect, spring-security-oauth2-resource-server, or github.com/coreos/go-oidc indicates oauth.
106
+ - rest_framework with TokenAuthentication or djangorestframework-simplejwt (Token scheme) indicates token.
107
+ - express-session, cookie-session, iron-session, next-auth, gorilla/sessions, or laravel/session indicates cookie.
108
+ - laravel/sanctum or laravel/passport on API routes indicates bearer; on frontend web routes indicates cookie.
109
+ b. Environment variables (.env, docker-compose, README):
110
+ - JWT_SECRET, ACCESS_TOKEN_SECRET, FIREBASE_SECRET, or SUPABASE_JWT_SECRET indicates bearer.
111
+ - API_KEY, X_API_KEY, ADMIN_KEY, or SERVICE_KEY (used as header value, not JWT signing) indicates apiKey.
112
+ - CLIENT_ID paired with CLIENT_SECRET, OAUTH_CLIENT_*, or OIDC_* indicates oauth.
113
+ - SESSION_SECRET, COOKIE_SECRET, or NEXTAUTH_SECRET indicates cookie.
114
+ c. Source code and middleware patterns (auth, middleware, or security config files):
115
+ - Node/Express: req.headers.authorization split "Bearer" indicates bearer. "Authorization: Token" indicates token. Custom header check indicates apiKey. Session or cookie middleware indicates cookie.
116
+ - FastAPI/Starlette: HTTPBearer, OAuth2PasswordBearer, or Depends(get_current_user) indicates bearer. SessionMiddleware indicates cookie.
117
+ - Django/DRF: TokenAuthentication indicates token. JWTAuthentication indicates bearer. SessionAuthentication indicates cookie.
118
+ - Spring: JwtDecoder or JwtAuthenticationFilter indicates bearer. HttpSecurity.formLogin() with sessions indicates cookie.
119
+ - Go: ParseWithClaims or GetHeader("Authorization") indicates bearer. gorilla/sessions indicates cookie.
120
+ - Rails: authenticate_or_request_with_http_token indicates bearer. before_action :authenticate_user! (Devise) indicates cookie.
121
+ - Laravel: auth:sanctum on API indicates bearer. middleware("auth") on web indicates cookie.
122
+ - Rust/Axum: Authorization<Bearer> extraction indicates bearer.
123
+ d. Query-parameter auth: Some APIs pass credentials as a URL query param rather than a header (such as ?key=<key>, ?api_key=<key>, or ?access_token=<token>). Signals include source code reading credentials from req.query.key, request.query_params["api_key"], or @RequestParam("token"), and API docs or README showing auth examples like /endpoint?key=<your-key>.
124
+ e. Fallback: For frontend or UI services, use none. For backend APIs with no header-based auth signals, use bearer.
125
+ 4. api.authHeader: The exact HTTP header name carrying the credential.
126
+ - For bearer, basic, oauth, or token: always "Authorization" (inferred automatically, so you may omit it).
127
+ - For cookie or session: always "Cookie" (also inferred automatically).
128
+ - For apiKey: this is required. Set to the actual custom header name such as "X-API-Key" or "X-Admin-Key".
129
+ - For none: set authHeader to "".
130
+ 5. api.authScheme: The Authorization header prefix (the word before the token). Standard types derive this automatically: bearer uses "Bearer", token uses "Token", and basic uses "Basic". Set explicitly only for custom or non-standard schemes (for example, Hawk uses "Hawk" and Digest uses "Digest"). Omit for cookie, session, or apiKey since they do not use the Authorization header.
131
+ </api_fields>
132
+
133
+ ### Runtime fields
134
+ <runtime_fields>
135
+ 1. runtimeDetails.runtime (required): One of local, docker, or k8s. Detect per service:
136
+ - If the service is listed in an application docker compose file (one that builds or runs the application, not infrastructure-only files containing only databases, caches, or queues) or has a Dockerfile as its intended run method, use "docker".
137
+ - If k8s manifests exist (charts/, k8s/, deploy/), use "k8s".
138
+ - If the service has no Docker or k8s configuration and is run directly via a language runtime (npm, python, java, etc.), use "local".
139
+ A repo may have MIXED runtimes. A backend in docker-compose.yml uses "docker" while a frontend run with pnpm or npm locally uses "local". Include ALL services regardless of runtime.
140
+ If the frontend is bundled inside the API Docker container (no separate frontend service in the compose file), both frontend and backend services should use runtime "docker", share the same baseUrl (the container's exposed URL), and use the same serverStartCommand since they run in the same container.
141
+ 2. runtimeDetails.serverStartCommand: The command to start or deploy the service. It MUST match the runtime:
142
+ - For "docker" runtime: Use a Docker command such as "docker compose up -d <service-name>" for the default docker-compose.yml, or "docker compose -f <compose-file> up -d <service-name>" when using a non-default compose file. This is always derivable from the application docker compose file and service name.
143
+ - For "k8s" runtime: Use a deploy command such as "kubectl apply -f deploy/", "helm install myrelease .", or "skaffold run". This is always derivable from the manifests or charts present in the repo.
144
+ - For "local" runtime: Use an application command such as "uvicorn main:app", "npm run dev", or "java -jar app.jar". Derive from Makefile, package.json scripts, or README. If no start command is discoverable, omit this field entirely.
145
+ NEVER mix runtime types with incompatible commands (for example, using "uvicorn" with runtime "docker" will cause errors). For "local" runtime, NEVER fabricate a command. Only use commands found in Makefile, package.json scripts, or README.
146
+ 3. runtimeDetails.dockerNetwork: Docker network name. ONLY set when runtime is "docker". NEVER set for "local" or "k8s".
147
+ 4. runtimeDetails.k8sNamespace: Kubernetes namespace. ONLY set when runtime is "k8s". NEVER set for "local" or "docker".
148
+ 5. runtimeDetails.k8sContext: Kubernetes context. ONLY set when runtime is "k8s". NEVER set for "local" or "docker".
149
+ </runtime_fields>
150
+
151
+ ### Verification
152
+ <verification>
153
+ Before calling skyramp_init_workspace, confirm all of the following:
154
+ 1. Always scan the repo and find services. A repo should have at least one service.
155
+ 2. ALL services are included, both backend and frontend. The workspace config is a complete registry of the entire repo, not just the service relevant to your current task. A fullstack or monorepo MUST have multiple services. If you found only one, re-scan every top-level directory before proceeding.
156
+ 3. Services NOT in docker-compose.yml (such as a frontend run with pnpm or npm locally) MUST still be included with runtime "local".
157
+ 4. Every service has api.baseUrl set to a valid, discoverable URL. Use localhost for local services or the actual deployment URL for cloud or external services. Never fabricate a URL.
158
+ 5. Every service with authType apiKey has authHeader explicitly set to the actual custom header name (such as "X-API-Key" or "X-Admin-Key"). If you cannot find the header name in the source code, env vars, or README, do NOT use authType apiKey. Use authType none instead and add a YAML comment explaining auth is unresolved.
159
+ 6. framework matches language (python uses pytest or robot, typescript or javascript uses playwright, java uses junit).
160
+ 7. testDirectory follows the stable resolution rules above: framework config file when present (Playwright testDir in playwright.config.ts, pytest testpaths in pytest.ini or pyproject.toml, JUnit test source dir in pom.xml or build.gradle); otherwise the deterministic default (tests/skyramp for a single service, tests/skyramp/<serviceDirName> for multiple services).
161
+ 8. If serverStartCommand is provided, it matches the runtime.
162
+ 9. For services in docker-compose.yml: runtime MUST be "docker" and the command MUST be a docker command such as "docker compose up -d <service-name>". Always include it since it is derivable from the service name.
163
+ 10. NEVER use application-level commands (uvicorn, npm, node, python, java, etc.) with runtime "docker".
164
+ 11. For "local" runtime: if no start command is discoverable from Makefile, package.json scripts, or README, omit serverStartCommand rather than guessing.
165
+ 12. dockerNetwork is set only when runtime is "docker".
166
+ 13. k8sNamespace and k8sContext are set only when runtime is "k8s".
167
+ 14. If force-recreating a workspace.yml due to a schema validation error, correct the schema error but keep any valid system-under-test setup (serviceName, baseUrl, auth config, runtime details) as it is.
168
+ </verification>
169
+
170
+ Once verified, call skyramp_init_workspace with:
171
+ - workspacePath: the repository root path
172
+ - services: the array built above
173
+ - scanToken: the token returned by the first call to skyramp_init_scan
174
+ - force: defaults to false. Set to true only if the user explicitly asks to overwrite an existing .skyramp/workspace.yml, or if the existing file must be regenerated due to a validation failure such as a schema mismatch, unknown fields, or a parse error.`;
@@ -1,3 +1,4 @@
1
+ import { isTestbotEnabled } from "../utils/featureFlags.js";
1
2
  /**
2
3
  * Skyramp personas injected into tool descriptions and prompts.
3
4
  *
@@ -19,5 +20,5 @@ export const SKYRAMP_QA_PERSONA = `You are acting as a Skyramp QA Automation Eng
19
20
  * avoid duplicating it in every tool description.
20
21
  */
21
22
  export function getPersonaPrefix() {
22
- return process.env.SKYRAMP_FEATURE_TESTBOT ? '' : `${SKYRAMP_QA_PERSONA}\n\n`;
23
+ return isTestbotEnabled() ? '' : `${SKYRAMP_QA_PERSONA}\n\n`;
23
24
  }
@@ -74,8 +74,9 @@ ${candidateFilesSection}`;
74
74
  if (inlineMode) {
75
75
  // Testbot inline mode: all maintenance logic lives here so the testbot
76
76
  // prompt only orchestrates steps without duplicating rules.
77
+ // No persona statement here — the outer testbot prompt already establishes
78
+ // the agent's context; a nested identity statement causes role confusion.
77
79
  return `<drift_analysis_rules>
78
- You are acting as a Skyramp Integration Architect.
79
80
  For this maintenance step: assess each existing test against the diff returned by \`skyramp_analyze_changes\` and apply the correct action (IGNORE, UPDATE, REGENERATE, or DELETE) directly — no separate analysis step.
80
81
 
81
82
  ${buildActionDecisionMatrix()}
@@ -1,4 +1,32 @@
1
1
  import { buildDriftAnalysisPrompt } from "./drift-analysis-prompt.js";
2
+ describe("buildDriftAnalysisPrompt - inline mode (no stateFile)", () => {
3
+ function inlinePrompt() {
4
+ return buildDriftAnalysisPrompt({
5
+ existingTests: [],
6
+ scannedEndpoints: [],
7
+ repositoryPath: "/repo",
8
+ // stateFile omitted → inline mode
9
+ });
10
+ }
11
+ it("wraps inline rules in drift_analysis_rules XML tags", () => {
12
+ const prompt = inlinePrompt();
13
+ expect(prompt).toContain("<drift_analysis_rules>");
14
+ expect(prompt).toContain("</drift_analysis_rules>");
15
+ });
16
+ it("does not contain the persona statement", () => {
17
+ const prompt = inlinePrompt();
18
+ expect(prompt).not.toContain("You are acting as a Skyramp Integration Architect");
19
+ });
20
+ it("does not contain the standalone Test Health Analysis header", () => {
21
+ const prompt = inlinePrompt();
22
+ expect(prompt).not.toContain("# Test Health Analysis");
23
+ });
24
+ it("does not contain the skyramp_actions CTA (that belongs to standalone mode)", () => {
25
+ const prompt = inlinePrompt();
26
+ // Inline mode final step directs applying changes directly, not calling skyramp_actions
27
+ expect(prompt).not.toContain("call `skyramp_actions`");
28
+ });
29
+ });
2
30
  describe("buildDriftAnalysisPrompt - scanned endpoints rendering", () => {
3
31
  // Reproduces the [object Object] bug: skeletonEndpoints from analyzeChangesTool
4
32
  // stores methods as objects { method: string, ... }, not plain strings.
@@ -12,12 +12,22 @@ const FRONTEND_EXT = /\.(tsx?|jsx?|vue|svelte|css|scss|less|html|svg)$/i;
12
12
  * Returned as an empty string when no router context is available.
13
13
  */
14
14
  function buildPathResolutionTableStep(p) {
15
- if (!p.routerMountContext.length || p.wsSchemaPath)
16
- return "";
17
- return `### Step 1.5: Build path resolution table
18
- The **Routing entry-point files** section above lists the files to read.
19
-
20
- **Read each of those files** and trace every router mount call to understand nesting the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
15
+ // Case A: spec was fetched successfully — instruct LLM to validate paths against it
16
+ if (p.wsSchemaPath && p.specFetchSucceeded) {
17
+ return `### Step 1.5: Validate all endpoint paths against the OpenAPI spec
18
+ Fetch \`${p.wsSchemaPath}\` and extract all keys from \`spec.paths\`.
19
+ **Before placing any path in a tool call**, confirm it exists in that list.
20
+ If a path is NOT in the spec **and it did not come from the PR diff**, find the correct spelling by matching resource name do NOT use it unverified.
21
+ Paths the PR explicitly added or modified may not yet appear in the spec (spec lag) — treat those as valid.
22
+ `;
23
+ }
24
+ // Case B: no spec (or spec unreachable) but router mount context available
25
+ if (p.routerMountContext.length) {
26
+ const hasInlined = (p.routerFileContents?.length ?? 0) > 0;
27
+ return `### Step 1.5: Build path resolution table
28
+ ${hasInlined
29
+ ? "The **Routing entry-point files** section above contains the inlined file contents — use them directly to trace every router mount call"
30
+ : "The **Routing entry-point files** section above lists the files to read.\n\n**Read each of those files** and trace every router mount call"} to understand nesting — the pattern varies by framework but the structure is universal: a parent attaches a child router with an optional extra prefix segment. If a prefix is a variable (e.g. \`prefix=api_prefix\`), resolve the variable's value by reading the assignment or the config/settings file it comes from. Examples of what to look for (non-exhaustive):
21
31
  - Python (FastAPI/Flask): \`parent.include_router(child, prefix="...")\`, \`app.register_blueprint(...)\`
22
32
  - JS/TS (Express/Fastify/Hapi): \`app.use('/path', childRouter)\`, \`router.use('/path', sub)\`
23
33
  - NestJS: \`@Module({ imports: [FeatureModule] })\` — trace the module import chain; each \`@Controller('prefix')\` contributes a segment
@@ -33,6 +43,20 @@ Chain all segments from the app root down through every intermediate mount to ea
33
43
 
34
44
  **This table is authoritative.** Before placing any URL in a tool call, look up the source file. If the pre-built catalog shows a different path, use the table value.
35
45
 
46
+ `;
47
+ }
48
+ // Case C: no spec AND no router context — source-verify fallback
49
+ // Note: also fires when a spec was configured (wsSchemaPath set) but could not be
50
+ // fetched at analysis time (specFetchSucceeded = false). When that happens the LLM
51
+ // should know a spec was expected so it can be extra-skeptical about path correctness.
52
+ const specFailedNote = p.wsSchemaPath && !p.specFetchSucceeded
53
+ ? `\n> ⚠️ A spec was configured (\`${p.wsSchemaPath}\`) but could not be loaded at analysis time — treat all paths as unverified until confirmed against source.`
54
+ : "";
55
+ return `### Step 1.5: Verify endpoint paths from source files
56
+ The endpoint catalog below was produced by static regex analysis and is **unverified**.
57
+ Before using any path in a tool call, read the route definition file identified in the "Source" column and confirm the path string exactly.
58
+ Pay special attention to mount prefixes — a router at \`/api/v1\` + route \`/version\` → path is \`/api/v1/version\`, not \`/api/server-version\`.
59
+ ${specFailedNote}
36
60
  `;
37
61
  }
38
62
  // Inline note added to any step where the LLM reads Java source files. Java Spring
@@ -125,6 +149,33 @@ No diff was available — read the changed source files listed above directly to
125
149
  ${diffHasJavaFiles ? JAVA_SPRING_NOTE : ""}
126
150
  For each endpoint found: note the HTTP method, full path, and source file.
127
151
  Also compare against the endpoint catalog to identify any endpoints that appear in the catalog but are no longer present in the source files — these are removed endpoints.`;
152
+ // Step 2.3: Caller-tracing instruction — only emitted when the PR touches backend code
153
+ // files that contain no route annotations (utilities, helpers, services). Tells the LLM
154
+ // to search for callers of the changed functions to find the actual HTTP surface
155
+ // rather than falling back to the proximity-scanned CRUD endpoints. (Bug 5 fix)
156
+ //
157
+ // We filter out:
158
+ // - Frontend component files (.jsx/.tsx/.vue/.svelte) — UI changes have no callers
159
+ // in the HTTP graph; emitting this block for them produces irrelevant instructions.
160
+ // - Non-code files (docs, config, assets, lockfiles) — they have no "changed symbols"
161
+ // to trace and listing them as bullets is misleading.
162
+ const BACKEND_CODE_EXT = /\.(ts|js|mjs|cjs|py|java|kt|rb|go|cs|php|rs|scala|swift|c|cpp|h|hpp)$/i;
163
+ const traceableUnmatched = (p.unmatchedFiles ?? []).filter(f => BACKEND_CODE_EXT.test(f));
164
+ const callerTracingStep = isDiffScope && !isUIOnly && traceableUnmatched.length > 0
165
+ ? `
166
+ ### Step 2.3: Trace callers of changed non-route files
167
+ The following changed files contain **no HTTP endpoint registrations** (no route annotations, controller mappings, or handler decorators). Their changes will only be tested if you find and target the HTTP endpoints that *call* them:
168
+
169
+ ${traceableUnmatched.map(f => `- \`${f}\``).join("\n")}
170
+
171
+ For each file above:
172
+ 1. **Find the changed symbols** — read the diff (or the file) to identify which functions, methods, or classes were modified.
173
+ 2. **Search for callers** — look for import statements and call sites of those symbols across service, handler, and controller files. Use fully qualified names (e.g. \`DataUtils.addFileData\`, not just \`addFileData\`) to avoid false matches in large monorepos.
174
+ 3. **Trace to HTTP registration** — from each caller, follow up to the route/controller registration (Spring \`@PostMapping\`, Express \`router.post\`, FastAPI \`@router.post\`, etc.) to identify the endpoint(s) that invoke the changed logic.
175
+ 4. **Augment the endpoint list** from Step 2 with these execution-path endpoints.
176
+ 5. If an execution or processing endpoint is found (path ending in \`/execute\`, \`/run\`, \`/trigger\`, \`/process\`, \`/invoke\`, or similar), it **MUST** be included in the test candidates. Do not produce coverage consisting solely of CRUD endpoints when an execution-path endpoint was found — CRUD tests may still be included but must not be the only coverage.
177
+ `
178
+ : "";
128
179
  const criticalPatternStep = `### Step 2.5: Identify critical patterns for test categorization
129
180
  Look for these patterns in model/schema/handler files to inform test recommendations:
130
181
  - **Unique constraints**: \`@unique\`, \`unique: true\`, unique indexes, \`.refine()\` uniqueness checks, \`UNIQUE\` in SQL migrations
@@ -168,22 +219,29 @@ Call \`skyramp_recommend_tests\` with:
168
219
  ### Step 1: Read the changed files and diff
169
220
  ${changedFiles}${diffFileRef}
170
221
  ${buildPathResolutionTableStep(p)}${step2}
171
-
222
+ ${callerTracingStep}
172
223
  ${criticalPatternStep}
173
224
 
174
225
  ${step3Content}`;
175
226
  }
176
227
  export function buildAnalysisOutputText(p) {
177
228
  const isDiffScope = p.analysisScope === AnalysisScope.CurrentBranchDiff;
178
- // Router mounting context is unique to this prompt (not in recommendationPrompt).
179
- // Branch diff, endpoint catalog, auth config, and OpenAPI spec are omitted here
180
- // because they are already present in the recommendation prompt that is
181
- // concatenated in the same tool response.
182
- const routerSection = !p.wsSchemaPath && p.routerMountContext.length
229
+ // Router mounting context is unique to this prompt; shown whenever mount context
230
+ // is available, regardless of whether a spec is configured.
231
+ const routerSection = p.routerMountContext.length
183
232
  ? `
184
233
  ## Routing entry-point files
185
- Read these in Step 1.5 to trace the full router/module hierarchy:
186
- ${p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
234
+ ${p.routerFileContents?.length
235
+ ? p.routerFileContents.map(({ file, content }) => `### \`${file}\`\n\`\`\`\n${content}\n\`\`\``)
236
+ .join("\n\n") + (p.routerMountContext.length > (p.routerFileContents?.length ?? 0)
237
+ ? `\n\nAdditional files (too large to inline — read manually if needed):\n` +
238
+ p.routerMountContext
239
+ .filter(f => !(p.routerFileContents ?? []).some(r => r.file === f))
240
+ .map(f => `- \`${f}\``)
241
+ .join("\n")
242
+ : "")
243
+ : `Read these in Step 1.5 to trace the full router/module hierarchy:\n` +
244
+ p.routerMountContext.map(f => `- \`${f}\``).join("\n")}`
187
245
  : "";
188
246
  const enrichment = buildEnrichmentInstructions(p);
189
247
  return `# Repository Analysis