from __future__ import annotations import importlib.util import json import subprocess import sys 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" / "facebook-page-generator" SCRIPT_PATH = SKILL_ROOT / "scripts" / "render_facebook_page.py" FAST_SCRIPT_PATH = SKILL_ROOT / "scripts" / "fast_single_post.py" LEAD_PROMPT_PATH = BACKEND_ROOT / "packages" / "harness" / "deerflow" / "agents" / "lead_agent" / "prompt.py" def _load_renderer(): spec = importlib.util.spec_from_file_location("facebook_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 == "facebook-page-generator" assert "Facebook" 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["facebook-page-generator"].category == SkillCategory.PUBLIC assert discovered_by_name["facebook-page-generator"].enabled is True lead_prompt_source = LEAD_PROMPT_PATH.read_text(encoding="utf-8") assert "/mnt/skills/public/facebook-page-generator/SKILL.md" in lead_prompt_source assert "专用 Facebook 路由" in lead_prompt_source assert "fast_single_post.py --spec" in lead_prompt_source assert "动态与评论正文必须使用简体中文" in lead_prompt_source def test_default_json_renders_editable_facebook_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" / "facebook-template.html").read_text(encoding="utf-8") html = renderer.render_document(data, template) assert summary["modules"] == list(renderer.MODULES) assert summary["counts"]["posts"] >= 1 assert renderer.DATA_MARKER not in html assert 'id="circleData"' in html assert 'id="openSettings"' in html assert 'id="settingsModal"' 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 "readAsDataURL" in html assert "文件不能超过 20MB" not in html assert '"brand": "Facebook"' in html assert "Facebook · 社交动态" in html data["site"]["pageTitle"] = "Facebook · 智能体开发圈" titled_html = renderer.render_document(data, template) assert "Facebook · 智能体开发圈" in titled_html assert "Facebook · 社交动态" not in titled_html assert "facebook-page-data-v2:${dataFingerprint}" in html assert "const readStorage = (key)" in html assert "const savedTheme = readStorage('circle-theme')" in html assert "writeStorage('circle-theme', next)" in html assert "const savedTheme = localStorage.getItem('circle-theme')" not in html assert "const storageKey = 'circle-page-data-v1'" not in html data["posts"]["items"][0]["text"] = "" escaped_html = renderer.render_document(data, template) assert "