@skyramp/mcp 0.3.2-rc.pom-3 → 0.3.2
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/commands/commandLibrary.d.ts +1 -0
- package/build/commands/commandLibrary.js +19 -13
- package/build/commands/localDevTestChangesCommand.d.ts +15 -0
- package/build/commands/localDevTestChangesCommand.js +201 -0
- package/build/index.js +80 -6
- package/build/prompts/code-reuse.js +3 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.d.ts +1 -0
- package/build/prompts/enhance-assertions/sharedAssertionRules.js +17 -0
- package/build/prompts/initialize-workspace/initializeWorkspacePrompt.js +11 -9
- package/build/prompts/local-dev/local-dev-plan.d.ts +35 -0
- package/build/prompts/local-dev/local-dev-plan.js +429 -0
- package/build/prompts/local-dev/local-dev-prompts.d.ts +4 -0
- package/build/prompts/local-dev/local-dev-prompts.js +190 -0
- package/build/prompts/pom-aware-code-reuse.js +12 -0
- package/build/prompts/prompt-utils.d.ts +8 -0
- package/build/prompts/prompt-utils.js +33 -0
- package/build/prompts/sut-setup/modes/adaptWorkflowPrompt.js +37 -14
- package/build/prompts/sut-setup/shared.js +16 -12
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -29
- package/build/prompts/test-recommendation/scopeAssessment.d.ts +24 -7
- package/build/prompts/test-recommendation/scopeAssessment.js +111 -12
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +41 -4
- package/build/prompts/testbot/testbot-prompts.d.ts +0 -5
- package/build/prompts/testbot/testbot-prompts.js +32 -52
- package/build/recommendation/planRanker.d.ts +15 -2
- package/build/recommendation/planRanker.js +76 -5
- package/build/resources/testbotResource.js +2 -1
- package/build/services/AnalyticsService.d.ts +1 -1
- package/build/services/TestExecutionService.d.ts +2 -1
- package/build/services/TestExecutionService.js +8 -3
- package/build/services/TestGenerationService.d.ts +2 -2
- package/build/services/TestGenerationService.js +39 -21
- package/build/services/containerEnv.js +3 -1
- package/build/tool-phases.js +6 -0
- package/build/tools/code-refactor/codeReuseTool.js +43 -4
- package/build/tools/code-refactor/enhanceAssertionsTool.js +68 -18
- package/build/tools/code-refactor/reuse-outcome.d.ts +109 -0
- package/build/tools/code-refactor/reuse-outcome.js +158 -0
- package/build/tools/code-refactor/reuse-state.d.ts +45 -0
- package/build/tools/code-refactor/reuse-state.js +140 -0
- package/build/tools/enrichTestWithMocksTool.d.ts +28 -0
- package/build/tools/enrichTestWithMocksTool.js +726 -0
- package/build/tools/executeSkyrampTestTool.d.ts +11 -0
- package/build/tools/executeSkyrampTestTool.js +62 -21
- package/build/tools/generate-tests/batchMockGenerationTool.d.ts +106 -0
- package/build/tools/generate-tests/batchMockGenerationTool.js +545 -0
- package/build/tools/generate-tests/generateBatchScenarioRestTool.js +25 -23
- package/build/tools/generate-tests/generateContractRestTool.js +2 -2
- package/build/tools/generate-tests/generateE2ERestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateE2ERestTool.js +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.d.ts +1 -1
- package/build/tools/generate-tests/generateLoadRestTool.js +1 -1
- package/build/tools/generate-tests/generateMockRestTool.d.ts +179 -6
- package/build/tools/generate-tests/generateMockRestTool.js +391 -22
- package/build/tools/generate-tests/loadTestSchema.d.ts +1 -1
- package/build/tools/generate-tests/planGuard.js +2 -22
- package/build/tools/generateEnrichedIntegrationTestTool.d.ts +25 -0
- package/build/tools/generateEnrichedIntegrationTestTool.js +235 -0
- package/build/tools/localDevWorkerComposeTool.d.ts +24 -0
- package/build/tools/localDevWorkerComposeTool.js +264 -0
- package/build/tools/one-click/oneClickTool.d.ts +13 -0
- package/build/tools/one-click/oneClickTool.js +195 -24
- package/build/tools/preflightMockCheckTool.d.ts +2 -0
- package/build/tools/preflightMockCheckTool.js +96 -0
- package/build/tools/queryProxyMocksTool.d.ts +70 -0
- package/build/tools/queryProxyMocksTool.js +522 -0
- package/build/tools/runExistingTestsTool.d.ts +31 -0
- package/build/tools/runExistingTestsTool.js +214 -13
- package/build/tools/submitReportTool.js +38 -3
- package/build/tools/test-management/analyzeChangesTool.d.ts +2 -1
- package/build/tools/test-management/analyzeChangesTool.js +63 -40
- package/build/tools/test-management/registerTestPlanTool.js +55 -7
- package/build/tools/trace/startTraceCollectionTool.js +3 -3
- package/build/types/FrontendIntegration.d.ts +3 -0
- package/build/types/FrontendIntegration.js +3 -0
- package/build/types/OneClickCommands.d.ts +1 -1
- package/build/types/Recommendation.d.ts +20 -0
- package/build/types/Recommendation.js +32 -6
- package/build/types/RepositoryAnalysis.d.ts +131 -14
- package/build/types/RepositoryAnalysis.js +16 -2
- package/build/types/ReuseOutcome.d.ts +63 -0
- package/build/types/ReuseOutcome.js +33 -0
- package/build/types/TestExecution.d.ts +1 -0
- package/build/types/TestTypes.d.ts +25 -7
- package/build/types/TestTypes.js +22 -6
- package/build/types/TestbotReport.d.ts +7 -0
- package/build/types/index.d.ts +2 -0
- package/build/types/index.js +1 -0
- package/build/utils/AnalysisStateManager.d.ts +26 -0
- package/build/utils/AnalysisStateManager.js +31 -1
- package/build/utils/analyze-openapi.js +18 -1
- package/build/utils/branchDiff.d.ts +17 -1
- package/build/utils/branchDiff.js +99 -14
- package/build/utils/featureFlags.d.ts +31 -0
- package/build/utils/featureFlags.js +37 -0
- package/build/utils/frontendIntegration.js +8 -1
- package/build/utils/grpcMockValidation.d.ts +1 -0
- package/build/utils/grpcMockValidation.js +49 -0
- package/build/utils/httpMethodValidation.d.ts +4 -0
- package/build/utils/httpMethodValidation.js +15 -0
- package/build/utils/logger.js +1 -1
- package/build/utils/mockCompatibility.d.ts +49 -0
- package/build/utils/mockCompatibility.js +82 -0
- package/build/utils/pom-scope/pom-files.js +7 -0
- package/build/utils/pom-verify/bindings.js +11 -1
- package/build/utils/pom-verify/re-exports.d.ts +10 -0
- package/build/utils/pom-verify/re-exports.js +61 -0
- package/build/utils/pom-verify/resolve.d.ts +4 -1
- package/build/utils/pom-verify/resolve.js +7 -4
- package/build/utils/pom-verify/verify.d.ts +5 -0
- package/build/utils/pom-verify/verify.js +25 -3
- package/build/utils/progress.js +10 -5
- package/build/utils/proxy-terminal.js +3 -3
- package/build/utils/routeParsers.d.ts +3 -9
- package/build/utils/routeParsers.js +79 -4
- package/build/utils/utils.js +2 -2
- package/build/utils/versions.d.ts +4 -3
- package/build/utils/versions.js +3 -1
- package/build/utils/workspaceAuth.d.ts +46 -0
- package/build/utils/workspaceAuth.js +156 -1
- package/build/workspace/testSuites.d.ts +2 -1
- package/build/workspace/testSuites.js +1 -1
- package/build/workspace/workspace.d.ts +108 -32
- package/build/workspace/workspace.js +32 -4
- package/package.json +5 -2
- package/build/adapters/jestAdapter.test.d.ts +0 -1
- package/build/adapters/jestAdapter.test.js +0 -93
- package/build/adapters/mochaAdapter.test.d.ts +0 -1
- package/build/adapters/mochaAdapter.test.js +0 -63
- package/build/adapters/playwrightAdapter.test.d.ts +0 -1
- package/build/adapters/playwrightAdapter.test.js +0 -169
- package/build/adapters/pytestAdapter.test.d.ts +0 -1
- package/build/adapters/pytestAdapter.test.js +0 -90
- package/build/prompts/code-reuse.test.d.ts +0 -1
- package/build/prompts/code-reuse.test.js +0 -62
- package/build/prompts/pom-aware-code-reuse.test.d.ts +0 -1
- package/build/prompts/pom-aware-code-reuse.test.js +0 -11
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.d.ts +0 -1
- package/build/prompts/test-maintenance/drift-analysis-prompt.test.js +0 -51
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/analysisOutputPrompt.test.js +0 -264
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.d.ts +0 -1
- package/build/prompts/test-recommendation/mergeEnrichedScenarios.test.js +0 -126
- package/build/prompts/test-recommendation/promptPlan.test.d.ts +0 -1
- package/build/prompts/test-recommendation/promptPlan.test.js +0 -336
- package/build/prompts/test-recommendation/scopeAssessment.test.d.ts +0 -1
- package/build/prompts/test-recommendation/scopeAssessment.test.js +0 -371
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.d.ts +0 -1
- package/build/prompts/test-recommendation/test-recommendation-prompt.test.js +0 -1925
- package/build/prompts/testbot/testbot-prompts.test.d.ts +0 -1
- package/build/prompts/testbot/testbot-prompts.test.js +0 -451
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/diversityBalancedBudgeter.test.js +0 -138
- package/build/recommendation/budgeters/fixedNBudgeter.test.d.ts +0 -1
- package/build/recommendation/budgeters/fixedNBudgeter.test.js +0 -66
- package/build/recommendation/discriminators.test.d.ts +0 -1
- package/build/recommendation/discriminators.test.js +0 -324
- package/build/recommendation/diversity.test.d.ts +0 -1
- package/build/recommendation/diversity.test.js +0 -77
- package/build/recommendation/planRanker.test.d.ts +0 -1
- package/build/recommendation/planRanker.test.js +0 -110
- package/build/resources/testbotResource.test.d.ts +0 -1
- package/build/resources/testbotResource.test.js +0 -44
- package/build/services/AnalyticsService.test.d.ts +0 -1
- package/build/services/AnalyticsService.test.js +0 -86
- package/build/services/ScenarioGenerationService.integration.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.integration.test.js +0 -162
- package/build/services/ScenarioGenerationService.test.d.ts +0 -1
- package/build/services/ScenarioGenerationService.test.js +0 -414
- package/build/services/TestDiscoveryService.test.d.ts +0 -1
- package/build/services/TestDiscoveryService.test.js +0 -983
- package/build/services/TestExecutionService.test.d.ts +0 -1
- package/build/services/TestExecutionService.test.js +0 -1032
- package/build/services/TestGenerationService.test.d.ts +0 -1
- package/build/services/TestGenerationService.test.js +0 -578
- package/build/tool-phase-coverage.test.d.ts +0 -1
- package/build/tool-phase-coverage.test.js +0 -49
- package/build/tools/code-refactor/codeReuseTool.test.d.ts +0 -1
- package/build/tools/code-refactor/codeReuseTool.test.js +0 -572
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateBatchScenarioRestTool.test.js +0 -433
- package/build/tools/generate-tests/generateContractRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateContractRestTool.test.js +0 -142
- package/build/tools/generate-tests/generateIntegrationRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateIntegrationRestTool.test.js +0 -159
- package/build/tools/generate-tests/generateLoadRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateLoadRestTool.test.js +0 -169
- package/build/tools/generate-tests/generateUIRestTool.test.d.ts +0 -1
- package/build/tools/generate-tests/generateUIRestTool.test.js +0 -32
- package/build/tools/generate-tests/planGuard.test.d.ts +0 -1
- package/build/tools/generate-tests/planGuard.test.js +0 -185
- package/build/tools/generate-tests/scenarioLint.test.d.ts +0 -1
- package/build/tools/generate-tests/scenarioLint.test.js +0 -100
- package/build/tools/runExistingTestsTool.test.d.ts +0 -1
- package/build/tools/runExistingTestsTool.test.js +0 -379
- package/build/tools/submitReportTool.test.d.ts +0 -1
- package/build/tools/submitReportTool.test.js +0 -1428
- package/build/tools/test-management/actionsTool.test.d.ts +0 -1
- package/build/tools/test-management/actionsTool.test.js +0 -436
- package/build/tools/test-management/analyzeChangesTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeChangesTool.test.js +0 -522
- package/build/tools/test-management/analyzeTestHealthTool.test.d.ts +0 -1
- package/build/tools/test-management/analyzeTestHealthTool.test.js +0 -385
- package/build/tools/test-management/registerTestPlanTool.test.d.ts +0 -1
- package/build/tools/test-management/registerTestPlanTool.test.js +0 -296
- package/build/tools/trace/resolveSaveStoragePath.test.d.ts +0 -1
- package/build/tools/trace/resolveSaveStoragePath.test.js +0 -17
- package/build/tools/trace/resolveSessionPaths.test.d.ts +0 -1
- package/build/tools/trace/resolveSessionPaths.test.js +0 -104
- package/build/tools/trace/sessionState.test.d.ts +0 -1
- package/build/tools/trace/sessionState.test.js +0 -17
- package/build/tools/workspace/initializeWorkspaceTool.test.d.ts +0 -1
- package/build/tools/workspace/initializeWorkspaceTool.test.js +0 -139
- package/build/tools/workspace/serviceUpsert.test.d.ts +0 -1
- package/build/tools/workspace/serviceUpsert.test.js +0 -50
- package/build/utils/AnalysisStateManager.test.d.ts +0 -1
- package/build/utils/AnalysisStateManager.test.js +0 -134
- package/build/utils/dartRouteExtractor.test.d.ts +0 -1
- package/build/utils/dartRouteExtractor.test.js +0 -307
- package/build/utils/docker.test.d.ts +0 -1
- package/build/utils/docker.test.js +0 -114
- package/build/utils/featureFlags.test.d.ts +0 -1
- package/build/utils/featureFlags.test.js +0 -81
- package/build/utils/fileWalk.test.d.ts +0 -1
- package/build/utils/fileWalk.test.js +0 -252
- package/build/utils/frontendIntegration.test.d.ts +0 -1
- package/build/utils/frontendIntegration.test.js +0 -229
- package/build/utils/frontendSelectors.test.d.ts +0 -1
- package/build/utils/frontendSelectors.test.js +0 -118
- package/build/utils/gitStaging.test.d.ts +0 -1
- package/build/utils/gitStaging.test.js +0 -111
- package/build/utils/httpDefaults.test.d.ts +0 -1
- package/build/utils/httpDefaults.test.js +0 -21
- package/build/utils/importerHop.test.d.ts +0 -1
- package/build/utils/importerHop.test.js +0 -469
- package/build/utils/pathAffinityClassification.test.d.ts +0 -1
- package/build/utils/pathAffinityClassification.test.js +0 -208
- package/build/utils/planMatchKeys.test.d.ts +0 -1
- package/build/utils/planMatchKeys.test.js +0 -123
- package/build/utils/pom-scope/index.test.d.ts +0 -1
- package/build/utils/pom-scope/index.test.js +0 -278
- package/build/utils/pom-scope/pom-files.test.d.ts +0 -1
- package/build/utils/pom-scope/pom-files.test.js +0 -29
- package/build/utils/pom-scope/scoring.test.d.ts +0 -1
- package/build/utils/pom-scope/scoring.test.js +0 -39
- package/build/utils/pom-scope/selector-extractor.test.d.ts +0 -1
- package/build/utils/pom-scope/selector-extractor.test.js +0 -67
- package/build/utils/pom-verify/bindings.test.d.ts +0 -1
- package/build/utils/pom-verify/bindings.test.js +0 -164
- package/build/utils/pom-verify/calls.test.d.ts +0 -1
- package/build/utils/pom-verify/calls.test.js +0 -61
- package/build/utils/pom-verify/resolve.test.d.ts +0 -1
- package/build/utils/pom-verify/resolve.test.js +0 -114
- package/build/utils/pom-verify/verify.test.d.ts +0 -1
- package/build/utils/pom-verify/verify.test.js +0 -374
- package/build/utils/pr-comment-parser.test.d.ts +0 -1
- package/build/utils/pr-comment-parser.test.js +0 -428
- package/build/utils/progress.test.d.ts +0 -1
- package/build/utils/progress.test.js +0 -37
- package/build/utils/projectMetadata.test.d.ts +0 -1
- package/build/utils/projectMetadata.test.js +0 -172
- package/build/utils/pythonMountPrefixes.test.d.ts +0 -1
- package/build/utils/pythonMountPrefixes.test.js +0 -113
- package/build/utils/repoScanner.test.d.ts +0 -1
- package/build/utils/repoScanner.test.js +0 -190
- package/build/utils/reportVerification.test.d.ts +0 -1
- package/build/utils/reportVerification.test.js +0 -185
- package/build/utils/routeParsers.test.d.ts +0 -1
- package/build/utils/routeParsers.test.js +0 -1011
- package/build/utils/scenarioDrafting.test.d.ts +0 -1
- package/build/utils/scenarioDrafting.test.js +0 -876
- package/build/utils/sourceRouteExtractor.test.d.ts +0 -1
- package/build/utils/sourceRouteExtractor.test.js +0 -738
- package/build/utils/telemetry.test.d.ts +0 -1
- package/build/utils/telemetry.test.js +0 -70
- package/build/utils/trace-parser.test.d.ts +0 -6
- package/build/utils/trace-parser.test.js +0 -140
- package/build/utils/uiPageEnumerator.test.d.ts +0 -1
- package/build/utils/uiPageEnumerator.test.js +0 -824
- package/build/utils/utils.test.d.ts +0 -1
- package/build/utils/utils.test.js +0 -103
- package/build/utils/walkerCharacterization.test.d.ts +0 -1
- package/build/utils/walkerCharacterization.test.js +0 -233
- package/build/utils/workspaceAuth.test.d.ts +0 -1
- package/build/utils/workspaceAuth.test.js +0 -260
- package/build/workspace/testSuites.test.d.ts +0 -1
- package/build/workspace/testSuites.test.js +0 -23
- package/build/workspace/workspace.test.d.ts +0 -1
- package/build/workspace/workspace.test.js +0 -264
|
@@ -1,1032 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck - Jest ESM type inference issues
|
|
2
|
-
import { jest } from "@jest/globals";
|
|
3
|
-
import * as os from "os";
|
|
4
|
-
import * as path from "path";
|
|
5
|
-
import { buildContainerEnv, rewriteLocalhostForDocker } from "./containerEnv.js";
|
|
6
|
-
// Mock dockerode before importing TestExecutionService
|
|
7
|
-
const mockRun = jest.fn();
|
|
8
|
-
const mockListImages = jest.fn();
|
|
9
|
-
jest.unstable_mockModule("dockerode", () => ({
|
|
10
|
-
default: jest.fn().mockImplementation(() => ({
|
|
11
|
-
run: mockRun,
|
|
12
|
-
listImages: mockListImages,
|
|
13
|
-
})),
|
|
14
|
-
}));
|
|
15
|
-
// Get actual fs for spreading, then mock specific methods
|
|
16
|
-
const actualFs = await import("fs");
|
|
17
|
-
const mockAccessSync = jest.fn();
|
|
18
|
-
const mockExistsSync = jest.fn().mockReturnValue(true);
|
|
19
|
-
const mockReaddirSync = jest.fn().mockImplementation((_path, options) => {
|
|
20
|
-
if (options?.withFileTypes) {
|
|
21
|
-
return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
|
|
22
|
-
}
|
|
23
|
-
return ["test_file.py"];
|
|
24
|
-
});
|
|
25
|
-
const mockReadFileSync = jest.fn().mockReturnValue("");
|
|
26
|
-
const mockWriteFileSync = jest.fn();
|
|
27
|
-
const mockMkdirSync = jest.fn();
|
|
28
|
-
const mockRmSync = jest.fn();
|
|
29
|
-
jest.unstable_mockModule("fs", () => ({
|
|
30
|
-
...actualFs,
|
|
31
|
-
default: {
|
|
32
|
-
...actualFs.default,
|
|
33
|
-
accessSync: mockAccessSync,
|
|
34
|
-
existsSync: mockExistsSync,
|
|
35
|
-
readdirSync: mockReaddirSync,
|
|
36
|
-
readFileSync: mockReadFileSync,
|
|
37
|
-
writeFileSync: mockWriteFileSync,
|
|
38
|
-
mkdirSync: mockMkdirSync,
|
|
39
|
-
rmSync: mockRmSync,
|
|
40
|
-
constants: actualFs.default.constants,
|
|
41
|
-
},
|
|
42
|
-
accessSync: mockAccessSync,
|
|
43
|
-
existsSync: mockExistsSync,
|
|
44
|
-
readdirSync: mockReaddirSync,
|
|
45
|
-
readFileSync: mockReadFileSync,
|
|
46
|
-
writeFileSync: mockWriteFileSync,
|
|
47
|
-
mkdirSync: mockMkdirSync,
|
|
48
|
-
rmSync: mockRmSync,
|
|
49
|
-
}));
|
|
50
|
-
// Mock logger
|
|
51
|
-
jest.unstable_mockModule("../utils/logger.js", () => ({
|
|
52
|
-
logger: {
|
|
53
|
-
debug: jest.fn(),
|
|
54
|
-
info: jest.fn(),
|
|
55
|
-
error: jest.fn(),
|
|
56
|
-
warning: jest.fn(),
|
|
57
|
-
},
|
|
58
|
-
}));
|
|
59
|
-
// Real temp directory trees for tests that need actual directory traversal
|
|
60
|
-
// (findNestedNodeModules / findExcludedPaths walk the real fs, delegated to
|
|
61
|
-
// via mockReaddirSync — see each describe block's beforeEach). Every root
|
|
62
|
-
// created here is tracked and removed after its test so these don't leak
|
|
63
|
-
// into the OS temp dir across runs.
|
|
64
|
-
const makeTreeRoots = [];
|
|
65
|
-
function makeTree(files) {
|
|
66
|
-
const root = actualFs.mkdtempSync(path.join(os.tmpdir(), "nnm-"));
|
|
67
|
-
makeTreeRoots.push(root);
|
|
68
|
-
for (const [rel, content] of Object.entries(files)) {
|
|
69
|
-
const abs = path.join(root, rel);
|
|
70
|
-
if (content === null)
|
|
71
|
-
actualFs.mkdirSync(abs, { recursive: true });
|
|
72
|
-
else {
|
|
73
|
-
actualFs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
74
|
-
actualFs.writeFileSync(abs, content);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return root;
|
|
78
|
-
}
|
|
79
|
-
afterEach(() => {
|
|
80
|
-
while (makeTreeRoots.length > 0) {
|
|
81
|
-
const root = makeTreeRoots.pop();
|
|
82
|
-
actualFs.rmSync(root, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
describe("rewriteLocalhostForDocker", () => {
|
|
86
|
-
it("rewrites http://localhost:port", () => {
|
|
87
|
-
expect(rewriteLocalhostForDocker("http://localhost:8000")).toBe("http://host.docker.internal:8000");
|
|
88
|
-
});
|
|
89
|
-
it("rewrites http://127.0.0.1:port", () => {
|
|
90
|
-
expect(rewriteLocalhostForDocker("http://127.0.0.1:4200")).toBe("http://host.docker.internal:4200");
|
|
91
|
-
});
|
|
92
|
-
it("rewrites https://localhost:port", () => {
|
|
93
|
-
expect(rewriteLocalhostForDocker("https://localhost:443")).toBe("https://host.docker.internal:443");
|
|
94
|
-
});
|
|
95
|
-
it("rewrites localhost without port", () => {
|
|
96
|
-
expect(rewriteLocalhostForDocker("http://localhost/api")).toBe("http://host.docker.internal/api");
|
|
97
|
-
});
|
|
98
|
-
it("rewrites localhost with no path or port", () => {
|
|
99
|
-
expect(rewriteLocalhostForDocker("http://localhost")).toBe("http://host.docker.internal");
|
|
100
|
-
});
|
|
101
|
-
it("does not rewrite non-localhost URLs", () => {
|
|
102
|
-
expect(rewriteLocalhostForDocker("http://api.example.com:8000")).toBe("http://api.example.com:8000");
|
|
103
|
-
});
|
|
104
|
-
it("does not rewrite localhost in path", () => {
|
|
105
|
-
expect(rewriteLocalhostForDocker("http://api.example.com/localhost")).toBe("http://api.example.com/localhost");
|
|
106
|
-
});
|
|
107
|
-
it("does not rewrite partial matches like localhost-extra", () => {
|
|
108
|
-
expect(rewriteLocalhostForDocker("http://localhost-extra:8000")).toBe("http://localhost-extra:8000");
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
describe("buildContainerEnv", () => {
|
|
112
|
-
const baseOptions = { token: "test-token", language: "python" };
|
|
113
|
-
const emptyHostEnv = {};
|
|
114
|
-
it("always includes SKYRAMP_TEST_TOKEN and SKYRAMP_IN_DOCKER", () => {
|
|
115
|
-
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
116
|
-
expect(env).toContain("SKYRAMP_TEST_TOKEN=test-token");
|
|
117
|
-
expect(env).toContain("SKYRAMP_IN_DOCKER=true");
|
|
118
|
-
});
|
|
119
|
-
it("defaults token to empty string when not provided", () => {
|
|
120
|
-
const env = buildContainerEnv({ language: "typescript" }, undefined, emptyHostEnv);
|
|
121
|
-
expect(env).toContain("SKYRAMP_TEST_TOKEN=");
|
|
122
|
-
});
|
|
123
|
-
it("adds PYTEST_ADDOPTS for python language", () => {
|
|
124
|
-
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
125
|
-
expect(env).toContain("PYTEST_ADDOPTS=--noconftest -c /dev/null");
|
|
126
|
-
});
|
|
127
|
-
it("does not add PYTEST_ADDOPTS for non-python language", () => {
|
|
128
|
-
const env = buildContainerEnv({ ...baseOptions, language: "typescript" }, undefined, emptyHostEnv);
|
|
129
|
-
expect(env.find((e) => e.startsWith("PYTEST_ADDOPTS="))).toBeUndefined();
|
|
130
|
-
});
|
|
131
|
-
it("adds PLAYWRIGHT_SAVE_STORAGE_PATH when saveStoragePath provided", () => {
|
|
132
|
-
const env = buildContainerEnv(baseOptions, "/tmp/storage", emptyHostEnv);
|
|
133
|
-
expect(env).toContain("PLAYWRIGHT_SAVE_STORAGE_PATH=/tmp/storage");
|
|
134
|
-
});
|
|
135
|
-
it("does not add PLAYWRIGHT_SAVE_STORAGE_PATH when not provided", () => {
|
|
136
|
-
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
137
|
-
expect(env.find((e) => e.startsWith("PLAYWRIGHT_SAVE_STORAGE_PATH="))).toBeUndefined();
|
|
138
|
-
});
|
|
139
|
-
describe("base URL env var forwarding", () => {
|
|
140
|
-
it("rewrites localhost to host.docker.internal in bridge mode (default)", () => {
|
|
141
|
-
const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://localhost:8000" };
|
|
142
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
143
|
-
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:8000");
|
|
144
|
-
});
|
|
145
|
-
it("rewrites 127.0.0.1 to host.docker.internal in bridge mode", () => {
|
|
146
|
-
const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://127.0.0.1:4200" };
|
|
147
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
148
|
-
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:4200");
|
|
149
|
-
});
|
|
150
|
-
it("preserves localhost when useHostNetwork is true", () => {
|
|
151
|
-
const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://localhost:8000" };
|
|
152
|
-
const env = buildContainerEnv({ ...baseOptions, useHostNetwork: true }, undefined, hostEnv);
|
|
153
|
-
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://localhost:8000");
|
|
154
|
-
});
|
|
155
|
-
it("does not rewrite non-localhost URLs", () => {
|
|
156
|
-
const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://api.example.com:8000" };
|
|
157
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
158
|
-
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://api.example.com:8000");
|
|
159
|
-
});
|
|
160
|
-
it("rewrites SKYRAMP_TEST_SERVICE_URL_* vars in bridge mode", () => {
|
|
161
|
-
const hostEnv = {
|
|
162
|
-
SKYRAMP_TEST_SERVICE_URL_BACKEND: "http://localhost:8000",
|
|
163
|
-
SKYRAMP_TEST_SERVICE_URL_FRONTEND: "http://localhost:5173",
|
|
164
|
-
};
|
|
165
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
166
|
-
expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://host.docker.internal:8000");
|
|
167
|
-
expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://host.docker.internal:5173");
|
|
168
|
-
});
|
|
169
|
-
it("skips SKYRAMP_TEST_BASE_URL when value is empty", () => {
|
|
170
|
-
const hostEnv = { SKYRAMP_TEST_BASE_URL: "" };
|
|
171
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
172
|
-
expect(env.find((e) => e.startsWith("SKYRAMP_TEST_BASE_URL="))).toBeUndefined();
|
|
173
|
-
});
|
|
174
|
-
it("does not forward unrelated SKYRAMP_ vars", () => {
|
|
175
|
-
const hostEnv = {
|
|
176
|
-
SKYRAMP_TEST_BASE_URL: "http://localhost:8000",
|
|
177
|
-
SKYRAMP_OTHER_VAR: "should-not-appear",
|
|
178
|
-
SKYRAMP_TEST_SERVICE_NOT_URL: "also-no",
|
|
179
|
-
};
|
|
180
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
181
|
-
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://host.docker.internal:8000");
|
|
182
|
-
expect(env.find((e) => e.includes("SKYRAMP_OTHER_VAR"))).toBeUndefined();
|
|
183
|
-
expect(env.find((e) => e.includes("SKYRAMP_TEST_SERVICE_NOT_URL"))).toBeUndefined();
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
it("forwards SKYRAMP_DEBUG from host env", () => {
|
|
187
|
-
const hostEnv = { SKYRAMP_DEBUG: "true" };
|
|
188
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
189
|
-
expect(env).toContain("SKYRAMP_DEBUG=true");
|
|
190
|
-
});
|
|
191
|
-
it("forwards API_KEY from host env", () => {
|
|
192
|
-
const hostEnv = { API_KEY: "my-key" };
|
|
193
|
-
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
194
|
-
expect(env).toContain("API_KEY=my-key");
|
|
195
|
-
});
|
|
196
|
-
});
|
|
197
|
-
describe("detectSessionFiles", () => {
|
|
198
|
-
// Import after mocks are set up so the fs mock applies
|
|
199
|
-
let detectSessionFiles;
|
|
200
|
-
beforeAll(async () => {
|
|
201
|
-
const mod = await import("./TestExecutionService.js");
|
|
202
|
-
detectSessionFiles = mod.detectSessionFiles;
|
|
203
|
-
});
|
|
204
|
-
it("detects string-literal storageState (TS/JS)", () => {
|
|
205
|
-
mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: '/abs/path/session.json' });`);
|
|
206
|
-
expect(detectSessionFiles("/ws/test.spec.ts")).toEqual([
|
|
207
|
-
"/abs/path/session.json",
|
|
208
|
-
]);
|
|
209
|
-
});
|
|
210
|
-
it("detects skyramp codegen path.join(__dirname, '<file>') pattern and resolves to host-absolute path", () => {
|
|
211
|
-
// Reproduces SKYR-3321 generated test shape — must resolve to the host
|
|
212
|
-
// absolute path so the executor's absolute-path mount branch makes the
|
|
213
|
-
// file visible at that same path inside the container.
|
|
214
|
-
mockReadFileSync.mockReturnValueOnce(`
|
|
215
|
-
import path from 'path';
|
|
216
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
217
|
-
const SESSION_STORAGE = path.join(__dirname, 'skyramp_session_storage.json');
|
|
218
|
-
test.use({ storageState: SESSION_STORAGE });
|
|
219
|
-
`);
|
|
220
|
-
expect(detectSessionFiles("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts")).toEqual([
|
|
221
|
-
"/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json",
|
|
222
|
-
]);
|
|
223
|
-
});
|
|
224
|
-
it("detects inline storageState: path.join(__dirname, '<file>')", () => {
|
|
225
|
-
mockReadFileSync.mockReturnValueOnce(`test.use({ storageState: path.join(__dirname, 'session.json') });`);
|
|
226
|
-
expect(detectSessionFiles("/ws/spec.ts")).toEqual(["/ws/session.json"]);
|
|
227
|
-
});
|
|
228
|
-
it("ignores storageState references inside comments", () => {
|
|
229
|
-
mockReadFileSync.mockReturnValueOnce(`
|
|
230
|
-
// storageState: '/should/not/match.json'
|
|
231
|
-
// path.join(__dirname, 'also-not.json')
|
|
232
|
-
test('x', () => {});
|
|
233
|
-
`);
|
|
234
|
-
expect(detectSessionFiles("/ws/spec.ts")).toEqual([]);
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
describe("isBrowserTest", () => {
|
|
238
|
-
let isBrowserTest;
|
|
239
|
-
beforeAll(async () => {
|
|
240
|
-
const mod = await import("./TestExecutionService.js");
|
|
241
|
-
isBrowserTest = mod.isBrowserTest;
|
|
242
|
-
});
|
|
243
|
-
it("returns true for UI test type", () => {
|
|
244
|
-
expect(isBrowserTest("ui")).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
it("returns true for E2E test type", () => {
|
|
247
|
-
expect(isBrowserTest("e2e")).toBe(true);
|
|
248
|
-
});
|
|
249
|
-
it("returns false for contract test type", () => {
|
|
250
|
-
expect(isBrowserTest("contract")).toBe(false);
|
|
251
|
-
});
|
|
252
|
-
it("returns false for integration test type", () => {
|
|
253
|
-
expect(isBrowserTest("integration")).toBe(false);
|
|
254
|
-
});
|
|
255
|
-
it("returns false for fuzz test type", () => {
|
|
256
|
-
expect(isBrowserTest("fuzz")).toBe(false);
|
|
257
|
-
});
|
|
258
|
-
it("returns false for smoke test type", () => {
|
|
259
|
-
expect(isBrowserTest("smoke")).toBe(false);
|
|
260
|
-
});
|
|
261
|
-
it("returns false for load test type", () => {
|
|
262
|
-
expect(isBrowserTest("load")).toBe(false);
|
|
263
|
-
});
|
|
264
|
-
it("returns false for mock test type", () => {
|
|
265
|
-
expect(isBrowserTest("mock")).toBe(false);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
describe("getVideoSubdir", () => {
|
|
269
|
-
let getVideoSubdir;
|
|
270
|
-
beforeAll(async () => {
|
|
271
|
-
const mod = await import("./TestExecutionService.js");
|
|
272
|
-
getVideoSubdir = mod.getVideoSubdir;
|
|
273
|
-
});
|
|
274
|
-
it("generates deterministic subdirectory name with basename and hash", () => {
|
|
275
|
-
const testFile = "/workspace/tests/login.spec.ts";
|
|
276
|
-
const result = getVideoSubdir(testFile);
|
|
277
|
-
// Should be "basename-8hexchars" format
|
|
278
|
-
// Note: path.extname only strips the final extension (.ts), so .spec remains in basename
|
|
279
|
-
expect(result).toMatch(/^login\.spec-[0-9a-f]{8}$/);
|
|
280
|
-
});
|
|
281
|
-
it("strips file extension from basename", () => {
|
|
282
|
-
const result = getVideoSubdir("/workspace/checkout.spec.ts");
|
|
283
|
-
// path.extname('.spec.ts') returns '.ts', so 'checkout.spec' is the basename
|
|
284
|
-
expect(result).toMatch(/^checkout\.spec-[0-9a-f]{8}$/);
|
|
285
|
-
expect(result).toContain(".spec");
|
|
286
|
-
});
|
|
287
|
-
it("produces different hashes for different paths", () => {
|
|
288
|
-
const path1 = "/workspace/tests/login.spec.ts";
|
|
289
|
-
const path2 = "/workspace/tests/checkout.spec.ts";
|
|
290
|
-
const result1 = getVideoSubdir(path1);
|
|
291
|
-
const result2 = getVideoSubdir(path2);
|
|
292
|
-
expect(result1).not.toBe(result2);
|
|
293
|
-
// Different hashes
|
|
294
|
-
expect(result1.split("-")[1]).not.toBe(result2.split("-")[1]);
|
|
295
|
-
});
|
|
296
|
-
it("produces same hash for the same path (deterministic)", () => {
|
|
297
|
-
const testFile = "/workspace/tests/products.spec.ts";
|
|
298
|
-
const result1 = getVideoSubdir(testFile);
|
|
299
|
-
const result2 = getVideoSubdir(testFile);
|
|
300
|
-
expect(result1).toBe(result2);
|
|
301
|
-
});
|
|
302
|
-
it("handles paths with no extension", () => {
|
|
303
|
-
const result = getVideoSubdir("/workspace/test_file");
|
|
304
|
-
expect(result).toMatch(/^test_file-[0-9a-f]{8}$/);
|
|
305
|
-
});
|
|
306
|
-
it("handles paths with multiple dots", () => {
|
|
307
|
-
const result = getVideoSubdir("/workspace/test.spec.e2e.ts");
|
|
308
|
-
// Should strip only the last extension (.ts)
|
|
309
|
-
expect(result).toMatch(/^test\.spec\.e2e-[0-9a-f]{8}$/);
|
|
310
|
-
});
|
|
311
|
-
});
|
|
312
|
-
describe("TestExecutionService.executeTest - Docker env forwarding", () => {
|
|
313
|
-
// Import after mocks are set up
|
|
314
|
-
let TestExecutionService;
|
|
315
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
316
|
-
beforeAll(async () => {
|
|
317
|
-
const mod = await import("./TestExecutionService.js");
|
|
318
|
-
TestExecutionService = mod.TestExecutionService;
|
|
319
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
320
|
-
});
|
|
321
|
-
beforeEach(() => {
|
|
322
|
-
jest.clearAllMocks();
|
|
323
|
-
// Simulate image already cached
|
|
324
|
-
mockListImages.mockResolvedValue([
|
|
325
|
-
{ RepoTags: [EXECUTOR_DOCKER_IMAGE] },
|
|
326
|
-
]);
|
|
327
|
-
});
|
|
328
|
-
afterEach(() => {
|
|
329
|
-
// Clean up env vars we set during tests
|
|
330
|
-
delete process.env.SKYRAMP_TEST_BASE_URL;
|
|
331
|
-
delete process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND;
|
|
332
|
-
delete process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND;
|
|
333
|
-
});
|
|
334
|
-
it("passes SKYRAMP_TEST_BASE_URL to Docker container Env", async () => {
|
|
335
|
-
process.env.SKYRAMP_TEST_BASE_URL = "http://external-host:8000";
|
|
336
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
337
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
338
|
-
const service = new TestExecutionService();
|
|
339
|
-
await service.executeTest({
|
|
340
|
-
testFile: "/workspace/test_file.py",
|
|
341
|
-
workspacePath: "/workspace",
|
|
342
|
-
language: "python",
|
|
343
|
-
testType: "smoke",
|
|
344
|
-
});
|
|
345
|
-
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
346
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
347
|
-
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_BASE_URL=http://external-host:8000");
|
|
348
|
-
});
|
|
349
|
-
it("passes multiple SKYRAMP_TEST_SERVICE_URL_* to Docker container Env", async () => {
|
|
350
|
-
process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND = "http://host1:8000";
|
|
351
|
-
process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = "http://host2:5173";
|
|
352
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
353
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
354
|
-
const service = new TestExecutionService();
|
|
355
|
-
await service.executeTest({
|
|
356
|
-
testFile: "/workspace/test_file.py",
|
|
357
|
-
workspacePath: "/workspace",
|
|
358
|
-
language: "python",
|
|
359
|
-
testType: "smoke",
|
|
360
|
-
});
|
|
361
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
362
|
-
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://host1:8000");
|
|
363
|
-
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://host2:5173");
|
|
364
|
-
});
|
|
365
|
-
it("does not include base URL vars when not set in host env", async () => {
|
|
366
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
367
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
368
|
-
const service = new TestExecutionService();
|
|
369
|
-
await service.executeTest({
|
|
370
|
-
testFile: "/workspace/test_file.py",
|
|
371
|
-
workspacePath: "/workspace",
|
|
372
|
-
language: "python",
|
|
373
|
-
testType: "smoke",
|
|
374
|
-
});
|
|
375
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
376
|
-
const envWithBaseUrl = dockerOptions.Env.filter((e) => e.startsWith("SKYRAMP_TEST_BASE_URL=") ||
|
|
377
|
-
e.startsWith("SKYRAMP_TEST_SERVICE_URL_"));
|
|
378
|
-
expect(envWithBaseUrl).toHaveLength(0);
|
|
379
|
-
});
|
|
380
|
-
// Approach B: every workspace mount is mirrored at the host-absolute path so
|
|
381
|
-
// tests that embed absolute references (storageState, fixtures, snapshots)
|
|
382
|
-
// resolve correctly inside the executor regardless of which path-shape the
|
|
383
|
-
// codegen happens to emit.
|
|
384
|
-
it("mirrors each workspace file mount at both /home/user/<f> and the host-absolute path", async () => {
|
|
385
|
-
mockReaddirSync.mockImplementation((_path, options) => {
|
|
386
|
-
if (options?.withFileTypes) {
|
|
387
|
-
return [
|
|
388
|
-
{ name: "xdr_dashboard.spec.ts", isFile: () => true, isDirectory: () => false },
|
|
389
|
-
{ name: "skyramp_session_storage.json", isFile: () => true, isDirectory: () => false },
|
|
390
|
-
];
|
|
391
|
-
}
|
|
392
|
-
return ["xdr_dashboard.spec.ts", "skyramp_session_storage.json"];
|
|
393
|
-
});
|
|
394
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
395
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
396
|
-
const service = new TestExecutionService();
|
|
397
|
-
await service.executeTest({
|
|
398
|
-
testFile: "/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts",
|
|
399
|
-
workspacePath: "/Users/pedro/projects/cisco-xdr-tests",
|
|
400
|
-
language: "typescript",
|
|
401
|
-
testType: "ui",
|
|
402
|
-
});
|
|
403
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
404
|
-
const targets = dockerOptions.HostConfig.Mounts.map((m) => m.Target);
|
|
405
|
-
// Canonical /home/user mount
|
|
406
|
-
expect(targets).toContain("/home/user/xdr_dashboard.spec.ts");
|
|
407
|
-
expect(targets).toContain("/home/user/skyramp_session_storage.json");
|
|
408
|
-
// Host-absolute mirror — the fix for absolute paths leaking out of `__dirname`
|
|
409
|
-
expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/xdr_dashboard.spec.ts");
|
|
410
|
-
expect(targets).toContain("/Users/pedro/projects/cisco-xdr-tests/skyramp_session_storage.json");
|
|
411
|
-
});
|
|
412
|
-
it("does not double-mount when workspacePath equals /home/user", async () => {
|
|
413
|
-
mockReaddirSync.mockImplementation((_path, options) => {
|
|
414
|
-
if (options?.withFileTypes) {
|
|
415
|
-
return [{ name: "test_file.py", isFile: () => true, isDirectory: () => false }];
|
|
416
|
-
}
|
|
417
|
-
return ["test_file.py"];
|
|
418
|
-
});
|
|
419
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
420
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
421
|
-
const service = new TestExecutionService();
|
|
422
|
-
await service.executeTest({
|
|
423
|
-
testFile: "/home/user/test_file.py",
|
|
424
|
-
workspacePath: "/home/user",
|
|
425
|
-
language: "python",
|
|
426
|
-
testType: "smoke",
|
|
427
|
-
});
|
|
428
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
429
|
-
const targetCounts = {};
|
|
430
|
-
for (const m of dockerOptions.HostConfig.Mounts) {
|
|
431
|
-
targetCounts[m.Target] = (targetCounts[m.Target] ?? 0) + 1;
|
|
432
|
-
}
|
|
433
|
-
// No mount target should appear twice (no host-absolute mirror when workspace == /home/user)
|
|
434
|
-
for (const [t, n] of Object.entries(targetCounts)) {
|
|
435
|
-
expect({ target: t, count: n }).toEqual({ target: t, count: 1 });
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
});
|
|
439
|
-
describe("TestExecutionService.executeTest - Video capture for browser tests", () => {
|
|
440
|
-
let TestExecutionService;
|
|
441
|
-
let getVideoSubdir;
|
|
442
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
443
|
-
beforeAll(async () => {
|
|
444
|
-
const mod = await import("./TestExecutionService.js");
|
|
445
|
-
TestExecutionService = mod.TestExecutionService;
|
|
446
|
-
getVideoSubdir = mod.getVideoSubdir;
|
|
447
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
448
|
-
});
|
|
449
|
-
beforeEach(() => {
|
|
450
|
-
jest.clearAllMocks();
|
|
451
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
452
|
-
mockExistsSync.mockReturnValue(true);
|
|
453
|
-
mockReaddirSync.mockImplementation((_path, options) => {
|
|
454
|
-
if (options?.withFileTypes) {
|
|
455
|
-
return [
|
|
456
|
-
{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false },
|
|
457
|
-
{ name: "test.py", isFile: () => true, isDirectory: () => false },
|
|
458
|
-
];
|
|
459
|
-
}
|
|
460
|
-
return ["test.spec.ts", "test.py"];
|
|
461
|
-
});
|
|
462
|
-
// Reset writeFileSync mock to default behavior
|
|
463
|
-
mockWriteFileSync.mockImplementation(() => { });
|
|
464
|
-
});
|
|
465
|
-
it("mounts generated Playwright config at /home/user/playwright.config.ts for TypeScript UI test", async () => {
|
|
466
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
467
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
468
|
-
const service = new TestExecutionService();
|
|
469
|
-
await service.executeTest({
|
|
470
|
-
testFile: "/workspace/login.spec.ts",
|
|
471
|
-
workspacePath: "/workspace",
|
|
472
|
-
language: "typescript",
|
|
473
|
-
testType: "ui",
|
|
474
|
-
});
|
|
475
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
476
|
-
const playwrightConfigMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === "/home/user/playwright.config.ts");
|
|
477
|
-
expect(playwrightConfigMount).toBeDefined();
|
|
478
|
-
expect(playwrightConfigMount.Type).toBe("bind");
|
|
479
|
-
// Source should be a temp file containing the generated config
|
|
480
|
-
expect(playwrightConfigMount.Source).toContain("skyramp-playwright-");
|
|
481
|
-
expect(playwrightConfigMount.Source).toContain(".config.ts");
|
|
482
|
-
});
|
|
483
|
-
it("generated Playwright config for TypeScript contains video:on and outputDir", async () => {
|
|
484
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
485
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
486
|
-
let capturedConfigContent = "";
|
|
487
|
-
mockWriteFileSync.mockImplementation((filePath, content) => {
|
|
488
|
-
if (filePath.includes("skyramp-playwright-") && filePath.endsWith(".config.ts")) {
|
|
489
|
-
capturedConfigContent = content;
|
|
490
|
-
}
|
|
491
|
-
});
|
|
492
|
-
const service = new TestExecutionService();
|
|
493
|
-
await service.executeTest({
|
|
494
|
-
testFile: "/workspace/checkout.spec.ts",
|
|
495
|
-
workspacePath: "/workspace",
|
|
496
|
-
language: "typescript",
|
|
497
|
-
testType: "e2e",
|
|
498
|
-
});
|
|
499
|
-
expect(capturedConfigContent).toContain('video: "on"');
|
|
500
|
-
expect(capturedConfigContent).toContain("outputDir:");
|
|
501
|
-
expect(capturedConfigContent).toContain(".skyramp/videos/");
|
|
502
|
-
});
|
|
503
|
-
it("mounts generated pytest.ini at /home/user/pytest.ini for Python UI test", async () => {
|
|
504
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
505
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
506
|
-
const service = new TestExecutionService();
|
|
507
|
-
await service.executeTest({
|
|
508
|
-
testFile: "/workspace/test_login.py",
|
|
509
|
-
workspacePath: "/workspace",
|
|
510
|
-
language: "python",
|
|
511
|
-
testType: "ui",
|
|
512
|
-
});
|
|
513
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
514
|
-
const pytestConfigMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target === "/home/user/pytest.ini");
|
|
515
|
-
expect(pytestConfigMount).toBeDefined();
|
|
516
|
-
expect(pytestConfigMount.Type).toBe("bind");
|
|
517
|
-
expect(pytestConfigMount.Source).toContain("skyramp-pytest-");
|
|
518
|
-
expect(pytestConfigMount.Source).toContain(".ini");
|
|
519
|
-
});
|
|
520
|
-
it("generated pytest.ini for Python contains --video on and --output", async () => {
|
|
521
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
522
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
523
|
-
let capturedPytestContent = "";
|
|
524
|
-
mockWriteFileSync.mockImplementation((filePath, content) => {
|
|
525
|
-
if (filePath.includes("skyramp-pytest-") && filePath.endsWith(".ini")) {
|
|
526
|
-
capturedPytestContent = content;
|
|
527
|
-
}
|
|
528
|
-
});
|
|
529
|
-
const service = new TestExecutionService();
|
|
530
|
-
await service.executeTest({
|
|
531
|
-
testFile: "/workspace/test_checkout.py",
|
|
532
|
-
workspacePath: "/workspace",
|
|
533
|
-
language: "python",
|
|
534
|
-
testType: "e2e",
|
|
535
|
-
});
|
|
536
|
-
expect(capturedPytestContent).toContain("--video on");
|
|
537
|
-
expect(capturedPytestContent).toContain("--output");
|
|
538
|
-
expect(capturedPytestContent).toContain(".skyramp/videos/");
|
|
539
|
-
});
|
|
540
|
-
it("mounts video output directory as writable for UI test", async () => {
|
|
541
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
542
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
543
|
-
const service = new TestExecutionService();
|
|
544
|
-
await service.executeTest({
|
|
545
|
-
testFile: "/workspace/tests/login.spec.ts",
|
|
546
|
-
workspacePath: "/workspace",
|
|
547
|
-
language: "typescript",
|
|
548
|
-
testType: "ui",
|
|
549
|
-
});
|
|
550
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
551
|
-
const videoSubdir = getVideoSubdir("/workspace/tests/login.spec.ts");
|
|
552
|
-
// Each execution gets a unique per-run dir: `${videoSubdir}-<timestamp>`,
|
|
553
|
-
// so match by prefix rather than exact equality.
|
|
554
|
-
const videoMountPrefix = `/home/user/.skyramp/videos/${videoSubdir}`;
|
|
555
|
-
const videoMount = dockerOptions.HostConfig.Mounts.find((m) => m.Target.startsWith(videoMountPrefix));
|
|
556
|
-
expect(videoMount).toBeDefined();
|
|
557
|
-
expect(videoMount.Type).toBe("bind");
|
|
558
|
-
expect(videoMount.ReadOnly).toBe(false);
|
|
559
|
-
expect(videoMount.Source).toContain(".skyramp/videos/");
|
|
560
|
-
expect(videoMount.Source).toContain(videoSubdir);
|
|
561
|
-
});
|
|
562
|
-
it("does not mount video directory for non-browser tests", async () => {
|
|
563
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
564
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
565
|
-
const service = new TestExecutionService();
|
|
566
|
-
await service.executeTest({
|
|
567
|
-
testFile: "/workspace/contract_test.py",
|
|
568
|
-
workspacePath: "/workspace",
|
|
569
|
-
language: "python",
|
|
570
|
-
testType: "contract",
|
|
571
|
-
});
|
|
572
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
573
|
-
const videoMounts = dockerOptions.HostConfig.Mounts.filter((m) => m.Target.includes(".skyramp/videos"));
|
|
574
|
-
expect(videoMounts).toHaveLength(0);
|
|
575
|
-
});
|
|
576
|
-
it("uses minimal static Playwright config for non-browser TypeScript tests", async () => {
|
|
577
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
578
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
579
|
-
let usedGeneratedConfig = false;
|
|
580
|
-
mockWriteFileSync.mockImplementation((filePath, content) => {
|
|
581
|
-
if (filePath.includes("skyramp-playwright-") && content.includes('video: "on"')) {
|
|
582
|
-
usedGeneratedConfig = true;
|
|
583
|
-
}
|
|
584
|
-
});
|
|
585
|
-
const service = new TestExecutionService();
|
|
586
|
-
await service.executeTest({
|
|
587
|
-
testFile: "/workspace/contract_test.spec.ts",
|
|
588
|
-
workspacePath: "/workspace",
|
|
589
|
-
language: "typescript",
|
|
590
|
-
testType: "contract",
|
|
591
|
-
});
|
|
592
|
-
// Should NOT generate a video-enabled config for contract test
|
|
593
|
-
expect(usedGeneratedConfig).toBe(false);
|
|
594
|
-
});
|
|
595
|
-
it("populates videoPath when video.webm exists after execution", async () => {
|
|
596
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
597
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
598
|
-
const videoSubdir = getVideoSubdir("/workspace/test.spec.ts");
|
|
599
|
-
mockReaddirSync.mockImplementation((dir, options) => {
|
|
600
|
-
// Mock video directory scan
|
|
601
|
-
if (dir.includes(".skyramp/videos") && dir.includes(videoSubdir)) {
|
|
602
|
-
if (options?.withFileTypes) {
|
|
603
|
-
return [{ name: "video.webm", isFile: () => true, isDirectory: () => false }];
|
|
604
|
-
}
|
|
605
|
-
return ["video.webm"];
|
|
606
|
-
}
|
|
607
|
-
// Default workspace files
|
|
608
|
-
if (options?.withFileTypes) {
|
|
609
|
-
return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
|
|
610
|
-
}
|
|
611
|
-
return ["test.spec.ts"];
|
|
612
|
-
});
|
|
613
|
-
const service = new TestExecutionService();
|
|
614
|
-
const result = await service.executeTest({
|
|
615
|
-
testFile: "/workspace/test.spec.ts",
|
|
616
|
-
workspacePath: "/workspace",
|
|
617
|
-
language: "typescript",
|
|
618
|
-
testType: "ui",
|
|
619
|
-
});
|
|
620
|
-
expect(result.videoPath).toBeDefined();
|
|
621
|
-
expect(result.videoPath).toContain(".skyramp/videos/");
|
|
622
|
-
expect(result.videoPath).toContain("video.webm");
|
|
623
|
-
});
|
|
624
|
-
it("leaves videoPath undefined when no video.webm found", async () => {
|
|
625
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
626
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
627
|
-
const videoSubdir = getVideoSubdir("/workspace/test.spec.ts");
|
|
628
|
-
mockReaddirSync.mockImplementation((dir, options) => {
|
|
629
|
-
// Mock empty video directory
|
|
630
|
-
if (dir.includes(".skyramp/videos") && dir.includes(videoSubdir)) {
|
|
631
|
-
if (options?.withFileTypes) {
|
|
632
|
-
return [];
|
|
633
|
-
}
|
|
634
|
-
return [];
|
|
635
|
-
}
|
|
636
|
-
// Default workspace files
|
|
637
|
-
if (options?.withFileTypes) {
|
|
638
|
-
return [{ name: "test.spec.ts", isFile: () => true, isDirectory: () => false }];
|
|
639
|
-
}
|
|
640
|
-
return ["test.spec.ts"];
|
|
641
|
-
});
|
|
642
|
-
const service = new TestExecutionService();
|
|
643
|
-
const result = await service.executeTest({
|
|
644
|
-
testFile: "/workspace/test.spec.ts",
|
|
645
|
-
workspacePath: "/workspace",
|
|
646
|
-
language: "typescript",
|
|
647
|
-
testType: "ui",
|
|
648
|
-
});
|
|
649
|
-
expect(result.videoPath).toBeUndefined();
|
|
650
|
-
});
|
|
651
|
-
it("leaves videoPath undefined for non-browser tests", async () => {
|
|
652
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
653
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
654
|
-
const service = new TestExecutionService();
|
|
655
|
-
const result = await service.executeTest({
|
|
656
|
-
testFile: "/workspace/integration_test.py",
|
|
657
|
-
workspacePath: "/workspace",
|
|
658
|
-
language: "python",
|
|
659
|
-
testType: "integration",
|
|
660
|
-
});
|
|
661
|
-
expect(result.videoPath).toBeUndefined();
|
|
662
|
-
});
|
|
663
|
-
});
|
|
664
|
-
// SKYR-3819: tests delivered to a cross-repo test repository (testsRepoDir) live
|
|
665
|
-
// outside workspacePath. The executor must mount the test repo under a reserved
|
|
666
|
-
// /home/user subdir — inside the generated Playwright config's testDir — and
|
|
667
|
-
// anchor the container test path there, or the runner reports "No tests found" /
|
|
668
|
-
// "file or directory not found".
|
|
669
|
-
describe("TestExecutionService.executeTest - cross-repo test repo mounts", () => {
|
|
670
|
-
let TestExecutionService;
|
|
671
|
-
let TEST_REPO_CONTAINER_SUBDIR;
|
|
672
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
673
|
-
beforeAll(async () => {
|
|
674
|
-
const mod = await import("./TestExecutionService.js");
|
|
675
|
-
TestExecutionService = mod.TestExecutionService;
|
|
676
|
-
TEST_REPO_CONTAINER_SUBDIR = mod.TEST_REPO_CONTAINER_SUBDIR;
|
|
677
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
678
|
-
});
|
|
679
|
-
const dirent = (name, dir = false) => ({
|
|
680
|
-
name,
|
|
681
|
-
isFile: () => !dir,
|
|
682
|
-
isDirectory: () => dir,
|
|
683
|
-
});
|
|
684
|
-
beforeEach(() => {
|
|
685
|
-
jest.clearAllMocks();
|
|
686
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
687
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
688
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
689
|
-
// Path-aware directory layout:
|
|
690
|
-
// /workspace → SUT repo (src/)
|
|
691
|
-
// /test-repo → cross-repo test repo (tests/, node_modules/, package.json)
|
|
692
|
-
mockReaddirSync.mockImplementation((dir, options) => {
|
|
693
|
-
const listing = {
|
|
694
|
-
"/workspace": [dirent("src", true)],
|
|
695
|
-
"/workspace/src": [],
|
|
696
|
-
"/test-repo": [dirent("tests", true), dirent("node_modules", true), dirent("package.json")],
|
|
697
|
-
"/test-repo/tests": [dirent("orders_test.py")],
|
|
698
|
-
};
|
|
699
|
-
const entries = listing[dir] ?? [];
|
|
700
|
-
return options?.withFileTypes ? entries : entries.map((e) => e.name);
|
|
701
|
-
});
|
|
702
|
-
});
|
|
703
|
-
it("mounts test repo entries under the reserved /home/user subdir with host mirrors", async () => {
|
|
704
|
-
const service = new TestExecutionService();
|
|
705
|
-
await service.executeTest({
|
|
706
|
-
testFile: "/test-repo/tests/orders_test.py",
|
|
707
|
-
workspacePath: "/workspace",
|
|
708
|
-
testRepoPath: "/test-repo",
|
|
709
|
-
language: "python",
|
|
710
|
-
testType: "integration",
|
|
711
|
-
});
|
|
712
|
-
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
713
|
-
const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
|
|
714
|
-
expect(mounts).toContainEqual(expect.objectContaining({
|
|
715
|
-
Source: "/test-repo/tests",
|
|
716
|
-
Target: `/home/user/${TEST_REPO_CONTAINER_SUBDIR}/tests`,
|
|
717
|
-
}));
|
|
718
|
-
// Host-absolute mirror so absolute references embedded in tests resolve
|
|
719
|
-
expect(mounts).toContainEqual(expect.objectContaining({ Source: "/test-repo/tests", Target: "/test-repo/tests" }));
|
|
720
|
-
});
|
|
721
|
-
it("anchors the container test path inside the reserved subdir", async () => {
|
|
722
|
-
const service = new TestExecutionService();
|
|
723
|
-
await service.executeTest({
|
|
724
|
-
testFile: "/test-repo/tests/orders_test.py",
|
|
725
|
-
workspacePath: "/workspace",
|
|
726
|
-
testRepoPath: "/test-repo",
|
|
727
|
-
language: "python",
|
|
728
|
-
testType: "integration",
|
|
729
|
-
});
|
|
730
|
-
const command = mockRun.mock.calls[0][1];
|
|
731
|
-
expect(command).toContain(`/home/user/${TEST_REPO_CONTAINER_SUBDIR}/tests/orders_test.py`);
|
|
732
|
-
});
|
|
733
|
-
it("applies mount exclusions and null shadows to the test repo", async () => {
|
|
734
|
-
const service = new TestExecutionService();
|
|
735
|
-
await service.executeTest({
|
|
736
|
-
testFile: "/test-repo/tests/orders_test.py",
|
|
737
|
-
workspacePath: "/workspace",
|
|
738
|
-
testRepoPath: "/test-repo",
|
|
739
|
-
language: "python",
|
|
740
|
-
testType: "integration",
|
|
741
|
-
});
|
|
742
|
-
const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
|
|
743
|
-
// node_modules is never mounted
|
|
744
|
-
expect(mounts.filter((m) => m.Source === "/test-repo/node_modules")).toHaveLength(0);
|
|
745
|
-
// package.json is shadowed with the empty-JSON file at both targets
|
|
746
|
-
const pkgShadows = mounts.filter((m) => m.Target === `/home/user/${TEST_REPO_CONTAINER_SUBDIR}/package.json` ||
|
|
747
|
-
m.Target === "/test-repo/package.json");
|
|
748
|
-
expect(pkgShadows).toHaveLength(2);
|
|
749
|
-
for (const m of pkgShadows) {
|
|
750
|
-
expect(m.Source).toMatch(/skyramp-empty\.json$/);
|
|
751
|
-
}
|
|
752
|
-
});
|
|
753
|
-
it("throws an actionable error when testFile is outside the workspace and no testRepoPath is known", async () => {
|
|
754
|
-
const service = new TestExecutionService();
|
|
755
|
-
await expect(service.executeTest({
|
|
756
|
-
testFile: "/test-repo/tests/orders_test.py",
|
|
757
|
-
workspacePath: "/workspace",
|
|
758
|
-
language: "python",
|
|
759
|
-
testType: "integration",
|
|
760
|
-
})).rejects.toThrow(/outside the workspace/);
|
|
761
|
-
});
|
|
762
|
-
it("does not add test repo mounts when testFile is inside the workspace", async () => {
|
|
763
|
-
mockReaddirSync.mockImplementation((dir, options) => {
|
|
764
|
-
const listing = {
|
|
765
|
-
"/workspace": [dirent("tests", true)],
|
|
766
|
-
"/workspace/tests": [dirent("orders_test.py")],
|
|
767
|
-
};
|
|
768
|
-
const entries = listing[dir] ?? [];
|
|
769
|
-
return options?.withFileTypes ? entries : entries.map((e) => e.name);
|
|
770
|
-
});
|
|
771
|
-
const service = new TestExecutionService();
|
|
772
|
-
await service.executeTest({
|
|
773
|
-
testFile: "/workspace/tests/orders_test.py",
|
|
774
|
-
workspacePath: "/workspace",
|
|
775
|
-
testRepoPath: "/test-repo",
|
|
776
|
-
language: "python",
|
|
777
|
-
testType: "integration",
|
|
778
|
-
});
|
|
779
|
-
const command = mockRun.mock.calls[0][1];
|
|
780
|
-
expect(command).toContain("/home/user/tests/orders_test.py");
|
|
781
|
-
const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
|
|
782
|
-
expect(mounts.filter((m) => m.Target.includes(TEST_REPO_CONTAINER_SUBDIR))).toHaveLength(0);
|
|
783
|
-
});
|
|
784
|
-
});
|
|
785
|
-
// Copilot review on PR #623: guard hazardous or stale cross-repo roots.
|
|
786
|
-
describe("TestExecutionService.executeTest - cross-repo root guards", () => {
|
|
787
|
-
let TestExecutionService;
|
|
788
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
789
|
-
beforeAll(async () => {
|
|
790
|
-
const mod = await import("./TestExecutionService.js");
|
|
791
|
-
TestExecutionService = mod.TestExecutionService;
|
|
792
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
793
|
-
});
|
|
794
|
-
beforeEach(() => {
|
|
795
|
-
jest.clearAllMocks();
|
|
796
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
797
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
798
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
799
|
-
});
|
|
800
|
-
afterEach(() => {
|
|
801
|
-
mockAccessSync.mockReset();
|
|
802
|
-
});
|
|
803
|
-
it("rejects a filesystem-root testRepoPath instead of mounting the host root", async () => {
|
|
804
|
-
const service = new TestExecutionService();
|
|
805
|
-
await expect(service.executeTest({
|
|
806
|
-
testFile: "/outside/tests/orders_test.py",
|
|
807
|
-
workspacePath: "/workspace",
|
|
808
|
-
testRepoPath: "/",
|
|
809
|
-
language: "python",
|
|
810
|
-
testType: "integration",
|
|
811
|
-
})).rejects.toThrow(/filesystem root/);
|
|
812
|
-
expect(mockRun).not.toHaveBeenCalled();
|
|
813
|
-
});
|
|
814
|
-
it("throws an actionable error when the cross-repo test repository is missing or unreadable", async () => {
|
|
815
|
-
mockAccessSync.mockImplementation((p) => {
|
|
816
|
-
if (String(p).startsWith("/test-repo")) {
|
|
817
|
-
throw new Error("EACCES");
|
|
818
|
-
}
|
|
819
|
-
});
|
|
820
|
-
const service = new TestExecutionService();
|
|
821
|
-
await expect(service.executeTest({
|
|
822
|
-
testFile: "/test-repo/tests/orders_test.py",
|
|
823
|
-
workspacePath: "/workspace",
|
|
824
|
-
testRepoPath: "/test-repo",
|
|
825
|
-
language: "python",
|
|
826
|
-
testType: "integration",
|
|
827
|
-
})).rejects.toThrow(/test repository path does not exist or is not readable/);
|
|
828
|
-
expect(mockRun).not.toHaveBeenCalled();
|
|
829
|
-
});
|
|
830
|
-
});
|
|
831
|
-
describe("findNestedNodeModules", () => {
|
|
832
|
-
let findNestedNodeModules;
|
|
833
|
-
beforeAll(async () => {
|
|
834
|
-
const mod = await import("./TestExecutionService.js");
|
|
835
|
-
findNestedNodeModules = mod.findNestedNodeModules;
|
|
836
|
-
});
|
|
837
|
-
beforeEach(() => {
|
|
838
|
-
// These tests need real directory traversal — delegate the readdirSync
|
|
839
|
-
// mock (used by TestExecutionService.ts's `fs` binding) back to the real
|
|
840
|
-
// fs so it reflects the on-disk tree built by makeTree() below.
|
|
841
|
-
mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
|
|
842
|
-
});
|
|
843
|
-
const relSorted = (root, abs) => abs.map((p) => path.relative(root, p).replace(/\\/g, "/")).sort();
|
|
844
|
-
it("returns nested node_modules but not the top-level one", () => {
|
|
845
|
-
const root = makeTree({
|
|
846
|
-
"node_modules/playwright/index.js": "x",
|
|
847
|
-
"tests/ui-testing/node_modules/@playwright/test/index.js": "x",
|
|
848
|
-
"tests/ui-testing/pages/dashboard-list.js": "x",
|
|
849
|
-
"packages/web/node_modules/react/index.js": "x",
|
|
850
|
-
});
|
|
851
|
-
expect(relSorted(root, findNestedNodeModules(root))).toEqual([
|
|
852
|
-
"packages/web/node_modules",
|
|
853
|
-
"tests/ui-testing/node_modules",
|
|
854
|
-
]);
|
|
855
|
-
});
|
|
856
|
-
it("never descends into a node_modules (no nested-inside-node_modules hits)", () => {
|
|
857
|
-
const root = makeTree({
|
|
858
|
-
"tests/node_modules/foo/node_modules/bar/index.js": "x",
|
|
859
|
-
});
|
|
860
|
-
expect(relSorted(root, findNestedNodeModules(root))).toEqual([
|
|
861
|
-
"tests/node_modules",
|
|
862
|
-
]);
|
|
863
|
-
});
|
|
864
|
-
it("returns empty when there are no nested node_modules", () => {
|
|
865
|
-
const root = makeTree({ "node_modules/a/index.js": "x", "src/app.js": "x" });
|
|
866
|
-
expect(findNestedNodeModules(root)).toEqual([]);
|
|
867
|
-
});
|
|
868
|
-
});
|
|
869
|
-
describe("TestExecutionService.executeTest - nested node_modules shadow + NODE_PATH wrap", () => {
|
|
870
|
-
let TestExecutionService;
|
|
871
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
872
|
-
beforeAll(async () => {
|
|
873
|
-
const mod = await import("./TestExecutionService.js");
|
|
874
|
-
TestExecutionService = mod.TestExecutionService;
|
|
875
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
876
|
-
});
|
|
877
|
-
beforeEach(() => {
|
|
878
|
-
jest.clearAllMocks();
|
|
879
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
880
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
881
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
882
|
-
// Real directory traversal so findNestedNodeModules / the workspace-mount
|
|
883
|
-
// scan see the on-disk tree built by makeTree() below (same delegation as
|
|
884
|
-
// the findNestedNodeModules describe block above).
|
|
885
|
-
mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
|
|
886
|
-
// Real existsSync too, so the production `if (!fs.existsSync(pkgPath))
|
|
887
|
-
// continue` package-existence gate is genuinely exercised against the
|
|
888
|
-
// on-disk tree instead of the file-level always-true default.
|
|
889
|
-
mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
|
|
890
|
-
});
|
|
891
|
-
afterEach(() => {
|
|
892
|
-
// Restore the file-level always-true default for describes that follow.
|
|
893
|
-
mockExistsSync.mockReturnValue(true);
|
|
894
|
-
});
|
|
895
|
-
it("shadows a leaked nested @playwright/test and wraps the run command with NODE_PATH", async () => {
|
|
896
|
-
const root = makeTree({
|
|
897
|
-
"tests/node_modules/@playwright/test/index.js": "module.exports = {};",
|
|
898
|
-
"tests/sample.spec.ts": "test('noop', () => {});",
|
|
899
|
-
});
|
|
900
|
-
const service = new TestExecutionService();
|
|
901
|
-
await service.executeTest({
|
|
902
|
-
testFile: path.join(root, "tests/sample.spec.ts"),
|
|
903
|
-
workspacePath: root,
|
|
904
|
-
language: "typescript",
|
|
905
|
-
testType: "smoke",
|
|
906
|
-
});
|
|
907
|
-
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
908
|
-
const dockerOptions = mockRun.mock.calls[0][3];
|
|
909
|
-
const mounts = dockerOptions.HostConfig.Mounts;
|
|
910
|
-
const shadowMount = mounts.find((m) => m.ReadOnly && m.Target.endsWith("tests/node_modules/@playwright/test"));
|
|
911
|
-
expect(shadowMount).toBeDefined();
|
|
912
|
-
// The fixture only creates a nested @playwright/test — no
|
|
913
|
-
// @skyramp/skyramp package exists on disk, so the existence gate
|
|
914
|
-
// (`if (!fs.existsSync(pkgPath)) continue`) must skip it. Asserting its
|
|
915
|
-
// absence here only proves the gate is live because existsSync is now
|
|
916
|
-
// delegated to the real fs above (it would false-pass against the
|
|
917
|
-
// always-true mock default).
|
|
918
|
-
const nonexistentPkgShadow = mounts.find((m) => m.Target.endsWith("tests/node_modules/@skyramp/skyramp"));
|
|
919
|
-
expect(nonexistentPkgShadow).toBeUndefined();
|
|
920
|
-
const command = mockRun.mock.calls[0][1];
|
|
921
|
-
expect(command.slice(0, 2)).toEqual(["bash", "-c"]);
|
|
922
|
-
expect(command[2]).toContain("NODE_PATH");
|
|
923
|
-
expect(command[2]).toContain("/root/runner.sh");
|
|
924
|
-
});
|
|
925
|
-
});
|
|
926
|
-
describe("TestExecutionService.executeTest - cross-repo context nested node_modules shadow", () => {
|
|
927
|
-
let TestExecutionService;
|
|
928
|
-
let TEST_REPO_CONTAINER_SUBDIR;
|
|
929
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
930
|
-
beforeAll(async () => {
|
|
931
|
-
const mod = await import("./TestExecutionService.js");
|
|
932
|
-
TestExecutionService = mod.TestExecutionService;
|
|
933
|
-
TEST_REPO_CONTAINER_SUBDIR = mod.TEST_REPO_CONTAINER_SUBDIR;
|
|
934
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
935
|
-
});
|
|
936
|
-
beforeEach(() => {
|
|
937
|
-
jest.clearAllMocks();
|
|
938
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
939
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
940
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
941
|
-
// Real directory traversal so findNestedNodeModules sees the on-disk tree
|
|
942
|
-
// built by makeTree() below (same delegation as the sibling
|
|
943
|
-
// workspace-context describe block above).
|
|
944
|
-
mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
|
|
945
|
-
// Real existsSync too, so the production `if (!fs.existsSync(pkgPath))
|
|
946
|
-
// continue` package-existence gate is genuinely exercised against the
|
|
947
|
-
// on-disk tree instead of the file-level always-true default.
|
|
948
|
-
mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
|
|
949
|
-
});
|
|
950
|
-
afterEach(() => {
|
|
951
|
-
// Restore the file-level always-true default for describes that follow.
|
|
952
|
-
mockExistsSync.mockReturnValue(true);
|
|
953
|
-
});
|
|
954
|
-
it("shadows a leaked nested @playwright/test in the cross-repo test root, mirroring at the host path unconditionally", async () => {
|
|
955
|
-
const workspaceRoot = makeTree({});
|
|
956
|
-
const testRepoRoot = makeTree({
|
|
957
|
-
"e2e/node_modules/@playwright/test/index.js": "module.exports = {};",
|
|
958
|
-
"e2e/sample.spec.ts": "test('noop', () => {});",
|
|
959
|
-
});
|
|
960
|
-
const service = new TestExecutionService();
|
|
961
|
-
await service.executeTest({
|
|
962
|
-
testFile: path.join(testRepoRoot, "e2e/sample.spec.ts"),
|
|
963
|
-
workspacePath: workspaceRoot,
|
|
964
|
-
testRepoPath: testRepoRoot,
|
|
965
|
-
language: "typescript",
|
|
966
|
-
testType: "smoke",
|
|
967
|
-
});
|
|
968
|
-
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
969
|
-
const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
|
|
970
|
-
// Container-side shadow lives under the reserved test-repo subdir.
|
|
971
|
-
const containerShadow = mounts.find((m) => m.ReadOnly &&
|
|
972
|
-
m.Target.includes(TEST_REPO_CONTAINER_SUBDIR) &&
|
|
973
|
-
m.Target.endsWith("e2e/node_modules/@playwright/test"));
|
|
974
|
-
expect(containerShadow).toBeDefined();
|
|
975
|
-
// Cross-repo mounts always mirror at the host-absolute path too (unlike
|
|
976
|
-
// the workspace context, where mirroring is conditional on
|
|
977
|
-
// workspacePath !== containerMountPath) — assert that unconditional
|
|
978
|
-
// mirror explicitly, since it's what distinguishes this context.
|
|
979
|
-
const hostPkgPath = path.join(testRepoRoot, "e2e/node_modules/@playwright/test");
|
|
980
|
-
const hostMirrorShadow = mounts.find((m) => m.ReadOnly && m.Target === hostPkgPath);
|
|
981
|
-
expect(hostMirrorShadow).toBeDefined();
|
|
982
|
-
expect(hostMirrorShadow.Source).toBe(containerShadow.Source);
|
|
983
|
-
});
|
|
984
|
-
});
|
|
985
|
-
describe("TestExecutionService.executeTest - root-level Playwright config shadow", () => {
|
|
986
|
-
let TestExecutionService;
|
|
987
|
-
let EXECUTOR_DOCKER_IMAGE;
|
|
988
|
-
beforeAll(async () => {
|
|
989
|
-
const mod = await import("./TestExecutionService.js");
|
|
990
|
-
TestExecutionService = mod.TestExecutionService;
|
|
991
|
-
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
992
|
-
});
|
|
993
|
-
beforeEach(() => {
|
|
994
|
-
jest.clearAllMocks();
|
|
995
|
-
mockListImages.mockResolvedValue([{ RepoTags: [EXECUTOR_DOCKER_IMAGE] }]);
|
|
996
|
-
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
997
|
-
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
998
|
-
// Real directory traversal so the entry-mount filter and the
|
|
999
|
-
// PLAYWRIGHT_CONFIG_FILES shadow scan see the on-disk tree built by
|
|
1000
|
-
// makeTree() below (same delegation as the sibling shadow describes above).
|
|
1001
|
-
mockReaddirSync.mockImplementation((...args) => actualFs.readdirSync(...args));
|
|
1002
|
-
mockExistsSync.mockImplementation((...args) => actualFs.existsSync(...args));
|
|
1003
|
-
});
|
|
1004
|
-
afterEach(() => {
|
|
1005
|
-
// Restore the file-level always-true default for describes that follow.
|
|
1006
|
-
mockExistsSync.mockReturnValue(true);
|
|
1007
|
-
});
|
|
1008
|
-
// Regression test: a root-level playwright.config.js used to win the
|
|
1009
|
-
// pushMount target-dedup race over its minimal-config shadow, because the
|
|
1010
|
-
// entry-mount filter excluded EXCLUDED_MOUNT_ITEMS/MOUNT_NULL_ITEMS but not
|
|
1011
|
-
// PLAYWRIGHT_CONFIG_FILES — so the repo's own root config (which may import
|
|
1012
|
-
// dotenv, or simply isn't the video-enabled config the executor expects)
|
|
1013
|
-
// leaked into the container instead of being shadowed.
|
|
1014
|
-
it("shadows a root-level playwright.config.js with the minimal/generated config instead of the workspace's own file", async () => {
|
|
1015
|
-
const root = makeTree({
|
|
1016
|
-
"playwright.config.js": "module.exports = { testDir: './tests' };",
|
|
1017
|
-
"tests/sample.spec.ts": "test('noop', () => {});",
|
|
1018
|
-
});
|
|
1019
|
-
const service = new TestExecutionService();
|
|
1020
|
-
await service.executeTest({
|
|
1021
|
-
testFile: path.join(root, "tests/sample.spec.ts"),
|
|
1022
|
-
workspacePath: root,
|
|
1023
|
-
language: "typescript",
|
|
1024
|
-
testType: "smoke",
|
|
1025
|
-
});
|
|
1026
|
-
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
1027
|
-
const mounts = mockRun.mock.calls[0][3].HostConfig.Mounts;
|
|
1028
|
-
const configMount = mounts.find((m) => m.Target === path.join("/home/user", "playwright.config.js"));
|
|
1029
|
-
expect(configMount).toBeDefined();
|
|
1030
|
-
expect(configMount.Source).not.toBe(path.join(root, "playwright.config.js"));
|
|
1031
|
-
});
|
|
1032
|
-
});
|