@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.
Files changed (28) hide show
  1. package/build/index.js +18 -26
  2. package/build/prompts/test-maintenance/drift-analysis-prompt.js +59 -0
  3. package/build/prompts/test-maintenance/driftAnalysisSections.js +153 -0
  4. package/build/prompts/test-recommendation/analysisOutputPrompt.js +21 -9
  5. package/build/prompts/test-recommendation/registerRecommendTestsPrompt.js +34 -38
  6. package/build/prompts/test-recommendation/test-recommendation-prompt.js +56 -9
  7. package/build/prompts/testbot/testbot-prompts.js +113 -100
  8. package/build/services/DriftAnalysisService.js +1 -1
  9. package/build/services/ScenarioGenerationService.js +5 -1
  10. package/build/services/TestExecutionService.js +2 -24
  11. package/build/services/TestExecutionService.test.js +167 -0
  12. package/build/services/containerEnv.js +35 -0
  13. package/build/tools/generate-tests/generateScenarioRestTool.js +7 -1
  14. package/build/tools/submitReportTool.js +6 -6
  15. package/build/tools/test-management/actionsTool.js +396 -0
  16. package/build/tools/test-management/analyzeChangesTool.js +750 -0
  17. package/build/tools/test-management/analyzeTestHealthTool.js +132 -0
  18. package/build/tools/test-management/executeTestsTool.js +198 -0
  19. package/build/tools/test-management/index.js +5 -0
  20. package/build/tools/test-management/stateCleanupTool.js +163 -0
  21. package/build/tools/test-recommendation/recommendTestsTool.js +1 -1
  22. package/build/utils/analyze-openapi.js +2 -2
  23. package/build/utils/pr-comment-parser.js +157 -36
  24. package/build/utils/pr-comment-parser.test.js +427 -0
  25. package/package.json +1 -1
  26. package/build/tools/initTestbotTool.js +0 -187
  27. package/build/tools/initTestbotTool.test.js +0 -194
  28. 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 for all tasks below.
