Add Beat Saber plugin update audit skill
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: beatsaber-plugin-update-auditor
|
||||||
|
description: Audit Beat Saber plugin-helper locks for available plugin updates and follow-up work. Use when the user asks to check all Beat Saber plugins for updates, compare locked mods against GitHub or BeatMods, review plugin/version compatibility tracking notes, identify PR/local/manual builds that need follow-up, or skip paid/private Patreon and Discord plugin sources during update checks.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Beat Saber Plugin Update Auditor
|
||||||
|
|
||||||
|
Use this skill from the `plugin-helper` repo to produce an update audit, not to
|
||||||
|
blindly update plugins. Keep public, private, and experimental sources distinct.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Confirm repo context:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
test -f pyproject.toml && test -d src/plugin_helper && test -d locks && test -d registry/plugins
|
||||||
|
git status --short
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Choose the instance from the user request. If omitted, prefer `1.44.1` only
|
||||||
|
when that is clearly the active migration context; otherwise list instances:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src .venv/bin/python -m plugin_helper instances
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run the update audit with repo-local state unless the user explicitly targets
|
||||||
|
another configured profile:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state updates --instance <instance>
|
||||||
|
PYTHONPATH=src .venv/bin/python -m plugin_helper --state-dir .state updates --instance <instance> --json
|
||||||
|
```
|
||||||
|
|
||||||
|
The command checks ordinary GitHub locks, `beatmods-*` locks via BeatMods
|
||||||
|
verified metadata, skips `patreon/` and `discord/` sources, and marks local,
|
||||||
|
PR, manual, failed-trial, or pending-smoke entries as review items.
|
||||||
|
|
||||||
|
4. Read the compatibility tracker before proposing updates for review items:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sed -n '1,380p' docs/notes/install-and-verify-plugins-1.44.1.md
|
||||||
|
sed -n '1,220p' docs/notes/naluluna-mod-assistant.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Follow up on rows marked failed, omitted, currently failed, smoke blocked,
|
||||||
|
manual resmoke pending, local build, PR build, or local/private. Do not
|
||||||
|
reinstall failed or omitted plugins unless the notes identify a newer
|
||||||
|
compatible source.
|
||||||
|
|
||||||
|
5. For each public update candidate, inspect source notes before changing the
|
||||||
|
lock. Prefer upstream GitHub release artifacts when available; use BeatMods
|
||||||
|
CDN only for verified fallback cases, BeatMods-only packages, inaccessible
|
||||||
|
upstream assets, or framework/library dependencies.
|
||||||
|
|
||||||
|
6. Do not attempt automated public update checks for:
|
||||||
|
|
||||||
|
- `patreon/NalulunaModAssistant` or Naluluna bundles
|
||||||
|
- `discord/BeatSaberPlus` or BeatSaberPlus/ChatPlexSDK bundles
|
||||||
|
- other paid or closed-source plugin zips
|
||||||
|
|
||||||
|
Report them as skipped/manual follow-up only.
|
||||||
|
|
||||||
|
7. If the user asks to apply an update, switch to the `beatsaber-plugin-manager`
|
||||||
|
workflow for download, lock update, planning, apply, and smoketest.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Summarize in four groups:
|
||||||
|
|
||||||
|
- Public updates found: plugin, current version, candidate version, source.
|
||||||
|
- Current public locks: count only unless the user asks for every row.
|
||||||
|
- Review follow-ups: local builds, PR builds, failed trials, blocked smokes,
|
||||||
|
or notes saying compatibility work is pending.
|
||||||
|
- Skipped private sources: Patreon/Discord paid or closed-source packages.
|
||||||
|
|
||||||
|
Mention BeatMods/GitHub API errors separately from "no update found"; those are
|
||||||
|
unknowns, not proof that the plugin is current.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Beat Saber Plugin Update Auditor"
|
||||||
|
short_description: "Audit plugin locks for GitHub and BeatMods updates."
|
||||||
|
default_prompt: "Check the locked Beat Saber plugins for updates and review items."
|
||||||
@@ -152,6 +152,18 @@ Install assets are currently expected to already exist locally, usually under:
|
|||||||
<state-dir>/instances/<instance>/downloads/<plugin-id>/
|
<state-dir>/instances/<instance>/downloads/<plugin-id>/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Use `updates` to audit the version lock against obvious public update sources:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
PYTHONPATH=src python -m plugin_helper --state-dir .state updates --instance 1.44.1
|
||||||
|
```
|
||||||
|
|
||||||
|
The command checks GitHub release assets for ordinary repository-backed locks
|
||||||
|
and BeatMods verified metadata for `beatmods-*` locks. Patreon and Discord
|
||||||
|
paid/private sources are skipped, and local builds, PR builds, failed
|
||||||
|
compatibility trials, and manual installs are marked for review instead of being
|
||||||
|
treated as routine updates.
|
||||||
|
|
||||||
## Beat Saber Data Backups
|
## Beat Saber Data Backups
|
||||||
|
|
||||||
`backup-userdata` copies the mounted Windows `UserData` folder and Beat Saber
|
`backup-userdata` copies the mounted Windows `UserData` folder and Beat Saber
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
@@ -49,6 +52,24 @@ def normalize_mods(payload: Any) -> list[BeatModsEntry]:
|
|||||||
return [normalize_entry(entry) for entry in extract_mods(payload)]
|
return [normalize_entry(entry) for entry in extract_mods(payload)]
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_verified_mods(game_version: str) -> list[dict[str, Any]]:
|
||||||
|
query = urlencode(
|
||||||
|
{
|
||||||
|
"status": "verified",
|
||||||
|
"gameVersion": game_version,
|
||||||
|
"gameName": "BeatSaber",
|
||||||
|
"platform": "steampc",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
request = Request(
|
||||||
|
f"https://beatmods.com/api/mods?{query}",
|
||||||
|
headers={"User-Agent": "Mozilla/5.0 plugin-helper"},
|
||||||
|
)
|
||||||
|
with urlopen(request, timeout=20) as response:
|
||||||
|
data = json.loads(response.read().decode("utf-8"))
|
||||||
|
return extract_mods(data)
|
||||||
|
|
||||||
|
|
||||||
def by_version_id(entries: list[BeatModsEntry]) -> dict[int, BeatModsEntry]:
|
def by_version_id(entries: list[BeatModsEntry]) -> dict[int, BeatModsEntry]:
|
||||||
return {entry.version_id: entry for entry in entries if entry.version_id is not None}
|
return {entry.version_id: entry for entry in entries if entry.version_id is not None}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pathlib import Path
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .config import repo_root, resolve_runtime_config
|
from .config import repo_root, resolve_runtime_config
|
||||||
|
from .beatmods import fetch_verified_mods
|
||||||
from .bootstrap import run_bootstrap
|
from .bootstrap import run_bootstrap
|
||||||
from .bsipa import check_bsipa_health
|
from .bsipa import check_bsipa_health
|
||||||
from .checker import check_lock
|
from .checker import check_lock
|
||||||
@@ -33,12 +34,13 @@ def print_updates(report: dict[str, Any]) -> None:
|
|||||||
print(
|
print(
|
||||||
f"{report['instance']} updates: "
|
f"{report['instance']} updates: "
|
||||||
f"{summary['updates']} available, {summary['current']} current, "
|
f"{summary['updates']} available, {summary['current']} current, "
|
||||||
|
f"{summary.get('reviews', 0)} review, {summary.get('skipped', 0)} skipped, "
|
||||||
f"{summary['warnings']} warnings, {summary['errors']} errors"
|
f"{summary['warnings']} warnings, {summary['errors']} errors"
|
||||||
)
|
)
|
||||||
if not plugins:
|
if not plugins:
|
||||||
return
|
return
|
||||||
|
|
||||||
headers = ("Plugin", "Current", "Latest", "Asset", "Status")
|
headers = ("Plugin", "Current", "Latest", "Asset", "Status", "Notes")
|
||||||
rows = [
|
rows = [
|
||||||
(
|
(
|
||||||
f"{plugin['name']} ({plugin['id']})",
|
f"{plugin['name']} ({plugin['id']})",
|
||||||
@@ -46,6 +48,7 @@ def print_updates(report: dict[str, Any]) -> None:
|
|||||||
plugin.get("latestTag") or "(unknown)",
|
plugin.get("latestTag") or "(unknown)",
|
||||||
plugin.get("latestAsset") or plugin.get("currentAsset") or "(unknown)",
|
plugin.get("latestAsset") or plugin.get("currentAsset") or "(unknown)",
|
||||||
plugin["status"],
|
plugin["status"],
|
||||||
|
", ".join(plugin.get("reviewReasons") or plugin.get("messages") or ()),
|
||||||
)
|
)
|
||||||
for plugin in plugins
|
for plugin in plugins
|
||||||
]
|
]
|
||||||
@@ -142,7 +145,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
|
|
||||||
updates = subcommands.add_parser(
|
updates = subcommands.add_parser(
|
||||||
"updates",
|
"updates",
|
||||||
help="Check GitHub for newer matching releases for locked plugins",
|
help="Check GitHub and BeatMods for newer matching releases for locked plugins",
|
||||||
parents=[_common_parent()],
|
parents=[_common_parent()],
|
||||||
)
|
)
|
||||||
updates.add_argument("--instance", required=True)
|
updates.add_argument("--instance", required=True)
|
||||||
@@ -406,6 +409,7 @@ def run(argv: list[str] | None = None) -> int:
|
|||||||
registry=load_registry(registry_path),
|
registry=load_registry(registry_path),
|
||||||
lockfile=load_lockfile(lock_path),
|
lockfile=load_lockfile(lock_path),
|
||||||
fetch_releases=fetch_releases,
|
fetch_releases=fetch_releases,
|
||||||
|
fetch_beatmods=fetch_verified_mods,
|
||||||
selected=set(args.plugin) if args.plugin else None,
|
selected=set(args.plugin) if args.plugin else None,
|
||||||
include_prerelease=args.include_prerelease,
|
include_prerelease=args.include_prerelease,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,28 @@ import fnmatch
|
|||||||
import re
|
import re
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from .beatmods import BeatModsEntry, normalize_mods
|
||||||
from .models import Lockfile, Registry
|
from .models import Lockfile, Registry
|
||||||
|
|
||||||
|
|
||||||
FetchReleases = Callable[[str], list[dict[str, Any]]]
|
FetchReleases = Callable[[str], list[dict[str, Any]]]
|
||||||
|
FetchBeatMods = Callable[[str], list[dict[str, Any]]]
|
||||||
|
|
||||||
|
PRIVATE_SOURCE_PREFIXES = ("discord/", "patreon/")
|
||||||
|
REVIEW_PATTERNS = (
|
||||||
|
"local build",
|
||||||
|
"localbuild",
|
||||||
|
"github pr",
|
||||||
|
"/pull/",
|
||||||
|
" pr ",
|
||||||
|
"manual",
|
||||||
|
"compatibility trial",
|
||||||
|
"failed compatibility",
|
||||||
|
"currently failed",
|
||||||
|
"smoke blocked",
|
||||||
|
"resmoke pending",
|
||||||
|
"do not reinstall",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _release_tag(release: dict[str, Any]) -> str:
|
def _release_tag(release: dict[str, Any]) -> str:
|
||||||
@@ -23,6 +41,71 @@ def _asset_name(asset: dict[str, Any]) -> str:
|
|||||||
return str(asset.get("name") or "")
|
return str(asset.get("name") or "")
|
||||||
|
|
||||||
|
|
||||||
|
def _source_kind(repo: str | None) -> str:
|
||||||
|
if not repo:
|
||||||
|
return "missing"
|
||||||
|
lowered = repo.lower()
|
||||||
|
if lowered.startswith(PRIVATE_SOURCE_PREFIXES):
|
||||||
|
return "private"
|
||||||
|
if re.fullmatch(r"[^/\s]+/[^/\s]+", repo):
|
||||||
|
return "github"
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def _is_beatmods_locked(tag: str | None, reason: str | None) -> bool:
|
||||||
|
return str(tag or "").startswith("beatmods-") or "beatmods" in str(reason or "").lower()
|
||||||
|
|
||||||
|
|
||||||
|
def _current_beatmods_version(tag: str | None, reason: str | None, asset: str | None) -> str | None:
|
||||||
|
if tag and tag.startswith("beatmods-"):
|
||||||
|
return tag.removeprefix("beatmods-")
|
||||||
|
for text in (reason, asset):
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
match = re.search(r"\bBeatMods(?:\s+verified)?\s+[^0-9]*(\d+(?:\.\d+){1,4})\b", text, re.I)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
match = re.search(r"-(\d+(?:\.\d+){1,4})(?:\.zip|\+|-|$)", text)
|
||||||
|
if match:
|
||||||
|
return match.group(1)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _normalized_name(text: str) -> str:
|
||||||
|
return re.sub(r"[^a-z0-9]+", "", text.lower())
|
||||||
|
|
||||||
|
|
||||||
|
def _beatmods_match(
|
||||||
|
entries: list[BeatModsEntry],
|
||||||
|
*,
|
||||||
|
plugin_id: str,
|
||||||
|
plugin_name: str,
|
||||||
|
) -> BeatModsEntry | None:
|
||||||
|
wanted = {_normalized_name(plugin_id), _normalized_name(plugin_name)}
|
||||||
|
for entry in entries:
|
||||||
|
if _normalized_name(entry.name) in wanted:
|
||||||
|
return entry
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _review_reasons(tag: str | None, reason: str | None) -> list[str]:
|
||||||
|
text = f"{tag or ''} {reason or ''}".lower()
|
||||||
|
reasons: list[str] = []
|
||||||
|
if any(pattern in text for pattern in ("local build", "localbuild")):
|
||||||
|
reasons.append("local build")
|
||||||
|
if any(pattern in text for pattern in ("github pr", "/pull/", " pr ")) or str(tag or "").startswith("pr-"):
|
||||||
|
reasons.append("pull request build")
|
||||||
|
if "failed" in text or "compatibility trial" in text:
|
||||||
|
reasons.append("compatibility failure history")
|
||||||
|
if "smoke blocked" in text or "resmoke pending" in text or "currently failed" in text:
|
||||||
|
reasons.append("verification follow-up pending")
|
||||||
|
if "manual" in text:
|
||||||
|
reasons.append("manual install notes")
|
||||||
|
if not reasons and any(pattern in text for pattern in REVIEW_PATTERNS):
|
||||||
|
reasons.append("special handling noted")
|
||||||
|
return reasons
|
||||||
|
|
||||||
|
|
||||||
def _semver_key(tag: str) -> tuple[int, tuple[int, ...], str]:
|
def _semver_key(tag: str) -> tuple[int, tuple[int, ...], str]:
|
||||||
match = re.search(r"(\d+(?:\.\d+){0,3})", tag)
|
match = re.search(r"(\d+(?:\.\d+){0,3})", tag)
|
||||||
if not match:
|
if not match:
|
||||||
@@ -99,18 +182,22 @@ def check_updates(
|
|||||||
registry: Registry,
|
registry: Registry,
|
||||||
lockfile: Lockfile,
|
lockfile: Lockfile,
|
||||||
fetch_releases: FetchReleases,
|
fetch_releases: FetchReleases,
|
||||||
|
fetch_beatmods: FetchBeatMods | None = None,
|
||||||
selected: set[str] | None = None,
|
selected: set[str] | None = None,
|
||||||
include_prerelease: bool = False,
|
include_prerelease: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
selected_ids = selected or {plugin.id for plugin in lockfile.plugins}
|
selected_ids = selected or {plugin.id for plugin in lockfile.plugins}
|
||||||
plugins: list[dict[str, Any]] = []
|
plugins: list[dict[str, Any]] = []
|
||||||
summary = {"current": 0, "updates": 0, "warnings": 0, "errors": 0}
|
summary = {"current": 0, "updates": 0, "warnings": 0, "errors": 0, "skipped": 0, "reviews": 0}
|
||||||
|
beatmods_entries: list[BeatModsEntry] | None = None
|
||||||
|
beatmods_error: Exception | None = None
|
||||||
|
|
||||||
for locked in lockfile.plugins:
|
for locked in lockfile.plugins:
|
||||||
if locked.id not in selected_ids:
|
if locked.id not in selected_ids:
|
||||||
continue
|
continue
|
||||||
registry_plugin = registry.get(locked.id)
|
registry_plugin = registry.get(locked.id)
|
||||||
repo = locked.repo or (registry_plugin.repo if registry_plugin else None)
|
repo = locked.repo or (registry_plugin.repo if registry_plugin else None)
|
||||||
|
review_reasons = _review_reasons(locked.tag, locked.reason)
|
||||||
entry: dict[str, Any] = {
|
entry: dict[str, Any] = {
|
||||||
"id": locked.id,
|
"id": locked.id,
|
||||||
"name": registry_plugin.name if registry_plugin else locked.id,
|
"name": registry_plugin.name if registry_plugin else locked.id,
|
||||||
@@ -123,13 +210,106 @@ def check_updates(
|
|||||||
"latestAssetSha256": None,
|
"latestAssetSha256": None,
|
||||||
"status": "unknown",
|
"status": "unknown",
|
||||||
"messages": [],
|
"messages": [],
|
||||||
|
"review": bool(review_reasons),
|
||||||
|
"reviewReasons": review_reasons,
|
||||||
}
|
}
|
||||||
|
if review_reasons:
|
||||||
|
summary["reviews"] += 1
|
||||||
|
|
||||||
|
source_kind = _source_kind(repo)
|
||||||
|
if source_kind == "private":
|
||||||
|
entry["status"] = "skipped"
|
||||||
|
entry["messages"].append("paid/private source; check manually outside public update APIs")
|
||||||
|
summary["skipped"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
needs_manual_review = any(
|
||||||
|
reason in review_reasons
|
||||||
|
for reason in (
|
||||||
|
"local build",
|
||||||
|
"pull request build",
|
||||||
|
"verification follow-up pending",
|
||||||
|
"manual install notes",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if review_reasons and (needs_manual_review or not _is_beatmods_locked(locked.tag, locked.reason)):
|
||||||
|
entry["status"] = "review"
|
||||||
|
entry["messages"].append("special install or compatibility history; review notes before updating")
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if _is_beatmods_locked(locked.tag, locked.reason):
|
||||||
|
if fetch_beatmods is None:
|
||||||
|
entry["status"] = "warning"
|
||||||
|
entry["messages"].append("BeatMods check unavailable")
|
||||||
|
summary["warnings"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
if beatmods_entries is None and beatmods_error is None:
|
||||||
|
try:
|
||||||
|
beatmods_entries = normalize_mods(fetch_beatmods(lockfile.beat_saber_version))
|
||||||
|
except Exception as exc:
|
||||||
|
beatmods_error = exc
|
||||||
|
if beatmods_error is not None:
|
||||||
|
entry["status"] = "error"
|
||||||
|
entry["messages"].append(f"BeatMods: {beatmods_error}")
|
||||||
|
summary["errors"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
beatmods_match = _beatmods_match(
|
||||||
|
beatmods_entries or [],
|
||||||
|
plugin_id=locked.id,
|
||||||
|
plugin_name=entry["name"],
|
||||||
|
)
|
||||||
|
if beatmods_match is not None and beatmods_match.mod_version:
|
||||||
|
current_version = _current_beatmods_version(locked.tag, locked.reason, locked.asset)
|
||||||
|
entry["latestTag"] = f"beatmods-{beatmods_match.mod_version}"
|
||||||
|
entry["latestAsset"] = (
|
||||||
|
f"{beatmods_match.name}-{beatmods_match.mod_version}.zip"
|
||||||
|
if beatmods_match.name
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
entry["latestBeatModsVersionId"] = beatmods_match.version_id
|
||||||
|
entry["latestBeatModsZipHash"] = beatmods_match.zip_hash
|
||||||
|
entry["latestAssetUrl"] = (
|
||||||
|
f"https://beatmods.com/cdn/mod/{beatmods_match.zip_hash}.zip"
|
||||||
|
if beatmods_match.zip_hash
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if current_version == beatmods_match.mod_version:
|
||||||
|
entry["status"] = "current"
|
||||||
|
summary["current"] += 1
|
||||||
|
else:
|
||||||
|
entry["status"] = "update"
|
||||||
|
entry["messages"].append(
|
||||||
|
f"BeatMods verified {beatmods_match.mod_version} for Beat Saber {lockfile.beat_saber_version}"
|
||||||
|
)
|
||||||
|
summary["updates"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if source_kind != "github":
|
||||||
|
entry["status"] = "warning"
|
||||||
|
entry["messages"].append("no matching BeatMods verified entry found")
|
||||||
|
summary["warnings"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
entry["messages"].append("no matching BeatMods verified entry found; falling back to GitHub")
|
||||||
|
|
||||||
if not repo:
|
if not repo:
|
||||||
entry["status"] = "warning"
|
entry["status"] = "warning"
|
||||||
entry["messages"].append("missing repository")
|
entry["messages"].append("missing repository")
|
||||||
summary["warnings"] += 1
|
summary["warnings"] += 1
|
||||||
plugins.append(entry)
|
plugins.append(entry)
|
||||||
continue
|
continue
|
||||||
|
if source_kind != "github":
|
||||||
|
entry["status"] = "warning"
|
||||||
|
entry["messages"].append("unsupported repository source")
|
||||||
|
summary["warnings"] += 1
|
||||||
|
plugins.append(entry)
|
||||||
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
releases = fetch_releases(repo)
|
releases = fetch_releases(repo)
|
||||||
|
|||||||
@@ -1703,6 +1703,129 @@ instance = "1.40.8"
|
|||||||
self.assertEqual(result["plugins"][0]["status"], "update")
|
self.assertEqual(result["plugins"][0]["status"], "update")
|
||||||
self.assertEqual(result["plugins"][0]["latestAssetSha256"], "new")
|
self.assertEqual(result["plugins"][0]["latestAssetSha256"], "new")
|
||||||
|
|
||||||
|
def test_update_check_reports_beatmods_update(self) -> None:
|
||||||
|
registry = Registry(
|
||||||
|
{
|
||||||
|
"songcore": RegistryPlugin(
|
||||||
|
id="songcore",
|
||||||
|
name="SongCore",
|
||||||
|
repo="Kylemc1413/SongCore",
|
||||||
|
asset_patterns=("SongCore-*.zip",),
|
||||||
|
install_strategy="bsipa-zip",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lockfile = Lockfile(
|
||||||
|
beat_saber_version="1.44.1",
|
||||||
|
instance="1.44.1",
|
||||||
|
plugins=(
|
||||||
|
LockedPlugin(
|
||||||
|
id="songcore",
|
||||||
|
repo="Kylemc1413/SongCore",
|
||||||
|
tag="beatmods-3.16.0",
|
||||||
|
asset="SongCore-3.16.0.zip",
|
||||||
|
sha256="old",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = check_updates(
|
||||||
|
registry=registry,
|
||||||
|
lockfile=lockfile,
|
||||||
|
fetch_releases=lambda repo: [],
|
||||||
|
fetch_beatmods=lambda game_version: [
|
||||||
|
{
|
||||||
|
"mod": {"id": 1, "name": "SongCore", "gitUrl": "https://github.com/Kylemc1413/SongCore"},
|
||||||
|
"latest": {"id": 2600, "modVersion": "3.17.0", "zipHash": "abc"},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["summary"]["updates"], 1)
|
||||||
|
self.assertEqual(result["plugins"][0]["status"], "update")
|
||||||
|
self.assertEqual(result["plugins"][0]["latestTag"], "beatmods-3.17.0")
|
||||||
|
self.assertEqual(result["plugins"][0]["latestBeatModsZipHash"], "abc")
|
||||||
|
|
||||||
|
def test_update_check_skips_private_sources(self) -> None:
|
||||||
|
registry = Registry(
|
||||||
|
{
|
||||||
|
"naluluna": RegistryPlugin(
|
||||||
|
id="naluluna",
|
||||||
|
name="Naluluna",
|
||||||
|
repo="patreon/NalulunaModAssistant",
|
||||||
|
install_strategy="root-zip",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lockfile = Lockfile(
|
||||||
|
beat_saber_version="1.44.1",
|
||||||
|
instance="1.44.1",
|
||||||
|
plugins=(
|
||||||
|
LockedPlugin(
|
||||||
|
id="naluluna",
|
||||||
|
repo="patreon/NalulunaModAssistant",
|
||||||
|
tag="installed-1.44.1",
|
||||||
|
asset="Naluluna-1.44.1-installed-bundle.zip",
|
||||||
|
sha256="hash",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = check_updates(
|
||||||
|
registry=registry,
|
||||||
|
lockfile=lockfile,
|
||||||
|
fetch_releases=lambda repo: self.fail("private sources should not query GitHub"),
|
||||||
|
fetch_beatmods=lambda game_version: self.fail("private sources should not query BeatMods"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["summary"]["skipped"], 1)
|
||||||
|
self.assertEqual(result["plugins"][0]["status"], "skipped")
|
||||||
|
self.assertIn("paid/private", result["plugins"][0]["messages"][0])
|
||||||
|
|
||||||
|
def test_update_check_marks_local_pr_build_for_review(self) -> None:
|
||||||
|
registry = Registry(
|
||||||
|
{
|
||||||
|
"jdfixer": RegistryPlugin(
|
||||||
|
id="jdfixer",
|
||||||
|
name="JDFixer",
|
||||||
|
repo="zeph-yr/JDFixer",
|
||||||
|
asset_patterns=("JDFixer.dll",),
|
||||||
|
install_strategy="dll-to-plugins",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
lockfile = Lockfile(
|
||||||
|
beat_saber_version="1.44.1",
|
||||||
|
instance="1.44.1",
|
||||||
|
plugins=(
|
||||||
|
LockedPlugin(
|
||||||
|
id="jdfixer",
|
||||||
|
repo="zeph-yr/JDFixer",
|
||||||
|
tag="pr-26-3fce6ce",
|
||||||
|
asset="JDFixer.dll",
|
||||||
|
sha256="hash",
|
||||||
|
reason=(
|
||||||
|
"Local build from GitHub PR https://github.com/zeph-yr/JDFixer/pull/26. "
|
||||||
|
"Use this PR build instead of the failed upstream release asset."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
result = check_updates(
|
||||||
|
registry=registry,
|
||||||
|
lockfile=lockfile,
|
||||||
|
fetch_releases=lambda repo: self.fail("review builds should not be treated as routine GitHub checks"),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(result["summary"]["reviews"], 1)
|
||||||
|
self.assertEqual(result["plugins"][0]["status"], "review")
|
||||||
|
self.assertEqual(result["plugins"][0]["reviewReasons"], [
|
||||||
|
"local build",
|
||||||
|
"pull request build",
|
||||||
|
"compatibility failure history",
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
def _make_tui_fixture(
|
def _make_tui_fixture(
|
||||||
root: Path,
|
root: Path,
|
||||||
|
|||||||
Reference in New Issue
Block a user