| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044 |
- # Regression tests for interactive setup wizard.
- # Classification: keep tests here when they cover cross-cutting setup helpers, finalization/migration, backup, or security-check behavior that does not fit collect_/env_/generate_/validate_ buckets.
- from __future__ import annotations
- import subprocess
- from pathlib import Path
- import pytest
- from tests.setup._helpers import (
- REPO_ROOT,
- assert_single_compose_backup,
- parse_lines,
- run_bash,
- run_bash_process,
- run_bash_lines,
- write_text_lines,
- )
- pytestmark = pytest.mark.offline
- def test_prepare_compose_runtime_overrides_keeps_env_unchanged() -> None:
- """Loopback endpoints should be rewritten only for compose overrides."""
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- 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"
- prepare_compose_runtime_overrides
- printf 'ENV_LLM=%s\\n' "${{ENV_VALUES[LLM_BINDING_HOST]}}"
- printf 'ENV_EMBEDDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
- printf 'ENV_RERANK=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}"
- printf 'COMPOSE_LLM=%s\\n' "${{COMPOSE_ENV_OVERRIDES[LLM_BINDING_HOST]}}"
- printf 'COMPOSE_EMBEDDING=%s\\n' "${{COMPOSE_ENV_OVERRIDES[EMBEDDING_BINDING_HOST]}}"
- printf 'COMPOSE_RERANK=%s\\n' "${{COMPOSE_ENV_OVERRIDES[RERANK_BINDING_HOST]}}\"
- """)
- values = parse_lines(output)
- assert values["ENV_LLM"] == "http://localhost:11434"
- assert values["ENV_EMBEDDING"] == "http://127.0.0.1:11434"
- assert values["ENV_RERANK"] == "http://localhost:8000/rerank"
- assert values["COMPOSE_LLM"] == "http://host.docker.internal:11434"
- assert values["COMPOSE_EMBEDDING"] == "http://host.docker.internal:11434"
- assert values["COMPOSE_RERANK"] == "http://host.docker.internal:8000/rerank"
- def test_existing_ssl_env_keeps_compose_mount_overrides(tmp_path: Path) -> None:
- """Compose regeneration should preserve working SSL mounts without implying `.env` is permanently dual-purpose."""
- 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",
- )
- 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")
- env_file = tmp_path / ".env"
- env_file.write_text(
- "\n".join(["SSL=true", f"SSL_CERTFILE={cert_path}", f"SSL_KEYFILE={key_path}"])
- + "\n",
- encoding="utf-8",
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- prepare_compose_env_overrides
- stage_ssl_assets "$SSL_CERT_SOURCE_PATH" "$SSL_KEY_SOURCE_PATH"
- generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
- """)
- generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
- encoding="utf-8"
- )
- assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose
- assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose
- assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose
- assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose
- def test_finalize_base_setup_rewrites_ssl_env_to_preserved_compose_paths(
- tmp_path: Path,
- ) -> None:
- """Compose-target reruns should rewrite broken SSL source paths to preserved staged compose paths."""
- staged_dir = tmp_path / "data" / "certs"
- staged_dir.mkdir(parents=True)
- (staged_dir / "server.pem").write_text("cert", encoding="utf-8")
- (staged_dir / "server.key").write_text("key", encoding="utf-8")
- write_text_lines(
- tmp_path / ".env",
- [
- "SSL=true",
- "SSL_CERTFILE=/missing/original-cert.pem",
- "SSL_KEYFILE=/missing/original-key.pem",
- "LIGHTRAG_KV_STORAGE=JsonKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " volumes:",
- " - ./.env:/app/.env",
- " - ./data/certs/server.pem:/app/data/certs/server.pem:ro",
- " - ./data/certs/server.key:/app/data/certs/server.key:ro",
- " environment:",
- " SSL_CERTFILE: /app/data/certs/server.pem",
- " SSL_KEYFILE: /app/data/certs/server.key",
- ],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- initialize_default_storage_backends
- show_summary() {{ :; }}
- confirm_default_yes() {{
- case "$1" in
- "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
- *) return 0 ;;
- esac
- }}
- confirm_required_yes_no() {{ return 0; }}
- finalize_base_setup
- if validate_env_file; then
- printf 'VALID=yes\\n'
- else
- printf 'VALID=no\\n'
- fi
- """)
- values = parse_lines(output)
- generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
- assert "SSL_CERTFILE=/app/data/certs/server.pem" in generated_env
- assert "SSL_KEYFILE=/app/data/certs/server.key" in generated_env
- assert values["VALID"] == "yes"
- def test_removing_ssl_strips_wizard_bind_mounts_from_compose(tmp_path: Path) -> None:
- """Re-running setup without SSL must remove only wizard-managed SSL mounts."""
- compose_file = tmp_path / "docker-compose.final.yml"
- compose_file.write_text(
- "\n".join(
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " volumes:",
- ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"',
- ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"',
- ' - "./data/rag_storage:/app/data/rag_storage"',
- ' - "./data/inputs:/app/data/inputs"',
- ' - "./custom-data:/app/data/custom"',
- " environment:",
- ' SSL_CERTFILE: "/app/data/certs/cert.pem"',
- ' SSL_KEYFILE: "/app/data/certs/key.pem"',
- ]
- )
- + "\n",
- encoding="utf-8",
- )
- (tmp_path / "env.example").write_text(
- (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8"
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- generate_docker_compose "{tmp_path}/docker-compose.final.yml\"
- """)
- result = compose_file.read_text(encoding="utf-8")
- assert "/app/data/certs/cert.pem" not in result
- assert "/app/data/certs/key.pem" not in result
- assert "./data/rag_storage:/app/data/rag_storage" in result
- assert "./data/inputs:/app/data/inputs" in result
- assert "./custom-data:/app/data/custom" in result
- def test_find_generated_compose_file_prefers_final_compose_file(tmp_path: Path) -> None:
- """Compose discovery should prefer docker-compose.final.yml over legacy files."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- ["services:", " lightrag:", " image: final/lightrag"],
- )
- write_text_lines(
- tmp_path / "docker-compose.development.yml",
- ["services:", " lightrag:", " image: dev/lightrag"],
- )
- write_text_lines(
- tmp_path / "docker-compose.production.yml",
- ["services:", " lightrag:", " image: prod/lightrag"],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- printf 'COMPOSE=%s\\n' "$(find_generated_compose_file)\"
- """)
- values = parse_lines(output)
- assert values["COMPOSE"] == str(tmp_path / "docker-compose.final.yml")
- def test_find_generated_compose_file_falls_back_to_order_without_profile(
- tmp_path: Path,
- ) -> None:
- """Without legacy profile metadata, compose migration should use the default order."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- write_text_lines(
- tmp_path / "docker-compose.development.yml",
- ["services:", " lightrag:", " image: dev/lightrag"],
- )
- write_text_lines(
- tmp_path / "docker-compose.production.yml",
- ["services:", " lightrag:", " image: prod/lightrag"],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- printf 'COMPOSE=%s\\n' "$(find_generated_compose_file)\"
- """)
- values = parse_lines(output)
- assert values["COMPOSE"] == str(tmp_path / "docker-compose.development.yml")
- def test_switching_both_providers_off_bedrock_preserves_saved_aws_credentials(
- tmp_path: Path,
- ) -> None:
- """Switching LLM/Embedding away from Bedrock must preserve user-set AWS_* values.
- AWS credentials are process-level SDK settings and may be used by code paths
- outside the active LLM/embedding binding (S3, SecretsManager, etc.), so the
- wizard must not erase them just because the active binding is no longer
- ``bedrock``. Only the explicit ``collect_bedrock_credentials`` ambient branch
- is allowed to clear them.
- """
- write_text_lines(
- tmp_path / ".env",
- [
- "LLM_BINDING=bedrock",
- "LLM_MODEL=anthropic.claude-3-5-sonnet-20241022-v2:0",
- "LLM_BINDING_HOST=https://bedrock.amazonaws.com",
- "EMBEDDING_BINDING=bedrock",
- "EMBEDDING_MODEL=amazon.titan-embed-text-v2:0",
- "EMBEDDING_DIM=1024",
- "EMBEDDING_BINDING_HOST=https://bedrock.amazonaws.com",
- "AWS_ACCESS_KEY_ID=AKIAOLDKEY",
- "AWS_SECRET_ACCESS_KEY=oldsecretvalue",
- "AWS_SESSION_TOKEN=oldsess",
- "AWS_REGION=us-east-1",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- [
- "# AWS_ACCESS_KEY_ID=your_aws_access_key_id",
- "# AWS_SECRET_ACCESS_KEY=your_aws_secret_access_key",
- "# AWS_SESSION_TOKEN=your_optional_aws_session_token",
- "# AWS_REGION=us-east-1",
- "LLM_BINDING=openai",
- "LLM_MODEL=gpt-4o",
- "LLM_BINDING_HOST=https://api.openai.com/v1",
- "LLM_BINDING_API_KEY=your_api_key",
- "EMBEDDING_BINDING=openai",
- "EMBEDDING_MODEL=text-embedding-3-large",
- "EMBEDDING_DIM=3072",
- "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
- "EMBEDDING_BINDING_API_KEY=your_api_key",
- ],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- prompt_choice() {{ printf 'openai'; }}
- prompt_with_default() {{ printf '%s' "$2"; }}
- prompt_secret_until_valid_with_default() {{ printf 'fresh-key'; }}
- collect_llm_config
- collect_embedding_config
- generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env.generated"
- printf 'AWS_ACCESS_KEY_ID=%s\\n' "${{ENV_VALUES[AWS_ACCESS_KEY_ID]-}}"
- printf 'AWS_SECRET_ACCESS_KEY=%s\\n' "${{ENV_VALUES[AWS_SECRET_ACCESS_KEY]-}}"
- printf 'AWS_SESSION_TOKEN=%s\\n' "${{ENV_VALUES[AWS_SESSION_TOKEN]-}}"
- printf 'AWS_REGION=%s\\n' "${{ENV_VALUES[AWS_REGION]-}}"
- """)
- values = parse_lines(output)
- generated_lines = (
- (tmp_path / ".env.generated").read_text(encoding="utf-8").splitlines()
- )
- assert values["AWS_ACCESS_KEY_ID"] == "AKIAOLDKEY"
- assert values["AWS_SECRET_ACCESS_KEY"] == "oldsecretvalue"
- assert values["AWS_SESSION_TOKEN"] == "oldsess"
- assert values["AWS_REGION"] == "us-east-1"
- assert "AWS_ACCESS_KEY_ID=AKIAOLDKEY" in generated_lines
- assert "AWS_SECRET_ACCESS_KEY=oldsecretvalue" in generated_lines
- assert "AWS_SESSION_TOKEN=oldsess" in generated_lines
- assert "AWS_REGION=us-east-1" in generated_lines
- def test_load_existing_env_forces_cohere_binding_for_vllm_rerank(
- tmp_path: Path,
- ) -> None:
- """Loading a Docker-managed vLLM rerank config should normalize the binding to cohere."""
- write_text_lines(
- tmp_path / ".env",
- [
- "RERANK_BINDING=jina",
- "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
- "RERANK_BINDING_HOST=http://localhost:8000/rerank",
- ],
- )
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}"
- printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]}}\"
- """)
- assert values["RERANK_BINDING"] == "cohere"
- assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == "vllm"
- @pytest.mark.parametrize(
- ("llm_binding", "embedding_binding", "expected_llm_host", "expected_embed_host"),
- [
- ("bedrock", "bedrock", "DEFAULT_BEDROCK_ENDPOINT", "DEFAULT_BEDROCK_ENDPOINT"),
- ("gemini", "gemini", "DEFAULT_GEMINI_ENDPOINT", "DEFAULT_GEMINI_ENDPOINT"),
- ("bedrock", "openai", "DEFAULT_BEDROCK_ENDPOINT", ""),
- ],
- ids=["bedrock-both", "gemini-both", "bedrock-llm-only"],
- )
- def test_load_existing_env_backfills_sentinel_hosts_for_bedrock_and_gemini(
- tmp_path: Path,
- llm_binding: str,
- embedding_binding: str,
- expected_llm_host: str,
- expected_embed_host: str,
- ) -> None:
- """Flows that skip collect_*_config (--server, --storage) must not let env.example's openai URL leak through for sentinel-based providers."""
- write_text_lines(
- tmp_path / ".env",
- [
- f"LLM_BINDING={llm_binding}",
- f"EMBEDDING_BINDING={embedding_binding}",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env.generated"
- printf 'LOADED_LLM_HOST=%s\\n' "${{ENV_VALUES[LLM_BINDING_HOST]:-}}"
- printf 'LOADED_EMBED_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]:-}}\"
- """)
- assert values["LOADED_LLM_HOST"] == expected_llm_host
- generated_lines = (
- (tmp_path / ".env.generated").read_text(encoding="utf-8").splitlines()
- )
- llm_host_line = next(
- line for line in generated_lines if line.startswith("LLM_BINDING_HOST=")
- )
- assert llm_host_line == f"LLM_BINDING_HOST={expected_llm_host}"
- if expected_embed_host:
- assert values["LOADED_EMBED_HOST"] == expected_embed_host
- embed_host_line = next(
- line
- for line in generated_lines
- if line.startswith("EMBEDDING_BINDING_HOST=")
- )
- assert embed_host_line == f"EMBEDDING_BINDING_HOST={expected_embed_host}"
- def test_finalize_base_setup_uses_compose_native_storage_endpoints_on_rerun(
- tmp_path: Path,
- ) -> None:
- """Preserved managed storage services should inject compose-native endpoints on base reruns."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker",
- "LIGHTRAG_SETUP_MILVUS_DEPLOYMENT=docker",
- "NEO4J_URI=neo4j://localhost:7687",
- "MILVUS_URI=http://localhost:19530",
- "LIGHTRAG_KV_STORAGE=JsonKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " neo4j:",
- " image: neo4j:latest",
- " milvus:",
- " image: milvusdb/milvus:v2.6.11",
- " milvus-etcd:",
- " image: quay.io/coreos/etcd:v3.5.16",
- " milvus-minio:",
- " image: minio/minio:latest",
- "volumes:",
- " neo4j_data:",
- " milvus_data:",
- " milvus-etcd_data:",
- " milvus-minio_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
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- confirm_default_yes() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- finalize_base_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- assert 'NEO4J_URI: "neo4j://neo4j:7687"' in result
- assert 'MILVUS_URI: "http://milvus:19530"' in result
- assert 'NEO4J_URI: "neo4j://host.docker.internal:7687"' not in result
- assert 'MILVUS_URI: "http://host.docker.internal:19530"' not in result
- assert (
- """ milvus:
- condition: service_healthy"""
- in result
- )
- assert (
- """ milvus-etcd:
- condition: service_healthy"""
- not in result
- )
- assert (
- """ milvus-minio:
- condition: service_healthy"""
- not in result
- )
- def test_finalize_base_setup_migrates_mongodb_to_atlas_local_for_mongo_vector_storage(
- tmp_path: Path,
- ) -> None:
- """Base reruns should upgrade docker-managed MongoDB to Atlas Local when Mongo vector storage needs it."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
- "LIGHTRAG_KV_STORAGE=MongoKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
- "MONGO_URI=mongodb://localhost:27017/?directConnection=true",
- "MONGO_DATABASE=LightRAG",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongo:8.2.4",
- " volumes:",
- " - mongo_data:/data/db",
- "volumes:",
- " mongo_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
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- confirm_default_yes() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- finalize_base_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- assert "image: mongodb/mongodb-atlas-local:" in result
- assert "mongo_config_data:/data/configdb" in result
- assert "mongo_mongot_data:/data/mongot" in result
- assert "image: mongo:8.2.4" not in result
- def test_finalize_base_setup_rejects_invalid_preserved_mongo_vector_config(
- tmp_path: Path,
- ) -> None:
- """Base reruns should fail before writing when preserved Mongo vector config is invalid."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
- "LIGHTRAG_KV_STORAGE=MongoKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
- "MONGO_URI=mongodb://mongo.example.com:27017/?directConnection=true",
- "MONGO_DATABASE=LightRAG",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongo:8.2.4",
- " volumes:",
- " - mongo_data:/data/db",
- "volumes:",
- " mongo_data:",
- ],
- )
- result = run_bash_process(
- f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- confirm_default_yes() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- finalize_base_setup
- """,
- cwd=tmp_path,
- )
- assert result.returncode != 0
- assert (
- "MongoVectorDBStorage requires the bundled Atlas Local endpoint"
- in result.stderr
- )
- assert "image: mongo:8.2.4" in (tmp_path / "docker-compose.final.yml").read_text(
- encoding="utf-8"
- )
- def test_finalize_base_setup_drops_stale_storage_services_missing_from_env_markers(
- tmp_path: Path,
- ) -> None:
- """env-base should treat storage Docker state in `.env` as authoritative."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LLM_BINDING=openai",
- "LLM_MODEL=gpt-4o-mini",
- "LLM_BINDING_HOST=https://api.openai.com/v1",
- "LLM_BINDING_API_KEY=sk-existing",
- "EMBEDDING_BINDING=openai",
- "EMBEDDING_MODEL=text-embedding-3-small",
- "EMBEDDING_DIM=1536",
- "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
- "EMBEDDING_BINDING_API_KEY=sk-existing",
- "LIGHTRAG_KV_STORAGE=JsonKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " redis:",
- " image: redis:latest",
- " qdrant:",
- " image: qdrant/qdrant:latest",
- "volumes:",
- " redis_data:",
- " qdrant_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
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- confirm_default_yes() {{ return 1; }}
- confirm_default_no() {{ return 1; }}
- validate_sensitive_env_literals() {{ return 0; }}
- finalize_base_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
- assert " lightrag:" in result
- assert " redis:" not in result
- assert " qdrant:" not in result
- assert "redis_data:" not in result
- assert "qdrant_data:" not in result
- assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
- @pytest.mark.parametrize(
- ("changed_key", "changed_value", "expected_rewrite"),
- [
- ("NEO4J_PASSWORD", "updated-password", "no"),
- ("NEO4J_DATABASE", "updated-database", "yes"),
- ],
- ids=["neo4j-password-does-not-rewrite", "neo4j-database-rewrites"],
- )
- def test_configure_storage_compose_rewrites_only_rewrites_neo4j_on_database_change(
- changed_key: str, changed_value: str, expected_rewrite: str
- ) -> None:
- """Neo4j service rewrites should be driven by database changes, not credentials."""
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- EXISTING_MANAGED_ROOT_SERVICE_SET[neo4j]=1
- DOCKER_SERVICE_SET[neo4j]=1
- ORIGINAL_ENV_VALUES[NEO4J_PASSWORD]="original-password"
- ORIGINAL_ENV_VALUES[NEO4J_DATABASE]="neo4j"
- ENV_VALUES[NEO4J_PASSWORD]="original-password"
- ENV_VALUES[NEO4J_DATABASE]="neo4j"
- ENV_VALUES[{changed_key}]="{changed_value}"
- configure_storage_compose_rewrites
- if [[ -n "${{COMPOSE_REWRITE_SERVICE_SET[neo4j]+set}}" ]]; then
- printf 'REWRITE=yes\\n'
- else
- printf 'REWRITE=no\\n'
- fi
- """)
- values = parse_lines(output)
- assert values["REWRITE"] == expected_rewrite
- @pytest.mark.parametrize(
- ("changed_key", "changed_value", "expected_rewrite"),
- [
- ("POSTGRES_HOST", "db.example.com", "no"),
- ("POSTGRES_PORT", "6543", "no"),
- ("POSTGRES_USER", "updated-user", "yes"),
- ("POSTGRES_PASSWORD", "updated-password", "yes"),
- ("POSTGRES_DATABASE", "updated-database", "yes"),
- ],
- ids=[
- "postgres-host-does-not-rewrite",
- "postgres-port-does-not-rewrite",
- "postgres-user-rewrites",
- "postgres-password-rewrites",
- "postgres-database-rewrites",
- ],
- )
- def test_configure_storage_compose_rewrites_only_rewrites_postgres_for_service_env_changes(
- changed_key: str, changed_value: str, expected_rewrite: str
- ) -> None:
- """Postgres service rewrites should only follow changes emitted into the postgres block."""
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- EXISTING_MANAGED_ROOT_SERVICE_SET[postgres]=1
- DOCKER_SERVICE_SET[postgres]=1
- ORIGINAL_ENV_VALUES[POSTGRES_HOST]="localhost"
- ORIGINAL_ENV_VALUES[POSTGRES_PORT]="5432"
- ORIGINAL_ENV_VALUES[POSTGRES_USER]="rag"
- ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]="rag"
- ORIGINAL_ENV_VALUES[POSTGRES_DATABASE]="lightrag"
- ENV_VALUES[POSTGRES_HOST]="localhost"
- ENV_VALUES[POSTGRES_PORT]="5432"
- ENV_VALUES[POSTGRES_USER]="rag"
- ENV_VALUES[POSTGRES_PASSWORD]="rag"
- ENV_VALUES[POSTGRES_DATABASE]="lightrag"
- ENV_VALUES[{changed_key}]="{changed_value}"
- configure_storage_compose_rewrites
- if [[ -n "${{COMPOSE_REWRITE_SERVICE_SET[postgres]+set}}" ]]; then
- printf 'REWRITE=yes\\n'
- else
- printf 'REWRITE=no\\n'
- fi
- """)
- values = parse_lines(output)
- assert values["REWRITE"] == expected_rewrite
- @pytest.mark.parametrize(
- ("vector_storage", "deployment_marker", "expected_rewrite"),
- [
- ("MongoVectorDBStorage", "docker", "yes"),
- ("NanoVectorDBStorage", "docker", "no"),
- ("MongoVectorDBStorage", "", "no"),
- ],
- ids=[
- "mongo-vector-with-docker-rewrites",
- "non-mongo-vector-does-not-rewrite",
- "mongo-vector-without-docker-does-not-rewrite",
- ],
- )
- def test_configure_mongodb_compose_migration_rewrite_only_runs_for_atlas_local_vector_path(
- tmp_path: Path, vector_storage: str, deployment_marker: str, expected_rewrite: str
- ) -> None:
- """Atlas Local migration should only run for docker-managed MongoDB vector storage."""
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongo:8.2.4",
- " volumes:",
- " - mongo_data:/data/db",
- "volumes:",
- " mongo_data:",
- ],
- )
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="{vector_storage}"
- ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]="{deployment_marker}"
- EXISTING_MANAGED_ROOT_SERVICE_SET[mongodb]=1
- DOCKER_SERVICE_SET[mongodb]=1
- configure_mongodb_compose_migration_rewrite "$REPO_ROOT/docker-compose.final.yml"
- if [[ -n "${{COMPOSE_REWRITE_SERVICE_SET[mongodb]+set}}" ]]; then
- printf 'REWRITE=yes\\n'
- else
- printf 'REWRITE=no\\n'
- fi
- """)
- assert values["REWRITE"] == expected_rewrite
- def test_configure_mongodb_compose_migration_rewrite_repairs_missing_mongot_volume(
- tmp_path: Path,
- ) -> None:
- """Atlas Local compose rewrites should repair stale MongoDB services missing mongot persistence."""
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongodb/mongodb-atlas-local:8",
- " volumes:",
- " - mongo_data:/data/db",
- " - mongo_config_data:/data/configdb",
- "volumes:",
- " mongo_data:",
- " mongo_config_data:",
- ],
- )
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MongoVectorDBStorage"
- ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]="docker"
- EXISTING_MANAGED_ROOT_SERVICE_SET[mongodb]=1
- DOCKER_SERVICE_SET[mongodb]=1
- configure_mongodb_compose_migration_rewrite "$REPO_ROOT/docker-compose.final.yml"
- if [[ -n "${{COMPOSE_REWRITE_SERVICE_SET[mongodb]+set}}" ]]; then
- printf 'REWRITE=yes\\n'
- else
- printf 'REWRITE=no\\n'
- fi
- """)
- assert values["REWRITE"] == "yes"
- def test_switching_to_non_docker_storage_removes_stale_services_from_compose(
- tmp_path: Path,
- ) -> None:
- """env-storage must strip managed storage services while preserving user sidecars."""
- compose_file = tmp_path / "docker-compose.final.yml"
- compose_file.write_text(
- "\n".join(
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " postgres:",
- " image: gzdaniel/postgres-for-rag:pg18-age-pgvector",
- " neo4j:",
- " image: neo4j:5.26.21-community",
- " sidecar:",
- " image: busybox",
- ' command: ["sleep", "infinity"]',
- " volumes:",
- " - sidecar_data:/data",
- "volumes:",
- " postgres_data:",
- " neo4j_data:",
- " sidecar_data:",
- ]
- )
- + "\n",
- encoding="utf-8",
- )
- env_file = tmp_path / ".env"
- env_file.write_text("LLM_BINDING=openai\n", encoding="utf-8")
- (tmp_path / "env.example").write_text(
- (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8"
- )
- (tmp_path / "docker-compose.yml").write_text(
- (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- select_storage_backends() {{
- ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
- ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
- ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
- ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
- }}
- collect_database_config() {{ :; }}
- collect_docker_image_tags() {{ :; }}
- validate_required_variables() {{ return 0; }}
- confirm_default_yes() {{ return 0; }}
- confirm_default_no() {{ return 1; }}
- confirm_required_yes_no() {{ return 0; }}
- env_storage_flow
- """)
- result = compose_file.read_text(encoding="utf-8")
- assert "postgres:" not in result
- assert "neo4j:" not in result
- assert "postgres_data:" not in result
- assert "neo4j_data:" not in result
- assert " lightrag:" in result
- assert " sidecar:" in result
- assert "sidecar_data:" in result
- @pytest.mark.parametrize(
- ("env_key", "env_value", "expected_value"),
- [
- ("POSTGRES_HOST", "127.0.0.1", "host.docker.internal"),
- ("REDIS_URI", "redis://localhost:6379", "redis://host.docker.internal:6379"),
- (
- "MONGO_URI",
- "mongodb://127.0.0.1:27017/",
- "mongodb://host.docker.internal:27017/",
- ),
- (
- "MONGO_URI",
- "mongodb://root:root@localhost:27017/",
- "mongodb://root:root@host.docker.internal:27017/",
- ),
- ("NEO4J_URI", "neo4j://localhost:7687", "neo4j://host.docker.internal:7687"),
- ("MILVUS_URI", "http://localhost:19530", "http://host.docker.internal:19530"),
- ("QDRANT_URL", "http://127.0.0.1:6333", "http://host.docker.internal:6333"),
- ("MEMGRAPH_URI", "bolt://localhost:7687", "bolt://host.docker.internal:7687"),
- ("POSTGRES_HOST", "0.0.0.0", "host.docker.internal"),
- (
- "LLM_BINDING_HOST",
- "http://0.0.0.0:11434",
- "http://host.docker.internal:11434",
- ),
- (
- "RERANK_BINDING_HOST",
- "http://0.0.0.0:8000/rerank",
- "http://host.docker.internal:8000/rerank",
- ),
- ],
- ids=[
- "postgres-loopback-host",
- "redis-loopback-uri",
- "mongo-loopback-uri",
- "mongo-authenticated-loopback-uri",
- "neo4j-loopback-uri",
- "milvus-loopback-uri",
- "qdrant-loopback-uri",
- "memgraph-loopback-uri",
- "postgres-zero-host",
- "llm-zero-host",
- "rerank-zero-host",
- ],
- )
- def test_prepare_compose_runtime_overrides_rewrites_container_endpoints(
- env_key: str, env_value: str, expected_value: str
- ) -> None:
- """Loopback and 0.0.0.0 endpoints should be rewritten for container reachability."""
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- ENV_VALUES[{env_key}]="{env_value}"
- prepare_compose_runtime_overrides
- printf '{env_key}=%s\\n' "${{COMPOSE_ENV_OVERRIDES[{env_key}]}}\"
- """)
- assert values[env_key] == expected_value
- @pytest.mark.parametrize(
- ("host_value", "expected_port_mapping"),
- [
- ("127.0.0.1", "${HOST:-0.0.0.0}:${PORT:-9621}:9621"),
- ("192.168.1.10", "${HOST:-0.0.0.0}:${PORT:-9621}:9621"),
- ],
- ids=["loopback-bind", "lan-bind"],
- )
- def test_prepare_compose_runtime_overrides_normalizes_server_binding(
- host_value: str, expected_port_mapping: str
- ) -> None:
- """Compose runtime should keep variable-based publishing while fixing container bind values."""
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- ENV_VALUES[HOST]="{host_value}"
- ENV_VALUES[PORT]="8080"
- prepare_compose_runtime_overrides
- printf 'HOST=%s\\n' "${{COMPOSE_ENV_OVERRIDES[HOST]}}"
- printf 'PORT=%s\\n' "${{COMPOSE_ENV_OVERRIDES[PORT]}}"
- printf 'PORT_MAPPING=%s\\n' "${{LIGHTRAG_COMPOSE_SERVER_PORT_MAPPING}}\"
- """)
- assert values["HOST"] == "0.0.0.0"
- assert values["PORT"] == "9621"
- assert values["PORT_MAPPING"] == expected_port_mapping
- def test_finalize_server_setup_skips_embedded_milvus_sub_services(
- tmp_path: Path,
- ) -> None:
- """finalize_server_setup must keep prefixed Milvus child services on rerun."""
- compose_file = tmp_path / "docker-compose.final.yml"
- compose_file.write_text(
- "\n".join(
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " milvus:",
- " image: milvusdb/milvus:v2.6.11",
- " milvus-etcd:",
- " image: quay.io/coreos/etcd:v3.5.16",
- " milvus-minio:",
- " image: minio/minio:RELEASE.2024-12-13T22-19-12Z",
- "volumes:",
- " milvus_data:",
- " milvus-etcd_data:",
- " milvus-minio_data:",
- ]
- )
- + "\n",
- encoding="utf-8",
- )
- (tmp_path / "env.example").write_text(
- (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8"
- )
- write_text_lines(tmp_path / ".env", ["LIGHTRAG_SETUP_MILVUS_DEPLOYMENT=docker"])
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- collect_server_config() {{ :; }}
- collect_security_config() {{ :; }}
- collect_ssl_config() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- finalize_server_setup
- """)
- result = compose_file.read_text(encoding="utf-8")
- assert "milvus" in result
- assert "milvus-etcd" in result
- assert "milvus-minio" in result
- assert (
- """ milvus:
- condition: service_healthy"""
- in result
- )
- assert (
- """ milvus-etcd:
- condition: service_healthy"""
- not in result
- )
- assert (
- """ milvus-minio:
- condition: service_healthy"""
- not in result
- )
- def test_finalize_server_setup_uses_compose_native_neo4j_endpoint_on_rerun(
- tmp_path: Path,
- ) -> None:
- """Preserved managed services should inject compose-native endpoints on server reruns."""
- write_text_lines(
- tmp_path / ".env",
- ["LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker", "NEO4J_URI=neo4j://localhost:7687"],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " neo4j:",
- " image: neo4j:latest",
- ],
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- validate_security_config() {{ return 0; }}
- finalize_server_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- assert 'NEO4J_URI: "neo4j://neo4j:7687"' in result
- assert 'NEO4J_URI: "neo4j://host.docker.internal:7687"' not in result
- def test_finalize_server_setup_migrates_mongodb_to_atlas_local_for_mongo_vector_storage(
- tmp_path: Path,
- ) -> None:
- """Server reruns should upgrade docker-managed MongoDB to Atlas Local when Mongo vector storage needs it."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
- "LIGHTRAG_KV_STORAGE=MongoKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
- "MONGO_URI=mongodb://localhost:27017/?directConnection=true",
- "MONGO_DATABASE=LightRAG",
- "HOST=0.0.0.0",
- "PORT=9621",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongo:8.2.4",
- " volumes:",
- " - mongo_data:/data/db",
- "volumes:",
- " mongo_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
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- validate_security_config() {{ return 0; }}
- finalize_server_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- assert "image: mongodb/mongodb-atlas-local:" in result
- assert "mongo_config_data:/data/configdb" in result
- assert "mongo_mongot_data:/data/mongot" in result
- assert "image: mongo:8.2.4" not in result
- def test_finalize_server_setup_rejects_invalid_preserved_mongo_vector_config(
- tmp_path: Path,
- ) -> None:
- """Server reruns should fail before writing when preserved Mongo vector config is invalid."""
- write_text_lines(
- tmp_path / ".env",
- [
- "LIGHTRAG_RUNTIME_TARGET=compose",
- "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
- "LIGHTRAG_KV_STORAGE=MongoKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
- "MONGO_URI=mongodb://mongo.example.com:27017/?directConnection=true",
- "MONGO_DATABASE=LightRAG",
- "HOST=0.0.0.0",
- "PORT=9621",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " mongodb:",
- " image: mongo:8.2.4",
- " volumes:",
- " - mongo_data:/data/db",
- "volumes:",
- " mongo_data:",
- ],
- )
- result = run_bash_process(
- f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- validate_sensitive_env_literals() {{ return 0; }}
- validate_security_config() {{ return 0; }}
- finalize_server_setup
- """,
- cwd=tmp_path,
- )
- assert result.returncode != 0
- assert (
- "MongoVectorDBStorage requires the bundled Atlas Local endpoint"
- in result.stderr
- )
- assert "image: mongo:8.2.4" in (tmp_path / "docker-compose.final.yml").read_text(
- encoding="utf-8"
- )
- def test_finalize_server_setup_drops_stale_managed_services_missing_from_env_markers(
- tmp_path: Path,
- ) -> None:
- """env-server should remove stale wizard-managed services not marked in `.env`."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0", "PORT=9621"])
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " redis:",
- " image: redis:latest",
- " vllm-embed:",
- " image: vllm/vllm-openai:latest",
- "volumes:",
- " redis_data:",
- " vllm_embed_cache:",
- ],
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- collect_server_config() {{ :; }}
- collect_security_config() {{ :; }}
- collect_ssl_config() {{ :; }}
- confirm_required_yes_no() {{ return 0; }}
- confirm_default_yes() {{
- case "$1" in
- "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
- *) return 0 ;;
- esac
- }}
- validate_sensitive_env_literals() {{ return 0; }}
- validate_security_config() {{ return 0; }}
- finalize_server_setup
- """)
- result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
- generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
- assert " redis:" not in result
- assert " vllm-embed:" not in result
- assert "redis_data:" not in result
- assert "vllm_embed_cache:" not in result
- assert " lightrag:" in result
- assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
- def test_detect_managed_root_services_deduplicates_embedded_milvus_children(
- tmp_path: Path,
- ) -> None:
- """Managed service discovery should collapse Milvus child services to the root service."""
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " milvus:",
- " image: milvusdb/milvus:v2.6.11",
- " milvus-etcd:",
- " image: quay.io/coreos/etcd:v3.5.16",
- " milvus-minio:",
- " image: minio/minio:latest",
- " neo4j:",
- " image: neo4j:latest",
- ],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- detect_managed_root_services "{tmp_path}/docker-compose.final.yml\"
- """)
- assert output.splitlines() == ["milvus", "neo4j"]
- def test_detect_managed_root_services_groups_opensearch_dashboards_under_opensearch(
- tmp_path: Path,
- ) -> None:
- """opensearch-dashboards must collapse to the opensearch root so orphan dashboards blocks don't masquerade as external services."""
- write_text_lines(
- tmp_path / "docker-compose.final.yml",
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " opensearch:",
- " image: opensearchproject/opensearch:3",
- " dashboards:",
- " image: opensearchproject/opensearch-dashboards:3",
- ],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- detect_managed_root_services "{tmp_path}/docker-compose.final.yml\"
- """)
- assert output.splitlines() == ["opensearch"]
- def test_compose_has_non_wizard_services_ignores_orphan_dashboards(
- tmp_path: Path,
- ) -> None:
- """A leftover opensearch-dashboards alone must not be treated as an external service that blocks the host-mode prompt."""
- compose_file = tmp_path / "docker-compose.final.yml"
- write_text_lines(
- compose_file,
- [
- "services:",
- " lightrag:",
- " image: example/lightrag:test",
- " dashboards:",
- " image: opensearchproject/opensearch-dashboards:3",
- ],
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- if compose_has_non_wizard_services "{compose_file}"; then
- printf 'RESULT=non_wizard_detected\\n'
- else
- printf 'RESULT=wizard_only\\n'
- fi
- """)
- values = parse_lines(output)
- assert values["RESULT"] == "wizard_only"
- def test_finalize_server_setup_allows_risky_security_config_and_security_check_reports_it(
- tmp_path: Path,
- ) -> None:
- """Wizard writes `.env` without blocking, while security-check reports risky settings."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:secret",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health,/api/*",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- confirm_default_yes() {{ return 0; }}
- confirm_required_yes_no() {{ return 0; }}
- if finalize_server_setup; then
- printf 'RESULT=success\\n'
- else
- printf 'RESULT=failure\\n'
- fi
- """)
- values = parse_lines(output)
- assert values["RESULT"] == "success"
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "WHITELIST_PATHS exposes /api routes" in result.stdout
- def test_finalize_server_setup_allows_predictable_auth_passwords_and_security_check_reports_it(
- tmp_path: Path,
- ) -> None:
- """Server setup should not block on weak password prefixes that belong to security audit."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:Passw0rd!",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- show_summary() {{ :; }}
- confirm_default_yes() {{ return 0; }}
- confirm_required_yes_no() {{ return 0; }}
- if finalize_server_setup; then
- printf 'RESULT=success\\n'
- else
- printf 'RESULT=failure\\n'
- fi
- """)
- values = parse_lines(output)
- assert values["RESULT"] == "success"
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "AUTH_ACCOUNTS uses a predictable password prefix." in result.stdout
- def test_finalize_server_setup_rejects_malformed_auth_accounts(tmp_path: Path) -> None:
- """Server setup should fail fast instead of persisting invalid AUTH_ACCOUNTS syntax."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- output = run_bash(
- f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- collect_server_config() {{ :; }}
- collect_ssl_config() {{ :; }}
- ENV_VALUES[AUTH_ACCOUNTS]="admin"
- ENV_VALUES[TOKEN_SECRET]="jwt-secret"
- show_summary() {{ :; }}
- confirm_default_yes() {{ return 0; }}
- confirm_required_yes_no() {{ return 0; }}
- if finalize_server_setup; then
- printf 'RESULT=success\\n'
- else
- printf 'RESULT=failure\\n'
- fi
- printf 'ENV=%s\\n' "$(cat "$REPO_ROOT/.env")\"
- """,
- cwd=tmp_path,
- )
- values = parse_lines(output)
- assert values["RESULT"] == "failure"
- assert values["ENV"] == "HOST=0.0.0.0"
- def test_ssl_staging_uses_distinct_names_for_same_basename_inputs(
- tmp_path: Path,
- ) -> None:
- """Cert/key files with the same basename should stage to distinct paths."""
- env_example = tmp_path / "env.example"
- env_example.write_text(
- "\n".join(
- ["SSL_CERTFILE=/placeholder/cert.pem", "SSL_KEYFILE=/placeholder/key.pem"]
- )
- + "\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",
- )
- cert_dir = tmp_path / "certs"
- key_dir = tmp_path / "keys"
- cert_dir.mkdir()
- key_dir.mkdir()
- cert_path = cert_dir / "server.pem"
- cert_path.write_text("cert", encoding="utf-8")
- key_path = key_dir / "server.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}"
- 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_docker_compose "$REPO_ROOT/docker-compose.generated.yml\"
- """)
- generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
- encoding="utf-8"
- )
- staged_cert = tmp_path / "data" / "certs" / "cert-server.pem"
- staged_key = tmp_path / "data" / "certs" / "key-server.pem"
- assert staged_cert.read_text(encoding="utf-8") == "cert"
- assert staged_key.read_text(encoding="utf-8") == "key"
- assert 'SSL_CERTFILE: "/app/data/certs/cert-server.pem"' in generated_compose
- assert 'SSL_KEYFILE: "/app/data/certs/key-server.pem"' in generated_compose
- assert (
- "./data/certs/cert-server.pem:/app/data/certs/cert-server.pem:ro"
- in generated_compose
- )
- assert (
- "./data/certs/key-server.pem:/app/data/certs/key-server.pem:ro"
- in generated_compose
- )
- def test_ssl_staging_skips_copy_for_already_staged_relative_paths(
- tmp_path: Path,
- ) -> None:
- """Re-running setup with already-staged certs should not fail on identical copies."""
- staged_dir = tmp_path / "data" / "certs"
- staged_dir.mkdir(parents=True)
- cert_path = staged_dir / "server.pem"
- key_path = staged_dir / "server.key"
- cert_path.write_text("cert", encoding="utf-8")
- key_path.write_text("key", encoding="utf-8")
- run_bash(f"""
- set -euo pipefail
- cd "{tmp_path}"
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- stage_ssl_assets "./data/certs/server.pem" "./data/certs/server.key\"
- """)
- assert cert_path.read_text(encoding="utf-8") == "cert"
- assert key_path.read_text(encoding="utf-8") == "key"
- @pytest.mark.parametrize(
- ("name", "env_lines", "setup_snippet", "finalize_call"),
- [
- (
- "base",
- [],
- "\n".join(
- [
- 'ENV_VALUES[VLLM_EMBED_DEVICE]="cpu"',
- 'ENV_VALUES[VLLM_EMBED_MODEL]="BAAI/bge-m3"',
- 'ENV_VALUES[VLLM_EMBED_PORT]="8001"',
- 'ENV_VALUES[VLLM_EMBED_API_KEY]="local-key"',
- 'add_docker_service "vllm-embed"',
- "confirm_default_no() { return 1; }",
- ]
- ),
- "finalize_base_setup",
- ),
- (
- "storage",
- [
- "LIGHTRAG_KV_STORAGE=PGKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage",
- "POSTGRES_USER=lightrag",
- "POSTGRES_PASSWORD=secret",
- "POSTGRES_DATABASE=lightrag",
- ],
- 'add_docker_service "postgres"',
- "finalize_storage_setup",
- ),
- ],
- ids=["base", "storage"],
- )
- def test_finalize_flows_stage_inherited_ssl_assets_for_compose(
- tmp_path: Path,
- name: str,
- env_lines: list[str],
- setup_snippet: str,
- finalize_call: str,
- ) -> None:
- """Compose-writing finalize flows should stage inherited SSL assets before mounting them."""
- cert_path = tmp_path / f"{name}-source-cert.pem"
- key_path = tmp_path / f"{name}-source-key.pem"
- cert_path.write_text("cert", encoding="utf-8")
- key_path.write_text("key", encoding="utf-8")
- write_text_lines(
- tmp_path / ".env",
- [
- *env_lines,
- "SSL=true",
- f"SSL_CERTFILE={cert_path}",
- f"SSL_KEYFILE={key_path}",
- ],
- )
- write_text_lines(
- tmp_path / "env.example",
- (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
- )
- (tmp_path / "docker-compose.yml").write_text(
- (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
- )
- run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- reset_state
- load_existing_env_if_present
- {setup_snippet}
- show_summary() {{ :; }}
- confirm_default_yes() {{ return 0; }}
- confirm_required_yes_no() {{ return 0; }}
- {finalize_call}
- """)
- generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
- encoding="utf-8"
- )
- staged_cert = tmp_path / "data" / "certs" / f"{name}-source-cert.pem"
- staged_key = tmp_path / "data" / "certs" / f"{name}-source-key.pem"
- assert staged_cert.read_text(encoding="utf-8") == "cert"
- assert staged_key.read_text(encoding="utf-8") == "key"
- assert (
- f"./data/certs/{name}-source-cert.pem:/app/data/certs/{name}-source-cert.pem:ro"
- in generated_compose
- )
- assert (
- f"./data/certs/{name}-source-key.pem:/app/data/certs/{name}-source-key.pem:ro"
- in generated_compose
- )
- def test_security_check_reports_missing_authentication(tmp_path: Path) -> None:
- """Security audit should flag unauthenticated API exposure."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "No API protection is configured." in result.stdout
- def test_security_check_passes_for_authenticated_minimal_config(tmp_path: Path) -> None:
- """Security audit should pass for a minimally hardened config."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:secret",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health",
- ],
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 0
- def test_security_check_reports_predictable_auth_password_prefix(
- tmp_path: Path,
- ) -> None:
- """Security audit should flag AUTH_ACCOUNTS passwords with predictable prefixes."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:admin123!",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health",
- ],
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "AUTH_ACCOUNTS uses a predictable password prefix." in result.stdout
- def test_security_check_reports_api_key_only_with_default_whitelist(
- tmp_path: Path,
- ) -> None:
- """API-key-only deployment with unset WHITELIST_PATHS inherits /api/* and must be flagged."""
- write_text_lines(tmp_path / ".env", ["LIGHTRAG_API_KEY=my-secret-key"])
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "WHITELIST_PATHS exposes /api routes" in result.stdout
- def test_security_check_reports_api_key_only_with_explicit_api_wildcard_whitelist(
- tmp_path: Path,
- ) -> None:
- """API-key-only deployment with WHITELIST_PATHS=/health,/api/* must be flagged."""
- write_text_lines(
- tmp_path / ".env",
- ["LIGHTRAG_API_KEY=my-secret-key", "WHITELIST_PATHS=/health,/api/*"],
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "WHITELIST_PATHS exposes /api routes" in result.stdout
- def test_security_check_passes_for_api_key_only_with_safe_whitelist(
- tmp_path: Path,
- ) -> None:
- """API-key-only deployment with a safe WHITELIST_PATHS should pass the security check."""
- write_text_lines(
- tmp_path / ".env", ["LIGHTRAG_API_KEY=my-secret-key", "WHITELIST_PATHS=/health"]
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 0
- assert "No obvious security issues found" in result.stdout
- def test_security_check_ignores_default_opensearch_password_when_opensearch_unused(
- tmp_path: Path,
- ) -> None:
- """Security audit should ignore OpenSearch defaults when no OpenSearch storage is selected."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:secret",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health",
- "LIGHTRAG_KV_STORAGE=JsonKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
- "OPENSEARCH_PASSWORD=LightRAG2026_!@",
- ],
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 0
- assert "OPENSEARCH_PASSWORD uses a well-known default value." not in result.stdout
- def test_security_check_reports_default_opensearch_password_when_opensearch_selected(
- tmp_path: Path,
- ) -> None:
- """Security audit should flag the default OpenSearch password when OpenSearch is selected."""
- write_text_lines(
- tmp_path / ".env",
- [
- "AUTH_ACCOUNTS=admin:secret",
- "TOKEN_SECRET=jwt-secret",
- "WHITELIST_PATHS=/health",
- "LIGHTRAG_KV_STORAGE=OpenSearchKVStorage",
- "LIGHTRAG_VECTOR_STORAGE=OpenSearchVectorDBStorage",
- "LIGHTRAG_GRAPH_STORAGE=OpenSearchGraphStorage",
- "LIGHTRAG_DOC_STATUS_STORAGE=OpenSearchDocStatusStorage",
- "OPENSEARCH_HOSTS=localhost:9200",
- "OPENSEARCH_USER=admin",
- "OPENSEARCH_PASSWORD=LightRAG2026_!@",
- ],
- )
- result = subprocess.run(
- [
- "bash",
- "--norc",
- "--noprofile",
- "-c",
- f"""
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- security_check_env_file
- """,
- ],
- cwd=REPO_ROOT,
- capture_output=True,
- text=True,
- check=False,
- )
- assert result.returncode == 1
- assert "OPENSEARCH_PASSWORD uses a well-known default value." in result.stdout
- def test_show_summary_masks_auth_accounts() -> None:
- """Configuration summaries should not print auth account passwords."""
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- reset_state
- ENV_VALUES[AUTH_ACCOUNTS]="admin:secret,reader:hunter2"
- ENV_VALUES[TOKEN_SECRET]="jwt-secret"
- ENV_VALUES[HOST]="0.0.0.0"
- show_summary
- """)
- assert "AUTH_ACCOUNTS=***" in output
- assert "TOKEN_SECRET=***" in output
- assert "admin:secret" not in output
- assert "reader:hunter2" not in output
- def test_opensearch_index_validators_accept_zero_padded_values() -> None:
- """OpenSearch shard and replica validators should accept zero-padded decimals."""
- values = run_bash_lines(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- if validate_positive_integer "08"; then
- printf 'SHARDS=valid\\n'
- else
- printf 'SHARDS=invalid\\n'
- fi
- if validate_non_negative_integer "09"; then
- printf 'REPLICAS=valid\\n'
- else
- printf 'REPLICAS=invalid\\n'
- fi
- """)
- assert values["SHARDS"] == "valid"
- assert values["REPLICAS"] == "valid"
- def test_backup_only_backs_up_env_and_generated_compose(tmp_path: Path) -> None:
- """backup_only should back up both .env and the active generated compose file."""
- compose_content = (
- "\n".join(["services:", " lightrag:", " image: example/lightrag:test"])
- + "\n"
- )
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- (tmp_path / "docker-compose.final.yml").write_text(
- compose_content, encoding="utf-8"
- )
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- backup_only
- """)
- env_backups = sorted(tmp_path.glob(".env.backup.*"))
- assert len(env_backups) == 1
- assert env_backups[0].read_text(encoding="utf-8") == "HOST=0.0.0.0\n"
- assert "Backed up .env to" in output
- assert "Backed up compose file to" in output
- assert_single_compose_backup(tmp_path, compose_content)
- def test_backup_only_skips_compose_backup_when_no_generated_compose_exists(
- tmp_path: Path,
- ) -> None:
- """backup_only should still succeed when only .env exists."""
- write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0"])
- output = run_bash(f"""
- set -euo pipefail
- source "{REPO_ROOT}/scripts/setup/setup.sh"
- REPO_ROOT="{tmp_path}"
- backup_only
- """)
- env_backups = sorted(tmp_path.glob(".env.backup.*"))
- assert len(env_backups) == 1
- assert "Backed up .env to" in output
- assert "Backed up compose file to" not in output
- assert list(tmp_path.glob("docker-compose.backup*.yml")) == []
|