# Regression tests for interactive setup wizard. # Classification: keep tests here when they exercise env_* top-level wizard flows and their end-to-end env/compose rewrite outcomes. from __future__ import annotations from pathlib import Path import pytest from tests.setup._helpers import ( REPO_ROOT, assert_single_compose_backup, parse_lines, run_bash, run_bash_process, run_bash_lines, write_storage_setup_files, write_text_lines, ) pytestmark = pytest.mark.offline def test_env_base_flow_preserves_non_inference_env_values(tmp_path: Path) -> None: """env-base wizard should leave server, security, and observability values untouched.""" env_file = tmp_path / ".env" env_file.write_text( "\n".join( [ "HOST=127.0.0.1", "PORT=9999", "WEBUI_TITLE=Existing Title", "WEBUI_DESCRIPTION=Existing Description", "SSL=true", "SSL_CERTFILE=/some/cert.pem", "SSL_KEYFILE=/some/key.pem", "AUTH_ACCOUNTS=admin:secret", "TOKEN_SECRET=jwt-secret", "LIGHTRAG_API_KEY=api-key", "LANGFUSE_ENABLE_TRACE=true", "LANGFUSE_SECRET_KEY=langfuse-secret", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING_API_KEY=sk-existing", ] ) + "\n", encoding="utf-8", ) output = run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ return 1; }} finalize_base_setup() {{ printf 'HOST=%s\\n' "${{ENV_VALUES[HOST]}}" printf 'PORT=%s\\n' "${{ENV_VALUES[PORT]}}" printf 'WEBUI_TITLE=%s\\n' "${{ENV_VALUES[WEBUI_TITLE]}}" printf 'WEBUI_DESCRIPTION=%s\\n' "${{ENV_VALUES[WEBUI_DESCRIPTION]}}" printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}" printf 'LLM_BINDING_API_KEY=%s\\n' "${{ENV_VALUES[LLM_BINDING_API_KEY]}}" printf 'EMBEDDING_BINDING_API_KEY=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_API_KEY]}}" printf 'SSL_SET=%s\\n' "${{ENV_VALUES[SSL]+set}}" printf 'AUTH_ACCOUNTS_SET=%s\\n' "${{ENV_VALUES[AUTH_ACCOUNTS]+set}}" printf 'TOKEN_SECRET_SET=%s\\n' "${{ENV_VALUES[TOKEN_SECRET]+set}}" printf 'LIGHTRAG_API_KEY_SET=%s\\n' "${{ENV_VALUES[LIGHTRAG_API_KEY]+set}}" printf 'LANGFUSE_ENABLE_TRACE_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_ENABLE_TRACE]+set}}" printf 'LANGFUSE_SECRET_KEY_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_SECRET_KEY]+set}}" }} env_base_flow """) values = parse_lines(output) assert values["HOST"] == "127.0.0.1" assert values["PORT"] == "9999" assert values["WEBUI_TITLE"] == "Existing Title" assert values["WEBUI_DESCRIPTION"] == "Existing Description" assert values["LLM_BINDING"] == "openai" assert values["LLM_BINDING_API_KEY"] == "sk-existing" assert values["EMBEDDING_BINDING_API_KEY"] == "sk-existing" assert values["SSL_SET"] == "set" assert values["AUTH_ACCOUNTS_SET"] == "set" assert values["TOKEN_SECRET_SET"] == "set" assert values["LIGHTRAG_API_KEY_SET"] == "set" assert values["LANGFUSE_ENABLE_TRACE_SET"] == "set" assert values["LANGFUSE_SECRET_KEY_SET"] == "set" def test_env_base_flow_preserves_existing_provider_bindings_on_rerun( tmp_path: Path, ) -> None: """Rerunning env-base should keep prior LLM and embedding provider settings.""" env_file = tmp_path / ".env" env_file.write_text( "\n".join( [ "LLM_BINDING=ollama", "LLM_MODEL=llama3.2:latest", "LLM_BINDING_HOST=http://localhost:11434", "EMBEDDING_BINDING=ollama", "EMBEDDING_MODEL=nomic-embed-text:latest", "EMBEDDING_DIM=768", "EMBEDDING_BINDING_HOST=http://localhost:11434", ] ) + "\n", encoding="utf-8", ) output = run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ return 1; }} finalize_base_setup() {{ printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}" printf 'LLM_MODEL=%s\\n' "${{ENV_VALUES[LLM_MODEL]}}" printf 'LLM_BINDING_HOST=%s\\n' "${{ENV_VALUES[LLM_BINDING_HOST]}}" printf 'EMBEDDING_BINDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING]}}" printf 'EMBEDDING_MODEL=%s\\n' "${{ENV_VALUES[EMBEDDING_MODEL]}}" printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}" printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}" }} env_base_flow """) values = parse_lines(output) assert values["LLM_BINDING"] == "ollama" assert values["LLM_MODEL"] == "llama3.2:latest" assert values["LLM_BINDING_HOST"] == "http://localhost:11434" assert values["EMBEDDING_BINDING"] == "ollama" assert values["EMBEDDING_MODEL"] == "nomic-embed-text:latest" assert values["EMBEDDING_DIM"] == "768" assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:11434" def test_env_base_flow_preserves_existing_vllm_embedding_settings_on_rerun( tmp_path: Path, ) -> None: """Rerunning env-base should keep saved local vLLM embedding settings.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=BAAI/custom-embed", "EMBEDDING_DIM=768", "EMBEDDING_BINDING_HOST=http://localhost:9101/v1", "EMBEDDING_BINDING_API_KEY=embed-key", "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm", "VLLM_EMBED_MODEL=BAAI/custom-embed", "VLLM_EMBED_PORT=9101", "VLLM_EMBED_DEVICE=cpu", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; *) return 1 ;; esac }} finalize_base_setup() {{ printf 'EMBEDDING_MODEL=%s\\n' "${{ENV_VALUES[EMBEDDING_MODEL]}}" printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}" printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}" printf 'VLLM_EMBED_MODEL=%s\\n' "${{ENV_VALUES[VLLM_EMBED_MODEL]}}" printf 'VLLM_EMBED_PORT=%s\\n' "${{ENV_VALUES[VLLM_EMBED_PORT]}}" }} env_base_flow """) assert values["EMBEDDING_MODEL"] == "BAAI/custom-embed" assert values["EMBEDDING_DIM"] == "768" assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1" assert values["VLLM_EMBED_MODEL"] == "BAAI/custom-embed" assert values["VLLM_EMBED_PORT"] == "9101" def test_env_base_flow_resets_remote_embedding_host_when_switching_to_vllm( tmp_path: Path, ) -> None: """Switching a remote embedding provider to local vLLM should restore localhost.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=jina", "EMBEDDING_MODEL=jina-embeddings-v4", "EMBEDDING_DIM=2048", "EMBEDDING_BINDING_HOST=https://api.jina.ai/v1/embeddings", "EMBEDDING_BINDING_API_KEY=jina-key", "VLLM_EMBED_PORT=9101", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; "Enable reranking?") return 1 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1; }} finalize_base_setup() {{ printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}" printf 'LIGHTRAG_SETUP_EMBEDDING_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]}}" }} env_base_flow """) assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1" assert values["LIGHTRAG_SETUP_EMBEDDING_PROVIDER"] == "vllm" def test_env_base_flow_preserves_existing_vllm_embedding_device_on_gpu_host( tmp_path: Path, ) -> None: """Saved vLLM embedding CPU/GPU mode should win over auto-detected GPU defaults.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=BAAI/custom-embed", "EMBEDDING_DIM=1024", "EMBEDDING_BINDING_HOST=http://localhost:9101/v1", "EMBEDDING_BINDING_API_KEY=embed-key", "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm", "VLLM_EMBED_MODEL=BAAI/custom-embed", "VLLM_EMBED_PORT=9101", "VLLM_EMBED_DEVICE=cpu", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; *) return 1 ;; esac }} finalize_base_setup() {{ printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}" }} env_base_flow """) assert values["VLLM_EMBED_DEVICE"] == "cpu" def test_env_base_flow_preserves_existing_vllm_embedding_cuda_device_on_rerun( tmp_path: Path, ) -> None: """Saved vLLM embedding CUDA mode should survive env-base reruns.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=BAAI/custom-embed", "EMBEDDING_DIM=1024", "EMBEDDING_BINDING_HOST=http://localhost:9101/v1", "EMBEDDING_BINDING_API_KEY=embed-key", "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm", "VLLM_EMBED_MODEL=BAAI/custom-embed", "VLLM_EMBED_PORT=9101", "VLLM_EMBED_DEVICE=cuda", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; *) return 1 ;; esac }} finalize_base_setup() {{ printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}" }} env_base_flow """) assert values["VLLM_EMBED_DEVICE"] == "cuda" def test_env_base_flow_defaults_new_vllm_embedding_to_cuda_on_gpu_host( tmp_path: Path, ) -> None: """Fresh local vLLM embedding setup should honor GPU auto-detection.""" values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; "Enable reranking?") return 1 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1 }} finalize_base_setup() {{ printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}" }} env_base_flow """) assert values["VLLM_EMBED_DEVICE"] == "cuda" def test_env_base_flow_forced_vllm_cuda_selection_writes_cuda_devices_to_env( tmp_path: Path, ) -> None: """Forced CUDA selection should drive both .env devices and GPU compose templates.""" write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / "docker-compose.yml", (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8").splitlines(), ) result = run_bash_process( f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" host_cuda_available() {{ return 1; }} prompt_choice() {{ case "$1" in "LLM provider") printf 'ollama' ;; "Embedding device"|"Rerank device") printf 'cuda' ;; *) printf '%s' "$2" ;; esac }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; "Enable reranking?") return 0 ;; "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in *"The compose file will be created/updated. Continue?"*) return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """, cwd=tmp_path, ) assert result.returncode == 0, result.stderr generated_env = (tmp_path / ".env").read_text(encoding="utf-8") generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert "VLLM_EMBED_DEVICE=cuda" in generated_env assert "VLLM_RERANK_DEVICE=cuda" in generated_env assert generated_compose.count("capabilities: [gpu]") >= 2 assert ( "CUDA device selected for vLLM embedding but no NVIDIA driver detected on host." in result.stdout ) assert ( "CUDA device selected for vLLM rerank but no NVIDIA driver detected on host." in result.stdout ) def test_env_base_flow_vllm_defaults_prefer_original_env_values_on_rerun( tmp_path: Path, ) -> None: """vLLM prompt defaults should prefer the loaded `.env` snapshot over later mutations.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=BAAI/original-embed", "EMBEDDING_DIM=1024", "EMBEDDING_BINDING_HOST=http://localhost:9101/v1", "EMBEDDING_BINDING_API_KEY=embed-key", "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm", "VLLM_EMBED_MODEL=BAAI/original-embed", "VLLM_EMBED_PORT=9101", "VLLM_EMBED_DEVICE=cpu", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/original-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/original-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cpu", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} collect_llm_config() {{ ENV_VALUES[VLLM_EMBED_MODEL]="BAAI/mutated-embed" ENV_VALUES[EMBEDDING_DIM]="2048" ENV_VALUES[VLLM_EMBED_PORT]="9991" ENV_VALUES[EMBEDDING_BINDING_HOST]="http://localhost:9991/v1" ENV_VALUES[VLLM_EMBED_DEVICE]="cuda" ENV_VALUES[VLLM_RERANK_MODEL]="BAAI/mutated-rerank" ENV_VALUES[VLLM_RERANK_PORT]="9990" ENV_VALUES[RERANK_BINDING_HOST]="http://localhost:9990/rerank" ENV_VALUES[VLLM_RERANK_DEVICE]="cuda" }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Enable reranking?") return 0 ;; "Run embedding model locally via Docker (vLLM)?") return 0 ;; "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} finalize_base_setup() {{ printf 'VLLM_EMBED_MODEL=%s\\n' "${{ENV_VALUES[VLLM_EMBED_MODEL]}}" printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}" printf 'VLLM_EMBED_PORT=%s\\n' "${{ENV_VALUES[VLLM_EMBED_PORT]}}" printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}" printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}" printf 'VLLM_RERANK_MODEL=%s\\n' "${{ENV_VALUES[VLLM_RERANK_MODEL]}}" printf 'VLLM_RERANK_PORT=%s\\n' "${{ENV_VALUES[VLLM_RERANK_PORT]}}" printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}" printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}" }} env_base_flow """) assert values["VLLM_EMBED_MODEL"] == "BAAI/original-embed" assert values["EMBEDDING_DIM"] == "1024" assert values["VLLM_EMBED_PORT"] == "9101" assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1" assert values["VLLM_EMBED_DEVICE"] == "cpu" assert values["VLLM_RERANK_MODEL"] == "BAAI/original-rerank" assert values["VLLM_RERANK_PORT"] == "9200" assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank" assert values["VLLM_RERANK_DEVICE"] == "cpu" def test_env_base_flow_vllm_device_prompt_is_first_after_docker_choice( tmp_path: Path, ) -> None: """vLLM should ask for device before model-specific prompts once Docker is selected.""" values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" PROMPT_LOG_FILE="$(mktemp)" : > "$PROMPT_LOG_FILE" prompt_choice() {{ printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE" printf '%s' "$2" }} prompt_with_default() {{ printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE" printf '%s' "$2" }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 0 ;; "Enable reranking?") return 0 ;; "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1 }} finalize_base_setup() {{ :; }} env_base_flow printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")" """) prompt_log = values["PROMPT_LOG"] assert prompt_log.index("Embedding device") < prompt_log.index("Embedding model") assert prompt_log.index("Rerank device") < prompt_log.index("Rerank model") def test_env_base_flow_preserves_ssl_config_on_rerun(tmp_path: Path) -> None: """env-base should preserve SSL config on rerun, even when old paths are stale.""" cases = { "stale-paths": [ "SSL=true", "SSL_CERTFILE=/missing/cert.pem", "SSL_KEYFILE=/missing/key.pem", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING_API_KEY=sk-existing", ], "existing-paths": [ "SSL=true", "SSL_CERTFILE=/some/cert.pem", "SSL_KEYFILE=/some/key.pem", ], } for case_name, env_lines in cases.items(): case_dir = tmp_path / case_name case_dir.mkdir() write_text_lines( case_dir / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines(case_dir / ".env", env_lines) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{case_dir}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) generated_lines = (case_dir / ".env").read_text(encoding="utf-8").splitlines() for line in env_lines: assert line in generated_lines def test_env_base_flow_preserves_existing_compose_ssl_when_env_paths_are_stale( tmp_path: Path, ) -> None: """env-base should keep compose SSL wiring when inherited source paths no longer exist.""" write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / ".env", [ "SSL=true", "SSL_CERTFILE=/missing/cert.pem", "SSL_KEYFILE=/missing/key.pem", "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=text-embedding-3-small", "EMBEDDING_DIM=1536", "EMBEDDING_BINDING_HOST=https://api.openai.com/v1", "EMBEDDING_BINDING_API_KEY=sk-existing", ], ) write_text_lines( tmp_path / "docker-compose.final.yml", [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' SSL_CERTFILE: "/app/data/certs/cert.pem"', ' SSL_KEYFILE: "/app/data/certs/key.pem"', " volumes:", ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"', ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"', ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Run LightRAG Server via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose def test_env_base_flow_preserves_existing_storage_images_on_rerun( tmp_path: Path, ) -> None: """env-base should preserve postgres and neo4j images from an existing compose rerun.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " image: registry.example.com/postgres-for-rag:patched", " neo4j:", " image: registry.example.com/neo4j:custom", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" host_cuda_available() {{ return 1; }} collect_llm_config() {{ :; }} collect_embedding_config() {{ :; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in *"The compose file will be created/updated. Continue?"*) return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} env_base_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: registry.example.com/postgres-for-rag:patched" in result assert "image: registry.example.com/neo4j:custom" in result def test_env_base_flow_backs_up_legacy_generated_compose_before_rewrite( tmp_path: Path, ) -> None: """env-base should back up the active legacy compose file before regenerating final output.""" legacy_compose = ( "\n".join(["services:", " lightrag:", " image: prod/lightrag"]) + "\n" ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=text-embedding-3-small", "EMBEDDING_DIM=1536", "EMBEDDING_BINDING_HOST=https://api.openai.com/v1", "EMBEDDING_BINDING_API_KEY=sk-existing", ], ) (tmp_path / "docker-compose.production.yml").write_text( legacy_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ case "$1" in "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;; *) printf '%s' "$2" ;; esac }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Run LightRAG Server via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) assert_single_compose_backup(tmp_path, legacy_compose) assert (tmp_path / "docker-compose.final.yml").exists() assert (tmp_path / "docker-compose.production.yml").read_text( encoding="utf-8" ) == legacy_compose def test_env_base_flow_deletes_compose_when_switching_lightrag_to_host( tmp_path: Path, ) -> None: """env-base should back up and delete compose when no Docker services remain.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " redis:", " image: redis:latest", ] ) + "\n" ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / ".env", [ "LIGHTRAG_RUNTIME_TARGET=compose", "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "EMBEDDING_BINDING=openai", "EMBEDDING_MODEL=text-embedding-3-small", "EMBEDDING_DIM=1536", "EMBEDDING_BINDING_HOST=https://api.openai.com/v1", "EMBEDDING_BINDING_API_KEY=sk-existing", ], ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ case "$1" in "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;; *) printf '%s' "$2" ;; esac }} confirm_default_no() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) assert_single_compose_backup(tmp_path, existing_compose) assert not (tmp_path / "docker-compose.final.yml").exists() generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env def test_env_base_flow_generates_env_and_compose_files(tmp_path: Path) -> None: """env-base should generate `.env` and docker-compose output for hosted and local providers.""" cases = { "openai": { "prompt_choice": "prompt_choice() { printf '%s' \"$2\"; }", "prompt_secret": """ prompt_secret_until_valid_with_default() { case "$1" in "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;; *) printf '%s' "$2" ;; esac } """, "env_assertions": [ "LLM_BINDING=openai", "LLM_BINDING_API_KEY=sk-test-key", "EMBEDDING_BINDING_API_KEY=sk-test-key", ], }, "ollama": { "prompt_choice": """ prompt_choice() { case "$1" in "LLM provider") printf 'ollama' ;; "Embedding provider") printf 'ollama' ;; *) printf '%s' "$2" ;; esac } """, "prompt_secret": "prompt_secret_until_valid_with_default() { printf '%s' \"$2\"; }", "env_assertions": ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"], }, } for case_name, case in cases.items(): case_dir = tmp_path / case_name case_dir.mkdir() write_text_lines( case_dir / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( case_dir / "docker-compose.yml", (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{case_dir}" {case['prompt_choice']} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} {case['prompt_secret']} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 1 ;; "Enable reranking?") return 1 ;; "Run LightRAG Server via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) generated_env = (case_dir / ".env").read_text(encoding="utf-8") generated_compose = (case_dir / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env assert "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage" in generated_env assert "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage" in generated_env assert "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage" in generated_env for expected_line in case["env_assertions"]: assert expected_line in generated_env assert "services:" in generated_compose assert " lightrag:" in generated_compose assert "env_file:" not in generated_compose def test_env_base_flow_generates_validatable_env_on_clean_checkout( tmp_path: Path, ) -> None: """Fresh env-base output should include default storage selections and pass validation.""" write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ case "$1" in "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;; *) printf '%s' "$2" ;; esac }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow validate_env_file """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env assert "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage" in generated_env assert "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage" in generated_env assert "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage" in generated_env assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env assert "LIGHTRAG_SETUP_PROFILE=" not in generated_env def test_env_storage_flow_drops_legacy_setup_profile_on_write(tmp_path: Path) -> None: """Modular flows should not persist LIGHTRAG_SETUP_PROFILE into regenerated .env files.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_SETUP_PROFILE=production", "LIGHTRAG_KV_STORAGE=JsonKVStorage", "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage", "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage", "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} confirm_default_yes() {{ return 0; }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env assert "LIGHTRAG_SETUP_PROFILE=" not in generated_env def test_env_base_flow_registers_vllm_rerank_service_for_docker_deployment( tmp_path: Path, ) -> None: """Choosing docker rerank in env-base should add vllm-rerank to DOCKER_SERVICE_SET.""" output = run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" reset_state collect_llm_config() {{ :; }} collect_embedding_config() {{ :; }} prompt_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 1 ;; "Enable reranking?") return 0 ;; "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1; }} finalize_base_setup() {{ if [[ -n "${{DOCKER_SERVICE_SET[vllm-rerank]+set}}" ]]; then printf 'HAS_VLLM_SERVICE=yes\\n' else printf 'HAS_VLLM_SERVICE=no\\n' fi }} env_base_flow """) values = parse_lines(output) assert values["HAS_VLLM_SERVICE"] == "yes" def test_env_base_flow_preserves_existing_vllm_rerank_settings_on_rerun( tmp_path: Path, ) -> None: """Rerunning env-base should keep saved local vLLM rerank model and port.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/custom-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/custom-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cpu", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Enable reranking?") return 0 ;; "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ return 1; }} collect_embedding_config() {{ :; }} finalize_base_setup() {{ printf 'RERANK_MODEL=%s\\n' "${{ENV_VALUES[RERANK_MODEL]}}" printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}" printf 'VLLM_RERANK_MODEL=%s\\n' "${{ENV_VALUES[VLLM_RERANK_MODEL]}}" printf 'VLLM_RERANK_PORT=%s\\n' "${{ENV_VALUES[VLLM_RERANK_PORT]}}" }} env_base_flow """) assert values["RERANK_MODEL"] == "BAAI/custom-rerank" assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank" assert values["VLLM_RERANK_MODEL"] == "BAAI/custom-rerank" assert values["VLLM_RERANK_PORT"] == "9200" def test_env_base_flow_does_not_repeat_rerank_docker_prompt_when_declined( tmp_path: Path, ) -> None: """Declining rerank Docker at the outer prompt should switch to endpoint-based config.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/custom-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/custom-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cpu", ], ) output = run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" DOCKER_PROMPT_COUNT=0 RERANK_MODEL_PROMPT_LOG="$REPO_ROOT/rerank-model-prompts.log" : > "$RERANK_MODEL_PROMPT_LOG" prompt_choice() {{ case "$1" in "vLLM device") echo "unexpected vLLM device prompt" >&2 return 91 ;; *) printf '%s' "$2" ;; esac }} prompt_with_default() {{ case "$1" in "vLLM rerank model") echo "unexpected vLLM rerank model prompt" >&2 return 93 ;; "Rerank model") printf 'hit ' >> "$RERANK_MODEL_PROMPT_LOG" printf '%s' "$2" return 0 ;; "Rerank endpoint") printf '%s' "https://rerank.example.internal/rerank" return 0 ;; esac printf '%s' "$2" }} prompt_until_valid() {{ case "$1" in "vLLM rerank port") echo "unexpected vLLM rerank port prompt" >&2 return 92 ;; esac printf '%s' "$2" }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ return 1; }} confirm_default_yes() {{ case "$1" in "Enable reranking?") return 0 ;; "Run rerank service locally via Docker?") DOCKER_PROMPT_COUNT=$((DOCKER_PROMPT_COUNT + 1)) return 1 ;; *) return 1 ;; esac }} collect_embedding_config() {{ :; }} finalize_base_setup() {{ local rerank_model_prompt_count rerank_model_prompt_count="$(wc -l < "$RERANK_MODEL_PROMPT_LOG" | tr -d '[:space:]')" printf 'DOCKER_PROMPT_COUNT=%s\\n' "$DOCKER_PROMPT_COUNT" printf 'RERANK_MODEL_PROMPT_COUNT=%s\\n' "$rerank_model_prompt_count" printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}" printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}}" }} env_base_flow """) values = parse_lines(output) assert values["DOCKER_PROMPT_COUNT"] == "1" assert values["RERANK_MODEL_PROMPT_COUNT"] == "1" assert values["RERANK_BINDING_HOST"] == "https://rerank.example.internal/rerank" assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == "" assert "vLLM uses the Cohere-compatible rerank API." not in output def test_env_base_flow_comments_rerank_setup_marker_when_switching_off_docker( tmp_path: Path, ) -> None: """Switching rerank from Docker to a non-Docker provider should drop the setup marker.""" write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/custom-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/custom-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cpu", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ case "$1" in "Rerank provider") printf 'cohere' ;; *) printf '%s' "$2" ;; esac }} prompt_with_default() {{ case "$1" in "Rerank endpoint") printf '%s' "https://api.cohere.com/v2/rerank" ;; *) printf '%s' "$2" ;; esac }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run embedding model locally via Docker (vLLM)?") return 1 ;; "Run rerank service locally via Docker?") return 1 ;; "Run LightRAG Server via Docker?") return 1 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in "Enable reranking?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_base_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") active_marker_lines = [ line for line in generated_env.splitlines() if line.startswith("LIGHTRAG_SETUP_RERANK_PROVIDER=") ] assert "RERANK_BINDING=cohere" in generated_env assert active_marker_lines == [] def test_env_base_flow_resets_remote_rerank_host_when_switching_to_vllm( tmp_path: Path, ) -> None: """Switching a remote reranker to local vLLM should restore localhost.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=jina", "RERANK_MODEL=jina-reranker-v2-base-multilingual", "RERANK_BINDING_HOST=https://api.jina.ai/v1/rerank", "RERANK_BINDING_API_KEY=jina-key", "VLLM_RERANK_PORT=9200", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in "Enable reranking?") return 0 ;; *) return 1 ;; esac }} collect_embedding_config() {{ :; }} finalize_base_setup() {{ printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}" printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}" printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]}}" }} env_base_flow """) assert values["RERANK_BINDING"] == "cohere" assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank" assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == "vllm" def test_env_base_flow_preserves_existing_vllm_rerank_device_on_gpu_host( tmp_path: Path, ) -> None: """Saved vLLM rerank CPU/GPU mode should win over auto-detected GPU defaults.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/custom-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/custom-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cpu", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Enable reranking?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} collect_embedding_config() {{ :; }} finalize_base_setup() {{ printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}" }} env_base_flow """) assert values["VLLM_RERANK_DEVICE"] == "cpu" def test_env_base_flow_preserves_existing_vllm_rerank_cuda_device_on_rerun( tmp_path: Path, ) -> None: """Saved vLLM rerank CUDA mode should survive env-base reruns.""" write_text_lines( tmp_path / ".env", [ "LLM_BINDING=openai", "LLM_MODEL=gpt-4o-mini", "LLM_BINDING_HOST=https://api.openai.com/v1", "LLM_BINDING_API_KEY=sk-existing", "RERANK_BINDING=cohere", "RERANK_MODEL=BAAI/custom-rerank", "RERANK_BINDING_HOST=http://localhost:9200/rerank", "RERANK_BINDING_API_KEY=rerank-key", "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm", "VLLM_RERANK_MODEL=BAAI/custom-rerank", "VLLM_RERANK_PORT=9200", "VLLM_RERANK_DEVICE=cuda", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" nvidia-smi() {{ return 0; }} prompt_choice() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_no() {{ case "$1" in "Enable reranking?") return 0 ;; *) return 1 ;; esac }} confirm_default_yes() {{ case "$1" in "Run rerank service locally via Docker?") return 0 ;; *) return 1 ;; esac }} collect_embedding_config() {{ :; }} finalize_base_setup() {{ printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}" }} env_base_flow """) assert values["VLLM_RERANK_DEVICE"] == "cuda" def test_env_storage_flow_applies_selected_storage_backends(tmp_path: Path) -> None: """env-storage should honor the selected backends without auto-applying a preset.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_KV_STORAGE=JsonKVStorage", "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage", "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage", "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage", "LLM_BINDING=ollama", "LLM_MODEL=llama3.2:latest", "LLM_BINDING_HOST=http://localhost:11434", "EMBEDDING_BINDING=ollama", "EMBEDDING_MODEL=nomic-embed-text:latest", "EMBEDDING_DIM=768", "EMBEDDING_BINDING_HOST=http://localhost:11434", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="RedisKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MilvusVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="RedisDocStatusStorage" }} collect_database_config() {{ :; }} collect_docker_image_tags() {{ :; }} finalize_storage_setup() {{ printf 'LIGHTRAG_KV_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_KV_STORAGE]}}" printf 'LIGHTRAG_VECTOR_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]}}" printf 'LIGHTRAG_GRAPH_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]}}" printf 'LIGHTRAG_DOC_STATUS_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]}}" printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}" printf 'EMBEDDING_BINDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING]}}" }} env_storage_flow """) assert values["LIGHTRAG_KV_STORAGE"] == "RedisKVStorage" assert values["LIGHTRAG_VECTOR_STORAGE"] == "MilvusVectorDBStorage" assert values["LIGHTRAG_GRAPH_STORAGE"] == "Neo4JStorage" assert values["LIGHTRAG_DOC_STATUS_STORAGE"] == "RedisDocStatusStorage" assert values["LLM_BINDING"] == "ollama" assert values["EMBEDDING_BINDING"] == "ollama" def test_env_storage_flow_reuses_saved_storage_docker_default(tmp_path: Path) -> None: """Saved storage deployment metadata should drive the next Docker prompt default.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_KV_STORAGE=PGKVStorage", "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage", "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage", "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage", ], ) values = run_bash_lines(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" REQUIRED_DB_TYPES[postgresql]=1 }} collect_postgres_config() {{ printf 'POSTGRES_DEFAULT_DOCKER=%s\\n' "$1" }} finalize_storage_setup() {{ :; }} env_storage_flow """) assert values["POSTGRES_DEFAULT_DOCKER"] == "yes" def test_env_storage_flow_writes_storage_docker_marker_for_selected_service( tmp_path: Path, ) -> None: """Choosing a bundled storage service should persist its deployment marker in `.env`.""" write_text_lines( tmp_path / ".env", ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"] ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.yml").write_text( (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" REQUIRED_DB_TYPES[postgresql]=1 }} collect_postgres_config() {{ add_docker_service "postgres" ENV_VALUES[POSTGRES_HOST]="localhost" ENV_VALUES[POSTGRES_PORT]="5432" ENV_VALUES[POSTGRES_USER]="lightrag" ENV_VALUES[POSTGRES_PASSWORD]="secret" ENV_VALUES[POSTGRES_DATABASE]="lightrag" }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert any( line == "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker" for line in generated_env.splitlines() ) assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_storage_flow_writes_opensearch_docker_marker_for_selected_service( tmp_path: Path, ) -> None: """Choosing bundled OpenSearch should persist its deployment marker in `.env`.""" write_text_lines( tmp_path / ".env", ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"] ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.yml").write_text( (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="OpenSearchKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="OpenSearchVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="OpenSearchGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="OpenSearchDocStatusStorage" REQUIRED_DB_TYPES[opensearch]=1 }} collect_opensearch_config() {{ add_docker_service "opensearch" ENV_VALUES[OPENSEARCH_HOSTS]="localhost:9200" ENV_VALUES[OPENSEARCH_USER]="admin" ENV_VALUES[OPENSEARCH_PASSWORD]="secret" ENV_VALUES[OPENSEARCH_USE_SSL]="true" ENV_VALUES[OPENSEARCH_VERIFY_CERTS]="false" }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert any( line == "LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT=docker" for line in generated_env.splitlines() ) assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_storage_flow_removes_storage_docker_marker_when_switching_to_host( tmp_path: Path, ) -> None: """Choosing a host-managed storage backend should clear a previously saved Docker marker.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_KV_STORAGE=PGKVStorage", "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage", "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage", "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" REQUIRED_DB_TYPES[postgresql]=1 }} collect_postgres_config() {{ ENV_VALUES[POSTGRES_HOST]="localhost" ENV_VALUES[POSTGRES_PORT]="5432" ENV_VALUES[POSTGRES_USER]="lightrag" ENV_VALUES[POSTGRES_PASSWORD]="secret" ENV_VALUES[POSTGRES_DATABASE]="lightrag" }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert not any( line.startswith("LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=") for line in generated_env.splitlines() ) assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env def test_env_storage_flow_clears_unused_storage_docker_markers(tmp_path: Path) -> None: """Markers for databases no longer required by the selected backends should be removed.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_KV_STORAGE=PGKVStorage", "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage", "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage", "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert not any( line.startswith("LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=") for line in generated_env.splitlines() ) assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env def test_env_storage_flow_generates_env_and_compose_files(tmp_path: Path) -> None: """env-storage should write updated .env and a docker-compose.final.yml.""" env_file = tmp_path / ".env" env_file.write_text( "\n".join( [ "LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama", "AUTH_ACCOUNTS=admin:secret", "TOKEN_SECRET=jwt-secret", "WHITELIST_PATHS=/health", ] ) + "\n", encoding="utf-8", ) (tmp_path / "env.example").write_text( (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8" ) (tmp_path / "docker-compose.yml").write_text( (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MilvusVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" add_docker_service "postgres" add_docker_service "neo4j" }} collect_database_config() {{ :; }} collect_docker_image_tags() {{ :; }} validate_required_variables() {{ return 0; }} prompt_secret_with_default() {{ printf '%s' "$2"; }} prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }} confirm_default_yes() {{ case "$1" in *) return 1 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert "LIGHTRAG_KV_STORAGE=PGKVStorage" in generated_env assert "LIGHTRAG_GRAPH_STORAGE=Neo4JStorage" in generated_env assert "LLM_BINDING=ollama" in generated_env assert "services:" in generated_compose assert " lightrag:" in generated_compose assert "env_file:" not in generated_compose def test_env_storage_flow_uses_host_defaults_for_empty_postgres_docker_credentials( tmp_path: Path, ) -> None: """env-storage should write the host-mode postgres defaults when old `.env` creds are empty.""" env_file = tmp_path / ".env" env_file.write_text( "\n".join( [ "LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama", "AUTH_ACCOUNTS=admin:secret", "TOKEN_SECRET=jwt-secret", "WHITELIST_PATHS=/health", "POSTGRES_USER=", "POSTGRES_PASSWORD=", "POSTGRES_DATABASE=", ] ) + "\n", encoding="utf-8", ) (tmp_path / "env.example").write_text( (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8" ) (tmp_path / "docker-compose.yml").write_text( (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8" ) run_bash( f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" PROMPT_LOG_FILE="$(mktemp)" : > "$PROMPT_LOG_FILE" select_storage_backends() {{ REQUIRED_DB_TYPES[postgresql]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" }} confirm_default_no() {{ if [[ "$1" == "Run PostgreSQL locally via Docker?" ]]; then return 0 fi return 1 }} confirm_default_yes() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} prompt_with_default() {{ printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE" case "$1" in "PostgreSQL host") printf 'localhost' ;; *) printf '%s' "$2" ;; esac }} prompt_secret_with_default() {{ printf 'secret:%s\\n' "$1" >> "$PROMPT_LOG_FILE" printf '%s' "$2" }} env_storage_flow printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\" """, cwd=tmp_path, ) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert "POSTGRES_USER=rag" in generated_env assert "POSTGRES_PASSWORD=rag" in generated_env assert "POSTGRES_DATABASE=lightrag" in generated_env assert 'POSTGRES_USER: "rag"' in generated_compose assert 'POSTGRES_PASSWORD: "rag"' in generated_compose assert 'POSTGRES_DB: "lightrag"' in generated_compose def test_env_storage_flow_preserves_existing_postgres_image_during_rewrite( tmp_path: Path, ) -> None: """Postgres env rewrites should keep an existing custom image.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "POSTGRES_USER=rag", "POSTGRES_PASSWORD=rag", "POSTGRES_DATABASE=rag", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " image: registry.example.com/postgres-for-rag:patched", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ REQUIRED_DB_TYPES[postgresql]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" }} collect_database_config() {{ if [[ "$1" == "postgresql" ]]; then add_docker_service "postgres" ENV_VALUES[POSTGRES_USER]="updated-user" fi }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: registry.example.com/postgres-for-rag:patched" in result assert 'POSTGRES_USER: "updated-user"' in result assert 'POSTGRES_DB: "rag"' in result def test_env_storage_flow_preserves_existing_neo4j_image_during_rewrite( tmp_path: Path, ) -> None: """Neo4j database rewrites should keep an existing custom image.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "NEO4J_USERNAME=neo4j", "NEO4J_PASSWORD=neo4j-password", "NEO4J_DATABASE=neo4j", ], [ "services:", " lightrag:", " image: example/lightrag:test", " neo4j:", " image: registry.example.com/neo4j:custom", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ REQUIRED_DB_TYPES[neo4j]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ if [[ "$1" == "neo4j" ]]; then add_docker_service "neo4j" ENV_VALUES[NEO4J_DATABASE]="updated-database" fi }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: registry.example.com/neo4j:custom" in result assert 'NEO4J_dbms_default__database: "updated-database"' in result def test_env_storage_flow_preserves_existing_postgres_and_neo4j_images_on_rewrite( tmp_path: Path, ) -> None: """Concurrent postgres and neo4j rewrites should preserve both custom images.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "POSTGRES_USER=rag", "POSTGRES_PASSWORD=rag", "POSTGRES_DATABASE=rag", "NEO4J_USERNAME=neo4j", "NEO4J_PASSWORD=neo4j-password", "NEO4J_DATABASE=neo4j", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " image: registry.example.com/postgres-for-rag:patched", " neo4j:", " image: registry.example.com/neo4j:custom", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ REQUIRED_DB_TYPES[postgresql]=1 REQUIRED_DB_TYPES[neo4j]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" }} collect_database_config() {{ case "$1" in postgresql) add_docker_service "postgres" ENV_VALUES[POSTGRES_USER]="updated-user" ;; neo4j) add_docker_service "neo4j" ENV_VALUES[NEO4J_DATABASE]="updated-database" ;; esac }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: registry.example.com/postgres-for-rag:patched" in result assert "image: registry.example.com/neo4j:custom" in result assert 'POSTGRES_USER: "updated-user"' in result assert 'NEO4J_dbms_default__database: "updated-database"' in result def test_env_storage_flow_uses_template_image_when_existing_service_has_no_image( tmp_path: Path, ) -> None: """A rewritten service without an existing image should fall back to the template image.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "POSTGRES_USER=rag", "POSTGRES_PASSWORD=rag", "POSTGRES_DATABASE=rag", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " environment:", ' LEGACY_SETTING: "1"', ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ REQUIRED_DB_TYPES[postgresql]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" }} collect_database_config() {{ if [[ "$1" == "postgresql" ]]; then add_docker_service "postgres" ENV_VALUES[POSTGRES_USER]="updated-user" fi }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: gzdaniel/postgres-for-rag:pg18-age-pgvector" in result assert 'POSTGRES_USER: "updated-user"' in result def test_env_storage_flow_force_rewrite_drops_preserved_storage_images( tmp_path: Path, ) -> None: """FORCE_REWRITE_COMPOSE should bypass preserved postgres and neo4j images.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "POSTGRES_USER=rag", "POSTGRES_PASSWORD=rag", "POSTGRES_DATABASE=rag", "NEO4J_USERNAME=neo4j", "NEO4J_PASSWORD=neo4j-password", "NEO4J_DATABASE=neo4j", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " image: registry.example.com/postgres-for-rag:patched", " neo4j:", " image: registry.example.com/neo4j:custom", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" FORCE_REWRITE_COMPOSE="yes" select_storage_backends() {{ REQUIRED_DB_TYPES[postgresql]=1 REQUIRED_DB_TYPES[neo4j]=1 ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage" }} collect_database_config() {{ case "$1" in postgresql) add_docker_service "postgres" ENV_VALUES[POSTGRES_USER]="updated-user" ;; neo4j) add_docker_service "neo4j" ENV_VALUES[NEO4J_DATABASE]="updated-database" ;; esac }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: gzdaniel/postgres-for-rag:pg18-age-pgvector" in result assert "image: neo4j:5-community" in result assert "registry.example.com/postgres-for-rag:patched" not in result assert "registry.example.com/neo4j:custom" not in result def test_env_storage_flow_backs_up_existing_compose_before_rewrite( tmp_path: Path, ) -> None: """env-storage should back up the current compose file before rewriting it.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' LEGACY_SETTING: "1"', " postgres:", " image: gzdaniel/postgres-for-rag:pg18-age-pgvector", ] ) + "\n" ) write_text_lines( tmp_path / ".env", ["LLM_BINDING=openai", "EMBEDDING_BINDING=openai"] ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) assert_single_compose_backup(tmp_path, existing_compose) assert (tmp_path / "docker-compose.final.yml").exists() def test_env_storage_flow_keeps_compose_mode_for_user_sidecars(tmp_path: Path) -> None: """env-storage should keep LightRAG in Docker when user sidecars are present.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' LEGACY_SETTING: "1"', " sidecar:", " image: busybox", ] ) + "\n" ) write_text_lines( tmp_path / ".env", ["LLM_BINDING=openai", "EMBEDDING_BINDING=openai"] ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ return 0; }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert_single_compose_backup(tmp_path, existing_compose) assert " lightrag:" in result assert " sidecar:" in result assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_storage_flow_preserves_mongodb_docker_marker_for_atlas_local_vector_storage( tmp_path: Path, ) -> None: """MongoDB Atlas Local vector storage should preserve the bundled Docker deployment marker.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker", "LIGHTRAG_KV_STORAGE=MongoKVStorage", "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage", "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage", "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="MongoKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MongoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="MongoGraphStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="MongoDocStatusStorage" REQUIRED_DB_TYPES[mongodb]=1 }} prompt_until_valid() {{ printf '%s' "$2"; }} prompt_with_default() {{ printf '%s' "$2"; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ return 0; }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker" in generated_env assert "MONGO_URI=mongodb://localhost:27017/?directConnection=true" in generated_env def test_env_storage_flow_preserves_existing_compose_ssl_when_env_paths_are_stale( tmp_path: Path, ) -> None: """env-storage should keep compose SSL wiring when inherited source paths no longer exist.""" write_text_lines( tmp_path / ".env", [ "SSL=true", "SSL_CERTFILE=/missing/cert.pem", "SSL_KEYFILE=/missing/key.pem", "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "LIGHTRAG_KV_STORAGE=JsonKVStorage", "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage", "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage", "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / "docker-compose.final.yml", [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' SSL_CERTFILE: "/app/data/certs/cert.pem"', ' SSL_KEYFILE: "/app/data/certs/key.pem"', " volumes:", ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"', ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"', ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose def test_env_server_flow_preserves_existing_compose_ssl_when_env_paths_are_stale( tmp_path: Path, ) -> None: """env-server should keep compose SSL wiring and variable-based port publishing.""" write_text_lines( tmp_path / ".env", [ "SSL=true", "SSL_CERTFILE=/missing/cert.pem", "SSL_KEYFILE=/missing/key.pem", "HOST=0.0.0.0", "PORT=9621", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / "docker-compose.final.yml", [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' SSL_CERTFILE: "/app/data/certs/cert.pem"', ' SSL_KEYFILE: "/app/data/certs/key.pem"', " volumes:", ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"', ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"', ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ ENV_VALUES[HOST]="0.0.0.0" ENV_VALUES[PORT]="8080" }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_server_flow """) generated_compose = (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose assert 'PORT: "9621"' in generated_compose assert ' - "${HOST:-0.0.0.0}:${PORT:-9621}:9621"' in generated_compose def test_env_server_flow_backs_up_existing_compose_before_rewrite( tmp_path: Path, ) -> None: """env-server should back up the current compose file before rewriting it.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' PORT: "9621"', ] ) + "\n" ) write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0", "PORT=9621"]) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ ENV_VALUES[HOST]="0.0.0.0" ENV_VALUES[PORT]="8080" }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} validate_sensitive_env_literals() {{ return 0; }} validate_security_config() {{ return 0; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_server_flow """) assert_single_compose_backup(tmp_path, existing_compose) assert (tmp_path / "docker-compose.final.yml").read_text( encoding="utf-8" ) != existing_compose def test_env_storage_flow_drops_stale_vllm_services_missing_from_env_markers( tmp_path: Path, ) -> None: """env-storage should remove stale vLLM services unless `.env` still marks them as Docker-managed.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_RUNTIME_TARGET=compose", "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "RERANK_BINDING=cohere", "LIGHTRAG_SETUP_RERANK_PROVIDER=cohere", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " vllm-embed:", " image: vllm/vllm-openai:latest", " vllm-rerank:", " image: vllm/vllm-openai:latest", "volumes:", " vllm_embed_cache:", " vllm_rerank_cache:", ] ) + "\n", encoding="utf-8", ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ return 1; }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert " vllm-embed:" not in result assert " vllm-rerank:" not in result assert "vllm_embed_cache:" not in result assert "vllm_rerank_cache:" not in result assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_storage_flow_preserves_vllm_services_marked_in_env(tmp_path: Path) -> None: """env-storage should restore vLLM services from `.env` markers even without old compose entries.""" write_text_lines( tmp_path / ".env", [ "LIGHTRAG_RUNTIME_TARGET=compose", "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "EMBEDDING_BINDING_HOST=http://localhost:8001/v1", "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm", "VLLM_EMBED_MODEL=BAAI/bge-m3", "VLLM_EMBED_PORT=8001", "VLLM_EMBED_DEVICE=cpu", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) write_text_lines( tmp_path / "docker-compose.final.yml", ["services:", " lightrag:", " image: example/lightrag:test"], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ return 1; }} confirm_default_no() {{ return 1; }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert " vllm-embed:" in result assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_storage_flow_deletes_compose_when_switching_lightrag_to_host( tmp_path: Path, ) -> None: """env-storage should back up and delete compose when no Docker services remain.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " redis:", " image: redis:latest", ] ) + "\n" ) write_text_lines( tmp_path / ".env", [ "LIGHTRAG_RUNTIME_TARGET=compose", "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" select_storage_backends() {{ ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage" ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage" ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage" ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage" }} collect_database_config() {{ :; }} validate_required_variables() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} confirm_default_yes() {{ return 1; }} confirm_default_no() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_storage_flow """) assert_single_compose_backup(tmp_path, existing_compose) assert not (tmp_path / "docker-compose.final.yml").exists() generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env def test_env_server_flow_preserves_existing_storage_images_on_compose_rewrite( tmp_path: Path, ) -> None: """env-server should preserve postgres and neo4j images when a compose rewrite is triggered.""" original_compose_lines = [ "services:", " lightrag:", " image: example/lightrag:test", " environment:", ' PORT: "9621"', " postgres:", " image: registry.example.com/postgres-for-rag:patched", " neo4j:", " image: registry.example.com/neo4j:custom", ] original_compose_content = "\n".join(original_compose_lines) + "\n" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "HOST=0.0.0.0", "PORT=9621", "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker", ], original_compose_lines, ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ ENV_VALUES[HOST]="0.0.0.0" ENV_VALUES[PORT]="8080" }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} confirm_default_yes() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;; *) return 0 ;; esac }} confirm_required_yes_no() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} validate_auth_accounts_runtime_config() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} env_server_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert_single_compose_backup(tmp_path, expected_content=original_compose_content) assert "image: registry.example.com/postgres-for-rag:patched" in result assert "image: registry.example.com/neo4j:custom" in result def test_env_server_flow_preserves_existing_storage_images_on_env_only_rerun( tmp_path: Path, ) -> None: """env-server write_env_only path should leave custom storage images untouched.""" write_storage_setup_files( tmp_path, [ "LLM_BINDING=openai", "EMBEDDING_BINDING=openai", "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker", "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker", ], [ "services:", " lightrag:", " image: example/lightrag:test", " postgres:", " image: registry.example.com/postgres-for-rag:patched", " neo4j:", " image: registry.example.com/neo4j:custom", ], ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ :; }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} confirm_required_yes_no() {{ return 0; }} validate_sensitive_env_literals() {{ return 0; }} validate_auth_accounts_runtime_config() {{ return 0; }} validate_mongo_vector_storage_config() {{ return 0; }} env_server_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") assert "image: registry.example.com/postgres-for-rag:patched" in result assert "image: registry.example.com/neo4j:custom" in result def test_env_server_flow_deletes_compose_when_switching_lightrag_to_host( tmp_path: Path, ) -> None: """env-server should back up and delete compose when no managed or sidecar services remain.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " redis:", " image: redis:latest", ] ) + "\n" ) write_text_lines( tmp_path / ".env", ["LIGHTRAG_RUNTIME_TARGET=compose", "HOST=0.0.0.0", "PORT=9621"], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ ENV_VALUES[HOST]="0.0.0.0" ENV_VALUES[PORT]="8080" }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} validate_sensitive_env_literals() {{ return 0; }} validate_security_config() {{ return 0; }} confirm_default_yes() {{ return 1; }} confirm_default_no() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_server_flow """) assert_single_compose_backup(tmp_path, existing_compose) assert not (tmp_path / "docker-compose.final.yml").exists() generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env def test_env_server_flow_keeps_compose_mode_for_user_sidecars(tmp_path: Path) -> None: """env-server should keep LightRAG in Docker when compose still carries user sidecars.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " sidecar:", " image: busybox", ] ) + "\n" ) write_text_lines( tmp_path / ".env", ["LIGHTRAG_RUNTIME_TARGET=compose", "HOST=0.0.0.0", "PORT=9621"], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) run_bash(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ ENV_VALUES[HOST]="0.0.0.0" ENV_VALUES[PORT]="8080" }} collect_security_config() {{ :; }} collect_ssl_config() {{ :; }} validate_sensitive_env_literals() {{ return 0; }} validate_security_config() {{ return 0; }} confirm_default_yes() {{ return 0; }} confirm_required_yes_no() {{ return 0; }} env_server_flow """) result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8") generated_env = (tmp_path / ".env").read_text(encoding="utf-8") assert " sidecar:" in result assert " lightrag:" in result assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env def test_env_server_flow_rejects_invalid_ssl_cert_when_switching_to_host( tmp_path: Path, ) -> None: """finalize_server_setup should reject a missing SSL cert even when switching to host mode.""" existing_compose = ( "\n".join( [ "services:", " lightrag:", " image: example/lightrag:test", " redis:", " image: redis:latest", ] ) + "\n" ) write_text_lines( tmp_path / ".env", [ "LIGHTRAG_RUNTIME_TARGET=compose", "HOST=0.0.0.0", "PORT=9621", "SSL=true", "SSL_CERTFILE=/nonexistent/cert.pem", "SSL_KEYFILE=/nonexistent/key.pem", ], ) write_text_lines( tmp_path / "env.example", (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(), ) (tmp_path / "docker-compose.final.yml").write_text( existing_compose, encoding="utf-8" ) result = run_bash_process(f""" set -euo pipefail source "{REPO_ROOT}/scripts/setup/setup.sh" REPO_ROOT="{tmp_path}" collect_server_config() {{ :; }} collect_security_config() {{ :; }} collect_ssl_config() {{ ENV_VALUES[SSL]="true" SSL_CERT_SOURCE_PATH="/nonexistent/cert.pem" SSL_KEY_SOURCE_PATH="/nonexistent/key.pem" }} validate_sensitive_env_literals() {{ return 0; }} validate_security_config() {{ return 0; }} confirm_default_yes() {{ return 1; }} confirm_default_no() {{ case "$1" in "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;; *) return 1 ;; esac }} confirm_required_yes_no() {{ return 0; }} env_server_flow """) assert result.returncode != 0 assert ( "Invalid SSL_CERTFILE" in result.stderr or "Invalid SSL_CERTFILE" in result.stdout ) assert (tmp_path / "docker-compose.final.yml").exists() assert "LIGHTRAG_RUNTIME_TARGET=compose" in (tmp_path / ".env").read_text( encoding="utf-8" )