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" / "threads-page-generator" SCRIPT_PATH = SKILL_ROOT / "scripts" / "render_threads_page.py" FAST_SCRIPT_PATH = SKILL_ROOT / "scripts" / "fast_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( "threads_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 == "threads-page-generator" assert "Threads" 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["threads-page-generator"].category == SkillCategory.PUBLIC assert discovered_by_name["threads-page-generator"].enabled is True lead_prompt_source = LEAD_PROMPT_PATH.read_text(encoding="utf-8") assert "/mnt/skills/public/threads-page-generator/SKILL.md" in lead_prompt_source assert "专用 Threads 路由" in lead_prompt_source def test_default_json_renders_editable_threads_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" / "threads-template.html").read_text(encoding="utf-8") runtime = (SKILL_ROOT / "assets" / "threads-runtime.js").read_text(encoding="utf-8") html = renderer.render_document(data, template, runtime) assert summary["modules"] == list(renderer.MODULES) assert summary["counts"]["threads"] >= 1 assert renderer.DATA_MARKER not in html assert renderer.RUNTIME_MARKER not in html assert 'id="threadsPageData"' in html assert 'id="threadList"' in html assert 'id="composeOverlay"' 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 ".thread-media,.image-grid" in html assert 'data-thread-id="' in html assert "replaceBuiltInVisuals" not in html assert "readAsDataURL" in html assert '"brand": "Threads"' in html assert "Threads · 串联想法" in html assert '"threads-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 'Threads · 智能体讨论" in titled_html assert "Threads · 串联想法" not in titled_html data["threads"]["items"][0]["text"] = "" escaped_html = renderer.render_document(data, template, runtime) assert "