| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860 |
- # 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
|