@skyramp/mcp 0.3.3 → 0.3.5

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 (59) hide show
  1. package/build/playwright/registerPlaywrightTools.js +42 -1
  2. package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
  3. package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
  4. package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
  5. package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
  6. package/build/prompts/test-recommendation/recommendationSections.js +5 -5
  7. package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
  8. package/build/prompts/testbot/testbot-prompts.js +16 -14
  9. package/build/recommendation/discriminators.d.ts +7 -1
  10. package/build/recommendation/discriminators.js +16 -3
  11. package/build/resources/testbotResource.js +0 -1
  12. package/build/services/ScenarioGenerationService.js +5 -2
  13. package/build/services/TestExecutionService.js +25 -1
  14. package/build/services/TestGenerationService.js +24 -9
  15. package/build/services/containerEnv.d.ts +12 -1
  16. package/build/services/containerEnv.js +94 -1
  17. package/build/tools/executeSkyrampTestTool.d.ts +9 -0
  18. package/build/tools/executeSkyrampTestTool.js +20 -6
  19. package/build/tools/execution-video-state.d.ts +21 -0
  20. package/build/tools/execution-video-state.js +51 -0
  21. package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
  22. package/build/tools/generate-tests/planGuard.d.ts +5 -5
  23. package/build/tools/generate-tests/planGuard.js +5 -17
  24. package/build/tools/submitReportTool.d.ts +83 -10
  25. package/build/tools/submitReportTool.js +257 -30
  26. package/build/tools/test-management/actionsTool.js +40 -39
  27. package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
  28. package/build/tools/test-management/analyzeChangesTool.js +41 -35
  29. package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
  30. package/build/tools/test-management/registerTestPlanTool.js +191 -41
  31. package/build/types/TestExecution.d.ts +14 -0
  32. package/build/types/TestTypes.js +3 -2
  33. package/build/types/TestbotPromptOptions.d.ts +0 -1
  34. package/build/types/TestbotReport.d.ts +24 -1
  35. package/build/types/TestbotReport.js +10 -1
  36. package/build/types/index.d.ts +1 -0
  37. package/build/types/index.js +1 -0
  38. package/build/utils/AnalysisStateManager.d.ts +48 -2
  39. package/build/utils/AnalysisStateManager.js +49 -13
  40. package/build/utils/reportLanguage.d.ts +43 -0
  41. package/build/utils/reportLanguage.js +125 -0
  42. package/build/utils/reportVerification.d.ts +10 -0
  43. package/build/utils/reportVerification.js +31 -0
  44. package/build/utils/scenarioDrafting.js +7 -1
  45. package/build/utils/skyrampMdContent.d.ts +1 -1
  46. package/build/utils/skyrampMdContent.js +1 -1
  47. package/build/utils/urlPath.d.ts +37 -0
  48. package/build/utils/urlPath.js +55 -0
  49. package/build/utils/utils.d.ts +45 -0
  50. package/build/utils/utils.js +50 -0
  51. package/build/utils/versions.d.ts +3 -3
  52. package/build/utils/versions.js +1 -1
  53. package/build/utils/workspaceAuth.d.ts +15 -15
  54. package/build/utils/workspaceAuth.js +32 -17
  55. package/build/workspace/queryParamResolution.d.ts +93 -0
  56. package/build/workspace/queryParamResolution.js +201 -0
  57. package/build/workspace/workspace.d.ts +104 -0
  58. package/build/workspace/workspace.js +24 -0
  59. package/package.json +3 -2
@@ -4,11 +4,37 @@
4
4
  */
5
5
  import { z } from "zod";
6
6
  import { logger } from "../utils/logger.js";
7
+ import { dualChannelResult } from "../utils/utils.js";
7
8
  import { PlaywrightTraceService, } from "./PlaywrightTraceService.js";
8
9
  let _service = null;
9
10
  export function getPlaywrightTraceService() {
10
11
  return _service;
11
12
  }
