@skyramp/mcp 0.3.2 → 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/index.js +47 -2
- package/build/playwright/PlaywrightTraceService.d.ts +8 -0
- package/build/playwright/PlaywrightTraceService.js +1 -0
- package/build/playwright/registerPlaywrightTools.js +42 -1
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +19 -0
- package/build/prompts/pom-aware-code-reuse.js +17 -8
- 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.d.ts +2 -12
- package/build/prompts/testbot/testbot-prompts.js +28 -16
- package/build/recommendation/discriminators.d.ts +7 -1
- package/build/recommendation/discriminators.js +16 -3
- package/build/resources/testbotResource.js +20 -4
- package/build/services/ScenarioGenerationService.js +5 -2
- package/build/services/TestExecutionService.d.ts +13 -8
- package/build/services/TestExecutionService.js +73 -26
- package/build/services/TestGenerationService.js +24 -9
- package/build/services/containerEnv.d.ts +12 -1
- package/build/services/containerEnv.js +118 -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/queryProxyMocksTool.js +0 -1
- package/build/tools/submitReportTool.d.ts +83 -10
- package/build/tools/submitReportTool.js +179 -29
- package/build/tools/test-management/actionsTool.js +52 -41
- 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/TestAnalysis.d.ts +7 -3
- package/build/types/TestExecution.d.ts +14 -0
- package/build/types/TestTypes.js +3 -2
- package/build/types/TestbotPromptOptions.d.ts +34 -0
- package/build/types/TestbotPromptOptions.js +1 -0
- package/build/types/TestbotReport.d.ts +10 -0
- package/build/types/TestbotReport.js +10 -1
- package/build/types/index.d.ts +2 -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/frontendSelectors.js +0 -1
- package/build/utils/gitStaging.d.ts +5 -0
- package/build/utils/gitStaging.js +1 -1
- package/build/utils/pom-catalog.d.ts +23 -0
- package/build/utils/pom-catalog.js +30 -0
- package/build/utils/pom-scope/pom-files.d.ts +14 -0
- package/build/utils/pom-scope/pom-files.js +32 -6
- package/build/utils/pom-scope/testIdDiscovery.d.ts +40 -0
- package/build/utils/pom-scope/testIdDiscovery.js +104 -0
- package/build/utils/reportVerification.d.ts +7 -2
- package/build/utils/reportVerification.js +9 -3
- 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/node_modules/playwright/ThirdPartyNotices.txt +319 -266
- package/node_modules/playwright/lib/dom-analyzer/blueprint.js +154 -27
- package/node_modules/playwright/lib/dom-analyzer/crawler.js +2 -2
- package/node_modules/playwright/lib/mcp/browser/tools/pageBlueprint.js +1 -1
- package/node_modules/playwright/lib/mcp/browser/tools/sitemap.js +6 -2
- package/node_modules/playwright/lib/mcp/skyramp/loadTraceTool.js +3 -2
- package/node_modules/playwright/lib/mcp/skyramp/resultCode.js +4 -3
- package/node_modules/playwright/lib/mcp/skyramp/skyRampImport.js +15 -3
- package/node_modules/playwright/lib/mcp/skyramp/specImport.js +781 -0
- package/node_modules/playwright/lib/mcp/skyramp/traceRecordingBackend.js +14 -3
- package/node_modules/playwright/lib/mcp/test/resultCode.test.js +2 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.js +1 -1
- package/node_modules/playwright/lib/mcp/test/skyRampExport.test.js +30 -0
- package/node_modules/playwright/lib/transform/babelBundleImpl.js +200 -199
- package/node_modules/playwright/node_modules/playwright-core/ThirdPartyNotices.txt +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/.package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/aws-lambda/handler.js +16 -25
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/bun/websocket.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/adapter/lambda-edge/handler.js +20 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/base.js +8 -14
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/components.js +41 -21
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/context.js +131 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/jsx/streaming.js +9 -7
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/cjs/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/client.js +10 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/client/utils.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/common.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/helper/css/index.js +9 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/base.js +15 -15
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/components.js +42 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/context.js +129 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/jsx/streaming.js +10 -8
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/bearer-auth/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cache/index.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/compress/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/cors/index.js +2 -5
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/etag/index.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/language/language.js +10 -32
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/method-override/index.js +5 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/serve-static/index.js +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/middleware/timing/timing.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/router/trie-router/node.js +9 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/aws-lambda/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/adapter/lambda-edge/handler.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/helper/websocket/index.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/base.d.ts +1 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/jsx/context.d.ts +39 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/context-storage/index.d.ts +2 -2
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/middleware/language/language.d.ts +18 -0
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/body.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/types/utils/types.d.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/body.js +12 -4
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/buffer.js +2 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/utils/ipaddr.js +6 -1
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/validator/validator.js +3 -3
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/package.json +29 -22
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/package-lock.json +3 -3
- package/node_modules/playwright/node_modules/playwright-core/lib/generated/pollingRecorderSource.js +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/jsonlReader.js +5 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/server/codegen/skyramp/specReader.js +781 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/recorderApp.js +25 -6
- package/node_modules/playwright/node_modules/playwright-core/lib/server/recorder/skyramp/replayEngine.js +3 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/utils.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/htmlReport/index.html +253 -27
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/{codeMirrorModule-D0BjbCb7.js → codeMirrorModule-DtudTj_v.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-BpDwp16L.js +422 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/index.html +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/{codeMirrorModule-Bzd72-bG.js → codeMirrorModule-FNMuBzX1.js} +1 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-Co9upU5h.js +1035 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.DXNIQ_dx.js +2 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.CIKB3XSv.js +5 -0
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +2 -2
- package/node_modules/playwright/node_modules/playwright-core/package.json +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/generated/pollingRecorderSource.ts +1 -1
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/jsonlReader.ts +4 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/codegen/skyramp/specReader.ts +1028 -0
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/recorderApp.ts +31 -8
- package/node_modules/playwright/node_modules/playwright-core/src/server/recorder/skyramp/replayEngine.ts +1 -0
- package/node_modules/playwright/node_modules/playwright-core/src/utils.ts +1 -0
- package/node_modules/playwright/package.json +1 -1
- package/package.json +10 -6
- package/node_modules/playwright/node_modules/playwright-core/bundles/mcp/node_modules/hono/dist/tsconfig.build.tsbuildinfo +0 -1
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/recorder/assets/index-lvTRGFx-.js +0 -193
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-DzxTioTK.js +0 -809
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/index.BGc30U3S.js +0 -2
- package/node_modules/playwright/node_modules/playwright-core/lib/vite/traceViewer/uiMode.IaDrb29A.js +0 -5
|
@@ -2,7 +2,7 @@ import { TestExecutionResult, BatchExecutionResult, TestExecutionOptions, Progre
|
|
|
2
2
|
import { EXECUTOR_DOCKER_IMAGE } from "../utils/versions.js";
|
|
3
3
|
export { EXECUTOR_DOCKER_IMAGE };
|
|
4
4
|
export declare const PLAYWRIGHT_CONFIG_FILES: string[];
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const MOUNT_SCAN_SKIP_DIRS: string[];
|
|
6
6
|
export declare const TEST_REPO_CONTAINER_SUBDIR = ".skyramp-test-repo";
|
|
7
7
|
export declare const MOUNT_NULL_ITEMS: string[];
|
|
8
8
|
/**
|
|
@@ -38,18 +38,23 @@ export declare function detectSessionFiles(testFilePath: string): string[];
|
|
|
38
38
|
*/
|
|
39
39
|
export declare function findExcludedPaths(dir: string, excludedItems: string[]): string[];
|
|
40
40
|
/**
|
|
41
|
-
* Find every
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* Find every `node_modules` directory under `dir`, including the top-level
|
|
42
|
+
* `<dir>/node_modules`. Never descends into a `node_modules`. Used to shadow
|
|
43
|
+
* SHADOWED_NESTED_PACKAGES inside each one: a repo-local `@playwright/test`
|
|
44
|
+
* becomes a second physical copy and trips Playwright's "Requiring
|
|
45
|
+
* @playwright/test second time" guard.
|
|
46
|
+
*
|
|
47
|
+
* The top-level one is deliberately NOT special-cased (SKYR-4140). When
|
|
48
|
+
* `workspacePath` is itself an npm package root — the standard `tests/<suite>/`
|
|
49
|
+
* layout — its node_modules IS the only one, so skipping it left the shadow
|
|
50
|
+
* machinery with nothing to act on and no customer dependency resolvable.
|
|
51
|
+
*
|
|
47
52
|
* Skips `.git` (nothing importable lives there) and does not follow symlinks —
|
|
48
53
|
* by design (a symlinked nested node_modules either dangles in-container,
|
|
49
54
|
* falling through to NODE_PATH's global copy, or its real-path target is
|
|
50
55
|
* walked directly).
|
|
51
56
|
*/
|
|
52
|
-
export declare function
|
|
57
|
+
export declare function findNodeModulesDirs(dir: string): string[];
|
|
53
58
|
export declare class TestExecutionService {
|
|
54
59
|
private docker;
|
|
55
60
|
private imageReady;
|
|
@@ -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 };
|
|
@@ -54,8 +56,11 @@ export const PLAYWRIGHT_CONFIG_FILES = [
|
|
|
54
56
|
"playwright.config.js",
|
|
55
57
|
"playwright.config.mjs",
|
|
56
58
|
];
|
|
57
|
-
// Directories
|
|
58
|
-
|
|
59
|
+
// Directories the recursive shadow scans must never descend into. node_modules
|
|
60
|
+
// IS mounted (see mountRepoTree) — but walking it would shadow every package's
|
|
61
|
+
// own package.json with empty JSON via MOUNT_NULL_ITEMS, breaking resolution
|
|
62
|
+
// for the whole tree.
|
|
63
|
+
export const MOUNT_SCAN_SKIP_DIRS = [
|
|
59
64
|
"node_modules",
|
|
60
65
|
];
|
|
61
66
|
// Reserved container subdir under /home/user where a cross-repo test repository
|
|
@@ -354,7 +359,7 @@ export function detectSessionFiles(testFilePath) {
|
|
|
354
359
|
*/
|
|
355
360
|
export function findExcludedPaths(dir, excludedItems) {
|
|
356
361
|
const results = [];
|
|
357
|
-
const shouldSkipDir = (entry) => excludedItems.includes(entry.name) ||
|
|
362
|
+
const shouldSkipDir = (entry) => excludedItems.includes(entry.name) || MOUNT_SCAN_SKIP_DIRS.includes(entry.name);
|
|
358
363
|
for (const [entry, fullPath] of walkDir(dir, { shouldSkipDir })) {
|
|
359
364
|
// Only shadow files — mounting /dev/null to a directory target causes Docker errors
|
|
360
365
|
if (excludedItems.includes(entry.name)) {
|
|
@@ -364,18 +369,23 @@ export function findExcludedPaths(dir, excludedItems) {
|
|
|
364
369
|
return results;
|
|
365
370
|
}
|
|
366
371
|
/**
|
|
367
|
-
* Find every
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
*
|
|
372
|
-
*
|
|
372
|
+
* Find every `node_modules` directory under `dir`, including the top-level
|
|
373
|
+
* `<dir>/node_modules`. Never descends into a `node_modules`. Used to shadow
|
|
374
|
+
* SHADOWED_NESTED_PACKAGES inside each one: a repo-local `@playwright/test`
|
|
375
|
+
* becomes a second physical copy and trips Playwright's "Requiring
|
|
376
|
+
* @playwright/test second time" guard.
|
|
377
|
+
*
|
|
378
|
+
* The top-level one is deliberately NOT special-cased (SKYR-4140). When
|
|
379
|
+
* `workspacePath` is itself an npm package root — the standard `tests/<suite>/`
|
|
380
|
+
* layout — its node_modules IS the only one, so skipping it left the shadow
|
|
381
|
+
* machinery with nothing to act on and no customer dependency resolvable.
|
|
382
|
+
*
|
|
373
383
|
* Skips `.git` (nothing importable lives there) and does not follow symlinks —
|
|
374
384
|
* by design (a symlinked nested node_modules either dangles in-container,
|
|
375
385
|
* falling through to NODE_PATH's global copy, or its real-path target is
|
|
376
386
|
* walked directly).
|
|
377
387
|
*/
|
|
378
|
-
export function
|
|
388
|
+
export function findNodeModulesDirs(dir) {
|
|
379
389
|
const results = [];
|
|
380
390
|
const walk = (current) => {
|
|
381
391
|
let entries;
|
|
@@ -392,8 +402,7 @@ export function findNestedNodeModules(dir) {
|
|
|
392
402
|
continue;
|
|
393
403
|
const full = path.join(current, entry.name);
|
|
394
404
|
if (entry.name === "node_modules") {
|
|
395
|
-
|
|
396
|
-
results.push(full); // skip the top-level node_modules
|
|
405
|
+
results.push(full);
|
|
397
406
|
continue; // never descend into a node_modules
|
|
398
407
|
}
|
|
399
408
|
walk(full);
|
|
@@ -593,17 +602,16 @@ export class TestExecutionService {
|
|
|
593
602
|
hostConfig.Mounts.push(mount);
|
|
594
603
|
};
|
|
595
604
|
// Mount one repo tree (the workspace, or a cross-repo test repository) into
|
|
596
|
-
// the container
|
|
605
|
+
// the container.
|
|
597
606
|
//
|
|
598
607
|
// Each top-level entry is bind-mounted at the canonical container path and —
|
|
599
608
|
// when mirrorToHost is set — at its host-absolute path too. The dual mount
|
|
600
609
|
// lets the test resolve any absolute reference the codegen happens to embed
|
|
601
610
|
// (storageState, fixture paths, snapshots) — including the host workspace
|
|
602
611
|
// path that Playwright's TypeScript loader sometimes produces from
|
|
603
|
-
// `__dirname` — without needing source-code detection.
|
|
604
|
-
// (
|
|
605
|
-
//
|
|
606
|
-
// PLAYWRIGHT_CONFIG_FILES shadows (minimal config) are applied at both
|
|
612
|
+
// `__dirname` — without needing source-code detection. MOUNT_NULL_ITEMS
|
|
613
|
+
// shadows (package.json → empty JSON, etc.), node_modules package shadows,
|
|
614
|
+
// and PLAYWRIGHT_CONFIG_FILES shadows (minimal config) are applied at both
|
|
607
615
|
// targets too so the protections survive regardless of which path the test
|
|
608
616
|
// resolves to.
|
|
609
617
|
//
|
|
@@ -611,7 +619,7 @@ export class TestExecutionService {
|
|
|
611
619
|
// later mounts over earlier ones, so the top-level entry mounts must be
|
|
612
620
|
// pushed BEFORE the shadow mounts that override paths inside them.
|
|
613
621
|
//
|
|
614
|
-
// The three recursive scans (findExcludedPaths × 2 +
|
|
622
|
+
// The three recursive scans (findExcludedPaths × 2 + findNodeModulesDirs)
|
|
615
623
|
// are deliberately kept as separate walks rather than fused into one pass:
|
|
616
624
|
// each skips node_modules/.git so the cost is small, and separate walks keep
|
|
617
625
|
// each shadow category's semantics independently testable.
|
|
@@ -623,11 +631,13 @@ export class TestExecutionService {
|
|
|
623
631
|
// otherwise the entry mount would win the dedup race and the repo's own
|
|
624
632
|
// config (which may import dotenv, or simply isn't the minimal/generated
|
|
625
633
|
// one) would leak into the container.
|
|
634
|
+
//
|
|
635
|
+
// node_modules is NOT filtered out (SKYR-4140): it carries the customer's
|
|
636
|
+
// test-support deps, and the per-package shadows below neutralise the only
|
|
637
|
+
// copies that must not load from the repo.
|
|
626
638
|
const entriesToMount = fs
|
|
627
639
|
.readdirSync(hostRoot)
|
|
628
|
-
.filter((file) => !
|
|
629
|
-
!MOUNT_NULL_ITEMS.includes(file) &&
|
|
630
|
-
!PLAYWRIGHT_CONFIG_FILES.includes(file));
|
|
640
|
+
.filter((file) => !MOUNT_NULL_ITEMS.includes(file) && !PLAYWRIGHT_CONFIG_FILES.includes(file));
|
|
631
641
|
for (const file of entriesToMount) {
|
|
632
642
|
const source = path.join(hostRoot, file);
|
|
633
643
|
pushMount({ Type: "bind", Target: path.join(containerRoot, file), Source: source });
|
|
@@ -644,10 +654,9 @@ export class TestExecutionService {
|
|
|
644
654
|
pushMount({ Type: "bind", Source: source, Target: absolutePath });
|
|
645
655
|
}
|
|
646
656
|
}
|
|
647
|
-
// Shadow SHADOWED_NESTED_PACKAGES inside every
|
|
648
|
-
//
|
|
649
|
-
|
|
650
|
-
for (const nested of findNestedNodeModules(hostRoot)) {
|
|
657
|
+
// Shadow SHADOWED_NESTED_PACKAGES inside every mounted node_modules — the
|
|
658
|
+
// repo's own top-level one included (see findNodeModulesDirs).
|
|
659
|
+
for (const nested of findNodeModulesDirs(hostRoot)) {
|
|
651
660
|
for (const pkg of SHADOWED_NESTED_PACKAGES) {
|
|
652
661
|
const pkgPath = path.join(nested, pkg);
|
|
653
662
|
if (!fs.existsSync(pkgPath))
|
|
@@ -711,6 +720,22 @@ export class TestExecutionService {
|
|
|
711
720
|
if (crossRepoTestRoot) {
|
|
712
721
|
mountRepoTree(crossRepoTestRoot, path.join(containerMountPath, TEST_REPO_CONTAINER_SUBDIR), true);
|
|
713
722
|
}
|
|
723
|
+
// runner.sh invokes `npx playwright test`, and npx resolves binaries from
|
|
724
|
+
// <cwd>/node_modules/.bin before PATH — cwd is always containerMountPath. Now
|
|
725
|
+
// that node_modules is mounted (SKYR-4140), a repo-local .bin/playwright would
|
|
726
|
+
// run instead of the image's global CLI. The per-package shadows above do not
|
|
727
|
+
// cover it under a pnpm layout, where .bin/playwright symlinks into .pnpm/
|
|
728
|
+
// rather than into node_modules/playwright. Shadow this one .bin — the only
|
|
729
|
+
// one npx can reach — so binary resolution falls through to PATH as before.
|
|
730
|
+
// Pushed after mountRepoTree so it layers over the node_modules entry mount.
|
|
731
|
+
if (fs.existsSync(path.join(workspacePath, "node_modules", ".bin"))) {
|
|
732
|
+
pushMount({
|
|
733
|
+
Type: "bind",
|
|
734
|
+
Source: EMPTY_DIR_PATH,
|
|
735
|
+
Target: path.join(containerMountPath, "node_modules", ".bin"),
|
|
736
|
+
ReadOnly: true,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
714
739
|
// Mount video output directory for browser tests (writable)
|
|
715
740
|
if (browserTest && videoHostDir) {
|
|
716
741
|
pushMount({
|
|
@@ -794,7 +819,29 @@ export class TestExecutionService {
|
|
|
794
819
|
mountedPaths.add(saveStorageTarget);
|
|
795
820
|
}
|
|
796
821
|
}
|
|
797
|
-
|
|
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);
|
|
798
845
|
// Capture output
|
|
799
846
|
let output = "";
|
|
800
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).
|
|
@@ -44,6 +102,65 @@ export function buildContainerEnv(options, saveStoragePath, hostEnv = process.en
|
|
|
44
102
|
env.push(`${key}=${shouldRewrite ? rewriteLocalhostForDocker(val) : val}`);
|
|
45
103
|
}
|
|
46
104
|
}
|
|
105
|
+
// Credentials that generated UI tests read from the environment instead of
|
|
106
|
+
// carrying as literals (SKYR-3862). Without this the container never sees
|
|
107
|
+
// them and every generated login fails its guard.
|
|
108
|
+
//
|
|
109
|
+
// Forwarded on presence, not truthiness: the generated guard deliberately
|
|
110
|
+
// distinguishes an unset variable (a misconfiguration, fails fast) from one
|
|
111
|
+
// set to "" (valid on forms that accept a blank password). A truthy check —
|
|
112
|
+
// as used for the URL vars above — would drop the empty case and collapse it
|
|
113
|
+
// into the failure case.
|
|
114
|
+
//
|
|
115
|
+
// TODO(SKYR-4110): only ONE credential pair is forwarded, because testbot
|
|
116
|
+
// exports only the first credential from its `uiCredentials` input. That input
|
|
117
|
+
// was designed to carry several, so the agent can pick per test case by
|
|
118
|
+
// authorization role — so a multi-role suite can get a credential its test did
|
|
119
|
+
// not expect, surfacing as a false 403. The fix is a keyed form
|
|
120
|
+
// (SKYRAMP_UI_PASSWORD_<KEY>), which needs no new forwarding logic here: the
|
|
121
|
+
// loop above already forwards SKYRAMP_TEST_SERVICE_URL_* by prefix, so the
|
|
122
|
+
// same approach applies. Keep the presence-not-truthiness rule when it lands.
|
|
123
|
+
for (const key of ["SKYRAMP_UI_USERNAME", "SKYRAMP_UI_PASSWORD"]) {
|
|
124
|
+
const val = hostEnv[key];
|
|
125
|
+
if (val !== undefined) {
|
|
126
|
+
env.push(`${key}=${val}`);
|
|
127
|
+
}
|
|
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
|
+
}
|
|
47
164
|
if (hostEnv.SKYRAMP_DEBUG) {
|
|
48
165
|
env.push(`SKYRAMP_DEBUG=${hostEnv.SKYRAMP_DEBUG}`);
|
|
49
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;
|
|
@@ -11,6 +11,7 @@ import { StateManager, getTestsRepoDir } from "../utils/AnalysisStateManager.js"
|
|
|
11
11
|
import { TestSource } from "../types/TestAnalysis.js";
|
|
12
12
|
import { logger } from "../utils/logger.js";
|
|
13
13
|
import { toolError } from "../utils/utils.js";
|
|
14
|
+
import { recordExecutionVideo } from "./execution-video-state.js";
|
|
14
15
|
const TOOL_NAME = "skyramp_execute_test";
|
|
15
16
|
export const CONTRACT_EXECUTION_MODES = ["provider", "consumer"];
|
|
16
17
|
/**
|
|
@@ -27,6 +28,17 @@ export function resolveEffectiveToken(unauthenticated, paramToken, envToken) {
|
|
|
27
28
|
export function shouldInjectSkyrampBaseUrl(testType, contractMode) {
|
|
28
29
|
return testType !== TestType.CONTRACT || contractMode !== "consumer";
|
|
29
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Append the recorded video path to execution output.
|
|
33
|
+
*
|
|
34
|
+
* Applied to the failure return as well as the success one (SKYR-4156): a video is
|
|
35
|
+
* most useful for a test that just failed, and surfacing it only on success meant a
|
|
36
|
+
* failing UI test reported no recording at all unless Playwright happened to print
|
|
37
|
+
* its own attachment line.
|
|
38
|
+
*/
|
|
39
|
+
export function withVideoInfo(output, videoPath) {
|
|
40
|
+
return videoPath ? `${output}\n\nVideo recording: ${videoPath}` : output;
|
|
41
|
+
}
|
|
30
42
|
export function registerExecuteSkyrampTestTool(server) {
|
|
31
43
|
server.registerTool(TOOL_NAME, {
|
|
32
44
|
description: `Execute a Skyramp-generated test in isolated containerized environments for reliable, deterministic testing. Call this once a test file exists on disk (from a skyramp_*_test_generation tool). First-time execution may take longer while Docker images download — this is expected, not a failure.`,
|
|
@@ -224,23 +236,25 @@ export function registerExecuteSkyrampTestTool(server) {
|
|
|
224
236
|
// Don't fail the tool call if stateFile update fails
|
|
225
237
|
}
|
|
226
238
|
}
|
|
239
|
+
// Record the recording for the report before returning, so it happens on the
|
|
240
|
+
// failure path too (SKYR-4156). skyramp_submit_report reads these records to
|
|
241
|
+
// populate testResults[].videoPath — testbot uploads only the video
|
|
242
|
+
// directories the report references, so an unrecorded video is never seen.
|
|
243
|
+
await recordExecutionVideo(result, params.stateFile);
|
|
227
244
|
// Progress is already reported by TestExecutionService
|
|
228
245
|
// Only report final status if not already at 100%
|
|
229
246
|
if (result.status !== TestExecutionStatus.Pass) {
|
|
230
|
-
errorResult = toolError(`Test execution failed: ${stripVTControlCharacters(result.output || "")}
|
|
247
|
+
errorResult = toolError(withVideoInfo(`Test execution failed: ${stripVTControlCharacters(result.output || "")}
|
|
231
248
|
|
|
232
|
-
If this failed with a 401 status code, ask the user for the authentication token (or where to read it from) and retry — leave the generated test file unchanged
|
|
249
|
+
If this failed with a 401 status code, ask the user for the authentication token (or where to read it from) and retry — leave the generated test file unchanged.`, result.videoPath));
|
|
233
250
|
return errorResult;
|
|
234
251
|
}
|
|
235
252
|
// Success - progress already reported by TestExecutionService
|
|
236
|
-
const videoInfo = result.videoPath
|
|
237
|
-
? `\n\nVideo recording: ${result.videoPath}`
|
|
238
|
-
: "";
|
|
239
253
|
return {
|
|
240
254
|
content: [
|
|
241
255
|
{
|
|
242
256
|
type: "text",
|
|
243
|
-
text: `Test execution result: ${stripVTControlCharacters(result.output || "")}
|
|
257
|
+
text: withVideoInfo(`Test execution result: ${stripVTControlCharacters(result.output || "")}`, result.videoPath),
|
|
244
258
|
},
|
|
245
259
|
],
|
|
246
260
|
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { TestExecutionResult } from "../types/TestExecution.js";
|
|
2
|
+
/**
|
|
3
|
+
* Persistence for the server-derived video recording of a browser test execution,
|
|
4
|
+
* keyed by test-file basename (what `newTestsCreated[].fileName` carries, so
|
|
5
|
+
* `skyramp_submit_report` can match without path normalization).
|
|
6
|
+
*
|
|
7
|
+
* Why this exists (SKYR-4156): testbot uploads only the `.skyramp/videos`
|
|
8
|
+
* directories the submitted report actually references, so a recording nobody
|
|
9
|
+
* references is a recording nobody ever sees. The path is known only inside
|
|
10
|
+
* `skyramp_execute_test`, and asking the agent to carry it across tool calls left
|
|
11
|
+
* the signal prose-mediated — it decayed to zero referenced videos, and every
|
|
12
|
+
* recording of the run was discarded. Recording it here makes the reference
|
|
13
|
+
* server-derived, the same line already drawn for POM reuse and maintenance
|
|
14
|
+
* before/after status.
|
|
15
|
+
*
|
|
16
|
+
* Every write is best-effort. Video observability must never fail an execution
|
|
17
|
+
* that otherwise succeeded, so a missing/unreadable/unwritable state file degrades
|
|
18
|
+
* to "no video link in the report" — the same outcome as running outside a Testbot
|
|
19
|
+
* run, where there is no run state to record into.
|
|
20
|
+
*/
|
|
21
|
+
export declare function recordExecutionVideo(result: TestExecutionResult, explicitStateFile?: string): Promise<void>;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import * as path from "path";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
import { resolveRunStatePath, StateManager, } from "../utils/AnalysisStateManager.js";
|
|
4
|
+
/**
|
|
5
|
+
* Persistence for the server-derived video recording of a browser test execution,
|
|
6
|
+
* keyed by test-file basename (what `newTestsCreated[].fileName` carries, so
|
|
7
|
+
* `skyramp_submit_report` can match without path normalization).
|
|
8
|
+
*
|
|
9
|
+
* Why this exists (SKYR-4156): testbot uploads only the `.skyramp/videos`
|
|
10
|
+
* directories the submitted report actually references, so a recording nobody
|
|
11
|
+
* references is a recording nobody ever sees. The path is known only inside
|
|
12
|
+
* `skyramp_execute_test`, and asking the agent to carry it across tool calls left
|
|
13
|
+
* the signal prose-mediated — it decayed to zero referenced videos, and every
|
|
14
|
+
* recording of the run was discarded. Recording it here makes the reference
|
|
15
|
+
* server-derived, the same line already drawn for POM reuse and maintenance
|
|
16
|
+
* before/after status.
|
|
17
|
+
*
|
|
18
|
+
* Every write is best-effort. Video observability must never fail an execution
|
|
19
|
+
* that otherwise succeeded, so a missing/unreadable/unwritable state file degrades
|
|
20
|
+
* to "no video link in the report" — the same outcome as running outside a Testbot
|
|
21
|
+
* run, where there is no run state to record into.
|
|
22
|
+
*/
|
|
23
|
+
export async function recordExecutionVideo(result, explicitStateFile) {
|
|
24
|
+
if (!result.videoPath)
|
|
25
|
+
return;
|
|
26
|
+
const stateFile = resolveRunStatePath(explicitStateFile);
|
|
27
|
+
if (!stateFile)
|
|
28
|
+
return;
|
|
29
|
+
try {
|
|
30
|
+
const manager = StateManager.fromStatePath(stateFile);
|
|
31
|
+
const data = await manager.readData();
|
|
32
|
+
if (!data)
|
|
33
|
+
return;
|
|
34
|
+
// Last write wins: a retried execution leaves a video directory per attempt,
|
|
35
|
+
// and the report must reference the attempt whose status it reports.
|
|
36
|
+
await manager.writeData({
|
|
37
|
+
...data,
|
|
38
|
+
executionVideos: {
|
|
39
|
+
...data.executionVideos,
|
|
40
|
+
[path.basename(result.testFile)]: {
|
|
41
|
+
videoPath: result.videoPath,
|
|
42
|
+
status: result.status,
|
|
43
|
+
executedAt: result.executedAt,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
logger.warning("Could not record the test video — the report will omit its video link", { testFile: result.testFile, error: String(err) });
|
|
50
|
+
}
|
|
51
|
+
}
|