150 lines
5.7 KiB
Python
150 lines
5.7 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" / "xinhua-page-generator"
|
|
SCRIPT_PATH = SKILL_ROOT / "scripts" / "render_xinhua_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("xinhua_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 == "xinhua-page-generator"
|
|
assert "新华社" 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["xinhua-page-generator"].category
|
|
== SkillCategory.PUBLIC
|
|
)
|
|
assert discovered_by_name["xinhua-page-generator"].enabled is True
|
|
|
|
lead_prompt_source = LEAD_PROMPT_PATH.read_text(encoding="utf-8")
|
|
assert "/mnt/skills/public/xinhua-page-generator/SKILL.md" in lead_prompt_source
|
|
assert "专用新华社路由" in lead_prompt_source
|
|
|
|
|
|
def test_default_json_renders_editable_xinhua_html(tmp_path: Path) -> None:
|
|
renderer = _load_renderer()
|
|
data = json.loads(
|
|
(SKILL_ROOT / "assets" / "default-data.json").read_text(encoding="utf-8")
|
|
)
|
|
counts = renderer.validate_document(data)
|
|
template = (SKILL_ROOT / "assets" / "xinhua-template.html").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
runtime = (SKILL_ROOT / "assets" / "xinhua-runtime.js").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
html = renderer.render_document(data, template, runtime)
|
|
|
|
assert counts["spotlight"] == 5
|
|
assert counts["focus_tabs"] == 3
|
|
assert counts["provinces"] >= 1
|
|
assert "__XINHUA_PAGE_DATA__" not in html
|
|
assert "__XINHUA_PAGE_RUNTIME__" not in html
|
|
assert 'id="xinhuaPageData"' in html
|
|
assert 'id="spotlight"' in html
|
|
assert 'id="focusTabs"' in html
|
|
assert 'id="provinceBar"' in html
|
|
assert 'id="liveModal"' 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 "decorateBuiltInVisuals" in html
|
|
assert "replaceBuiltInVisuals" not in html
|
|
assert 'data-spot-id="' in html
|
|
assert 'class="spot-select"' in html
|
|
assert ".focus-thumb,.spot-card,.involve-bg" in html
|
|
assert "readAsDataURL" in html
|
|
assert '"brandName":"新华社"' in html
|
|
assert '"xinhua-page-data-v1:" + fingerprint' in html
|
|
assert "const readStorage = (key)" in html
|
|
assert "window.localStorage.getItem" in html
|
|
assert "localStorage.getItem('huaxun-theme')" not in html
|
|
assert '<img src="http' not in html
|
|
assert '<video src="http' not in html
|
|
|
|
output = tmp_path / "xinhua-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_duplicate_visual_and_remote_url() -> None:
|
|
renderer = _load_renderer()
|
|
data = json.loads(
|
|
(SKILL_ROOT / "assets" / "default-data.json").read_text(encoding="utf-8")
|
|
)
|
|
data.pop("footer")
|
|
data["specials"]["spotlightItems"][1]["id"] = data["specials"][
|
|
"spotlightItems"
|
|
][0]["id"]
|
|
data["focus"]["tabs"][0]["columns"][0]["visual"] = "remote-image"
|
|
data["site"]["description"] = "https://example.com/remote"
|
|
|
|
with pytest.raises(renderer.DataValidationError) as exc_info:
|
|
renderer.validate_document(data)
|
|
|
|
message = str(exc_info.value)
|
|
assert "missing top-level module(s): footer" in message
|
|
assert "duplicates" in message
|
|
assert "visual must be one of" in message
|
|
assert "remote URLs" in message
|
|
|
|
|
|
def test_remote_media_contract_supports_image_video_and_webpage() -> None:
|
|
renderer = _load_renderer()
|
|
source = json.loads((SKILL_ROOT / "assets" / "default-data.json").read_text(encoding="utf-8"))
|
|
for media in (
|
|
{"type": "image", "mediaUrl": "https://cdn.example.com/news.jpg", "alt": "新闻图片"},
|
|
{"type": "video", "mediaUrl": "https://cdn.example.com/news.mp4", "posterUrl": "https://cdn.example.com/poster.jpg"},
|
|
{"type": "webpage", "pageUrl": "https://example.com/story", "title": "新闻原文", "embed": False},
|
|
):
|
|
data = json.loads(json.dumps(source, ensure_ascii=False))
|
|
data["specials"]["spotlightItems"][0]["media"] = media
|
|
renderer.validate_document(data)
|
|
runtime = (SKILL_ROOT / "assets" / "xinhua-runtime.js").read_text(encoding="utf-8")
|
|
assert "externalMedia" in runtime
|
|
assert 'rel="noopener noreferrer"' in runtime
|