13
+ /**
14
+ * The one browser tool whose result is re-shaped on the way out — see
15
+ * toStructuredBlueprintResult.
16
+ */
17
+ const BLUEPRINT_TOOL = "browser_blueprint";
18
+ /**
19
+ * Re-shape a browser_blueprint result onto both MCP result channels.
20
+ *
21
+ * SKYR-4194. The browser tools are registered here as passthroughs, and the fork
22
+ * returns the captured DOM as one `content[]` text block with no
23
+ * `structuredContent` — the shape whose spill the agent's file reader refuses.
24
+ * See `dualChannelResult` for why both channels carry the payload.
25
+ *
26
+ * An error result is returned untouched: the SDK's outputSchema check returns
27
+ * early on `isError`, so it needs no structuredContent.
28
+ */
29
+ function toStructuredBlueprintResult(result) {
30
+ if (result?.isError)
31
+ return result;
32
+ const blueprint = (result?.content ?? [])
33
+ .filter((block) => block?.type === "text")
34
+ .map((block) => block.text)
35
+ .join("\n");
36
+ return dualChannelResult({ blueprint });
37
+ }
12
38
  export async function registerPlaywrightTools(server, options) {
13
39
  _service = new PlaywrightTraceService(options);
14
40
  try {
@@ -68,9 +94,23 @@ export async function registerPlaywrightTools(server, options) {
68
94
  logger.info(`Filtering to ${filteredTools.length} essential tools (from ${tools.length} total)`);
69
95
  for (const tool of filteredTools) {
70
96
  const zodSchema = jsonSchemaToZod(tool.inputSchema);
97
+ const isBlueprint = tool.name === BLUEPRINT_TOOL;
71
98
  server.registerTool(tool.name, {
72
99
  description: tool.description || `Playwright tool: ${tool.name}`,
73
100
  inputSchema: zodSchema,
101
+ // SKYR-4194: only browser_blueprint returns its payload via
102
+ // structuredContent, which requires a declared outputSchema. The other
103
+ // browser tools return small results, and some return images, which this
104
+ // text-only re-shaping would drop.
105
+ ...(isBlueprint
106
+ ? {
107
+ outputSchema: {
108
+ blueprint: z
109
+ .string()
110
+ .describe("The captured PageBlueprint — sections, elements, and their logical names. Read and follow this field."),
111
+ },
112
+ }
113
+ : {}),
74
114
  }, async (params) => {
75
115
  if (!_service?.isInitialized()) {
76
116
  return {
@@ -83,7 +123,8 @@ export async function registerPlaywrightTools(server, options) {
83
123
  isError: true,
84
124
  };
85
125
  }
86
- return _service.callTool(tool.name, params);
126
+ const result = await _service.callTool(tool.name, params);
127
+ return isBlueprint ? toStructuredBlueprintResult(result) : result;
87
128
  });
88
129
  }
89
130
  logger.info(`Registered ${filteredTools.length} Playwright tools: ${filteredTools.map((t) => t.name).join(", ")}`);
@@ -154,6 +154,25 @@ expect(getResponseValue(productsPostResponse, "created_at")).toMatch(/^\\d{4}-\\
154
154
  },
155
155
  ],
156
156
  },
157
+ {
158
+ title: "Assert a nullable field is present, not merely null",
159
+ description: "Reading a field's value cannot tell a null field from a missing one — `get_response_value` / `getValue` return the same result for both, so `is None` does not prove the field exists. Use a schema check with an example body instead.",
160
+ subPoints: [
161
+ "An example carrying `null` for the field asserts presence and nullness together, and fails when the field is absent.",
162
+ "An example carrying a concrete value only asserts that value's type, so a nullable field checked against a string example fails once the real value is null.",
163
+ "`check_schema` / `checkSchema` takes the RESPONSE OBJECT, not a parsed body. The `@skyramp/skyramp` type declaration says otherwise and is wrong — passing a plain object makes every check silently return false (SKYR-4189).",
164
+ ],
165
+ examples: [
166
+ {
167
+ language: "python",
168
+ code: `assert skyramp.check_schema(order_response, '{"cancelled_at": null}')`,
169
+ },
170
+ {
171
+ language: "javascript",
172
+ code: `expect(checkSchema(orderResponse, '{"cancelled_at": null}')).toBe(true);`,
173
+ },
174
+ ],
175
+ },
157
176
  ];
158
177
  export function renderRule(index, rule) {
159
178
  const subPoints = rule.subPoints && rule.subPoints.length > 0
@@ -15,7 +15,7 @@ export function buildFileRenameStrategy() {
15
15
  }
16
16
  /** Strategy string for in-place UPDATE edits. */
17
17
  export function buildUpdateStrategy() {
18
- return `Apply each file in update_context yourself with the Edit tool, using its current_content and the changes described in context. Preserve all existing test logic — only add or adjust what context describes.
18
+ return `Apply each file in update_context yourself with the Edit tool. Read the file first, then make the changes described in context. Preserve all existing test logic — only add or adjust what context describes.
19
19
 
20
20
  When a file provides edit_sites, apply each site directly using its anchor as the Edit target — do not re-grep or re-page the file to relocate the sites; they were already found for you.
21
21
 
@@ -25,7 +25,7 @@ After editing, re-read each file to confirm the change landed, then call skyramp
25
25
  }
26
26
  /** Strategy string for REGENERATE — call generation tool to overwrite the file. */
27
27
  export function buildRegenerateStrategy() {
28
- return `For each file in regenerate_context, call the appropriate generation tool (skyramp_integration_test_generation or skyramp_contract_test_generation) with outputDir set to the file's directory and output set to the filename. Use existing_content to determine the test type, endpoint, auth pattern, and language. The generation tool will overwrite the file. Do NOT use skyramp_ui_test_generation here — UI test regeneration requires a recorded trace (playwrightInput) and must be handled separately.`;
28
+ return `For each file in regenerate_context, call the appropriate generation tool (skyramp_integration_test_generation or skyramp_contract_test_generation) with outputDir set to the file's directory and output set to the filename. The generation tool will overwrite the file. Do NOT use skyramp_ui_test_generation here — UI test regeneration requires a recorded trace (playwrightInput) and must be handled separately.`;
29
29
  }
30
30
  /** Strategy string for DELETE — remove obsolete test files whose covered endpoints/UI no longer exist. */
31
31
  export function buildDeleteStrategy() {
@@ -347,10 +347,7 @@ Static endpoint scan results are **best-effort hints only**. Do not assume the s
347
347
  ${staticHintCount > 0 ? `Static hints available: ${staticHintCount}. Verify every hinted method/path against source before using it.` : "Static hints available: 0. Build the endpoint list from source, router context, spec, and diff."}
348
348
 
349
349
  ${p.candidateRouteFiles && p.candidateRouteFiles.length > 0
350
- ? `Candidate files to inspect:\n${p.candidateRouteFiles
351
- .slice(0, 15)
352
- .map((f) => `- \`${f}\``)
353
- .join("\n")}${p.candidateRouteFiles.length > 15 ? `\n_(${p.candidateRouteFiles.length - 15} more files not shown)_` : ""}`
350
+ ? ""
354
351
  : staticHintCount > 0
355
352
  ? "Candidate files to inspect: none identified by static scanning. Verify the static hints against their source files, changed files, router context, and specs above."
356
353
  : "Candidate files to inspect: use the changed files and routing entry-point files above."}
@@ -32,4 +32,4 @@ export declare function buildTestQualityCriteria(): string;
32
32
  export declare function buildGenerationRules(isUIOnlyPR: boolean): string;
33
33
  export declare function buildVerificationChecklist(topN: number, maxGen: number, codeReviewStepLabel?: string): string;
34
34
  export declare function buildFewShotExamples(): string;
35
- export declare function buildToolWorkflows(authHeaderValue: string, authTypeValue?: string, explicitScheme?: string): string;
35
+ export declare function buildToolWorkflows(authHeaderValue: string, authTypeValue?: string, explicitScheme?: string, enrichStepLabel?: string): string;
@@ -1,6 +1,6 @@
1
1
  import { isContractConsumerModeEnabled } from "../../utils/featureFlags.js";
2
2
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
3
- import { WorkspaceAuthType, getAuthScheme, isAuthorizationHeaderName, AUTH_MIDDLEWARE_PATTERNS_STR } from "../../utils/workspaceAuth.js";
3
+ import { WorkspaceAuthType, getAuthScheme, isAuthorizationHeaderName } from "../../utils/workspaceAuth.js";
4
4
  // Cached at module-load — flags are process-wide and cannot change per call.
5
5
  const CONSUMER_MODE_ENABLED = isContractConsumerModeEnabled();
6
6
  const SERVICE_REFS = resolveServiceDetailsRef();
@@ -243,7 +243,7 @@ Reasoning: Catches a missing 404 guard on DELETE — verifies the handler return
243
243
  </example>
244
244
  </examples>`;
245
245
  }
246
- export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicitScheme) {
246
+ export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicitScheme, enrichStepLabel = "2") {
247
247
  const isAuthorizationHeader = isAuthorizationHeaderName(authHeaderValue);
248
248
  const noAuth = !authHeaderValue;
249
249
  let authGuidance;
@@ -251,7 +251,7 @@ export function buildToolWorkflows(authHeaderValue, authTypeValue = "", explicit
251
251
  if (noAuth) {
252
252
  authGuidance = `**Auth Verification Required:** The workspace config indicates no authentication, but you MUST verify this independently before omitting auth:
253
253
  1. **OpenAPI spec** \u2192 check \`securitySchemes\` / \`securityDefinitions\` for \`type: http\`, \`type: apiKey\`, or \`type: oauth2\`
254
- 2. **Source code** \u2192 look for known auth signals (${AUTH_MIDDLEWARE_PATTERNS_STR}).
254
+ 2. **Source code** \u2192 look for the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan.
255
255
  3. **Route definitions** \u2192 check if routes have auth decorators or middleware applied
256
256
  4. **Still unknown** \u2192 proceed with \`authHeader: ""\` and note "auth pattern unrecognized" in your recommendation description.
257
257
  If you find auth requirements, pass the appropriate \`authHeader\` and \`authScheme\` to EVERY tool call. Only pass \`authHeader: ""\` if you confirm the API is truly unauthenticated.
@@ -270,7 +270,7 @@ If you find auth requirements, pass the appropriate \`authHeader\` and \`authSch
270
270
  authGuidance = `**Auth Scheme:** The workspace \`api.authType\` is \`"${authTypeValue}"\`.
271
271
  **Where to find the scheme** (check in order):
272
272
  1. **OpenAPI spec** \u2192 look at \`securitySchemes\` / \`securityDefinitions\` for \`type: http, scheme: bearer\` or \`type: apiKey\`
273
- 2. **Source code** \u2192 auth middleware signals: ${AUTH_MIDDLEWARE_PATTERNS_STR}
273
+ 2. **Source code** \u2192 the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan
274
274
  3. **Workspace config** \u2192 use \`api.authType\` value as the scheme if source is inconclusive
275
275
  Pass the prefix as \`authScheme\` (e.g., \`"Bearer"\`, \`"Token"\`, \`"Basic"\`). If the API uses raw tokens with no prefix, pass \`authScheme: ""\`.
276
276
  **Do NOT guess the scheme.**
@@ -280,7 +280,7 @@ To skip auth entirely, pass \`authHeader: ""\`.`;
280
280
  authGuidance = `**Auth Scheme:** No \`api.authType\` in workspace config.
281
281
  **Where to find the scheme** (check in order):
282
282
  1. **OpenAPI spec** \u2192 look at \`securitySchemes\` / \`securityDefinitions\` for \`type: http, scheme: bearer\` or \`type: apiKey\`
283
- 2. **Source code** \u2192 auth middleware signals: ${AUTH_MIDDLEWARE_PATTERNS_STR}
283
+ 2. **Source code** \u2192 the auth middleware signals listed in Step ${enrichStepLabel} of the Execution Plan
284
284
  3. **Fallback** \u2192 use \`"Bearer"\` only if the project clearly uses JWT or OAuth; otherwise pass \`authScheme: ""\`
285
285
  Pass the prefix as \`authScheme\` (e.g., \`"Bearer"\`, \`"Token"\`, \`"Basic"\`). If the API uses raw tokens with no prefix, pass \`authScheme: ""\`.
286
286
  **Do NOT guess the scheme.**
@@ -5,7 +5,7 @@ import { logger } from "../../utils/logger.js";
5
5
  import { buildArchitectPreamble, buildContextFetchingGuidance, buildReasoningProtocol, buildToolWorkflows, buildFewShotExamples, buildVerificationChecklist, getAuthSnippets, MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, } from "./recommendationSections.js";
6
6
  import { CATEGORY_PRIORITY, Novelty, PriorityTier } from "../../types/TestRecommendation.js";
7
7
  import { buildScopeAssessmentSection, isFrontendFile } from "./scopeAssessment.js";
8
- import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW } from "./diffExecutionPlan.js";
8
+ import { buildExecutionPlan, EXEC_STEP_CODE_REVIEW, EXEC_STEP_ENRICH } from "./diffExecutionPlan.js";
9
9
  import { buildFullRepoRecommendations } from "./fullRepoCatalog.js";
10
10
  import { ANALYSIS_STEP_EXTRACT } from "./analysisOutputPrompt.js";
11
11
  import { TASK_GENERATE, buildExternalCoverageSet, externalDedupKey, isAttackSurfaceSecurityBoundary, taskRef, } from "./recommendationShared.js";
@@ -345,8 +345,15 @@ Treat the endpoint lists above as static hints. If source/diff inspection finds
345
345
  diffEndpointKeys.add(`${m.method} ${ep.path}`);
346
346
  }
347
347
  }
348
- const summaryLines = allEndpoints
349
- .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} ${i.response.statusCode} (${i.type}): ${i.description}`)))
348
+ // SKYR-4188: `description` held exactly "<METHOD> <path>" in 11,273 of 11,273
349
+ // interaction lines on eval run 32227152533 a copy of the start of its own
350
+ // line. Emit it only when it says something the line does not already say.
351
+ const interactionLabel = (i, method, path) => i.description && i.description !== `${method} ${path}` ? `: ${i.description}` : "";
352
+ // detailBlocks repeats every summary line and adds the bodies. The two cover
353
+ // the same endpoints unless the scope is a diff, so outside a diff the summary
354
+ // is pure repetition.
355
+ const summaryLines = !isDiffScope ? "" : allEndpoints
356
+ .flatMap((ep) => (ep.methods ?? []).flatMap((m) => (m.interactions ?? []).map((i) => ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}`)))
350
357
  .join("\n");
351
358
  const detailEndpoints = isDiffScope
352
359
  ? allEndpoints.filter((ep) => (ep.methods ?? []).some((m) => diffEndpointKeys.has(`${m.method} ${ep.path}`)))
@@ -357,13 +364,12 @@ Treat the endpoint lists above as static hints. If source/diff inspection finds
357
364
  const qParams = i.request.queryParams ? `\n queryParams: ${JSON.stringify(i.request.queryParams)}` : "";
358
365
  const resBody = i.response.body ? `\n responseBody: ${JSON.stringify(i.response.body)}` : "";
359
366
  const headers = i.request.headers ? `\n headers: ${JSON.stringify(i.request.headers)}` : "";
360
- return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type}): ${i.description}${reqBody}${qParams}${resBody}${headers}`;
367
+ return ` ${m.method} ${ep.path} → ${i.response.statusCode} (${i.type})${interactionLabel(i, m.method, ep.path)}${reqBody}${qParams}${resBody}${headers}`;
361
368
  })))
