mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
feat(career): host higher venues as opt-in content packs (#1023)
* feat(career): host higher venues as opt-in content packs Move the club and arena venue packs (~678 MB of crowd MP4s) out of the bundle and download them on demand, keeping the bar starter bundled so career still works offline. Leans on career's existing pack pipeline (_download_pack: stream -> sha256 -> extract -> validate -> swap), which already degrades gracefully when a pack is absent. - venues.json: club/arena gain `pack` URLs pointing at per-pack, versioned, immutable releases (venue-<id>-v<N>, matching the existing venue-arena-v1). Arena's sha256/bytes are the real published asset (verified end-to-end); club is a placeholder until its release is published. - tools/content_packs.py: reusable, reproducible pack build/publish/manifest tool. Byte-identical output for identical media (fixed order/mtime/perms, STORED) so a pack's hash can be known before upload. --local (file://) for offline tests, --publish for the per-pack release. Has a --selfcheck. - .github/workflows/content-packs.yml: workflow_dispatch automation that builds/publishes packs and opens the venues.json manifest-bump PR, so publishing is never a manual checklist. - test: round-trips a tool-built pack through career's real _download_pack. Part of the nightly-slimming effort (feedBack-desktop#122). The desktop bundle change (stop shipping club/arena) is a companion PR. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(career): don't offer a venue pack until its release is published A committed venues.json entry carries a 0-byte placeholder (and all-zero sha) until its release exists. Previously has_pack was true as soon as a `pack` object was present, so the UI showed a "Download" button that could only fail (the placeholder URL 404s). Gate on a real, publish-stamped size via _pack_published(): the card shows "coming soon" and the download endpoint 404s until the pack is actually published. Caught by a real bundle+runtime smoke. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(content-packs): address CodeRabbit review on #1023 - workflow: stop interpolating dispatch inputs into Bash (template injection flagged by zizmor). Pass venues/version via env, validate formats, use an argument array. - content_packs: reject top-level files the career downloader would refuse (PACK_FILENAME_RE) before publishing — a stray .DS_Store would otherwise ship and fail _validate_pack_dir for every client. + test. - content_packs: pin ZipInfo.create_system=3 so packs hash identically across Windows/Unix runners (was the documented reproducibility caveat). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(changelog): note opt-in career venue packs (#122) Signed-off-by: Matthew Harris Glover <matthew@harrisglover.com> * docs(content_packs): correct --publish usage in module docstring --publish is a flag (no tag arg) and publish() deliberately omits --clobber; the docstring said otherwise. Signed-off-by: byrongamatos <xasiklas@gmail.com> --------- Signed-off-by: Matthew Harris Glover <matthew@harrisglover.com> Signed-off-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
03e1c1d57e
commit
59bcf338a3
@@ -83,10 +83,25 @@ def test_download_without_published_pack_404s(client):
|
||||
|
||||
def test_download_locked_venue_403s(client, monkeypatch):
|
||||
club = career_routes._venue("club")
|
||||
monkeypatch.setitem(club, "pack", {"url": "http://x/pack.zip", "sha256": "0" * 64})
|
||||
monkeypatch.setitem(club, "pack", {"url": "http://x/pack.zip", "sha256": "0" * 64, "bytes": 123})
|
||||
assert client.post("/api/plugins/career/packs/club/download").status_code == 403
|
||||
|
||||
|
||||
def test_placeholder_pack_is_not_offered_until_published(client, monkeypatch):
|
||||
# A committed manifest carries a 0-byte placeholder until its release is
|
||||
# published. Such a pack must not be offered (has_pack False) and its
|
||||
# download must 404 — else the UI shows a button that can only fail.
|
||||
monkeypatch.setattr(career_routes, "_bundled", lambda vid: False)
|
||||
club = career_routes._venue("club")
|
||||
monkeypatch.setitem(club, "pack",
|
||||
{"url": "http://x/c.zip", "sha256": "0" * 64, "bytes": 0})
|
||||
by_id = {v["id"]: v for v in client.get("/api/plugins/career/state").json()["venues"]}
|
||||
assert by_id["club"]["has_pack"] is False # placeholder → not offered
|
||||
assert by_id["arena"]["has_pack"] is True # arena ships real bytes
|
||||
# Even forced, an unpublished pack won't start a download.
|
||||
assert client.post("/api/plugins/career/packs/club/download").status_code == 404
|
||||
|
||||
|
||||
def test_bundled_bar_pack_is_installed_and_served(client):
|
||||
state = client.get("/api/plugins/career/state").json()
|
||||
bar = {v["id"]: v for v in state["venues"]}["bar"]
|
||||
@@ -167,9 +182,53 @@ def test_download_worker_end_to_end(client, tmp_path):
|
||||
assert "sha256" in bad["error"]
|
||||
|
||||
|
||||
def test_content_packs_build_roundtrips_through_download(client, tmp_path):
|
||||
# tools/content_packs.py must produce a zip the real career worker accepts:
|
||||
# build_pack → manifest_entry → _download_pack → installed.
|
||||
from tools import content_packs
|
||||
|
||||
src = tmp_path / "bar"
|
||||
src.mkdir()
|
||||
for s in career_routes.REQUIRED_LOOPS:
|
||||
(src / f"{s}.mp4").write_bytes(b"fake-" + s.encode())
|
||||
(src / "cheer.mp4").write_bytes(b"fake-cheer")
|
||||
(src / "manifest.json").write_text(json.dumps({
|
||||
"venue": "bar", "version": 1,
|
||||
"loops": {s: f"{s}.mp4" for s in career_routes.REQUIRED_LOOPS},
|
||||
"stingers": {"cheer": "cheer.mp4"},
|
||||
}))
|
||||
out_dir = tmp_path / "packs"
|
||||
zip_path = out_dir / content_packs.pack_asset("bar", 1)
|
||||
info = content_packs.build_pack(src, zip_path)
|
||||
entry = content_packs.manifest_entry(zip_path, zip_path.resolve().as_uri())
|
||||
assert entry["sha256"] == info["sha256"] and entry["bytes"] == info["bytes"]
|
||||
|
||||
progress = {"status": "running", "bytes_done": 0, "bytes_total": 0, "error": None}
|
||||
career_routes._download_pack("bar", entry, progress)
|
||||
assert progress["status"] == "done", progress["error"]
|
||||
assert career_routes._installed("bar")
|
||||
|
||||
|
||||
def test_content_packs_rejects_files_the_downloader_would_refuse(tmp_path):
|
||||
# A stray file (e.g. macOS .DS_Store) must fail the build, not get published
|
||||
# and then break every client's download at _validate_pack_dir.
|
||||
from tools import content_packs
|
||||
|
||||
src = tmp_path / "bar"
|
||||
src.mkdir()
|
||||
(src / "bored.mp4").write_bytes(b"fake")
|
||||
(src / ".DS_Store").write_bytes(b"junk")
|
||||
try:
|
||||
content_packs.build_pack(src, tmp_path / "bar-pack-v1.zip")
|
||||
except ValueError as e:
|
||||
assert "downloader will reject" in str(e)
|
||||
else:
|
||||
raise AssertionError("build_pack accepted a .DS_Store the downloader rejects")
|
||||
|
||||
|
||||
def test_double_download_409s(client, monkeypatch):
|
||||
bar = career_routes._venue("bar")
|
||||
monkeypatch.setitem(bar, "pack", {"url": "http://x/pack.zip", "sha256": "0" * 64})
|
||||
monkeypatch.setitem(bar, "pack", {"url": "http://x/pack.zip", "sha256": "0" * 64, "bytes": 123})
|
||||
# Pretend one is already running.
|
||||
career_routes._state["downloads"]["bar"] = {"status": "running"}
|
||||
assert client.post("/api/plugins/career/packs/bar/download").status_code == 409
|
||||
|
||||
Reference in New Issue
Block a user