14
-
15
- ## Task 1: Recommend & Generate New Tests
16
-
17
- Read the diff at \`${diffFile}\`. Skip Task 1 if all changed files are non-application
18
- (CI/CD, docs, lock files, config). Otherwise proceed:
19
-
20
- ### Steps
21
-
22
- 1. Call \`skyramp_analyze_repository\` with \`repositoryPath\`: "${repositoryPath}", \`analysisScope\`: "current_branch_diff"${baseBranch ? `\n , \`baseBranch\`: "${baseBranch}"` : ''}
23
- 2. Call \`skyramp_recommend_tests\` with the returned \`sessionId\`.
24
- It returns ${maxRecommendations} ranked recommendations. Walk through them in rank order and generate
25
- up to ${maxGenerate} tests. Any recommendation you skip or cannot generate goes to
26
- \`additionalRecommendations\`.
27
-
28
- 3. **Generate** up to ${maxGenerate} tests by walking the ranked list top-to-bottom:
29
- - If a recommendation can be generated, generate it and count it.
30
- - If it cannot (e.g. E2E/UI without traces), move it to \`additionalRecommendations\`
31
- and continue to the next recommendation.
32
- - Stop once you have ${maxGenerate} generated tests OR exhaust all ${maxRecommendations} recommendations.
33
- - All remaining (ungenerated) recommendations go to \`additionalRecommendations\`.
34
- Keep a list of every file the CLI creates (test files AND scenario JSON files).
35
-
36
- **Frontend-only PRs** (no backend/API changes): only generate tests if relevant
37
- Playwright traces exist. If no traces are available, skip generation entirely and
38
- move all ${maxRecommendations} recommendations to \`additionalRecommendations\` with scenario steps and
39
- trace recording instructions. Do not generate integration tests for unchanged backend
40
- APIs just to fill the quota those tests don't validate the PR's changes.
41
-
42
- **How to generate each type:**
43
- - **Integration**: call \`skyramp_scenario_test_generation\` per step, then
44
- \`skyramp_integration_test_generation\` with the scenario file.
45
- The scenario JSON is written to the same \`outputDir\` as the test files
46
- (e.g. \`tests/scenario_<name>.json\`), not \`.skyramp/\`.
47
- - **Contract**: call \`skyramp_contract_test_generation\` with \`endpointURL\`, \`method\`,
48
- and \`requestData\` for POST/PUT endpoints.
49
- Pass \`apiSchema\` if an OpenAPI spec exists — it validates response structure.
50
- - **Fuzz**: call \`skyramp_fuzz_test_generation\` with \`endpointURL\`, \`method\`, \`requestData\`.
51
- Pass \`apiSchema\` if available it generates smarter boundary values.
52
- - **E2E/UI**: only generate when relevant Playwright traces exist (see step 5).
53
- Without traces, move the test to \`additionalRecommendations\` with scenario steps
54
- and trace recording instructions instead.
55
- - Skip smoke tests entirely.
56
-
57
- **Scenario quality:** Before generating, verify each step's preconditions are met by
58
- prior steps. For example, you can't update a membership that was never created — check
59
- the controller code for existence checks and ensure the scenario creates records first.
60
-
61
- **Filenames:** Pass a descriptive \`--output\` name per test to avoid CLI overwrites.
62
-
63
- 4. **Execute** the generated tests and record results.
64
-
65
- 5. **Trace search** for E2E/UI: look in \`\${testDirectory}\`, repo root, and \`.skyramp/\` for
66
- trace files (\`*trace*.json\`, \`*playwright*.zip\`). Only use a trace if it covers code
67
- changed in this PR and targets localhost — skip traces for external hosts or unrelated code.
68
-
69
- With relevant traces: backend + Playwright → \`skyramp_e2e_test_generation\`,
70
- Playwright only → \`skyramp_ui_test_generation\`.
71
-
72
- **After generation, fix chaining only.** The CLI may use literal/hardcoded IDs instead
73
- of dynamic values from prior responses. Fix these two cases:
74
- 1. **Path params:** variables like \`product_id = 'product_id'\` use the response accessor
75
- (e.g. \`getResponseValue(response, "response.id")\` in TS, \`skyramp.get_response_value(response, "id")\` in Python).
76
- 2. **Request body refs:** hardcoded IDs in request bodies (e.g. \`"product_id": 1\`) → replace
77
- with the dynamic ID extracted from the prior POST response (e.g. \`product_id\` variable or
78
- \`dataOverride\`/\`data_override\` for the field).
79
-
80
- Change ONLY chaining-related values (path param assignments and body ID references).
81
- Preserve everything else exactly as the CLI generated it headers, auth code, assertions,
82
- imports, and all other request body fields.
83
-
84
- ## Task 2: Existing Test Maintenance
85
-
86
- Run this task regardless of Task 1 outcome even if Task 1 was skipped or generated zero tests.
87
-
88
- 1. Call \`skyramp_discover_tests\` with \`repositoryPath\`: "${repositoryPath}".
89
- 2. If zero Skyramp tests found, report \`testMaintenance\` as an empty array.
90
- This is expected for new repos — pass \`issuesFound\` as an empty array \`[]\`. Do not add a "no tests found" entry.
91
- 3. If tests exist:
92
- a. Baseline them (from CI status or by executing).
93
- b. Run \`skyramp_analyze_test_drift\` \`skyramp_calculate_health_scores\` \`skyramp_actions\`.
94
- c. Apply actions (path renames, schema updates) in-place. Do not regenerate.
95
- d. Execute modified tests. Report before/after in \`testMaintenance\`.
96
-
97
- ## Task 3: Submit Report
98
-
99
- Verify Tasks 1 and 2 are complete, then call \`skyramp_submit_report\` with
100
- \`summaryOutputFile\`: "${summaryOutputFile}".
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** — list every generated test file (at most ${maxGenerate}):
105
- \`testType\`, \`endpoint\`, \`fileName\`, \`description\`, \`scenarioFile\`, \`traceFile\`, \`frontendTrace\`
106
- Use the actual file path returned by the generation tool for \`scenarioFile\`.
107
- Include scenario JSON files in the git commit alongside test files.
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** — remaining recommendations not generated:
112
- \`testType\`, \`scenarioName\`, \`priority\`, \`description\`, \`steps\`, artifact paths
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 PR data and tool outputs.`;
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 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);
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
- let baseline = baselineCommit ||
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]: [params.authToken ?? ""],
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
- // Prepare environment variables
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. For Authorization headers, include the scheme prefix (e.g., 'Bearer my-token'). For Cookie headers, use the cookie string (e.g., 'session=abc123'). For API key headers, use the raw key. If omitted, the header value is left empty (the CLI injects the real token at runtime)."),
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);