362
369
  .join("\n");
363
370
  interactionSection = `
364
371
  ## Endpoint Interactions
365
- ${summaryLines}
366
-
372
+ ${summaryLines ? `${summaryLines}\n` : ""}
367
373
  ### Detailed (request/response bodies)
368
374
  ${isDiffScope ? "Changed endpoints only. " : ""}Use source code schemas (Zod/Pydantic/DTOs) for actual request bodies and query parameters.
369
375
  ${detailBlocks}
@@ -557,7 +563,7 @@ ${isDiffScope
557
563
  5. \`skyramp_ui_test_generation({ playwrightInput: "<absolute_path_to_zip>" })\`
558
564
 
559
565
  **For E2E tests:** Same browser recording flow, then call \`skyramp_e2e_test_generation\` with the zip.`
560
- : buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme))
566
+ : buildToolWorkflows(authHeaderValue, authTypeValue, workspaceAuthScheme, EXEC_STEP_ENRICH))
561
567
  : ""}
562
568
 
563
569
  ${mainSection}
@@ -5,6 +5,7 @@ import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS, MAX_CRITICAL_TESTS, PATH_PA
5
5
  import { TASK_ANALYZE_MAINTAIN, TASK_GENERATE, TASK_SUBMIT, taskRef } from "../test-recommendation/recommendationShared.js";
