Files

241 lines
9.3 KiB
Python

from __future__ import annotations
import json
import re
import subprocess
import sys
from pathlib import Path
import pytest
BACKEND_ROOT = Path(__file__).resolve().parents[1]
SKILLS_ROOT = BACKEND_ROOT / "skills" / "public"
LEAD_PROMPT_PATH = BACKEND_ROOT / "packages" / "harness" / "deerflow" / "agents" / "lead_agent" / "prompt.py"
FAST_SKILLS = (
"tiktok-page-generator",
"youtube-page-generator",
"threads-page-generator",
"cnn-page-generator",
"bbc-page-generator",
"xinhua-page-generator",
"white-house-page-generator",
"manila-times-page-generator",
"gma-news-page-generator",
"yahoo-japan-page-generator",
"yomiuri-news-page-generator",
)
BRANDED_PAGE_SKILLS = (
"facebook-page-generator",
"x-page-generator",
*FAST_SKILLS,
)
BRAND_HEADER_OFFSET_SNIPPETS = {
"facebook-page-generator": (
".app-header{position:fixed;inset:var(--df-generated-header-height,0px)0auto;",
".sidebar{position:sticky;top:calc(var(--df-generated-header-height,0px)+var(--header-h)+20px);",
),
"x-page-generator": (
".left-fixed{position:fixed;top:var(--df-generated-header-height,0px);",
".feed-header{position:sticky;z-index:30;top:var(--df-generated-header-height,0px);",
),
"tiktok-page-generator": (
".app-header{position:fixed;z-index:80;inset:var(--df-generated-header-height,0px)0auto;",
".channel-tabs{position:fixed;z-index:55;top:calc(var(--df-generated-header-height,0px)+var(--header-h)+10px);",
),
"youtube-page-generator": (
".app-header{position:fixed;z-index:90;inset:var(--df-generated-header-height,0px)0auto;",
".category-bar{position:sticky;z-index:60;top:calc(var(--df-generated-header-height,0px)+var(--header-h));",
),
"threads-page-generator": (
".topbar{position:fixed;z-index:80;inset:var(--df-generated-header-height,0px)0auto;",
".mobile-header{position:fixed;z-index:75;inset:var(--df-generated-header-height,0px)0auto;",
),
"cnn-page-generator": (
".topbar{background:var(--nav);color:white;position:sticky;top:var(--df-generated-header-height,0px);",
),
"bbc-page-generator": (
".settings-trigger{position:fixed;z-index:70;top:calc(var(--df-generated-header-height,0px)+10px);",
),
"xinhua-page-generator": (
".settings-trigger{position:fixed;right:18px;top:calc(var(--df-generated-header-height,0px)+122px);",
),
"white-house-page-generator": (
".site-head{position:absolute;top:calc(var(--df-generated-header-height,0px)+6px);",
".site-head.scrolled{position:fixed;top:var(--df-generated-header-height,0px);",
),
"manila-times-page-generator": (
".primary-nav.stuck{top:var(--df-generated-header-height,0px)}",
".settings-button{top:calc(var(--df-generated-header-height,0px)+18px)}",
),
"gma-news-page-generator": (
"body.sticky.nav{top:var(--df-generated-header-height,0px)}",
".settings-button{top:calc(var(--df-generated-header-height,0px)+58px)}",
),
"yahoo-japan-page-generator": (
".settings-button{top:calc(var(--df-generated-header-height,0px)+20px)}",
),
"yomiuri-news-page-generator": (
".sticky.nav{top:var(--df-generated-header-height,0px)}",
".settings-button{top:calc(var(--df-generated-header-height,0px)+20px)}",
),
}
SPECIALIZED_FAST_SKILLS = {"threads-page-generator", "cnn-page-generator"}
SHARED_FAST_SKILLS = tuple(skill_name for skill_name in FAST_SKILLS if skill_name not in SPECIALIZED_FAST_SKILLS)
@pytest.mark.parametrize("skill_name", BRANDED_PAGE_SKILLS)
def test_platform_chrome_is_offset_below_generated_brand_header(skill_name: str) -> None:
templates = list((SKILLS_ROOT / skill_name / "assets").glob("*-template.html"))
assert len(templates) == 1
compact_template = re.sub(r"\s+", "", templates[0].read_text(encoding="utf-8"))
snippets = BRAND_HEADER_OFFSET_SNIPPETS[skill_name]
for snippet in snippets:
assert snippet in compact_template
@pytest.mark.parametrize("skill_name", BRANDED_PAGE_SKILLS)
def test_platform_modals_render_above_generated_brand_header(skill_name: str) -> None:
templates = list((SKILLS_ROOT / skill_name / "assets").glob("*-template.html"))
assert len(templates) == 1
template = templates[0].read_text(encoding="utf-8")
compact_template = re.sub(r"\s+", "", template)
assert "z-index:1200" in compact_template
assert "z-index:1300" in compact_template
if skill_name == "manila-times-page-generator":
assert ".skip-link:focus{top:calc(var(--df-generated-header-height,0px)+10px)}" in compact_template
else:
assert "translateY(calc(-100%-var(--df-generated-header-height,0px)-24px))" in compact_template
def test_fast_workflow_is_declared_by_all_fixed_template_skills() -> None:
for skill_name in FAST_SKILLS:
skill_text = (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8")
assert "## 极速工作流(默认)" in skill_text
assert "/scripts/fast_page.py --spec" in skill_text
for skill_name in SHARED_FAST_SKILLS:
skill_text = (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8")
assert "禁止读取默认数据、契约、模板、运行时或脚本源码" in skill_text
lead_prompt = LEAD_PROMPT_PATH.read_text(encoding="utf-8")
assert "专用固定模板极速规则" in lead_prompt
assert "fast_page.py --spec" in lead_prompt
support_manifest = json.loads((SKILLS_ROOT / "_shared" / ".deerflow-skill-support.json").read_text(encoding="utf-8"))
assert set(support_manifest["required_by"]) == set(SHARED_FAST_SKILLS)
@pytest.mark.parametrize(
("skill_name", "headline_path", "output_stem"),
[
("tiktok-page-generator", "videos.items.0.text", "tiktok-page"),
("youtube-page-generator", "videos.items.0.title", "youtube-page"),
("bbc-page-generator", "lead.title", "bbc-page"),
("xinhua-page-generator", "headline.title", "xinhua-page"),
("white-house-page-generator", "hero.title", "white-house-page"),
("manila-times-page-generator", "lead.main.title", "manila-times-page"),
("gma-news-page-generator", "lead.main.title", "gma-news-page"),
("yahoo-japan-page-generator", "news.feature.title", "yahoo-japan-page"),
("yomiuri-news-page-generator", "lead.main.title", "yomiuri-news-page"),
],
)
def test_compact_spec_renders_complete_page_in_one_call(
tmp_path: Path,
skill_name: str,
headline_path: str,
output_stem: str,
) -> None:
skill_root = SKILLS_ROOT / skill_name
spec_path = tmp_path / "page-spec.json"
spec_path.write_text(
json.dumps(
{
"site": {"pageTitle": f"极速生成 · {skill_name}"},
"$set": {headline_path: "这是通过小型规格 JSON 更新的中文主题内容"},
},
ensure_ascii=False,
),
encoding="utf-8",
)
command = [
sys.executable,
str(skill_root / "scripts" / "fast_page.py"),
"--spec",
str(spec_path),
"--output-dir",
str(tmp_path),
]
completed = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
encoding="utf-8",
)
assert completed.returncode == 0, completed.stderr
result = json.loads(completed.stdout)
assert result["status"] == "ok"
assert result["action"] == "fast-page-spec"
data_path = Path(result["data"])
html_path = Path(result["html"])
assert data_path.name == f"{output_stem}-data.json"
assert html_path.name == f"{output_stem}.html"
assert data_path.exists()
assert html_path.exists()
data = json.loads(data_path.read_text(encoding="utf-8"))
assert data["site"]["pageTitle"] == f"极速生成 · {skill_name}"
current: object = data
for part in headline_path.split("."):
current = current[int(part)] if isinstance(current, list) else current[part]
assert current == "这是通过小型规格 JSON 更新的中文主题内容"
html = html_path.read_text(encoding="utf-8")
assert "这是通过小型规格 JSON 更新的中文主题内容" in html
second = subprocess.run(
command,
check=False,
capture_output=True,
text=True,
encoding="utf-8",
)
assert second.returncode == 0, second.stderr
second_result = json.loads(second.stdout)
assert second_result["data"] != result["data"]
assert second_result["html"] != result["html"]
def test_compact_spec_rejects_unknown_modules(tmp_path: Path) -> None:
skill_root = SKILLS_ROOT / "tiktok-page-generator"
spec_path = tmp_path / "invalid-spec.json"
spec_path.write_text(
json.dumps({"unknownModule": {"title": "不允许"}}, ensure_ascii=False),
encoding="utf-8",
)
completed = subprocess.run(
[
sys.executable,
str(skill_root / "scripts" / "fast_page.py"),
"--spec",
str(spec_path),
"--output-dir",
str(tmp_path),
],
check=False,
capture_output=True,
text=True,
encoding="utf-8",
)
assert completed.returncode == 2
result = json.loads(completed.stdout)
assert result["status"] == "error"
assert "Unknown top-level spec keys" in result["error"]