DevPlace CI / test (push) Failing after 31m42s
Community nodes (Phase 1 of plan.md): the protocol specification in services/nodes/spec.py as the single source of truth, GET /nodes/spec.json, a generated protocol docs page, the Community nodes API docs group, and the implementation plan plus the residential proxy research. The no-open-proxy rule is enforced in the spec (open_proxy cannot be set). Agent Skills: register the Skills API docs group, cascade skills on account deletion, add the nested CLAUDE.md, README and routes docs, and cover the parser, the fetcher (end to end against a mock origin), the data layer and the routes with 73 unit and 16 api tests. Tests made hermetic with the repo's existing patterns instead of ambient environment: embeddings tests set OPENROUTER_API_KEY via monkeypatch, the workspace tests install FakeBackend via runtime.set_backend, and the skills install tests use run_async. DockerCliBackend now returns 127 (command not found) when the CLI is missing instead of raising FileNotFoundError, so a host without docker answers with a clean error rather than a 500. The Providers tab empty state no longer names the Model routes tab. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import pytest
|
|
|
|
from devplacepy.services.skills import limits
|
|
from devplacepy.services.skills.fetcher import (
|
|
SkillFetchError,
|
|
classify_candidate,
|
|
decode_text,
|
|
directory_url,
|
|
extract_candidates,
|
|
normalize_input_url,
|
|
origin_of,
|
|
)
|
|
from devplacepy.services.skills.parser import SkillFormatError
|
|
|
|
|
|
def test_normalize_adds_scheme_and_strips_fragment():
|
|
assert normalize_input_url("example.org/skill#frag") == "https://example.org/skill"
|
|
assert normalize_input_url(" http://example.org/a/SKILL.md ") == "http://example.org/a/SKILL.md"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"raw",
|
|
["", " ", "ftp://example.org/x", "https://user:pw@example.org/x", "x" * (limits.URL_MAX_CHARS + 1)],
|
|
)
|
|
def test_normalize_rejects_bad_urls(raw):
|
|
with pytest.raises(SkillFetchError):
|
|
normalize_input_url(raw)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url,expected",
|
|
[
|
|
("https://e.org/a/SKILL.md", "https://e.org/a/"),
|
|
("https://e.org/a/index.html", "https://e.org/a/"),
|
|
("https://e.org/a", "https://e.org/a/"),
|
|
("https://e.org/a/", "https://e.org/a/"),
|
|
("https://e.org", "https://e.org/"),
|
|
],
|
|
)
|
|
def test_directory_url(url, expected):
|
|
assert directory_url(url) == expected
|
|
|
|
|
|
def test_origin_uses_default_ports():
|
|
assert origin_of("https://Example.org/x") == ("https", "example.org", 443)
|
|
assert origin_of("http://example.org:8080/x") == ("http", "example.org", 8080)
|
|
|
|
|
|
def test_candidates_come_from_links_and_code_spans_once():
|
|
body = (
|
|
"See [the script](scripts/run.py) and `references/guide.md`.\n"
|
|
"Again [dup](scripts/run.py#anchor) and [other](docs/readme.md) and `not a path`."
|
|
)
|
|
assert extract_candidates(body) == ["scripts/run.py", "references/guide.md"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"candidate,accepted",
|
|
[
|
|
("scripts/run.py", True),
|
|
("./references/guide.md", True),
|
|
("assets/templates/SPEC.md", True),
|
|
("assets/img/hero.png", False),
|
|
("../scripts/run.py", False),
|
|
("scripts/../../etc/passwd", False),
|
|
("scripts/%2e%2e/x.py", False),
|
|
("scripts/a.py?x=1", False),
|
|
("/scripts/run.py", False),
|
|
("https://evil.org/scripts/run.py", False),
|
|
("scripts\\run.py", False),
|
|
("other/run.py", False),
|
|
("scripts", False),
|
|
("scripts/sp ace.py", False),
|
|
],
|
|
)
|
|
def test_classify_candidate_only_accepts_safe_relative_text_paths(candidate, accepted):
|
|
path, reason = classify_candidate(candidate)
|
|
assert (path is not None) is accepted
|
|
assert (reason == "") is accepted
|
|
|
|
|
|
def test_overlong_path_is_rejected():
|
|
path, reason = classify_candidate("scripts/" + "a" * limits.BUNDLED_PATH_MAX_CHARS + ".py")
|
|
assert path is None and reason
|
|
|
|
|
|
def test_decode_text():
|
|
assert decode_text("héllo".encode("utf-8")) == "héllo"
|
|
with pytest.raises(SkillFormatError):
|
|
decode_text(b"a\x00b")
|
|
with pytest.raises(SkillFormatError):
|
|
decode_text(b"\xff\xfe\xfa")
|