feat(system): add staged release updates

This commit is contained in:
jxxghp
2026-08-24 17:58:03 +08:00
parent 5415398027
commit 84326e20de
23 changed files with 1393 additions and 350 deletions
+116 -69
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
import importlib.util
import hashlib
import json
import sys
import tempfile
import unittest
import uuid
from pathlib import Path
from types import ModuleType, SimpleNamespace
@@ -15,12 +16,8 @@ MODULE_PATH = Path(__file__).resolve().parents[1] / "app" / "cli.py"
class _DummySystemHelper:
@staticmethod
def consume_one_shot_update_mode():
return None
@staticmethod
def get_auto_update_mode():
return "false"
def consume_one_shot_dev_update():
return False
def load_cli_module():
@@ -39,6 +36,7 @@ def load_cli_module():
PROXY_HOST="",
PIP_PROXY="",
GITHUB_TOKEN="",
MOVIEPILOT_AUTO_UPDATE="false",
PROXY={},
REPO_GITHUB_HEADERS=lambda _repo: {},
)
@@ -84,74 +82,123 @@ def load_cli_module():
return module
class CliAutoUpdateTests(unittest.TestCase):
def test_resolve_auto_update_targets_only_queries_backend_release(self):
module = load_cli_module()
def test_resolve_auto_update_targets_keeps_dev_branch_tracking():
module = load_cli_module()
with patch.object(module, "_git_current_branch", return_value="v3"):
assert module._resolve_auto_update_targets("dev") == "latest"
assert module._resolve_auto_update_targets("release") is None
with patch.object(module, "_latest_release_tag", return_value="v2.10.12") as latest_mock:
backend_ref = module._resolve_auto_update_targets("release")
latest_mock.assert_called_once_with(
module.BACKEND_RELEASES_API,
repo="jxxghp/MoviePilot",
prefix="v2",
)
self.assertEqual(backend_ref, "v2.10.12")
def test_one_shot_dev_update_overrides_disabled_default():
module = load_cli_module()
module.settings.MOVIEPILOT_AUTO_UPDATE = "false"
def test_best_effort_auto_update_does_not_pass_frontend_version_override(self):
module = load_cli_module()
run_result = SimpleNamespace(returncode=0, stdout="ok")
with patch.object(
module.SystemHelper, "consume_one_shot_dev_update", return_value=True
):
assert module._auto_update_mode() == "dev"
with patch.object(module, "_auto_update_mode", return_value="release"), patch.object(
module, "_resolve_auto_update_targets", return_value="v2.10.12"
), patch.object(module.subprocess, "run", return_value=run_result) as run_mock, patch.object(
module.click, "echo"
):
module._best_effort_auto_update()
command = run_mock.call_args.args[0]
self.assertEqual(command[1:5], [str(module._repo_root() / "scripts" / "local_setup.py"), "update", "all", "--ref"])
self.assertNotIn("--frontend-version", command)
def test_release_mode_does_not_update_during_start():
module = load_cli_module()
with patch.object(module, "_auto_update_mode", return_value="release"), patch.object(
module.subprocess, "run"
) as run_mock:
module._best_effort_auto_update()
run_mock.assert_not_called()
def test_best_effort_auto_update_passes_package_env_and_overrides_proxy(self):
module = load_cli_module()
module.settings.PROXY_HOST = "http://proxy.example:7890"
module.settings.PIP_PROXY = "https://mirror.example/simple"
run_result = SimpleNamespace(returncode=0, stdout="ok")
with patch.dict(module.os.environ, {"HTTPS_PROXY": "http://old.example:8080"}, clear=True), patch.object(
module, "_auto_update_mode", return_value="release"
), patch.object(module, "_resolve_auto_update_targets", return_value="v2.10.12"), patch.object(
module.subprocess, "run", return_value=run_result
) as run_mock, patch.object(
module.click, "echo"
):
module._best_effort_auto_update()
env = run_mock.call_args.kwargs["env"]
self.assertEqual(env["HTTPS_PROXY"], "http://proxy.example:7890")
self.assertEqual(env["PIP_PROXY"], "https://mirror.example/simple")
self.assertEqual(env["PACKAGE_CACHE_ROOT"], str(module.settings.PACKAGE_CACHE_PATH))
self.assertEqual(env["UV_CACHE_DIR"], str(module.settings.PACKAGE_CACHE_PATH / "uv"))
def test_best_effort_auto_update_derives_tool_cache_from_existing_root(self):
module = load_cli_module()
run_result = SimpleNamespace(returncode=0, stdout="ok")
package_cache_root = Path("/custom/package-cache-root")
with patch.dict(
module.os.environ,
def test_prepared_release_uses_downloaded_package_before_dev_mode():
module = load_cli_module()
module.PREPARED_UPDATE_ROOT.mkdir(parents=True)
backend = module.PREPARED_UPDATE_ROOT / "backend.zip"
frontend = module.PREPARED_UPDATE_ROOT / "frontend.zip"
backend.write_bytes(b"backend")
frontend.write_bytes(b"frontend")
module.PREPARED_UPDATE_MANIFEST.write_text(
json.dumps(
{
"PACKAGE_CACHE_ROOT": str(package_cache_root),
},
clear=True,
), patch.object(module, "_auto_update_mode", return_value="release"), patch.object(
module, "_resolve_auto_update_targets", return_value="v2.10.12"
), patch.object(module.subprocess, "run", return_value=run_result) as run_mock, patch.object(
module.click, "echo"
):
module._best_effort_auto_update()
"version": "v3.1.0",
"frontend_version": "v3.1.0",
"backend_archive": str(backend),
"frontend_archive": str(frontend),
"backend_sha256": hashlib.sha256(backend.read_bytes()).hexdigest(),
"frontend_sha256": hashlib.sha256(frontend.read_bytes()).hexdigest(),
}
),
encoding="utf-8",
)
run_result = SimpleNamespace(returncode=0, stdout="ok")
env = run_mock.call_args.kwargs["env"]
self.assertEqual(env["PACKAGE_CACHE_ROOT"], str(package_cache_root))
self.assertEqual(env["UV_CACHE_DIR"], str(package_cache_root / "uv"))
with patch.object(module, "_auto_update_mode", return_value="dev") as mode, patch.object(
module.subprocess, "run", return_value=run_result
) as run_mock, patch.object(module.click, "echo"):
module._best_effort_auto_update()
command = run_mock.call_args.args[0]
assert "--offline-backend" in command
assert command[command.index("--frontend-archive") + 1] == str(frontend)
assert not module.PREPARED_UPDATE_MANIFEST.exists()
mode.assert_not_called()
def test_best_effort_auto_update_does_not_pass_frontend_version_override():
module = load_cli_module()
run_result = SimpleNamespace(returncode=0, stdout="ok")
with patch.object(module, "_auto_update_mode", return_value="dev"), patch.object(
module, "_resolve_auto_update_targets", return_value="latest"
), patch.object(module.subprocess, "run", return_value=run_result) as run_mock, patch.object(
module.click, "echo"
):
module._best_effort_auto_update()
command = run_mock.call_args.args[0]
assert command[1:5] == [
str(module._repo_root() / "scripts" / "local_setup.py"),
"update",
"all",
"--ref",
]
assert "--frontend-version" not in command
def test_best_effort_auto_update_passes_package_env_and_overrides_proxy():
module = load_cli_module()
module.settings.PROXY_HOST = "http://proxy.example:7890"
module.settings.PIP_PROXY = "https://mirror.example/simple"
run_result = SimpleNamespace(returncode=0, stdout="ok")
with patch.dict(module.os.environ, {"HTTPS_PROXY": "http://old.example:8080"}, clear=True), patch.object(
module, "_auto_update_mode", return_value="dev"
), patch.object(module, "_resolve_auto_update_targets", return_value="latest"), patch.object(
module.subprocess, "run", return_value=run_result
) as run_mock, patch.object(module.click, "echo"):
module._best_effort_auto_update()
env = run_mock.call_args.kwargs["env"]
assert env["HTTPS_PROXY"] == "http://proxy.example:7890"
assert env["PIP_PROXY"] == "https://mirror.example/simple"
assert env["PACKAGE_CACHE_ROOT"] == str(module.settings.PACKAGE_CACHE_PATH)
assert env["UV_CACHE_DIR"] == str(module.settings.PACKAGE_CACHE_PATH / "uv")
def test_best_effort_auto_update_derives_tool_cache_from_existing_root():
module = load_cli_module()
run_result = SimpleNamespace(returncode=0, stdout="ok")
package_cache_root = Path("/custom/package-cache-root")
with patch.dict(
module.os.environ,
{"PACKAGE_CACHE_ROOT": str(package_cache_root)},
clear=True,
), patch.object(module, "_auto_update_mode", return_value="dev"), patch.object(
module, "_resolve_auto_update_targets", return_value="latest"
), patch.object(module.subprocess, "run", return_value=run_result) as run_mock, patch.object(
module.click, "echo"
):
module._best_effort_auto_update()
env = run_mock.call_args.kwargs["env"]
assert env["PACKAGE_CACHE_ROOT"] == str(package_cache_root)
assert env["UV_CACHE_DIR"] == str(package_cache_root / "uv")