@skyramp/mcp 0.3.3 → 0.3.4
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/playwright/registerPlaywrightTools.js +42 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
- package/build/prompts/test-maintenance/actionsInstructions.js +2 -2
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +1 -4
- package/build/prompts/test-recommendation/recommendationSections.d.ts +1 -1
- package/build/prompts/test-recommendation/recommendationSections.js +5 -5
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +13 -7
- package/build/prompts/testbot/testbot-prompts.js +6 -13
- package/build/recommendation/discriminators.d.ts +7 -1
- package/build/recommendation/discriminators.js +16 -3
- package/build/resources/testbotResource.js +0 -1
- package/build/services/ScenarioGenerationService.js +5 -2
- package/build/services/TestExecutionService.js +25 -1
- package/build/services/TestGenerationService.js +24 -9
- package/build/services/containerEnv.d.ts +12 -1
- package/build/services/containerEnv.js +94 -1
- package/build/tools/executeSkyrampTestTool.d.ts +9 -0
- package/build/tools/executeSkyrampTestTool.js +20 -6
- package/build/tools/execution-video-state.d.ts +21 -0
- package/build/tools/execution-video-state.js +51 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +31 -11
- package/build/tools/generate-tests/planGuard.d.ts +5 -5
- package/build/tools/generate-tests/planGuard.js +5 -17
- package/build/tools/submitReportTool.d.ts +83 -10
- package/build/tools/submitReportTool.js +169 -28
- package/build/tools/test-management/actionsTool.js +40 -39
- package/build/tools/test-management/analyzeChangesTool.d.ts +11 -0
- package/build/tools/test-management/analyzeChangesTool.js +37 -33
- package/build/tools/test-management/analyzeTestHealthTool.js +3 -3
- package/build/tools/test-management/registerTestPlanTool.js +113 -31
- package/build/types/TestExecution.d.ts +14 -0
- package/build/types/TestTypes.js +3 -2
- package/build/types/TestbotPromptOptions.d.ts +0 -1
- package/build/types/TestbotReport.d.ts +10 -0
- package/build/types/TestbotReport.js +10 -1
- package/build/types/index.d.ts +1 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +36 -2
- package/build/utils/AnalysisStateManager.js +34 -13
- package/build/utils/scenarioDrafting.js +7 -1
- package/build/utils/skyrampMdContent.d.ts +1 -1
- package/build/utils/skyrampMdContent.js +1 -1
- package/build/utils/urlPath.d.ts +37 -0
- package/build/utils/urlPath.js +55 -0
- package/build/utils/utils.d.ts +45 -0
- package/build/utils/utils.js +50 -0
- package/build/utils/versions.d.ts +3 -3
- package/build/utils/versions.js +1 -1
- package/build/utils/workspaceAuth.d.ts +15 -15
- package/build/utils/workspaceAuth.js +32 -17
- package/build/workspace/queryParamResolution.d.ts +93 -0
- package/build/workspace/queryParamResolution.js +201 -0
- package/build/workspace/workspace.d.ts +104 -0
- package/build/workspace/workspace.js +24 -0
- 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
|
-
|
|
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
|
|
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.
|
|
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
|
-
?
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
349
|
-
|
|
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})
|
|
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}
|
|
@@ -55,7 +55,7 @@ export function parseRelatedRepositories(raw) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
export function getTestbotPrompt(opts) {
|
|
58
|
-
const { prTitle, prDescription,
|
|
58
|
+
const { prTitle, prDescription, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, prNumber, userPrompt, services, uiCredentials, testsRepoDir, relatedRepositories, primaryRepo, planOnly = false, language, } = opts;
|
|
59
59
|
// maxCritical is intentionally unused (reserved) — see TestbotPromptOptions.
|
|
60
60
|
const maxGenerate = Math.min(Math.max(opts.maxGenerate ?? MAX_TESTS_TO_GENERATE, 0), maxRecommendations);
|
|
61
61
|
// TODO(SKYR-3636 follow-up): migrate Task 1 + Task 2 step bodies to PromptPlan
|
|
@@ -643,7 +643,7 @@ In these cases:
|
|
|
643
643
|
|
|
644
644
|
${task3CountRule}
|
|
645
645
|
|
|
646
|
-
${reportLanguageBlock}Call \`skyramp_submit_report\` with \`
|
|
646
|
+
${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
647
|
|
|
648
648
|
${hasRelatedRepos ? `
|
|
649
649
|
- **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 +652,10 @@ ${hasRelatedRepos ? `
|
|
|
652
652
|
- 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
653
|
|
|
654
654
|
${getTraceRecordingPromptText({ outputDir: `${repositoryPath}/.skyramp`, modularize: false })}`;
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
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.
|
|
655
|
+
// Neither path reaches the agent any more: SKYR-4147 made the report derive from the
|
|
656
|
+
// state file's directory, and that directory comes from the environment via
|
|
657
|
+
// runArtifactDir(). Keep it that way — a path the model retypes out of this prose is a
|
|
658
|
+
// path the model can corrupt, which is the defect SKYR-4147 fixed.
|
|
662
659
|
}
|
|
663
660
|
export function buildWorkspaceRecoveryPrefix(repositoryPath) {
|
|
664
661
|
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 +667,6 @@ export function registerTestbotPrompt(server) {
|
|
|
670
667
|
argsSchema: {
|
|
671
668
|
prTitle: z.string().describe("Pull request title"),
|
|
672
669
|
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
670
|
repositoryPath: z
|
|
677
671
|
.string()
|
|
678
672
|
.default(".")
|
|
@@ -737,7 +731,6 @@ export function registerTestbotPrompt(server) {
|
|
|
737
731
|
let prompt = getTestbotPrompt({
|
|
738
732
|
prTitle: args.prTitle,
|
|
739
733
|
prDescription: args.prDescription,
|
|
740
|
-
summaryOutputFile: args.summaryOutputFile,
|
|
741
734
|
repositoryPath: args.repositoryPath,
|
|
742
735
|
baseBranch: args.baseBranch,
|
|
743
736
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
368
|
-
//
|
|
369
|
-
//
|
|
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
25
|
+
export declare function buildContainerEnv(options: Pick<TestExecutionOptions, "token" | "language" | "useHostNetwork">, saveStoragePath?: string, hostEnv?: Record<string, string | undefined>, passthroughNames?: string[]): string[];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logger } from "../utils/logger.js";
|
|
1
2
|
/**
|
|
2
3
|
* Rewrite localhost / 127.0.0.1 URLs to host.docker.internal so the Docker
|
|
3
4
|
* executor container can reach services running on the host machine.
|
|
@@ -9,10 +10,67 @@
|
|
|
9
10
|
export function rewriteLocalhostForDocker(url) {
|
|
10
11
|
return url.replace(/^(https?:\/\/)(localhost|127\.0\.0\.1)([:/?#]|$)/, (_, scheme, _host, rest) => `${scheme}host.docker.internal${rest}`);
|
|
11
12
|
}
|
|
13
|
+
/** Names a workspace declaration may never forward. Matching is
|
|
14
|
+
* case-insensitive: `process.env` is case-insensitive on Windows, so a
|
|
15
|
+
* lower-case declaration there returns the real value and would otherwise
|
|
16
|
+
* pass the checks below.
|
|
17
|
+
*
|
|
18
|
+
* Two distinct concerns share one list. The first is CI identity: testbot puts
|
|
19
|
+
* the workflow's GitHub token in the MCP process env so the agent's `gh` calls
|
|
20
|
+
* authenticate, and a declaration naming it would push it into the container
|
|
21
|
+
* and from there into a query string — where the SUT's own access log records
|
|
22
|
+
* it. The second is internal signals the executor sets for itself, which a
|
|
23
|
+
* declaration must not be able to spoof or override.
|
|
24
|
+
*
|
|
25
|
+
* Deliberately NOT a blanket `*_TOKEN`: a token for the customer's own SUT is
|
|
26
|
+
* exactly the kind of value an RBAC query param carries, and refusing it would
|
|
27
|
+
* break the feature this list exists to protect. */
|
|
28
|
+
// Names the executor sets for itself. API_KEY carries the Skyramp LLM key and
|
|
29
|
+
// is forwarded unconditionally by the built-in block below, so a declaration
|
|
30
|
+
// adds nothing. UI Smart LLM features keep working either way.
|
|
31
|
+
const PASSTHROUGH_RESERVED_EXACT = new Set([
|
|
32
|
+
"API_KEY",
|
|
33
|
+
"PYTEST_ADDOPTS",
|
|
34
|
+
"PLAYWRIGHT_SAVE_STORAGE_PATH",
|
|
35
|
+
]);
|
|
36
|
+
const PASSTHROUGH_RESERVED_PREFIXES = ["SKYRAMP_"];
|
|
37
|
+
// CI identity and secret-shaped names. Testbot puts the workflow GitHub token
|
|
38
|
+
// in the MCP process env, and a declaration would send it to the container and
|
|
39
|
+
// into a query string, where the SUT access log records it.
|
|
40
|
+
const PASSTHROUGH_CREDENTIAL_EXACT = new Set(["GITHUB_TOKEN", "GH_TOKEN"]);
|
|
41
|
+
const PASSTHROUGH_CREDENTIAL_PREFIXES = ["ACTIONS_", "AWS_"];
|
|
42
|
+
const PASSTHROUGH_CREDENTIAL_SUBSTRINGS = ["_SECRET", "_PRIVATE_KEY"];
|
|
43
|
+
/** Why a name is refused, or null when it is allowed. The two reasons need
|
|
44
|
+
* different messages: a reserved name is already provided by the executor, so
|
|
45
|
+
* the declaration is only redundant, while a credential name is refused. */
|
|
46
|
+
export function passthroughDenyReason(name) {
|
|
47
|
+
const upper = name.toUpperCase();
|
|
48
|
+
if (PASSTHROUGH_CREDENTIAL_EXACT.has(upper) ||
|
|
49
|
+
PASSTHROUGH_CREDENTIAL_PREFIXES.some((p) => upper.startsWith(p)) ||
|
|
50
|
+
PASSTHROUGH_CREDENTIAL_SUBSTRINGS.some((s) => upper.includes(s))) {
|
|
51
|
+
return "credential";
|
|
52
|
+
}
|
|
53
|
+
if (PASSTHROUGH_RESERVED_EXACT.has(upper) ||
|
|
54
|
+
PASSTHROUGH_RESERVED_PREFIXES.some((p) => upper.startsWith(p))) {
|
|
55
|
+
return "reserved";
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
/** True when a name is refused regardless of who declared it. */
|
|
60
|
+
export function isDeniedPassthroughName(name) {
|
|
61
|
+
return passthroughDenyReason(name) !== null;
|
|
62
|
+
}
|
|
63
|
+
/** A name is interpolated directly into a Docker `Env` entry, which is a plain
|
|
64
|
+
* `NAME=value` string. A name holding `=`, whitespace or a newline produces a
|
|
65
|
+
* malformed entry and can inject a second variable. workspace.yml supplies
|
|
66
|
+
* these names, so `env.FOO=BAR` would yield the name `FOO=BAR`. */
|
|
67
|
+
export function isValidEnvVarName(name) {
|
|
68
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
69
|
+
}
|
|
12
70
|
/**
|
|
13
71
|
* Build the environment variable array for the Docker executor container.
|
|
14
72
|
*/
|
|
15
|
-
export function buildContainerEnv(options, saveStoragePath, hostEnv = process.env) {
|
|
73
|
+
export function buildContainerEnv(options, saveStoragePath, hostEnv = process.env, passthroughNames = []) {
|
|
16
74
|
const env = [
|
|
17
75
|
// Omit entirely when empty so os.getenv() returns None in the test —
|
|
18
76
|
// unauthenticated endpoints won't send an empty auth header (E7).
|
|
@@ -68,6 +126,41 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
|
|
|
68
126
|
env.push(`${key}=${val}`);
|
|
69
127
|
}
|
|
70
128
|
}
|
|
129
|
+
// Environment variables the workspace's query params declare via `env.VAR`
|
|
130
|
+
// (SKYR-4127). These references resolve where the test RUNS — inside this
|
|
131
|
+
// container — so a name that does not reach it resolves to nothing, and an
|
|
132
|
+
// unresolved query param is dropped from the request silently, yielding a
|
|
133
|
+
// passing test that exercised the wrong identity.
|
|
134
|
+
//
|
|
135
|
+
// The list comes from what workspace.yml declared, never from enumerating the
|
|
136
|
+
// ambient environment: nothing is forwarded that the workspace did not name.
|
|
137
|
+
// Forwarded on presence, not truthiness, for the same reason as the
|
|
138
|
+
// credentials above — an empty value is a legitimate declaration.
|
|
139
|
+
const seen = new Set();
|
|
140
|
+
for (const name of passthroughNames) {
|
|
141
|
+
if (seen.has(name))
|
|
142
|
+
continue;
|
|
143
|
+
seen.add(name);
|
|
144
|
+
if (!isValidEnvVarName(name)) {
|
|
145
|
+
logger.warning(`Ignoring ${JSON.stringify(name)} declared in workspace.yml: it is not a valid environment variable name.`);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const denyReason = passthroughDenyReason(name);
|
|
149
|
+
if (denyReason === "reserved") {
|
|
150
|
+
logger.warning(`Ignoring the workspace.yml declaration of ${name}: the executor sets this variable itself. ` +
|
|
151
|
+
`Its value still reaches the test.`);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (denyReason === "credential") {
|
|
155
|
+
logger.warning(`Refusing to forward ${name} to the test executor: the name is credential-shaped. ` +
|
|
156
|
+
`Declare a variable that carries only the value the request needs.`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const val = hostEnv[name];
|
|
160
|
+
if (val !== undefined) {
|
|
161
|
+
env.push(`${name}=${val}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
71
164
|
if (hostEnv.SKYRAMP_DEBUG) {
|
|
72
165
|
env.push(`SKYRAMP_DEBUG=${hostEnv.SKYRAMP_DEBUG}`);
|
|
73
166
|
}
|
|
@@ -10,4 +10,13 @@ export type ContractExecutionMode = (typeof CONTRACT_EXECUTION_MODES)[number];
|
|
|
10
10
|
*/
|
|
11
11
|
export declare function resolveEffectiveToken(unauthenticated?: boolean, paramToken?: string, envToken?: string): string;
|
|
12
12
|
export declare function shouldInjectSkyrampBaseUrl(testType: TestType, contractMode?: ContractExecutionMode): boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Append the recorded video path to execution output.
|
|
15
|
+
*
|
|
16
|
+
* Applied to the failure return as well as the success one (SKYR-4156): a video is
|
|
17
|
+
* most useful for a test that just failed, and surfacing it only on success meant a
|
|
18
|
+
* failing UI test reported no recording at all unless Playwright happened to print
|
|
19
|
+
* its own attachment line.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withVideoInfo(output: string, videoPath?: string): string;
|
|
13
22
|
export declare function registerExecuteSkyrampTestTool(server: McpServer): void;
|