v0.5.112: keep skill demos at 16:9 and harden web refresh deploy triggers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
voson
2026-07-30 01:48:01 +08:00
co-authored by Cursor
parent 28858c0703
commit fdd926e9bb
11 changed files with 295 additions and 48 deletions
+150
View File
@@ -1,9 +1,11 @@
from __future__ import annotations
import json
import os
import sys
import tempfile
import unittest
import urllib.error
from datetime import datetime, timedelta, timezone
from pathlib import Path
from unittest.mock import patch
@@ -14,8 +16,10 @@ sys.path.insert(0, str(ROOT / "web"))
from web import notify_site_traffic, refresh_web
from web.fetch_stratz_meta import preserve_failed_meta_brackets
from web.fetch_streamer_live import probe_streamers
from web.refresh_cache import restore_patches, restore_streamers
from shared.http_utils import write_json_atomic
from shared.paths import HEROES_JSON, RELATIONS_JSON, WEB_FRONTEND
class RefreshDigestTests(unittest.TestCase):
@@ -45,6 +49,82 @@ class RefreshDigestTests(unittest.TestCase):
)
self.assertNotEqual(before, refresh_web._semantic_file_digest(path))
def test_semantic_digest_ignores_streamer_is_live(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "streamers.json"
path.write_text(
json.dumps(
{
"fetched_at": "old",
"streamers": [
{
"id": "a",
"nickname": "A",
"is_live": False,
"live_probed_at": "t1",
}
],
}
),
encoding="utf-8",
)
before = refresh_web._semantic_file_digest(path)
path.write_text(
json.dumps(
{
"fetched_at": "new",
"streamers": [
{
"id": "a",
"nickname": "A",
"is_live": True,
"live_probed_at": "t2",
}
],
}
),
encoding="utf-8",
)
self.assertEqual(before, refresh_web._semantic_file_digest(path))
path.write_text(
json.dumps(
{
"fetched_at": "new",
"streamers": [
{
"id": "a",
"nickname": "B",
"is_live": True,
"live_probed_at": "t2",
}
],
}
),
encoding="utf-8",
)
self.assertNotEqual(before, refresh_web._semantic_file_digest(path))
def test_snapshot_watches_frontend_and_shared_inputs(self) -> None:
snap = refresh_web.snapshot()
self.assertIn(refresh_web._watch_key(RELATIONS_JSON), snap)
self.assertIn(refresh_web._watch_key(HEROES_JSON), snap)
self.assertIn(refresh_web._watch_key(WEB_FRONTEND / "app.js"), snap)
self.assertIn(
refresh_web._watch_key(WEB_FRONTEND / "functions") + "/",
snap,
)
self.assertIn(
refresh_web._watch_key(refresh_web.ROOT / "data" / "hero_grid_order.json"),
snap,
)
def test_frontend_change_is_data_not_asset(self) -> None:
before = {"web/frontend/app.js": "a", "web/assets/item_icons/": "x"}
after = {"web/frontend/app.js": "b", "web/assets/item_icons/": "x"}
data_changed, assets_changed = refresh_web.diff_snapshots(before, after)
self.assertTrue(data_changed)
self.assertFalse(assets_changed)
def test_assets_cannot_deploy_when_oss_is_skipped(self) -> None:
with self.assertRaises(RuntimeError):
refresh_web.validate_publish_options(
@@ -58,6 +138,31 @@ class RefreshDigestTests(unittest.TestCase):
skip_deploy=True,
)
def test_dry_run_skips_cache_restore_and_summary_write(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
summary_path = Path(tmp) / "summary.json"
calls: list[tuple] = []
def fake_run_script(script: str, *args: str, dry_run: bool = False, soft_fail: bool = False):
calls.append((script, args, dry_run))
return True
env = {"REFRESH_CACHE_ENABLED": "1"}
with (
patch.object(refresh_web, "SUMMARY_PATH", summary_path),
patch.object(refresh_web, "acquire_refresh_lock", return_value=object()),
patch.object(refresh_web, "release_refresh_lock"),
patch.object(refresh_web, "run_script", side_effect=fake_run_script),
patch.object(refresh_web, "run_weekly"),
patch.object(refresh_web, "run_daily", return_value=False),
patch.object(refresh_web, "run_patch_tier", return_value=False),
patch.dict(os.environ, env, clear=False),
patch.object(sys, "argv", ["refresh_web.py", "--tier", "weekly", "--dry-run"]),
):
refresh_web.main()
self.assertFalse(summary_path.is_file())
self.assertFalse(any(script == "refresh_cache.py" for script, *_ in calls))
def test_streamer_cache_preserves_manual_rows_and_restores_runtime_fields(self) -> None:
with tempfile.TemporaryDirectory() as tmp:
cached = Path(tmp) / "cached.json"
@@ -132,6 +237,51 @@ class RefreshDigestTests(unittest.TestCase):
self.assertEqual(set(payload["lookup"]["heroes"]), {"1", "2"})
class LiveProbeFallbackTests(unittest.TestCase):
def test_probe_failure_clears_stale_live_badge(self) -> None:
payload = {
"streamers": [
{
"id": "a",
"live_url": "https://live.bilibili.com/123",
"is_live": True,
"live_probed_at": "2026-07-29T00:00:00+00:00",
}
]
}
def boom(_room_id: str) -> bool:
raise urllib.error.URLError("blocked")
with patch("web.fetch_streamer_live.probe_bilibili", side_effect=boom):
live, offline, fail = probe_streamers(payload)
self.assertEqual((live, offline, fail), (0, 0, 1))
row = payload["streamers"][0]
self.assertFalse(row["is_live"])
self.assertNotIn("live_probed_at", row)
class NotifySummaryTests(unittest.TestCase):
def test_skipped_summary_is_labeled(self) -> None:
self.assertEqual(
notify_site_traffic._fmt_refresh_summary_result(
{
"ok": True,
"skipped": True,
"skip_reason": "full refresh already running",
}
),
"已跳过(full refresh already running",
)
self.assertFalse(
notify_site_traffic._summary_proves_refresh(
{"ok": True, "skipped": True, "steps": [{"step": "fetch_hero_stats.py", "ok": True}]},
"英雄统计",
"fetch_hero_stats.py",
)
)
class StratzFallbackTests(unittest.TestCase):
def test_failed_bracket_keeps_previous_data_and_marks_stale(self) -> None:
fresh = {