Files
devplacepy/tests/unit/database/skills.py
T
retoorandClaude Sonnet 5.5 f55c7ee5f4
DevPlace CI / test (push) Failing after 31m42s
Add community node protocol spec, Agent Skills, and make the failing tests hermetic
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>
2026-10-14 07:13:25 +02:00

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"