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:
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user