241 lines
9.3 KiB
Python
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"]
|