test_collect.py 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889
  1. # Regression tests for interactive setup wizard.
  2. # Classification: keep tests here when they target collect_* prompt/normalization logic for one config area before env/compose files are finalized.
  3. from __future__ import annotations
  4. from pathlib import Path
  5. import pytest
  6. from tests.setup._helpers import (
  7. REPO_ROOT,
  8. parse_lines,
  9. run_bash,
  10. run_bash_lines,
  11. write_text_lines,
  12. )
  13. pytestmark = pytest.mark.offline
  14. def test_collect_postgres_config_uses_fixed_bundled_port_and_compose_overrides() -> (
  15. None
  16. ):
  17. """Bundled PostgreSQL should use the fixed service port and compose overrides."""
  18. values = run_bash_lines(f"""
  19. set -euo pipefail
  20. source "{REPO_ROOT}/scripts/setup/setup.sh"
  21. reset_state
  22. confirm_default_yes() {{ return 0; }}
  23. prompt_with_default() {{
  24. case "$1" in
  25. "PostgreSQL host") printf 'localhost' ;;
  26. "PostgreSQL user") printf 'lightrag' ;;
  27. "PostgreSQL database") printf 'lightrag' ;;
  28. *) printf '%s' "$2" ;;
  29. esac
  30. }}
  31. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  32. mask_sensitive_input() {{ printf 'supersecret'; }}
  33. collect_postgres_config yes
  34. printf 'POSTGRES_HOST=%s\\n' "${{ENV_VALUES[POSTGRES_HOST]}}"
  35. printf 'POSTGRES_PORT=%s\\n' "${{ENV_VALUES[POSTGRES_PORT]}}"
  36. printf 'COMPOSE_POSTGRES_HOST=%s\\n' "${{COMPOSE_ENV_OVERRIDES[POSTGRES_HOST]}}"
  37. printf 'COMPOSE_POSTGRES_PORT=%s\\n' "${{COMPOSE_ENV_OVERRIDES[POSTGRES_PORT]}}"
  38. printf 'DOCKER_SERVICE=%s\\n' "${{DOCKER_SERVICES[0]}}\"
  39. """)
  40. assert values["POSTGRES_HOST"] == "localhost"
  41. assert values["POSTGRES_PORT"] == "5432"
  42. assert values["COMPOSE_POSTGRES_HOST"] == "postgres"
  43. assert values["COMPOSE_POSTGRES_PORT"] == "5432"
  44. assert values["DOCKER_SERVICE"] == "postgres"
  45. def test_collect_postgres_config_prompts_with_host_defaults_for_empty_docker_credentials() -> (
  46. None
  47. ):
  48. """Docker PostgreSQL should prompt for editable creds using the host-mode defaults."""
  49. values = run_bash_lines(f"""
  50. set -euo pipefail
  51. source "{REPO_ROOT}/scripts/setup/setup.sh"
  52. reset_state
  53. PROMPT_LOG_FILE="$(mktemp)"
  54. : > "$PROMPT_LOG_FILE"
  55. confirm_default_yes() {{ return 0; }}
  56. prompt_with_default() {{
  57. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  58. case "$1" in
  59. "PostgreSQL host") printf 'localhost' ;;
  60. *) printf '%s' "$2" ;;
  61. esac
  62. }}
  63. prompt_secret_with_default() {{
  64. printf 'secret:%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  65. printf '%s' "$2"
  66. }}
  67. ORIGINAL_ENV_VALUES[POSTGRES_USER]=""
  68. ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]=""
  69. ORIGINAL_ENV_VALUES[POSTGRES_DATABASE]=""
  70. collect_postgres_config yes
  71. printf 'POSTGRES_USER=%s\\n' "${{ENV_VALUES[POSTGRES_USER]}}"
  72. printf 'POSTGRES_PASSWORD=%s\\n' "${{ENV_VALUES[POSTGRES_PASSWORD]}}"
  73. printf 'POSTGRES_DATABASE=%s\\n' "${{ENV_VALUES[POSTGRES_DATABASE]}}"
  74. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\"
  75. """)
  76. assert values["POSTGRES_USER"] == "rag"
  77. assert values["POSTGRES_PASSWORD"] == "rag"
  78. assert values["POSTGRES_DATABASE"] == "lightrag"
  79. assert (
  80. values["PROMPT_LOG"]
  81. == "PostgreSQL host[localhost]|PostgreSQL user[rag]|secret:PostgreSQL password: [rag]|PostgreSQL database[lightrag]"
  82. )
  83. def test_collect_postgres_config_prompts_for_existing_docker_credentials() -> None:
  84. """Docker PostgreSQL should preserve editability when old `.env` creds already exist."""
  85. values = run_bash_lines(f"""
  86. set -euo pipefail
  87. source "{REPO_ROOT}/scripts/setup/setup.sh"
  88. reset_state
  89. PROMPT_LOG_FILE="$(mktemp)"
  90. : > "$PROMPT_LOG_FILE"
  91. confirm_default_yes() {{ return 0; }}
  92. prompt_with_default() {{
  93. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  94. case "$1" in
  95. "PostgreSQL host") printf 'localhost' ;;
  96. "PostgreSQL user") printf 'updated-user' ;;
  97. "PostgreSQL database") printf 'updated-db' ;;
  98. *) printf '%s' "$2" ;;
  99. esac
  100. }}
  101. prompt_secret_with_default() {{
  102. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  103. printf 'updated-password'
  104. }}
  105. ORIGINAL_ENV_VALUES[POSTGRES_USER]="existing-user"
  106. ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]="existing-password"
  107. ORIGINAL_ENV_VALUES[POSTGRES_DATABASE]="existing-db"
  108. collect_postgres_config yes
  109. printf 'POSTGRES_USER=%s\\n' "${{ENV_VALUES[POSTGRES_USER]}}"
  110. printf 'POSTGRES_PASSWORD=%s\\n' "${{ENV_VALUES[POSTGRES_PASSWORD]}}"
  111. printf 'POSTGRES_DATABASE=%s\\n' "${{ENV_VALUES[POSTGRES_DATABASE]}}"
  112. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\"
  113. """)
  114. assert values["POSTGRES_USER"] == "updated-user"
  115. assert values["POSTGRES_PASSWORD"] == "updated-password"
  116. assert values["POSTGRES_DATABASE"] == "updated-db"
  117. assert (
  118. values["PROMPT_LOG"]
  119. == "PostgreSQL host[localhost]|PostgreSQL user[existing-user]|PostgreSQL password: [existing-password]|PostgreSQL database[existing-db]"
  120. )
  121. def test_collect_postgres_config_still_prompts_for_host_credentials() -> None:
  122. """Host PostgreSQL should keep prompting even when saved creds are empty."""
  123. values = run_bash_lines(f"""
  124. set -euo pipefail
  125. source "{REPO_ROOT}/scripts/setup/setup.sh"
  126. reset_state
  127. PROMPT_LOG_FILE="$(mktemp)"
  128. : > "$PROMPT_LOG_FILE"
  129. confirm_default_no() {{ return 1; }}
  130. prompt_with_default() {{
  131. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  132. case "$1" in
  133. "PostgreSQL host") printf 'db.internal' ;;
  134. "PostgreSQL user") printf 'host-user' ;;
  135. "PostgreSQL database") printf 'host-db' ;;
  136. *) printf '%s' "$2" ;;
  137. esac
  138. }}
  139. prompt_until_valid() {{
  140. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  141. if [[ "$1" == "PostgreSQL port" ]]; then
  142. printf '6543'
  143. else
  144. printf '%s' "$2"
  145. fi
  146. }}
  147. prompt_secret_with_default() {{
  148. printf '%s[%s]\\n' "$1" "$2" >> "$PROMPT_LOG_FILE"
  149. printf 'host-password'
  150. }}
  151. ORIGINAL_ENV_VALUES[POSTGRES_USER]=""
  152. ORIGINAL_ENV_VALUES[POSTGRES_PASSWORD]=""
  153. collect_postgres_config no
  154. printf 'POSTGRES_HOST=%s\\n' "${{ENV_VALUES[POSTGRES_HOST]}}"
  155. printf 'POSTGRES_PORT=%s\\n' "${{ENV_VALUES[POSTGRES_PORT]}}"
  156. printf 'POSTGRES_USER=%s\\n' "${{ENV_VALUES[POSTGRES_USER]}}"
  157. printf 'POSTGRES_PASSWORD=%s\\n' "${{ENV_VALUES[POSTGRES_PASSWORD]}}"
  158. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\"
  159. """)
  160. assert values["POSTGRES_HOST"] == "db.internal"
  161. assert values["POSTGRES_PORT"] == "6543"
  162. assert values["POSTGRES_USER"] == "host-user"
  163. assert values["POSTGRES_PASSWORD"] == "host-password"
  164. assert (
  165. values["PROMPT_LOG"]
  166. == "PostgreSQL host[localhost]|PostgreSQL port[5432]|PostgreSQL user[rag]|PostgreSQL password: [rag]|PostgreSQL database[lightrag]"
  167. )
  168. def test_collect_server_config_includes_summary_language_last() -> None:
  169. """Server config should prompt for summary language after the WebUI fields."""
  170. values = run_bash_lines(f"""
  171. set -euo pipefail
  172. source "{REPO_ROOT}/scripts/setup/setup.sh"
  173. reset_state
  174. PROMPT_LOG_FILE="$(mktemp)"
  175. : > "$PROMPT_LOG_FILE"
  176. prompt_with_default() {{
  177. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  178. case "$1" in
  179. "Server host") printf '127.0.0.1' ;;
  180. "WebUI title") printf 'Custom KB' ;;
  181. "WebUI description") printf 'Custom description' ;;
  182. "Summary language") printf 'Chinese' ;;
  183. *) printf '%s' "$2" ;;
  184. esac
  185. }}
  186. prompt_until_valid() {{
  187. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  188. if [[ "$1" == "Server port" ]]; then
  189. printf '9630'
  190. else
  191. printf '%s' "$2"
  192. fi
  193. }}
  194. collect_server_config
  195. printf 'HOST=%s\\n' "${{ENV_VALUES[HOST]}}"
  196. printf 'PORT=%s\\n' "${{ENV_VALUES[PORT]}}"
  197. printf 'WEBUI_TITLE=%s\\n' "${{ENV_VALUES[WEBUI_TITLE]}}"
  198. printf 'WEBUI_DESCRIPTION=%s\\n' "${{ENV_VALUES[WEBUI_DESCRIPTION]}}"
  199. printf 'SUMMARY_LANGUAGE=%s\\n' "${{ENV_VALUES[SUMMARY_LANGUAGE]}}"
  200. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")\"
  201. """)
  202. assert values["HOST"] == "127.0.0.1"
  203. assert values["PORT"] == "9630"
  204. assert values["WEBUI_TITLE"] == "Custom KB"
  205. assert values["WEBUI_DESCRIPTION"] == "Custom description"
  206. assert values["SUMMARY_LANGUAGE"] == "Chinese"
  207. assert (
  208. values["PROMPT_LOG"]
  209. == "Server host|Server port|WebUI title|WebUI description|Summary language"
  210. )
  211. @pytest.mark.parametrize(
  212. ("setup_lines", "collector_call", "env_key", "expected_value"),
  213. [
  214. (
  215. [
  216. 'ENV_VALUES[POSTGRES_HOST]="db.example.com"',
  217. 'ENV_VALUES[POSTGRES_PORT]="6543"',
  218. ],
  219. "collect_postgres_config yes",
  220. "POSTGRES_HOST",
  221. "localhost",
  222. ),
  223. (
  224. [
  225. 'ENV_VALUES[POSTGRES_HOST]="db.example.com"',
  226. 'ENV_VALUES[POSTGRES_PORT]="6543"',
  227. ],
  228. "collect_postgres_config yes",
  229. "POSTGRES_PORT",
  230. "5432",
  231. ),
  232. (
  233. ['ENV_VALUES[NEO4J_URI]="neo4j+s://graph.example.com"'],
  234. "collect_neo4j_config yes",
  235. "NEO4J_URI",
  236. "neo4j://localhost:7687",
  237. ),
  238. (
  239. ['ENV_VALUES[MONGO_URI]="mongodb://mongo.example.com:27018/"'],
  240. "collect_mongodb_config yes",
  241. "MONGO_URI",
  242. "mongodb://localhost:27017/?directConnection=true",
  243. ),
  244. (
  245. ['ENV_VALUES[REDIS_URI]="redis://cache.example.com:6380/1"'],
  246. "collect_redis_config yes",
  247. "REDIS_URI",
  248. "redis://localhost:6379",
  249. ),
  250. (
  251. ['ENV_VALUES[MILVUS_URI]="http://milvus.example.com:19530"'],
  252. "collect_milvus_config yes",
  253. "MILVUS_URI",
  254. "http://localhost:19530",
  255. ),
  256. (
  257. ['ENV_VALUES[QDRANT_URL]="http://qdrant.example.com:6333"'],
  258. "collect_qdrant_config yes",
  259. "QDRANT_URL",
  260. "http://localhost:6333",
  261. ),
  262. (
  263. ['ENV_VALUES[MEMGRAPH_URI]="bolt://memgraph.example.com:7687"'],
  264. "collect_memgraph_config yes",
  265. "MEMGRAPH_URI",
  266. "bolt://localhost:7687",
  267. ),
  268. (
  269. ['ENV_VALUES[NEO4J_URI]="neo4j://localhost:7777"'],
  270. "collect_neo4j_config yes",
  271. "NEO4J_URI",
  272. "neo4j://localhost:7687",
  273. ),
  274. (
  275. ['ENV_VALUES[MILVUS_URI]="http://localhost:29530"'],
  276. "collect_milvus_config yes",
  277. "MILVUS_URI",
  278. "http://localhost:19530",
  279. ),
  280. (
  281. ['ENV_VALUES[QDRANT_URL]="http://localhost:16333"'],
  282. "collect_qdrant_config yes",
  283. "QDRANT_URL",
  284. "http://localhost:6333",
  285. ),
  286. (
  287. ['ENV_VALUES[MEMGRAPH_URI]="bolt://localhost:17687"'],
  288. "collect_memgraph_config yes",
  289. "MEMGRAPH_URI",
  290. "bolt://localhost:7687",
  291. ),
  292. ],
  293. ids=[
  294. "postgres-remote-host",
  295. "postgres-port-reset-to-bundled-default",
  296. "neo4j-remote-uri",
  297. "mongodb-remote-uri",
  298. "redis-remote-uri",
  299. "milvus-remote-uri",
  300. "qdrant-remote-uri",
  301. "memgraph-remote-uri",
  302. "neo4j-local-port",
  303. "milvus-local-port",
  304. "qdrant-local-port",
  305. "memgraph-local-port",
  306. ],
  307. )
  308. def test_collect_local_service_configs_normalize_stale_values(
  309. setup_lines: list[str], collector_call: str, env_key: str, expected_value: str
  310. ) -> None:
  311. """Bundled services should normalize stale remote or localhost endpoints on rerun."""
  312. setup_block = "\n".join(setup_lines)
  313. values = run_bash_lines(f"""
  314. set -euo pipefail
  315. source "{REPO_ROOT}/scripts/setup/setup.sh"
  316. reset_state
  317. {setup_block}
  318. confirm_default_yes() {{ return 0; }}
  319. prompt_choice() {{ printf '%s' "$2"; }}
  320. prompt_with_default() {{
  321. case "$1" in
  322. "PostgreSQL user") printf 'lightrag' ;;
  323. "PostgreSQL database") printf 'lightrag' ;;
  324. "Neo4j database") printf 'neo4j' ;;
  325. "MongoDB database") printf 'LightRAG' ;;
  326. "Milvus database name") printf 'lightrag' ;;
  327. *) printf '%s' "$2" ;;
  328. esac
  329. }}
  330. prompt_until_valid() {{ printf '%s' "$2"; }}
  331. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  332. {collector_call}
  333. printf '{env_key}=%s\\n' "${{ENV_VALUES[{env_key}]}}\"
  334. """)
  335. assert values[env_key] == expected_value
  336. def test_collect_ssl_config_can_disable_loaded_ssl_values(tmp_path: Path) -> None:
  337. """Declining SSL should clear previously loaded cert paths and staged sources."""
  338. cert_path = tmp_path / "cert.pem"
  339. cert_path.write_text("cert", encoding="utf-8")
  340. key_path = tmp_path / "key.pem"
  341. key_path.write_text("key", encoding="utf-8")
  342. env_file = tmp_path / ".env"
  343. env_file.write_text(
  344. "\n".join(["SSL=true", f"SSL_CERTFILE={cert_path}", f"SSL_KEYFILE={key_path}"])
  345. + "\n",
  346. encoding="utf-8",
  347. )
  348. output = run_bash(f"""
  349. set -euo pipefail
  350. source "{REPO_ROOT}/scripts/setup/setup.sh"
  351. REPO_ROOT="{tmp_path}"
  352. reset_state
  353. load_existing_env_if_present
  354. confirm_default_yes() {{ return 1; }}
  355. collect_ssl_config
  356. printf 'SSL_IS_SET=%s\\n' "${{ENV_VALUES[SSL]+set}}"
  357. printf 'SSL_CERTFILE_IS_SET=%s\\n' "${{ENV_VALUES[SSL_CERTFILE]+set}}"
  358. printf 'SSL_KEYFILE_IS_SET=%s\\n' "${{ENV_VALUES[SSL_KEYFILE]+set}}"
  359. printf 'SSL_CERT_SOURCE_PATH=%s\\n' "$SSL_CERT_SOURCE_PATH"
  360. printf 'SSL_KEY_SOURCE_PATH=%s\\n' "$SSL_KEY_SOURCE_PATH\"
  361. """)
  362. values = parse_lines(output)
  363. assert values["SSL_IS_SET"] == ""
  364. assert values["SSL_CERTFILE_IS_SET"] == ""
  365. assert values["SSL_KEYFILE_IS_SET"] == ""
  366. assert values["SSL_CERT_SOURCE_PATH"] == ""
  367. assert values["SSL_KEY_SOURCE_PATH"] == ""
  368. @pytest.mark.parametrize(
  369. ("collector_name", "binding_prefix", "env_lines"),
  370. [
  371. (
  372. "collect_llm_config",
  373. "LLM",
  374. [
  375. "LLM_BINDING=openai",
  376. "LLM_MODEL=gpt-4o",
  377. "LLM_BINDING_HOST=https://api.openai.com/v1",
  378. "LLM_BINDING_API_KEY=${OPENAI_API_KEY}",
  379. ],
  380. ),
  381. (
  382. "collect_embedding_config",
  383. "EMBEDDING",
  384. [
  385. "EMBEDDING_BINDING=openai",
  386. "EMBEDDING_MODEL=text-embedding-3-large",
  387. "EMBEDDING_DIM=3072",
  388. "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
  389. "EMBEDDING_BINDING_API_KEY=${OPENAI_API_KEY}",
  390. ],
  391. ),
  392. ],
  393. ids=["llm-bedrock-clears-api-key", "embedding-bedrock-clears-api-key"],
  394. )
  395. def test_collect_provider_config_clears_stale_api_key_for_bedrock(
  396. tmp_path: Path, collector_name: str, binding_prefix: str, env_lines: list[str]
  397. ) -> None:
  398. """Switching a provider to Bedrock should remove stale API-key settings."""
  399. write_text_lines(tmp_path / ".env", env_lines)
  400. output = run_bash(f"""
  401. set -euo pipefail
  402. source "{REPO_ROOT}/scripts/setup/setup.sh"
  403. REPO_ROOT="{tmp_path}"
  404. reset_state
  405. load_existing_env_if_present
  406. prompt_choice() {{ printf 'bedrock'; }}
  407. prompt_with_default() {{ printf '%s' "$2"; }}
  408. prompt_required_secret() {{ printf 'dummy-secret'; }}
  409. mask_sensitive_input() {{ printf ''; }}
  410. confirm_default_yes() {{ return 0; }}
  411. {collector_name}
  412. printf 'BINDING=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING]}}"
  413. printf 'API_KEY_SET=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_API_KEY]+set}}"
  414. if validate_sensitive_env_literals; then
  415. printf 'VALID=yes\\n'
  416. else
  417. printf 'VALID=no\\n'
  418. fi
  419. """)
  420. values = parse_lines(output)
  421. assert values["BINDING"] == "bedrock"
  422. assert values["API_KEY_SET"] == ""
  423. assert values["VALID"] == "yes"
  424. @pytest.mark.parametrize(
  425. (
  426. "collector_name",
  427. "binding_prefix",
  428. "provider_choice",
  429. "secret_stub",
  430. "expected_binding",
  431. "expected_model",
  432. "expected_host",
  433. "expected_dim",
  434. "expected_api_key_set",
  435. ),
  436. [
  437. (
  438. "collect_llm_config",
  439. "LLM",
  440. "ollama",
  441. "",
  442. "ollama",
  443. "mistral-nemo:latest",
  444. "http://localhost:11434",
  445. "",
  446. "",
  447. ),
  448. (
  449. "collect_embedding_config",
  450. "EMBEDDING",
  451. "jina",
  452. "prompt_secret_until_valid_with_default() { printf 'jina-secret-key'; }",
  453. "jina",
  454. "jina-embeddings-v4",
  455. "https://api.jina.ai/v1/embeddings",
  456. "2048",
  457. "set",
  458. ),
  459. (
  460. "collect_llm_config",
  461. "LLM",
  462. "gemini",
  463. "prompt_secret_until_valid_with_default() { printf 'gemini-secret-key'; }",
  464. "gemini",
  465. "gemini-flash-latest",
  466. "DEFAULT_GEMINI_ENDPOINT",
  467. "",
  468. "set",
  469. ),
  470. (
  471. "collect_llm_config",
  472. "LLM",
  473. "bedrock",
  474. """
  475. prompt_clearable_with_default() { printf ''; }
  476. prompt_required_secret() { return 1; }
  477. confirm_default_yes() { return 1; }
  478. """,
  479. "bedrock",
  480. "anthropic.claude-3-5-sonnet-20241022-v2:0",
  481. "DEFAULT_BEDROCK_ENDPOINT",
  482. "",
  483. "",
  484. ),
  485. (
  486. "collect_embedding_config",
  487. "EMBEDDING",
  488. "gemini",
  489. "prompt_secret_until_valid_with_default() { printf 'gemini-secret-key'; }",
  490. "gemini",
  491. "gemini-embedding-001",
  492. "DEFAULT_GEMINI_ENDPOINT",
  493. "1536",
  494. "set",
  495. ),
  496. (
  497. "collect_embedding_config",
  498. "EMBEDDING",
  499. "bedrock",
  500. """
  501. prompt_clearable_with_default() { printf ''; }
  502. prompt_required_secret() { return 1; }
  503. confirm_default_yes() { return 1; }
  504. """,
  505. "bedrock",
  506. "amazon.titan-embed-text-v2:0",
  507. "DEFAULT_BEDROCK_ENDPOINT",
  508. "1024",
  509. "",
  510. ),
  511. ],
  512. ids=[
  513. "llm-provider-defaults",
  514. "embedding-provider-defaults",
  515. "llm-gemini-sentinel-default",
  516. "llm-bedrock-sentinel-default",
  517. "embedding-gemini-sentinel-default",
  518. "embedding-bedrock-sentinel-default",
  519. ],
  520. )
  521. def test_collect_provider_config_uses_provider_specific_defaults(
  522. collector_name: str,
  523. binding_prefix: str,
  524. provider_choice: str,
  525. secret_stub: str,
  526. expected_binding: str,
  527. expected_model: str,
  528. expected_host: str,
  529. expected_dim: str,
  530. expected_api_key_set: str,
  531. ) -> None:
  532. """Fresh provider selection should pick provider-specific defaults."""
  533. output = run_bash(f"""
  534. set -euo pipefail
  535. source "{REPO_ROOT}/scripts/setup/setup.sh"
  536. reset_state
  537. prompt_choice() {{ printf '{provider_choice}'; }}
  538. prompt_with_default() {{ printf '%s' "$2"; }}
  539. {secret_stub}
  540. {collector_name}
  541. printf 'BINDING=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING]}}"
  542. printf 'MODEL=%s\\n' "${{ENV_VALUES[{binding_prefix}_MODEL]}}"
  543. printf 'HOST=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_HOST]}}"
  544. printf 'DIM=%s\\n' "${{ENV_VALUES[{binding_prefix}_DIM]:-}}"
  545. printf 'API_KEY_SET=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_API_KEY]+set}}\"
  546. """)
  547. values = parse_lines(output)
  548. assert values["BINDING"] == expected_binding
  549. assert values["MODEL"] == expected_model
  550. assert values["HOST"] == expected_host
  551. assert values["DIM"] == expected_dim
  552. assert values["API_KEY_SET"] == expected_api_key_set
  553. @pytest.mark.parametrize(
  554. (
  555. "collector_name",
  556. "binding_prefix",
  557. "env_lines",
  558. "expected_host",
  559. "expected_api_key",
  560. ),
  561. [
  562. (
  563. "collect_llm_config",
  564. "LLM",
  565. [
  566. "LLM_BINDING=gemini",
  567. "LLM_MODEL=gemini-flash-latest",
  568. "LLM_BINDING_HOST=https://generativelanguage.googleapis.com",
  569. "LLM_BINDING_API_KEY=gemini-existing-key",
  570. ],
  571. "https://generativelanguage.googleapis.com",
  572. "gemini-existing-key",
  573. ),
  574. (
  575. "collect_embedding_config",
  576. "EMBEDDING",
  577. [
  578. "EMBEDDING_BINDING=bedrock",
  579. "EMBEDDING_MODEL=amazon.titan-embed-text-v2:0",
  580. "EMBEDDING_DIM=1024",
  581. "EMBEDDING_BINDING_HOST=https://bedrock.amazonaws.com",
  582. ],
  583. "https://bedrock.amazonaws.com",
  584. "",
  585. ),
  586. ],
  587. ids=[
  588. "llm-rerun-preserves-explicit-gemini-host",
  589. "embedding-rerun-preserves-explicit-bedrock-host",
  590. ],
  591. )
  592. def test_collect_provider_config_preserves_explicit_host_on_rerun(
  593. tmp_path: Path,
  594. collector_name: str,
  595. binding_prefix: str,
  596. env_lines: list[str],
  597. expected_host: str,
  598. expected_api_key: str,
  599. ) -> None:
  600. """Reruns should keep saved explicit provider hosts instead of swapping to sentinels."""
  601. write_text_lines(tmp_path / ".env", env_lines)
  602. output = run_bash(f"""
  603. set -euo pipefail
  604. source "{REPO_ROOT}/scripts/setup/setup.sh"
  605. REPO_ROOT="{tmp_path}"
  606. reset_state
  607. load_existing_env_if_present
  608. prompt_choice() {{ printf '%s' "$2"; }}
  609. prompt_with_default() {{ printf '%s' "$2"; }}
  610. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  611. prompt_clearable_with_default() {{ printf ''; }}
  612. prompt_required_secret() {{ return 1; }}
  613. confirm_default_yes() {{ return 1; }}
  614. {collector_name}
  615. printf 'HOST=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_HOST]}}"
  616. printf 'API_KEY=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_API_KEY]:-}}\"
  617. """)
  618. values = parse_lines(output)
  619. assert values["HOST"] == expected_host
  620. assert values["API_KEY"] == expected_api_key
  621. @pytest.mark.parametrize(
  622. (
  623. "collector_name",
  624. "binding_prefix",
  625. "env_lines",
  626. "prompt_stubs",
  627. "expected_binding",
  628. "expected_model",
  629. "expected_host",
  630. "expected_dim",
  631. "expected_api_key",
  632. ),
  633. [
  634. (
  635. "collect_llm_config",
  636. "LLM",
  637. [
  638. "LLM_BINDING=openai-ollama",
  639. "LLM_MODEL=llama3.1:8b",
  640. "LLM_BINDING_HOST=http://localhost:11434/v1",
  641. "LLM_BINDING_API_KEY=sk-local-test-key",
  642. ],
  643. """
  644. prompt_with_default() { printf '%s' "$2"; }
  645. prompt_secret_until_valid_with_default() { printf '%s' "$2"; }
  646. """,
  647. "openai-ollama",
  648. "llama3.1:8b",
  649. "http://localhost:11434/v1",
  650. "",
  651. "sk-local-test-key",
  652. ),
  653. (
  654. "collect_embedding_config",
  655. "EMBEDDING",
  656. [
  657. "EMBEDDING_BINDING=lollms",
  658. "EMBEDDING_MODEL=lollms_embedding_model",
  659. "EMBEDDING_DIM=1024",
  660. "EMBEDDING_BINDING_HOST=http://localhost:9600",
  661. ],
  662. "prompt_with_default() { printf '%s' \"$2\"; }",
  663. "lollms",
  664. "lollms_embedding_model",
  665. "http://localhost:9600",
  666. "1024",
  667. "",
  668. ),
  669. ],
  670. ids=["llm-rerun-preserves-openai-ollama", "embedding-rerun-preserves-lollms"],
  671. )
  672. def test_collect_provider_config_preserves_supported_binding_on_rerun(
  673. tmp_path: Path,
  674. collector_name: str,
  675. binding_prefix: str,
  676. env_lines: list[str],
  677. prompt_stubs: str,
  678. expected_binding: str,
  679. expected_model: str,
  680. expected_host: str,
  681. expected_dim: str,
  682. expected_api_key: str,
  683. ) -> None:
  684. """Reruns should preserve supported provider bindings and their saved settings."""
  685. write_text_lines(tmp_path / ".env", env_lines)
  686. output = run_bash(f"""
  687. set -euo pipefail
  688. source "{REPO_ROOT}/scripts/setup/setup.sh"
  689. REPO_ROOT="{tmp_path}"
  690. reset_state
  691. load_existing_env_if_present
  692. {prompt_stubs}
  693. {collector_name}
  694. printf 'BINDING=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING]}}"
  695. printf 'MODEL=%s\\n' "${{ENV_VALUES[{binding_prefix}_MODEL]}}"
  696. printf 'HOST=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_HOST]}}"
  697. printf 'DIM=%s\\n' "${{ENV_VALUES[{binding_prefix}_DIM]:-}}"
  698. printf 'API_KEY=%s\\n' "${{ENV_VALUES[{binding_prefix}_BINDING_API_KEY]:-}}\"
  699. """)
  700. values = parse_lines(output)
  701. assert values["BINDING"] == expected_binding
  702. assert values["MODEL"] == expected_model
  703. assert values["HOST"] == expected_host
  704. assert values["DIM"] == expected_dim
  705. assert values["API_KEY"] == expected_api_key
  706. def test_collect_embedding_config_forces_ollama_for_openai_ollama_llm(
  707. tmp_path: Path,
  708. ) -> None:
  709. """`openai-ollama` should not preserve a conflicting embedding provider."""
  710. write_text_lines(
  711. tmp_path / ".env",
  712. [
  713. "LLM_BINDING=openai-ollama",
  714. "EMBEDDING_BINDING=openai",
  715. "EMBEDDING_MODEL=text-embedding-3-large",
  716. "EMBEDDING_DIM=3072",
  717. "EMBEDDING_BINDING_HOST=https://api.openai.com/v1",
  718. "EMBEDDING_BINDING_API_KEY=local-key",
  719. ],
  720. )
  721. output = run_bash(f"""
  722. set -euo pipefail
  723. source "{REPO_ROOT}/scripts/setup/setup.sh"
  724. REPO_ROOT="{tmp_path}"
  725. reset_state
  726. load_existing_env_if_present
  727. prompt_with_default() {{ printf '%s' "$2"; }}
  728. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  729. collect_embedding_config
  730. printf 'EMBEDDING_BINDING=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING]}}"
  731. printf 'EMBEDDING_MODEL=%s\\n' "${{ENV_VALUES[EMBEDDING_MODEL]}}"
  732. printf 'EMBEDDING_DIM=%s\\n' "${{ENV_VALUES[EMBEDDING_DIM]}}"
  733. printf 'EMBEDDING_BINDING_HOST=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_HOST]}}"
  734. printf 'EMBEDDING_BINDING_API_KEY_SET=%s\\n' "${{ENV_VALUES[EMBEDDING_BINDING_API_KEY]+set}}\"
  735. """)
  736. values = parse_lines(output)
  737. assert values["EMBEDDING_BINDING"] == "ollama"
  738. assert values["EMBEDDING_MODEL"] == "bge-m3:latest"
  739. assert values["EMBEDDING_DIM"] == "1024"
  740. assert values["EMBEDDING_BINDING_HOST"] == "http://localhost:11434"
  741. assert values["EMBEDDING_BINDING_API_KEY_SET"] == ""
  742. def test_collect_llm_config_allows_bedrock_ambient_credential_chain() -> None:
  743. """Bedrock setup should allow IAM roles, AWS profiles, or SSO without saved keys."""
  744. output = run_bash(f"""
  745. set -euo pipefail
  746. source "{REPO_ROOT}/scripts/setup/setup.sh"
  747. reset_state
  748. prompt_choice() {{ printf 'bedrock'; }}
  749. prompt_with_default() {{ printf '%s' "$2"; }}
  750. prompt_clearable_with_default() {{ printf ''; }}
  751. prompt_required_secret() {{ return 1; }}
  752. confirm_default_yes() {{ return 1; }}
  753. collect_llm_config
  754. printf 'LLM_BINDING=%s\\n' "${{ENV_VALUES[LLM_BINDING]}}"
  755. printf 'AWS_ACCESS_KEY_ID_SET=%s\\n' "${{ENV_VALUES[AWS_ACCESS_KEY_ID]+set}}"
  756. printf 'AWS_SECRET_ACCESS_KEY_SET=%s\\n' "${{ENV_VALUES[AWS_SECRET_ACCESS_KEY]+set}}"
  757. printf 'AWS_SESSION_TOKEN_SET=%s\\n' "${{ENV_VALUES[AWS_SESSION_TOKEN]+set}}"
  758. printf 'AWS_REGION_SET=%s\\n' "${{ENV_VALUES[AWS_REGION]+set}}\"
  759. """)
  760. values = parse_lines(output)
  761. assert values["LLM_BINDING"] == "bedrock"
  762. assert values["AWS_ACCESS_KEY_ID_SET"] == ""
  763. assert values["AWS_SECRET_ACCESS_KEY_SET"] == ""
  764. assert values["AWS_SESSION_TOKEN_SET"] == ""
  765. assert values["AWS_REGION_SET"] == ""
  766. def test_collect_llm_config_role_models_default_to_base_model_when_unset() -> None:
  767. """KEYWORD/QUERY_LLM_MODEL must default to the freshly-chosen LLM_MODEL when absent."""
  768. output = run_bash(f"""
  769. set -euo pipefail
  770. source "{REPO_ROOT}/scripts/setup/setup.sh"
  771. reset_state
  772. prompt_choice() {{ printf 'openai'; }}
  773. prompt_with_default() {{
  774. case "$1" in
  775. "LLM model") printf 'gpt-4o' ;;
  776. *) printf '%s' "$2" ;;
  777. esac
  778. }}
  779. prompt_secret_until_valid_with_default() {{ printf 'fake-key'; }}
  780. collect_llm_config
  781. printf 'LLM_MODEL=%s\\n' "${{ENV_VALUES[LLM_MODEL]}}"
  782. printf 'KEYWORD_LLM_MODEL=%s\\n' "${{ENV_VALUES[KEYWORD_LLM_MODEL]}}"
  783. printf 'QUERY_LLM_MODEL=%s\\n' "${{ENV_VALUES[QUERY_LLM_MODEL]}}\"
  784. """)
  785. values = parse_lines(output)
  786. assert values["LLM_MODEL"] == "gpt-4o"
  787. assert values["KEYWORD_LLM_MODEL"] == "gpt-4o"
  788. assert values["QUERY_LLM_MODEL"] == "gpt-4o"
  789. def test_collect_llm_config_role_models_reuse_existing_values(tmp_path: Path) -> None:
  790. """KEYWORD/QUERY_LLM_MODEL values already in .env survive a base wizard rerun."""
  791. write_text_lines(
  792. tmp_path / ".env",
  793. [
  794. "LLM_BINDING=openai",
  795. "LLM_MODEL=gpt-4o",
  796. "LLM_BINDING_HOST=https://api.openai.com/v1",
  797. "LLM_BINDING_API_KEY=existing-key",
  798. "KEYWORD_LLM_MODEL=custom-keyword-model",
  799. "QUERY_LLM_MODEL=custom-query-model",
  800. ],
  801. )
  802. output = run_bash(f"""
  803. set -euo pipefail
  804. source "{REPO_ROOT}/scripts/setup/setup.sh"
  805. REPO_ROOT="{tmp_path}"
  806. reset_state
  807. load_existing_env_if_present
  808. prompt_choice() {{ printf 'openai'; }}
  809. prompt_with_default() {{ printf '%s' "$2"; }}
  810. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  811. collect_llm_config
  812. printf 'LLM_MODEL=%s\\n' "${{ENV_VALUES[LLM_MODEL]}}"
  813. printf 'KEYWORD_LLM_MODEL=%s\\n' "${{ENV_VALUES[KEYWORD_LLM_MODEL]}}"
  814. printf 'QUERY_LLM_MODEL=%s\\n' "${{ENV_VALUES[QUERY_LLM_MODEL]}}\"
  815. """)
  816. values = parse_lines(output)
  817. assert values["LLM_MODEL"] == "gpt-4o"
  818. assert values["KEYWORD_LLM_MODEL"] == "custom-keyword-model"
  819. assert values["QUERY_LLM_MODEL"] == "custom-query-model"
  820. def test_collect_rerank_config_preserves_api_key_when_disabled(tmp_path: Path) -> None:
  821. """Disabling reranking should preserve credentials so they survive re-enable."""
  822. write_text_lines(
  823. tmp_path / ".env",
  824. [
  825. "RERANK_BINDING=cohere",
  826. "RERANK_MODEL=rerank-v3.5",
  827. "RERANK_BINDING_HOST=https://api.cohere.com/v1/rerank",
  828. "RERANK_BINDING_API_KEY=test-api-key-literal",
  829. ],
  830. )
  831. values = run_bash_lines(f"""
  832. set -euo pipefail
  833. source "{REPO_ROOT}/scripts/setup/setup.sh"
  834. REPO_ROOT="{tmp_path}"
  835. reset_state
  836. load_existing_env_if_present
  837. confirm_default_yes() {{ return 1; }}
  838. collect_rerank_config
  839. printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}"
  840. printf 'RERANK_BINDING_API_KEY_SET=%s\\n' "${{ENV_VALUES[RERANK_BINDING_API_KEY]+set}}"
  841. if validate_sensitive_env_literals; then
  842. printf 'VALID=yes\\n'
  843. else
  844. printf 'VALID=no\\n'
  845. fi
  846. """)
  847. assert values["RERANK_BINDING"] == "null"
  848. assert values["RERANK_BINDING_API_KEY_SET"] == "set"
  849. assert values["VALID"] == "yes"
  850. def test_collect_rerank_config_does_not_offer_vllm_provider_option() -> None:
  851. """The generic rerank provider prompt should only expose valid RERANK_BINDING values."""
  852. output = run_bash(f"""
  853. set -euo pipefail
  854. source "{REPO_ROOT}/scripts/setup/setup.sh"
  855. reset_state
  856. ENV_VALUES[RERANK_BINDING]="cohere"
  857. confirm_default_no() {{ return 0; }}
  858. prompt_choice() {{
  859. case "$1" in
  860. "Rerank provider")
  861. shift 2
  862. for option in "$@"; do
  863. if [[ "$option" == "vllm" ]]; then
  864. echo "unexpected vllm option" >&2
  865. return 91
  866. fi
  867. done
  868. printf 'cohere'
  869. ;;
  870. *)
  871. printf '%s' "$2"
  872. ;;
  873. esac
  874. }}
  875. prompt_with_default() {{ printf '%s' "$2"; }}
  876. prompt_secret_until_valid_with_default() {{ printf 'cohere-secret-123'; }}
  877. collect_rerank_config
  878. printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}\"
  879. """)
  880. values = parse_lines(output)
  881. assert values["RERANK_BINDING"] == "cohere"
  882. def test_collect_rerank_config_switching_from_vllm_clears_local_defaults() -> None:
  883. """Switching from local vLLM to hosted rerank should replace stale vLLM values with provider defaults."""
  884. output = run_bash(f"""
  885. set -euo pipefail
  886. source "{REPO_ROOT}/scripts/setup/setup.sh"
  887. reset_state
  888. ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]="vllm"
  889. ENV_VALUES[RERANK_BINDING]="cohere"
  890. ENV_VALUES[RERANK_MODEL]="BAAI/bge-reranker-v2-m3"
  891. ENV_VALUES[RERANK_BINDING_HOST]="http://localhost:8000/rerank"
  892. confirm_default_no() {{ return 0; }}
  893. prompt_choice() {{
  894. case "$1" in
  895. "Rerank provider") printf 'cohere' ;;
  896. *) printf '%s' "$2" ;;
  897. esac
  898. }}
  899. prompt_with_default() {{ printf '%s' "$2"; }}
  900. prompt_secret_until_valid_with_default() {{ printf 'cohere-secret-123'; }}
  901. collect_rerank_config
  902. printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}"
  903. printf 'LIGHTRAG_SETUP_RERANK_PROVIDER=%s\\n' "${{ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]:-}}"
  904. printf 'RERANK_MODEL=%s\\n' "${{ENV_VALUES[RERANK_MODEL]:-}}"
  905. printf 'RERANK_BINDING_HOST=%s\\n' "${{ENV_VALUES[RERANK_BINDING_HOST]:-}}\"
  906. """)
  907. values = parse_lines(output)
  908. assert values["RERANK_BINDING"] == "cohere"
  909. assert values["LIGHTRAG_SETUP_RERANK_PROVIDER"] == ""
  910. assert values["RERANK_MODEL"] != "BAAI/bge-reranker-v2-m3"
  911. assert values["RERANK_MODEL"] == "rerank-v3.5"
  912. assert "localhost:8000" not in values["RERANK_BINDING_HOST"]
  913. assert "cohere" in values["RERANK_BINDING_HOST"]
  914. def test_collect_rerank_config_ignores_vllm_marker_when_docker_is_predeclined() -> None:
  915. """A predeclined Docker path should default the provider prompt to the binding, not the setup marker."""
  916. output = run_bash(f"""
  917. set -euo pipefail
  918. source "{REPO_ROOT}/scripts/setup/setup.sh"
  919. reset_state
  920. ENV_VALUES[LIGHTRAG_SETUP_RERANK_PROVIDER]="vllm"
  921. ENV_VALUES[RERANK_BINDING]="cohere"
  922. prompt_choice() {{
  923. case "$1" in
  924. "Rerank provider")
  925. if [[ "$2" != "cohere" ]]; then
  926. echo "unexpected rerank provider default: $2" >&2
  927. return 91
  928. fi
  929. printf 'cohere'
  930. ;;
  931. *)
  932. printf '%s' "$2"
  933. ;;
  934. esac
  935. }}
  936. prompt_with_default() {{ printf '%s' "$2"; }}
  937. prompt_secret_until_valid_with_default() {{ printf 'cohere-secret-123'; }}
  938. collect_rerank_config "yes" "no"
  939. printf 'RERANK_BINDING=%s\\n' "${{ENV_VALUES[RERANK_BINDING]}}\"
  940. """)
  941. values = parse_lines(output)
  942. assert values["RERANK_BINDING"] == "cohere"
  943. def test_collect_milvus_config_defaults_to_existing_database_name() -> None:
  944. """Milvus database prompt should preserve the documented default database."""
  945. values = run_bash_lines(f"""
  946. set -euo pipefail
  947. source "{REPO_ROOT}/scripts/setup/setup.sh"
  948. reset_state
  949. confirm_default_yes() {{ return 1; }}
  950. prompt_with_default() {{
  951. printf '%s' "$2"
  952. }}
  953. prompt_until_valid() {{
  954. printf '%s' "$2"
  955. }}
  956. collect_milvus_config no
  957. printf 'MILVUS_DB_NAME=%s\\n' "${{ENV_VALUES[MILVUS_DB_NAME]}}\"
  958. """)
  959. assert values["MILVUS_DB_NAME"] == "lightrag"
  960. def test_collect_milvus_config_initializes_minio_credentials_for_local_docker(
  961. tmp_path: Path,
  962. ) -> None:
  963. """Local Docker Milvus should write default MinIO credentials when none exist yet."""
  964. env_file = tmp_path / ".env"
  965. env_example = tmp_path / "env.example"
  966. env_example.write_text((REPO_ROOT / "env.example").read_text(encoding="utf-8"))
  967. run_bash(
  968. f"""
  969. set -euo pipefail
  970. source "{REPO_ROOT}/scripts/setup/setup.sh"
  971. REPO_ROOT="{tmp_path}"
  972. reset_state
  973. confirm_default_yes() {{ return 0; }}
  974. prompt_choice() {{ printf '%s' "$2"; }}
  975. prompt_with_default() {{
  976. printf '%s' "$2"
  977. }}
  978. prompt_until_valid() {{
  979. printf '%s' "$2"
  980. }}
  981. collect_milvus_config yes
  982. generate_env_file "$REPO_ROOT/env.example" "$REPO_ROOT/.env\"
  983. """,
  984. cwd=tmp_path,
  985. )
  986. env_text = env_file.read_text(encoding="utf-8")
  987. assert "MINIO_ACCESS_KEY_ID=minioadmin" in env_text
  988. assert "MINIO_SECRET_ACCESS_KEY=minioadmin" in env_text
  989. @pytest.mark.parametrize(
  990. ("setup_lines", "nvidia_impl", "expected_device"),
  991. [
  992. (['ENV_VALUES[MILVUS_DEVICE]="cpu"'], "nvidia-smi() { return 0; }", "cpu"),
  993. (['ENV_VALUES[MILVUS_DEVICE]="cuda"'], "nvidia-smi() { return 1; }", "cuda"),
  994. ([], "nvidia-smi() { return 0; }", "cuda"),
  995. ],
  996. ids=["saved-cpu-wins", "saved-cuda-wins", "gpu-host-defaults-to-cuda"],
  997. )
  998. def test_collect_milvus_config_resolves_device_default_for_local_docker(
  999. setup_lines: list[str], nvidia_impl: str, expected_device: str
  1000. ) -> None:
  1001. """Milvus device defaults should prefer saved state and otherwise use host CUDA detection."""
  1002. setup_block = "\n".join(setup_lines)
  1003. values = run_bash_lines(f"""
  1004. set -euo pipefail
  1005. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1006. reset_state
  1007. {setup_block}
  1008. {nvidia_impl}
  1009. confirm_default_yes() {{ return 0; }}
  1010. prompt_choice() {{ printf '%s' "$2"; }}
  1011. prompt_with_default() {{ printf '%s' "$2"; }}
  1012. prompt_until_valid() {{ printf '%s' "$2"; }}
  1013. collect_milvus_config yes
  1014. printf 'MILVUS_DEVICE=%s\\n' "${{ENV_VALUES[MILVUS_DEVICE]}}\"
  1015. """)
  1016. assert values["MILVUS_DEVICE"] == expected_device
  1017. @pytest.mark.parametrize(
  1018. ("collector_call", "device_prompt", "endpoint_prompt"),
  1019. [
  1020. ("collect_milvus_config yes", "Milvus device", "Milvus URI"),
  1021. ("collect_qdrant_config yes", "Qdrant device", "Qdrant URL"),
  1022. ],
  1023. )
  1024. def test_local_vector_db_device_prompt_is_first_follow_up_after_docker_choice(
  1025. collector_call: str, device_prompt: str, endpoint_prompt: str
  1026. ) -> None:
  1027. """GPU/CPU selection should be the first prompt after choosing local Docker deployment."""
  1028. values = run_bash_lines(f"""
  1029. set -euo pipefail
  1030. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1031. reset_state
  1032. PROMPT_LOG_FILE="$(mktemp)"
  1033. : > "$PROMPT_LOG_FILE"
  1034. confirm_default_yes() {{ return 0; }}
  1035. prompt_choice() {{
  1036. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  1037. printf '%s' "$2"
  1038. }}
  1039. prompt_with_default() {{
  1040. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  1041. printf '%s' "$2"
  1042. }}
  1043. prompt_until_valid() {{
  1044. printf '%s\\n' "$1" >> "$PROMPT_LOG_FILE"
  1045. printf '%s' "$2"
  1046. }}
  1047. {collector_call}
  1048. printf 'PROMPT_LOG=%s\\n' "$(paste -sd '|' "$PROMPT_LOG_FILE")"
  1049. """)
  1050. assert values["PROMPT_LOG"].startswith(f"{device_prompt}|{endpoint_prompt}")
  1051. def test_collect_mongodb_config_local_service_strips_stale_credentials_on_rerun() -> (
  1052. None
  1053. ):
  1054. """Bundled MongoDB should keep host `.env` aligned with the unauthenticated template."""
  1055. values = run_bash_lines(f"""
  1056. set -euo pipefail
  1057. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1058. reset_state
  1059. ENV_VALUES[MONGO_URI]="mongodb://root:secret@localhost:27018/"
  1060. confirm_default_yes() {{ return 0; }}
  1061. prompt_until_valid() {{ printf '%s' "$2"; }}
  1062. prompt_with_default() {{
  1063. if [[ "$1" == "MongoDB database" ]]; then
  1064. printf 'LightRAG'
  1065. else
  1066. printf '%s' "$2"
  1067. fi
  1068. }}
  1069. collect_mongodb_config yes
  1070. printf 'MONGO_URI=%s\\n' "${{ENV_VALUES[MONGO_URI]}}"
  1071. printf 'COMPOSE_MONGO_URI=%s\\n' "${{COMPOSE_ENV_OVERRIDES[MONGO_URI]}}"
  1072. printf 'DOCKER_SERVICE=%s\\n' "${{DOCKER_SERVICES[0]}}\"
  1073. """)
  1074. assert values["MONGO_URI"] == "mongodb://localhost:27017/?directConnection=true"
  1075. assert (
  1076. values["COMPOSE_MONGO_URI"] == "mongodb://mongodb:27017/?directConnection=true"
  1077. )
  1078. assert values["DOCKER_SERVICE"] == "mongodb"
  1079. def test_collect_mongodb_config_resets_wizard_managed_local_uri_when_switching_off_docker() -> (
  1080. None
  1081. ):
  1082. """Switching off bundled MongoDB should not preserve the old wizard-managed local URI."""
  1083. values = run_bash_lines(f"""
  1084. set -euo pipefail
  1085. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1086. reset_state
  1087. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MongoVectorDBStorage"
  1088. ENV_VALUES[MONGO_URI]="mongodb://localhost:27017/?directConnection=true"
  1089. confirm_default_yes() {{ return 1; }}
  1090. prompt_until_valid() {{ printf '%s' "$2"; }}
  1091. prompt_with_default() {{ printf '%s' "$2"; }}
  1092. collect_mongodb_config yes
  1093. printf 'MONGO_URI=%s\\n' "${{ENV_VALUES[MONGO_URI]}}"
  1094. printf 'COMPOSE_MONGO_URI=%s\\n' "${{COMPOSE_ENV_OVERRIDES[MONGO_URI]-}}\"
  1095. """)
  1096. assert values["MONGO_URI"] == "mongodb+srv://cluster.example.mongodb.net/"
  1097. assert values["COMPOSE_MONGO_URI"] == ""
  1098. def test_collect_mongodb_config_preserves_external_atlas_local_uri_when_switching_off_docker() -> (
  1099. None
  1100. ):
  1101. """Switching off bundled MongoDB should keep an explicitly configured external Atlas Local URI."""
  1102. values = run_bash_lines(f"""
  1103. set -euo pipefail
  1104. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1105. reset_state
  1106. ENV_VALUES[LIGHTRAG_VECTOR_STORAGE]="MongoVectorDBStorage"
  1107. ENV_VALUES[MONGO_URI]="mongodb://atlas-local.example.com:27017/LightRAG?replicaSet=rs0&directConnection=true"
  1108. confirm_default_yes() {{ return 1; }}
  1109. prompt_until_valid() {{ printf '%s' "$2"; }}
  1110. prompt_with_default() {{ printf '%s' "$2"; }}
  1111. collect_mongodb_config yes
  1112. printf 'MONGO_URI=%s\\n' "${{ENV_VALUES[MONGO_URI]}}"
  1113. printf 'COMPOSE_MONGO_URI=%s\\n' "${{COMPOSE_ENV_OVERRIDES[MONGO_URI]-}}\"
  1114. """)
  1115. assert (
  1116. values["MONGO_URI"]
  1117. == "mongodb://atlas-local.example.com:27017/LightRAG?replicaSet=rs0&directConnection=true"
  1118. )
  1119. assert values["COMPOSE_MONGO_URI"] == ""
  1120. def test_collect_redis_config_local_service_normalizes_custom_host_port() -> None:
  1121. """Bundled Redis should keep host `.env` aligned with the published local port."""
  1122. values = run_bash_lines(f"""
  1123. set -euo pipefail
  1124. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1125. reset_state
  1126. ENV_VALUES[REDIS_URI]="redis://localhost:6380/1"
  1127. confirm_default_yes() {{ return 0; }}
  1128. prompt_until_valid() {{ printf '%s' "$2"; }}
  1129. collect_redis_config yes
  1130. printf 'REDIS_URI=%s\\n' "${{ENV_VALUES[REDIS_URI]}}"
  1131. printf 'COMPOSE_REDIS_URI=%s\\n' "${{COMPOSE_ENV_OVERRIDES[REDIS_URI]}}"
  1132. printf 'DOCKER_SERVICE=%s\\n' "${{DOCKER_SERVICES[0]}}\"
  1133. """)
  1134. assert values["REDIS_URI"] == "redis://localhost:6379/1"
  1135. assert values["COMPOSE_REDIS_URI"] == "redis://redis:6379"
  1136. assert values["DOCKER_SERVICE"] == "redis"
  1137. def test_collect_security_config_can_clear_existing_values_on_rerun(
  1138. tmp_path: Path,
  1139. ) -> None:
  1140. """Rerunning security setup should be able to remove previously saved values."""
  1141. env_file = tmp_path / ".env"
  1142. env_file.write_text(
  1143. "\n".join(
  1144. [
  1145. "AUTH_ACCOUNTS=admin:secret",
  1146. "TOKEN_SECRET=jwt-secret",
  1147. "TOKEN_EXPIRE_HOURS=72",
  1148. "LIGHTRAG_API_KEY=api-key",
  1149. "WHITELIST_PATHS=/health,/api/*,/docs",
  1150. ]
  1151. )
  1152. + "\n",
  1153. encoding="utf-8",
  1154. )
  1155. output = run_bash(f"""
  1156. set -euo pipefail
  1157. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1158. REPO_ROOT="{tmp_path}"
  1159. reset_state
  1160. load_existing_env_if_present
  1161. confirm_default_no() {{ return 0; }}
  1162. prompt_clearable_with_default() {{ printf '%s' "$CLEAR_INPUT_SENTINEL"; }}
  1163. prompt_clearable_secret_with_default() {{ printf '%s' "$CLEAR_INPUT_SENTINEL"; }}
  1164. collect_security_config yes no
  1165. generate_env_file "{REPO_ROOT}/env.example" "$REPO_ROOT/.env.generated"
  1166. printf 'AUTH_ACCOUNTS_SET=%s\\n' "${{ENV_VALUES[AUTH_ACCOUNTS]+set}}"
  1167. printf 'TOKEN_SECRET_SET=%s\\n' "${{ENV_VALUES[TOKEN_SECRET]+set}}"
  1168. printf 'TOKEN_EXPIRE_HOURS_SET=%s\\n' "${{ENV_VALUES[TOKEN_EXPIRE_HOURS]+set}}"
  1169. printf 'LIGHTRAG_API_KEY_SET=%s\\n' "${{ENV_VALUES[LIGHTRAG_API_KEY]+set}}"
  1170. printf 'WHITELIST_PATHS_SET=%s\\n' "${{ENV_VALUES[WHITELIST_PATHS]+set}}\"
  1171. """)
  1172. values = parse_lines(output)
  1173. generated_lines = (
  1174. (tmp_path / ".env.generated").read_text(encoding="utf-8").splitlines()
  1175. )
  1176. assert values["AUTH_ACCOUNTS_SET"] == ""
  1177. assert values["TOKEN_SECRET_SET"] == ""
  1178. assert values["TOKEN_EXPIRE_HOURS_SET"] == ""
  1179. assert values["LIGHTRAG_API_KEY_SET"] == ""
  1180. assert values["WHITELIST_PATHS_SET"] == "set"
  1181. assert not any(line.startswith("AUTH_ACCOUNTS=") for line in generated_lines)
  1182. assert not any(line.startswith("TOKEN_SECRET=") for line in generated_lines)
  1183. assert not any(line.startswith("TOKEN_EXPIRE_HOURS=") for line in generated_lines)
  1184. assert not any(line.startswith("LIGHTRAG_API_KEY=") for line in generated_lines)
  1185. assert "WHITELIST_PATHS=" in generated_lines
  1186. def test_collect_security_config_preserves_explicit_empty_whitelist_on_rerun(
  1187. tmp_path: Path,
  1188. ) -> None:
  1189. """Rerunning security setup should keep an explicitly empty whitelist unchanged."""
  1190. env_file = tmp_path / ".env"
  1191. env_file.write_text("WHITELIST_PATHS=\n", encoding="utf-8")
  1192. output = run_bash(f"""
  1193. set -euo pipefail
  1194. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1195. REPO_ROOT="{tmp_path}"
  1196. reset_state
  1197. load_existing_env_if_present
  1198. prompt_clearable_with_default() {{ printf '%s' "$2"; }}
  1199. prompt_clearable_secret_with_default() {{ printf '%s' "$2"; }}
  1200. collect_security_config no no
  1201. printf 'WHITELIST_PATHS_SET=%s\\n' "${{ENV_VALUES[WHITELIST_PATHS]+set}}"
  1202. printf 'WHITELIST_PATHS=%s\\n' "${{ENV_VALUES[WHITELIST_PATHS]}}\"
  1203. """)
  1204. values = parse_lines(output)
  1205. assert values["WHITELIST_PATHS_SET"] == "set"
  1206. assert values["WHITELIST_PATHS"] == ""
  1207. def test_collect_observability_config_clears_existing_values_on_rerun(
  1208. tmp_path: Path,
  1209. ) -> None:
  1210. """Rerunning setup should remove saved Langfuse settings when observability is declined."""
  1211. env_file = tmp_path / ".env"
  1212. env_file.write_text(
  1213. "\n".join(
  1214. [
  1215. "LANGFUSE_ENABLE_TRACE=true",
  1216. "LANGFUSE_SECRET_KEY=old-secret",
  1217. "LANGFUSE_PUBLIC_KEY=old-public",
  1218. "LANGFUSE_HOST=https://langfuse.example",
  1219. ]
  1220. )
  1221. + "\n",
  1222. encoding="utf-8",
  1223. )
  1224. output = run_bash(f"""
  1225. set -euo pipefail
  1226. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1227. REPO_ROOT="{tmp_path}"
  1228. reset_state
  1229. load_existing_env_if_present
  1230. collect_observability_config
  1231. generate_env_file "{REPO_ROOT}/env.example" "$REPO_ROOT/.env.generated"
  1232. printf 'LANGFUSE_ENABLE_TRACE_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_ENABLE_TRACE]+set}}"
  1233. printf 'LANGFUSE_SECRET_KEY_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_SECRET_KEY]+set}}"
  1234. printf 'LANGFUSE_PUBLIC_KEY_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_PUBLIC_KEY]+set}}"
  1235. printf 'LANGFUSE_HOST_SET=%s\\n' "${{ENV_VALUES[LANGFUSE_HOST]+set}}\"
  1236. """)
  1237. values = parse_lines(output)
  1238. generated_lines = (
  1239. (tmp_path / ".env.generated").read_text(encoding="utf-8").splitlines()
  1240. )
  1241. assert values["LANGFUSE_ENABLE_TRACE_SET"] == ""
  1242. assert values["LANGFUSE_SECRET_KEY_SET"] == ""
  1243. assert values["LANGFUSE_PUBLIC_KEY_SET"] == ""
  1244. assert values["LANGFUSE_HOST_SET"] == ""
  1245. assert not any(
  1246. line.startswith("LANGFUSE_ENABLE_TRACE=") for line in generated_lines
  1247. )
  1248. assert not any(line.startswith("LANGFUSE_SECRET_KEY=") for line in generated_lines)
  1249. assert not any(line.startswith("LANGFUSE_PUBLIC_KEY=") for line in generated_lines)
  1250. assert not any(line.startswith("LANGFUSE_HOST=") for line in generated_lines)
  1251. def test_collect_neo4j_config_bundled_service_keeps_username_editable(
  1252. tmp_path: Path,
  1253. ) -> None:
  1254. """Bundled Neo4j should preserve editable credentials and existing database overrides."""
  1255. compose_file = tmp_path / "docker-compose.yml"
  1256. compose_file.write_text(
  1257. "\n".join(
  1258. [
  1259. "services:",
  1260. " lightrag:",
  1261. " image: example/lightrag:test",
  1262. " env_file:",
  1263. " - .env",
  1264. ]
  1265. )
  1266. + "\n",
  1267. encoding="utf-8",
  1268. )
  1269. output = run_bash(f"""
  1270. set -euo pipefail
  1271. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1272. REPO_ROOT="{tmp_path}"
  1273. reset_state
  1274. ENV_VALUES[NEO4J_USERNAME]="custom-user"
  1275. ENV_VALUES[NEO4J_PASSWORD]="existing-password"
  1276. ENV_VALUES[NEO4J_DATABASE]="custom-db"
  1277. confirm_default_yes() {{ return 0; }}
  1278. prompt_until_valid() {{ printf '%s' "$2"; }}
  1279. prompt_log_file="$(mktemp)"
  1280. trap 'rm -f "$prompt_log_file"' EXIT
  1281. prompt_with_default() {{
  1282. printf '%s\\n' "$1" >> "$prompt_log_file"
  1283. if [[ "$1" == "Neo4j database" ]]; then
  1284. printf 'custom-db-2'
  1285. else
  1286. printf '%s' "$2"
  1287. fi
  1288. }}
  1289. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1290. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1291. collect_neo4j_config yes
  1292. generate_docker_compose "$REPO_ROOT/docker-compose.generated.yml"
  1293. printf 'NEO4J_USERNAME=%s\\n' "${{ENV_VALUES[NEO4J_USERNAME]}}"
  1294. printf 'NEO4J_PASSWORD=%s\\n' "${{ENV_VALUES[NEO4J_PASSWORD]}}"
  1295. printf 'NEO4J_DATABASE=%s\\n' "${{ENV_VALUES[NEO4J_DATABASE]}}"
  1296. printf 'DOCKER_SERVICE=%s\\n' "${{DOCKER_SERVICES[0]}}"
  1297. printf 'DATABASE_PROMPTS=%s\\n' "$(grep -c '^Neo4j database$' "$prompt_log_file" || true)\"
  1298. """)
  1299. values = parse_lines(output)
  1300. generated_compose = (tmp_path / "docker-compose.generated.yml").read_text(
  1301. encoding="utf-8"
  1302. )
  1303. assert values["NEO4J_USERNAME"] == "custom-user"
  1304. assert values["NEO4J_PASSWORD"] == "existing-password"
  1305. assert values["NEO4J_DATABASE"] == "custom-db-2"
  1306. assert values["DOCKER_SERVICE"] == "neo4j"
  1307. assert values["DATABASE_PROMPTS"] == "1"
  1308. assert (
  1309. "NEO4J_AUTH: ${NEO4J_USERNAME:?missing}/${NEO4J_PASSWORD:?missing}"
  1310. in generated_compose
  1311. )
  1312. assert 'NEO4J_dbms_default__database: "custom-db-2"' in generated_compose
  1313. def test_collect_neo4j_config_bundled_service_defaults_database_when_unset() -> None:
  1314. """Bundled Neo4j should pin the community default database when no prior value exists."""
  1315. output = run_bash(f"""
  1316. set -euo pipefail
  1317. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1318. reset_state
  1319. prompt_log_file="$(mktemp)"
  1320. trap 'rm -f "$prompt_log_file"' EXIT
  1321. confirm_default_yes() {{ return 0; }}
  1322. prompt_until_valid() {{ printf '%s' "$2"; }}
  1323. prompt_with_default() {{
  1324. printf '%s\\n' "$1" >> "$prompt_log_file"
  1325. printf '%s' "$2"
  1326. }}
  1327. prompt_secret_until_valid_with_default() {{ printf 'secure-password'; }}
  1328. collect_neo4j_config yes
  1329. printf 'DATABASE=%s\\n' "${{ENV_VALUES[NEO4J_DATABASE]}}"
  1330. printf 'DATABASE_PROMPTS=%s\\n' "$(grep -c '^Neo4j database$' "$prompt_log_file" || true)\"
  1331. """)
  1332. values = parse_lines(output)
  1333. assert values["DATABASE"] == "neo4j"
  1334. assert values["DATABASE_PROMPTS"] == "0"
  1335. def test_collect_neo4j_config_uses_existing_password_as_default_in_docker_mode() -> (
  1336. None
  1337. ):
  1338. """Bundled Neo4j should preserve the existing password when the default is accepted."""
  1339. output = run_bash(f"""
  1340. set -euo pipefail
  1341. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1342. reset_state
  1343. ENV_VALUES[NEO4J_PASSWORD]="from-env-password"
  1344. confirm_default_yes() {{ return 0; }}
  1345. prompt_until_valid() {{ printf '%s' "$2"; }}
  1346. prompt_with_default() {{ printf '%s' "$2"; }}
  1347. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1348. collect_neo4j_config yes
  1349. printf 'PASSWORD=%s\\n' "${{ENV_VALUES[NEO4J_PASSWORD]}}\"
  1350. """)
  1351. values = parse_lines(output)
  1352. assert values["PASSWORD"] == "from-env-password"
  1353. def test_collect_neo4j_config_uses_existing_password_as_default_in_external_mode() -> (
  1354. None
  1355. ):
  1356. """External Neo4j should preserve the existing password when the default is accepted."""
  1357. output = run_bash(f"""
  1358. set -euo pipefail
  1359. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1360. reset_state
  1361. ENV_VALUES[NEO4J_PASSWORD]="from-env-password"
  1362. confirm_default_no() {{ return 1; }}
  1363. prompt_until_valid() {{ printf '%s' "$2"; }}
  1364. prompt_with_default() {{ printf '%s' "$2"; }}
  1365. prompt_secret_with_default() {{ printf '%s' "$2"; }}
  1366. collect_neo4j_config no
  1367. printf 'PASSWORD=%s\\n' "${{ENV_VALUES[NEO4J_PASSWORD]}}\"
  1368. """)
  1369. values = parse_lines(output)
  1370. assert values["PASSWORD"] == "from-env-password"
  1371. def test_collect_neo4j_config_bundled_service_reprompts_for_empty_credentials() -> None:
  1372. """Bundled Neo4j should reject empty username and password values."""
  1373. output = run_bash(f"""
  1374. set -euo pipefail
  1375. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1376. reset_state
  1377. prompt_log_file="$(mktemp)"
  1378. trap 'rm -f "$prompt_log_file"' EXIT
  1379. confirm_default_yes() {{ return 0; }}
  1380. prompt_until_valid() {{
  1381. local prompt="$1"
  1382. local default="$2"
  1383. local validator="$3"
  1384. shift 3
  1385. local value=""
  1386. while true; do
  1387. if [[ "$prompt" == "Neo4j URI" ]]; then
  1388. value="$default"
  1389. else
  1390. printf 'username\\n' >> "$prompt_log_file"
  1391. if [[ "$(grep -c '^username$' "$prompt_log_file")" -eq 1 ]]; then
  1392. value=""
  1393. else
  1394. value="neo4j-user"
  1395. fi
  1396. fi
  1397. if "$validator" "$value" "$@"; then
  1398. printf '%s' "$value"
  1399. return 0
  1400. fi
  1401. done
  1402. }}
  1403. prompt_with_default() {{ printf '%s' "$2"; }}
  1404. prompt_secret_until_valid_with_default() {{
  1405. local prompt="$1"
  1406. local default="$2"
  1407. local validator="$3"
  1408. shift 3
  1409. local value=""
  1410. while true; do
  1411. printf 'password\\n' >> "$prompt_log_file"
  1412. if [[ "$(grep -c '^password$' "$prompt_log_file")" -eq 1 ]]; then
  1413. value=""
  1414. else
  1415. value="secure-password"
  1416. fi
  1417. if "$validator" "$value" "$@"; then
  1418. printf '%s' "$value"
  1419. return 0
  1420. fi
  1421. done
  1422. }}
  1423. collect_neo4j_config yes
  1424. printf 'USERNAME=%s\\n' "${{ENV_VALUES[NEO4J_USERNAME]}}"
  1425. printf 'PASSWORD=%s\\n' "${{ENV_VALUES[NEO4J_PASSWORD]}}"
  1426. printf 'USERNAME_CALLS=%s\\n' "$(grep -c '^username$' "$prompt_log_file")"
  1427. printf 'PASSWORD_CALLS=%s\\n' "$(grep -c '^password$' "$prompt_log_file")\"
  1428. """)
  1429. values = parse_lines(output)
  1430. assert values["USERNAME"] == "neo4j-user"
  1431. assert values["PASSWORD"] == "secure-password"
  1432. assert values["USERNAME_CALLS"] == "2"
  1433. assert values["PASSWORD_CALLS"] == "2"
  1434. def test_collect_neo4j_config_external_service_still_uses_standard_prompts() -> None:
  1435. """External Neo4j setup should keep the non-Docker prompt behavior unchanged."""
  1436. output = run_bash(f"""
  1437. set -euo pipefail
  1438. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1439. reset_state
  1440. prompt_log_file="$(mktemp)"
  1441. trap 'rm -f "$prompt_log_file"' EXIT
  1442. confirm_default_no() {{ return 1; }}
  1443. prompt_until_valid() {{ printf '%s' "$2"; }}
  1444. prompt_with_default() {{
  1445. printf 'with_default\\n' >> "$prompt_log_file"
  1446. if [[ "$1" == "Neo4j username" ]]; then
  1447. printf 'external-user'
  1448. elif [[ "$1" == "Neo4j database" ]]; then
  1449. printf 'external-db'
  1450. else
  1451. printf '%s' "$2"
  1452. fi
  1453. }}
  1454. prompt_secret_with_default() {{
  1455. printf 'secret_with_default\\n' >> "$prompt_log_file"
  1456. printf 'external-password'
  1457. }}
  1458. collect_neo4j_config no
  1459. printf 'USERNAME=%s\\n' "${{ENV_VALUES[NEO4J_USERNAME]}}"
  1460. printf 'PASSWORD=%s\\n' "${{ENV_VALUES[NEO4J_PASSWORD]}}"
  1461. printf 'DATABASE=%s\\n' "${{ENV_VALUES[NEO4J_DATABASE]}}"
  1462. printf 'USERNAME_PROMPTS=%s\\n' "$(grep -c '^with_default$' "$prompt_log_file")"
  1463. printf 'PASSWORD_PROMPTS=%s\\n' "$(grep -c '^secret_with_default$' "$prompt_log_file")\"
  1464. """)
  1465. values = parse_lines(output)
  1466. assert values["USERNAME"] == "external-user"
  1467. assert values["PASSWORD"] == "external-password"
  1468. assert values["DATABASE"] == "external-db"
  1469. assert values["USERNAME_PROMPTS"] == "2"
  1470. assert values["PASSWORD_PROMPTS"] == "1"
  1471. def test_collect_opensearch_config_preserves_graphlookup_auto_detection() -> None:
  1472. """collect_opensearch_config should leave PPL graphlookup unset unless explicitly configured."""
  1473. values = run_bash_lines(f"""
  1474. set -euo pipefail
  1475. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1476. reset_state
  1477. confirm_default_yes() {{ return 0; }}
  1478. confirm_default_no() {{ return 1; }}
  1479. prompt_until_valid() {{ printf '%s' "$2"; }}
  1480. prompt_with_default() {{ printf '%s' "$2"; }}
  1481. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1482. collect_opensearch_config "yes"
  1483. if [[ -v 'ENV_VALUES[OPENSEARCH_USE_PPL_GRAPHLOOKUP]' ]]; then
  1484. printf 'GRAPHLOOKUP_SET=yes\\n'
  1485. else
  1486. printf 'GRAPHLOOKUP_SET=no\\n'
  1487. fi
  1488. """)
  1489. assert values["GRAPHLOOKUP_SET"] == "no"
  1490. def test_collect_opensearch_config_preserves_explicit_graphlookup_override() -> None:
  1491. """collect_opensearch_config should keep an existing PPL graphlookup override."""
  1492. values = run_bash_lines(f"""
  1493. set -euo pipefail
  1494. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1495. reset_state
  1496. ENV_VALUES[OPENSEARCH_USE_PPL_GRAPHLOOKUP]="true"
  1497. confirm_default_yes() {{ return 0; }}
  1498. confirm_default_no() {{ return 1; }}
  1499. prompt_until_valid() {{ printf '%s' "$2"; }}
  1500. prompt_with_default() {{ printf '%s' "$2"; }}
  1501. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1502. collect_opensearch_config "yes"
  1503. printf 'GRAPHLOOKUP=%s\\n' "${{ENV_VALUES[OPENSEARCH_USE_PPL_GRAPHLOOKUP]}}\"
  1504. """)
  1505. assert values["GRAPHLOOKUP"] == "true"
  1506. def test_collect_opensearch_config_forces_docker_verify_certs_false() -> None:
  1507. """collect_opensearch_config should force OPENSEARCH_VERIFY_CERTS=false for Docker."""
  1508. values = run_bash_lines(f"""
  1509. set -euo pipefail
  1510. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1511. reset_state
  1512. ENV_VALUES[OPENSEARCH_USE_SSL]="false"
  1513. ENV_VALUES[OPENSEARCH_VERIFY_CERTS]="true"
  1514. confirm_default_yes() {{ return 0; }}
  1515. confirm_default_no() {{ return 1; }}
  1516. prompt_until_valid() {{ printf '%s' "$2"; }}
  1517. prompt_with_default() {{ printf '%s' "$2"; }}
  1518. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1519. collect_opensearch_config "yes"
  1520. printf 'USE_SSL=%s\\n' "${{ENV_VALUES[OPENSEARCH_USE_SSL]}}"
  1521. printf 'VERIFY_CERTS=%s\\n' "${{ENV_VALUES[OPENSEARCH_VERIFY_CERTS]}}\"
  1522. """)
  1523. assert values["USE_SSL"] == "false"
  1524. assert values["VERIFY_CERTS"] == "false"
  1525. def test_collect_opensearch_config_defaults_docker_tls_flags_when_unset() -> None:
  1526. """collect_opensearch_config should supply Docker TLS defaults when .env has no values."""
  1527. values = run_bash_lines(f"""
  1528. set -euo pipefail
  1529. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1530. reset_state
  1531. confirm_default_yes() {{ return 0; }}
  1532. confirm_default_no() {{ return 1; }}
  1533. prompt_until_valid() {{ printf '%s' "$2"; }}
  1534. prompt_with_default() {{ printf '%s' "$2"; }}
  1535. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1536. collect_opensearch_config "yes"
  1537. printf 'USE_SSL=%s\\n' "${{ENV_VALUES[OPENSEARCH_USE_SSL]}}"
  1538. printf 'VERIFY_CERTS=%s\\n' "${{ENV_VALUES[OPENSEARCH_VERIFY_CERTS]}}\"
  1539. """)
  1540. assert values["USE_SSL"] == "true"
  1541. assert values["VERIFY_CERTS"] == "false"
  1542. def test_collect_opensearch_config_uses_original_index_settings_as_defaults() -> None:
  1543. """collect_opensearch_config should prefer ORIGINAL_ENV_VALUES for shard/replica defaults."""
  1544. values = run_bash_lines(f"""
  1545. set -euo pipefail
  1546. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1547. reset_state
  1548. default_log="$(mktemp)"
  1549. ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]="3"
  1550. ORIGINAL_ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]="2"
  1551. ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]="9"
  1552. ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]="8"
  1553. confirm_default_yes() {{ return 1; }}
  1554. confirm_default_no() {{ return 1; }}
  1555. prompt_until_valid() {{
  1556. case "$1" in
  1557. "Number of index shards"|"Number of index replicas (use 2 for 3-AZ clusters)")
  1558. printf '%s=%s\\n' "$1" "$2" >> "$default_log"
  1559. ;;
  1560. esac
  1561. printf '%s' "$2"
  1562. }}
  1563. prompt_with_default() {{ printf '%s' "$2"; }}
  1564. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1565. collect_opensearch_config "no"
  1566. printf 'SHARDS=%s\\n' "${{ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]}}"
  1567. printf 'REPLICAS=%s\\n' "${{ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]}}"
  1568. printf 'DEFAULTS=%s\\n' "$(tr '\\n' ';' < "$default_log")\"
  1569. """)
  1570. assert values["SHARDS"] == "3"
  1571. assert values["REPLICAS"] == "2"
  1572. assert "Number of index shards=3;" in values["DEFAULTS"]
  1573. assert "Number of index replicas (use 2 for 3-AZ clusters)=2;" in values["DEFAULTS"]
  1574. def test_collect_opensearch_config_validates_index_settings_during_prompt() -> None:
  1575. """collect_opensearch_config should validate shard and replica prompts."""
  1576. values = run_bash_lines(f"""
  1577. set -euo pipefail
  1578. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1579. reset_state
  1580. validator_file="$(mktemp)"
  1581. confirm_default_yes() {{ return 1; }}
  1582. confirm_default_no() {{ return 1; }}
  1583. prompt_until_valid() {{
  1584. case "$1" in
  1585. "Number of index shards"|"Number of index replicas (use 2 for 3-AZ clusters)")
  1586. printf '%s=%s\\n' "$1" "$3" >> "$validator_file"
  1587. ;;
  1588. esac
  1589. printf '%s' "$2"
  1590. }}
  1591. prompt_with_default() {{ printf '%s' "$2"; }}
  1592. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1593. collect_opensearch_config "no"
  1594. printf 'VALIDATORS=%s\\n' "$(tr '\\n' ';' < "$validator_file")\"
  1595. """)
  1596. assert "Number of index shards=validate_positive_integer;" in values["VALIDATORS"]
  1597. assert (
  1598. "Number of index replicas (use 2 for 3-AZ clusters)=validate_non_negative_integer;"
  1599. in values["VALIDATORS"]
  1600. )
  1601. def test_collect_opensearch_config_validates_hosts_during_prompt() -> None:
  1602. """collect_opensearch_config should validate OPENSEARCH_HOSTS at prompt time."""
  1603. values = run_bash_lines(f"""
  1604. set -euo pipefail
  1605. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1606. reset_state
  1607. validator_file="$(mktemp)"
  1608. confirm_default_yes() {{ return 0; }}
  1609. confirm_default_no() {{ return 1; }}
  1610. prompt_until_valid() {{
  1611. printf '%s' "$3" > "$validator_file"
  1612. printf '%s' "$2"
  1613. }}
  1614. prompt_with_default() {{ printf '%s' "$2"; }}
  1615. prompt_secret_until_valid_with_default() {{ printf '%s' "$2"; }}
  1616. collect_opensearch_config "yes"
  1617. printf 'HOST_VALIDATOR=%s\\n' "$(cat "$validator_file")\"
  1618. """)
  1619. assert values["HOST_VALIDATOR"] == "validate_opensearch_hosts_format"
  1620. def test_collect_opensearch_config_validates_password_during_prompt() -> None:
  1621. """collect_opensearch_config should validate OPENSEARCH_PASSWORD at prompt time."""
  1622. values = run_bash_lines(f"""
  1623. set -euo pipefail
  1624. source "{REPO_ROOT}/scripts/setup/setup.sh"
  1625. reset_state
  1626. validator_file="$(mktemp)"
  1627. confirm_default_yes() {{ return 0; }}
  1628. confirm_default_no() {{ return 1; }}
  1629. prompt_until_valid() {{ printf '%s' "$2"; }}
  1630. prompt_with_default() {{ printf '%s' "$2"; }}
  1631. prompt_secret_until_valid_with_default() {{
  1632. printf '%s' "$3" > "$validator_file"
  1633. printf '%s' "$2"
  1634. }}
  1635. collect_opensearch_config "yes"
  1636. printf 'PASSWORD_VALIDATOR=%s\\n' "$(cat "$validator_file")\"
  1637. """)
  1638. assert values["PASSWORD_VALIDATOR"] == "validate_opensearch_password_strength"