@skyramp/mcp 0.0.62 → 0.0.63-rc.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/index.js +18 -26
- package/build/prompts/test-maintenance/drift-analysis-prompt.js +59 -0
- package/build/prompts/test-maintenance/driftAnalysisSections.js +153 -0
- package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -9
- package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +34 -38
- package/build/prompts/test-recommendation/test-recommendation-prompt.js +56 -9
- package/build/prompts/testbot/testbot-prompts.js +113 -100
- package/build/services/DriftAnalysisService.js +1 -1
- package/build/services/ScenarioGenerationService.js +5 -1
- package/build/services/TestExecutionService.js +2 -24
- package/build/services/TestExecutionService.test.js +167 -0
- package/build/services/containerEnv.js +35 -0
- package/build/tools/generate-tests/generateScenarioRestTool.js +7 -1
- package/build/tools/submitReportTool.js +6 -6
- package/build/tools/test-management/actionsTool.js +396 -0
- package/build/tools/test-management/analyzeChangesTool.js +750 -0
- package/build/tools/test-management/analyzeTestHealthTool.js +132 -0
- package/build/tools/test-management/executeTestsTool.js +198 -0
- package/build/tools/test-management/index.js +5 -0
- package/build/tools/test-management/stateCleanupTool.js +163 -0
- package/build/tools/test-recommendation/recommendTestsTool.js +1 -1
- package/build/utils/analyze-openapi.js +2 -2
- package/build/utils/pr-comment-parser.js +157 -36
- package/build/utils/pr-comment-parser.test.js +427 -0
- package/package.json +1 -1
- package/build/tools/initTestbotTool.js +0 -187
- package/build/tools/initTestbotTool.test.js +0 -194
- package/build/tools/test-recommendation/analyzeRepositoryTool.js +0 -505
|
@@ -3,115 +3,123 @@ import { z } from "zod";
|
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
4
|
import { AnalyticsService } from "../../services/AnalyticsService.js";
|
|
5
5
|
import { MAX_TESTS_TO_GENERATE, MAX_RECOMMENDATIONS } from "../test-recommendation/recommendationSections.js";
|
|
6
|
-
function getTestbotPrompt(prTitle, prDescription, diffFile, testDirectory, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE) {
|
|
6
|
+
function getTestbotPrompt(prTitle, prDescription, diffFile, testDirectory, summaryOutputFile, repositoryPath, baseBranch, maxRecommendations = MAX_RECOMMENDATIONS, maxGenerate = MAX_TESTS_TO_GENERATE, prNumber) {
|
|
7
7
|
return `<TITLE>${prTitle}</TITLE>
|
|
8
8
|
<DESCRIPTION>${prDescription}</DESCRIPTION>
|
|
9
9
|
<CODE CHANGES>${diffFile}</CODE CHANGES>
|
|
10
10
|
<TEST DIRECTORY>${testDirectory}</TEST DIRECTORY>
|
|
11
11
|
<REPOSITORY PATH>${repositoryPath}</REPOSITORY PATH>
|
|
12
12
|
|
|
13
|
-
Use the Skyramp MCP server tools
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
13
|
+
Use the Skyramp MCP server tools. Follow the steps below in order.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Step 1: Analyze
|
|
18
|
+
|
|
19
|
+
Read the diff at \`${diffFile}\`.
|
|
20
|
+
If all changed files are non-application (CI/CD, docs, lock files, config only) → skip to Step 4 (Submit Report) with empty arrays.
|
|
21
|
+
|
|
22
|
+
Otherwise:
|
|
23
|
+
|
|
24
|
+
**Incremental mode:** Tests generated by prior bot runs on this PR are still in the
|
|
25
|
+
working tree. Step 2/3 handles their maintenance (drift detection, health checks, fixes).
|
|
26
|
+
Only generate tests for NEW endpoints or code paths not already covered by existing bot
|
|
27
|
+
tests. The analyze tool uses PR comment history to avoid duplicates.
|
|
28
|
+
|
|
29
|
+
1. Call \`skyramp_analyze_changes\` with \`repositoryPath\`: "${repositoryPath}", \`scope\`: "branch_diff", \`topN\`: ${maxRecommendations}${prNumber ? `, \`prNumber\`: ${prNumber}` : ""} — discovers existing Skyramp tests, scans endpoints changed in the diff, loads workspace config, and returns ${maxRecommendations} ranked ADD recommendations.${prNumber ? " Uses PR comment history to avoid re-recommending already-generated tests." : ""}
|
|
30
|
+
2. Call \`skyramp_analyze_test_health\` with the \`stateFile\` from step 1 (skip if zero existing tests found) — scores each existing test for drift against the diff and assigns UPDATE / REGENERATE / VERIFY / ADD actions.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Step 2: Decide — one action per affected test / endpoint
|
|
35
|
+
|
|
36
|
+
Using the diff, the recommendations, and the health assessment, assign exactly one action to each item:
|
|
37
|
+
|
|
38
|
+
### For each **existing Skyramp test**:
|
|
39
|
+
- **UPDATE** — the diff touches the endpoint this test covers AND adds/changes fields the test should assert (e.g. new response field, changed status code, renamed path). The test still runs but has a coverage gap or will break.
|
|
40
|
+
- **REGENERATE** — the endpoint was substantially restructured or the test is fundamentally broken by the diff.
|
|
41
|
+
- **VERIFY** — the diff touches related code but the test is unaffected; no action needed.
|
|
42
|
+
- **DELETE** — the endpoint the test covers was removed entirely.
|
|
43
|
+
- **ADD** — existing tests for this endpoint do not capture a new scenario introduced by the diff (e.g. a new flow, a new field combination). A net-new test is needed alongside the existing ones.
|
|
44
|
+
|
|
45
|
+
### For each **endpoint whose route definition is new in the diff** (no existing Skyramp test):
|
|
46
|
+
- **ADD** — the diff introduced this route; generate a new test.
|
|
47
|
+
- **VERIFY** — the endpoint existed before this diff (only a model/field change touched it); log as a coverage gap but do not generate a test.
|
|
48
|
+
|
|
49
|
+
### Decision rules (apply in order):
|
|
50
|
+
1. If the diff adds/removes/renames a field in a response this test asserts → **UPDATE** (not ADD).
|
|
51
|
+
2. If the diff adds a **brand-new route definition** (e.g. a new \`@router.get\`, \`@app.route\`, \`router.get()\` line) → **ADD**.
|
|
52
|
+
2.5. If the diff makes an **additive, non-breaking change** to an existing route (e.g. new optional query params, new optional request fields, new optional response fields) AND an existing test already covers that route → **UPDATE** that test to assert the new behavior. Do NOT create a new file.
|
|
53
|
+
3. If an existing test covers the endpoint but the new behavior requires a **distinct setup or workflow** (e.g. a new auth path, a new multi-step flow, a new error/edge-case branch) → **ADD** (alongside the existing test).
|
|
54
|
+
4. If the test is unrelated to the diff → **VERIFY** (no action).
|
|
55
|
+
5. Only use **ADD** for endpoints whose route was introduced in this diff. An endpoint that existed before but now lacks a test is a pre-existing coverage gap — log it in \`additionalRecommendations\`, do NOT generate a test for it.
|
|
56
|
+
6. Do NOT add a new test when an UPDATE to an existing test is the right fix.
|
|
57
|
+
|
|
58
|
+
Output your decision table:
|
|
59
|
+
\`\`\`
|
|
60
|
+
Test/Endpoint | Action | Reason
|
|
61
|
+
<file or METHOD /path> | <ACTION> | <1 sentence>
|
|
62
|
+
\`\`\`
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Step 3: Act
|
|
67
|
+
|
|
68
|
+
Execute the actions from Step 2. Limit total generated/updated tests to ${maxGenerate}.
|
|
69
|
+
|
|
70
|
+
### UPDATE
|
|
71
|
+
Edit the existing test file directly:
|
|
72
|
+
- Add missing assertions for new response fields (e.g. \`assert "archived" in resp\` or \`assert resp["archived"] >= 0\`).
|
|
73
|
+
- Fix path/method changes in the test.
|
|
74
|
+
- Do not regenerate — only apply the minimal change needed.
|
|
75
|
+
|
|
76
|
+
### REGENERATE
|
|
77
|
+
Call the appropriate generation tool to replace the existing test from scratch.
|
|
78
|
+
Use the same filename so it overwrites the old file.
|
|
79
|
+
|
|
80
|
+
### ADD
|
|
81
|
+
Generate a net-new test. Use a unique descriptive filename to avoid overwriting existing files.
|
|
82
|
+
|
|
83
|
+
**How to generate each type (for ADD and REGENERATE):**
|
|
84
|
+
- **Integration**: call \`skyramp_scenario_test_generation\` per step (sequentially), then \`skyramp_integration_test_generation\` with the scenario file.
|
|
85
|
+
Scenario JSON goes in the same \`outputDir\` (e.g. \`tests/scenario_<name>.json\`), not \`.skyramp/\`.
|
|
86
|
+
- **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`, and \`requestData\` for POST/PUT/PATCH.
|
|
87
|
+
Pass \`apiSchema\` if an OpenAPI spec exists.
|
|
88
|
+
- **Fuzz**: call \`skyramp_fuzz_test_generation\` with \`endpointURL\`, \`method\`, \`requestData\`.
|
|
89
|
+
- **E2E/UI**: only if relevant Playwright traces exist in \`${testDirectory}\`, repo root, or \`.skyramp/\`.
|
|
90
|
+
Without traces, move to \`additionalRecommendations\` with scenario steps and trace recording instructions.
|
|
91
|
+
- Skip smoke tests entirely.
|
|
92
|
+
|
|
93
|
+
**Scenario quality:** Verify preconditions before each step (e.g. create before update).
|
|
94
|
+
|
|
95
|
+
**After generation, fix chaining only:**
|
|
96
|
+
- Path params like \`id = 'id'\` → \`skyramp.get_response_value(prev_response, "id")\`
|
|
97
|
+
- Hardcoded IDs in request bodies → dynamic values from prior response
|
|
98
|
+
- Change ONLY chaining values. Preserve everything else exactly as generated.
|
|
99
|
+
|
|
100
|
+
After all actions, execute the changed/generated test files and record pass/fail.
|
|
101
|
+
|
|
102
|
+
### VERIFY / DELETE
|
|
103
|
+
- VERIFY: no file changes. Note in \`testMaintenance\`.
|
|
104
|
+
- DELETE: remove the test file.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Step 4: Submit Report
|
|
109
|
+
|
|
110
|
+
Call \`skyramp_submit_report\` with \`summaryOutputFile\`: "${summaryOutputFile}".
|
|
101
111
|
|
|
102
112
|
\`commitMessage\`: under 72 chars, e.g. "add integration tests for /products and /orders"
|
|
103
113
|
|
|
104
|
-
**newTestsCreated** —
|
|
105
|
-
\`testType\`, \`endpoint\`, \`fileName\`, \`description\`, \`scenarioFile
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
Every test file in the commit should appear here. If you over-generated, delete extras first.
|
|
109
|
-
If no tests were generated (e.g. frontend-only PR without traces), pass an empty array.
|
|
114
|
+
**newTestsCreated** — every file generated or updated (at most ${maxGenerate}):
|
|
115
|
+
\`testType\`, \`endpoint\`, \`fileName\`, \`description\`, \`scenarioFile\`
|
|
116
|
+
If action was UPDATE, set \`testType\` to \`"<type> (updated)"\`.
|
|
117
|
+
If no tests were generated or updated, pass an empty array.
|
|
110
118
|
|
|
111
|
-
**additionalRecommendations** —
|
|
112
|
-
\`testType\`, \`scenarioName\`, \`priority\`, \`description\`, \`steps
|
|
119
|
+
**additionalRecommendations** — items you could not act on (quota exceeded, no traces, etc.):
|
|
120
|
+
\`testType\`, \`scenarioName\`, \`priority\`, \`description\`, \`steps\`
|
|
113
121
|
|
|
114
|
-
**businessCaseAnalysis** — based only on
|
|
122
|
+
**businessCaseAnalysis** — 2-3 sentences based only on the diff and tool outputs.`;
|
|
115
123
|
}
|
|
116
124
|
export function registerTestbotPrompt(server) {
|
|
117
125
|
logger.info("Registering testbot prompt");
|
|
@@ -144,9 +152,13 @@ export function registerTestbotPrompt(server) {
|
|
|
144
152
|
.number()
|
|
145
153
|
.default(MAX_TESTS_TO_GENERATE)
|
|
146
154
|
.describe(`Maximum number of tests to generate.`),
|
|
155
|
+
prNumber: z
|
|
156
|
+
.number()
|
|
157
|
+
.optional()
|
|
158
|
+
.describe("GitHub PR number. Passed to skyramp_analyze_changes to fetch previous TestBot comments for recommendation consistency across commits."),
|
|
147
159
|
},
|
|
148
160
|
}, (args) => {
|
|
149
|
-
const prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.diffFile, args.testDirectory, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate);
|
|
161
|
+
const prompt = getTestbotPrompt(args.prTitle, args.prDescription, args.diffFile, args.testDirectory, args.summaryOutputFile, args.repositoryPath, args.baseBranch, args.maxRecommendations, args.maxGenerate, args.prNumber);
|
|
150
162
|
AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
|
|
151
163
|
return {
|
|
152
164
|
messages: [
|
|
@@ -179,7 +191,8 @@ export function registerTestbotResource(server) {
|
|
|
179
191
|
const param = (name, fallback) => uri.searchParams.get(name) ?? fallback;
|
|
180
192
|
const maxRec = parseInt(uri.searchParams.get("maxRecommendations") || "", 10);
|
|
181
193
|
const maxGen = parseInt(uri.searchParams.get("maxGenerate") || "", 10);
|
|
182
|
-
const
|
|
194
|
+
const prNum = parseInt(uri.searchParams.get("prNumber") || "", 10);
|
|
195
|
+
const prompt = getTestbotPrompt(param("prTitle", ""), param("prDescription", ""), param("diffFile", ".skyramp_git_diff"), param("testDirectory", "tests"), param("summaryOutputFile", ""), param("repositoryPath", "."), uri.searchParams.get("baseBranch") || undefined, isNaN(maxRec) ? MAX_RECOMMENDATIONS : maxRec, isNaN(maxGen) ? MAX_TESTS_TO_GENERATE : maxGen, isNaN(prNum) ? undefined : prNum);
|
|
183
196
|
AnalyticsService.pushMCPToolEvent("skyramp_testbot_prompt", undefined, {}).catch(() => { });
|
|
184
197
|
return {
|
|
185
198
|
contents: [
|
|
@@ -42,7 +42,7 @@ export class EnhancedDriftAnalysisService {
|
|
|
42
42
|
if (!(await this.git.checkIsRepo())) {
|
|
43
43
|
throw new Error(`Not a git repository: ${repositoryPath}`);
|
|
44
44
|
}
|
|
45
|
-
|
|
45
|
+
const baseline = baselineCommit ||
|
|
46
46
|
(await this.getTestBaselineCommit(testFile, repositoryPath));
|
|
47
47
|
// Handle no git history case
|
|
48
48
|
if (!baseline) {
|
|
@@ -129,9 +129,13 @@ ${JSON.stringify(traceRequest, null, 2)}
|
|
|
129
129
|
.some(v => v.includes("application/json"));
|
|
130
130
|
const responseBody = params.responseBody || (isJsonResponse ? "{}" : "");
|
|
131
131
|
const authHeaderName = params.authHeader || "Authorization";
|
|
132
|
+
let authValue = params.authToken ?? "";
|
|
133
|
+
if (!authValue && authHeaderName === "Authorization") {
|
|
134
|
+
authValue = "Bearer SKYRAMP_PLACEHOLDER_TOKEN";
|
|
135
|
+
}
|
|
132
136
|
const requestHeaders = {
|
|
133
137
|
"Content-Type": ["application/json"],
|
|
134
|
-
[authHeaderName]: [
|
|
138
|
+
[authHeaderName]: [authValue],
|
|
135
139
|
};
|
|
136
140
|
return {
|
|
137
141
|
Source: "192.168.65.1:39998",
|
|
@@ -4,6 +4,7 @@ import fs from "fs";
|
|
|
4
4
|
import { Writable } from "stream";
|
|
5
5
|
import { stripVTControlCharacters } from "util";
|
|
6
6
|
import { logger } from "../utils/logger.js";
|
|
7
|
+
import { buildContainerEnv } from "./containerEnv.js";
|
|
7
8
|
const DEFAULT_TIMEOUT = 300000; // 5 minutes
|
|
8
9
|
const MAX_CONCURRENT_EXECUTIONS = 5;
|
|
9
10
|
export const EXECUTOR_DOCKER_IMAGE = "skyramp/executor:v1.3.13";
|
|
@@ -379,30 +380,7 @@ export class TestExecutionService {
|
|
|
379
380
|
mountedPaths.add(saveStorageTarget);
|
|
380
381
|
}
|
|
381
382
|
}
|
|
382
|
-
|
|
383
|
-
const env = [
|
|
384
|
-
`SKYRAMP_TEST_TOKEN=${options.token || ""}`,
|
|
385
|
-
"SKYRAMP_IN_DOCKER=true",
|
|
386
|
-
];
|
|
387
|
-
// Skyramp-generated tests are standalone HTTP tests that never need host repo
|
|
388
|
-
// conftest.py files or pytest configuration. --noconftest prevents loading any
|
|
389
|
-
// conftest in the test directory tree (avoids missing deps like boto3, django).
|
|
390
|
-
// -c /dev/null overrides all config file discovery (pyproject.toml, pytest.ini,
|
|
391
|
-
// setup.cfg, tox.ini) so user-repo plugins (e.g. pytest-timeout) not installed
|
|
392
|
-
// in the executor container don't cause INTERNALERROR at collection time.
|
|
393
|
-
if (options.language === "python") {
|
|
394
|
-
env.push(`PYTEST_ADDOPTS=--noconftest -c /dev/null`);
|
|
395
|
-
}
|
|
396
|
-
// Add save storage path to environment if provided
|
|
397
|
-
if (saveStorageTargetPath) {
|
|
398
|
-
env.push(`PLAYWRIGHT_SAVE_STORAGE_PATH=${saveStorageTargetPath}`);
|
|
399
|
-
}
|
|
400
|
-
if (process.env.SKYRAMP_DEBUG) {
|
|
401
|
-
env.push(`SKYRAMP_DEBUG=${process.env.SKYRAMP_DEBUG}`);
|
|
402
|
-
}
|
|
403
|
-
if (process.env.API_KEY) {
|
|
404
|
-
env.push(`API_KEY=${process.env.API_KEY}`);
|
|
405
|
-
}
|
|
383
|
+
const env = buildContainerEnv(options, saveStorageTargetPath);
|
|
406
384
|
// Capture output
|
|
407
385
|
let output = "";
|
|
408
386
|
class DockerStream extends Writable {
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { buildContainerEnv } from "./containerEnv.js";
|
|
2
|
+
// Mock dockerode before importing TestExecutionService
|
|
3
|
+
const mockRun = jest.fn();
|
|
4
|
+
const mockListImages = jest.fn();
|
|
5
|
+
jest.mock("dockerode", () => {
|
|
6
|
+
return jest.fn().mockImplementation(() => ({
|
|
7
|
+
run: mockRun,
|
|
8
|
+
listImages: mockListImages,
|
|
9
|
+
}));
|
|
10
|
+
});
|
|
11
|
+
// Mock fs for workspace/file validation
|
|
12
|
+
jest.mock("fs", () => ({
|
|
13
|
+
...jest.requireActual("fs"),
|
|
14
|
+
accessSync: jest.fn(),
|
|
15
|
+
existsSync: jest.fn().mockReturnValue(true),
|
|
16
|
+
readdirSync: jest.fn().mockReturnValue(["test_file.py"]),
|
|
17
|
+
readFileSync: jest.fn().mockReturnValue(""),
|
|
18
|
+
}));
|
|
19
|
+
// Mock logger
|
|
20
|
+
jest.mock("../utils/logger.js", () => ({
|
|
21
|
+
logger: {
|
|
22
|
+
debug: jest.fn(),
|
|
23
|
+
info: jest.fn(),
|
|
24
|
+
error: jest.fn(),
|
|
25
|
+
warning: jest.fn(),
|
|
26
|
+
},
|
|
27
|
+
}));
|
|
28
|
+
describe("buildContainerEnv", () => {
|
|
29
|
+
const baseOptions = { token: "test-token", language: "python" };
|
|
30
|
+
const emptyHostEnv = {};
|
|
31
|
+
it("always includes SKYRAMP_TEST_TOKEN and SKYRAMP_IN_DOCKER", () => {
|
|
32
|
+
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
33
|
+
expect(env).toContain("SKYRAMP_TEST_TOKEN=test-token");
|
|
34
|
+
expect(env).toContain("SKYRAMP_IN_DOCKER=true");
|
|
35
|
+
});
|
|
36
|
+
it("defaults token to empty string when not provided", () => {
|
|
37
|
+
const env = buildContainerEnv({ language: "typescript" }, undefined, emptyHostEnv);
|
|
38
|
+
expect(env).toContain("SKYRAMP_TEST_TOKEN=");
|
|
39
|
+
});
|
|
40
|
+
it("adds PYTEST_ADDOPTS for python language", () => {
|
|
41
|
+
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
42
|
+
expect(env).toContain("PYTEST_ADDOPTS=--noconftest");
|
|
43
|
+
});
|
|
44
|
+
it("does not add PYTEST_ADDOPTS for non-python language", () => {
|
|
45
|
+
const env = buildContainerEnv({ ...baseOptions, language: "typescript" }, undefined, emptyHostEnv);
|
|
46
|
+
expect(env.find((e) => e.startsWith("PYTEST_ADDOPTS="))).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
it("adds PLAYWRIGHT_SAVE_STORAGE_PATH when saveStoragePath provided", () => {
|
|
49
|
+
const env = buildContainerEnv(baseOptions, "/tmp/storage", emptyHostEnv);
|
|
50
|
+
expect(env).toContain("PLAYWRIGHT_SAVE_STORAGE_PATH=/tmp/storage");
|
|
51
|
+
});
|
|
52
|
+
it("does not add PLAYWRIGHT_SAVE_STORAGE_PATH when not provided", () => {
|
|
53
|
+
const env = buildContainerEnv(baseOptions, undefined, emptyHostEnv);
|
|
54
|
+
expect(env.find((e) => e.startsWith("PLAYWRIGHT_SAVE_STORAGE_PATH="))).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
describe("base URL env var forwarding", () => {
|
|
57
|
+
it("forwards SKYRAMP_TEST_BASE_URL from host env", () => {
|
|
58
|
+
const hostEnv = { SKYRAMP_TEST_BASE_URL: "http://localhost:8000" };
|
|
59
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
60
|
+
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://localhost:8000");
|
|
61
|
+
});
|
|
62
|
+
it("forwards SKYRAMP_TEST_SERVICE_URL_* vars from host env", () => {
|
|
63
|
+
const hostEnv = {
|
|
64
|
+
SKYRAMP_TEST_SERVICE_URL_BACKEND: "http://localhost:8000",
|
|
65
|
+
SKYRAMP_TEST_SERVICE_URL_FRONTEND: "http://localhost:5173",
|
|
66
|
+
};
|
|
67
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
68
|
+
expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://localhost:8000");
|
|
69
|
+
expect(env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://localhost:5173");
|
|
70
|
+
});
|
|
71
|
+
it("skips SKYRAMP_TEST_BASE_URL when value is empty", () => {
|
|
72
|
+
const hostEnv = { SKYRAMP_TEST_BASE_URL: "" };
|
|
73
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
74
|
+
expect(env.find((e) => e.startsWith("SKYRAMP_TEST_BASE_URL="))).toBeUndefined();
|
|
75
|
+
});
|
|
76
|
+
it("does not forward unrelated SKYRAMP_ vars", () => {
|
|
77
|
+
const hostEnv = {
|
|
78
|
+
SKYRAMP_TEST_BASE_URL: "http://localhost:8000",
|
|
79
|
+
SKYRAMP_OTHER_VAR: "should-not-appear",
|
|
80
|
+
SKYRAMP_TEST_SERVICE_NOT_URL: "also-no",
|
|
81
|
+
};
|
|
82
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
83
|
+
expect(env).toContain("SKYRAMP_TEST_BASE_URL=http://localhost:8000");
|
|
84
|
+
expect(env.find((e) => e.includes("SKYRAMP_OTHER_VAR"))).toBeUndefined();
|
|
85
|
+
expect(env.find((e) => e.includes("SKYRAMP_TEST_SERVICE_NOT_URL"))).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
it("forwards SKYRAMP_DEBUG from host env", () => {
|
|
89
|
+
const hostEnv = { SKYRAMP_DEBUG: "true" };
|
|
90
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
91
|
+
expect(env).toContain("SKYRAMP_DEBUG=true");
|
|
92
|
+
});
|
|
93
|
+
it("forwards API_KEY from host env", () => {
|
|
94
|
+
const hostEnv = { API_KEY: "my-key" };
|
|
95
|
+
const env = buildContainerEnv(baseOptions, undefined, hostEnv);
|
|
96
|
+
expect(env).toContain("API_KEY=my-key");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe("TestExecutionService.executeTest - Docker env forwarding", () => {
|
|
100
|
+
// Import after mocks are set up
|
|
101
|
+
let TestExecutionService;
|
|
102
|
+
let EXECUTOR_DOCKER_IMAGE;
|
|
103
|
+
beforeAll(async () => {
|
|
104
|
+
const mod = await import("./TestExecutionService.js");
|
|
105
|
+
TestExecutionService = mod.TestExecutionService;
|
|
106
|
+
EXECUTOR_DOCKER_IMAGE = mod.EXECUTOR_DOCKER_IMAGE;
|
|
107
|
+
});
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
jest.clearAllMocks();
|
|
110
|
+
// Simulate image already cached
|
|
111
|
+
mockListImages.mockResolvedValue([
|
|
112
|
+
{ RepoTags: [EXECUTOR_DOCKER_IMAGE] },
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
// Clean up env vars we set during tests
|
|
117
|
+
delete process.env.SKYRAMP_TEST_BASE_URL;
|
|
118
|
+
delete process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND;
|
|
119
|
+
delete process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND;
|
|
120
|
+
});
|
|
121
|
+
it("passes SKYRAMP_TEST_BASE_URL to Docker container Env", async () => {
|
|
122
|
+
process.env.SKYRAMP_TEST_BASE_URL = "http://external-host:8000";
|
|
123
|
+
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
124
|
+
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
125
|
+
const service = new TestExecutionService();
|
|
126
|
+
await service.executeTest({
|
|
127
|
+
testFile: "/workspace/test_file.py",
|
|
128
|
+
workspacePath: "/workspace",
|
|
129
|
+
language: "python",
|
|
130
|
+
testType: "smoke",
|
|
131
|
+
});
|
|
132
|
+
expect(mockRun).toHaveBeenCalledTimes(1);
|
|
133
|
+
const dockerOptions = mockRun.mock.calls[0][3];
|
|
134
|
+
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_BASE_URL=http://external-host:8000");
|
|
135
|
+
});
|
|
136
|
+
it("passes multiple SKYRAMP_TEST_SERVICE_URL_* to Docker container Env", async () => {
|
|
137
|
+
process.env.SKYRAMP_TEST_SERVICE_URL_BACKEND = "http://host1:8000";
|
|
138
|
+
process.env.SKYRAMP_TEST_SERVICE_URL_FRONTEND = "http://host2:5173";
|
|
139
|
+
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
140
|
+
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
141
|
+
const service = new TestExecutionService();
|
|
142
|
+
await service.executeTest({
|
|
143
|
+
testFile: "/workspace/test_file.py",
|
|
144
|
+
workspacePath: "/workspace",
|
|
145
|
+
language: "python",
|
|
146
|
+
testType: "smoke",
|
|
147
|
+
});
|
|
148
|
+
const dockerOptions = mockRun.mock.calls[0][3];
|
|
149
|
+
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_BACKEND=http://host1:8000");
|
|
150
|
+
expect(dockerOptions.Env).toContain("SKYRAMP_TEST_SERVICE_URL_FRONTEND=http://host2:5173");
|
|
151
|
+
});
|
|
152
|
+
it("does not include base URL vars when not set in host env", async () => {
|
|
153
|
+
const mockContainer = { remove: jest.fn().mockResolvedValue(undefined) };
|
|
154
|
+
mockRun.mockResolvedValue([{ StatusCode: 0 }, mockContainer]);
|
|
155
|
+
const service = new TestExecutionService();
|
|
156
|
+
await service.executeTest({
|
|
157
|
+
testFile: "/workspace/test_file.py",
|
|
158
|
+
workspacePath: "/workspace",
|
|
159
|
+
language: "python",
|
|
160
|
+
testType: "smoke",
|
|
161
|
+
});
|
|
162
|
+
const dockerOptions = mockRun.mock.calls[0][3];
|
|
163
|
+
const envWithBaseUrl = dockerOptions.Env.filter((e) => e.startsWith("SKYRAMP_TEST_BASE_URL=") ||
|
|
164
|
+
e.startsWith("SKYRAMP_TEST_SERVICE_URL_"));
|
|
165
|
+
expect(envWithBaseUrl).toHaveLength(0);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the environment variable array for the Docker executor container.
|
|
3
|
+
*/
|
|
4
|
+
export function buildContainerEnv(options, saveStoragePath, hostEnv = process.env) {
|
|
5
|
+
const env = [
|
|
6
|
+
`SKYRAMP_TEST_TOKEN=${options.token || ""}`,
|
|
7
|
+
"SKYRAMP_IN_DOCKER=true",
|
|
8
|
+
];
|
|
9
|
+
// Skyramp-generated tests are standalone HTTP tests that never need host repo
|
|
10
|
+
// conftest.py files. --noconftest prevents loading any conftest in the test
|
|
11
|
+
// directory tree (avoids missing deps like boto3, celery, django).
|
|
12
|
+
// Note: we intentionally omit -c /dev/null because it disrupts pytest's rootdir
|
|
13
|
+
// detection, causing flaky import failures depending on project structure.
|
|
14
|
+
if (options.language === "python") {
|
|
15
|
+
env.push(`PYTEST_ADDOPTS=--noconftest`);
|
|
16
|
+
}
|
|
17
|
+
if (saveStoragePath) {
|
|
18
|
+
env.push(`PLAYWRIGHT_SAVE_STORAGE_PATH=${saveStoragePath}`);
|
|
19
|
+
}
|
|
20
|
+
// Forward base URL env vars so generated tests can resolve URLs inside the container
|
|
21
|
+
for (const [key, val] of Object.entries(hostEnv)) {
|
|
22
|
+
if ((key === "SKYRAMP_TEST_BASE_URL" ||
|
|
23
|
+
key.startsWith("SKYRAMP_TEST_SERVICE_URL_")) &&
|
|
24
|
+
val) {
|
|
25
|
+
env.push(`${key}=${val}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (hostEnv.SKYRAMP_DEBUG) {
|
|
29
|
+
env.push(`SKYRAMP_DEBUG=${hostEnv.SKYRAMP_DEBUG}`);
|
|
30
|
+
}
|
|
31
|
+
if (hostEnv.API_KEY) {
|
|
32
|
+
env.push(`API_KEY=${hostEnv.API_KEY}`);
|
|
33
|
+
}
|
|
34
|
+
return env;
|
|
35
|
+
}
|
|
@@ -47,7 +47,13 @@ const scenarioTestSchema = {
|
|
|
47
47
|
authToken: z
|
|
48
48
|
.string()
|
|
49
49
|
.optional()
|
|
50
|
-
.describe("Full auth token value to include in the request header.
|
|
50
|
+
.describe("Full auth token value to include in the request header. "
|
|
51
|
+
+ "IMPORTANT: You MUST provide a placeholder value — do NOT omit this parameter for authenticated endpoints. "
|
|
52
|
+
+ "For Authorization headers, pass 'Bearer SKYRAMP_PLACEHOLDER_TOKEN'. "
|
|
53
|
+
+ "For Cookie headers, pass 'session=SKYRAMP_PLACEHOLDER_TOKEN'. "
|
|
54
|
+
+ "For X-API-Key headers, pass 'SKYRAMP_PLACEHOLDER_TOKEN'. "
|
|
55
|
+
+ "The CLI replaces the placeholder with the real token (from SKYRAMP_TEST_TOKEN env var) at runtime. "
|
|
56
|
+
+ "If this is left empty, the generated integration test will send requests without proper auth and fail with 401/403."),
|
|
51
57
|
responseHeaders: z
|
|
52
58
|
.record(z.array(z.string()))
|
|
53
59
|
.optional()
|
|
@@ -63,17 +63,17 @@ export function registerSubmitReportTool(server) {
|
|
|
63
63
|
newTestsCreated: z
|
|
64
64
|
.array(newTestSchema)
|
|
65
65
|
.describe("List of new tests created. Use empty array [] if none."),
|
|
66
|
+
additionalRecommendations: z
|
|
67
|
+
.array(additionalRecommendationSchema)
|
|
68
|
+
.optional()
|
|
69
|
+
.default([])
|
|
70
|
+
.describe("Recommended tests that were not generated (lower priority). Include the remaining recommendations from skyramp_recommend_tests that were not implemented."),
|
|
66
71
|
testMaintenance: z
|
|
67
72
|
.array(testMaintenanceSchema)
|
|
68
73
|
.describe("List of existing test modifications with before/after execution results. Use empty array [] if none."),
|
|
69
74
|
testResults: z
|
|
70
75
|
.array(testResultSchema)
|
|
71
76
|
.describe("List of ALL test execution results. One entry per test executed."),
|
|
72
|
-
additionalRecommendations: z
|
|
73
|
-
.array(additionalRecommendationSchema)
|
|
74
|
-
.optional()
|
|
75
|
-
.default([])
|
|
76
|
-
.describe("Recommended tests that were not generated (lower priority). Include the remaining recommendations from skyramp_recommend_tests that were not implemented."),
|
|
77
77
|
issuesFound: z
|
|
78
78
|
.array(descriptionSchema)
|
|
79
79
|
.describe("List of issues, failures, or bugs found. Use empty array [] if none."),
|
|
@@ -93,9 +93,9 @@ export function registerSubmitReportTool(server) {
|
|
|
93
93
|
const reportJson = JSON.stringify({
|
|
94
94
|
businessCaseAnalysis: params.businessCaseAnalysis,
|
|
95
95
|
newTestsCreated: params.newTestsCreated,
|
|
96
|
+
additionalRecommendations: params.additionalRecommendations ?? [],
|
|
96
97
|
testMaintenance: params.testMaintenance,
|
|
97
98
|
testResults: params.testResults,
|
|
98
|
-
additionalRecommendations: params.additionalRecommendations ?? [],
|
|
99
99
|
issuesFound: params.issuesFound,
|
|
100
100
|
commitMessage: (params.commitMessage ?? "").replace(/[\r\n]+/g, " ").trim().slice(0, 72) || DEFAULT_COMMIT_MESSAGE,
|
|
101
101
|
}, null, 2);
|