@skyramp/mcp 0.0.63-rc.5 → 0.0.63-rc.6
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.
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +15 -4
- package/build/prompts/test-recommendation/recommendationSections.js +50 -5
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +35 -6
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +1 -0
- package/build/prompts/testbot/testbot-prompts.js +2 -1
- package/build/services/ScenarioGenerationService.js +24 -1
- package/build/services/TestExecutionService.js +1 -1
- package/build/tools/executeSkyrampTestTool.js +1 -1
- package/build/tools/generate-tests/generateScenarioRestTool.js +33 -2
- package/build/tools/test-management/analyzeChangesTool.js +6 -1
- package/build/tools/test-management/executeTestsTool.js +1 -1
- package/build/tools/workspace/initializeWorkspaceTool.js +3 -3
- package/build/types/RepositoryAnalysis.js +1 -0
- package/build/utils/trace-parser.js +32 -11
- package/build/utils/workspaceAuth.js +8 -8
- package/build/utils/workspaceAuth.test.js +31 -13
- package/package.json +2 -2
|
@@ -14,9 +14,14 @@ Read \`package.json\` / \`requirements.txt\`, \`docker-compose.yml\`, route/cont
|
|
|
14
14
|
and model/schema files (Zod schemas, Pydantic models, TypeScript interfaces, DTOs)
|
|
15
15
|
to understand the tech stack, endpoint shapes, auth mechanisms, and request/response schemas.
|
|
16
16
|
|
|
17
|
-
### Step 2: Identify resource relationships
|
|
17
|
+
### Step 2: Identify resource relationships and parameter locations
|
|
18
18
|
Map how endpoints relate to each other — which POST creates resources consumed by other endpoints?
|
|
19
19
|
**Resolve nested/sub-router paths** from the Router Mounting section above.
|
|
20
|
+
**CRITICAL — Distinguish query params vs request body:** For each endpoint, determine whether
|
|
21
|
+
parameters are sent as URL query params (typical for GET search/filter/list) or request body
|
|
22
|
+
(typical for POST/PUT/PATCH). Look at FastAPI \`Query()\` annotations, Express \`req.query\` usage,
|
|
23
|
+
Spring \`@RequestParam\`, Flask \`request.args\`, etc. Populate \`queryParams\` in interactions
|
|
24
|
+
for GET endpoints that accept search/filter/pagination parameters.
|
|
20
25
|
|
|
21
26
|
${nextStep}`;
|
|
22
27
|
}
|
|
@@ -28,7 +33,11 @@ ${nextStep}`;
|
|
|
28
33
|
Read handler code for the changed endpoints and their model/schema files (Zod schemas,
|
|
29
34
|
Pydantic models, DTOs) to understand request/response shapes. Find related endpoints via
|
|
30
35
|
imports, shared models, adjacent route files. Resolve nested/sub-router paths from Router
|
|
31
|
-
Mounting context
|
|
36
|
+
Mounting context.
|
|
37
|
+
**CRITICAL — Query params vs body:** For GET endpoints (especially search/filter/list),
|
|
38
|
+
identify which parameters are URL query params vs request body. Look at framework-specific
|
|
39
|
+
annotations (FastAPI \`Query()\`, Express \`req.query\`, Spring \`@RequestParam\`, etc.).
|
|
40
|
+
Pass these as \`queryParams\` (not \`requestBody\`) when generating scenarios.`
|
|
32
41
|
: isUIOnly
|
|
33
42
|
? `### Step 2: Identify consumed API endpoints
|
|
34
43
|
UI-only PR — read changed components to find API calls (fetch, axios, hooks).`
|
|
@@ -50,11 +59,13 @@ Call \`skyramp_analyze_test_health\` with \`stateFile: "${p.stateFile ?? p.sessi
|
|
|
50
59
|
: `### Step 3: Draft integration scenarios
|
|
51
60
|
Draft multi-step scenarios simulating realistic user workflows:
|
|
52
61
|
- **Cross-resource data flow**: Foreign key relationships, parent→child creation, verification
|
|
53
|
-
- **Search/filter verification**: Create data, search
|
|
62
|
+
- **Search/filter verification**: Create data, search for it using \`queryParams\`, verify results
|
|
54
63
|
- **Negative/error paths**: Invalid references → appropriate errors
|
|
55
64
|
- **UI user journeys**: Concrete browser steps for frontend changes
|
|
56
65
|
|
|
57
|
-
**Quality:** Realistic request bodies
|
|
66
|
+
**Quality:** Realistic request bodies for POST/PUT/PATCH, \`queryParams\` for GET search/filter,
|
|
67
|
+
response data verification, actual field names for chaining.
|
|
68
|
+
**Parameter placement:** GET search/filter endpoints MUST use \`queryParams\`, not \`requestBody\`.
|
|
58
69
|
|
|
59
70
|
### Step 4: Call recommend tests
|
|
60
71
|
Call \`skyramp_recommend_tests\` with \`sessionId: "${p.sessionId}"\``;
|
|
@@ -20,6 +20,13 @@ export function getAuthSnippets(authHeaderValue) {
|
|
|
20
20
|
}
|
|
21
21
|
return { authSchemeSnippet: "", authTokenSnippet: "" };
|
|
22
22
|
}
|
|
23
|
+
export const PATH_PARAM_UUID_GUIDANCE = `**Path parameters in contract/fuzz tests:** for endpoints with path params (e.g. \`/coupons/{coupon_id}\`), ` +
|
|
24
|
+
`keep the placeholder in \`endpointURL\` — do NOT substitute it. ` +
|
|
25
|
+
`Pass the concrete value separately via \`pathParams\` (e.g. \`coupon_id=3fa85f64-5717-4562-b3fc-2c963f66afa6\`). ` +
|
|
26
|
+
`This lets the CLI derive a clean filename from the resource name (\`coupons_GET_...\`) instead of the UUID. ` +
|
|
27
|
+
`Prefer example values from the OpenAPI schema (\`example\`, \`x-example\`, or enum values). ` +
|
|
28
|
+
`If no example exists, use a realistic UUID with varied hex digits such as \`3fa85f64-5717-4562-b3fc-2c963f66afa6\`. ` +
|
|
29
|
+
`Never use all-same-digit patterns like \`00000000-0000-0000-0000-000000000000\` or \`22222222-2222-2222-2222-222222222222\` — these look like placeholders and are rarely valid resource IDs.`;
|
|
23
30
|
export function buildPrioritizationDimensions() {
|
|
24
31
|
return `## Prioritization Dimensions (evaluate each candidate test)
|
|
25
32
|
|
|
@@ -145,7 +152,27 @@ replace them with accurate ones.
|
|
|
145
152
|
|
|
146
153
|
**Available test types:**
|
|
147
154
|
- **Integration** — multi-endpoint workflows that chain data across resources
|
|
148
|
-
- **Contract** —
|
|
155
|
+
- **Contract** — validates API contracts between services. Two distinct modes — choose based on role:
|
|
156
|
+
- **Provider contract test** (\`providerMode: true\`): Recommend when this codebase IS the API provider/owner.
|
|
157
|
+
Use when: new endpoints are added, existing responses are modified, an OpenAPI spec exists to validate against,
|
|
158
|
+
or you need to verify the implementation still honors its contracts after a code change.
|
|
159
|
+
The test calls the real provider and asserts the response conforms to the spec.
|
|
160
|
+
- **Consumer contract test** (\`consumerMode: true\`): Recommend when this codebase CALLS another service's API.
|
|
161
|
+
Use when: the service makes outbound HTTP calls to downstream APIs, you need to verify outbound requests
|
|
162
|
+
conform to the downstream contract, or you want to catch consumer-side drift before it reaches the provider.
|
|
163
|
+
The test uses a request-aware mock as the provider — no live downstream service needed.
|
|
164
|
+
- **Both modes** (\`providerMode: true, consumerMode: true\`): Recommend for service boundaries where this
|
|
165
|
+
codebase is simultaneously an API owner (upstream) AND a client of another service (downstream).
|
|
166
|
+
- **Default (neither set)**: generates a standard contract test equivalent to both modes. Use only when
|
|
167
|
+
the role is unclear or as a fallback when no spec is available.
|
|
168
|
+
|
|
169
|
+
**Signal for consumer contract test:** Look for outbound HTTP client code (fetch, axios, httpx, requests, http.Client),
|
|
170
|
+
service client classes, or calls to external base URLs in the codebase. If an endpoint's implementation
|
|
171
|
+
makes downstream calls, that downstream boundary is a consumer contract test candidate.
|
|
172
|
+
|
|
173
|
+
**Signal for provider contract test:** Look for new or modified endpoint handlers, route changes, response
|
|
174
|
+
shape modifications, or the presence of an OpenAPI spec. If the diff adds/changes an endpoint this service owns,
|
|
175
|
+
that is a provider contract test candidate.
|
|
149
176
|
|
|
150
177
|
**Do NOT recommend Fuzz tests.** Fuzz testing is available as a manual tool but must not appear in automated recommendations.
|
|
151
178
|
- **E2E** — user journeys spanning frontend to backend (needs Playwright traces)
|
|
@@ -170,7 +197,11 @@ export function buildToolWorkflows(authHeaderValue, authTypeValue = "") {
|
|
|
170
197
|
let authGuidance;
|
|
171
198
|
let authParams;
|
|
172
199
|
if (noAuth) {
|
|
173
|
-
authGuidance =
|
|
200
|
+
authGuidance = `**Auth Verification Required:** The workspace config indicates no authentication, but you MUST verify this independently before omitting auth:
|
|
201
|
+
1. **OpenAPI spec** \u2192 check \`securitySchemes\` / \`securityDefinitions\` for \`type: http\`, \`type: apiKey\`, or \`type: oauth2\`
|
|
202
|
+
2. **Source code** \u2192 look for auth middleware (\`passport\`, \`jwt.verify\`, \`authMiddleware\`, \`@requires_auth\`, \`Depends(get_current_user)\`, \`@UseGuards\`), route guards, or token extraction logic
|
|
203
|
+
3. **Route definitions** \u2192 check if routes have auth decorators or middleware applied
|
|
204
|
+
If you find auth requirements, pass the appropriate \`authHeader\` (e.g., "Authorization") and \`authScheme\` (e.g., "Bearer") to EVERY tool call. Only pass \`authHeader: ""\` if you confirm the API is truly unauthenticated.`;
|
|
174
205
|
authParams = { authHeader: "" };
|
|
175
206
|
}
|
|
176
207
|
else if (isAuthorizationHeader) {
|
|
@@ -203,7 +234,7 @@ To skip auth for unauthenticated endpoints, pass \`authHeader: ""\`.`;
|
|
|
203
234
|
}
|
|
204
235
|
const authCallParams = serializeAuthCallParams(authParams);
|
|
205
236
|
const authHeaderLine = noAuth
|
|
206
|
-
? `**No Auth
|
|
237
|
+
? `**No Auth (from workspace config):** Workspace indicates no authentication. **Verify independently** — if you find auth in the OpenAPI spec or source code, override with the correct \`authHeader\` and \`authScheme\`.`
|
|
207
238
|
: `**Auth params:** \`${authCallParams}\` — pass to EVERY tool call below.`;
|
|
208
239
|
return `## How to Generate Tests — Tool Workflows
|
|
209
240
|
|
|
@@ -212,10 +243,14 @@ ${authGuidance}
|
|
|
212
243
|
|
|
213
244
|
**For multi-endpoint workflows (integration tests) — Scenario → Integration pipeline:**
|
|
214
245
|
1. Call \`skyramp_scenario_test_generation\` once per step: \`scenarioName\`, \`destination\`,
|
|
215
|
-
\`baseURL\`, \`method\`, \`path\`, \`requestBody\`, \`responseBody\`, \`${authCallParams}\`.
|
|
246
|
+
\`baseURL\`, \`method\`, \`path\`, \`requestBody\` OR \`queryParams\`, \`responseBody\`, \`${authCallParams}\`.
|
|
216
247
|
\`statusCode\` is optional — defaults: POST→201, DELETE→204, GET/PUT/PATCH→200. Only override for non-standard codes.
|
|
217
248
|
**OpenAPI spec is NOT required.** \`apiSchema\` is OPTIONAL — omit it if no spec exists.
|
|
218
|
-
|
|
249
|
+
**CRITICAL — Query params vs request body:**
|
|
250
|
+
- For **POST/PUT/PATCH**: use \`requestBody\` with realistic field values from source code schemas.
|
|
251
|
+
- For **GET/DELETE with search/filter/pagination**: use \`queryParams\` (JSON string, e.g., \`{"q": "bear", "limit": 10}\`).
|
|
252
|
+
NEVER put query parameters in \`requestBody\` for GET requests — GET request bodies are non-standard and may be ignored or rejected.
|
|
253
|
+
- For **GET by ID**: no \`requestBody\` or \`queryParams\` needed — the ID is in the path.
|
|
219
254
|
\`responseBody\` should match the actual API response shape from source code (including all fields
|
|
220
255
|
returned by the controller — e.g., \`id\`, \`ownerId\`, \`createdAt\`, included relations like \`collection\`, \`tags\`).
|
|
221
256
|
Wrap in \`{"response": ...}\` if the API uses an envelope pattern. If omitted, a synthetic response is generated.
|
|
@@ -233,6 +268,16 @@ ${authGuidance}
|
|
|
233
268
|
If an OpenAPI spec exists, ALSO pass \`apiSchema\` — it enables schema-aware validation
|
|
234
269
|
(contract tests verify response structure against the spec).
|
|
235
270
|
Without a spec, \`endpointURL\` alone is sufficient.
|
|
271
|
+
${PATH_PARAM_UUID_GUIDANCE}
|
|
272
|
+
|
|
273
|
+
**Contract test mode selection — set based on this service's role at the boundary:**
|
|
274
|
+
- \`providerMode: true\` — this service IS the API; validates the implementation matches the spec.
|
|
275
|
+
Use for new or modified endpoints this codebase owns, especially when an OpenAPI spec is present.
|
|
276
|
+
- \`consumerMode: true\` — this service CALLS another API; validates outbound requests conform to the downstream contract.
|
|
277
|
+
Use when the endpoint's implementation makes HTTP calls to external services (look for fetch/axios/httpx/http.Client/service clients).
|
|
278
|
+
A request-aware mock stands in for the real downstream service — no live dependency needed.
|
|
279
|
+
- Both — use when the service boundary is both a provider (owns an API) and a consumer (calls a downstream API).
|
|
280
|
+
- Neither (default) — use only when the role is ambiguous or no spec is available.
|
|
236
281
|
|
|
237
282
|
**For UI tests (no Playwright recording):**
|
|
238
283
|
1. \`skyramp_start_trace_collection\` (playwright: true)
|
|
@@ -108,9 +108,10 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
|
|
|
108
108
|
const detailBlocks = detailEndpoints
|
|
109
109
|
.flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => {
|
|
110
110
|
const reqBody = i.request.body ? `\n requestBody: ${JSON.stringify(i.request.body)}` : "";
|
|
111
|
+
const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
|
|
111
112
|
const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
|
|
112
113
|
const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
|
|
113
|
-
return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${resBody}${headers}`;
|
|
114
|
+
return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${qParams}${resBody}${headers}`;
|
|
114
115
|
})))
|
|
115
116
|
.join("\n");
|
|
116
117
|
interactionSection = `
|
|
@@ -118,7 +119,7 @@ Affected services: ${diffContext.affectedServices.join(", ") || "N/A"}
|
|
|
118
119
|
${summaryLines}
|
|
119
120
|
|
|
120
121
|
### Detailed (request/response bodies)
|
|
121
|
-
${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies.
|
|
122
|
+
${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
|
|
122
123
|
${detailBlocks}
|
|
123
124
|
`;
|
|
124
125
|
}
|
|
@@ -131,24 +132,51 @@ ${detailBlocks}
|
|
|
131
132
|
const scenarioBlocks = scenarios
|
|
132
133
|
.map((s) => {
|
|
133
134
|
const stepLines = s.steps.map((st) => ` ${st.order ?? ""}. **${st.method} ${st.path}** → ${st.expectedStatusCode ?? 200}: ${st.description || ""}`).join("\n");
|
|
134
|
-
const toolCalls = s.steps.map((st) =>
|
|
135
|
+
const toolCalls = s.steps.map((st) => {
|
|
136
|
+
const isBodyMethod = ["POST", "PUT", "PATCH"].includes(st.method);
|
|
137
|
+
const hasQueryParams = st.queryParams && Object.keys(st.queryParams).length > 0;
|
|
138
|
+
const isIdPath = /\{\w+\}$/.test(st.path);
|
|
139
|
+
let dataParam;
|
|
140
|
+
if (isBodyMethod) {
|
|
141
|
+
dataParam = `requestBody: <from source schemas for ${st.method} ${st.path}>`;
|
|
142
|
+
}
|
|
143
|
+
else if (hasQueryParams) {
|
|
144
|
+
dataParam = `queryParams: '${JSON.stringify(st.queryParams)}'`;
|
|
145
|
+
}
|
|
146
|
+
else if (isIdPath) {
|
|
147
|
+
dataParam = "";
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
dataParam = `queryParams: <derive from source code for ${st.method} ${st.path} as a JSON string of URL query params>`;
|
|
151
|
+
}
|
|
152
|
+
const dataSnippet = dataParam ? `, ${dataParam}` : "";
|
|
153
|
+
const authSnippet = authHeaderValue
|
|
154
|
+
? `, authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet}`
|
|
155
|
+
: `, authHeader: <determine from OpenAPI spec securitySchemes or source code auth middleware — use "" ONLY if confirmed unauthenticated>`;
|
|
156
|
+
return ` skyramp_scenario_test_generation({ scenarioName: "${s.scenarioName}", destination: "${s.scenarioName}", baseURL: "${baseUrl}", method: "${st.method}", path: "${st.path}", statusCode: ${st.expectedStatusCode ?? 200}${authSnippet}${dataSnippet} })`;
|
|
157
|
+
}).join("\n");
|
|
135
158
|
return (` ### ${s.scenarioName} (${s.category}, ${s.priority})\n` +
|
|
136
159
|
` ${s.description}\n` +
|
|
137
160
|
` **Steps:**\n${stepLines}\n` +
|
|
138
161
|
` **Chaining keys:** ${s.chainingKeys.join(", ") || "none"}\n` +
|
|
139
162
|
` **Tool calls:**\n${toolCalls}\n` +
|
|
140
|
-
` Then: skyramp_integration_test_generation({ scenarioFile: "scenario_${s.scenarioName}.json"
|
|
163
|
+
` Then: skyramp_integration_test_generation({ scenarioFile: "scenario_${s.scenarioName}.json"${authHeaderValue ? `, authHeader: "${authHeaderValue}"${authSchemeSnippet}${authTokenSnippet}` : `, authHeader: <same auth as scenario calls above>`} })`);
|
|
141
164
|
})
|
|
142
165
|
.join("\n\n");
|
|
166
|
+
const authWarning = !authHeaderValue
|
|
167
|
+
? `\n**WARNING — No auth configured.** Before generating scenarios without auth, verify the API does not require authentication by checking the OpenAPI spec \`securitySchemes\` and source code auth middleware. If auth is needed, include \`authHeader\` (and \`authScheme\` if applicable) in every \`skyramp_scenario_test_generation\` call.\n`
|
|
168
|
+
: "";
|
|
143
169
|
scenarioSection = `
|
|
144
170
|
## Drafted Scenarios
|
|
145
|
-
**Base URL:** \`${baseUrl}\` | **Auth:** \`${authHeaderValue}\`${authTypeValue ? ` | **Auth type:** \`${authTypeValue}\`` : ""}
|
|
171
|
+
**Base URL:** \`${baseUrl}\` | **Auth:** \`${authHeaderValue || "none (verify independently)"}\`${authTypeValue ? ` | **Auth type:** \`${authTypeValue}\`` : ""}${authWarning}
|
|
146
172
|
|
|
147
173
|
Only use scenarios where resources are ACTUALLY related in the codebase. Replace any
|
|
148
174
|
scenario that pairs unrelated resources with one reflecting real foreign key relationships.
|
|
149
175
|
|
|
150
176
|
**Quality bar:** Realistic request bodies, actual foreign keys for chaining, response data
|
|
151
177
|
verification (not just status codes), realistic test data (not "test product").
|
|
178
|
+
**Query vs Body:** For GET/DELETE requests, use \`queryParams\` (not \`requestBody\`) for search terms,
|
|
179
|
+
filters, pagination. Only POST/PUT/PATCH use \`requestBody\`.
|
|
152
180
|
**Path verification:** Cross-reference paths against Router Mounting context — use correct
|
|
153
181
|
nested paths. **Request bodies:** Replace placeholders with actual schemas from source code.
|
|
154
182
|
|
|
@@ -164,7 +192,8 @@ Draft at least 2-3 MEANINGFUL scenarios based on your codebase analysis:
|
|
|
164
192
|
2. **Search/filter + verify** — create data, search, verify results
|
|
165
193
|
3. **Error handling** — invalid cross-resource references → appropriate errors
|
|
166
194
|
|
|
167
|
-
Use base URL: \`${analysis.apiEndpoints.baseUrl}\` and auth: \`${authHeaderValue}\`${authTypeValue ? ` (auth type: \`${authTypeValue}\`)` : ""}.
|
|
195
|
+
Use base URL: \`${analysis.apiEndpoints.baseUrl}\` and auth: \`${authHeaderValue || "none — verify independently from OpenAPI spec and source code"}\`${authTypeValue ? ` (auth type: \`${authTypeValue}\`)` : ""}.
|
|
196
|
+
${!authHeaderValue ? "**Verify auth requirements** from OpenAPI spec and source code before omitting auth headers." : ""}
|
|
168
197
|
`;
|
|
169
198
|
}
|
|
170
199
|
}
|
|
@@ -242,6 +242,7 @@ describe("buildRecommendationPrompt — PR History section", () => {
|
|
|
242
242
|
expect(getOccurrences).toBe(1);
|
|
243
243
|
expect(recSection).toContain("contract — POST /api/items");
|
|
244
244
|
});
|
|
245
|
+
// Ensure de-duplicates across different scenarios independently
|
|
245
246
|
it("de-duplicates across different scenarios independently", () => {
|
|
246
247
|
const ctx = makePRContext({
|
|
247
248
|
previousRecommendations: [
|
|
@@ -2,7 +2,7 @@ import { ResourceTemplate, } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
-
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS } from "../test-recommendation/recommendationSections.js";
|
|
5
|
+
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PARAM_UUID_GUIDANCE } from "../test-recommendation/recommendationSections.js";
|
|
6
6
|
function getTestbotPrompt(prTitle, prDescription, diffFile, testDirectory, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, maxCritical = MAX_CRITICAL_TESTS, prNumber, userPrompt) {
|
|
7
7
|
const promptSection = userPrompt ? `## Follow-up Request via @skyramp-testbot
|
|
8
8
|
|
|
@@ -119,6 +119,7 @@ Generate a net-new test. Use a unique descriptive filename to avoid overwriting
|
|
|
119
119
|
For internal/microservice APIs: add \`providerMode: true\` to verify implementation matches the contract.
|
|
120
120
|
For client-facing APIs consumed by frontend: add \`consumerMode: true\`.
|
|
121
121
|
For critical service boundaries: pass both \`providerMode\` and \`consumerMode\`.
|
|
122
|
+
- ${PATH_PARAM_UUID_GUIDANCE}
|
|
122
123
|
- **E2E/UI**: only if relevant Playwright traces exist in \`${testDirectory}\`, repo root, or \`.skyramp/\`.
|
|
123
124
|
Without traces, move to \`additionalRecommendations\`. Do NOT suggest calling \`skyramp_ui_test_generation\`
|
|
124
125
|
or \`skyramp_e2e_test_generation\` — those require traces. Instead, tell the user to record Playwright
|
|
@@ -134,6 +134,29 @@ ${JSON.stringify(traceRequest, null, 2)}
|
|
|
134
134
|
const requestHeaders = {
|
|
135
135
|
"Content-Type": ["application/json"],
|
|
136
136
|
};
|
|
137
|
+
let queryParams = {};
|
|
138
|
+
if (params.queryParams) {
|
|
139
|
+
try {
|
|
140
|
+
const parsed = JSON.parse(params.queryParams);
|
|
141
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
142
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
143
|
+
queryParams[k] = Array.isArray(v)
|
|
144
|
+
? v.map(String)
|
|
145
|
+
: [String(v)];
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
logger.warning("queryParams JSON is not a plain object, ignoring", {
|
|
150
|
+
queryParams: params.queryParams,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
logger.warning("Could not parse queryParams, ignoring", {
|
|
156
|
+
queryParams: params.queryParams,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
137
160
|
const authHeaderName = params.authHeader ?? "";
|
|
138
161
|
if (authHeaderName) {
|
|
139
162
|
if (params.authToken) {
|
|
@@ -158,7 +181,7 @@ ${JSON.stringify(traceRequest, null, 2)}
|
|
|
158
181
|
ResponseHeaders: responseHeaders,
|
|
159
182
|
Method: method,
|
|
160
183
|
Path: basePath ? basePath + params.path : params.path,
|
|
161
|
-
QueryParams:
|
|
184
|
+
QueryParams: queryParams,
|
|
162
185
|
StatusCode: statusCode,
|
|
163
186
|
Port: port,
|
|
164
187
|
Timestamp: timestamp,
|
|
@@ -8,7 +8,7 @@ import { logger } from "../utils/logger.js";
|
|
|
8
8
|
import { buildContainerEnv } from "./containerEnv.js";
|
|
9
9
|
const DEFAULT_TIMEOUT = 300000; // 5 minutes
|
|
10
10
|
const MAX_CONCURRENT_EXECUTIONS = 5;
|
|
11
|
-
export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.
|
|
11
|
+
export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.14";
|
|
12
12
|
const DOCKER_PLATFORM = "linux/amd64";
|
|
13
13
|
const EXECUTION_PROGRESS_INTERVAL = 10000; // 10 seconds between progress updates during execution
|
|
14
14
|
// Temp file with valid empty JSON — used instead of /dev/null for .json config files
|
|
@@ -99,7 +99,7 @@ For detailed documentation visit: https://www.skyramp.dev/docs/quickstart`,
|
|
|
99
99
|
`Cannot determine SKYRAMP_TEST_BASE_URL — test file matches multiple services:`,
|
|
100
100
|
...candidates.map((c) => ` • ${c.serviceName}: ${c.baseUrl}`),
|
|
101
101
|
``,
|
|
102
|
-
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's
|
|
102
|
+
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
103
103
|
].join("\n"),
|
|
104
104
|
}],
|
|
105
105
|
isError: true,
|
|
@@ -2,6 +2,8 @@ import { z } from "zod";
|
|
|
2
2
|
import { ScenarioGenerationService } from "../../services/ScenarioGenerationService.js";
|
|
3
3
|
import { baseSchema, AUTH_PLACEHOLDER_TOKEN } from "../../types/TestTypes.js";
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
|
+
import { getWorkspaceAuthConfig, WorkspaceAuthType } from "../../utils/workspaceAuth.js";
|
|
6
|
+
import { logger } from "../../utils/logger.js";
|
|
5
7
|
const scenarioTestSchema = {
|
|
6
8
|
scenarioName: z
|
|
7
9
|
.string()
|
|
@@ -29,7 +31,16 @@ const scenarioTestSchema = {
|
|
|
29
31
|
requestBody: z
|
|
30
32
|
.string()
|
|
31
33
|
.optional()
|
|
32
|
-
.describe("JSON string of the request body parsed by AI from the scenario"
|
|
34
|
+
.describe("JSON string of the request body parsed by AI from the scenario. "
|
|
35
|
+
+ "IMPORTANT: For GET and DELETE requests, do NOT put query/filter/search parameters here — use queryParams instead. "
|
|
36
|
+
+ "requestBody should only contain data sent in the HTTP body (typically for POST, PUT, PATCH)."),
|
|
37
|
+
queryParams: z
|
|
38
|
+
.string()
|
|
39
|
+
.optional()
|
|
40
|
+
.describe("JSON string of URL query parameters (e.g., '{\"q\": \"bear\", \"limit\": 10}'). "
|
|
41
|
+
+ "Use this for GET request filters, search terms, pagination, sorting — any parameter that belongs in the URL query string. "
|
|
42
|
+
+ "CRITICAL: For search/filter/list endpoints (e.g., GET /products/search?q=bear&limit=10), parameters MUST go here, NOT in requestBody. "
|
|
43
|
+
+ "GET request bodies are non-standard and may be ignored or rejected by servers and frameworks, so always encode these parameters in the URL query string instead of the request body."),
|
|
33
44
|
responseBody: z
|
|
34
45
|
.string()
|
|
35
46
|
.optional()
|
|
@@ -91,7 +102,8 @@ Returns a single TraceRequest object with:
|
|
|
91
102
|
The AI should parse the natural language scenario and provide:
|
|
92
103
|
- HTTP method (POST, GET, PUT, DELETE)
|
|
93
104
|
- API path with CONCRETE ID values, not templates (e.g., /api/v1/products/70885, NOT /api/v1/products/{product_id})
|
|
94
|
-
- Request body (JSON string
|
|
105
|
+
- Request body (JSON string) for POST/PUT/PATCH requests
|
|
106
|
+
- Query parameters (JSON string) for GET search/filter/list requests — NEVER put query params in requestBody
|
|
95
107
|
- Response body (JSON string, if applicable)
|
|
96
108
|
- Status code (optional, defaults based on method)
|
|
97
109
|
- Entity details (name, price, quantity, ID as needed)
|
|
@@ -112,6 +124,25 @@ When generating an integration test using the scenario file created by this tool
|
|
|
112
124
|
Passing both scenarioFile and apiSchema/endpointURL will cause the test generation to fail.`,
|
|
113
125
|
inputSchema: scenarioTestSchema,
|
|
114
126
|
}, async (params) => {
|
|
127
|
+
if (!params.authHeader) {
|
|
128
|
+
try {
|
|
129
|
+
const repoPath = params.outputDir || process.cwd();
|
|
130
|
+
const wsAuth = await getWorkspaceAuthConfig(repoPath);
|
|
131
|
+
if (wsAuth.authHeader && wsAuth.authType !== WorkspaceAuthType.None) {
|
|
132
|
+
logger.info("Auth header was empty — resolved from workspace config", {
|
|
133
|
+
authHeader: wsAuth.authHeader,
|
|
134
|
+
authType: wsAuth.authType,
|
|
135
|
+
});
|
|
136
|
+
params.authHeader = wsAuth.authHeader;
|
|
137
|
+
if (/^authorization$/i.test(wsAuth.authHeader) && wsAuth.authType) {
|
|
138
|
+
params.authScheme = params.authScheme || wsAuth.authType;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
logger.warning("Could not resolve auth from workspace config");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
115
146
|
const service = new ScenarioGenerationService();
|
|
116
147
|
const result = await service.parseScenario(params);
|
|
117
148
|
AnalyticsService.pushMCPToolEvent(TOOL_NAME, result, params).catch(() => {
|
|
@@ -463,10 +463,15 @@ to produce a unified state file for the test health workflow.
|
|
|
463
463
|
if (methodObj) {
|
|
464
464
|
const alreadyHasStatus = methodObj.interactions.some((i) => i.response.statusCode === entry.statusCode);
|
|
465
465
|
if (!alreadyHasStatus) {
|
|
466
|
+
const traceRequest = {};
|
|
467
|
+
if (entry.requestBody)
|
|
468
|
+
traceRequest.body = entry.requestBody;
|
|
469
|
+
if (entry.queryParams)
|
|
470
|
+
traceRequest.queryParams = entry.queryParams;
|
|
466
471
|
methodObj.interactions.push({
|
|
467
472
|
description: `${entry.method} ${entry.path} \u2192 ${entry.statusCode} (from trace)`,
|
|
468
473
|
type: "success",
|
|
469
|
-
request:
|
|
474
|
+
request: Object.keys(traceRequest).length > 0 ? traceRequest : {},
|
|
470
475
|
response: {
|
|
471
476
|
statusCode: entry.statusCode,
|
|
472
477
|
description: `Observed in trace (${traceResult.format})`,
|
|
@@ -140,7 +140,7 @@ back into the state file for use by \`skyramp_actions\`.
|
|
|
140
140
|
`Cannot determine SKYRAMP_TEST_BASE_URL — one or more test files match multiple services:`,
|
|
141
141
|
...lines,
|
|
142
142
|
``,
|
|
143
|
-
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's
|
|
143
|
+
`Re-invoke with SKYRAMP_TEST_BASE_URL set to the correct service URL, or make each service's testDirectory unique in .skyramp/workspace.yml.`,
|
|
144
144
|
].join("\n"),
|
|
145
145
|
}],
|
|
146
146
|
isError: true,
|
|
@@ -29,7 +29,7 @@ Fields per service:
|
|
|
29
29
|
python → pytest or robot
|
|
30
30
|
typescript / javascript → playwright
|
|
31
31
|
java → junit
|
|
32
|
-
|
|
32
|
+
testDirectory — service root relative to repo root where the test files will be generated, e.g. "services/api-gateway"
|
|
33
33
|
api.schemaPath — path or URL to OpenAPI/Protobuf/GraphQL schema
|
|
34
34
|
Search for: openapi.json, swagger.yaml, *.proto, *.graphql
|
|
35
35
|
Framework defaults: FastAPI → /openapi.json, Express → /api-docs, Spring → /v3/api-docs
|
|
@@ -188,8 +188,8 @@ Create one service entry per deployable unit. You MUST include:
|
|
|
188
188
|
parts.push(svc.language);
|
|
189
189
|
if (svc.framework)
|
|
190
190
|
parts.push(svc.framework);
|
|
191
|
-
if (svc.
|
|
192
|
-
parts.push(`@ ${svc.
|
|
191
|
+
if (svc.testDirectory)
|
|
192
|
+
parts.push(`@ ${svc.testDirectory}`);
|
|
193
193
|
lines.push(` - ${parts.join(" | ")}`);
|
|
194
194
|
if (svc.api?.schemaPath)
|
|
195
195
|
lines.push(` API: ${svc.api.schemaPath}`);
|
|
@@ -64,6 +64,7 @@ export const scenarioStepSchema = z.object({
|
|
|
64
64
|
description: z.string(),
|
|
65
65
|
interactionType: z.enum(["success", "error", "edge-case"]),
|
|
66
66
|
requestBody: z.record(z.any()).optional(),
|
|
67
|
+
queryParams: z.record(z.any()).optional(),
|
|
67
68
|
responseBody: z.record(z.any()).optional(),
|
|
68
69
|
expectedStatusCode: z.number(),
|
|
69
70
|
expectedResponseFields: z.array(z.string()).optional(),
|
|
@@ -38,16 +38,27 @@ function sanitizeHeaders(headers) {
|
|
|
38
38
|
return result;
|
|
39
39
|
}
|
|
40
40
|
function parseSkyrampTrace(data) {
|
|
41
|
-
return data.map((entry) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
return data.map((entry) => {
|
|
42
|
+
const rawQp = entry.QueryParams || entry.queryParams;
|
|
43
|
+
let queryParams;
|
|
44
|
+
if (rawQp && typeof rawQp === "object" && !Array.isArray(rawQp) && Object.keys(rawQp).length > 0) {
|
|
45
|
+
queryParams = {};
|
|
46
|
+
for (const [k, v] of Object.entries(rawQp)) {
|
|
47
|
+
queryParams[k] = Array.isArray(v) ? v.map(String) : [String(v)];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
method: (entry.method || entry.Method || entry.request?.method || "GET").toUpperCase(),
|
|
52
|
+
path: entry.path || entry.Path || entry.url || entry.request?.url || "/",
|
|
53
|
+
statusCode: entry.statusCode || entry.StatusCode || entry.status || entry.response?.status || 0,
|
|
54
|
+
requestHeaders: entry.request?.headers ? sanitizeHeaders(entry.request.headers) : undefined,
|
|
55
|
+
requestBody: entry.request?.body || entry.requestBody || entry.RequestBody,
|
|
56
|
+
queryParams,
|
|
57
|
+
responseHeaders: entry.response?.headers ? sanitizeHeaders(entry.response.headers) : undefined,
|
|
58
|
+
responseBody: summarizeBody(entry.response?.body || entry.responseBody || entry.ResponseBody),
|
|
59
|
+
timestamp: entry.timestamp || entry.Timestamp || entry.request?.timestamp || "",
|
|
60
|
+
};
|
|
61
|
+
});
|
|
51
62
|
}
|
|
52
63
|
function parseHarTrace(harData) {
|
|
53
64
|
const entries = harData?.log?.entries || [];
|
|
@@ -79,12 +90,22 @@ function parseHarTrace(harData) {
|
|
|
79
90
|
resBody = res.content.text;
|
|
80
91
|
}
|
|
81
92
|
}
|
|
93
|
+
const queryParams = {};
|
|
94
|
+
url.searchParams.forEach((v, k) => {
|
|
95
|
+
if (queryParams[k]) {
|
|
96
|
+
queryParams[k].push(v);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
queryParams[k] = [v];
|
|
100
|
+
}
|
|
101
|
+
});
|
|
82
102
|
return {
|
|
83
103
|
method: (req.method || "GET").toUpperCase(),
|
|
84
|
-
path: url.pathname
|
|
104
|
+
path: url.pathname,
|
|
85
105
|
statusCode: res.status || 0,
|
|
86
106
|
requestHeaders: sanitizeHeaders(reqHeaders),
|
|
87
107
|
requestBody: reqBody,
|
|
108
|
+
queryParams: Object.keys(queryParams).length > 0 ? queryParams : undefined,
|
|
88
109
|
responseHeaders: sanitizeHeaders(resHeaders),
|
|
89
110
|
responseBody: summarizeBody(resBody),
|
|
90
111
|
timestamp: entry.startedDateTime || "",
|
|
@@ -20,12 +20,12 @@ export async function getWorkspaceAuthHeader(repositoryPath) {
|
|
|
20
20
|
* Returns scheme+host+port for the service that owns the given test file.
|
|
21
21
|
*
|
|
22
22
|
* Matching strategy (in order):
|
|
23
|
-
* 1. Filter services whose `
|
|
24
|
-
* 2. If multiple services match (shared
|
|
23
|
+
* 1. Filter services whose `testDirectory` is a path prefix of `testFile`.
|
|
24
|
+
* 2. If multiple services match (shared testDirectory), narrow by `language`.
|
|
25
25
|
* 3. If still ambiguous, return all candidates so the LLM can resolve it —
|
|
26
26
|
* never guess when multiple services are plausible owners.
|
|
27
27
|
*
|
|
28
|
-
* e.g. repositoryPath=/repo,
|
|
28
|
+
* e.g. repositoryPath=/repo, testDirectory=backend/tests, testFile=/repo/backend/tests/test_api.py
|
|
29
29
|
* → { baseUrl: "http://localhost:8000", candidates: [] }
|
|
30
30
|
*/
|
|
31
31
|
export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
|
|
@@ -42,12 +42,12 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
|
|
|
42
42
|
const absTestFile = path.isAbsolute(testFile)
|
|
43
43
|
? testFile
|
|
44
44
|
: path.resolve(repositoryPath, testFile);
|
|
45
|
-
// Step 1: filter by
|
|
45
|
+
// Step 1: filter by testDirectory prefix
|
|
46
46
|
let matches = services.filter((s) => {
|
|
47
|
-
if (!s.
|
|
47
|
+
if (!s.testDirectory || !s.api?.baseUrl)
|
|
48
48
|
return false;
|
|
49
|
-
const
|
|
50
|
-
return absTestFile.startsWith(
|
|
49
|
+
const absTestDirectory = path.resolve(repositoryPath, s.testDirectory);
|
|
50
|
+
return absTestFile.startsWith(absTestDirectory + path.sep) || absTestFile === absTestDirectory;
|
|
51
51
|
});
|
|
52
52
|
// Step 2: narrow by language if still ambiguous
|
|
53
53
|
if (matches.length > 1 && language) {
|
|
@@ -64,7 +64,7 @@ export async function getWorkspaceBaseUrl(repositoryPath, testFile, language) {
|
|
|
64
64
|
return {
|
|
65
65
|
baseUrl: undefined,
|
|
66
66
|
candidates: matches.map((s) => ({
|
|
67
|
-
serviceName: s.serviceName ?? s.
|
|
67
|
+
serviceName: s.serviceName ?? s.testDirectory,
|
|
68
68
|
baseUrl: s.api.baseUrl,
|
|
69
69
|
})),
|
|
70
70
|
};
|
|
@@ -6,13 +6,13 @@ jest.mock("@skyramp/skyramp", () => {
|
|
|
6
6
|
{
|
|
7
7
|
serviceName: "backend",
|
|
8
8
|
language: "python",
|
|
9
|
-
|
|
9
|
+
testDirectory: "backend/tests",
|
|
10
10
|
api: { baseUrl: "http://localhost:8000" },
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
serviceName: "frontend",
|
|
14
14
|
language: "typescript",
|
|
15
|
-
|
|
15
|
+
testDirectory: "frontend/tests",
|
|
16
16
|
api: { baseUrl: "http://localhost:5173" },
|
|
17
17
|
},
|
|
18
18
|
],
|
|
@@ -30,17 +30,17 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
30
30
|
expect(result.baseUrl).toBeUndefined();
|
|
31
31
|
expect(result.candidates).toHaveLength(0);
|
|
32
32
|
});
|
|
33
|
-
it("should match backend service by
|
|
33
|
+
it("should match backend service by testDirectory prefix", async () => {
|
|
34
34
|
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/backend/tests/test_api.py`);
|
|
35
35
|
expect(result.baseUrl).toBe("http://localhost:8000");
|
|
36
36
|
expect(result.candidates).toHaveLength(0);
|
|
37
37
|
});
|
|
38
|
-
it("should match frontend service by
|
|
38
|
+
it("should match frontend service by testDirectory prefix", async () => {
|
|
39
39
|
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/frontend/tests/auth.spec.ts`);
|
|
40
40
|
expect(result.baseUrl).toBe("http://localhost:5173");
|
|
41
41
|
expect(result.candidates).toHaveLength(0);
|
|
42
42
|
});
|
|
43
|
-
it("should return no match when testFile does not match any service
|
|
43
|
+
it("should return no match when testFile does not match any service testDirectory", async () => {
|
|
44
44
|
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/other/tests/test_something.py`);
|
|
45
45
|
expect(result.baseUrl).toBeUndefined();
|
|
46
46
|
expect(result.candidates).toHaveLength(0);
|
|
@@ -53,7 +53,7 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
53
53
|
services: [
|
|
54
54
|
{
|
|
55
55
|
serviceName: "api",
|
|
56
|
-
|
|
56
|
+
testDirectory: "tests",
|
|
57
57
|
api: { baseUrl: "http://localhost:8000/api/v2" },
|
|
58
58
|
},
|
|
59
59
|
],
|
|
@@ -73,7 +73,7 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
73
73
|
expect(result.baseUrl).toBeUndefined();
|
|
74
74
|
expect(result.candidates).toHaveLength(0);
|
|
75
75
|
});
|
|
76
|
-
it("should disambiguate by language when multiple services share the same
|
|
76
|
+
it("should disambiguate by language when multiple services share the same testDirectory", async () => {
|
|
77
77
|
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
78
78
|
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
79
79
|
exists: jest.fn().mockResolvedValue(true),
|
|
@@ -82,13 +82,13 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
82
82
|
{
|
|
83
83
|
serviceName: "svc-python",
|
|
84
84
|
language: "python",
|
|
85
|
-
|
|
85
|
+
testDirectory: "shared/tests",
|
|
86
86
|
api: { baseUrl: "http://localhost:8000" },
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
serviceName: "svc-ts",
|
|
90
90
|
language: "typescript",
|
|
91
|
-
|
|
91
|
+
testDirectory: "shared/tests",
|
|
92
92
|
api: { baseUrl: "http://localhost:9000" },
|
|
93
93
|
},
|
|
94
94
|
],
|
|
@@ -98,7 +98,7 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
98
98
|
expect(result.baseUrl).toBe("http://localhost:8000");
|
|
99
99
|
expect(result.candidates).toHaveLength(0);
|
|
100
100
|
});
|
|
101
|
-
it("should return candidates when
|
|
101
|
+
it("should return candidates when testDirectory and language are both shared across services", async () => {
|
|
102
102
|
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
103
103
|
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
104
104
|
exists: jest.fn().mockResolvedValue(true),
|
|
@@ -107,13 +107,13 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
107
107
|
{
|
|
108
108
|
serviceName: "svc-a",
|
|
109
109
|
language: "python",
|
|
110
|
-
|
|
110
|
+
testDirectory: "shared/tests",
|
|
111
111
|
api: { baseUrl: "http://localhost:8000" },
|
|
112
112
|
},
|
|
113
113
|
{
|
|
114
114
|
serviceName: "svc-b",
|
|
115
115
|
language: "python",
|
|
116
|
-
|
|
116
|
+
testDirectory: "shared/tests",
|
|
117
117
|
api: { baseUrl: "http://localhost:9000" },
|
|
118
118
|
},
|
|
119
119
|
],
|
|
@@ -128,12 +128,30 @@ describe("getWorkspaceBaseUrl", () => {
|
|
|
128
128
|
"http://localhost:9000",
|
|
129
129
|
]);
|
|
130
130
|
});
|
|
131
|
+
it("should match service using testDirectory as the workspace field for test file lookup", async () => {
|
|
132
|
+
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
133
|
+
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
134
|
+
exists: jest.fn().mockResolvedValue(true),
|
|
135
|
+
read: jest.fn().mockResolvedValue({
|
|
136
|
+
services: [
|
|
137
|
+
{
|
|
138
|
+
serviceName: "api",
|
|
139
|
+
testDirectory: "tests",
|
|
140
|
+
api: { baseUrl: "http://localhost:9000" },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
}));
|
|
145
|
+
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/tests/test_api.py`);
|
|
146
|
+
expect(result.baseUrl).toBe("http://localhost:9000");
|
|
147
|
+
expect(result.candidates).toHaveLength(0);
|
|
148
|
+
});
|
|
131
149
|
it("should return no match when matched service baseUrl is not a valid URL", async () => {
|
|
132
150
|
const { WorkspaceConfigManager } = await import("@skyramp/skyramp");
|
|
133
151
|
WorkspaceConfigManager.mockImplementationOnce(() => ({
|
|
134
152
|
exists: jest.fn().mockResolvedValue(true),
|
|
135
153
|
read: jest.fn().mockResolvedValue({
|
|
136
|
-
services: [{ api: { baseUrl: "not-a-valid-url" },
|
|
154
|
+
services: [{ api: { baseUrl: "not-a-valid-url" }, testDirectory: "tests" }],
|
|
137
155
|
}),
|
|
138
156
|
}));
|
|
139
157
|
const result = await getWorkspaceBaseUrl(REPO, `${REPO}/tests/test_api.py`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyramp/mcp",
|
|
3
|
-
"version": "0.0.63-rc.
|
|
3
|
+
"version": "0.0.63-rc.6",
|
|
4
4
|
"main": "build/index.js",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@modelcontextprotocol/sdk": "^1.24.3",
|
|
48
48
|
"@playwright/test": "^1.55.0",
|
|
49
|
-
"@skyramp/skyramp": "1.3.
|
|
49
|
+
"@skyramp/skyramp": "1.3.14",
|
|
50
50
|
"dockerode": "^4.0.6",
|
|
51
51
|
"fast-glob": "^3.3.3",
|
|
52
52
|
"simple-git": "^3.30.0",
|