Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
"""Tests for website/scripts/extract-skills.py helpers.
|
||||
|
||||
Covers the two behavioral contracts added when the Skills Hub page gained
|
||||
per-skill source links and a cleaned-up category sidebar:
|
||||
|
||||
1. ``_source_url`` — every community skill must resolve to a clickable
|
||||
origin URL (explicit ``extra`` URL preferred, else synthesized from the
|
||||
identifier shape). Built-in/optional skills intentionally return "" —
|
||||
they have a generated docs page (docsPath) instead.
|
||||
|
||||
2. ``_guess_category`` — tags only map to a curated category bucket;
|
||||
unknown tags fall to ``uncategorized`` (folded into "Other" later) so the
|
||||
sidebar doesn't fill with one-off junk like version strings or brand
|
||||
names.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
EXTRACT = REPO_ROOT / "website" / "scripts" / "extract-skills.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mod():
|
||||
spec = importlib.util.spec_from_file_location("extract_skills", EXTRACT)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# _source_url
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_source_url_prefers_explicit_detail_url(mod):
|
||||
extra = {"detail_url": "https://skills.sh/owner/repo/skill"}
|
||||
assert (
|
||||
mod._source_url("skills.sh", "skills-sh/owner/repo/skill", extra)
|
||||
== "https://skills.sh/owner/repo/skill"
|
||||
)
|
||||
|
||||
|
||||
def test_source_url_prefers_browse_sh_source_url(mod):
|
||||
# browse.sh adapter carries its origin under extra["source_url"].
|
||||
extra = {"source_url": "https://airbnb.com/host"}
|
||||
assert (
|
||||
mod._source_url("browse-sh", "browse-sh/airbnb.com/login-abc", extra)
|
||||
== "https://airbnb.com/host"
|
||||
)
|
||||
|
||||
|
||||
def test_source_url_synthesizes_github_tree_url(mod):
|
||||
url = mod._source_url("github", "anthropics/skills/skills/algorithmic-art", {})
|
||||
assert url == "https://github.com/anthropics/skills/tree/main/skills/algorithmic-art"
|
||||
|
||||
|
||||
def test_source_url_synthesizes_github_root_when_no_subpath(mod):
|
||||
assert mod._source_url("github", "owner/repo", {}) == "https://github.com/owner/repo"
|
||||
|
||||
|
||||
def test_source_url_synthesizes_clawhub(mod):
|
||||
# ClawHub URLs require the owner handle; without it we cannot build a
|
||||
# valid URL, so the result is "" (better than a broken 404 link).
|
||||
assert mod._source_url("clawhub", "go-music-skill", {}) == ""
|
||||
|
||||
|
||||
def test_source_url_synthesizes_clawhub_strips_prefix(mod):
|
||||
# identifier may arrive already prefixed; we must not double-prefix.
|
||||
assert (
|
||||
mod._source_url("clawhub", "clawhub/go-music-skill", {})
|
||||
== ""
|
||||
)
|
||||
|
||||
|
||||
def test_source_url_synthesizes_clawhub_with_owner(mod):
|
||||
# When the owner handle is available in extra, the URL includes it.
|
||||
assert (
|
||||
mod._source_url("clawhub", "go-music-skill", {"owner": "somepublisher"})
|
||||
== "https://clawhub.ai/somepublisher/skills/go-music-skill"
|
||||
)
|
||||
|
||||
|
||||
def test_source_url_synthesizes_clawhub_with_owner_strips_prefix(mod):
|
||||
# Owner + prefixed identifier: prefix is stripped, owner is used.
|
||||
assert (
|
||||
mod._source_url("clawhub", "clawhub/go-music-skill", {"owner": "somepublisher"})
|
||||
== "https://clawhub.ai/somepublisher/skills/go-music-skill"
|
||||
)
|
||||
|
||||
|
||||
def test_source_url_synthesizes_lobehub(mod):
|
||||
assert mod._source_url("lobehub", "lobehub/chinese-paper", {}) == "https://lobehub.com/agent/chinese-paper"
|
||||
|
||||
|
||||
def test_source_url_empty_for_unknown_source_without_identifier(mod):
|
||||
assert mod._source_url("mystery", "", {}) == ""
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# _guess_category
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
def test_guess_category_maps_known_tag(mod):
|
||||
assert mod._guess_category(["security"]) == "security"
|
||||
assert mod._guess_category(["machine-learning"]) == "mlops"
|
||||
assert mod._guess_category(["crypto"]) == "blockchain"
|
||||
|
||||
|
||||
def test_guess_category_accepts_literal_curated_key(mod):
|
||||
# A skill tagged literally with a curated category key should route there.
|
||||
assert mod._guess_category(["devops"]) == "devops"
|
||||
|
||||
|
||||
def test_guess_category_rejects_junk_tag(mod):
|
||||
# This is the whole point: version strings / brand names must NOT become
|
||||
# their own sidebar category. They land in "uncategorized" → "Other".
|
||||
assert mod._guess_category(["0.10.7 Dev"]) == "uncategorized"
|
||||
assert mod._guess_category(["Doramagic Crystal"]) == "uncategorized"
|
||||
assert mod._guess_category(["Ap2"]) == "uncategorized"
|
||||
|
||||
|
||||
def test_guess_category_empty_tags(mod):
|
||||
assert mod._guess_category([]) == "uncategorized"
|
||||
|
||||
|
||||
def test_guess_category_skips_first_junk_tag_for_later_known_tag(mod):
|
||||
# First tag is junk, second is curated — we should still find the curated one.
|
||||
assert mod._guess_category(["Some Brand", "security"]) == "security"
|
||||
@@ -0,0 +1,133 @@
|
||||
"""`llms.txt` is how an LLM learns what Hermes can do.
|
||||
|
||||
It is the index every model reads when pointed at our docs — including Hermes
|
||||
itself, whose `hermes-agent` skill routes unknown-feature questions there.
|
||||
`website/` is never packaged, so there is no shipped copy to fall back on.
|
||||
|
||||
The index used to be a hand-written list of page paths, and it rotted to 53%
|
||||
coverage: Bot Mode, the desktop app, computer use, web search, and 22 messaging
|
||||
platforms were all absent, which is why an agent asked how to make bots talk to
|
||||
each other answered that it couldn't. These tests hold the two directions of
|
||||
that contract — every page reachable, every link real — so the index tracks the
|
||||
docs tree instead of someone's memory of it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
GENERATOR = REPO_ROOT / "website" / "scripts" / "generate-llms-txt.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gen():
|
||||
spec = importlib.util.spec_from_file_location("generate_llms_txt", GENERATOR)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def index(gen) -> str:
|
||||
return gen.emit_llms_index()
|
||||
|
||||
|
||||
def _linked(gen, index: str) -> set[str]:
|
||||
return set(re.findall(rf"\]\({re.escape(gen.SITE_BASE)}/([^)]+)\)", index))
|
||||
|
||||
|
||||
def _pages_on_disk(gen) -> set[str]:
|
||||
"""Walk the docs tree directly, duplicating only the two documented
|
||||
exclusions.
|
||||
|
||||
Deliberately does not call `iter_docs()`: the index is built from that
|
||||
enumeration, so checking one against the other would pass even if the
|
||||
enumerator went blind to a whole tree — which is the failure being guarded.
|
||||
"""
|
||||
pages = set()
|
||||
for path in (*gen.DOCS.rglob("*.md"), *gen.DOCS.rglob("*.mdx")):
|
||||
rel = path.relative_to(gen.DOCS).with_suffix("")
|
||||
slug = str(rel.parent) if rel.name == "index" else str(rel)
|
||||
# The docs landing page is the index's subject; per-skill pages are
|
||||
# summarized by the two catalog reference pages.
|
||||
if slug == "." or slug.startswith(("user-guide/skills/bundled", "user-guide/skills/optional")):
|
||||
continue
|
||||
pages.add(slug)
|
||||
return pages
|
||||
|
||||
|
||||
def test_every_docs_page_is_indexed(gen, index):
|
||||
"""The regression: a page the index omits is a feature the agent denies."""
|
||||
pages = _pages_on_disk(gen)
|
||||
assert len(pages) > 100, "docs root resolved wrong — the rest of this file proves nothing"
|
||||
|
||||
missing = pages - _linked(gen, index)
|
||||
assert not missing, (
|
||||
f"{len(missing)} docs pages missing from llms.txt: {sorted(missing)} — "
|
||||
"they should have been absorbed into a section automatically"
|
||||
)
|
||||
|
||||
|
||||
def test_the_enumerator_sees_the_whole_docs_tree(gen):
|
||||
"""Everything downstream trusts `iter_docs()`, so pin it to the filesystem."""
|
||||
assert set(gen.iter_docs()) == _pages_on_disk(gen)
|
||||
|
||||
|
||||
def test_every_indexed_page_exists(gen, index):
|
||||
"""The other direction: a renamed page leaves the index pointing at a 404."""
|
||||
for slug in sorted(_linked(gen, index)):
|
||||
assert gen.doc_path(slug) is not None, (
|
||||
f"llms.txt links {slug}, which is not in the docs tree — "
|
||||
"drop the SECTIONS row and let the page be absorbed under its new path"
|
||||
)
|
||||
|
||||
|
||||
def test_pages_are_listed_once(gen, index):
|
||||
"""Curating a page must promote it, not duplicate it."""
|
||||
entries = re.findall(rf"^- \[.*?\]\({re.escape(gen.SITE_BASE)}/([^)]+)\)", index, re.MULTILINE)
|
||||
duplicated = {slug for slug in entries if entries.count(slug) > 1}
|
||||
assert not duplicated, f"listed more than once in llms.txt: {sorted(duplicated)}"
|
||||
|
||||
|
||||
def test_curation_orders_pages_without_gatekeeping_them(gen):
|
||||
"""SECTIONS decides what leads a section, never what the index contains."""
|
||||
curated = {slug for _section, items in gen.SECTIONS for slug, _t, _d in items}
|
||||
pages = set(gen.iter_docs())
|
||||
|
||||
assert curated < pages, "every page is curated — absorption is no longer exercised"
|
||||
assert gen.section_for("user-guide/features/some-feature-shipped-tomorrow") in dict(gen.ABSORB)
|
||||
assert gen.section_for("a-tree-nobody-anticipated/page") == gen.MISC_SECTION
|
||||
|
||||
|
||||
def test_section_landing_pages_resolve_to_their_directory(gen):
|
||||
"""`messaging/index.md` is served at `/messaging`; `/messaging/index` 404s."""
|
||||
assert gen.slug_for(gen.DOCS / "user-guide" / "messaging" / "index.md") == "user-guide/messaging"
|
||||
assert "user-guide/messaging/index" not in _linked(gen, gen.emit_llms_index())
|
||||
|
||||
|
||||
def test_mdx_pages_are_indexed_without_their_imports(gen):
|
||||
"""MDX docs are real pages; their component imports are not prose."""
|
||||
mdx = [p for p in gen.DOCS.rglob("*.mdx") if gen.slug_for(p)]
|
||||
assert mdx, "no .mdx docs — this test no longer guards anything"
|
||||
assert {gen.slug_for(p) for p in mdx} <= set(gen.iter_docs())
|
||||
|
||||
_meta, body = gen.read_frontmatter(mdx[0])
|
||||
assert not re.search(r"^import\s", body, re.MULTILINE)
|
||||
|
||||
|
||||
def test_per_skill_catalog_pages_stay_out(gen):
|
||||
"""~195 generated skill pages would bury the product docs in the index."""
|
||||
assert not [slug for slug in gen.iter_docs() if slug.startswith(gen.SKILL_CATALOG)]
|
||||
assert "reference/skills-catalog" in gen.iter_docs(), "the summary page must remain"
|
||||
|
||||
|
||||
def test_bot_mode_is_reachable(gen, index):
|
||||
"""The page behind the original complaint, and the answer it has to carry."""
|
||||
assert "user-guide/bot-mode" in _linked(gen, index)
|
||||
assert "hermes peer dm" in (gen.DOCS / "user-guide" / "bot-mode.md").read_text(encoding="utf-8")
|
||||
@@ -0,0 +1,116 @@
|
||||
"""Tests for website/scripts/generate-skill-docs.py.
|
||||
|
||||
The generator turns every `skills/**/SKILL.md` into a Docusaurus page before
|
||||
the `docs-site-checks` CI workflow runs `ascii-guard lint` on the result. If
|
||||
a SKILL.md contains ASCII diagrams (box-drawing chars in a fenced code block)
|
||||
without its own `<!-- ascii-guard-ignore -->` markers, the generator must
|
||||
add them defensively — otherwise every PR touching `website/**` fails lint
|
||||
on unrelated skill content.
|
||||
|
||||
Regression for issue #15305.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
GENERATOR = REPO_ROOT / "website" / "scripts" / "generate-skill-docs.py"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def gen_module():
|
||||
"""Load generate-skill-docs.py as a module (hyphenated filename, not importable via normal import)."""
|
||||
spec = importlib.util.spec_from_file_location("generate_skill_docs", GENERATOR)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def test_code_block_without_box_chars_is_not_wrapped(gen_module):
|
||||
"""Plain bash/python code blocks should stay uncluttered."""
|
||||
body = "Intro.\n\n```bash\npip install foo\nfoo --run\n```\n\nOutro."
|
||||
result = gen_module.mdx_escape_body(body)
|
||||
assert "ascii-guard-ignore" not in result
|
||||
assert "pip install foo" in result
|
||||
|
||||
|
||||
def test_code_block_with_box_chars_gets_wrapped(gen_module):
|
||||
"""A code fence containing Unicode box-drawing chars must be wrapped in
|
||||
ascii-guard-ignore comments so the docs-site-checks lint can't fail on
|
||||
a skill's own diagram (issue #15305)."""
|
||||
body = (
|
||||
"Some text.\n\n"
|
||||
"```\n"
|
||||
"┌─────────┐\n"
|
||||
"│ diagram │\n"
|
||||
"└─────────┘\n"
|
||||
"```\n\n"
|
||||
"More text."
|
||||
)
|
||||
result = gen_module.mdx_escape_body(body)
|
||||
assert "<!-- ascii-guard-ignore -->" in result
|
||||
assert "<!-- ascii-guard-ignore-end -->" in result
|
||||
# The wrapper must sit OUTSIDE the fence, not inside.
|
||||
wrap_open = result.index("<!-- ascii-guard-ignore -->")
|
||||
fence_open = result.index("```\n┌")
|
||||
assert wrap_open < fence_open
|
||||
|
||||
|
||||
def test_multiple_code_blocks_only_box_ones_wrapped(gen_module):
|
||||
"""Mixed body: plain code stays plain, box code gets wrapped."""
|
||||
body = (
|
||||
"```bash\necho hi\n```\n\n"
|
||||
"```\n┌──┐\n│ │\n└──┘\n```\n\n"
|
||||
"```python\nprint('ok')\n```"
|
||||
)
|
||||
result = gen_module.mdx_escape_body(body)
|
||||
# exactly one wrap pair
|
||||
assert result.count("<!-- ascii-guard-ignore -->") == 1
|
||||
assert result.count("<!-- ascii-guard-ignore-end -->") == 1
|
||||
# plain blocks untouched
|
||||
assert "echo hi" in result
|
||||
assert "print('ok')" in result
|
||||
|
||||
|
||||
def test_tilde_fenced_box_is_wrapped(gen_module):
|
||||
"""The generator supports both ``` and ~~~ fences — both must be covered."""
|
||||
body = "~~~\n│ box │\n~~~"
|
||||
result = gen_module.mdx_escape_body(body)
|
||||
assert "<!-- ascii-guard-ignore -->" in result
|
||||
|
||||
|
||||
def test_already_wrapped_source_double_wraps_harmlessly(gen_module):
|
||||
"""If the SKILL.md already has ascii-guard-ignore markers, the generator's
|
||||
extra wrap is harmless (ascii-guard tolerates adjacent duplicate markers).
|
||||
The test just verifies we don't crash and the content survives."""
|
||||
body = (
|
||||
"<!-- ascii-guard-ignore -->\n"
|
||||
"```\n┌─┐\n└─┘\n```\n"
|
||||
"<!-- ascii-guard-ignore-end -->"
|
||||
)
|
||||
result = gen_module.mdx_escape_body(body)
|
||||
assert "┌─┐" in result
|
||||
# At least one marker pair survives
|
||||
assert "<!-- ascii-guard-ignore -->" in result
|
||||
assert "<!-- ascii-guard-ignore-end -->" in result
|
||||
|
||||
|
||||
def test_box_drawing_detection_covers_common_chars(gen_module):
|
||||
"""Smoke-test that the char set covers box-drawing ranges actually used
|
||||
in skill diagrams."""
|
||||
# Sample from real SKILL.md diagrams (segment-anything, research-paper-writing, etc.)
|
||||
for ch in "┌┐└┘─│├┤┬┴┼═║╔╗╚╝╭╮╯╰▶◀▲▼":
|
||||
assert ch in gen_module._BOX_DRAWING_CHARS, f"missing: {ch!r}"
|
||||
|
||||
|
||||
def test_bundled_catalog_explains_missing_local_skills(gen_module):
|
||||
"""The bundled catalog should explain how to restore a listed skill that
|
||||
was removed from the local profile's skills tree."""
|
||||
result = gen_module.build_catalog_md_bundled([])
|
||||
assert "respects local deletions and user edits" in result
|
||||
assert "hermes skills reset <name> --restore" in result
|
||||
Reference in New Issue
Block a user