"""Real staged publication, stale-update rejection and live PyPI wheels, no registry changes.""" import hashlib import json import os import subprocess import sys import tempfile from pathlib import Path from unittest.mock import patch PROJECT = Path(__file__).resolve().parent.parent def inside(packages=False): from host.bundle import ROOT, installation_root from host.updates import update from host.media import capabilities application = installation_root() marker = application / "installation.json" before = json.loads(marker.read_text(encoding="utf-8-sig"))["current"] if packages: import io import time from host.main import NativeHost host = NativeHost(io.BytesIO()) try: response = host.start_update([{"id": "packages", "version": "latest-compatible"}]) deadline = time.monotonic() + 500 while host.jobs[response["jobId"]]["status"] == "running" and time.monotonic() < deadline: time.sleep(0.1) job = host.jobs[response["jobId"]] assert job["status"] == "complete", job result = job["result"] finally: host.close() after = Path(json.loads(marker.read_text(encoding="utf-8-sig"))["current"]) versions = json.loads((after / "bundle.json").read_text(encoding="utf-8"))["pythonPackages"] assert result["restartRequired"] and "yt-dlp" in {key.lower() for key in versions} print(json.dumps({"livePyPI": "passed", "nativeWorkerUpdate": "passed", "packages": versions})) return archive = PROJECT / ".build-cache/node.zip" data = archive.read_bytes() candidate = {"id": "node", "version": "v24.21.0", "url": "https://nodejs.org/dist/v24.21.0/node-v24.21.0-win-x64.zip", "sha256": hashlib.sha256(data).hexdigest()} class Response: headers = {"Content-Length": str(len(data))} def __enter__(self): return self def __exit__(self, *args): return False def iter_content(self, count): for offset in range(0, len(data), count): yield data[offset:offset + count] dll = ROOT / "runtime/python/python313.dll" with dll.open("rb") as opened, patch("host.updates.get", return_value=Response()): result = update(lambda *args: None, lambda _: None, [candidate]) after = json.loads(marker.read_text(encoding="utf-8-sig"))["current"] assert after != before and result["restartRequired"] and dll.is_file() and opened.read(2) == b"MZ" try: update(lambda *args: None, lambda _: None, [candidate]) except ValueError as error: assert "другой установкой" in str(error) else: raise AssertionError("A stale helper must not replace the newer runtime") try: update(lambda *args: None, lambda _: None, [{**candidate, "sha256": "0" * 64}]) except ValueError as error: assert "Контрольная сумма" in str(error) else: raise AssertionError("Modified downloads must be rejected") assert json.loads(marker.read_text(encoding="utf-8-sig"))["current"] == after assert not list((application / "releases").glob(".ytdl-update-*")) == [], "The published release must remain" print(json.dumps({"stagedActivation": "passed", "openOldDLL": "preserved", "staleUpdate": "rejected", "badHash": "rejected"})) def outer(): artifacts = PROJECT / "test-artifacts"; artifacts.mkdir(exist_ok=True) with tempfile.TemporaryDirectory(prefix="update-test-", dir=artifacts) as temporary: root = Path(temporary) / "Helper" process = subprocess.run([str(PROJECT / "dist/YouTubeDL-Helper-Setup.exe"), "--silent", "--no-register", "--target-root", str(root)], capture_output=True, text=True, encoding="utf-8", timeout=180) assert process.returncode == 0, process.stdout + process.stderr records = [] for action in ("--inside", "--inside-packages"): marker = json.loads((root / "installation.json").read_text(encoding="utf-8-sig")) python = Path(marker["current"]) / "runtime/python/python.exe" result = subprocess.run([str(python), "-I", "-X", "utf8", "-B", str(Path(__file__).resolve()), action], capture_output=True, text=True, encoding="utf-8", timeout=600) assert result.returncode == 0, result.stdout[-1000:] + result.stderr[-4000:] records.append(json.loads(result.stdout.splitlines()[-1])) (artifacts / "component-update.json").write_text(json.dumps(records, indent=2), encoding="utf-8") print("Component updates passed: live PyPI downloads, verified stage activation, retained old DLL, bad hash rejection and stale-update rejection.") if __name__ == "__main__": if "--inside" in sys.argv: inside() elif "--inside-packages" in sys.argv: inside(True) else: outer()