Files

178 lines
6.3 KiB
Python

from __future__ import annotations
import importlib.util
import json
from pathlib import Path
import pytest
from deerflow.skills.parser import parse_skill_file
from deerflow.skills.storage.local_skill_storage import LocalSkillStorage
from deerflow.skills.types import SkillCategory
from deerflow.skills.validation import _validate_skill_frontmatter
BACKEND_ROOT = Path(__file__).resolve().parents[1]
SKILL_ROOT = BACKEND_ROOT / "skills" / "public" / "youtube-page-generator"
SCRIPT_PATH = SKILL_ROOT / "scripts" / "render_youtube_page.py"
LEAD_PROMPT_PATH = (
BACKEND_ROOT
/ "packages"
/ "harness"
/ "deerflow"
/ "agents"
/ "lead_agent"
/ "prompt.py"
)
def _load_renderer():
spec = importlib.util.spec_from_file_location(
"youtube_page_renderer",
SCRIPT_PATH,
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
def test_skill_is_discoverable_and_valid() -> None:
valid, error, _metadata = _validate_skill_frontmatter(SKILL_ROOT)
assert valid, error
skill = parse_skill_file(SKILL_ROOT / "SKILL.md", SkillCategory.PUBLIC)
assert skill is not None
assert skill.name == "youtube-page-generator"
assert "YouTube" in skill.description
discovered = LocalSkillStorage(
host_path=str(SKILL_ROOT.parent.parent)
).load_skills(enabled_only=True)
discovered_by_name = {item.name: item for item in discovered}
assert (
discovered_by_name["youtube-page-generator"].category
== SkillCategory.PUBLIC
)
assert discovered_by_name["youtube-page-generator"].enabled is True
lead_prompt_source = LEAD_PROMPT_PATH.read_text(encoding="utf-8")
assert (
"/mnt/skills/public/youtube-page-generator/SKILL.md"
in lead_prompt_source
)
assert "专用 YouTube 路由" in lead_prompt_source
def test_default_json_renders_editable_youtube_html(tmp_path: Path) -> None:
renderer = _load_renderer()
data = json.loads(
(SKILL_ROOT / "assets" / "default-data.json").read_text(
encoding="utf-8"
)
)
summary = renderer.validate_document(data)
template = (SKILL_ROOT / "assets" / "youtube-template.html").read_text(
encoding="utf-8"
)
runtime = (SKILL_ROOT / "assets" / "youtube-runtime.js").read_text(
encoding="utf-8"
)
html = renderer.render_document(data, template, runtime)
assert summary["modules"] == list(renderer.MODULES)
assert summary["counts"]["videos"] >= 1
assert renderer.DATA_MARKER not in html
assert renderer.RUNTIME_MARKER not in html
assert 'id="youtubePageData"' in html
assert 'id="videoGrid"' in html
assert 'id="watchModal"' in html
assert 'id="openSettings"' in html
assert 'id="moduleTabs"' in html
assert 'id="applyJson"' in html
assert "uploadJsonMedia" in html
assert "installInlineMediaReplace" in html
assert "dfInlineMediaInput" in html
assert 'input.accept = "image/*,video/*"' in html
assert 'button.title = "上传图片或视频并替换"' in html
assert "active.expected" not in html
assert "previous.posterUrl" not in html
assert ".thumbnail-visual>video" in html
assert ".watch-visual>video{object-fit:contain;object-position:center center}" in html
assert 'class="watch-visual" id="watchVisual"' in html
assert 'class="duration"' not in html
assert "video.duration" not in html
assert 'muted playsinline preload="metadata"' in html
assert "decorateBuiltInVisuals" in html
assert "replaceBuiltInVisuals" not in html
assert "readAsDataURL" in html
assert '"brand": "YouTube"' in html
assert "<title>YouTube · 精彩视频</title>" in html
assert '"youtube-page-data-v1:" + pageFingerprint' in html
assert "const readStorage = (key)" in html
assert "window.localStorage.getItem" in html
assert "const cachedData = localStorage.getItem" not in html
assert '<img src="http' not in html
assert '<video src="http' not in html
data["site"]["pageTitle"] = "YouTube · 智能体频道"
titled_html = renderer.render_document(data, template, runtime)
assert "<title>YouTube · 智能体频道</title>" in titled_html
assert "<title>YouTube · 精彩视频</title>" not in titled_html
data["videos"]["items"][0]["title"] = (
"</script><script>alert('x')</script>"
)
escaped_html = renderer.render_document(data, template, runtime)
assert "</script><script>alert" not in escaped_html
assert "<\\/script><script>alert" in escaped_html
output = tmp_path / "youtube-page.html"
renderer._write_new(output, html)
assert output.is_file()
with pytest.raises(
renderer.DataValidationError,
match="Refusing to overwrite",
):
renderer._write_new(output, html)
def test_validation_rejects_missing_module_and_duplicate_videos() -> None:
renderer = _load_renderer()
data = json.loads(
(SKILL_ROOT / "assets" / "default-data.json").read_text(
encoding="utf-8"
)
)
data.pop("shorts")
data["videos"]["items"][1]["id"] = data["videos"]["items"][0]["id"]
data["videos"]["items"][0]["visual"] = "remote-thumbnail"
with pytest.raises(renderer.DataValidationError) as exc_info:
renderer.validate_document(data)
message = str(exc_info.value)
assert "Missing top-level module: shorts" in message
assert "must be unique" in message
assert "visual must be one of" in message
def test_remote_image_and_webpage_media_are_supported() -> None:
renderer = _load_renderer()
data = json.loads((SKILL_ROOT / "assets" / "default-data.json").read_text(encoding="utf-8"))
data["videos"]["items"][0]["media"] = {
"type": "webpage",
"pageUrl": "https://example.com/watch",
"title": "演示网页",
"mediaUrl": "https://cdn.example.com/watch.jpg",
"embed": False,
}
data["shorts"]["items"][0]["media"] = {
"type": "image",
"mediaUrl": "https://cdn.example.com/short.jpg",
"alt": "Shorts 封面",
}
renderer.validate_document(data)
runtime = (SKILL_ROOT / "assets" / "youtube-runtime.js").read_text(encoding="utf-8")
assert 'media.type === "webpage"' in runtime
assert "mediaMarkup(video.media" in runtime