1861 lines
60 KiB
Python
1861 lines
60 KiB
Python
# Regression tests for interactive setup wizard.
|
|
# Classification: keep tests here when they cover generate_* helpers that render or rewrite .env and docker-compose file contents.
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.setup._helpers import (
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
REPO_ROOT,
|
|
parse_lines,
|
|
run_bash,
|
|
write_text_lines,
|
|
)
|
|
|
|
pytestmark = pytest.mark.offline
|
|
|
|
|
|
def test_generate_files_keep_host_env_values_and_inject_compose_overrides(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""This generation path keeps host-style values in `.env` and injects compose-only overrides separately."""
|
|
env_example = tmp_path / "env.example"
|
|
env_example.write_text(
|
|
"\n".join(
|
|
[
|
|
"SSL_CERTFILE=/placeholder/cert.pem",
|
|
"SSL_KEYFILE=/placeholder/key.pem",
|
|
"LLM_BINDING_HOST=https://api.example.com/v1",
|
|
"EMBEDDING_BINDING_HOST=https://api.example.com/v1",
|
|
"RERANK_BINDING_HOST=https://api.example.com/v1",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
compose_file = tmp_path / "docker-compose.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" env_file:",
|
|
" - .env",
|
|
" volumes:",
|
|
" - ./.env:/app/.env",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
cert_path = tmp_path / "cert.pem"
|
|
cert_path.write_text("cert", encoding="utf-8")
|
|
key_path = tmp_path / "key.pem"
|
|
key_path.write_text("key", encoding="utf-8")
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[SSL_CERTFILE]="{cert_path}"
|
|
ENV_VALUES[SSL_KEYFILE]="{key_path}"
|
|
ENV_VALUES[LLM_BINDING_HOST]="http://localhost:11434"
|
|
ENV_VALUES[EMBEDDING_BINDING_HOST]="http://127.0.0.1:11434"
|
|
ENV_VALUES[RERANK_BINDING_HOST]="http://localhost:8000/rerank"
|
|
SSL_CERT_SOURCE_PATH="{cert_path}"
|
|
SSL_KEY_SOURCE_PATH="{key_path}"
|
|
|
|
prepare_compose_env_overrides
|
|
stage_ssl_assets "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert f"SSL_CERTFILE={cert_path}" in generated_env
|
|
assert f"SSL_KEYFILE={key_path}" in generated_env
|
|
assert "LLM_BINDING_HOST=http://localhost:11434" in generated_env
|
|
assert "EMBEDDING_BINDING_HOST=http://127.0.0.1:11434" in generated_env
|
|
assert "RERANK_BINDING_HOST=http://localhost:8000/rerank" in generated_env
|
|
assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose
|
|
assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose
|
|
assert 'LLM_BINDING_HOST: "http://host.docker.internal:11434"' in generated_compose
|
|
assert (
|
|
'EMBEDDING_BINDING_HOST: "http://host.docker.internal:11434"'
|
|
in generated_compose
|
|
)
|
|
assert (
|
|
'RERANK_BINDING_HOST: "http://host.docker.internal:8000/rerank"'
|
|
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 "env_file:" not in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_removes_lightrag_env_file_to_preserve_dollar_values(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Generated compose should remove `env_file` and skip empty environment blocks."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" container_name: lightrag",
|
|
" image: example/lightrag:test",
|
|
" env_file:",
|
|
" - .env",
|
|
" volumes:",
|
|
" - ./.env:/app/.env",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "env_file:" not in generated_compose
|
|
assert "environment:" not in generated_compose
|
|
assert "container_name:" not in generated_compose
|
|
assert "- ./.env:/app/.env" in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_removes_lightrag_container_name_from_existing_output(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Compose regeneration should strip fixed lightrag container names from prior output."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" container_name: lightrag",
|
|
" image: example/lightrag:test",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "container_name:" not in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_preserves_list_style_lightrag_environment(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Compose regeneration should not mix mapping entries into list-style environments."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" environment:",
|
|
" - PORT=9621",
|
|
" - FOO=bar",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
set_compose_override "PORT" "1234"
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert ' - "PORT=1234"' in generated_compose
|
|
assert " - FOO=bar" in generated_compose
|
|
assert " PORT:" not in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_injects_healthchecks_and_lightrag_depends_on(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Generated compose should gate LightRAG on all managed dependencies becoming healthy."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.yml",
|
|
["services:", " lightrag:", " image: example/lightrag:test"],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
add_docker_service postgres
|
|
add_docker_service neo4j
|
|
add_docker_service mongodb
|
|
add_docker_service redis
|
|
add_docker_service milvus
|
|
add_docker_service qdrant
|
|
add_docker_service memgraph
|
|
add_docker_service vllm-embed
|
|
add_docker_service vllm-rerank
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
lightrag_start = generated_compose.index(" lightrag:\n")
|
|
embed_start = generated_compose.index("\n vllm-embed:\n")
|
|
lightrag_block = generated_compose[lightrag_start:embed_start]
|
|
assert " depends_on:" in generated_compose
|
|
assert " depends_on:" in lightrag_block
|
|
for service_name in (
|
|
"postgres",
|
|
"neo4j",
|
|
"mongodb",
|
|
"redis",
|
|
"milvus",
|
|
"qdrant",
|
|
"memgraph",
|
|
"vllm-embed",
|
|
"vllm-rerank",
|
|
):
|
|
assert (
|
|
f""" {service_name}:
|
|
condition: service_healthy"""
|
|
in lightrag_block
|
|
)
|
|
assert generated_compose.count(" healthcheck:") == 10
|
|
assert " milvus-etcd:" in generated_compose
|
|
assert " milvus-minio:" in generated_compose
|
|
assert (
|
|
""" milvus-etcd:
|
|
condition: service_healthy"""
|
|
in generated_compose
|
|
)
|
|
assert (
|
|
""" milvus-minio:
|
|
condition: service_healthy"""
|
|
in generated_compose
|
|
)
|
|
|
|
|
|
def test_generate_docker_compose_preserves_user_depends_on_and_removes_stale_managed_entries(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Compose regeneration should preserve user dependencies while refreshing wizard-managed ones."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" depends_on:",
|
|
" sidecar:",
|
|
" condition: service_started",
|
|
" postgres:",
|
|
" condition: service_started",
|
|
" vllm-embed:",
|
|
" condition: service_healthy",
|
|
" sidecar:",
|
|
" image: busybox",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
add_docker_service postgres
|
|
add_docker_service redis
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert (
|
|
""" sidecar:
|
|
condition: service_started"""
|
|
in generated_compose
|
|
)
|
|
assert (
|
|
""" postgres:
|
|
condition: service_healthy"""
|
|
in generated_compose
|
|
)
|
|
assert (
|
|
""" redis:
|
|
condition: service_healthy"""
|
|
in generated_compose
|
|
)
|
|
assert (
|
|
""" vllm-embed:
|
|
condition: service_healthy"""
|
|
not in generated_compose
|
|
)
|
|
|
|
|
|
def test_generate_docker_compose_repairs_misplaced_lightrag_depends_on_from_existing_output(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Regeneration should move stale lightrag depends_on content back onto the lightrag service."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" environment:",
|
|
" vllm-rerank:",
|
|
" image: example/vllm:test",
|
|
" restart: unless-stopped",
|
|
" depends_on:",
|
|
" my-service:",
|
|
" condition: service_healthy",
|
|
"volumes:",
|
|
" vllm_rerank_cache:",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
add_docker_service vllm-rerank
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
lightrag_start = generated_compose.index(" lightrag:\n")
|
|
rerank_start = generated_compose.index("\n vllm-rerank:\n")
|
|
lightrag_block = generated_compose[lightrag_start:rerank_start]
|
|
rerank_block = generated_compose[rerank_start:]
|
|
assert " depends_on:" in lightrag_block
|
|
assert (
|
|
""" my-service:
|
|
condition: service_healthy"""
|
|
in lightrag_block
|
|
)
|
|
assert (
|
|
""" vllm-rerank:
|
|
condition: service_healthy"""
|
|
in lightrag_block
|
|
)
|
|
assert " depends_on:" not in rerank_block
|
|
assert generated_compose.count("\n vllm-rerank:\n") == 1
|
|
|
|
|
|
def test_generate_docker_compose_normalizes_lightrag_restart_policy_from_existing_output(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Regeneration should replace legacy lightrag restart with deploy.restart_policy."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" restart: unless-stopped",
|
|
" extra_hosts:",
|
|
' - "host.docker.internal:host-gateway"',
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
lightrag_start = generated_compose.index(" lightrag:\n")
|
|
lightrag_block = generated_compose[lightrag_start:]
|
|
assert " restart: unless-stopped" not in lightrag_block
|
|
assert " deploy:\n" in lightrag_block
|
|
assert " restart_policy:\n" in lightrag_block
|
|
assert " condition: on-failure\n" in lightrag_block
|
|
assert " max_attempts: 10\n" in lightrag_block
|
|
|
|
|
|
def test_generate_docker_compose_normalizes_lightrag_restart_policy_without_blank_line_before_deploy(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Regeneration should move the separator blank line after deploy, not before it."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" restart: unless-stopped",
|
|
"",
|
|
" sidecar:",
|
|
" image: busybox",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert (
|
|
""" image: example/lightrag:test
|
|
|
|
deploy:
|
|
"""
|
|
not in generated_compose
|
|
)
|
|
assert (
|
|
""" image: example/lightrag:test
|
|
deploy:
|
|
"""
|
|
in generated_compose
|
|
)
|
|
assert " max_attempts: 10\n\n volumes:\n" in generated_compose
|
|
assert (
|
|
" - ./data/prompts:/app/data/prompts\n\n sidecar:\n" in generated_compose
|
|
)
|
|
|
|
|
|
def test_generate_docker_compose_preserves_non_managed_named_volumes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Retained services should keep their referenced top-level named volumes."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" volumes:",
|
|
" - my_cache:/app/cache",
|
|
" sidecar:",
|
|
" image: busybox",
|
|
' command: ["sleep", "infinity"]',
|
|
" volumes:",
|
|
" - sidecar_data:/data",
|
|
" postgres:",
|
|
" image: old/postgres:image",
|
|
" volumes:",
|
|
" - postgres_data:/var/lib/postgresql/data",
|
|
"volumes:",
|
|
" my_cache:",
|
|
" driver: local",
|
|
" sidecar_data:",
|
|
" driver: local",
|
|
" postgres_data:",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert " sidecar:" in result
|
|
assert "my_cache:/app/cache" in result
|
|
assert "sidecar_data:/data" in result
|
|
assert " my_cache:" in result
|
|
assert " driver: local" in result
|
|
assert " sidecar_data:" in result
|
|
assert "postgres_data:" not in result
|
|
|
|
|
|
def test_generate_docker_compose_inserts_managed_services_before_top_level_sections(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Managed services should stay inside services: even when custom top-level sections exist."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" volumes:",
|
|
" - ./.env:/app/.env",
|
|
" worker:",
|
|
" image: example/worker:test",
|
|
" networks:",
|
|
" - appnet",
|
|
"networks:",
|
|
" appnet:",
|
|
" driver: bridge",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
ENV_VALUES[POSTGRES_USER]="lightrag"
|
|
ENV_VALUES[POSTGRES_PASSWORD]="secret"
|
|
ENV_VALUES[POSTGRES_DATABASE]="lightrag"
|
|
add_docker_service "postgres"
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert " postgres:" in result
|
|
assert "\n\nnetworks:\n" in result
|
|
assert result.index("\n postgres:") < result.index("\nnetworks:\n")
|
|
assert " appnet:" in result
|
|
|
|
|
|
def test_generate_docker_compose_cleans_marker_and_blank_lines_when_only_lightrag_remains(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Regeneration should not leave a managed-services marker or stacked blank lines behind."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" depends_on:",
|
|
" vllm-embed:",
|
|
" condition: service_healthy",
|
|
" vllm-rerank:",
|
|
" condition: service_healthy",
|
|
"",
|
|
" vllm-embed:",
|
|
" image: example/vllm:embed",
|
|
"",
|
|
" vllm-rerank:",
|
|
" image: example/vllm:rerank",
|
|
"",
|
|
"",
|
|
"",
|
|
"# __WIZARD_MANAGED_SERVICES__",
|
|
"networks:",
|
|
" appnet:",
|
|
" driver: bridge",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert " vllm-embed:" not in result
|
|
assert " vllm-rerank:" not in result
|
|
assert "__WIZARD_MANAGED_SERVICES__" not in result
|
|
assert "depends_on:" not in result
|
|
assert " max_attempts: 10\n\nnetworks:\n" in result
|
|
|
|
|
|
def test_generate_docker_compose_keeps_blank_line_between_managed_service_and_top_level_sections(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Managed service blocks should stay visually separated from following top-level sections."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
"networks:",
|
|
" web_network:",
|
|
" driver: bridge",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
add_docker_service "vllm-embed"
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert " vllm-embed:" in result
|
|
assert (
|
|
""" max_attempts: 10
|
|
depends_on:
|
|
"""
|
|
in result
|
|
)
|
|
assert " restart: unless-stopped\n\nnetworks:\n" in result
|
|
|
|
|
|
def test_generate_docker_compose_keeps_single_blank_line_before_generated_volumes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Generated top-level volumes should be separated from prior sections by one blank line."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
"networks:",
|
|
" web_network:",
|
|
" driver: bridge",
|
|
"",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
|
|
add_docker_service "vllm-embed"
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert "\n\nvolumes:\n" in result
|
|
assert "\n\n\nvolumes:\n" not in result
|
|
|
|
|
|
def test_generate_env_file_comments_out_later_duplicate_active_keys(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Commented example keys should not be overridden by later active defaults."""
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[EMBEDDING_BINDING]="ollama"
|
|
ENV_VALUES[EMBEDDING_MODEL]="bge-m3:latest"
|
|
ENV_VALUES[EMBEDDING_DIM]="1024"
|
|
ENV_VALUES[EMBEDDING_BINDING_HOST]="http://localhost:11434"
|
|
|
|
generate_env_file "{REPO_ROOT}/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_env = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
active_embedding_lines = [
|
|
line for line in generated_env if line.startswith("EMBEDDING_BINDING=")
|
|
]
|
|
active_model_lines = [
|
|
line for line in generated_env if line.startswith("EMBEDDING_MODEL=")
|
|
]
|
|
active_host_lines = [
|
|
line for line in generated_env if line.startswith("EMBEDDING_BINDING_HOST=")
|
|
]
|
|
assert active_embedding_lines == ["EMBEDDING_BINDING=ollama"]
|
|
assert active_model_lines == ["EMBEDDING_MODEL=bge-m3:latest"]
|
|
assert active_host_lines == ["EMBEDDING_BINDING_HOST=http://localhost:11434"]
|
|
assert "# EMBEDDING_BINDING=openai" in generated_env
|
|
|
|
|
|
def test_generate_env_file_preserves_custom_variables_not_declared_in_template(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Reruns should keep custom `.env` variables that are not declared in env.example."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
"# Custom integration settings",
|
|
"EXTRA_API_BASE='https://example.com/api'",
|
|
"# EXTRA_API_TOKEN=secret",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
|
|
assert "HOST=0.0.0.0" in generated_env
|
|
assert "PORT=9621" in generated_env
|
|
assert PRESERVED_HEADER in generated_env
|
|
assert "# Custom integration settings" not in generated_env
|
|
assert "EXTRA_API_BASE='https://example.com/api'" in generated_env
|
|
assert "# EXTRA_API_TOKEN=secret" in generated_env
|
|
|
|
|
|
def test_generate_env_file_keeps_preserved_section_idempotent_across_reruns(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Repeated reruns should keep a single preserved marker and its leading blank line."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
PRESERVED_HEADER,
|
|
"",
|
|
"# Custom integration settings",
|
|
"EXTRA_API_BASE='https://example.com/api'",
|
|
"# EXTRA_API_TOKEN=secret",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker = PRESERVED_HEADER
|
|
notice = PRESERVED_NOTICE
|
|
marker_indexes = [idx for idx, line in enumerate(generated_lines) if line == marker]
|
|
assert marker_indexes == [3]
|
|
assert generated_lines[2] == ""
|
|
assert generated_lines[4] == notice
|
|
assert generated_lines[5] == ""
|
|
assert generated_lines[6] == "# Custom integration settings"
|
|
assert generated_lines[7] == "EXTRA_API_BASE='https://example.com/api'"
|
|
assert generated_lines[8] == "# EXTRA_API_TOKEN=secret"
|
|
|
|
|
|
def test_generate_env_file_preserves_multi_line_comments_inside_preserved_section(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Only comments already inside the preserved section should survive reruns."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
"# External note that should not migrate",
|
|
PRESERVED_HEADER,
|
|
"",
|
|
"# Group A",
|
|
"# Shared settings",
|
|
"EXTRA_API_BASE='https://example.com/api'",
|
|
"",
|
|
"# Group B",
|
|
"EXTRA_API_TOKEN=secret",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker = PRESERVED_HEADER
|
|
notice = PRESERVED_NOTICE
|
|
marker_index = generated_lines.index(marker)
|
|
assert generated_lines.count(marker) == 1
|
|
assert generated_lines.count(notice) == 1
|
|
assert "# External note that should not migrate" not in generated_lines
|
|
assert generated_lines[marker_index + 1] == notice
|
|
assert generated_lines[marker_index + 2] == ""
|
|
assert generated_lines[marker_index + 3] == "# Group A"
|
|
assert generated_lines[marker_index + 4] == "# Shared settings"
|
|
assert (
|
|
generated_lines[marker_index + 5] == "EXTRA_API_BASE='https://example.com/api'"
|
|
)
|
|
assert generated_lines[marker_index + 6] == ""
|
|
assert generated_lines[marker_index + 7] == "# Group B"
|
|
assert generated_lines[marker_index + 8] == "EXTRA_API_TOKEN=secret"
|
|
|
|
|
|
def test_generate_env_file_preserves_trailing_comments_at_end_of_preserved_section(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Free-form comments after the last preserved variable should survive reruns."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"",
|
|
"EXTRA_API_BASE='https://example.com/api'",
|
|
"# Free-form note",
|
|
"# This should stay at EOF",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert generated_lines[-3] == "EXTRA_API_BASE='https://example.com/api'"
|
|
assert generated_lines[-2] == "# Free-form note"
|
|
assert generated_lines[-1] == "# This should stay at EOF"
|
|
|
|
|
|
def test_generate_env_file_appends_new_external_entries_after_existing_preserved_block(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""New template-external entries should be appended after the existing preserved payload."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"EXTRA_EARLY=alpha",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"",
|
|
"# Existing note",
|
|
"EXTRA_EXISTING=omega",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker_index = generated_lines.index(PRESERVED_HEADER)
|
|
assert generated_lines[marker_index + 1] == PRESERVED_NOTICE
|
|
assert generated_lines[marker_index + 2] == ""
|
|
assert generated_lines[marker_index + 3] == "# Existing note"
|
|
assert generated_lines[marker_index + 4] == "EXTRA_EXISTING=omega"
|
|
assert generated_lines[marker_index + 5] == "EXTRA_EARLY=alpha"
|
|
|
|
|
|
def test_generate_env_file_appends_multiple_new_external_entries_in_discovery_order(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Multiple new external entries should append after preserved payload in source order."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"EXTRA_FIRST=one",
|
|
"# Outside comment should not migrate",
|
|
"EXTRA_SECOND=two",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"",
|
|
"EXTRA_EXISTING=existing",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker_index = generated_lines.index(PRESERVED_HEADER)
|
|
assert "# Outside comment should not migrate" not in generated_lines
|
|
assert generated_lines[marker_index + 3] == "EXTRA_EXISTING=existing"
|
|
assert generated_lines[marker_index + 4] == "EXTRA_FIRST=one"
|
|
assert generated_lines[marker_index + 5] == "EXTRA_SECOND=two"
|
|
|
|
|
|
def test_generate_env_file_keeps_commented_template_keys_inside_preserved_section(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Commented env vars already placed in preserved should survive even if the template declares them."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
["HOST=0.0.0.0", "# PORT=9621", "# ENTITY_EXTRACTION_USE_JSON=true"],
|
|
)
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"",
|
|
"# ENTITY_EXTRACTION_USE_JSON=true",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker_index = generated_lines.index(PRESERVED_HEADER)
|
|
assert generated_lines.count("# ENTITY_EXTRACTION_USE_JSON=true") == 2
|
|
assert generated_lines[marker_index + 3] == "# ENTITY_EXTRACTION_USE_JSON=true"
|
|
|
|
|
|
def test_generate_env_file_recognizes_lowercase_extra_variables(tmp_path: Path) -> None:
|
|
"""Lowercase template-external variables should be preserved like uppercase ones."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(tmp_path / ".env", ["HOST=127.0.0.1", "workspace_name=demo"])
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert PRESERVED_HEADER in generated_lines
|
|
assert "workspace_name=demo" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_recognizes_lowercase_commented_extra_variables(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Lowercase commented env vars should create and survive in the preserved section."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(tmp_path / ".env", ["HOST=127.0.0.1", "# workspace_name=demo"])
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert PRESERVED_HEADER in generated_lines
|
|
assert "# workspace_name=demo" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_uses_template_preserved_block_when_env_missing(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Missing `.env` should still produce the preserved block from env.example."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert PRESERVED_HEADER in generated_lines
|
|
assert PRESERVED_NOTICE in generated_lines
|
|
assert generated_lines.count(PRESERVED_HEADER) == 1
|
|
assert generated_lines.count(PRESERVED_NOTICE) == 1
|
|
assert "### Template preserved comment" in generated_lines
|
|
assert "# template_example=true" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_keeps_template_separator_adjacent_to_preserved_header(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Injected template preserved blocks should not add a blank line after the copied separator."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
"##########################################################################",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
header_index = generated_lines.index(PRESERVED_HEADER)
|
|
assert (
|
|
generated_lines[header_index - 1]
|
|
== "##########################################################################"
|
|
)
|
|
|
|
|
|
def test_generate_env_file_does_not_inject_template_payload_when_old_preserved_exists(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Existing preserved blocks should stay authoritative over template preserved payload."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
],
|
|
)
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
PRESERVED_HEADER,
|
|
"",
|
|
"# Existing preserved comment",
|
|
"EXTRA_OLD=1",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert PRESERVED_HEADER in generated_lines
|
|
assert PRESERVED_NOTICE in generated_lines
|
|
assert "### Template preserved comment" not in generated_lines
|
|
assert "# template_example=true" not in generated_lines
|
|
assert "# Existing preserved comment" in generated_lines
|
|
assert "EXTRA_OLD=1" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_keeps_old_preserved_lines_even_when_they_match_template(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Old preserved content should not be removed just because it matches env.example."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
],
|
|
)
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
"",
|
|
PRESERVED_HEADER,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
"EXTRA_OLD=1",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert PRESERVED_HEADER in generated_lines
|
|
assert PRESERVED_NOTICE in generated_lines
|
|
assert "### Template preserved comment" in generated_lines
|
|
assert "# template_example=true" in generated_lines
|
|
assert "EXTRA_OLD=1" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_preserves_comments_before_active_template_keys_in_preserved(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Comments in preserved should survive even when followed by active template-managed keys."""
|
|
write_text_lines(tmp_path / "env.example", ["HOST=0.0.0.0", "# PORT=9621"])
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"HOST=127.0.0.1",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"",
|
|
"# Preserved note before active template key",
|
|
"# Another note",
|
|
"PORT=9999",
|
|
"EXTRA_AFTER=1",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
ENV_VALUES[PORT]="9621"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
marker_index = generated_lines.index(PRESERVED_HEADER)
|
|
assert (
|
|
generated_lines[marker_index + 3]
|
|
== "# Preserved note before active template key"
|
|
)
|
|
assert generated_lines[marker_index + 4] == "# Another note"
|
|
assert "PORT=9999" not in generated_lines[marker_index + 1 :]
|
|
assert "EXTRA_AFTER=1" in generated_lines
|
|
|
|
|
|
def test_generate_env_file_appends_extra_variables_after_template_preserved_block(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Extras from old `.env` should append after the template preserved block when none existed before."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
],
|
|
)
|
|
write_text_lines(tmp_path / ".env", ["HOST=127.0.0.1", "EXTRA_NEW=1"])
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert generated_lines[-3] == "### Template preserved comment"
|
|
assert generated_lines[-2] == "# template_example=true"
|
|
assert generated_lines[-1] == "EXTRA_NEW=1"
|
|
|
|
|
|
def test_generate_env_file_appends_commented_env_vars_after_template_preserved_block(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Commented env vars from old `.env` should append after the template preserved block when none existed before."""
|
|
write_text_lines(
|
|
tmp_path / "env.example",
|
|
[
|
|
"HOST=0.0.0.0",
|
|
PRESERVED_HEADER,
|
|
PRESERVED_NOTICE,
|
|
"### Template preserved comment",
|
|
"# template_example=true",
|
|
],
|
|
)
|
|
write_text_lines(tmp_path / ".env", ["HOST=127.0.0.1", "# EXTRA_COMMENTED=1"])
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
load_env_file "$REPO_ROOT/.env"
|
|
ENV_VALUES[HOST]="0.0.0.0"
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert generated_lines[-3] == "### Template preserved comment"
|
|
assert generated_lines[-2] == "# template_example=true"
|
|
assert generated_lines[-1] == "# EXTRA_COMMENTED=1"
|
|
|
|
|
|
def test_generate_env_file_round_trips_dollar_signs_in_single_quoted_values(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Quoted values containing `$` should survive generate/load cycles unchanged."""
|
|
env_example = tmp_path / "env.example"
|
|
env_example.write_text(
|
|
"\n".join(
|
|
[
|
|
"TOKEN_SECRET=placeholder",
|
|
"LIGHTRAG_API_KEY=placeholder",
|
|
"WEBUI_DESCRIPTION=placeholder",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
output = run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[TOKEN_SECRET]='abc$HOME'
|
|
ENV_VALUES[LIGHTRAG_API_KEY]='plain$token'
|
|
ENV_VALUES[WEBUI_DESCRIPTION]='value with "$PATH" and $HOME'
|
|
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
reset_state
|
|
load_env_file "$REPO_ROOT/.env"
|
|
|
|
printf 'TOKEN_SECRET=%s\\n' "${{ENV_VALUES[TOKEN_SECRET]}}"
|
|
printf 'LIGHTRAG_API_KEY=%s\\n' "${{ENV_VALUES[LIGHTRAG_API_KEY]}}"
|
|
printf 'WEBUI_DESCRIPTION=%s\\n' "${{ENV_VALUES[WEBUI_DESCRIPTION]}}\"
|
|
""")
|
|
values = parse_lines(output)
|
|
generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
|
|
assert "TOKEN_SECRET='abc$HOME'" in generated_env
|
|
assert "LIGHTRAG_API_KEY='plain$token'" in generated_env
|
|
assert "WEBUI_DESCRIPTION='value with \"$PATH\" and $HOME'" in generated_env
|
|
assert values["TOKEN_SECRET"] == "abc$HOME"
|
|
assert values["LIGHTRAG_API_KEY"] == "plain$token"
|
|
assert values["WEBUI_DESCRIPTION"] == 'value with "$PATH" and $HOME'
|
|
|
|
|
|
def test_generate_env_file_avoids_double_quotes_for_compose_sensitive_strings(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Setup output should avoid double quotes for affected string variables."""
|
|
env_example = tmp_path / "env.example"
|
|
env_example.write_text(
|
|
"\n".join(
|
|
[
|
|
"WEBUI_TITLE='My Graph KB'",
|
|
"WEBUI_DESCRIPTION='Simple and Fast Graph Based RAG System'",
|
|
"# AUTH_ACCOUNTS='admin:admin123,user1:{bcrypt}$2b$12$hash'",
|
|
"# LANGFUSE_SECRET_KEY=''",
|
|
"# LANGFUSE_PUBLIC_KEY=''",
|
|
"# LANGFUSE_HOST='https://cloud.langfuse.com'",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[WEBUI_TITLE]='My Graph KB'
|
|
ENV_VALUES[WEBUI_DESCRIPTION]='Simple and Fast Graph Based RAG System'
|
|
ENV_VALUES[AUTH_ACCOUNTS]='admin:admin123,user1:pa$$word'
|
|
ENV_VALUES[LANGFUSE_SECRET_KEY]='sk-lf-secret'
|
|
ENV_VALUES[LANGFUSE_PUBLIC_KEY]='pk-lf-public'
|
|
ENV_VALUES[LANGFUSE_HOST]='https://langfuse.example'
|
|
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
|
|
""")
|
|
generated_lines = (tmp_path / ".env").read_text(encoding="utf-8").splitlines()
|
|
assert "WEBUI_TITLE='My Graph KB'" in generated_lines
|
|
assert (
|
|
"WEBUI_DESCRIPTION='Simple and Fast Graph Based RAG System'" in generated_lines
|
|
)
|
|
assert "AUTH_ACCOUNTS='admin:admin123,user1:pa$$word'" in generated_lines
|
|
assert "LANGFUSE_SECRET_KEY=sk-lf-secret" in generated_lines
|
|
assert "LANGFUSE_PUBLIC_KEY=pk-lf-public" in generated_lines
|
|
assert "LANGFUSE_HOST=https://langfuse.example" in generated_lines
|
|
assert not any(
|
|
line.startswith('WEBUI_TITLE="')
|
|
or line.startswith('WEBUI_DESCRIPTION="')
|
|
or line.startswith('AUTH_ACCOUNTS="')
|
|
or line.startswith('LANGFUSE_SECRET_KEY="')
|
|
or line.startswith('LANGFUSE_PUBLIC_KEY="')
|
|
or line.startswith('LANGFUSE_HOST="')
|
|
for line in generated_lines
|
|
)
|
|
|
|
|
|
def test_generate_docker_compose_escapes_dollar_signs_in_overrides_and_service_secrets(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Compose generation should keep `$` literals in runtime overrides and bundled secrets."""
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" env_file:",
|
|
" - .env",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[MONGO_URI]='mongodb://user:p$HOME@localhost:27017/'
|
|
ENV_VALUES[POSTGRES_USER]='user$ID'
|
|
ENV_VALUES[POSTGRES_PASSWORD]='pass$HOME'
|
|
ENV_VALUES[POSTGRES_DATABASE]='db$NAME'
|
|
ENV_VALUES[NEO4J_PASSWORD]='neo$PASS'
|
|
ENV_VALUES[NEO4J_DATABASE]='graph$DB'
|
|
ENV_VALUES[MINIO_ACCESS_KEY_ID]='minio$USER'
|
|
ENV_VALUES[MINIO_SECRET_ACCESS_KEY]='minio$SECRET'
|
|
|
|
prepare_compose_runtime_overrides
|
|
add_docker_service postgres
|
|
add_docker_service neo4j
|
|
add_docker_service milvus
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert (
|
|
'MONGO_URI: "mongodb://user:p$$HOME@host.docker.internal:27017/"'
|
|
in generated_compose
|
|
)
|
|
assert 'POSTGRES_USER: "user$$ID"' in generated_compose
|
|
assert 'POSTGRES_PASSWORD: "pass$$HOME"' in generated_compose
|
|
assert 'POSTGRES_DB: "db$$NAME"' in generated_compose
|
|
assert (
|
|
"NEO4J_AUTH: ${NEO4J_USERNAME:?missing}/${NEO4J_PASSWORD:?missing}"
|
|
in generated_compose
|
|
)
|
|
assert 'NEO4J_dbms_default__database: "graph$$DB"' in generated_compose
|
|
assert 'MINIO_ACCESS_KEY_ID: "${MINIO_ACCESS_KEY_ID:?missing}"' in generated_compose
|
|
assert (
|
|
'MINIO_SECRET_ACCESS_KEY: "${MINIO_SECRET_ACCESS_KEY:?missing}"'
|
|
in generated_compose
|
|
)
|
|
assert 'MINIO_ROOT_USER: "${MINIO_ACCESS_KEY_ID:?missing}"' in generated_compose
|
|
assert (
|
|
'MINIO_ROOT_PASSWORD: "${MINIO_SECRET_ACCESS_KEY:?missing}"'
|
|
in generated_compose
|
|
)
|
|
assert "milvus-etcd" in generated_compose
|
|
assert "milvus-minio" in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_uses_template_images_even_with_old_env_overrides(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Managed services should be regenerated from templates instead of legacy image overrides."""
|
|
write_text_lines(
|
|
tmp_path / ".env",
|
|
[
|
|
"POSTGRES_IMAGE=registry.example.com/postgres-for-rag:patched",
|
|
"VLLM_EMBED_IMAGE_TAG=patched",
|
|
],
|
|
)
|
|
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}"
|
|
reset_state
|
|
load_existing_env_if_present
|
|
add_docker_service postgres
|
|
add_docker_service vllm-embed
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
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: vllm/vllm-openai-cpu:latest" in result
|
|
assert "registry.example.com/postgres-for-rag:patched" not in result
|
|
assert "vllm/vllm-openai-cpu:patched" not in result
|
|
|
|
|
|
def test_generate_docker_compose_preserves_long_form_named_sidecar_volumes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Managed-service regeneration must not misparse preserved long-form named volumes."""
|
|
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", "EMBEDDING_BINDING=openai"]
|
|
)
|
|
write_text_lines(
|
|
tmp_path / "docker-compose.final.yml",
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" sidecar:",
|
|
" image: busybox",
|
|
' command: ["sleep", "infinity"]',
|
|
" volumes:",
|
|
" - source: sidecar_data",
|
|
" target: /data",
|
|
" type: volume",
|
|
"volumes:",
|
|
" sidecar_data:",
|
|
],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
load_existing_env_if_present
|
|
add_docker_service postgres
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert " sidecar_data:" in result
|
|
assert "\n source:\n" not in result
|
|
|
|
|
|
def test_generate_docker_compose_includes_all_atlas_local_mongodb_volumes(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""MongoDB Atlas Local should emit data, config, and mongot named volumes."""
|
|
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}"
|
|
reset_state
|
|
add_docker_service mongodb
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
|
|
assert "hostname: mongodb" in result
|
|
assert "image: mongodb/mongodb-atlas-local:" in result
|
|
assert "mongo_data:/data/db" in result
|
|
assert "mongo_config_data:/data/configdb" in result
|
|
assert "mongo_mongot_data:/data/mongot" in result
|
|
assert "healthcheck:" not in result
|
|
assert (
|
|
"\nvolumes:\n mongo_data:\n mongo_config_data:\n mongo_mongot_data:\n"
|
|
in result
|
|
)
|
|
|
|
|
|
def test_generate_docker_compose_injects_server_host_and_port_overrides(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Generated compose should preserve variable-based host publishing and fix container bind values."""
|
|
compose_file = tmp_path / "docker-compose.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" env_file:",
|
|
" - .env",
|
|
" ports:",
|
|
' - "${PORT:-9621}:9621"',
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[HOST]="localhost"
|
|
ENV_VALUES[PORT]="8080"
|
|
|
|
prepare_compose_runtime_overrides
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert 'HOST: "0.0.0.0"' in generated_compose
|
|
assert 'PORT: "9621"' in generated_compose
|
|
assert ' - "${HOST:-0.0.0.0}:${PORT:-9621}:9621"' in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_injects_env_overrides_into_lightrag_not_after_managed_services(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Env overrides must appear inside the lightrag environment block, not after managed services.
|
|
|
|
When the base compose has a top-level volumes: section, the strip pass inserts a
|
|
__WIZARD_MANAGED_SERVICES__ marker at the point where volumes: begins. Before the
|
|
fix the environment injector would miss that marker (column-0 comment) as an
|
|
end-of-environment boundary and append overrides after it — which placed them outside
|
|
the lightrag service once postgres/neo4j were merged in.
|
|
"""
|
|
compose_file = tmp_path / "docker-compose.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" environment:",
|
|
" EXISTING_KEY: existing_value",
|
|
" volumes:",
|
|
" - ./.env:/app/.env",
|
|
"volumes:",
|
|
" some_volume:",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[POSTGRES_USER]="lightrag"
|
|
ENV_VALUES[POSTGRES_PASSWORD]="secret"
|
|
ENV_VALUES[POSTGRES_DATABASE]="lightrag"
|
|
add_docker_service "postgres"
|
|
set_compose_override "LLM_BINDING_HOST" "http://host.docker.internal:11434"
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
result = (tmp_path / "docker-compose.generated.yml").read_text(encoding="utf-8")
|
|
lightrag_pos = result.index(" lightrag:")
|
|
postgres_pos = result.index(" postgres:")
|
|
override_pos = result.index('LLM_BINDING_HOST: "http://host.docker.internal:11434"')
|
|
assert lightrag_pos < override_pos < postgres_pos
|
|
|
|
|
|
def test_generate_docker_compose_vllm_gpu_honors_documented_gpu_selector(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""GPU vLLM compose should honor the documented CUDA selector variables."""
|
|
env_example = tmp_path / "env.example"
|
|
env_example.write_text(
|
|
"\n".join(
|
|
[
|
|
"# VLLM_RERANK_DEVICE=cuda",
|
|
"# CUDA_VISIBLE_DEVICES=-1",
|
|
"# NVIDIA_VISIBLE_DEVICES=all",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
compose_file = tmp_path / "docker-compose.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" env_file:",
|
|
" - .env",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[VLLM_RERANK_DEVICE]="cuda"
|
|
ENV_VALUES[CUDA_VISIBLE_DEVICES]="0"
|
|
add_docker_service "vllm-rerank"
|
|
|
|
generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env"
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
|
|
""")
|
|
generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "CUDA_VISIBLE_DEVICES=0" in generated_env
|
|
assert "NVIDIA_VISIBLE_DEVICES: ${NVIDIA_VISIBLE_DEVICES:-all}" in generated_compose
|
|
assert (
|
|
""" vllm-rerank:
|
|
condition: service_healthy"""
|
|
in generated_compose
|
|
)
|
|
assert " healthcheck:" in generated_compose
|
|
assert "VLLM_RERANK_PORT:-8000" in generated_compose
|
|
assert 'grep -q ":$${PORT_HEX} "' in generated_compose
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("device", "expected_image"),
|
|
[
|
|
("cpu", "image: milvusdb/milvus:v2.6.11"),
|
|
("cuda", "image: milvusdb/milvus:v2.6.11-gpu"),
|
|
],
|
|
)
|
|
def test_generate_docker_compose_selects_milvus_template_from_device(
|
|
tmp_path: Path, device: str, expected_image: str
|
|
) -> None:
|
|
"""Milvus compose generation should switch templates based on MILVUS_DEVICE."""
|
|
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",
|
|
["services:", " lightrag:", " image: example/lightrag:test"],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
ENV_VALUES[MILVUS_DEVICE]="{device}"
|
|
add_docker_service milvus
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert expected_image in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_pairs_dashboards_with_opensearch(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
"""Generating the opensearch service block must always emit a paired dashboards service so it remains under wizard management."""
|
|
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",
|
|
["services:", " lightrag:", " image: example/lightrag:test"],
|
|
)
|
|
run_bash(f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
add_docker_service opensearch
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml\"
|
|
""")
|
|
generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert " opensearch:" in generated_compose
|
|
assert " dashboards:" in generated_compose
|
|
assert "opensearchproject/opensearch-dashboards:3" in generated_compose
|
|
assert "OPENSEARCH_HOSTS: '[\"https://opensearch:9200\"]'" in generated_compose
|
|
assert "condition: service_healthy" in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_omits_config_ini_mount_from_base_template(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
compose_file = tmp_path / "docker-compose.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" volumes:",
|
|
" - ./data/rag_storage:/app/data/rag_storage",
|
|
" - ./data/inputs:/app/data/inputs",
|
|
" - ./.env:/app/.env",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
run_bash(
|
|
f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml"
|
|
"""
|
|
)
|
|
|
|
generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
assert "./config.ini:/app/config.ini" not in generated_compose
|
|
assert "./data/rag_storage:/app/data/rag_storage" in generated_compose
|
|
assert "./data/inputs:/app/data/inputs" in generated_compose
|
|
assert "./.env:/app/.env" in generated_compose
|
|
|
|
|
|
def test_generate_docker_compose_preserves_existing_config_ini_mount(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
compose_file = tmp_path / "docker-compose.final.yml"
|
|
compose_file.write_text(
|
|
"\n".join(
|
|
[
|
|
"services:",
|
|
" lightrag:",
|
|
" image: example/lightrag:test",
|
|
" volumes:",
|
|
" - ./data/rag_storage:/app/data/rag_storage",
|
|
" - ./data/inputs:/app/data/inputs",
|
|
" - ./config.ini:/app/config.ini",
|
|
" - ./.env:/app/.env",
|
|
]
|
|
)
|
|
+ "\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
run_bash(
|
|
f"""
|
|
set -euo pipefail
|
|
source "{REPO_ROOT}/scripts/setup/setup.sh"
|
|
REPO_ROOT="{tmp_path}"
|
|
reset_state
|
|
|
|
generate_docker_compose "$REPO_ROOT/docker-compose.final.yml"
|
|
"""
|
|
)
|
|
|
|
generated_compose = compose_file.read_text(encoding="utf-8")
|
|
|
|
assert "./config.ini:/app/config.ini" in generated_compose
|
|
assert "./data/rag_storage:/app/data/rag_storage" in generated_compose
|
|
assert "./data/inputs:/app/data/inputs" in generated_compose
|
|
assert "./.env:/app/.env" in generated_compose
|