"""XKB 只能有一條設定優先順序的規則,而且只有一份實作。 2026-09-09 的故障是:排程在啟動腳本前把自己的 LLM_API_URL / LLM_MODEL 放進 環境,蓋掉 --env-file 指名的端點,於是書籤轉卡連續八天回報成功卻 0 產出。 修完那一個之後往下看,發現同一條規則在這個 repo 裡寫了兩次: tools/runtime_config.py 一份,tools/embedding_providers.py 自己又抄了一份, 負責 EMBEDDING_* 與各家 API key。兩份的 dotenv 解析逐字相同、優先順序相同、 洞也相同——EMBEDDING_PROVIDER 或 GEMINI_API_KEY 同樣會被外面的環境蓋掉。 沒人發現,因為沒有人把它們當成同一條規則看。 這裡測三件事:規則只有一份實作、規則本身是什麼、embedding 那條路真的走它。 """ from __future__ import annotations import os import sys import tempfile import unittest from pathlib import Path from unittest import mock ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "tools")) import runtime_config # noqa: E402 import embedding_providers # noqa: E402 class OneImplementation(unittest.TestCase): def test_embedding_config_uses_the_shared_loader_result(self): """Exercise the active config path, not a spelling in an unused wrapper.""" env_file = Path("fixture.env") with mock.patch.object(embedding_providers, "runtime_env", return_value={ "EMBEDDING_MODEL": "gemini-shared-loader"}) as loader, \ mock.patch.object(embedding_providers, "_load_xkb_config", return_value={}): config = embedding_providers.load_config(env_file) loader.assert_called_once_with(env_file) self.assertEqual(config.model, "gemini-shared-loader") class TheRule(unittest.TestCase): def _env_file(self, tmp: str, body: str) -> Path: path = Path(tmp) / "xkb.env" path.write_text(body, encoding="utf-8") return path def test_a_real_process_value_still_wins(self): """互動時臨時覆蓋一個設定,仍然要有效。""" with tempfile.TemporaryDirectory() as tmp: env_file = self._env_file(tmp, "XKB_TEST_MODEL=from-file\n") with mock.patch.dict(os.environ, {"XKB_TEST_MODEL": "from-process"}): settings = runtime_config.runtime_env(env_file) self.assertEqual(settings["XKB_TEST_MODEL"], "from-process") def test_an_empty_process_value_does_not_blank_out_the_file(self): """`export FOO=` 是「匯出了但沒填」,不是一個值。 讓它贏,得到的是一把空憑證和一個既不提檔案也不提變數的錯誤—— 又一條安靜出錯的路。 """ with tempfile.TemporaryDirectory() as tmp: env_file = self._env_file(tmp, "XKB_TEST_MODEL=from-file\n") with mock.patch.dict(os.environ, {"XKB_TEST_MODEL": ""}): settings = runtime_config.runtime_env(env_file) self.assertEqual(settings["XKB_TEST_MODEL"], "from-file") def test_an_empty_process_value_with_no_file_value_stays_empty(self): """檔案沒提到的鍵不受影響,這不是「把空值一律當沒設定」。""" with tempfile.TemporaryDirectory() as tmp: env_file = self._env_file(tmp, "XKB_TEST_OTHER=x\n") with mock.patch.dict(os.environ, {"XKB_TEST_MODEL": ""}): settings = runtime_config.runtime_env(env_file) self.assertEqual(settings["XKB_TEST_MODEL"], "") class EmbeddingObeysIt(unittest.TestCase): def test_config_reads_the_env_file_through_the_shared_rule(self): with tempfile.TemporaryDirectory() as tmp: env_file = Path(tmp) / "xkb.env" env_file.write_text( "EMBEDDING_PROVIDER=gemini\nEMBEDDING_MODEL=gemini-from-file\n", encoding="utf-8", ) with mock.patch.dict(os.environ, {"EMBEDDING_MODEL": ""}): config = embedding_providers.load_config(env_file=env_file) self.assertEqual(config.model, "gemini-from-file") def test_a_real_env_var_still_overrides_the_file(self): with tempfile.TemporaryDirectory() as tmp: env_file = Path(tmp) / "xkb.env" env_file.write_text( "EMBEDDING_PROVIDER=gemini\nEMBEDDING_MODEL=gemini-from-file\n", encoding="utf-8", ) with mock.patch.dict(os.environ, {"EMBEDDING_MODEL": "gemini-from-process"}): config = embedding_providers.load_config(env_file=env_file) self.assertEqual(config.model, "gemini-from-process") def test_the_api_key_comes_from_the_file_when_the_variable_is_empty(self): """這是實務上最痛的那個:空的 GEMINI_API_KEY 蓋掉檔案裡真的那把。""" with tempfile.TemporaryDirectory() as tmp: env_file = Path(tmp) / "xkb.env" env_file.write_text( "EMBEDDING_PROVIDER=gemini\n" "EMBEDDING_MODEL=gemini-embedding-2-preview\n" "GEMINI_API_KEY=key-from-file\n", encoding="utf-8", ) with mock.patch.dict(os.environ, {"GEMINI_API_KEY": ""}): provider = embedding_providers.get_provider(env_file=env_file) self.assertEqual(provider.api_key, "key-from-file") if __name__ == "__main__": unittest.main()