test_env.py 99 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027
  1. # Regression tests for interactive setup wizard.
  2. # Classification: keep tests here when they exercise env_* top-level wizard flows and their end-to-end env/compose rewrite outcomes.
  3. from __future__ import annotations
  4. from pathlib import Path
  5. import pytest
  6. from tests.setup._helpers import (
  7. REPO_ROOT,
  8. assert_single_compose_backup,
  9. parse_lines,
  10. run_bash,
  11. run_bash_process,
  12. run_bash_lines,
  13. write_storage_setup_files,
  14. write_text_lines,
  15. )
  16. pytestmark = pytest.mark.offline
  17. def test_env_base_flow_preserves_non_inference_env_values(tmp_path: Path) -> None:
  18. """env-base wizard should leave server, security, and observability values untouched."""
  19. env_file = tmp_path / ".env"
  20. env_file.write_text(
  21. "\n".join(
  22. [
  23. "HOST=127.0.0.1",
  24. "PORT=9999",
  25. "WEBUI_TITLE=Existing Title",
  26. "WEBUI_DESCRIPTION=Existing Description",
  27. "SSL=true",
  28. "SSL_CERTFILE=/some/cert.pem",
  29. "SSL_KEYFILE=/some/key.pem",
  30. "AUTH_ACCOUNTS=admin:secret",
  31. "TOKEN_SECRET=jwt-secret",
  32. "LIGHTRAG_API_KEY=api-key",
  33. "LANGFUSE_ENABLE_TRACE=true",
  34. "LANGFUSE_SECRET_KEY=langfuse-secret",
  35. "LLM_BINDING_API_KEY=sk-existing",
  36. "EMBEDDING_BINDING_API_KEY=sk-existing",
  37. ]
  38. )
  39. + "\n",
  40. encoding="utf-8",
  41. )
  42. output = run_bash(f"""
  43. set -euo pipefail
  44. source "{REPO_ROOT}/scripts/setup/setup.sh"
  45. REPO_ROOT="{tmp_path}"
  46. prompt_choice() {{ printf '%s' "$2"; }}
  47. prompt_with_default() {{ printf '%s' "$2"; }}
  48. prompt_until_valid() {{ printf '%s' "$2"; }}
  49. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  50. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  51. confirm_default_no() {{ return 1; }}
  52. confirm_default_yes() {{ return 1; }}
  53. finalize_base_setup() {{
  54. printf 'HOST=%s\\n' "${{ENV_VALUES[HOST]}}"
  55. printf 'PORT=%s\\n' "${{ENV_VALUES[PORT]}}"
  56. printf 'WEBUI_TITLE=%s\\n' "${{ENV_VALUES[WEBUI_TITLE]}}"
  57. printf 'WEBUI_DESCRIPTION=%s\\n' "${{ENV_VALUES[WEBUI_DESCRIPTION]}}"
  58. printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}"
  59. printf 'LLM_BINDING_API_KEY=%s\\n' "${{ENV_VALUES[LLM_BINDING_API_KEY]}}"
  60. printf 'EMBEDDING_BINDING_API_KEY=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_API_KEY]}}"
  61. printf 'SSL_SET=%s\\n' "${{ENV_VALUES[SSL]+set}}"
  62. printf 'AUTH_ACCOUNTS_SET=%s\\n' "${{ENV_VALUES[AUTH_ACCOUNTS]+set}}"
  63. printf 'TOKEN_SECRET_SET=%s\\n' "${{ENV_VALUES[TOKEN_SECRET]+set}}"
  64. printf 'LIGHTRAG_API_KEY_SET=%s\\n' "${{ENV_VALUES[LIGHTRAG_API_KEY]+set}}"
  65. printf 'LANGFUSE_ENABLE_TRACE_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_ENABLE_TRACE]+set}}"
  66. printf 'LANGFUSE_SECRET_KEY_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_SECRET_KEY]+set}}"
  67. }}
  68. env_base_flow
  69. """)
  70. values = parse_lines(output)
  71. assert values["HOST"] == "127.0.0.1"
  72. assert values["PORT"] == "9999"
  73. assert values["WEBUI_TITLE"] == "Existing Title"
  74. assert values["WEBUI_DESCRIPTION"] == "Existing Description"
  75. assert values["LLM_BINDING"] == "openai"
  76. assert values["LLM_BINDING_API_KEY"] == "sk-existing"
  77. assert values["EMBEDDING_BINDING_API_KEY"] == "sk-existing"
  78. assert values["SSL_SET"] == "set"
  79. assert values["AUTH_ACCOUNTS_SET"] == "set"
  80. assert values["TOKEN_SECRET_SET"] == "set"
  81. assert values["LIGHTRAG_API_KEY_SET"] == "set"
  82. assert values["LANGFUSE_ENABLE_TRACE_SET"] == "set"
  83. assert values["LANGFUSE_SECRET_KEY_SET"] == "set"
  84. def test_env_base_flow_preserves_existing_provider_bindings_on_rerun(
  85. tmp_path: Path,
  86. ) -> None:
  87. """Rerunning env-base should keep prior LLM and embedding provider settings."""
  88. env_file = tmp_path / ".env"
  89. env_file.write_text(
  90. "\n".join(
  91. [
  92. "LLM_BINDING=ollama",
  93. "LLM_MODEL=llama3.2:latest",
  94. "LLM_BINDING_HOST=http://localhost:11434",
  95. "EMBEDDING_BINDING=ollama",
  96. "EMBEDDING_MODEL=nomic-embed-text:latest",
  97. "EMBEDDING_DIM=768",
  98. "EMBEDDING_BINDING_HOST=http://localhost:11434",
  99. ]
  100. )
  101. + "\n",
  102. encoding="utf-8",
  103. )
  104. output = run_bash(f"""
  105. set -euo pipefail
  106. source "{REPO_ROOT}/scripts/setup/setup.sh"
  107. REPO_ROOT="{tmp_path}"
  108. prompt_choice() {{ printf '%s' "$2"; }}
  109. prompt_with_default() {{ printf '%s' "$2"; }}
  110. prompt_until_valid() {{ printf '%s' "$2"; }}
  111. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  112. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  113. confirm_default_no() {{ return 1; }}
  114. confirm_default_yes() {{ return 1; }}
  115. finalize_base_setup() {{
  116. printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}"
  117. printf 'LLM_MODEL=%s\\n' "${{ENV_VALUES[LLM_MODEL]}}"
  118. printf 'LLM_BINDING_HOST=%s\\n' "${{ENV_VALUES[LLM_BINDING_HOST]}}"
  119. printf 'EMBEDDING_BINDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING]}}"
  120. printf 'EMBEDDING_MODEL=%s\\n' "${{ENV_VALUES[EMBEDDING_MODEL]}}"
  121. printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}"
  122. printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
  123. }}
  124. env_base_flow
  125. """)
  126. values = parse_lines(output)
  127. assert values["LLM_BINDING"] == "ollama"
  128. assert values["LLM_MODEL"] == "llama3.2:latest"
  129. assert values["LLM_BINDING_HOST"] == "http://localhost:11434"
  130. assert values["EMBEDDING_BINDING"] == "ollama"
  131. assert values["EMBEDDING_MODEL"] == "nomic-embed-text:latest"
  132. assert values["EMBEDDING_DIM"] == "768"
  133. assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:11434"
  134. def test_env_base_flow_preserves_existing_vllm_embedding_settings_on_rerun(
  135. tmp_path: Path,
  136. ) -> None:
  137. """Rerunning env-base should keep saved local vLLM embedding settings."""
  138. write_text_lines(
  139. tmp_path / ".env",
  140. [
  141. "LLM_BINDING=openai",
  142. "LLM_MODEL=gpt-4o-mini",
  143. "LLM_BINDING_HOST=https://api.openai.com/v1",
  144. "LLM_BINDING_API_KEY=sk-existing",
  145. "EMBEDDING_BINDING=openai",
  146. "EMBEDDING_MODEL=BAAI/custom-embed",
  147. "EMBEDDING_DIM=768",
  148. "EMBEDDING_BINDING_HOST=http://localhost:9101/v1",
  149. "EMBEDDING_BINDING_API_KEY=embed-key",
  150. "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm",
  151. "VLLM_EMBED_MODEL=BAAI/custom-embed",
  152. "VLLM_EMBED_PORT=9101",
  153. "VLLM_EMBED_DEVICE=cpu",
  154. ],
  155. )
  156. values = run_bash_lines(f"""
  157. set -euo pipefail
  158. source "{REPO_ROOT}/scripts/setup/setup.sh"
  159. REPO_ROOT="{tmp_path}"
  160. prompt_choice() {{ printf '%s' "$2"; }}
  161. prompt_with_default() {{ printf '%s' "$2"; }}
  162. prompt_until_valid() {{ printf '%s' "$2"; }}
  163. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  164. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  165. confirm_default_no() {{ return 1; }}
  166. confirm_default_yes() {{
  167. case "$1" in
  168. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  169. *) return 1 ;;
  170. esac
  171. }}
  172. finalize_base_setup() {{
  173. printf 'EMBEDDING_MODEL=%s\\n' "${{ENV_VALUES[EMBEDDING_MODEL]}}"
  174. printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}"
  175. printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
  176. printf 'VLLM_EMBED_MODEL=%s\\n' "${{ENV_VALUES[VLLM_EMBED_MODEL]}}"
  177. printf 'VLLM_EMBED_PORT=%s\\n' "${{ENV_VALUES[VLLM_EMBED_PORT]}}"
  178. }}
  179. env_base_flow
  180. """)
  181. assert values["EMBEDDING_MODEL"] == "BAAI/custom-embed"
  182. assert values["EMBEDDING_DIM"] == "768"
  183. assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1"
  184. assert values["VLLM_EMBED_MODEL"] == "BAAI/custom-embed"
  185. assert values["VLLM_EMBED_PORT"] == "9101"
  186. def test_env_base_flow_resets_remote_embedding_host_when_switching_to_vllm(
  187. tmp_path: Path,
  188. ) -> None:
  189. """Switching a remote embedding provider to local vLLM should restore localhost."""
  190. write_text_lines(
  191. tmp_path / ".env",
  192. [
  193. "LLM_BINDING=openai",
  194. "LLM_MODEL=gpt-4o-mini",
  195. "LLM_BINDING_HOST=https://api.openai.com/v1",
  196. "LLM_BINDING_API_KEY=sk-existing",
  197. "EMBEDDING_BINDING=jina",
  198. "EMBEDDING_MODEL=jina-embeddings-v4",
  199. "EMBEDDING_DIM=2048",
  200. "EMBEDDING_BINDING_HOST=https://api.jina.ai/v1/embeddings",
  201. "EMBEDDING_BINDING_API_KEY=jina-key",
  202. "VLLM_EMBED_PORT=9101",
  203. ],
  204. )
  205. values = run_bash_lines(f"""
  206. set -euo pipefail
  207. source "{REPO_ROOT}/scripts/setup/setup.sh"
  208. REPO_ROOT="{tmp_path}"
  209. prompt_choice() {{ printf '%s' "$2"; }}
  210. prompt_with_default() {{ printf '%s' "$2"; }}
  211. prompt_until_valid() {{ printf '%s' "$2"; }}
  212. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  213. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  214. confirm_default_no() {{
  215. case "$1" in
  216. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  217. "Enable reranking?") return 1 ;;
  218. *) return 1 ;;
  219. esac
  220. }}
  221. confirm_default_yes() {{ return 1; }}
  222. finalize_base_setup() {{
  223. printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
  224. printf 'LIGHTRAG_SETUP_EMBEDDING_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_EMBEDDING_PROVIDER]}}"
  225. }}
  226. env_base_flow
  227. """)
  228. assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1"
  229. assert values["LIGHTRAG_SETUP_EMBEDDING_PROVIDER"] == "vllm"
  230. def test_env_base_flow_preserves_existing_vllm_embedding_device_on_gpu_host(
  231. tmp_path: Path,
  232. ) -> None:
  233. """Saved vLLM embedding CPU/GPU mode should win over auto-detected GPU defaults."""
  234. write_text_lines(
  235. tmp_path / ".env",
  236. [
  237. "LLM_BINDING=openai",
  238. "LLM_MODEL=gpt-4o-mini",
  239. "LLM_BINDING_HOST=https://api.openai.com/v1",
  240. "LLM_BINDING_API_KEY=sk-existing",
  241. "EMBEDDING_BINDING=openai",
  242. "EMBEDDING_MODEL=BAAI/custom-embed",
  243. "EMBEDDING_DIM=1024",
  244. "EMBEDDING_BINDING_HOST=http://localhost:9101/v1",
  245. "EMBEDDING_BINDING_API_KEY=embed-key",
  246. "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm",
  247. "VLLM_EMBED_MODEL=BAAI/custom-embed",
  248. "VLLM_EMBED_PORT=9101",
  249. "VLLM_EMBED_DEVICE=cpu",
  250. ],
  251. )
  252. values = run_bash_lines(f"""
  253. set -euo pipefail
  254. source "{REPO_ROOT}/scripts/setup/setup.sh"
  255. REPO_ROOT="{tmp_path}"
  256. nvidia-smi() {{ return 0; }}
  257. prompt_choice() {{ printf '%s' "$2"; }}
  258. prompt_with_default() {{ printf '%s' "$2"; }}
  259. prompt_until_valid() {{ printf '%s' "$2"; }}
  260. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  261. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  262. confirm_default_no() {{ return 1; }}
  263. confirm_default_yes() {{
  264. case "$1" in
  265. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  266. *) return 1 ;;
  267. esac
  268. }}
  269. finalize_base_setup() {{
  270. printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}"
  271. }}
  272. env_base_flow
  273. """)
  274. assert values["VLLM_EMBED_DEVICE"] == "cpu"
  275. def test_env_base_flow_preserves_existing_vllm_embedding_cuda_device_on_rerun(
  276. tmp_path: Path,
  277. ) -> None:
  278. """Saved vLLM embedding CUDA mode should survive env-base reruns."""
  279. write_text_lines(
  280. tmp_path / ".env",
  281. [
  282. "LLM_BINDING=openai",
  283. "LLM_MODEL=gpt-4o-mini",
  284. "LLM_BINDING_HOST=https://api.openai.com/v1",
  285. "LLM_BINDING_API_KEY=sk-existing",
  286. "EMBEDDING_BINDING=openai",
  287. "EMBEDDING_MODEL=BAAI/custom-embed",
  288. "EMBEDDING_DIM=1024",
  289. "EMBEDDING_BINDING_HOST=http://localhost:9101/v1",
  290. "EMBEDDING_BINDING_API_KEY=embed-key",
  291. "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm",
  292. "VLLM_EMBED_MODEL=BAAI/custom-embed",
  293. "VLLM_EMBED_PORT=9101",
  294. "VLLM_EMBED_DEVICE=cuda",
  295. ],
  296. )
  297. values = run_bash_lines(f"""
  298. set -euo pipefail
  299. source "{REPO_ROOT}/scripts/setup/setup.sh"
  300. REPO_ROOT="{tmp_path}"
  301. nvidia-smi() {{ return 0; }}
  302. prompt_choice() {{ printf '%s' "$2"; }}
  303. prompt_with_default() {{ printf '%s' "$2"; }}
  304. prompt_until_valid() {{ printf '%s' "$2"; }}
  305. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  306. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  307. confirm_default_no() {{ return 1; }}
  308. confirm_default_yes() {{
  309. case "$1" in
  310. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  311. *) return 1 ;;
  312. esac
  313. }}
  314. finalize_base_setup() {{
  315. printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}"
  316. }}
  317. env_base_flow
  318. """)
  319. assert values["VLLM_EMBED_DEVICE"] == "cuda"
  320. def test_env_base_flow_defaults_new_vllm_embedding_to_cuda_on_gpu_host(
  321. tmp_path: Path,
  322. ) -> None:
  323. """Fresh local vLLM embedding setup should honor GPU auto-detection."""
  324. values = run_bash_lines(f"""
  325. set -euo pipefail
  326. source "{REPO_ROOT}/scripts/setup/setup.sh"
  327. REPO_ROOT="{tmp_path}"
  328. nvidia-smi() {{ return 0; }}
  329. prompt_choice() {{ printf '%s' "$2"; }}
  330. prompt_with_default() {{ printf '%s' "$2"; }}
  331. prompt_until_valid() {{ printf '%s' "$2"; }}
  332. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  333. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  334. confirm_default_no() {{
  335. case "$1" in
  336. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  337. "Enable reranking?") return 1 ;;
  338. *) return 1 ;;
  339. esac
  340. }}
  341. confirm_default_yes() {{
  342. return 1
  343. }}
  344. finalize_base_setup() {{
  345. printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}"
  346. }}
  347. env_base_flow
  348. """)
  349. assert values["VLLM_EMBED_DEVICE"] == "cuda"
  350. def test_env_base_flow_forced_vllm_cuda_selection_writes_cuda_devices_to_env(
  351. tmp_path: Path,
  352. ) -> None:
  353. """Forced CUDA selection should drive both .env devices and GPU compose templates."""
  354. write_text_lines(
  355. tmp_path / "env.example",
  356. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  357. )
  358. write_text_lines(
  359. tmp_path / "docker-compose.yml",
  360. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8").splitlines(),
  361. )
  362. result = run_bash_process(
  363. f"""
  364. set -euo pipefail
  365. source "{REPO_ROOT}/scripts/setup/setup.sh"
  366. REPO_ROOT="{tmp_path}"
  367. host_cuda_available() {{ return 1; }}
  368. prompt_choice() {{
  369. case "$1" in
  370. "LLM provider") printf 'ollama' ;;
  371. "Embedding device"|"Rerank device") printf 'cuda' ;;
  372. *) printf '%s' "$2" ;;
  373. esac
  374. }}
  375. prompt_with_default() {{ printf '%s' "$2"; }}
  376. prompt_until_valid() {{ printf '%s' "$2"; }}
  377. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  378. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  379. confirm_default_no() {{
  380. case "$1" in
  381. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  382. "Enable reranking?") return 0 ;;
  383. "Run rerank service locally via Docker?") return 0 ;;
  384. *) return 1 ;;
  385. esac
  386. }}
  387. confirm_default_yes() {{
  388. case "$1" in
  389. *"The compose file will be created/updated. Continue?"*) return 0 ;;
  390. *) return 1 ;;
  391. esac
  392. }}
  393. confirm_required_yes_no() {{ return 0; }}
  394. env_base_flow
  395. """,
  396. cwd=tmp_path,
  397. )
  398. assert result.returncode == 0, result.stderr
  399. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  400. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  401. encoding="utf-8"
  402. )
  403. assert "VLLM_EMBED_DEVICE=cuda" in generated_env
  404. assert "VLLM_RERANK_DEVICE=cuda" in generated_env
  405. assert generated_compose.count("capabilities: [gpu]") >= 2
  406. assert (
  407. "CUDA device selected for vLLM embedding but no NVIDIA driver detected on host."
  408. in result.stdout
  409. )
  410. assert (
  411. "CUDA device selected for vLLM rerank but no NVIDIA driver detected on host."
  412. in result.stdout
  413. )
  414. def test_env_base_flow_vllm_defaults_prefer_original_env_values_on_rerun(
  415. tmp_path: Path,
  416. ) -> None:
  417. """vLLM prompt defaults should prefer the loaded `.env` snapshot over later mutations."""
  418. write_text_lines(
  419. tmp_path / ".env",
  420. [
  421. "LLM_BINDING=openai",
  422. "LLM_MODEL=gpt-4o-mini",
  423. "LLM_BINDING_HOST=https://api.openai.com/v1",
  424. "LLM_BINDING_API_KEY=sk-existing",
  425. "EMBEDDING_BINDING=openai",
  426. "EMBEDDING_MODEL=BAAI/original-embed",
  427. "EMBEDDING_DIM=1024",
  428. "EMBEDDING_BINDING_HOST=http://localhost:9101/v1",
  429. "EMBEDDING_BINDING_API_KEY=embed-key",
  430. "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm",
  431. "VLLM_EMBED_MODEL=BAAI/original-embed",
  432. "VLLM_EMBED_PORT=9101",
  433. "VLLM_EMBED_DEVICE=cpu",
  434. "RERANK_BINDING=cohere",
  435. "RERANK_MODEL=BAAI/original-rerank",
  436. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  437. "RERANK_BINDING_API_KEY=rerank-key",
  438. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  439. "VLLM_RERANK_MODEL=BAAI/original-rerank",
  440. "VLLM_RERANK_PORT=9200",
  441. "VLLM_RERANK_DEVICE=cpu",
  442. ],
  443. )
  444. values = run_bash_lines(f"""
  445. set -euo pipefail
  446. source "{REPO_ROOT}/scripts/setup/setup.sh"
  447. REPO_ROOT="{tmp_path}"
  448. nvidia-smi() {{ return 0; }}
  449. collect_llm_config() {{
  450. ENV_VALUES[VLLM_EMBED_MODEL]="BAAI/mutated-embed"
  451. ENV_VALUES[EMBEDDING_DIM]="2048"
  452. ENV_VALUES[VLLM_EMBED_PORT]="9991"
  453. ENV_VALUES[EMBEDDING_BINDING_HOST]="http://localhost:9991/v1"
  454. ENV_VALUES[VLLM_EMBED_DEVICE]="cuda"
  455. ENV_VALUES[VLLM_RERANK_MODEL]="BAAI/mutated-rerank"
  456. ENV_VALUES[VLLM_RERANK_PORT]="9990"
  457. ENV_VALUES[RERANK_BINDING_HOST]="http://localhost:9990/rerank"
  458. ENV_VALUES[VLLM_RERANK_DEVICE]="cuda"
  459. }}
  460. prompt_choice() {{ printf '%s' "$2"; }}
  461. prompt_with_default() {{ printf '%s' "$2"; }}
  462. prompt_until_valid() {{ printf '%s' "$2"; }}
  463. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  464. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  465. confirm_default_no() {{ return 1; }}
  466. confirm_default_yes() {{
  467. case "$1" in
  468. "Enable reranking?") return 0 ;;
  469. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  470. "Run rerank service locally via Docker?") return 0 ;;
  471. *) return 1 ;;
  472. esac
  473. }}
  474. finalize_base_setup() {{
  475. printf 'VLLM_EMBED_MODEL=%s\\n' "${{ENV_VALUES[VLLM_EMBED_MODEL]}}"
  476. printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}"
  477. printf 'VLLM_EMBED_PORT=%s\\n' "${{ENV_VALUES[VLLM_EMBED_PORT]}}"
  478. printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
  479. printf 'VLLM_EMBED_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_EMBED_DEVICE]}}"
  480. printf 'VLLM_RERANK_MODEL=%s\\n' "${{ENV_VALUES[VLLM_RERANK_MODEL]}}"
  481. printf 'VLLM_RERANK_PORT=%s\\n' "${{ENV_VALUES[VLLM_RERANK_PORT]}}"
  482. printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}"
  483. printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}"
  484. }}
  485. env_base_flow
  486. """)
  487. assert values["VLLM_EMBED_MODEL"] == "BAAI/original-embed"
  488. assert values["EMBEDDING_DIM"] == "1024"
  489. assert values["VLLM_EMBED_PORT"] == "9101"
  490. assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:9101/v1"
  491. assert values["VLLM_EMBED_DEVICE"] == "cpu"
  492. assert values["VLLM_RERANK_MODEL"] == "BAAI/original-rerank"
  493. assert values["VLLM_RERANK_PORT"] == "9200"
  494. assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank"
  495. assert values["VLLM_RERANK_DEVICE"] == "cpu"
  496. def test_env_base_flow_vllm_device_prompt_is_first_after_docker_choice(
  497. tmp_path: Path,
  498. ) -> None:
  499. """vLLM should ask for device before model-specific prompts once Docker is selected."""
  500. values = run_bash_lines(f"""
  501. set -euo pipefail
  502. source "{REPO_ROOT}/scripts/setup/setup.sh"
  503. REPO_ROOT="{tmp_path}"
  504. PROMPT_LOG_FILE="$(mktemp)"
  505. : > "$PROMPT_LOG_FILE"
  506. prompt_choice() {{
  507. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  508. printf '%s' "$2"
  509. }}
  510. prompt_with_default() {{
  511. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  512. printf '%s' "$2"
  513. }}
  514. prompt_until_valid() {{ printf '%s' "$2"; }}
  515. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  516. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  517. confirm_default_no() {{
  518. case "$1" in
  519. "Run embedding model locally via Docker (vLLM)?") return 0 ;;
  520. "Enable reranking?") return 0 ;;
  521. "Run rerank service locally via Docker?") return 0 ;;
  522. *) return 1 ;;
  523. esac
  524. }}
  525. confirm_default_yes() {{
  526. return 1
  527. }}
  528. finalize_base_setup() {{ :; }}
  529. env_base_flow
  530. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")"
  531. """)
  532. prompt_log = values["PROMPT_LOG"]
  533. assert prompt_log.index("Embedding device") < prompt_log.index("Embedding model")
  534. assert prompt_log.index("Rerank device") < prompt_log.index("Rerank model")
  535. def test_env_base_flow_preserves_ssl_config_on_rerun(tmp_path: Path) -> None:
  536. """env-base should preserve SSL config on rerun, even when old paths are stale."""
  537. cases = {
  538. "stale-paths": [
  539. "SSL=true",
  540. "SSL_CERTFILE=/missing/cert.pem",
  541. "SSL_KEYFILE=/missing/key.pem",
  542. "LLM_BINDING_API_KEY=sk-existing",
  543. "EMBEDDING_BINDING_API_KEY=sk-existing",
  544. ],
  545. "existing-paths": [
  546. "SSL=true",
  547. "SSL_CERTFILE=/some/cert.pem",
  548. "SSL_KEYFILE=/some/key.pem",
  549. ],
  550. }
  551. for case_name, env_lines in cases.items():
  552. case_dir = tmp_path / case_name
  553. case_dir.mkdir()
  554. write_text_lines(
  555. case_dir / "env.example",
  556. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  557. )
  558. write_text_lines(case_dir / ".env", env_lines)
  559. run_bash(f"""
  560. set -euo pipefail
  561. source "{REPO_ROOT}/scripts/setup/setup.sh"
  562. REPO_ROOT="{case_dir}"
  563. prompt_choice() {{ printf '%s' "$2"; }}
  564. prompt_with_default() {{ printf '%s' "$2"; }}
  565. prompt_until_valid() {{ printf '%s' "$2"; }}
  566. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  567. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  568. confirm_default_no() {{ return 1; }}
  569. confirm_default_yes() {{
  570. case "$1" in
  571. *) return 1 ;;
  572. esac
  573. }}
  574. confirm_required_yes_no() {{ return 0; }}
  575. env_base_flow
  576. """)
  577. generated_lines = (case_dir / ".env").read_text(encoding="utf-8").splitlines()
  578. for line in env_lines:
  579. assert line in generated_lines
  580. def test_env_base_flow_preserves_existing_compose_ssl_when_env_paths_are_stale(
  581. tmp_path: Path,
  582. ) -> None:
  583. """env-base should keep compose SSL wiring when inherited source paths no longer exist."""
  584. write_text_lines(
  585. tmp_path / "env.example",
  586. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  587. )
  588. write_text_lines(
  589. tmp_path / ".env",
  590. [
  591. "SSL=true",
  592. "SSL_CERTFILE=/missing/cert.pem",
  593. "SSL_KEYFILE=/missing/key.pem",
  594. "LLM_BINDING=openai",
  595. "LLM_MODEL=gpt-4o-mini",
  596. "LLM_BINDING_HOST=https://api.openai.com/v1",
  597. "LLM_BINDING_API_KEY=sk-existing",
  598. "EMBEDDING_BINDING=openai",
  599. "EMBEDDING_MODEL=text-embedding-3-small",
  600. "EMBEDDING_DIM=1536",
  601. "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
  602. "EMBEDDING_BINDING_API_KEY=sk-existing",
  603. ],
  604. )
  605. write_text_lines(
  606. tmp_path / "docker-compose.final.yml",
  607. [
  608. "services:",
  609. " lightrag:",
  610. " image: example/lightrag:test",
  611. " environment:",
  612. ' SSL_CERTFILE: "/app/data/certs/cert.pem"',
  613. ' SSL_KEYFILE: "/app/data/certs/key.pem"',
  614. " volumes:",
  615. ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"',
  616. ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"',
  617. ],
  618. )
  619. run_bash(f"""
  620. set -euo pipefail
  621. source "{REPO_ROOT}/scripts/setup/setup.sh"
  622. REPO_ROOT="{tmp_path}"
  623. prompt_choice() {{ printf '%s' "$2"; }}
  624. prompt_with_default() {{ printf '%s' "$2"; }}
  625. prompt_until_valid() {{ printf '%s' "$2"; }}
  626. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  627. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  628. confirm_default_no() {{ return 1; }}
  629. confirm_default_yes() {{
  630. case "$1" in
  631. "Run LightRAG Server via Docker?") return 0 ;;
  632. *) return 1 ;;
  633. esac
  634. }}
  635. confirm_required_yes_no() {{ return 0; }}
  636. env_base_flow
  637. """)
  638. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  639. encoding="utf-8"
  640. )
  641. assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose
  642. assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose
  643. assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose
  644. assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose
  645. def test_env_base_flow_preserves_existing_storage_images_on_rerun(
  646. tmp_path: Path,
  647. ) -> None:
  648. """env-base should preserve postgres and neo4j images from an existing compose rerun."""
  649. write_storage_setup_files(
  650. tmp_path,
  651. [
  652. "LLM_BINDING=openai",
  653. "EMBEDDING_BINDING=openai",
  654. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  655. "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker",
  656. ],
  657. [
  658. "services:",
  659. " lightrag:",
  660. " image: example/lightrag:test",
  661. " postgres:",
  662. " image: registry.example.com/postgres-for-rag:patched",
  663. " neo4j:",
  664. " image: registry.example.com/neo4j:custom",
  665. ],
  666. )
  667. run_bash(f"""
  668. set -euo pipefail
  669. source "{REPO_ROOT}/scripts/setup/setup.sh"
  670. REPO_ROOT="{tmp_path}"
  671. host_cuda_available() {{ return 1; }}
  672. collect_llm_config() {{ :; }}
  673. collect_embedding_config() {{ :; }}
  674. confirm_default_no() {{ return 1; }}
  675. confirm_default_yes() {{
  676. case "$1" in
  677. *"The compose file will be created/updated. Continue?"*) return 0 ;;
  678. *) return 1 ;;
  679. esac
  680. }}
  681. confirm_required_yes_no() {{ return 0; }}
  682. validate_sensitive_env_literals() {{ return 0; }}
  683. validate_mongo_vector_storage_config() {{ return 0; }}
  684. env_base_flow
  685. """)
  686. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  687. assert "image: registry.example.com/postgres-for-rag:patched" in result
  688. assert "image: registry.example.com/neo4j:custom" in result
  689. def test_env_base_flow_backs_up_legacy_generated_compose_before_rewrite(
  690. tmp_path: Path,
  691. ) -> None:
  692. """env-base should back up the active legacy compose file before regenerating final output."""
  693. legacy_compose = (
  694. "\n".join(["services:", " lightrag:", " image: prod/lightrag"]) + "\n"
  695. )
  696. write_text_lines(
  697. tmp_path / "env.example",
  698. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  699. )
  700. write_text_lines(
  701. tmp_path / ".env",
  702. [
  703. "LLM_BINDING=openai",
  704. "LLM_MODEL=gpt-4o-mini",
  705. "LLM_BINDING_HOST=https://api.openai.com/v1",
  706. "LLM_BINDING_API_KEY=sk-existing",
  707. "EMBEDDING_BINDING=openai",
  708. "EMBEDDING_MODEL=text-embedding-3-small",
  709. "EMBEDDING_DIM=1536",
  710. "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
  711. "EMBEDDING_BINDING_API_KEY=sk-existing",
  712. ],
  713. )
  714. (tmp_path / "docker-compose.production.yml").write_text(
  715. legacy_compose, encoding="utf-8"
  716. )
  717. run_bash(f"""
  718. set -euo pipefail
  719. source "{REPO_ROOT}/scripts/setup/setup.sh"
  720. REPO_ROOT="{tmp_path}"
  721. prompt_choice() {{ printf '%s' "$2"; }}
  722. prompt_with_default() {{ printf '%s' "$2"; }}
  723. prompt_until_valid() {{ printf '%s' "$2"; }}
  724. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  725. prompt_secret_until_valid_with_default() {{
  726. case "$1" in
  727. "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;;
  728. *) printf '%s' "$2" ;;
  729. esac
  730. }}
  731. confirm_default_no() {{ return 1; }}
  732. confirm_default_yes() {{
  733. case "$1" in
  734. "Run LightRAG Server via Docker?") return 0 ;;
  735. *) return 1 ;;
  736. esac
  737. }}
  738. confirm_required_yes_no() {{ return 0; }}
  739. env_base_flow
  740. """)
  741. assert_single_compose_backup(tmp_path, legacy_compose)
  742. assert (tmp_path / "docker-compose.final.yml").exists()
  743. assert (tmp_path / "docker-compose.production.yml").read_text(
  744. encoding="utf-8"
  745. ) == legacy_compose
  746. def test_env_base_flow_deletes_compose_when_switching_lightrag_to_host(
  747. tmp_path: Path,
  748. ) -> None:
  749. """env-base should back up and delete compose when no Docker services remain."""
  750. existing_compose = (
  751. "\n".join(
  752. [
  753. "services:",
  754. " lightrag:",
  755. " image: example/lightrag:test",
  756. " redis:",
  757. " image: redis:latest",
  758. ]
  759. )
  760. + "\n"
  761. )
  762. write_text_lines(
  763. tmp_path / "env.example",
  764. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  765. )
  766. write_text_lines(
  767. tmp_path / ".env",
  768. [
  769. "LIGHTRAG_RUNTIME_TARGET=compose",
  770. "LLM_BINDING=openai",
  771. "LLM_MODEL=gpt-4o-mini",
  772. "LLM_BINDING_HOST=https://api.openai.com/v1",
  773. "LLM_BINDING_API_KEY=sk-existing",
  774. "EMBEDDING_BINDING=openai",
  775. "EMBEDDING_MODEL=text-embedding-3-small",
  776. "EMBEDDING_DIM=1536",
  777. "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
  778. "EMBEDDING_BINDING_API_KEY=sk-existing",
  779. ],
  780. )
  781. (tmp_path / "docker-compose.final.yml").write_text(
  782. existing_compose, encoding="utf-8"
  783. )
  784. run_bash(f"""
  785. set -euo pipefail
  786. source "{REPO_ROOT}/scripts/setup/setup.sh"
  787. REPO_ROOT="{tmp_path}"
  788. prompt_choice() {{ printf '%s' "$2"; }}
  789. prompt_with_default() {{ printf '%s' "$2"; }}
  790. prompt_until_valid() {{ printf '%s' "$2"; }}
  791. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  792. prompt_secret_until_valid_with_default() {{
  793. case "$1" in
  794. "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;;
  795. *) printf '%s' "$2" ;;
  796. esac
  797. }}
  798. confirm_default_no() {{
  799. case "$1" in
  800. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;;
  801. *) return 1 ;;
  802. esac
  803. }}
  804. confirm_default_yes() {{ return 1; }}
  805. confirm_required_yes_no() {{ return 0; }}
  806. env_base_flow
  807. """)
  808. assert_single_compose_backup(tmp_path, existing_compose)
  809. assert not (tmp_path / "docker-compose.final.yml").exists()
  810. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  811. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  812. def test_env_base_flow_generates_env_and_compose_files(tmp_path: Path) -> None:
  813. """env-base should generate `.env` and docker-compose output for hosted and local providers."""
  814. cases = {
  815. "openai": {
  816. "prompt_choice": "prompt_choice() { printf '%s' \"$2\"; }",
  817. "prompt_secret": """
  818. prompt_secret_until_valid_with_default() {
  819. case "$1" in
  820. "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;;
  821. *) printf '%s' "$2" ;;
  822. esac
  823. }
  824. """,
  825. "env_assertions": [
  826. "LLM_BINDING=openai",
  827. "LLM_BINDING_API_KEY=sk-test-key",
  828. "EMBEDDING_BINDING_API_KEY=sk-test-key",
  829. ],
  830. },
  831. "ollama": {
  832. "prompt_choice": """
  833. prompt_choice() {
  834. case "$1" in
  835. "LLM provider") printf 'ollama' ;;
  836. "Embedding provider") printf 'ollama' ;;
  837. *) printf '%s' "$2" ;;
  838. esac
  839. }
  840. """,
  841. "prompt_secret": "prompt_secret_until_valid_with_default() { printf '%s' \"$2\"; }",
  842. "env_assertions": ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"],
  843. },
  844. }
  845. for case_name, case in cases.items():
  846. case_dir = tmp_path / case_name
  847. case_dir.mkdir()
  848. write_text_lines(
  849. case_dir / "env.example",
  850. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  851. )
  852. write_text_lines(
  853. case_dir / "docker-compose.yml",
  854. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8").splitlines(),
  855. )
  856. run_bash(f"""
  857. set -euo pipefail
  858. source "{REPO_ROOT}/scripts/setup/setup.sh"
  859. REPO_ROOT="{case_dir}"
  860. {case['prompt_choice']}
  861. prompt_with_default() {{ printf '%s' "$2"; }}
  862. prompt_until_valid() {{ printf '%s' "$2"; }}
  863. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  864. {case['prompt_secret']}
  865. confirm_default_no() {{
  866. case "$1" in
  867. "Run embedding model locally via Docker (vLLM)?") return 1 ;;
  868. "Enable reranking?") return 1 ;;
  869. "Run LightRAG Server via Docker?") return 0 ;;
  870. *) return 1 ;;
  871. esac
  872. }}
  873. confirm_default_yes() {{
  874. case "$1" in
  875. *) return 1 ;;
  876. esac
  877. }}
  878. confirm_required_yes_no() {{ return 0; }}
  879. env_base_flow
  880. """)
  881. generated_env = (case_dir / ".env").read_text(encoding="utf-8")
  882. generated_compose = (case_dir / "docker-compose.final.yml").read_text(
  883. encoding="utf-8"
  884. )
  885. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  886. assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env
  887. assert "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage" in generated_env
  888. assert "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage" in generated_env
  889. assert "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage" in generated_env
  890. for expected_line in case["env_assertions"]:
  891. assert expected_line in generated_env
  892. assert "services:" in generated_compose
  893. assert " lightrag:" in generated_compose
  894. assert "env_file:" not in generated_compose
  895. def test_env_base_flow_generates_validatable_env_on_clean_checkout(
  896. tmp_path: Path,
  897. ) -> None:
  898. """Fresh env-base output should include default storage selections and pass validation."""
  899. write_text_lines(
  900. tmp_path / "env.example",
  901. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  902. )
  903. run_bash(f"""
  904. set -euo pipefail
  905. source "{REPO_ROOT}/scripts/setup/setup.sh"
  906. REPO_ROOT="{tmp_path}"
  907. prompt_choice() {{ printf '%s' "$2"; }}
  908. prompt_with_default() {{ printf '%s' "$2"; }}
  909. prompt_until_valid() {{ printf '%s' "$2"; }}
  910. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  911. prompt_secret_until_valid_with_default() {{
  912. case "$1" in
  913. "LLM API key: "|"Embedding API key: ") printf 'sk-test-key' ;;
  914. *) printf '%s' "$2" ;;
  915. esac
  916. }}
  917. confirm_default_no() {{ return 1; }}
  918. confirm_default_yes() {{
  919. case "$1" in
  920. *) return 1 ;;
  921. esac
  922. }}
  923. confirm_required_yes_no() {{ return 0; }}
  924. env_base_flow
  925. validate_env_file
  926. """)
  927. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  928. assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env
  929. assert "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage" in generated_env
  930. assert "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage" in generated_env
  931. assert "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage" in generated_env
  932. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  933. assert "LIGHTRAG_SETUP_PROFILE=" not in generated_env
  934. def test_env_storage_flow_drops_legacy_setup_profile_on_write(tmp_path: Path) -> None:
  935. """Modular flows should not persist LIGHTRAG_SETUP_PROFILE into regenerated .env files."""
  936. write_text_lines(
  937. tmp_path / ".env",
  938. [
  939. "LIGHTRAG_SETUP_PROFILE=production",
  940. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  941. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  942. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  943. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  944. ],
  945. )
  946. write_text_lines(
  947. tmp_path / "env.example",
  948. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  949. )
  950. run_bash(f"""
  951. set -euo pipefail
  952. source "{REPO_ROOT}/scripts/setup/setup.sh"
  953. REPO_ROOT="{tmp_path}"
  954. select_storage_backends() {{
  955. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  956. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  957. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  958. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  959. }}
  960. collect_database_config() {{ :; }}
  961. validate_required_variables() {{ return 0; }}
  962. confirm_default_yes() {{ return 0; }}
  963. confirm_default_no() {{ return 1; }}
  964. confirm_required_yes_no() {{ return 0; }}
  965. env_storage_flow
  966. """)
  967. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  968. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  969. assert "LIGHTRAG_SETUP_PROFILE=" not in generated_env
  970. def test_env_base_flow_registers_vllm_rerank_service_for_docker_deployment(
  971. tmp_path: Path,
  972. ) -> None:
  973. """Choosing docker rerank in env-base should add vllm-rerank to DOCKER_SERVICE_SET."""
  974. output = run_bash(f"""
  975. set -euo pipefail
  976. source "{REPO_ROOT}/scripts/setup/setup.sh"
  977. REPO_ROOT="{tmp_path}"
  978. reset_state
  979. collect_llm_config() {{ :; }}
  980. collect_embedding_config() {{ :; }}
  981. prompt_with_default() {{ printf '%s' "$2"; }}
  982. confirm_default_no() {{
  983. case "$1" in
  984. "Run embedding model locally via Docker (vLLM)?") return 1 ;;
  985. "Enable reranking?") return 0 ;;
  986. "Run rerank service locally via Docker?") return 0 ;;
  987. *) return 1 ;;
  988. esac
  989. }}
  990. confirm_default_yes() {{ return 1; }}
  991. finalize_base_setup() {{
  992. if [[ -n "${{DOCKER_SERVICE_SET[vllm-rerank]+set}}" ]]; then
  993. printf 'HAS_VLLM_SERVICE=yes\\n'
  994. else
  995. printf 'HAS_VLLM_SERVICE=no\\n'
  996. fi
  997. }}
  998. env_base_flow
  999. """)
  1000. values = parse_lines(output)
  1001. assert values["HAS_VLLM_SERVICE"] == "yes"
  1002. def test_env_base_flow_preserves_existing_vllm_rerank_settings_on_rerun(
  1003. tmp_path: Path,
  1004. ) -> None:
  1005. """Rerunning env-base should keep saved local vLLM rerank model and port."""
  1006. write_text_lines(
  1007. tmp_path / ".env",
  1008. [
  1009. "LLM_BINDING=openai",
  1010. "LLM_MODEL=gpt-4o-mini",
  1011. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1012. "LLM_BINDING_API_KEY=sk-existing",
  1013. "RERANK_BINDING=cohere",
  1014. "RERANK_MODEL=BAAI/custom-rerank",
  1015. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  1016. "RERANK_BINDING_API_KEY=rerank-key",
  1017. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  1018. "VLLM_RERANK_MODEL=BAAI/custom-rerank",
  1019. "VLLM_RERANK_PORT=9200",
  1020. "VLLM_RERANK_DEVICE=cpu",
  1021. ],
  1022. )
  1023. values = run_bash_lines(f"""
  1024. set -euo pipefail
  1025. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1026. REPO_ROOT="{tmp_path}"
  1027. prompt_choice() {{ printf '%s' "$2"; }}
  1028. prompt_with_default() {{ printf '%s' "$2"; }}
  1029. prompt_until_valid() {{ printf '%s' "$2"; }}
  1030. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1031. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1032. confirm_default_no() {{
  1033. case "$1" in
  1034. "Enable reranking?") return 0 ;;
  1035. "Run rerank service locally via Docker?") return 0 ;;
  1036. *) return 1 ;;
  1037. esac
  1038. }}
  1039. confirm_default_yes() {{ return 1; }}
  1040. collect_embedding_config() {{ :; }}
  1041. finalize_base_setup() {{
  1042. printf 'RERANK_MODEL=%s\\n' "${{ENV_VALUES[RERANK_MODEL]}}"
  1043. printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}"
  1044. printf 'VLLM_RERANK_MODEL=%s\\n' "${{ENV_VALUES[VLLM_RERANK_MODEL]}}"
  1045. printf 'VLLM_RERANK_PORT=%s\\n' "${{ENV_VALUES[VLLM_RERANK_PORT]}}"
  1046. }}
  1047. env_base_flow
  1048. """)
  1049. assert values["RERANK_MODEL"] == "BAAI/custom-rerank"
  1050. assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank"
  1051. assert values["VLLM_RERANK_MODEL"] == "BAAI/custom-rerank"
  1052. assert values["VLLM_RERANK_PORT"] == "9200"
  1053. def test_env_base_flow_does_not_repeat_rerank_docker_prompt_when_declined(
  1054. tmp_path: Path,
  1055. ) -> None:
  1056. """Declining rerank Docker at the outer prompt should switch to endpoint-based config."""
  1057. write_text_lines(
  1058. tmp_path / ".env",
  1059. [
  1060. "LLM_BINDING=openai",
  1061. "LLM_MODEL=gpt-4o-mini",
  1062. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1063. "LLM_BINDING_API_KEY=sk-existing",
  1064. "RERANK_BINDING=cohere",
  1065. "RERANK_MODEL=BAAI/custom-rerank",
  1066. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  1067. "RERANK_BINDING_API_KEY=rerank-key",
  1068. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  1069. "VLLM_RERANK_MODEL=BAAI/custom-rerank",
  1070. "VLLM_RERANK_PORT=9200",
  1071. "VLLM_RERANK_DEVICE=cpu",
  1072. ],
  1073. )
  1074. output = run_bash(f"""
  1075. set -euo pipefail
  1076. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1077. REPO_ROOT="{tmp_path}"
  1078. DOCKER_PROMPT_COUNT=0
  1079. RERANK_MODEL_PROMPT_LOG="$REPO_ROOT/rerank-model-prompts.log"
  1080. : > "$RERANK_MODEL_PROMPT_LOG"
  1081. prompt_choice() {{
  1082. case "$1" in
  1083. "vLLM device")
  1084. echo "unexpected vLLM device prompt" >&2
  1085. return 91
  1086. ;;
  1087. *)
  1088. printf '%s' "$2"
  1089. ;;
  1090. esac
  1091. }}
  1092. prompt_with_default() {{
  1093. case "$1" in
  1094. "vLLM rerank model")
  1095. echo "unexpected vLLM rerank model prompt" >&2
  1096. return 93
  1097. ;;
  1098. "Rerank model")
  1099. printf 'hit
  1100. ' >> "$RERANK_MODEL_PROMPT_LOG"
  1101. printf '%s' "$2"
  1102. return 0
  1103. ;;
  1104. "Rerank endpoint")
  1105. printf '%s' "https://rerank.example.internal/rerank"
  1106. return 0
  1107. ;;
  1108. esac
  1109. printf '%s' "$2"
  1110. }}
  1111. prompt_until_valid() {{
  1112. case "$1" in
  1113. "vLLM rerank port")
  1114. echo "unexpected vLLM rerank port prompt" >&2
  1115. return 92
  1116. ;;
  1117. esac
  1118. printf '%s' "$2"
  1119. }}
  1120. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1121. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1122. confirm_default_no() {{ return 1; }}
  1123. confirm_default_yes() {{
  1124. case "$1" in
  1125. "Enable reranking?") return 0 ;;
  1126. "Run rerank service locally via Docker?")
  1127. DOCKER_PROMPT_COUNT=$((DOCKER_PROMPT_COUNT + 1))
  1128. return 1
  1129. ;;
  1130. *) return 1 ;;
  1131. esac
  1132. }}
  1133. collect_embedding_config() {{ :; }}
  1134. finalize_base_setup() {{
  1135. local rerank_model_prompt_count
  1136. rerank_model_prompt_count="$(wc -l < "$RERANK_MODEL_PROMPT_LOG" | tr -d '[:space:]')"
  1137. printf 'DOCKER_PROMPT_COUNT=%s\\n' "$DOCKER_PROMPT_COUNT"
  1138. printf 'RERANK_MODEL_PROMPT_COUNT=%s\\n' "$rerank_model_prompt_count"
  1139. printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}"
  1140. printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}}"
  1141. }}
  1142. env_base_flow
  1143. """)
  1144. values = parse_lines(output)
  1145. assert values["DOCKER_PROMPT_COUNT"] == "1"
  1146. assert values["RERANK_MODEL_PROMPT_COUNT"] == "1"
  1147. assert values["RERANK_BINDING_HOST"] == "https://rerank.example.internal/rerank"
  1148. assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == ""
  1149. assert "vLLM uses the Cohere-compatible rerank API." not in output
  1150. def test_env_base_flow_comments_rerank_setup_marker_when_switching_off_docker(
  1151. tmp_path: Path,
  1152. ) -> None:
  1153. """Switching rerank from Docker to a non-Docker provider should drop the setup marker."""
  1154. write_text_lines(
  1155. tmp_path / "env.example",
  1156. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  1157. )
  1158. write_text_lines(
  1159. tmp_path / ".env",
  1160. [
  1161. "LLM_BINDING=openai",
  1162. "LLM_MODEL=gpt-4o-mini",
  1163. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1164. "LLM_BINDING_API_KEY=sk-existing",
  1165. "RERANK_BINDING=cohere",
  1166. "RERANK_MODEL=BAAI/custom-rerank",
  1167. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  1168. "RERANK_BINDING_API_KEY=rerank-key",
  1169. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  1170. "VLLM_RERANK_MODEL=BAAI/custom-rerank",
  1171. "VLLM_RERANK_PORT=9200",
  1172. "VLLM_RERANK_DEVICE=cpu",
  1173. ],
  1174. )
  1175. run_bash(f"""
  1176. set -euo pipefail
  1177. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1178. REPO_ROOT="{tmp_path}"
  1179. prompt_choice() {{
  1180. case "$1" in
  1181. "Rerank provider") printf 'cohere' ;;
  1182. *) printf '%s' "$2" ;;
  1183. esac
  1184. }}
  1185. prompt_with_default() {{
  1186. case "$1" in
  1187. "Rerank endpoint") printf '%s' "https://api.cohere.com/v2/rerank" ;;
  1188. *) printf '%s' "$2" ;;
  1189. esac
  1190. }}
  1191. prompt_until_valid() {{ printf '%s' "$2"; }}
  1192. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1193. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1194. confirm_default_no() {{
  1195. case "$1" in
  1196. "Run embedding model locally via Docker (vLLM)?") return 1 ;;
  1197. "Run rerank service locally via Docker?") return 1 ;;
  1198. "Run LightRAG Server via Docker?") return 1 ;;
  1199. *) return 1 ;;
  1200. esac
  1201. }}
  1202. confirm_default_yes() {{
  1203. case "$1" in
  1204. "Enable reranking?") return 0 ;;
  1205. *) return 1 ;;
  1206. esac
  1207. }}
  1208. confirm_required_yes_no() {{ return 0; }}
  1209. env_base_flow
  1210. """)
  1211. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1212. active_marker_lines = [
  1213. line
  1214. for line in generated_env.splitlines()
  1215. if line.startswith("LIGHTRAG_SETUP_RERANK_PROVIDER=")
  1216. ]
  1217. assert "RERANK_BINDING=cohere" in generated_env
  1218. assert active_marker_lines == []
  1219. def test_env_base_flow_resets_remote_rerank_host_when_switching_to_vllm(
  1220. tmp_path: Path,
  1221. ) -> None:
  1222. """Switching a remote reranker to local vLLM should restore localhost."""
  1223. write_text_lines(
  1224. tmp_path / ".env",
  1225. [
  1226. "LLM_BINDING=openai",
  1227. "LLM_MODEL=gpt-4o-mini",
  1228. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1229. "LLM_BINDING_API_KEY=sk-existing",
  1230. "RERANK_BINDING=jina",
  1231. "RERANK_MODEL=jina-reranker-v2-base-multilingual",
  1232. "RERANK_BINDING_HOST=https://api.jina.ai/v1/rerank",
  1233. "RERANK_BINDING_API_KEY=jina-key",
  1234. "VLLM_RERANK_PORT=9200",
  1235. ],
  1236. )
  1237. values = run_bash_lines(f"""
  1238. set -euo pipefail
  1239. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1240. REPO_ROOT="{tmp_path}"
  1241. prompt_choice() {{ printf '%s' "$2"; }}
  1242. prompt_with_default() {{ printf '%s' "$2"; }}
  1243. prompt_until_valid() {{ printf '%s' "$2"; }}
  1244. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1245. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1246. confirm_default_no() {{
  1247. case "$1" in
  1248. "Run rerank service locally via Docker?") return 0 ;;
  1249. *) return 1 ;;
  1250. esac
  1251. }}
  1252. confirm_default_yes() {{
  1253. case "$1" in
  1254. "Enable reranking?") return 0 ;;
  1255. *) return 1 ;;
  1256. esac
  1257. }}
  1258. collect_embedding_config() {{ :; }}
  1259. finalize_base_setup() {{
  1260. printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}"
  1261. printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]}}"
  1262. printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]}}"
  1263. }}
  1264. env_base_flow
  1265. """)
  1266. assert values["RERANK_BINDING"] == "cohere"
  1267. assert values["RERANK_BINDING_HOST"] == "http://localhost:9200/rerank"
  1268. assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == "vllm"
  1269. def test_env_base_flow_preserves_existing_vllm_rerank_device_on_gpu_host(
  1270. tmp_path: Path,
  1271. ) -> None:
  1272. """Saved vLLM rerank CPU/GPU mode should win over auto-detected GPU defaults."""
  1273. write_text_lines(
  1274. tmp_path / ".env",
  1275. [
  1276. "LLM_BINDING=openai",
  1277. "LLM_MODEL=gpt-4o-mini",
  1278. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1279. "LLM_BINDING_API_KEY=sk-existing",
  1280. "RERANK_BINDING=cohere",
  1281. "RERANK_MODEL=BAAI/custom-rerank",
  1282. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  1283. "RERANK_BINDING_API_KEY=rerank-key",
  1284. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  1285. "VLLM_RERANK_MODEL=BAAI/custom-rerank",
  1286. "VLLM_RERANK_PORT=9200",
  1287. "VLLM_RERANK_DEVICE=cpu",
  1288. ],
  1289. )
  1290. values = run_bash_lines(f"""
  1291. set -euo pipefail
  1292. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1293. REPO_ROOT="{tmp_path}"
  1294. nvidia-smi() {{ return 0; }}
  1295. prompt_choice() {{ printf '%s' "$2"; }}
  1296. prompt_with_default() {{ printf '%s' "$2"; }}
  1297. prompt_until_valid() {{ printf '%s' "$2"; }}
  1298. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1299. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1300. confirm_default_no() {{
  1301. case "$1" in
  1302. "Enable reranking?") return 0 ;;
  1303. *) return 1 ;;
  1304. esac
  1305. }}
  1306. confirm_default_yes() {{
  1307. case "$1" in
  1308. "Run rerank service locally via Docker?") return 0 ;;
  1309. *) return 1 ;;
  1310. esac
  1311. }}
  1312. collect_embedding_config() {{ :; }}
  1313. finalize_base_setup() {{
  1314. printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}"
  1315. }}
  1316. env_base_flow
  1317. """)
  1318. assert values["VLLM_RERANK_DEVICE"] == "cpu"
  1319. def test_env_base_flow_preserves_existing_vllm_rerank_cuda_device_on_rerun(
  1320. tmp_path: Path,
  1321. ) -> None:
  1322. """Saved vLLM rerank CUDA mode should survive env-base reruns."""
  1323. write_text_lines(
  1324. tmp_path / ".env",
  1325. [
  1326. "LLM_BINDING=openai",
  1327. "LLM_MODEL=gpt-4o-mini",
  1328. "LLM_BINDING_HOST=https://api.openai.com/v1",
  1329. "LLM_BINDING_API_KEY=sk-existing",
  1330. "RERANK_BINDING=cohere",
  1331. "RERANK_MODEL=BAAI/custom-rerank",
  1332. "RERANK_BINDING_HOST=http://localhost:9200/rerank",
  1333. "RERANK_BINDING_API_KEY=rerank-key",
  1334. "LIGHTRAG_SETUP_RERANK_PROVIDER=vllm",
  1335. "VLLM_RERANK_MODEL=BAAI/custom-rerank",
  1336. "VLLM_RERANK_PORT=9200",
  1337. "VLLM_RERANK_DEVICE=cuda",
  1338. ],
  1339. )
  1340. values = run_bash_lines(f"""
  1341. set -euo pipefail
  1342. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1343. REPO_ROOT="{tmp_path}"
  1344. nvidia-smi() {{ return 0; }}
  1345. prompt_choice() {{ printf '%s' "$2"; }}
  1346. prompt_with_default() {{ printf '%s' "$2"; }}
  1347. prompt_until_valid() {{ printf '%s' "$2"; }}
  1348. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1349. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1350. confirm_default_no() {{
  1351. case "$1" in
  1352. "Enable reranking?") return 0 ;;
  1353. *) return 1 ;;
  1354. esac
  1355. }}
  1356. confirm_default_yes() {{
  1357. case "$1" in
  1358. "Run rerank service locally via Docker?") return 0 ;;
  1359. *) return 1 ;;
  1360. esac
  1361. }}
  1362. collect_embedding_config() {{ :; }}
  1363. finalize_base_setup() {{
  1364. printf 'VLLM_RERANK_DEVICE=%s\\n' "${{ENV_VALUES[VLLM_RERANK_DEVICE]}}"
  1365. }}
  1366. env_base_flow
  1367. """)
  1368. assert values["VLLM_RERANK_DEVICE"] == "cuda"
  1369. def test_env_storage_flow_applies_selected_storage_backends(tmp_path: Path) -> None:
  1370. """env-storage should honor the selected backends without auto-applying a preset."""
  1371. write_text_lines(
  1372. tmp_path / ".env",
  1373. [
  1374. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  1375. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  1376. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  1377. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  1378. "LLM_BINDING=ollama",
  1379. "LLM_MODEL=llama3.2:latest",
  1380. "LLM_BINDING_HOST=http://localhost:11434",
  1381. "EMBEDDING_BINDING=ollama",
  1382. "EMBEDDING_MODEL=nomic-embed-text:latest",
  1383. "EMBEDDING_DIM=768",
  1384. "EMBEDDING_BINDING_HOST=http://localhost:11434",
  1385. ],
  1386. )
  1387. values = run_bash_lines(f"""
  1388. set -euo pipefail
  1389. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1390. REPO_ROOT="{tmp_path}"
  1391. select_storage_backends() {{
  1392. ENV_VALUES[LIGHTRAG_KV_STORAGE]="RedisKVStorage"
  1393. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MilvusVectorDBStorage"
  1394. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage"
  1395. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="RedisDocStatusStorage"
  1396. }}
  1397. collect_database_config() {{ :; }}
  1398. collect_docker_image_tags() {{ :; }}
  1399. finalize_storage_setup() {{
  1400. printf 'LIGHTRAG_KV_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_KV_STORAGE]}}"
  1401. printf 'LIGHTRAG_VECTOR_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]}}"
  1402. printf 'LIGHTRAG_GRAPH_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]}}"
  1403. printf 'LIGHTRAG_DOC_STATUS_STORAGE=%s\\n' "${{ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]}}"
  1404. printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}"
  1405. printf 'EMBEDDING_BINDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING]}}"
  1406. }}
  1407. env_storage_flow
  1408. """)
  1409. assert values["LIGHTRAG_KV_STORAGE"] == "RedisKVStorage"
  1410. assert values["LIGHTRAG_VECTOR_STORAGE"] == "MilvusVectorDBStorage"
  1411. assert values["LIGHTRAG_GRAPH_STORAGE"] == "Neo4JStorage"
  1412. assert values["LIGHTRAG_DOC_STATUS_STORAGE"] == "RedisDocStatusStorage"
  1413. assert values["LLM_BINDING"] == "ollama"
  1414. assert values["EMBEDDING_BINDING"] == "ollama"
  1415. def test_env_storage_flow_reuses_saved_storage_docker_default(tmp_path: Path) -> None:
  1416. """Saved storage deployment metadata should drive the next Docker prompt default."""
  1417. write_text_lines(
  1418. tmp_path / ".env",
  1419. [
  1420. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  1421. "LIGHTRAG_KV_STORAGE=PGKVStorage",
  1422. "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage",
  1423. "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage",
  1424. "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage",
  1425. ],
  1426. )
  1427. values = run_bash_lines(f"""
  1428. set -euo pipefail
  1429. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1430. REPO_ROOT="{tmp_path}"
  1431. select_storage_backends() {{
  1432. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1433. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1434. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1435. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1436. REQUIRED_DB_TYPES[postgresql]=1
  1437. }}
  1438. collect_postgres_config() {{
  1439. printf 'POSTGRES_DEFAULT_DOCKER=%s\\n' "$1"
  1440. }}
  1441. finalize_storage_setup() {{ :; }}
  1442. env_storage_flow
  1443. """)
  1444. assert values["POSTGRES_DEFAULT_DOCKER"] == "yes"
  1445. def test_env_storage_flow_writes_storage_docker_marker_for_selected_service(
  1446. tmp_path: Path,
  1447. ) -> None:
  1448. """Choosing a bundled storage service should persist its deployment marker in `.env`."""
  1449. write_text_lines(
  1450. tmp_path / ".env", ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"]
  1451. )
  1452. write_text_lines(
  1453. tmp_path / "env.example",
  1454. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  1455. )
  1456. (tmp_path / "docker-compose.yml").write_text(
  1457. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
  1458. )
  1459. run_bash(f"""
  1460. set -euo pipefail
  1461. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1462. REPO_ROOT="{tmp_path}"
  1463. select_storage_backends() {{
  1464. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1465. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1466. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1467. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1468. REQUIRED_DB_TYPES[postgresql]=1
  1469. }}
  1470. collect_postgres_config() {{
  1471. add_docker_service "postgres"
  1472. ENV_VALUES[POSTGRES_HOST]="localhost"
  1473. ENV_VALUES[POSTGRES_PORT]="5432"
  1474. ENV_VALUES[POSTGRES_USER]="lightrag"
  1475. ENV_VALUES[POSTGRES_PASSWORD]="secret"
  1476. ENV_VALUES[POSTGRES_DATABASE]="lightrag"
  1477. }}
  1478. validate_required_variables() {{ return 0; }}
  1479. validate_mongo_vector_storage_config() {{ return 0; }}
  1480. validate_sensitive_env_literals() {{ return 0; }}
  1481. confirm_default_yes() {{
  1482. case "$1" in
  1483. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  1484. *) return 0 ;;
  1485. esac
  1486. }}
  1487. confirm_default_no() {{ return 1; }}
  1488. confirm_required_yes_no() {{ return 0; }}
  1489. env_storage_flow
  1490. """)
  1491. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1492. assert any(
  1493. line == "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker"
  1494. for line in generated_env.splitlines()
  1495. )
  1496. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  1497. def test_env_storage_flow_writes_opensearch_docker_marker_for_selected_service(
  1498. tmp_path: Path,
  1499. ) -> None:
  1500. """Choosing bundled OpenSearch should persist its deployment marker in `.env`."""
  1501. write_text_lines(
  1502. tmp_path / ".env", ["LLM_BINDING=ollama", "EMBEDDING_BINDING=ollama"]
  1503. )
  1504. write_text_lines(
  1505. tmp_path / "env.example",
  1506. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  1507. )
  1508. (tmp_path / "docker-compose.yml").write_text(
  1509. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
  1510. )
  1511. run_bash(f"""
  1512. set -euo pipefail
  1513. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1514. REPO_ROOT="{tmp_path}"
  1515. select_storage_backends() {{
  1516. ENV_VALUES[LIGHTRAG_KV_STORAGE]="OpenSearchKVStorage"
  1517. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="OpenSearchVectorDBStorage"
  1518. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="OpenSearchGraphStorage"
  1519. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="OpenSearchDocStatusStorage"
  1520. REQUIRED_DB_TYPES[opensearch]=1
  1521. }}
  1522. collect_opensearch_config() {{
  1523. add_docker_service "opensearch"
  1524. ENV_VALUES[OPENSEARCH_HOSTS]="localhost:9200"
  1525. ENV_VALUES[OPENSEARCH_USER]="admin"
  1526. ENV_VALUES[OPENSEARCH_PASSWORD]="secret"
  1527. ENV_VALUES[OPENSEARCH_USE_SSL]="true"
  1528. ENV_VALUES[OPENSEARCH_VERIFY_CERTS]="false"
  1529. }}
  1530. validate_required_variables() {{ return 0; }}
  1531. validate_mongo_vector_storage_config() {{ return 0; }}
  1532. validate_sensitive_env_literals() {{ return 0; }}
  1533. confirm_default_yes() {{
  1534. case "$1" in
  1535. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  1536. *) return 0 ;;
  1537. esac
  1538. }}
  1539. confirm_default_no() {{ return 1; }}
  1540. confirm_required_yes_no() {{ return 0; }}
  1541. env_storage_flow
  1542. """)
  1543. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1544. assert any(
  1545. line == "LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT=docker"
  1546. for line in generated_env.splitlines()
  1547. )
  1548. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  1549. def test_env_storage_flow_removes_storage_docker_marker_when_switching_to_host(
  1550. tmp_path: Path,
  1551. ) -> None:
  1552. """Choosing a host-managed storage backend should clear a previously saved Docker marker."""
  1553. write_text_lines(
  1554. tmp_path / ".env",
  1555. [
  1556. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  1557. "LIGHTRAG_KV_STORAGE=PGKVStorage",
  1558. "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage",
  1559. "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage",
  1560. "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage",
  1561. ],
  1562. )
  1563. write_text_lines(
  1564. tmp_path / "env.example",
  1565. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  1566. )
  1567. run_bash(f"""
  1568. set -euo pipefail
  1569. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1570. REPO_ROOT="{tmp_path}"
  1571. select_storage_backends() {{
  1572. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1573. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1574. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1575. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1576. REQUIRED_DB_TYPES[postgresql]=1
  1577. }}
  1578. collect_postgres_config() {{
  1579. ENV_VALUES[POSTGRES_HOST]="localhost"
  1580. ENV_VALUES[POSTGRES_PORT]="5432"
  1581. ENV_VALUES[POSTGRES_USER]="lightrag"
  1582. ENV_VALUES[POSTGRES_PASSWORD]="secret"
  1583. ENV_VALUES[POSTGRES_DATABASE]="lightrag"
  1584. }}
  1585. validate_required_variables() {{ return 0; }}
  1586. validate_mongo_vector_storage_config() {{ return 0; }}
  1587. validate_sensitive_env_literals() {{ return 0; }}
  1588. confirm_default_yes() {{
  1589. case "$1" in
  1590. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  1591. *) return 0 ;;
  1592. esac
  1593. }}
  1594. confirm_default_no() {{ return 1; }}
  1595. confirm_required_yes_no() {{ return 0; }}
  1596. env_storage_flow
  1597. """)
  1598. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1599. assert not any(
  1600. line.startswith("LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=")
  1601. for line in generated_env.splitlines()
  1602. )
  1603. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  1604. def test_env_storage_flow_clears_unused_storage_docker_markers(tmp_path: Path) -> None:
  1605. """Markers for databases no longer required by the selected backends should be removed."""
  1606. write_text_lines(
  1607. tmp_path / ".env",
  1608. [
  1609. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  1610. "LIGHTRAG_KV_STORAGE=PGKVStorage",
  1611. "LIGHTRAG_VECTOR_STORAGE=PGVectorStorage",
  1612. "LIGHTRAG_GRAPH_STORAGE=PGGraphStorage",
  1613. "LIGHTRAG_DOC_STATUS_STORAGE=PGDocStatusStorage",
  1614. ],
  1615. )
  1616. write_text_lines(
  1617. tmp_path / "env.example",
  1618. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  1619. )
  1620. run_bash(f"""
  1621. set -euo pipefail
  1622. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1623. REPO_ROOT="{tmp_path}"
  1624. select_storage_backends() {{
  1625. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  1626. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  1627. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  1628. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  1629. }}
  1630. collect_database_config() {{ :; }}
  1631. validate_required_variables() {{ return 0; }}
  1632. validate_mongo_vector_storage_config() {{ return 0; }}
  1633. validate_sensitive_env_literals() {{ return 0; }}
  1634. confirm_default_yes() {{
  1635. case "$1" in
  1636. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  1637. *) return 0 ;;
  1638. esac
  1639. }}
  1640. confirm_default_no() {{ return 1; }}
  1641. confirm_required_yes_no() {{ return 0; }}
  1642. env_storage_flow
  1643. """)
  1644. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1645. assert not any(
  1646. line.startswith("LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=")
  1647. for line in generated_env.splitlines()
  1648. )
  1649. assert "LIGHTRAG_KV_STORAGE=JsonKVStorage" in generated_env
  1650. def test_env_storage_flow_generates_env_and_compose_files(tmp_path: Path) -> None:
  1651. """env-storage should write updated .env and a docker-compose.final.yml."""
  1652. env_file = tmp_path / ".env"
  1653. env_file.write_text(
  1654. "\n".join(
  1655. [
  1656. "LLM_BINDING=ollama",
  1657. "EMBEDDING_BINDING=ollama",
  1658. "AUTH_ACCOUNTS=admin:secret",
  1659. "TOKEN_SECRET=jwt-secret",
  1660. "WHITELIST_PATHS=/health",
  1661. ]
  1662. )
  1663. + "\n",
  1664. encoding="utf-8",
  1665. )
  1666. (tmp_path / "env.example").write_text(
  1667. (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8"
  1668. )
  1669. (tmp_path / "docker-compose.yml").write_text(
  1670. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
  1671. )
  1672. run_bash(f"""
  1673. set -euo pipefail
  1674. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1675. REPO_ROOT="{tmp_path}"
  1676. select_storage_backends() {{
  1677. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1678. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MilvusVectorDBStorage"
  1679. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage"
  1680. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1681. add_docker_service "postgres"
  1682. add_docker_service "neo4j"
  1683. }}
  1684. collect_database_config() {{ :; }}
  1685. collect_docker_image_tags() {{ :; }}
  1686. validate_required_variables() {{ return 0; }}
  1687. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1688. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1689. confirm_default_yes() {{
  1690. case "$1" in
  1691. *) return 1 ;;
  1692. esac
  1693. }}
  1694. confirm_default_no() {{ return 1; }}
  1695. confirm_required_yes_no() {{ return 0; }}
  1696. env_storage_flow
  1697. """)
  1698. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1699. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  1700. encoding="utf-8"
  1701. )
  1702. assert "LIGHTRAG_KV_STORAGE=PGKVStorage" in generated_env
  1703. assert "LIGHTRAG_GRAPH_STORAGE=Neo4JStorage" in generated_env
  1704. assert "LLM_BINDING=ollama" in generated_env
  1705. assert "services:" in generated_compose
  1706. assert " lightrag:" in generated_compose
  1707. assert "env_file:" not in generated_compose
  1708. def test_env_storage_flow_uses_host_defaults_for_empty_postgres_docker_credentials(
  1709. tmp_path: Path,
  1710. ) -> None:
  1711. """env-storage should write the host-mode postgres defaults when old `.env` creds are empty."""
  1712. env_file = tmp_path / ".env"
  1713. env_file.write_text(
  1714. "\n".join(
  1715. [
  1716. "LLM_BINDING=ollama",
  1717. "EMBEDDING_BINDING=ollama",
  1718. "AUTH_ACCOUNTS=admin:secret",
  1719. "TOKEN_SECRET=jwt-secret",
  1720. "WHITELIST_PATHS=/health",
  1721. "POSTGRES_USER=",
  1722. "POSTGRES_PASSWORD=",
  1723. "POSTGRES_DATABASE=",
  1724. ]
  1725. )
  1726. + "\n",
  1727. encoding="utf-8",
  1728. )
  1729. (tmp_path / "env.example").write_text(
  1730. (REPO_ROOT / "env.example").read_text(encoding="utf-8"), encoding="utf-8"
  1731. )
  1732. (tmp_path / "docker-compose.yml").write_text(
  1733. (REPO_ROOT / "docker-compose.yml").read_text(encoding="utf-8"), encoding="utf-8"
  1734. )
  1735. run_bash(
  1736. f"""
  1737. set -euo pipefail
  1738. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1739. REPO_ROOT="{tmp_path}"
  1740. PROMPT_LOG_FILE="$(mktemp)"
  1741. : > "$PROMPT_LOG_FILE"
  1742. select_storage_backends() {{
  1743. REQUIRED_DB_TYPES[postgresql]=1
  1744. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1745. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1746. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1747. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1748. }}
  1749. confirm_default_no() {{
  1750. if [[ "$1" == "Run PostgreSQL locally via Docker?" ]]; then
  1751. return 0
  1752. fi
  1753. return 1
  1754. }}
  1755. confirm_default_yes() {{ return 0; }}
  1756. confirm_required_yes_no() {{ return 0; }}
  1757. prompt_with_default() {{
  1758. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  1759. case "$1" in
  1760. "PostgreSQL host") printf 'localhost' ;;
  1761. *) printf '%s' "$2" ;;
  1762. esac
  1763. }}
  1764. prompt_secret_with_default() {{
  1765. printf 'secret:%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  1766. printf '%s' "$2"
  1767. }}
  1768. env_storage_flow
  1769. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\"
  1770. """,
  1771. cwd=tmp_path,
  1772. )
  1773. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  1774. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  1775. encoding="utf-8"
  1776. )
  1777. assert "POSTGRES_USER=rag" in generated_env
  1778. assert "POSTGRES_PASSWORD=rag" in generated_env
  1779. assert "POSTGRES_DATABASE=lightrag" in generated_env
  1780. assert 'POSTGRES_USER: "rag"' in generated_compose
  1781. assert 'POSTGRES_PASSWORD: "rag"' in generated_compose
  1782. assert 'POSTGRES_DB: "lightrag"' in generated_compose
  1783. def test_env_storage_flow_preserves_existing_postgres_image_during_rewrite(
  1784. tmp_path: Path,
  1785. ) -> None:
  1786. """Postgres env rewrites should keep an existing custom image."""
  1787. write_storage_setup_files(
  1788. tmp_path,
  1789. [
  1790. "LLM_BINDING=openai",
  1791. "EMBEDDING_BINDING=openai",
  1792. "POSTGRES_USER=rag",
  1793. "POSTGRES_PASSWORD=rag",
  1794. "POSTGRES_DATABASE=rag",
  1795. ],
  1796. [
  1797. "services:",
  1798. " lightrag:",
  1799. " image: example/lightrag:test",
  1800. " postgres:",
  1801. " image: registry.example.com/postgres-for-rag:patched",
  1802. ],
  1803. )
  1804. run_bash(f"""
  1805. set -euo pipefail
  1806. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1807. REPO_ROOT="{tmp_path}"
  1808. select_storage_backends() {{
  1809. REQUIRED_DB_TYPES[postgresql]=1
  1810. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1811. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1812. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1813. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1814. }}
  1815. collect_database_config() {{
  1816. if [[ "$1" == "postgresql" ]]; then
  1817. add_docker_service "postgres"
  1818. ENV_VALUES[POSTGRES_USER]="updated-user"
  1819. fi
  1820. }}
  1821. validate_required_variables() {{ return 0; }}
  1822. validate_mongo_vector_storage_config() {{ return 0; }}
  1823. validate_sensitive_env_literals() {{ return 0; }}
  1824. confirm_required_yes_no() {{ return 0; }}
  1825. env_storage_flow
  1826. """)
  1827. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  1828. assert "image: registry.example.com/postgres-for-rag:patched" in result
  1829. assert 'POSTGRES_USER: "updated-user"' in result
  1830. assert 'POSTGRES_DB: "rag"' in result
  1831. def test_env_storage_flow_preserves_existing_neo4j_image_during_rewrite(
  1832. tmp_path: Path,
  1833. ) -> None:
  1834. """Neo4j database rewrites should keep an existing custom image."""
  1835. write_storage_setup_files(
  1836. tmp_path,
  1837. [
  1838. "LLM_BINDING=openai",
  1839. "EMBEDDING_BINDING=openai",
  1840. "NEO4J_USERNAME=neo4j",
  1841. "NEO4J_PASSWORD=neo4j-password",
  1842. "NEO4J_DATABASE=neo4j",
  1843. ],
  1844. [
  1845. "services:",
  1846. " lightrag:",
  1847. " image: example/lightrag:test",
  1848. " neo4j:",
  1849. " image: registry.example.com/neo4j:custom",
  1850. ],
  1851. )
  1852. run_bash(f"""
  1853. set -euo pipefail
  1854. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1855. REPO_ROOT="{tmp_path}"
  1856. select_storage_backends() {{
  1857. REQUIRED_DB_TYPES[neo4j]=1
  1858. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  1859. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  1860. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage"
  1861. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  1862. }}
  1863. collect_database_config() {{
  1864. if [[ "$1" == "neo4j" ]]; then
  1865. add_docker_service "neo4j"
  1866. ENV_VALUES[NEO4J_DATABASE]="updated-database"
  1867. fi
  1868. }}
  1869. validate_required_variables() {{ return 0; }}
  1870. validate_mongo_vector_storage_config() {{ return 0; }}
  1871. validate_sensitive_env_literals() {{ return 0; }}
  1872. confirm_required_yes_no() {{ return 0; }}
  1873. env_storage_flow
  1874. """)
  1875. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  1876. assert "image: registry.example.com/neo4j:custom" in result
  1877. assert 'NEO4J_dbms_default__database: "updated-database"' in result
  1878. def test_env_storage_flow_preserves_existing_postgres_and_neo4j_images_on_rewrite(
  1879. tmp_path: Path,
  1880. ) -> None:
  1881. """Concurrent postgres and neo4j rewrites should preserve both custom images."""
  1882. write_storage_setup_files(
  1883. tmp_path,
  1884. [
  1885. "LLM_BINDING=openai",
  1886. "EMBEDDING_BINDING=openai",
  1887. "POSTGRES_USER=rag",
  1888. "POSTGRES_PASSWORD=rag",
  1889. "POSTGRES_DATABASE=rag",
  1890. "NEO4J_USERNAME=neo4j",
  1891. "NEO4J_PASSWORD=neo4j-password",
  1892. "NEO4J_DATABASE=neo4j",
  1893. ],
  1894. [
  1895. "services:",
  1896. " lightrag:",
  1897. " image: example/lightrag:test",
  1898. " postgres:",
  1899. " image: registry.example.com/postgres-for-rag:patched",
  1900. " neo4j:",
  1901. " image: registry.example.com/neo4j:custom",
  1902. ],
  1903. )
  1904. run_bash(f"""
  1905. set -euo pipefail
  1906. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1907. REPO_ROOT="{tmp_path}"
  1908. select_storage_backends() {{
  1909. REQUIRED_DB_TYPES[postgresql]=1
  1910. REQUIRED_DB_TYPES[neo4j]=1
  1911. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1912. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1913. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage"
  1914. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1915. }}
  1916. collect_database_config() {{
  1917. case "$1" in
  1918. postgresql)
  1919. add_docker_service "postgres"
  1920. ENV_VALUES[POSTGRES_USER]="updated-user"
  1921. ;;
  1922. neo4j)
  1923. add_docker_service "neo4j"
  1924. ENV_VALUES[NEO4J_DATABASE]="updated-database"
  1925. ;;
  1926. esac
  1927. }}
  1928. validate_required_variables() {{ return 0; }}
  1929. validate_mongo_vector_storage_config() {{ return 0; }}
  1930. validate_sensitive_env_literals() {{ return 0; }}
  1931. confirm_required_yes_no() {{ return 0; }}
  1932. env_storage_flow
  1933. """)
  1934. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  1935. assert "image: registry.example.com/postgres-for-rag:patched" in result
  1936. assert "image: registry.example.com/neo4j:custom" in result
  1937. assert 'POSTGRES_USER: "updated-user"' in result
  1938. assert 'NEO4J_dbms_default__database: "updated-database"' in result
  1939. def test_env_storage_flow_uses_template_image_when_existing_service_has_no_image(
  1940. tmp_path: Path,
  1941. ) -> None:
  1942. """A rewritten service without an existing image should fall back to the template image."""
  1943. write_storage_setup_files(
  1944. tmp_path,
  1945. [
  1946. "LLM_BINDING=openai",
  1947. "EMBEDDING_BINDING=openai",
  1948. "POSTGRES_USER=rag",
  1949. "POSTGRES_PASSWORD=rag",
  1950. "POSTGRES_DATABASE=rag",
  1951. ],
  1952. [
  1953. "services:",
  1954. " lightrag:",
  1955. " image: example/lightrag:test",
  1956. " postgres:",
  1957. " environment:",
  1958. ' LEGACY_SETTING: "1"',
  1959. ],
  1960. )
  1961. run_bash(f"""
  1962. set -euo pipefail
  1963. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1964. REPO_ROOT="{tmp_path}"
  1965. select_storage_backends() {{
  1966. REQUIRED_DB_TYPES[postgresql]=1
  1967. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  1968. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  1969. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="PGGraphStorage"
  1970. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  1971. }}
  1972. collect_database_config() {{
  1973. if [[ "$1" == "postgresql" ]]; then
  1974. add_docker_service "postgres"
  1975. ENV_VALUES[POSTGRES_USER]="updated-user"
  1976. fi
  1977. }}
  1978. validate_required_variables() {{ return 0; }}
  1979. validate_mongo_vector_storage_config() {{ return 0; }}
  1980. validate_sensitive_env_literals() {{ return 0; }}
  1981. confirm_required_yes_no() {{ return 0; }}
  1982. env_storage_flow
  1983. """)
  1984. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  1985. assert "image: gzdaniel/postgres-for-rag:pg18-age-pgvector" in result
  1986. assert 'POSTGRES_USER: "updated-user"' in result
  1987. def test_env_storage_flow_force_rewrite_drops_preserved_storage_images(
  1988. tmp_path: Path,
  1989. ) -> None:
  1990. """FORCE_REWRITE_COMPOSE should bypass preserved postgres and neo4j images."""
  1991. write_storage_setup_files(
  1992. tmp_path,
  1993. [
  1994. "LLM_BINDING=openai",
  1995. "EMBEDDING_BINDING=openai",
  1996. "POSTGRES_USER=rag",
  1997. "POSTGRES_PASSWORD=rag",
  1998. "POSTGRES_DATABASE=rag",
  1999. "NEO4J_USERNAME=neo4j",
  2000. "NEO4J_PASSWORD=neo4j-password",
  2001. "NEO4J_DATABASE=neo4j",
  2002. ],
  2003. [
  2004. "services:",
  2005. " lightrag:",
  2006. " image: example/lightrag:test",
  2007. " postgres:",
  2008. " image: registry.example.com/postgres-for-rag:patched",
  2009. " neo4j:",
  2010. " image: registry.example.com/neo4j:custom",
  2011. ],
  2012. )
  2013. run_bash(f"""
  2014. set -euo pipefail
  2015. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2016. REPO_ROOT="{tmp_path}"
  2017. FORCE_REWRITE_COMPOSE="yes"
  2018. select_storage_backends() {{
  2019. REQUIRED_DB_TYPES[postgresql]=1
  2020. REQUIRED_DB_TYPES[neo4j]=1
  2021. ENV_VALUES[LIGHTRAG_KV_STORAGE]="PGKVStorage"
  2022. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="PGVectorStorage"
  2023. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="Neo4JStorage"
  2024. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="PGDocStatusStorage"
  2025. }}
  2026. collect_database_config() {{
  2027. case "$1" in
  2028. postgresql)
  2029. add_docker_service "postgres"
  2030. ENV_VALUES[POSTGRES_USER]="updated-user"
  2031. ;;
  2032. neo4j)
  2033. add_docker_service "neo4j"
  2034. ENV_VALUES[NEO4J_DATABASE]="updated-database"
  2035. ;;
  2036. esac
  2037. }}
  2038. validate_required_variables() {{ return 0; }}
  2039. validate_mongo_vector_storage_config() {{ return 0; }}
  2040. validate_sensitive_env_literals() {{ return 0; }}
  2041. confirm_required_yes_no() {{ return 0; }}
  2042. env_storage_flow
  2043. """)
  2044. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2045. assert "image: gzdaniel/postgres-for-rag:pg18-age-pgvector" in result
  2046. assert "image: neo4j:5-community" in result
  2047. assert "registry.example.com/postgres-for-rag:patched" not in result
  2048. assert "registry.example.com/neo4j:custom" not in result
  2049. def test_env_storage_flow_backs_up_existing_compose_before_rewrite(
  2050. tmp_path: Path,
  2051. ) -> None:
  2052. """env-storage should back up the current compose file before rewriting it."""
  2053. existing_compose = (
  2054. "\n".join(
  2055. [
  2056. "services:",
  2057. " lightrag:",
  2058. " image: example/lightrag:test",
  2059. " environment:",
  2060. ' LEGACY_SETTING: "1"',
  2061. " postgres:",
  2062. " image: gzdaniel/postgres-for-rag:pg18-age-pgvector",
  2063. ]
  2064. )
  2065. + "\n"
  2066. )
  2067. write_text_lines(
  2068. tmp_path / ".env", ["LLM_BINDING=openai", "EMBEDDING_BINDING=openai"]
  2069. )
  2070. write_text_lines(
  2071. tmp_path / "env.example",
  2072. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2073. )
  2074. (tmp_path / "docker-compose.final.yml").write_text(
  2075. existing_compose, encoding="utf-8"
  2076. )
  2077. run_bash(f"""
  2078. set -euo pipefail
  2079. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2080. REPO_ROOT="{tmp_path}"
  2081. select_storage_backends() {{
  2082. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2083. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2084. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2085. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2086. }}
  2087. collect_database_config() {{ :; }}
  2088. validate_required_variables() {{ return 0; }}
  2089. validate_mongo_vector_storage_config() {{ return 0; }}
  2090. validate_sensitive_env_literals() {{ return 0; }}
  2091. confirm_default_yes() {{
  2092. case "$1" in
  2093. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  2094. *) return 0 ;;
  2095. esac
  2096. }}
  2097. confirm_default_no() {{ return 1; }}
  2098. confirm_required_yes_no() {{ return 0; }}
  2099. env_storage_flow
  2100. """)
  2101. assert_single_compose_backup(tmp_path, existing_compose)
  2102. assert (tmp_path / "docker-compose.final.yml").exists()
  2103. def test_env_storage_flow_keeps_compose_mode_for_user_sidecars(tmp_path: Path) -> None:
  2104. """env-storage should keep LightRAG in Docker when user sidecars are present."""
  2105. existing_compose = (
  2106. "\n".join(
  2107. [
  2108. "services:",
  2109. " lightrag:",
  2110. " image: example/lightrag:test",
  2111. " environment:",
  2112. ' LEGACY_SETTING: "1"',
  2113. " sidecar:",
  2114. " image: busybox",
  2115. ]
  2116. )
  2117. + "\n"
  2118. )
  2119. write_text_lines(
  2120. tmp_path / ".env", ["LLM_BINDING=openai", "EMBEDDING_BINDING=openai"]
  2121. )
  2122. write_text_lines(
  2123. tmp_path / "env.example",
  2124. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2125. )
  2126. (tmp_path / "docker-compose.final.yml").write_text(
  2127. existing_compose, encoding="utf-8"
  2128. )
  2129. run_bash(f"""
  2130. set -euo pipefail
  2131. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2132. REPO_ROOT="{tmp_path}"
  2133. select_storage_backends() {{
  2134. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2135. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2136. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2137. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2138. }}
  2139. collect_database_config() {{ :; }}
  2140. validate_required_variables() {{ return 0; }}
  2141. validate_mongo_vector_storage_config() {{ return 0; }}
  2142. validate_sensitive_env_literals() {{ return 0; }}
  2143. confirm_default_yes() {{ return 0; }}
  2144. confirm_default_no() {{ return 1; }}
  2145. confirm_required_yes_no() {{ return 0; }}
  2146. env_storage_flow
  2147. """)
  2148. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2149. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2150. assert_single_compose_backup(tmp_path, existing_compose)
  2151. assert " lightrag:" in result
  2152. assert " sidecar:" in result
  2153. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  2154. def test_env_storage_flow_preserves_mongodb_docker_marker_for_atlas_local_vector_storage(
  2155. tmp_path: Path,
  2156. ) -> None:
  2157. """MongoDB Atlas Local vector storage should preserve the bundled Docker deployment marker."""
  2158. write_text_lines(
  2159. tmp_path / ".env",
  2160. [
  2161. "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker",
  2162. "LIGHTRAG_KV_STORAGE=MongoKVStorage",
  2163. "LIGHTRAG_VECTOR_STORAGE=MongoVectorDBStorage",
  2164. "LIGHTRAG_GRAPH_STORAGE=MongoGraphStorage",
  2165. "LIGHTRAG_DOC_STATUS_STORAGE=MongoDocStatusStorage",
  2166. ],
  2167. )
  2168. write_text_lines(
  2169. tmp_path / "env.example",
  2170. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2171. )
  2172. run_bash(f"""
  2173. set -euo pipefail
  2174. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2175. REPO_ROOT="{tmp_path}"
  2176. select_storage_backends() {{
  2177. ENV_VALUES[LIGHTRAG_KV_STORAGE]="MongoKVStorage"
  2178. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MongoVectorDBStorage"
  2179. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="MongoGraphStorage"
  2180. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="MongoDocStatusStorage"
  2181. REQUIRED_DB_TYPES[mongodb]=1
  2182. }}
  2183. prompt_until_valid() {{ printf '%s' "$2"; }}
  2184. prompt_with_default() {{ printf '%s' "$2"; }}
  2185. validate_required_variables() {{ return 0; }}
  2186. validate_mongo_vector_storage_config() {{ return 0; }}
  2187. validate_sensitive_env_literals() {{ return 0; }}
  2188. confirm_default_yes() {{ return 0; }}
  2189. confirm_default_no() {{ return 1; }}
  2190. confirm_required_yes_no() {{ return 0; }}
  2191. env_storage_flow
  2192. """)
  2193. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2194. assert "LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker" in generated_env
  2195. assert "MONGO_URI=mongodb://localhost:27017/?directConnection=true" in generated_env
  2196. def test_env_storage_flow_preserves_existing_compose_ssl_when_env_paths_are_stale(
  2197. tmp_path: Path,
  2198. ) -> None:
  2199. """env-storage should keep compose SSL wiring when inherited source paths no longer exist."""
  2200. write_text_lines(
  2201. tmp_path / ".env",
  2202. [
  2203. "SSL=true",
  2204. "SSL_CERTFILE=/missing/cert.pem",
  2205. "SSL_KEYFILE=/missing/key.pem",
  2206. "LLM_BINDING=openai",
  2207. "EMBEDDING_BINDING=openai",
  2208. "LIGHTRAG_KV_STORAGE=JsonKVStorage",
  2209. "LIGHTRAG_VECTOR_STORAGE=NanoVectorDBStorage",
  2210. "LIGHTRAG_GRAPH_STORAGE=NetworkXStorage",
  2211. "LIGHTRAG_DOC_STATUS_STORAGE=JsonDocStatusStorage",
  2212. ],
  2213. )
  2214. write_text_lines(
  2215. tmp_path / "env.example",
  2216. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2217. )
  2218. write_text_lines(
  2219. tmp_path / "docker-compose.final.yml",
  2220. [
  2221. "services:",
  2222. " lightrag:",
  2223. " image: example/lightrag:test",
  2224. " environment:",
  2225. ' SSL_CERTFILE: "/app/data/certs/cert.pem"',
  2226. ' SSL_KEYFILE: "/app/data/certs/key.pem"',
  2227. " volumes:",
  2228. ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"',
  2229. ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"',
  2230. ],
  2231. )
  2232. run_bash(f"""
  2233. set -euo pipefail
  2234. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2235. REPO_ROOT="{tmp_path}"
  2236. select_storage_backends() {{
  2237. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2238. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2239. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2240. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2241. }}
  2242. collect_database_config() {{ :; }}
  2243. validate_required_variables() {{ return 0; }}
  2244. confirm_default_yes() {{
  2245. case "$1" in
  2246. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  2247. *) return 0 ;;
  2248. esac
  2249. }}
  2250. confirm_default_no() {{ return 1; }}
  2251. confirm_required_yes_no() {{ return 0; }}
  2252. env_storage_flow
  2253. """)
  2254. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  2255. encoding="utf-8"
  2256. )
  2257. assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose
  2258. assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose
  2259. assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose
  2260. assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose
  2261. def test_env_server_flow_preserves_existing_compose_ssl_when_env_paths_are_stale(
  2262. tmp_path: Path,
  2263. ) -> None:
  2264. """env-server should keep compose SSL wiring and variable-based port publishing."""
  2265. write_text_lines(
  2266. tmp_path / ".env",
  2267. [
  2268. "SSL=true",
  2269. "SSL_CERTFILE=/missing/cert.pem",
  2270. "SSL_KEYFILE=/missing/key.pem",
  2271. "HOST=0.0.0.0",
  2272. "PORT=9621",
  2273. ],
  2274. )
  2275. write_text_lines(
  2276. tmp_path / "env.example",
  2277. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2278. )
  2279. write_text_lines(
  2280. tmp_path / "docker-compose.final.yml",
  2281. [
  2282. "services:",
  2283. " lightrag:",
  2284. " image: example/lightrag:test",
  2285. " environment:",
  2286. ' SSL_CERTFILE: "/app/data/certs/cert.pem"',
  2287. ' SSL_KEYFILE: "/app/data/certs/key.pem"',
  2288. " volumes:",
  2289. ' - "./data/certs/cert.pem:/app/data/certs/cert.pem:ro"',
  2290. ' - "./data/certs/key.pem:/app/data/certs/key.pem:ro"',
  2291. ],
  2292. )
  2293. run_bash(f"""
  2294. set -euo pipefail
  2295. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2296. REPO_ROOT="{tmp_path}"
  2297. collect_server_config() {{
  2298. ENV_VALUES[HOST]="0.0.0.0"
  2299. ENV_VALUES[PORT]="8080"
  2300. }}
  2301. collect_security_config() {{ :; }}
  2302. collect_ssl_config() {{ :; }}
  2303. confirm_default_yes() {{
  2304. case "$1" in
  2305. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  2306. *) return 0 ;;
  2307. esac
  2308. }}
  2309. confirm_required_yes_no() {{ return 0; }}
  2310. env_server_flow
  2311. """)
  2312. generated_compose = (tmp_path / "docker-compose.final.yml").read_text(
  2313. encoding="utf-8"
  2314. )
  2315. assert 'SSL_CERTFILE: "/app/data/certs/cert.pem"' in generated_compose
  2316. assert 'SSL_KEYFILE: "/app/data/certs/key.pem"' in generated_compose
  2317. assert "./data/certs/cert.pem:/app/data/certs/cert.pem:ro" in generated_compose
  2318. assert "./data/certs/key.pem:/app/data/certs/key.pem:ro" in generated_compose
  2319. assert 'PORT: "9621"' in generated_compose
  2320. assert ' - "${HOST:-0.0.0.0}:${PORT:-9621}:9621"' in generated_compose
  2321. def test_env_server_flow_backs_up_existing_compose_before_rewrite(
  2322. tmp_path: Path,
  2323. ) -> None:
  2324. """env-server should back up the current compose file before rewriting it."""
  2325. existing_compose = (
  2326. "\n".join(
  2327. [
  2328. "services:",
  2329. " lightrag:",
  2330. " image: example/lightrag:test",
  2331. " environment:",
  2332. ' PORT: "9621"',
  2333. ]
  2334. )
  2335. + "\n"
  2336. )
  2337. write_text_lines(tmp_path / ".env", ["HOST=0.0.0.0", "PORT=9621"])
  2338. write_text_lines(
  2339. tmp_path / "env.example",
  2340. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2341. )
  2342. (tmp_path / "docker-compose.final.yml").write_text(
  2343. existing_compose, encoding="utf-8"
  2344. )
  2345. run_bash(f"""
  2346. set -euo pipefail
  2347. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2348. REPO_ROOT="{tmp_path}"
  2349. collect_server_config() {{
  2350. ENV_VALUES[HOST]="0.0.0.0"
  2351. ENV_VALUES[PORT]="8080"
  2352. }}
  2353. collect_security_config() {{ :; }}
  2354. collect_ssl_config() {{ :; }}
  2355. validate_sensitive_env_literals() {{ return 0; }}
  2356. validate_security_config() {{ return 0; }}
  2357. confirm_default_yes() {{
  2358. case "$1" in
  2359. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  2360. *) return 0 ;;
  2361. esac
  2362. }}
  2363. confirm_required_yes_no() {{ return 0; }}
  2364. env_server_flow
  2365. """)
  2366. assert_single_compose_backup(tmp_path, existing_compose)
  2367. assert (tmp_path / "docker-compose.final.yml").read_text(
  2368. encoding="utf-8"
  2369. ) != existing_compose
  2370. def test_env_storage_flow_drops_stale_vllm_services_missing_from_env_markers(
  2371. tmp_path: Path,
  2372. ) -> None:
  2373. """env-storage should remove stale vLLM services unless `.env` still marks them as Docker-managed."""
  2374. write_text_lines(
  2375. tmp_path / ".env",
  2376. [
  2377. "LIGHTRAG_RUNTIME_TARGET=compose",
  2378. "LLM_BINDING=openai",
  2379. "EMBEDDING_BINDING=openai",
  2380. "RERANK_BINDING=cohere",
  2381. "LIGHTRAG_SETUP_RERANK_PROVIDER=cohere",
  2382. ],
  2383. )
  2384. write_text_lines(
  2385. tmp_path / "env.example",
  2386. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2387. )
  2388. (tmp_path / "docker-compose.final.yml").write_text(
  2389. "\n".join(
  2390. [
  2391. "services:",
  2392. " lightrag:",
  2393. " image: example/lightrag:test",
  2394. " vllm-embed:",
  2395. " image: vllm/vllm-openai:latest",
  2396. " vllm-rerank:",
  2397. " image: vllm/vllm-openai:latest",
  2398. "volumes:",
  2399. " vllm_embed_cache:",
  2400. " vllm_rerank_cache:",
  2401. ]
  2402. )
  2403. + "\n",
  2404. encoding="utf-8",
  2405. )
  2406. run_bash(f"""
  2407. set -euo pipefail
  2408. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2409. REPO_ROOT="{tmp_path}"
  2410. select_storage_backends() {{
  2411. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2412. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2413. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2414. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2415. }}
  2416. collect_database_config() {{ :; }}
  2417. validate_required_variables() {{ return 0; }}
  2418. validate_mongo_vector_storage_config() {{ return 0; }}
  2419. validate_sensitive_env_literals() {{ return 0; }}
  2420. confirm_default_yes() {{ return 1; }}
  2421. confirm_default_no() {{ return 1; }}
  2422. confirm_required_yes_no() {{ return 0; }}
  2423. env_storage_flow
  2424. """)
  2425. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2426. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2427. assert " vllm-embed:" not in result
  2428. assert " vllm-rerank:" not in result
  2429. assert "vllm_embed_cache:" not in result
  2430. assert "vllm_rerank_cache:" not in result
  2431. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  2432. def test_env_storage_flow_preserves_vllm_services_marked_in_env(tmp_path: Path) -> None:
  2433. """env-storage should restore vLLM services from `.env` markers even without old compose entries."""
  2434. write_text_lines(
  2435. tmp_path / ".env",
  2436. [
  2437. "LIGHTRAG_RUNTIME_TARGET=compose",
  2438. "LLM_BINDING=openai",
  2439. "EMBEDDING_BINDING=openai",
  2440. "EMBEDDING_BINDING_HOST=http://localhost:8001/v1",
  2441. "LIGHTRAG_SETUP_EMBEDDING_PROVIDER=vllm",
  2442. "VLLM_EMBED_MODEL=BAAI/bge-m3",
  2443. "VLLM_EMBED_PORT=8001",
  2444. "VLLM_EMBED_DEVICE=cpu",
  2445. ],
  2446. )
  2447. write_text_lines(
  2448. tmp_path / "env.example",
  2449. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2450. )
  2451. write_text_lines(
  2452. tmp_path / "docker-compose.final.yml",
  2453. ["services:", " lightrag:", " image: example/lightrag:test"],
  2454. )
  2455. run_bash(f"""
  2456. set -euo pipefail
  2457. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2458. REPO_ROOT="{tmp_path}"
  2459. select_storage_backends() {{
  2460. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2461. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2462. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2463. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2464. }}
  2465. collect_database_config() {{ :; }}
  2466. validate_required_variables() {{ return 0; }}
  2467. validate_mongo_vector_storage_config() {{ return 0; }}
  2468. validate_sensitive_env_literals() {{ return 0; }}
  2469. confirm_default_yes() {{ return 1; }}
  2470. confirm_default_no() {{ return 1; }}
  2471. confirm_required_yes_no() {{ return 0; }}
  2472. env_storage_flow
  2473. """)
  2474. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2475. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2476. assert " vllm-embed:" in result
  2477. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  2478. def test_env_storage_flow_deletes_compose_when_switching_lightrag_to_host(
  2479. tmp_path: Path,
  2480. ) -> None:
  2481. """env-storage should back up and delete compose when no Docker services remain."""
  2482. existing_compose = (
  2483. "\n".join(
  2484. [
  2485. "services:",
  2486. " lightrag:",
  2487. " image: example/lightrag:test",
  2488. " redis:",
  2489. " image: redis:latest",
  2490. ]
  2491. )
  2492. + "\n"
  2493. )
  2494. write_text_lines(
  2495. tmp_path / ".env",
  2496. [
  2497. "LIGHTRAG_RUNTIME_TARGET=compose",
  2498. "LLM_BINDING=openai",
  2499. "EMBEDDING_BINDING=openai",
  2500. ],
  2501. )
  2502. write_text_lines(
  2503. tmp_path / "env.example",
  2504. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2505. )
  2506. (tmp_path / "docker-compose.final.yml").write_text(
  2507. existing_compose, encoding="utf-8"
  2508. )
  2509. run_bash(f"""
  2510. set -euo pipefail
  2511. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2512. REPO_ROOT="{tmp_path}"
  2513. select_storage_backends() {{
  2514. ENV_VALUES[LIGHTRAG_KV_STORAGE]="JsonKVStorage"
  2515. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="NanoVectorDBStorage"
  2516. ENV_VALUES[LIGHTRAG_GRAPH_STORAGE]="NetworkXStorage"
  2517. ENV_VALUES[LIGHTRAG_DOC_STATUS_STORAGE]="JsonDocStatusStorage"
  2518. }}
  2519. collect_database_config() {{ :; }}
  2520. validate_required_variables() {{ return 0; }}
  2521. validate_mongo_vector_storage_config() {{ return 0; }}
  2522. validate_sensitive_env_literals() {{ return 0; }}
  2523. confirm_default_yes() {{ return 1; }}
  2524. confirm_default_no() {{
  2525. case "$1" in
  2526. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;;
  2527. *) return 1 ;;
  2528. esac
  2529. }}
  2530. confirm_required_yes_no() {{ return 0; }}
  2531. env_storage_flow
  2532. """)
  2533. assert_single_compose_backup(tmp_path, existing_compose)
  2534. assert not (tmp_path / "docker-compose.final.yml").exists()
  2535. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2536. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  2537. def test_env_server_flow_preserves_existing_storage_images_on_compose_rewrite(
  2538. tmp_path: Path,
  2539. ) -> None:
  2540. """env-server should preserve postgres and neo4j images when a compose rewrite is triggered."""
  2541. original_compose_lines = [
  2542. "services:",
  2543. " lightrag:",
  2544. " image: example/lightrag:test",
  2545. " environment:",
  2546. ' PORT: "9621"',
  2547. " postgres:",
  2548. " image: registry.example.com/postgres-for-rag:patched",
  2549. " neo4j:",
  2550. " image: registry.example.com/neo4j:custom",
  2551. ]
  2552. original_compose_content = "\n".join(original_compose_lines) + "\n"
  2553. write_storage_setup_files(
  2554. tmp_path,
  2555. [
  2556. "LLM_BINDING=openai",
  2557. "EMBEDDING_BINDING=openai",
  2558. "HOST=0.0.0.0",
  2559. "PORT=9621",
  2560. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  2561. "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker",
  2562. ],
  2563. original_compose_lines,
  2564. )
  2565. run_bash(f"""
  2566. set -euo pipefail
  2567. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2568. REPO_ROOT="{tmp_path}"
  2569. collect_server_config() {{
  2570. ENV_VALUES[HOST]="0.0.0.0"
  2571. ENV_VALUES[PORT]="8080"
  2572. }}
  2573. collect_security_config() {{ :; }}
  2574. collect_ssl_config() {{ :; }}
  2575. confirm_default_yes() {{
  2576. case "$1" in
  2577. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 1 ;;
  2578. *) return 0 ;;
  2579. esac
  2580. }}
  2581. confirm_required_yes_no() {{ return 0; }}
  2582. validate_sensitive_env_literals() {{ return 0; }}
  2583. validate_auth_accounts_runtime_config() {{ return 0; }}
  2584. validate_mongo_vector_storage_config() {{ return 0; }}
  2585. env_server_flow
  2586. """)
  2587. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2588. assert_single_compose_backup(tmp_path, expected_content=original_compose_content)
  2589. assert "image: registry.example.com/postgres-for-rag:patched" in result
  2590. assert "image: registry.example.com/neo4j:custom" in result
  2591. def test_env_server_flow_preserves_existing_storage_images_on_env_only_rerun(
  2592. tmp_path: Path,
  2593. ) -> None:
  2594. """env-server write_env_only path should leave custom storage images untouched."""
  2595. write_storage_setup_files(
  2596. tmp_path,
  2597. [
  2598. "LLM_BINDING=openai",
  2599. "EMBEDDING_BINDING=openai",
  2600. "LIGHTRAG_SETUP_POSTGRES_DEPLOYMENT=docker",
  2601. "LIGHTRAG_SETUP_NEO4J_DEPLOYMENT=docker",
  2602. ],
  2603. [
  2604. "services:",
  2605. " lightrag:",
  2606. " image: example/lightrag:test",
  2607. " postgres:",
  2608. " image: registry.example.com/postgres-for-rag:patched",
  2609. " neo4j:",
  2610. " image: registry.example.com/neo4j:custom",
  2611. ],
  2612. )
  2613. run_bash(f"""
  2614. set -euo pipefail
  2615. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2616. REPO_ROOT="{tmp_path}"
  2617. collect_server_config() {{ :; }}
  2618. collect_security_config() {{ :; }}
  2619. collect_ssl_config() {{ :; }}
  2620. confirm_required_yes_no() {{ return 0; }}
  2621. validate_sensitive_env_literals() {{ return 0; }}
  2622. validate_auth_accounts_runtime_config() {{ return 0; }}
  2623. validate_mongo_vector_storage_config() {{ return 0; }}
  2624. env_server_flow
  2625. """)
  2626. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2627. assert "image: registry.example.com/postgres-for-rag:patched" in result
  2628. assert "image: registry.example.com/neo4j:custom" in result
  2629. def test_env_server_flow_deletes_compose_when_switching_lightrag_to_host(
  2630. tmp_path: Path,
  2631. ) -> None:
  2632. """env-server should back up and delete compose when no managed or sidecar services remain."""
  2633. existing_compose = (
  2634. "\n".join(
  2635. [
  2636. "services:",
  2637. " lightrag:",
  2638. " image: example/lightrag:test",
  2639. " redis:",
  2640. " image: redis:latest",
  2641. ]
  2642. )
  2643. + "\n"
  2644. )
  2645. write_text_lines(
  2646. tmp_path / ".env",
  2647. ["LIGHTRAG_RUNTIME_TARGET=compose", "HOST=0.0.0.0", "PORT=9621"],
  2648. )
  2649. write_text_lines(
  2650. tmp_path / "env.example",
  2651. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2652. )
  2653. (tmp_path / "docker-compose.final.yml").write_text(
  2654. existing_compose, encoding="utf-8"
  2655. )
  2656. run_bash(f"""
  2657. set -euo pipefail
  2658. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2659. REPO_ROOT="{tmp_path}"
  2660. collect_server_config() {{
  2661. ENV_VALUES[HOST]="0.0.0.0"
  2662. ENV_VALUES[PORT]="8080"
  2663. }}
  2664. collect_security_config() {{ :; }}
  2665. collect_ssl_config() {{ :; }}
  2666. validate_sensitive_env_literals() {{ return 0; }}
  2667. validate_security_config() {{ return 0; }}
  2668. confirm_default_yes() {{ return 1; }}
  2669. confirm_default_no() {{
  2670. case "$1" in
  2671. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;;
  2672. *) return 1 ;;
  2673. esac
  2674. }}
  2675. confirm_required_yes_no() {{ return 0; }}
  2676. env_server_flow
  2677. """)
  2678. assert_single_compose_backup(tmp_path, existing_compose)
  2679. assert not (tmp_path / "docker-compose.final.yml").exists()
  2680. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2681. assert "LIGHTRAG_RUNTIME_TARGET=host" in generated_env
  2682. def test_env_server_flow_keeps_compose_mode_for_user_sidecars(tmp_path: Path) -> None:
  2683. """env-server should keep LightRAG in Docker when compose still carries user sidecars."""
  2684. existing_compose = (
  2685. "\n".join(
  2686. [
  2687. "services:",
  2688. " lightrag:",
  2689. " image: example/lightrag:test",
  2690. " sidecar:",
  2691. " image: busybox",
  2692. ]
  2693. )
  2694. + "\n"
  2695. )
  2696. write_text_lines(
  2697. tmp_path / ".env",
  2698. ["LIGHTRAG_RUNTIME_TARGET=compose", "HOST=0.0.0.0", "PORT=9621"],
  2699. )
  2700. write_text_lines(
  2701. tmp_path / "env.example",
  2702. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2703. )
  2704. (tmp_path / "docker-compose.final.yml").write_text(
  2705. existing_compose, encoding="utf-8"
  2706. )
  2707. run_bash(f"""
  2708. set -euo pipefail
  2709. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2710. REPO_ROOT="{tmp_path}"
  2711. collect_server_config() {{
  2712. ENV_VALUES[HOST]="0.0.0.0"
  2713. ENV_VALUES[PORT]="8080"
  2714. }}
  2715. collect_security_config() {{ :; }}
  2716. collect_ssl_config() {{ :; }}
  2717. validate_sensitive_env_literals() {{ return 0; }}
  2718. validate_security_config() {{ return 0; }}
  2719. confirm_default_yes() {{ return 0; }}
  2720. confirm_required_yes_no() {{ return 0; }}
  2721. env_server_flow
  2722. """)
  2723. result = (tmp_path / "docker-compose.final.yml").read_text(encoding="utf-8")
  2724. generated_env = (tmp_path / ".env").read_text(encoding="utf-8")
  2725. assert " sidecar:" in result
  2726. assert " lightrag:" in result
  2727. assert "LIGHTRAG_RUNTIME_TARGET=compose" in generated_env
  2728. def test_env_server_flow_rejects_invalid_ssl_cert_when_switching_to_host(
  2729. tmp_path: Path,
  2730. ) -> None:
  2731. """finalize_server_setup should reject a missing SSL cert even when switching to host mode."""
  2732. existing_compose = (
  2733. "\n".join(
  2734. [
  2735. "services:",
  2736. " lightrag:",
  2737. " image: example/lightrag:test",
  2738. " redis:",
  2739. " image: redis:latest",
  2740. ]
  2741. )
  2742. + "\n"
  2743. )
  2744. write_text_lines(
  2745. tmp_path / ".env",
  2746. [
  2747. "LIGHTRAG_RUNTIME_TARGET=compose",
  2748. "HOST=0.0.0.0",
  2749. "PORT=9621",
  2750. "SSL=true",
  2751. "SSL_CERTFILE=/nonexistent/cert.pem",
  2752. "SSL_KEYFILE=/nonexistent/key.pem",
  2753. ],
  2754. )
  2755. write_text_lines(
  2756. tmp_path / "env.example",
  2757. (REPO_ROOT / "env.example").read_text(encoding="utf-8").splitlines(),
  2758. )
  2759. (tmp_path / "docker-compose.final.yml").write_text(
  2760. existing_compose, encoding="utf-8"
  2761. )
  2762. result = run_bash_process(f"""
  2763. set -euo pipefail
  2764. source "{REPO_ROOT}/scripts/setup/setup.sh"
  2765. REPO_ROOT="{tmp_path}"
  2766. collect_server_config() {{ :; }}
  2767. collect_security_config() {{ :; }}
  2768. collect_ssl_config() {{
  2769. ENV_VALUES[SSL]="true"
  2770. SSL_CERT_SOURCE_PATH="/nonexistent/cert.pem"
  2771. SSL_KEY_SOURCE_PATH="/nonexistent/key.pem"
  2772. }}
  2773. validate_sensitive_env_literals() {{ return 0; }}
  2774. validate_security_config() {{ return 0; }}
  2775. confirm_default_yes() {{ return 1; }}
  2776. confirm_default_no() {{
  2777. case "$1" in
  2778. "All wizard-managed services have been removed. Remove LightRAG from Docker and switch to host mode?") return 0 ;;
  2779. *) return 1 ;;
  2780. esac
  2781. }}
  2782. confirm_required_yes_no() {{ return 0; }}
  2783. env_server_flow
  2784. """)
  2785. assert result.returncode != 0
  2786. assert (
  2787. "Invalid SSL_CERTFILE" in result.stderr
  2788. or "Invalid SSL_CERTFILE" in result.stdout
  2789. )
  2790. assert (tmp_path / "docker-compose.final.yml").exists()
  2791. assert "LIGHTRAG_RUNTIME_TARGET=compose" in (tmp_path / ".env").read_text(
  2792. encoding="utf-8"
  2793. )