codeflash/tests/test_setup/test_e2e_setup.py
Kevin Turcios eceac13fc3 Merge remote-tracking branch 'origin/main' into omni-java
# Conflicts:
#	.claude/rules/architecture.md
#	.claude/rules/code-style.md
#	.github/workflows/claude.yml
#	.github/workflows/duplicate-code-detector.yml
#	codeflash/api/aiservice.py
#	codeflash/cli_cmds/console.py
#	codeflash/cli_cmds/logging_config.py
#	codeflash/code_utils/deduplicate_code.py
#	codeflash/discovery/discover_unit_tests.py
#	codeflash/languages/base.py
#	codeflash/languages/code_replacer.py
#	codeflash/languages/javascript/mocha_runner.py
#	codeflash/languages/javascript/support.py
#	codeflash/languages/python/support.py
#	codeflash/optimization/function_optimizer.py
#	codeflash/verification/parse_test_output.py
#	codeflash/verification/verification_utils.py
#	codeflash/verification/verifier.py
#	packages/codeflash/package-lock.json
#	packages/codeflash/package.json
#	tests/languages/javascript/test_support_dispatch.py
#	tests/test_codeflash_capture.py
#	tests/test_languages/test_javascript_test_runner.py
#	tests/test_multi_file_code_replacement.py
2026-03-04 01:52:32 -05:00

959 lines
33 KiB
Python

