@@ -209,3 +209,75 @@ def test_database_skips_mkdir_for_bare_filename(tmp_path, monkeypatch):
209209 apply_schema (database .connect ())
210210 assert (tmp_path / "bare.sqlite3" ).exists ()
211211 database .close ()
212+
213+
214+ # --------------------------------------------------------- 单一版本源
215+
216+ def test_app_version_reads_pyproject ():
217+ """版本号从 pyproject.toml 读取(唯一真源),与 tag 同源。"""
218+ import tomllib
219+
220+ from src .version import app_version
221+
222+ with open ("pyproject.toml" , "rb" ) as handle :
223+ expected = tomllib .load (handle )["project" ]["version" ]
224+ assert app_version () == expected
225+ # 带 v 前缀的 tag 去掉 v 后应与之一致(发布流程靠这个对齐)
226+ assert expected .count ("." ) == 2 and not expected .startswith ("v" )
227+
228+
229+ def test_app_version_falls_back_when_pyproject_missing (monkeypatch , tmp_path ):
230+ """读不到 pyproject 时回落到常量,绝不抛异常(版本号不该让服务起不来)。"""
231+ import src .version as version_module
232+
233+ monkeypatch .setattr (version_module , "_PYPROJECT" , tmp_path / "nope.toml" )
234+ version_module .app_version .cache_clear ()
235+ try :
236+ assert version_module .app_version () == version_module .FALLBACK_VERSION
237+ finally :
238+ version_module .app_version .cache_clear ()
239+
240+
241+ def test_app_version_falls_back_on_corrupt_pyproject (monkeypatch , tmp_path ):
242+ """pyproject 内容损坏(非法 TOML)同样回落,不抛异常。"""
243+ import src .version as version_module
244+
245+ broken = tmp_path / "pyproject.toml"
246+ broken .write_text ("this is not = = toml" , encoding = "utf-8" )
247+ monkeypatch .setattr (version_module , "_PYPROJECT" , broken )
248+ version_module .app_version .cache_clear ()
249+ try :
250+ assert version_module .app_version () == version_module .FALLBACK_VERSION
251+ finally :
252+ version_module .app_version .cache_clear ()
253+
254+
255+ def test_app_version_falls_back_on_missing_or_blank_field (monkeypatch , tmp_path ):
256+ """缺 [project].version、版本为空串、version 非字符串 → 全部回落。"""
257+ import src .version as version_module
258+
259+ for content in ('[project]\n name = "x"\n ' ,
260+ '[project]\n version = ""\n ' ,
261+ '[project]\n version = 123\n ' ):
262+ broken = tmp_path / "pyproject.toml"
263+ broken .write_text (content , encoding = "utf-8" )
264+ monkeypatch .setattr (version_module , "_PYPROJECT" , broken )
265+ version_module .app_version .cache_clear ()
266+ try :
267+ assert version_module .app_version () == version_module .FALLBACK_VERSION
268+ finally :
269+ version_module .app_version .cache_clear ()
270+
271+
272+ def test_app_version_is_cached ():
273+ """重复调用只解析一次(lru_cache)。"""
274+ from src .version import app_version
275+
276+ assert app_version () is app_version ()
277+
278+
279+ def test_openapi_reports_app_version ():
280+ """FastAPI 的 version 必须来自单一版本源(此前硬编码 0.1.0 会与 tag 漂移)。"""
281+ from src .main import app_version
282+
283+ assert app_version () # 可调用且非空(main 里的 import 绑定没断)
0 commit comments