Ctrl K
ComunidadGuías…

tests/test_one_config_rule.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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()