"""End-to-end tests for the setup flow.
These tests validate the complete setup experience across different:
- Languages (Python, JavaScript, TypeScript)
- Project structures (src/, flat, monorepo-like)
- Package managers (npm, yarn, pnpm, bun)
- Existing config scenarios
"""
import json
from argparse import Namespace
import pytest
import tomlkit
from codeflash.setup import (
CodeflashConfig,
detect_project,
handle_first_run,
has_existing_config,
is_first_run,
write_config,
)
# =============================================================================
# Fixtures for creating different project types
# =============================================================================
@pytest.fixture
def python_src_layout(tmp_path):
"""Create a Python project with src/ layout."""
# pyproject.toml with poetry
(tmp_path / "pyproject.toml").write_text(
"""
[tool.poetry]
name = "myapp"
version = "0.1.0"
[tool.ruff]
line-length = 120
[tool.pytest.ini_options]
testpaths = ["tests"]
""".strip()
)
# src/myapp package
src_dir = tmp_path / "src" / "myapp"
src_dir.mkdir(parents=True)
(src_dir / "__init__.py").write_text('__version__ = "0.1.0"')
(src_dir / "main.py").write_text("def main(): pass")
(src_dir / "utils.py").write_text("def helper(): pass")
# tests directory
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
(tests_dir / "__init__.py").write_text("")
(tests_dir / "test_main.py").write_text("def test_main(): pass")
(tests_dir / "conftest.py").write_text("import pytest")
# .git directory
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def python_flat_layout(tmp_path):
"""Create a Python project with flat layout (package at root)."""
(tmp_path / "pyproject.toml").write_text(
"""
[project]
name = "myapp"
version = "0.1.0"
[tool.black]
line-length = 88
""".strip()
)
# Package at root
pkg_dir = tmp_path / "myapp"
pkg_dir.mkdir()
(pkg_dir / "__init__.py").write_text("")
(pkg_dir / "core.py").write_text("def process(): pass")
# Tests at root
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
(tests_dir / "test_core.py").write_text("def test_process(): pass")
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def python_setup_py_project(tmp_path):
"""Create a Python project with setup.py (legacy)."""
(tmp_path / "setup.py").write_text(
"""
from setuptools import setup, find_packages
setup(
name="legacyapp",
version="1.0.0",
packages=find_packages(),
)
""".strip()
)
pkg_dir = tmp_path / "legacyapp"
pkg_dir.mkdir()
(pkg_dir / "__init__.py").write_text("")
(tmp_path / "tests").mkdir()
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def javascript_npm_project(tmp_path):
"""Create a JavaScript project with npm."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "my-js-app",
"version": "1.0.0",
"main": "src/index.js",
"scripts": {"test": "jest", "lint": "eslint src/"},
"devDependencies": {"jest": "^29.7.0", "prettier": "^3.0.0"},
},
indent=2,
)
)
(tmp_path / "package-lock.json").write_text("{}")
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "index.js").write_text("module.exports = {}")
(src_dir / "utils.js").write_text("function helper() {}")
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
(tests_dir / "index.test.js").write_text("test('works', () => {})")
(tmp_path / ".prettierrc").write_text("{}")
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def javascript_yarn_project(tmp_path):
"""Create a JavaScript project with yarn."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "yarn-app",
"version": "1.0.0",
"main": "lib/index.js",
"devDependencies": {"jest": "^29.0.0", "eslint": "^8.0.0"},
},
indent=2,
)
)
(tmp_path / "yarn.lock").write_text("# yarn lockfile")
lib_dir = tmp_path / "lib"
lib_dir.mkdir()
(lib_dir / "index.js").write_text("")
(tmp_path / "__tests__").mkdir()
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def javascript_pnpm_project(tmp_path):
"""Create a JavaScript project with pnpm."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "pnpm-app",
"version": "1.0.0",
"exports": {".": "./dist/index.js"},
"devDependencies": {"vitest": "^1.0.0"},
},
indent=2,
)
)
(tmp_path / "pnpm-lock.yaml").write_text("lockfileVersion: 5.4")
(tmp_path / "dist").mkdir()
(tmp_path / "src").mkdir()
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def javascript_bun_project(tmp_path):
"""Create a JavaScript project with bun."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "bun-app",
"version": "1.0.0",
"module": "src/index.ts",
"devDependencies": {"bun-types": "latest"},
},
indent=2,
)
)
(tmp_path / "bun.lockb").write_bytes(b"bun lockfile")
(tmp_path / "src").mkdir()
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def typescript_project(tmp_path):
"""Create a TypeScript project."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "ts-app",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {"build": "tsc", "test": "vitest"},
"devDependencies": {"typescript": "^5.0.0", "vitest": "^1.0.0", "@types/node": "^20.0.0"},
},
indent=2,
)
)
(tmp_path / "tsconfig.json").write_text(
json.dumps(
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": True,
},
"include": ["src/**/*"],
},
indent=2,
)
)
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "index.ts").write_text("export const main = () => {}")
(src_dir / "types.ts").write_text("export interface Config {}")
tests_dir = tmp_path / "tests"
tests_dir.mkdir()
(tests_dir / "index.test.ts").write_text("import { describe, it } from 'vitest'")
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def typescript_react_project(tmp_path):
"""Create a TypeScript React project (like Create React App)."""
(tmp_path / "package.json").write_text(
json.dumps(
{
"name": "react-app",
"version": "0.1.0",
"private": True,
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"jest": "^29.0.0",
},
"devDependencies": {
"@types/react": "^18.0.0",
"@testing-library/react": "^14.0.0",
"typescript": "^5.0.0",
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
},
},
indent=2,
)
)
(tmp_path / "tsconfig.json").write_text(
json.dumps({"compilerOptions": {"target": "es5", "lib": ["dom", "es2015"], "jsx": "react-jsx"}}, indent=2)
)
src_dir = tmp_path / "src"
src_dir.mkdir()
(src_dir / "App.tsx").write_text("export default function App() { return <div/>; }")
(src_dir / "index.tsx").write_text("import App from './App';")
(src_dir / "App.test.tsx").write_text("test('renders', () => {});")
(tmp_path / "package-lock.json").write_text("{}")
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def project_with_existing_config(tmp_path):
"""Create a project with existing codeflash config."""
(tmp_path / "pyproject.toml").write_text(
"""
[project]
name = "configured-app"
[tool.codeflash]
module-root = "src"
tests-root = "tests"
formatter-cmds = ["black $file"]
""".strip()
)
(tmp_path / "src").mkdir()
(tmp_path / "tests").mkdir()
(tmp_path / ".git").mkdir()
return tmp_path
@pytest.fixture
def mixed_python_js_project(tmp_path):
"""Create a project with both Python and JS files (monorepo-like)."""
# Python backend
(tmp_path / "pyproject.toml").write_text(
"""
[project]
name = "fullstack-app"
[tool.codeflash]
module-root = "backend"
""".strip()
)
backend_dir = tmp_path / "backend"
backend_dir.mkdir()
(backend_dir / "__init__.py").write_text("")
(backend_dir / "api.py").write_text("def handler(): pass")
# JS frontend
frontend_dir = tmp_path / "frontend"
frontend_dir.mkdir()
(frontend_dir / "package.json").write_text(json.dumps({"name": "frontend", "devDependencies": {"jest": "^29.0.0"}}))
(frontend_dir / "src").mkdir()
(frontend_dir / "src" / "app.js").write_text("")
(tmp_path / ".git").mkdir()
return tmp_path
# =============================================================================
# E2E Tests: Detection
# =============================================================================
class TestE2EDetection:
"""E2E tests for project detection across different setups."""
def test_python_src_layout_detection(self, python_src_layout):
"""Should correctly detect Python src/ layout project."""
detected = detect_project(python_src_layout)
assert detected.language == "python"
assert detected.project_root == python_src_layout
assert detected.module_root.name == "myapp"
assert detected.tests_root == python_src_layout / "tests"
assert detected.test_runner == "pytest"
assert any("ruff" in cmd for cmd in detected.formatter_cmds)
assert detected.confidence >= 0.9
def test_python_flat_layout_detection(self, python_flat_layout):
"""Should correctly detect Python flat layout project."""
detected = detect_project(python_flat_layout)
assert detected.language == "python"
assert detected.module_root.name == "myapp"
assert any("black" in cmd for cmd in detected.formatter_cmds)
def test_python_setup_py_detection(self, python_setup_py_project):
"""Should correctly detect legacy setup.py project."""
detected = detect_project(python_setup_py_project)
assert detected.language == "python"
assert detected.module_root.name == "legacyapp"
def test_javascript_npm_detection(self, javascript_npm_project):
"""Should correctly detect JavaScript npm project."""
detected = detect_project(javascript_npm_project)
assert detected.language == "javascript"
assert detected.module_root == javascript_npm_project / "src"
assert detected.test_runner == "jest"
assert any("prettier" in cmd for cmd in detected.formatter_cmds)
def test_javascript_yarn_detection(self, javascript_yarn_project):
"""Should correctly detect JavaScript yarn project."""
detected = detect_project(javascript_yarn_project)
assert detected.language == "javascript"
assert detected.module_root == javascript_yarn_project / "lib"
assert detected.tests_root == javascript_yarn_project / "__tests__"
def test_javascript_pnpm_detection(self, javascript_pnpm_project):
"""Should correctly detect JavaScript pnpm project."""
detected = detect_project(javascript_pnpm_project)
assert detected.language == "javascript"
assert detected.test_runner == "vitest"
def test_javascript_bun_detection(self, javascript_bun_project):
"""Should correctly detect JavaScript bun project."""
detected = detect_project(javascript_bun_project)
assert detected.language == "javascript"
assert detected.module_root == javascript_bun_project / "src"
def test_typescript_detection(self, typescript_project):
"""Should correctly detect TypeScript project."""
detected = detect_project(typescript_project)
assert detected.language == "typescript"
assert detected.test_runner == "vitest"
assert detected.tests_root == typescript_project / "tests"
def test_typescript_react_detection(self, typescript_react_project):
"""Should correctly detect TypeScript React project."""
detected = detect_project(typescript_react_project)
assert detected.language == "typescript"
assert detected.module_root == typescript_react_project / "src"
# React scripts uses jest under the hood
assert detected.test_runner == "jest"
# =============================================================================
# E2E Tests: First Run Check
# =============================================================================
class TestE2EFirstRunCheck:
"""E2E tests for first-run detection."""
def test_is_first_run_new_python_project(self, python_src_layout):
"""Should detect first run for new Python project."""
assert is_first_run(python_src_layout) is True
def test_is_first_run_new_js_project(self, javascript_npm_project):
"""Should detect first run for new JS project."""
assert is_first_run(javascript_npm_project) is True
def test_is_not_first_run_configured_project(self, project_with_existing_config):
"""Should detect existing config."""
assert is_first_run(project_with_existing_config) is False
def test_has_existing_config_python(self, project_with_existing_config):
"""Should find existing config in pyproject.toml."""
has_config, config_type = has_existing_config(project_with_existing_config)
assert has_config is True
assert config_type == "pyproject.toml"
def test_has_existing_config_js(self, tmp_path):
"""Should find existing config in package.json."""
(tmp_path / "package.json").write_text(json.dumps({"name": "test", "codeflash": {"moduleRoot": "src"}}))
has_config, config_type = has_existing_config(tmp_path)
assert has_config is True
assert config_type == "package.json"
# =============================================================================
# E2E Tests: Config Writing
# =============================================================================
class TestE2EConfigWriting:
"""E2E tests for writing config to native files."""
def test_write_config_python_preserves_existing(self, python_src_layout):
"""Should write config while preserving existing pyproject.toml content."""
detected = detect_project(python_src_layout)
success, message = write_config(detected)
assert success is True
# Read back and verify
content = (python_src_layout / "pyproject.toml").read_text()
data = tomlkit.parse(content)
# Original content preserved
assert data["tool"]["poetry"]["name"] == "myapp"
assert data["tool"]["ruff"]["line-length"] == 120
assert data["tool"]["pytest"]["ini_options"]["testpaths"] == ["tests"]
# Codeflash config added
assert "codeflash" in data["tool"]
assert "module-root" in data["tool"]["codeflash"]
def test_write_config_javascript_preserves_existing(self, javascript_npm_project):
"""Should write config while preserving existing package.json content."""
detected = detect_project(javascript_npm_project)
success, message = write_config(detected)
assert success is True
# Read back and verify
with (javascript_npm_project / "package.json").open() as f:
data = json.load(f)
# Original content preserved
assert data["name"] == "my-js-app"
assert data["devDependencies"]["jest"] == "^29.7.0"
assert data["scripts"]["test"] == "jest"
def test_write_config_typescript(self, typescript_project):
"""Should write config for TypeScript project."""
detected = detect_project(typescript_project)
success, message = write_config(detected)
assert success is True
with (typescript_project / "package.json").open() as f:
data = json.load(f)
# tsconfig.json should be unchanged
tsconfig = json.loads((typescript_project / "tsconfig.json").read_text())
assert tsconfig["compilerOptions"]["strict"] is True
def test_config_roundtrip_python(self, python_flat_layout):
"""Should be able to read back written config."""
# Detect and write
detected = detect_project(python_flat_layout)
write_config(detected)
# Read back
content = (python_flat_layout / "pyproject.toml").read_text()
data = tomlkit.parse(content)
codeflash_section = data["tool"]["codeflash"]
# Create config from written data
config = CodeflashConfig.from_pyproject_dict(dict(codeflash_section))
assert config.language == "python"
assert "myapp" in config.module_root
def test_config_roundtrip_javascript(self, javascript_npm_project):
"""Should be able to read back written config for JS."""
detected = detect_project(javascript_npm_project)
write_config(detected)
with (javascript_npm_project / "package.json").open() as f:
data = json.load(f)
if "codeflash" in data:
config = CodeflashConfig.from_package_json_dict(data["codeflash"])
assert config.language == "javascript"
# =============================================================================
# E2E Tests: First Run Experience
# =============================================================================
class TestE2EFirstRunExperience:
"""E2E tests for the complete first-run experience."""
def test_first_run_python_project(self, python_src_layout, monkeypatch):
"""Should complete first-run for Python project."""
monkeypatch.chdir(python_src_layout)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key-12345")
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.language == "python"
assert result.module_root.endswith("myapp")
assert result.tests_root is not None
assert result.tests_root.endswith("tests")
assert result.pytest_cmd == "pytest"
# Config should be written
content = (python_src_layout / "pyproject.toml").read_text()
assert "[tool.codeflash]" in content
def test_first_run_javascript_project(self, javascript_npm_project, monkeypatch):
"""Should complete first-run for JavaScript project."""
monkeypatch.chdir(javascript_npm_project)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key-12345")
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.language == "javascript"
assert result.module_root.endswith("src")
assert result.pytest_cmd == "jest" # Maps to test_runner
def test_first_run_typescript_project(self, typescript_project, monkeypatch):
"""Should complete first-run for TypeScript project."""
monkeypatch.chdir(typescript_project)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key-12345")
result = handle_first_run(skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.language == "typescript"
assert result.pytest_cmd == "vitest"
def test_first_run_with_existing_args(self, python_flat_layout, monkeypatch):
"""Should merge with existing CLI args."""
monkeypatch.chdir(python_flat_layout)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key-12345")
existing_args = Namespace(file="myapp/core.py", function="process", custom_flag=True)
result = handle_first_run(args=existing_args, skip_confirm=True, skip_api_key=True)
assert result is not None
assert result.custom_flag is True # Preserved
assert result.file == "myapp/core.py" # Preserved
assert result.language == "python" # Added
def test_subsequent_run_not_first_run(self, project_with_existing_config, monkeypatch):
"""Should not trigger first-run for configured project."""
monkeypatch.chdir(project_with_existing_config)
assert is_first_run(project_with_existing_config) is False
# =============================================================================
# E2E Tests: Edge Cases
# =============================================================================
class TestE2EEdgeCases:
"""E2E tests for edge cases and special scenarios."""
def test_empty_directory(self, tmp_path):
"""Should handle empty directory gracefully."""
detected = detect_project(tmp_path)
# Should default to Python
assert detected.language == "python"
# Low language confidence (0.3) + base (0.6) = ~0.72
assert detected.confidence < 0.8
def test_nested_project_detection(self, tmp_path):
"""Should find project root from nested directory."""
# Create project structure
(tmp_path / "pyproject.toml").write_text('[project]\nname = "root"')
deep_dir = tmp_path / "src" / "pkg" / "subpkg"
deep_dir.mkdir(parents=True)
# Detect from nested dir
detected = detect_project(deep_dir)
assert detected.project_root == tmp_path
def test_mixed_project_uses_existing_config(self, mixed_python_js_project):
"""Should respect existing config in mixed projects."""
has_config, config_type = has_existing_config(mixed_python_js_project)
assert has_config is True
assert config_type == "pyproject.toml"
def test_project_without_tests_dir(self, tmp_path):
"""Should handle project without tests directory."""
(tmp_path / "pyproject.toml").write_text('[project]\nname = "notests"')
(tmp_path / "src").mkdir()
detected = detect_project(tmp_path)
assert detected.tests_root is None
def test_project_without_formatter(self, tmp_path):
"""Should handle project without detectable formatter."""
(tmp_path / "package.json").write_text(
json.dumps({"name": "no-formatter", "devDependencies": {"jest": "^29.0.0"}})
)
detected = detect_project(tmp_path)
assert detected.formatter_cmds == []
def test_malformed_pyproject_toml(self, tmp_path):
"""Should handle malformed pyproject.toml."""
(tmp_path / "pyproject.toml").write_text("this is not valid toml {{{}}")
# Should not crash, just detect with lower confidence
detected = detect_project(tmp_path)
assert detected is not None
def test_malformed_package_json(self, tmp_path):
"""Should handle malformed package.json."""
(tmp_path / "package.json").write_text("not valid json")
detected = detect_project(tmp_path)
assert detected is not None
def test_display_dict_format(self, python_src_layout):
"""Should generate proper display dict for UI."""
detected = detect_project(python_src_layout)
display = detected.to_display_dict()
assert "Language" in display
assert "Module root" in display
assert "Tests root" in display
assert "Test runner" in display
assert "Formatter" in display
assert "Ignoring" in display
# Values should be user-friendly
assert display["Language"] == "Python"
assert display["Test runner"] == "pytest"
# =============================================================================
# E2E Tests: Config Schema Conversion
# =============================================================================
class TestE2EConfigConversion:
"""E2E tests for config format conversion."""
def test_python_config_to_toml_and_back(self, python_src_layout):
"""Should convert Python config to TOML and back without loss."""
detected = detect_project(python_src_layout)
original_config = CodeflashConfig.from_detected_project(detected)
# Convert to TOML dict
toml_dict = original_config.to_pyproject_dict()
# Convert back
restored_config = CodeflashConfig.from_pyproject_dict(toml_dict)
assert restored_config.module_root == original_config.module_root
assert restored_config.tests_root == original_config.tests_root
def test_js_config_to_json_and_back(self, javascript_npm_project):
"""Should convert JS config to JSON and back without loss."""
detected = detect_project(javascript_npm_project)
original_config = CodeflashConfig.from_detected_project(detected)
# Convert to JSON dict
json_dict = original_config.to_package_json_dict()
# Convert back
restored_config = CodeflashConfig.from_package_json_dict(json_dict)
# Note: Some defaults may differ, check key fields
if json_dict: # Only if there were non-default values
assert restored_config.language == "javascript"
# =============================================================================
# E2E Tests: Real-world Scenarios
# =============================================================================
class TestE2ERealWorldScenarios:
"""E2E tests simulating real-world usage scenarios."""
def test_scenario_new_user_python(self, python_src_layout, monkeypatch):
"""Scenario: New user runs codeflash on Python project for first time."""
monkeypatch.chdir(python_src_layout)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
# Step 1: Check if first run
assert is_first_run() is True
# Step 2: Handle first run
args = handle_first_run(skip_confirm=True, skip_api_key=True)
assert args is not None
# Step 3: Config is now saved
assert is_first_run() is False
# Step 4: Next run should use saved config
has_config, _ = has_existing_config(python_src_layout)
assert has_config is True
def test_scenario_new_user_javascript(self, javascript_npm_project, monkeypatch):
"""Scenario: New user runs codeflash on JS project for first time."""
monkeypatch.chdir(javascript_npm_project)
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
# Step 1: Check if first run
assert is_first_run() is True
# Step 2: Handle first run
args = handle_first_run(skip_confirm=True, skip_api_key=True)
assert args is not None
assert args.language == "javascript"
# Step 3: Verify package.json was updated (or left minimal)
with (javascript_npm_project / "package.json").open() as f:
data = json.load(f)
# Original content should still be there
assert data["name"] == "my-js-app"
def test_scenario_existing_user_reconfigure(self, project_with_existing_config, monkeypatch):
"""Scenario: Existing user wants to reconfigure."""
monkeypatch.chdir(project_with_existing_config)
# Not first run
assert is_first_run() is False
# But user can still detect and see what would be configured
detected = detect_project()
assert detected.language == "python"
def test_scenario_ci_environment(self, python_src_layout, monkeypatch):
"""Scenario: Running in CI environment (non-interactive)."""
monkeypatch.chdir(python_src_layout)
monkeypatch.setenv("CI", "true")
monkeypatch.setenv("CODEFLASH_API_KEY", "cf-test-key")
# In CI, we need config to exist or use --yes flag
# First run should still work with skip flags
args = handle_first_run(skip_confirm=True, skip_api_key=True)
assert args is not None
# =============================================================================
# E2E Tests: CLI Flags
# =============================================================================
class TestE2ECLIFlags:
"""E2E tests for --show-config and --reset-config CLI flags."""
def test_show_config_displays_detected_settings(self, python_src_layout, monkeypatch, capsys):
"""Should display detected project settings."""
monkeypatch.chdir(python_src_layout)
from codeflash.cli_cmds.cli import _handle_show_config
_handle_show_config()
# Can't easily capture Rich output, but ensure it doesn't crash
# and the function completes successfully
def test_show_config_indicates_saved_vs_detected(self, project_with_existing_config, monkeypatch):
"""Should indicate when config is saved vs auto-detected."""
monkeypatch.chdir(project_with_existing_config)
from codeflash.cli_cmds.cli import _handle_show_config
# Should complete without error
_handle_show_config()
def test_show_config_displays_config_path_when_saved(self, project_with_existing_config, monkeypatch):
"""Should display config file path when saved config exists."""
monkeypatch.chdir(project_with_existing_config)
# Track what gets printed
printed_messages = []
def mock_print(msg="", *args, **kwargs):
printed_messages.append(str(msg))
from codeflash.cli_cmds import console
monkeypatch.setattr(console.console, "print", mock_print)
from codeflash.cli_cmds.cli import _handle_show_config
_handle_show_config()
# Verify config path is displayed
all_output = "\n".join(printed_messages)
assert "pyproject.toml" in all_output
assert "Config file:" in all_output
def test_show_config_no_path_when_auto_detected(self, python_src_layout, monkeypatch):
"""Should not display config file path when config is auto-detected."""
monkeypatch.chdir(python_src_layout)
# Track what gets printed
printed_messages = []
def mock_print(msg="", *args, **kwargs):
printed_messages.append(str(msg))
from codeflash.cli_cmds import console
monkeypatch.setattr(console.console, "print", mock_print)
from codeflash.cli_cmds.cli import _handle_show_config
_handle_show_config()
# Verify no config path line is displayed
all_output = "\n".join(printed_messages)
assert "Config file:" not in all_output
assert "Auto-detected" in all_output
def test_reset_config_removes_from_pyproject(self, project_with_existing_config, monkeypatch):
"""Should remove codeflash config from pyproject.toml."""
monkeypatch.chdir(project_with_existing_config)
from codeflash.cli_cmds.cli import _handle_reset_config
# Verify config exists before
content_before = (project_with_existing_config / "pyproject.toml").read_text()
assert "[tool.codeflash]" in content_before
_handle_reset_config(confirm=False)
# Verify config removed after
content_after = (project_with_existing_config / "pyproject.toml").read_text()
assert "[tool.codeflash]" not in content_after
# Other sections should remain
assert "[project]" in content_after
def test_reset_config_removes_from_package_json(self, javascript_npm_project, monkeypatch):
"""Should remove codeflash config from package.json."""
# First add config
monkeypatch.chdir(javascript_npm_project)
# Add codeflash section
with (javascript_npm_project / "package.json").open() as f:
data = json.load(f)
data["codeflash"] = {"moduleRoot": "src"}
with (javascript_npm_project / "package.json").open("w") as f:
json.dump(data, f, indent=2)
from codeflash.cli_cmds.cli import _handle_reset_config
_handle_reset_config(confirm=False)
# Verify config removed
with (javascript_npm_project / "package.json").open() as f:
data_after = json.load(f)
assert "codeflash" not in data_after
assert data_after["name"] == "my-js-app" # Other content preserved
def test_reset_config_handles_no_config(self, python_src_layout, monkeypatch):
"""Should handle gracefully when no config exists to reset."""
monkeypatch.chdir(python_src_layout)
from codeflash.cli_cmds.cli import _handle_reset_config
# Should not crash when no codeflash config exists
_handle_reset_config(confirm=False)