6
6
  import { getTraceRecordingPromptText } from "../../playwright/traceRecordingPrompt.js";
7
7
  import { isContractConsumerModeEnabled, isPomReuseEnabled } from "../../utils/featureFlags.js";
8
+ import { setReportLanguage } from "../../utils/reportLanguage.js";
8
9
  import { resolveServiceDetailsRef } from "../../utils/utils.js";
9
10
  import { buildServiceContext, readWorkspaceServices, } from "../prompt-utils.js";
10
11
  // Cached at module-load — flags are process-wide and cannot change per call.
@@ -55,7 +56,7 @@ export function parseRelatedRepositories(raw) {
55
56
  }
56
57
  }
57
58
  export function getTestbotPrompt(opts) {
58
- const { prTitle, prDescription, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
59
+ const { prTitle, prDescription, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
59
60
  // maxCritical is intentionally unused (reserved) — see TestbotPromptOptions.
60
61
  const maxGenerate = Math.min(Math.max(opts.maxGenerate ?? MAX_TESTS_TO_GENERATE, 0), maxRecommendations);
61
62
  // TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
@@ -77,6 +78,14 @@ export function getTestbotPrompt(opts) {
77
78
  // SKYR-4023: user-facing report language. English display names improve
78
79
  // instruction-following vs bare codes; Intl.DisplayNames (Node >=18) names any
79
80
  // code correctly, falling back to the raw code only if it can't be resolved.
81
+ // SKYR-4185: rendering the prompt is the single capture point for the
82
+ // report language — the prompt registration AND the testbot resource both
83
+ // come through here, so skyramp_submit_report's language guardrail sees it
84
+ // regardless of which surface served the prompt (no dual wiring to forget).
85
+ // Set on EVERY render, not just non-English ones: last render wins, so an
86
+ // en/argless render disarms a language captured earlier in a long-lived
87
+ // server process instead of falsely rejecting an English report.
88
+ setReportLanguage(language && language !== 'en' ? language : undefined);
80
89
  let reportLanguageBlock = '';
81
90
  if (language && language !== 'en') {
82
91
  const reportLanguageName = new Intl.DisplayNames(['en'], { type: 'language' }).of(language) ?? language;
@@ -408,7 +417,7 @@ This is a plan-only evaluation run: the application under test is NOT running, a
408
417
 
409
418
  ${userPrompt ? "Generate only the tests that the user requested from the Additional Recommendations. The rules below still apply." : "Drift-based maintenance (Task 1) is complete. This step only processes the GENERATE list. Exception: if a GENERATE item targets a resource with an existing `[skyramp]` contract test, UPDATE that test file (see covered-resource handling below) — a new test case added to an existing file counts toward the budget and is reported in `newTestsCreated`."}
410
419
 
411
- - **MANDATORY — use the plan returned by \`skyramp_register_test_plan\` as-is**: Before generating anything, call \`skyramp_register_test_plan\` (\`stateFile\` required) with your complete candidate list — every test you would generate OR recommend, including the Execution Plan's own pre-ranked GENERATE/ADDITIONAL items and any candidate you drafted yourself, with a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for candidates probing changed logic. Its returned GENERATE list — not the Execution Plan's raw pre-ranked GENERATE section — governs ADD actions from this point on. You MUST generate exactly those scenarios in the exact order listed, keeping each item's \`scenarioName\` exactly as registered — the generation tools match on it and reject renamed or substituted scenarios. If parameter grounding uncovers a distinct bug-catching scenario not already registered, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Register the pooled, type-distributed selection instead of any single repo's GENERATE list. (In single-repo runs, register the GENERATE list exactly as-is.)` : ""}
420
+ - **MANDATORY — use the plan returned by \`skyramp_register_test_plan\` as-is**: Before generating anything, call \`skyramp_register_test_plan\` (\`stateFile\` required) with your complete candidate list — every test you would generate OR recommend, including the Execution Plan's own pre-ranked GENERATE/ADDITIONAL items and any candidate you drafted yourself, with a \`discriminator\` claim \`{kind, changedCodeAnchor}\` for candidates probing changed logic. Its returned GENERATE list — not the Execution Plan's raw pre-ranked GENERATE section — governs ADD actions from this point on. You MUST generate exactly those scenarios in the exact order listed, keeping each item's \`scenarioName\` exactly as registered — the generation tools match on it and reject renamed or substituted scenarios. If parameter grounding uncovers a distinct bug-catching scenario not already registered, generate it after all planned GENERATE items are complete and report it in \`newTestsCreated\` — this is an additional test driven by source-code analysis and does not count against the GENERATE budget.${hasRelatedRepos ? `\n - **Multi-repo exception:** this run has related repositories, so the per-repo GENERATE lists are NOT final — they are candidates re-selected by the cross-repo round-robin described in Task 1's "Cross-repo test generation". Register the pooled, type-distributed selection instead of any single repo's GENERATE list — call \`skyramp_register_test_plan\` ONCE for the whole run, with candidates from EVERY repo pooled into one list, including at least one UI candidate when any repo changed frontend files (the tool rejects a pooled registration without one). (In single-repo runs, register the GENERATE list exactly as-is.)` : ""}
412
421
  - **Do not fabricate tests outside the GENERATE list provided by \`skyramp_analyze_changes\`.** Changes that only modify, delete, or add fields to an EXISTING covered endpoint or component are maintenance: handle them in ${taskRef(TASK_ANALYZE_MAINTAIN)} by UPDATE/DELETE of the existing test, never by creating a new spec. If the GENERATE list is empty, create zero new tests and proceed to ${taskRef(TASK_SUBMIT)}.
413
422
  - Scenario JSON files are always new files — always generate them for new methods. Every generated scenario JSON must have a corresponding new integration test generated from it via \`skyramp_integration_test_generation\`.
414
423
  - Covered-resource handling (aligns with Execution Plan Step 0): When a GENERATE item targets a resource that already has an existing test file covering the same endpoint:
@@ -643,7 +652,7 @@ In these cases:
643
652
 
644
653
  ${task3CountRule}
645
654
 
646
- ${reportLanguageBlock}Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}" and \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
655
+ ${reportLanguageBlock}Call \`skyramp_submit_report\` with \`stateFile\` (from \`skyramp_analyze_changes\` output) — the stateFile is required for execution outcome tracking, and the report is written beside it. Field names, types, and formats are defined in the tool's parameter schema — follow them exactly.
647
656
 
648
657
  ${hasRelatedRepos ? `
649
658
  - **MULTI-REPO attribution**: Set the \`repository\` field (\`owner/repo\`) on EVERY \`newTestsCreated\`, \`testResults\`, \`issuesFound\`, and \`additionalRecommendations\` item — including items about the PRIMARY repo — so each finding is unambiguously attributed. The primary repo's \`repository\` is \`${primaryRepo || "<the primary repo's owner/repo>"}\`; items derived from a related repo's diff (from the \`<related_repositories>\` analysis) carry that repo's \`repository\` value. In \`businessCaseAnalysis\`, include a short per-repo subsection and call out any cross-repo correlations you found.
@@ -652,13 +661,10 @@ ${hasRelatedRepos ? `
652
661
  - For \`testType: "contract"\` entries: **\`primaryEndpoint\` is required** (e.g. \`"GET /api/v1/users/{user_id}"\`). The tool will reject the submission without it — do not omit it or you will be forced to resubmit.
653
662
 
654
663
  ${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modularize: false })}`;
655
- // TODO: merge stateFile and summaryOutputFile into a single file (follow-up PR).
656
- // Currently the stateFile path is derived server-side from RUNNER_TEMP (GitHub Actions only);
657
- // other CI systems (Jenkins, GitLab, Buildkite) require the caller to set RUNNER_TEMP.
658
- // Once merged, the path flows through summaryOutputFile already testbot-controlled and
659
- // CI-agnostic — eliminating the RUNNER_TEMP dependency and the LLM confusion that motivated
660
- // removing stateOutputFile from the prompt schema. Remove the RUNNER_TEMP branch in
661
- // AnalysisStateManager.ts when this is done.
664
+ // Neither path reaches the agent any more: SKYR-4147 made the report derive from the
665
+ // state file's directory, and that directory comes from the environment via
666
+ // runArtifactDir(). Keep it that way a path the model retypes out of this prose is a
667
+ // path the model can corrupt, which is the defect SKYR-4147 fixed.
662
668
  }
663
669
  export function buildWorkspaceRecoveryPrefix(repositoryPath) {
664
670
  return `IMPORTANT: The existing .skyramp/workspace.yml failed to parse or validate. Before proceeding with any tasks below, you MUST call skyramp_init_scan with workspacePath "${repositoryPath}" and force: true, then call skyramp_init_workspace with workspacePath "${repositoryPath}", the discovered services, scanToken, and force: true to regenerate the workspace file.\n\n`;
@@ -670,9 +676,6 @@ export function registerTestbotPrompt(server) {
670
676
  argsSchema: {
671
677
  prTitle: z.string().describe("Pull request title"),
672
678
  prDescription: z.string().describe("Pull request description/body"),
673
- summaryOutputFile: z
674
- .string()
675
- .describe("File path where the agent should write the testbot summary report"),
676
679
  repositoryPath: z
677
680
  .string()
678
681
  .default(".")
@@ -737,7 +740,6 @@ export function registerTestbotPrompt(server) {
737
740
  let prompt = getTestbotPrompt({
738
741
  prTitle: args.prTitle,
739
742
  prDescription: args.prDescription,
740
- summaryOutputFile: args.summaryOutputFile,
741
743
  repositoryPath: args.repositoryPath,
742
744
  baseBranch: args.baseBranch,
743
745
  maxRecommendations: args.maxRecommendations,
@@ -27,5 +27,11 @@ export interface DiscriminatorValidation {
27
27
  * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
28
28
  * occurring verbatim in `diffText` — grounds the claim in the real change.
29
29
  * 2. The `kind`-specific structural predicate must hold over `steps[]`.
30
+ *
31
+ * `reposWithoutDiff` names repos in the run whose section carries no branch
32
+ * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
33
+ * An anchor those repos' changes could contain cannot be disproven, so an
34
+ * unmatched anchor then fails with a "could not be checked" reason instead of
35
+ * the "not grounded" one — still unverified (no boost), but distinguishable.
30
36
  */
31
- export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string): DiscriminatorValidation;
37
+ export declare function validateDiscriminator(scenario: DraftedScenario, declared: DiscriminatorClaim, diffText: string, reposWithoutDiff?: string[]): DiscriminatorValidation;
@@ -14,10 +14,16 @@ const MIN_NEAR_MISS_LENGTH = 3;
14
14
  * 1. `changedCodeAnchor` must be a non-trivial string (>= 8 chars after trim)
15
15
  * occurring verbatim in `diffText` — grounds the claim in the real change.
16
16
  * 2. The `kind`-specific structural predicate must hold over `steps[]`.
17
+ *
18
+ * `reposWithoutDiff` names repos in the run whose section carries no branch
19
+ * diff (e.g. a related repo that fell back to its default branch, SKYR-4203).
20
+ * An anchor those repos' changes could contain cannot be disproven, so an
21
+ * unmatched anchor then fails with a "could not be checked" reason instead of
22
+ * the "not grounded" one — still unverified (no boost), but distinguishable.
17
23
  */
18
- export function validateDiscriminator(scenario, declared, diffText) {
24
+ export function validateDiscriminator(scenario, declared, diffText, reposWithoutDiff = []) {
19
25
  try {
20
- const anchorResult = verifyAnchor(declared?.changedCodeAnchor, diffText);
26
+ const anchorResult = verifyAnchor(declared?.changedCodeAnchor, diffText, reposWithoutDiff);
21
27
  if (!anchorResult.verified)
22
28
  return anchorResult;
23
29
  const steps = Array.isArray(scenario?.steps) ? scenario.steps : [];
@@ -43,7 +49,7 @@ export function validateDiscriminator(scenario, declared, diffText) {
43
49
  }
44
50
  }
45
51
  // ── Anchor grounding ──
46
- function verifyAnchor(rawAnchor, diffText) {
52
+ function verifyAnchor(rawAnchor, diffText, reposWithoutDiff = []) {
47
53
  const anchor = typeof rawAnchor === "string" ? rawAnchor.trim() : "";
48
54
  if (anchor.length < MIN_ANCHOR_LENGTH) {
49
55
  return {
@@ -57,6 +63,13 @@ function verifyAnchor(rawAnchor, diffText) {
57
63
  // text — compare whitespace-collapsed forms with diff markers stripped, or
58
64
  // legitimate multi-line anchors are systematically false-demoted.
59
65
  if (!diff.includes(anchor) && !normalizeForAnchorMatch(diff).includes(normalizeForAnchorMatch(anchor))) {
66
+ if (reposWithoutDiff.length > 0) {
67
+ return {
68
+ verified: false,
69
+ reason: `changedCodeAnchor "${anchor}" was not found in the available PR diffs, and no branch diff exists for ` +
70
+ `${reposWithoutDiff.join(", ")} (likely checked out on the default branch), so the claim could not be checked against those changes.`,
71
+ };
72
+ }
60
73
  return {
61
74
  verified: false,
62
75
  reason: `changedCodeAnchor "${anchor}" does not occur verbatim in the PR diff, so the discriminator claim is not grounded in the actual change.`,
@@ -29,7 +29,6 @@ export function registerTestbotResource(server) {
29
29
  const prompt = getTestbotPrompt({
30
30
  prTitle: param("prTitle", ""),
31
31
  prDescription: param("prDescription", ""),
32
- summaryOutputFile: param("summaryOutputFile", ""),
33
32
  repositoryPath,
34
33
  baseBranch: uri.searchParams.get("baseBranch") || undefined,
35
34
  maxRecommendations: isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec,
@@ -2,6 +2,7 @@ import { AUTH_PLACEHOLDER_TOKEN } from "../types/TestTypes.js";
2
2
  import { isAuthorizationHeaderName } from "../utils/workspaceAuth.js";
3
3
  import { inferExpectedStatus } from "../utils/httpDefaults.js";
4
4
  import { logger } from "../utils/logger.js";
5
+ import { deriveBasePath } from "../utils/urlPath.js";
5
6
  // Keys that trigger built-in prototype setters when used as bracket-notation
6
7
  // property names on a plain object — guard against prototype pollution from
7
8
  // LLM-controlled or user-controlled JSON input.
@@ -22,7 +23,10 @@ export class ScenarioGenerationService {
22
23
  let destination = params.destination;
23
24
  let scheme = "https";
24
25
  let port = 443;
25
- let basePath = "";
26
+ // Shared with generateBatchScenarioRestTool, which must predict this exact
27
+ // prefix to match workspace queryParamOverrides against the path this
28
+ // method will emit below (SKYR-4127). Keep it derived in one place.
29
+ const basePath = deriveBasePath(params.baseURL);
26
30
  if (params.baseURL) {
27
31
  try {
28
32
  const parsed = new URL(params.baseURL);
@@ -33,7 +37,6 @@ export class ScenarioGenerationService {
33
37
  : scheme === "https"
34
38
  ? 443
35
39
  : 80;
36
- basePath = parsed.pathname.replace(/\/$/, "");
37
40
  }
38
41
  catch {
39
42
  logger.warning("Could not parse baseURL, using destination param", {
@@ -9,6 +9,8 @@ import { logger } from "../utils/logger.js";
9
9
  import { TestExecutionStatus, } from "../types/TestExecution.js";
10
10
  import { TestType } from "../types/TestTypes.js";
11
11
  import { buildContainerEnv } from "./containerEnv.js";
12
+ import { getWorkspaceScopedQueryParams } from "../utils/workspaceAuth.js";
13
+ import { extractEnvVarNames } from "../workspace/queryParamResolution.js";
12
14
  import { EXECUTOR_DOCKER_IMAGE } from "../utils/versions.js";
13
15
  import { walkDir } from "../utils/fileWalk.js";
14
16
  export { EXECUTOR_DOCKER_IMAGE };
@@ -817,7 +819,29 @@ export class TestExecutionService {
817
819
  mountedPaths.add(saveStorageTarget);
818
820
  }
819
821
  }
820
- const env = buildContainerEnv(options, saveStorageTargetPath);
822
+ // Forward the environment variables this workspace's query params declare
823
+ // via `env.VAR` (SKYR-4127). The references resolve where the test runs, so
824
+ // without this they resolve to nothing inside the container and the query
825
+ // param is dropped from the request silently.
826
+ //
827
+ // getWorkspaceScopedQueryParams already returns undefined on a read or
828
+ // parse failure, so those degrade quietly to the pre-existing behavior.
829
+ // This guard covers only an unexpected throw, and must not fail an
830
+ // otherwise valid execution.
831
+ let passthroughNames = [];
832
+ try {
833
+ const scoped = await getWorkspaceScopedQueryParams(workspacePath);
834
+ if (scoped) {
835
+ passthroughNames = extractEnvVarNames(scoped);
836
+ if (passthroughNames.length > 0) {
837
+ logger.debug(`Forwarding workspace-declared environment variables to the executor: ${passthroughNames.join(", ")}`);
838
+ }
839
+ }
840
+ }
841
+ catch (err) {
842
+ logger.warning(`Could not resolve workspace environment passthrough: ${err instanceof Error ? err.message : String(err)}`);
843
+ }
844
+ const env = buildContainerEnv(options, saveStorageTargetPath, process.env, passthroughNames);
821
845
  // Capture output
822
846
  let output = "";
823
847
  class DockerStream extends Writable {
@@ -2,7 +2,8 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { SkyrampClient } from "@skyramp/skyramp";
4
4
  import { analyzeOpenAPIWithGivenEndpoint } from "../utils/analyze-openapi.js";
5
- import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, resolveAuthFromWorkspace, getWorkspaceSkipTLSVerify, getWorkspaceDefaultQueryParams, mergeQueryParamsString, } from "../utils/workspaceAuth.js";
5
+ import { isAuthorizationHeaderName, KNOWN_AUTH_HEADERS, resolveAuthFromWorkspace, getWorkspaceSkipTLSVerify, getWorkspaceScopedQueryParams, mergeQueryParamsString, } from "../utils/workspaceAuth.js";
6
+ import { extractPathname, resolveQueryParamsForPath, contributesNothing } from "../workspace/queryParamResolution.js";
6
7
  import { getPathParameterValidationError, OUTPUT_DIR_FIELD_NAME, PATH_PARAMS_FIELD_NAME, QUERY_PARAMS_FIELD_NAME, FORM_PARAMS_FIELD_NAME, validateParams, validatePath, validateRequestData, } from "../utils/utils.js";
7
8
  import { getEntryPoint } from "../utils/telemetry.js";
8
9
  import { getLanguageSteps } from "../utils/language-helper.js";
@@ -364,17 +365,31 @@ The generated test file remains unchanged and ready to use as-is.
364
365
  logger.warning("Could not resolve skipTLSVerify from workspace config");
365
366
  }
366
367
  }
367
- // Workspace-declared default query params (SKYR-4050): api.defaultQueryParams
368
- // are attached to every generated request for the service. Existing
369
- // queryParams entries (explicit caller values) always win on a key collision.
368
+ // Workspace-declared query params (SKYR-4050, path-scoped in SKYR-4127):
369
+ // api.defaultQueryParams is the base map; api.queryParamOverrides layers
370
+ // the most-specific matching pathPattern on top. Existing queryParams
371
+ // entries (explicit caller values) always win on a key collision.
370
372
  if (!SKIP_DEFAULT_QUERY_PARAMS_TEST_TYPES.has(this.getTestType())) {
371
373
  try {
372
374
  const repoPath = generateOptions.outputDir || process.cwd();
373
- const defaults = await getWorkspaceDefaultQueryParams(repoPath);
374
- if (defaults) {
375
- generateOptions.queryParams = mergeQueryParamsString(generateOptions.queryParams, defaults);
376
- logger.info("Merged workspace defaultQueryParams into queryParams", {
377
- keys: Object.keys(defaults),
375
+ const scoped = await getWorkspaceScopedQueryParams(repoPath);
376
+ // Skip the merge entirely when the resolved config contributes
377
+ // nothing otherwise an empty/null defaultQueryParams declaration
378
+ // with no overrides would set queryParams to "" where the old code
379
+ // left the field untouched (Finding 4, SKYR-4127 final review).
380
+ if (scoped && !contributesNothing(scoped)) {
381
+ const pathname = extractPathname(generateOptions.uri);
382
+ if (!pathname && scoped.overrides.length > 0) {
383
+ logger.warning("queryParamOverrides configured but this generation call has no single endpoint path; " +
384
+ "applying api.defaultQueryParams only", { patterns: scoped.overrides.map((o) => o.pathPattern) });
385
+ }
386
+ const effective = pathname
387
+ ? resolveQueryParamsForPath(scoped, pathname)
388
+ : { ...scoped.base };
389
+ generateOptions.queryParams = mergeQueryParamsString(generateOptions.queryParams, effective);
390
+ logger.info("Merged workspace query params into queryParams", {
391
+ keys: Object.keys(effective),
392
+ pathname,
378
393
  });
379
394
  }
380
395
  }
@@ -8,7 +8,18 @@ import { TestExecutionOptions } from "../types/TestExecution.js";
8
8
  * host.docker.internal is mapped via ExtraHosts in TestExecutionService.
9
9
  */
10
10
  export declare function rewriteLocalhostForDocker(url: string): string;
11
+ /** Why a name is refused, or null when it is allowed. The two reasons need
12
+ * different messages: a reserved name is already provided by the executor, so
13
+ * the declaration is only redundant, while a credential name is refused. */
14
+ export declare function passthroughDenyReason(name: string): "reserved" | "credential" | null;
15
+ /** True when a name is refused regardless of who declared it. */
16
+ export declare function isDeniedPassthroughName(name: string): boolean;
17
+ /** A name is interpolated directly into a Docker `Env` entry, which is a plain
18
+ * `NAME=value` string. A name holding `=`, whitespace or a newline produces a
19
+ * malformed entry and can inject a second variable. workspace.yml supplies
20
+ * these names, so `env.FOO=BAR` would yield the name `FOO=BAR`. */
21
+ export declare function isValidEnvVarName(name: string): boolean;
11
22
  /**
12
23
  * Build the environment variable array for the Docker executor container.
13
24
  */
14
- export declare function buildContainerEnv(options: Pick<TestExecutionOptions, "token" | "language" | "useHostNetwork">, saveStoragePath?: string, hostEnv?: Record<string, string | undefined>): string[];
25
+ export declare function buildContainerEnv(options: Pick<TestExecutionOptions, "token" | "language" | "useHostNetwork">, saveStoragePath?: string, hostEnv?: Record<string, string | undefined>, passthroughNames?: string[]): string[];