fix(database): fail startup on migration errors (#6279)

This commit is contained in:
InfinityPacer
2026-08-12 21:29:38 +08:00
committed by GitHub
parent 46c2a856c4
commit 0ac641c05d
4 changed files with 101 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
from configparser import ConfigParser as _ConfigParser
import traceback
from alembic.command import upgrade
from alembic.config import Config
@@ -38,5 +39,8 @@ def update_db():
alembic_cfg.set_main_option('sqlalchemy.url', db_url)
upgrade(alembic_cfg, 'head')
except Exception as e:
logger.error(f'数据库更新失败:{str(e)}')
except Exception as error:
logger.error(
f'数据库更新失败:{str(error)} - {traceback.format_exc()}'
)
raise

View File

@@ -0,0 +1,63 @@
import importlib.util
from pathlib import Path
import sys
import uuid
import pytest
from app.db import init as db_init
LOCAL_SETUP_PATH = (
Path(__file__).resolve().parents[1] / "scripts" / "local_setup.py"
)
def _load_local_setup_module():
"""加载隔离的本地安装脚本实例,避免测试间共享模块状态。"""
module_name = f"moviepilot_local_setup_migration_{uuid.uuid4().hex}"
spec = importlib.util.spec_from_file_location(module_name, LOCAL_SETUP_PATH)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_update_db_preserves_migration_error_and_traceback(monkeypatch) -> None:
"""迁移失败日志应保留堆栈,同时向调用方传播原始异常。"""
migration_error = RuntimeError("migration failed")
logged_errors: list[str] = []
def fail_upgrade(*_args, **_kwargs) -> None:
raise migration_error
monkeypatch.setattr(db_init, "upgrade", fail_upgrade)
monkeypatch.setattr(db_init.logger, "error", logged_errors.append)
with pytest.raises(RuntimeError) as raised:
db_init.update_db()
assert raised.value is migration_error
assert len(logged_errors) == 1
assert "数据库更新失败migration failed" in logged_errors[0]
assert "RuntimeError: migration failed" in logged_errors[0]
def test_local_setup_returns_failure_when_database_migration_fails(
monkeypatch,
capsys,
) -> None:
"""本地维护命令不得在迁移失败后继续访问业务表。"""
module = _load_local_setup_module()
migration_error = RuntimeError("migration failed")
def fail_sync() -> None:
raise migration_error
monkeypatch.setattr(sys, "argv", [str(LOCAL_SETUP_PATH), "sync-superuser"])
monkeypatch.setattr(module, "_resolve_interactive_config_dir", lambda *_: None)
monkeypatch.setattr(module, "configure_config_dir", lambda **_: Path("config"))
monkeypatch.setattr(module, "_sync_superuser_account_inner", fail_sync)
assert module.main() == 1
assert "migration failed" in capsys.readouterr().err

View File

@@ -261,3 +261,16 @@ def test_backend_ready_timeout_accepts_leading_zero_decimal(tmp_path: Path) -> N
assert "MOVIEPILOT_BACKEND_READY_TIMEOUT=08 无效" not in output
assert "MoviePilot Web 已可访问" in output
def test_backend_failure_keepalive_contract_is_explicit() -> None:
"""后端异常默认保活诊断,显式关闭后才退出容器。"""
content = (ROOT / "docker" / "entrypoint.sh").read_text(encoding="utf-8")
function = content.split(
"function diagnostic_keepalive() {", 1
)[1].split("\n}", 1)[0]
assert 'MOVIEPILOT_DOCKER_KEEPALIVE_ON_FAILURE:-true' in function
assert 'if [ "${keepalive}" = "false" ]' in function
assert 'graceful_exit "$exit_code" "python_exit"' in function
assert "容器将保持运行以便执行 moviepilot doctor" in function

View File

@@ -154,6 +154,25 @@ def test_application_preserves_stop_requested_before_startup(monkeypatch):
]
def test_application_does_not_start_server_after_migration_failure(monkeypatch):
"""数据库迁移失败时不得启动 API 服务。"""
from app import main
migration_error = RuntimeError("migration failed")
server_run = MagicMock()
monkeypatch.setattr(main.signal, "signal", MagicMock())
monkeypatch.setattr(main, "start_tray", MagicMock())
monkeypatch.setattr(main, "init_db", MagicMock())
monkeypatch.setattr(main, "update_db", MagicMock(side_effect=migration_error))
monkeypatch.setattr(main.Server, "run", server_run)
with pytest.raises(RuntimeError) as raised:
main.run_application()
assert raised.value is migration_error
server_run.assert_not_called()
def test_uvicorn_preserves_stop_requested_before_serve(monkeypatch):
"""Uvicorn 启动不能清除数据库初始化阶段已经发布的停止请求"""
from app import main