diff --git a/gui/main.py b/gui/main.py index ab43ec8..955d5b3 100644 --- a/gui/main.py +++ b/gui/main.py @@ -78,7 +78,9 @@ class UpdateThread(QThread): REPO_OWNER = "dazed" REPO_SLUG = "dazed-mtl-tool" REPO_BRANCH = "main" - ARCHIVE_ROOT = "dazed-mtl-tool" + # Gitea names the archive top folder after the repo display name (currently + # "dazedtl"). Resolve dynamically so a rename cannot silently no-op again. + ARCHIVE_ROOT = "dazedtl" SHA_FILE = str(LAST_UPDATE_SHA_PATH) # Top-level paths that should never be touched during update @@ -117,6 +119,25 @@ class UpdateThread(QThread): return False return True + @classmethod + def resolve_archive_root(cls, extract_dir: Path) -> Path: + """Locate the single top-level folder inside an extracted archive. + + Prefers ``ARCHIVE_ROOT`` when present; otherwise accepts exactly one + subdirectory (Gitea zip layout). Raises if the layout is unexpected. + """ + preferred = extract_dir / cls.ARCHIVE_ROOT + if preferred.is_dir(): + return preferred + dirs = sorted(p for p in extract_dir.iterdir() if p.is_dir()) + if len(dirs) == 1: + return dirs[0] + found = [p.name for p in dirs] + raise FileNotFoundError( + f"Could not find update archive root under {extract_dir} " + f"(expected {cls.ARCHIVE_ROOT!r}; found {found})" + ) + @staticmethod def _fmt_bytes(num: int) -> str: if num >= 1024 * 1024: @@ -211,16 +232,21 @@ class UpdateThread(QThread): with zipfile.ZipFile(zip_path, "r") as zf: zf.extractall(tmp) - extracted = tmp / self.ARCHIVE_ROOT - root = Path(".").resolve() + extracted = self.resolve_archive_root(tmp) + root = PROJECT_ROOT.resolve() install_files = [ src for src in extracted.rglob("*") if src.is_file() and self.should_install(src.relative_to(extracted)) ] - total_files = max(len(install_files), 1) + if not install_files: + raise RuntimeError( + "Update archive contained no installable files " + f"(root={extracted.name!r})" + ) + total_files = len(install_files) self.progress.emit("Applying", 85, "Installing updated files…") for index, src in enumerate(install_files, start=1): rel = src.relative_to(extracted) diff --git a/tests/test_bundled_updates.py b/tests/test_bundled_updates.py index f153f74..c6130c4 100644 --- a/tests/test_bundled_updates.py +++ b/tests/test_bundled_updates.py @@ -62,6 +62,67 @@ class UpdateThreadInstallFilterTests(unittest.TestCase): self.assertFalse(UpdateThread.should_install(Path(rel))) +class UpdateThreadArchiveRootTests(unittest.TestCase): + """Gitea zips nest files under a single top folder (repo display name).""" + + def test_prefers_configured_archive_root(self): + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + preferred = base / UpdateThread.ARCHIVE_ROOT + preferred.mkdir() + (base / "other").mkdir() + self.assertEqual(UpdateThread.resolve_archive_root(base), preferred) + + def test_falls_back_to_single_subdirectory(self): + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + only = base / "renamed-repo-folder" + only.mkdir() + (only / "gui").mkdir() + (only / "gui" / "main.py").write_text("# ok", encoding="utf-8") + self.assertEqual(UpdateThread.resolve_archive_root(base), only) + + def test_raises_when_archive_root_missing(self): + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + (base / "a").mkdir() + (base / "b").mkdir() + with self.assertRaises(FileNotFoundError): + UpdateThread.resolve_archive_root(base) + + def test_stale_archive_root_name_would_have_installed_nothing(self): + """Regression: wrong ARCHIVE_ROOT used to report success with 0 files.""" + from gui.main import UpdateThread + + with tempfile.TemporaryDirectory() as raw: + base = Path(raw) + real = base / "dazedtl" + real.mkdir() + (real / "gui").mkdir() + (real / "gui" / "main.py").write_text("print('hi')\n", encoding="utf-8") + (real / "data").mkdir() + (real / "data" / "help").mkdir() + (real / "data" / "help" / "00-welcome.md").write_text("# hi\n", encoding="utf-8") + + # Old hardcoded slug before the DazedTL rebrand. + stale = base / "dazed-mtl-tool" + self.assertFalse(stale.exists()) + self.assertEqual(list(stale.rglob("*")), []) + + extracted = UpdateThread.resolve_archive_root(base) + install_files = [ + src + for src in extracted.rglob("*") + if src.is_file() and UpdateThread.should_install(src.relative_to(extracted)) + ] + self.assertGreaterEqual(len(install_files), 2) + class ShippedDataTrackingTests(unittest.TestCase): """Guide/help and other shipped data/ files must be git-trackable. @@ -417,8 +478,11 @@ class MaintainerRefreshTests(unittest.TestCase): with patch.object(forge, "_fetch_plugins", return_value=plugin): self.assertTrue(forge.refresh_forge_plugins(log_fn=None)) - self.assertEqual((base / "Forge_MV.js").read_bytes(), plugin) + # Maintainer refresh updates MZ modern only; MV stays on the legacy bundle. + self.assertEqual((base / "Forge_MZ.js").read_bytes(), plugin) self.assertEqual((upstream / "Forge_MZ.js").read_bytes(), plugin) + self.assertFalse((base / "Forge_MV.js").exists()) + self.assertFalse((upstream / "Forge_MV.js").exists()) if __name__ == "__main__":