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>
145 lines
5.1 KiB
Python
145 lines
5.1 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from devplacepy.database import SOFT_DELETE_TABLES, UNREPORTABLE_TABLES, get_table
|
|
from devplacepy.database.skills import (
|
|
SkillLimitError,
|
|
count_skills,
|
|
delete_skill,
|
|
enabled_skill_summaries,
|
|
get_skill_row,
|
|
list_skills,
|
|
set_skill_enabled,
|
|
skill_view,
|
|
upsert_skill,
|
|
)
|
|
from devplacepy.services.moderation.deletion import OWNED_TABLES
|
|
from devplacepy.services.skills import limits
|
|
from devplacepy.services.skills.prompt import skill_index_lines
|
|
from devplacepy.utils import generate_uid
|
|
|
|
|
|
def fields(name, description="Does a thing.", **extra):
|
|
return {
|
|
"name": name,
|
|
"description": description,
|
|
"body": "# Body",
|
|
"files": json.dumps({"scripts/run.py": "print(1)"}),
|
|
"source_url": f"https://example.org/{name}/SKILL.md",
|
|
"license": "",
|
|
"compatibility": "",
|
|
"metadata": json.dumps({"author": "x"}),
|
|
"allowed_tools": "",
|
|
"sha256": "0" * 64,
|
|
"skipped": json.dumps([]),
|
|
"warnings": json.dumps([]),
|
|
**extra,
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def owner(local_db):
|
|
return generate_uid()
|
|
|
|
|
|
def test_skills_is_soft_deletable_private_and_cascaded_on_account_deletion():
|
|
assert "skills" in SOFT_DELETE_TABLES
|
|
assert "skills" in UNREPORTABLE_TABLES
|
|
assert ("skills", "user_uid") in OWNED_TABLES
|
|
|
|
|
|
def test_install_then_update_in_place(owner):
|
|
row, action = upsert_skill(owner, fields("alpha"))
|
|
assert action == "install"
|
|
assert row["enabled"] == 1
|
|
again, action = upsert_skill(owner, fields("alpha", description="Changed."))
|
|
assert action == "update"
|
|
assert again["uid"] == row["uid"]
|
|
assert again["description"] == "Changed."
|
|
assert count_skills(owner) == 1
|
|
|
|
|
|
def test_skills_are_private_to_their_owner(owner, local_db):
|
|
other = generate_uid()
|
|
upsert_skill(owner, fields("shared-name"))
|
|
assert get_skill_row(other, "shared-name") is None
|
|
assert list_skills(other) == []
|
|
row, action = upsert_skill(other, fields("shared-name"))
|
|
assert action == "install"
|
|
assert get_skill_row(owner, "shared-name")["uid"] != row["uid"]
|
|
|
|
|
|
def test_toggle_controls_the_prompt_index(owner):
|
|
upsert_skill(owner, fields("beta", description="Beta description."))
|
|
assert "beta: Beta description." in skill_index_lines(owner)
|
|
assert set_skill_enabled(owner, "beta", False)["enabled"] == 0
|
|
assert skill_index_lines(owner) == ""
|
|
assert set_skill_enabled(owner, "beta", True)["enabled"] == 1
|
|
assert set_skill_enabled(owner, "missing", True) is None
|
|
|
|
|
|
def test_delete_is_soft_and_reinstall_revives_the_same_row(owner):
|
|
row, _ = upsert_skill(owner, fields("gamma"))
|
|
assert delete_skill(owner, "gamma", owner) is True
|
|
assert get_skill_row(owner, "gamma") is None
|
|
assert delete_skill(owner, "gamma", owner) is False
|
|
physical = get_table("skills").find_one(uid=row["uid"])
|
|
assert physical["deleted_at"] is not None
|
|
assert physical["deleted_by"] == owner
|
|
revived, action = upsert_skill(owner, fields("gamma"))
|
|
assert action == "update"
|
|
assert revived["uid"] == row["uid"]
|
|
assert revived["deleted_at"] is None
|
|
assert revived["enabled"] == 1
|
|
|
|
|
|
def test_per_user_cap_is_enforced_including_on_revive(owner):
|
|
for index in range(limits.SKILLS_PER_USER_MAX):
|
|
upsert_skill(owner, fields(f"cap-{index}"))
|
|
with pytest.raises(SkillLimitError):
|
|
upsert_skill(owner, fields("one-too-many"))
|
|
delete_skill(owner, "cap-0", owner)
|
|
upsert_skill(owner, fields("fits-now"))
|
|
with pytest.raises(SkillLimitError):
|
|
upsert_skill(owner, fields("cap-0"))
|
|
|
|
|
|
def test_listing_is_alphabetical_and_excludes_deleted(owner):
|
|
for name in ("zeta", "alpha", "mid"):
|
|
upsert_skill(owner, fields(name))
|
|
delete_skill(owner, "mid", owner)
|
|
assert [row["name"] for row in list_skills(owner)] == ["alpha", "zeta"]
|
|
|
|
|
|
def test_skill_view_summary_and_detail(owner):
|
|
upsert_skill(owner, fields("delta"))
|
|
row = get_skill_row(owner, "delta")
|
|
summary = skill_view(row)
|
|
assert summary["file_count"] == 1
|
|
assert "body" not in summary and "files" not in summary
|
|
detail = skill_view(row, detail=True)
|
|
assert detail["body"] == "# Body"
|
|
assert detail["files"] == {"scripts/run.py": "print(1)"}
|
|
assert detail["metadata"] == {"author": "x"}
|
|
assert detail["skipped"] == [] and detail["warnings"] == []
|
|
|
|
|
|
def test_prompt_index_is_bounded(owner):
|
|
long_description = "word " * 500
|
|
for index in range(limits.PROMPT_MAX_SKILLS + 10):
|
|
upsert_skill(owner, fields(f"p{index:02d}", description=long_description))
|
|
lines = skill_index_lines(owner).split("\n")
|
|
assert len(enabled_skill_summaries(owner)) == limits.PROMPT_MAX_SKILLS
|
|
assert len(lines) <= limits.PROMPT_MAX_SKILLS
|
|
assert len("\n".join(lines)) <= limits.PROMPT_MAX_CHARS
|
|
for line in lines:
|
|
assert len(line) <= len("- p00: ") + limits.PROMPT_DESCRIPTION_MAX_CHARS
|
|
|
|
|
|
def test_prompt_index_collapses_whitespace_and_markdown_prefix(owner):
|
|
upsert_skill(owner, fields("epsilon", description="## Heading\n\n spaced out"))
|
|
assert skill_index_lines(owner) == "- epsilon: Heading spaced out"
|