Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/skillevaluator/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,14 @@
# SHARED CONSTANTS
# =============================================================================

# Field length constraints per SkillEvaluator specification
# Field limits for serialized metadata
NAME_MIN_LENGTH = 1
NAME_MAX_LENGTH = 64
TITLE_MIN_LENGTH = 1
TITLE_MAX_LENGTH = 256
DESCRIPTION_MIN_LENGTH = 1
DESCRIPTION_MAX_LENGTH = 1024
DESCRIPTION_MAX_BYTES = 1024
COMPATIBILITY_MAX_LENGTH = 500

# Maximum recommended line counts
Expand Down
9 changes: 8 additions & 1 deletion src/skillevaluator/models/skill.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from skillevaluator.constants import (
COMPATIBILITY_MAX_LENGTH,
DESCRIPTION_MAX_BYTES,
DESCRIPTION_MAX_LENGTH,
DESCRIPTION_MIN_LENGTH,
FORBIDDEN_SKILL_FIELDS,
Expand Down Expand Up @@ -161,7 +162,13 @@ def validate_name_format(cls, v: str) -> str:
@field_validator("description")
@classmethod
def validate_description_content(cls, v: str) -> str:
"""Reject descriptions that contain XML tags."""
"""Reject descriptions that exceed the serialized limit or contain XML tags."""
byte_length = len(v.encode("utf-8"))
if byte_length > DESCRIPTION_MAX_BYTES:
raise ValueError(
f"Description must be at most {DESCRIPTION_MAX_BYTES} UTF-8 bytes "
f"(got {byte_length} bytes)"
)
if XML_TAG_RE.search(v):
raise ValueError("Skill description must not contain XML tags")
return v
Expand Down
22 changes: 21 additions & 1 deletion tests/validators/test_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

from pathlib import Path

import pytest

from skillevaluator.models.skill import SkillFrontmatter
from skillevaluator.validators.schema import SchemaValidator


Expand Down Expand Up @@ -184,7 +187,7 @@ def test_consecutive_hyphens_rejected(self, tmp_path: Path):
assert any("consecutive" in err.lower() or "hyphen" in err.lower() for err in result.errors)

def test_description_length_constraints(self, tmp_path: Path):
"""Test validation enforces description length constraints (1-1024 chars)."""
"""Test validation enforces description length constraints (1-1024 UTF-8 bytes)."""
skill_dir = tmp_path / "long-description"
skill_dir.mkdir()

Expand All @@ -206,6 +209,23 @@ def test_description_length_constraints(self, tmp_path: Path):
assert not result.passed
assert any("1024" in err or "description" in err.lower() for err in result.errors)

def test_description_accepts_exact_utf8_byte_limit(self):
description = "a" * 1021 + "€"

frontmatter = SkillFrontmatter(name="exact-byte-limit", description=description)

assert len(description.encode("utf-8")) == 1024
assert frontmatter.description == description

def test_description_rejects_utf8_byte_overflow(self):
description = "a" * 1022 + "€"

with pytest.raises(ValueError, match=r"at most 1024 UTF-8 bytes \(got 1025 bytes\)"):
SkillFrontmatter(name="byte-overflow", description=description)

assert len(description) == 1023
assert len(description.encode("utf-8")) == 1025

def test_metadata_author_validation(self, tmp_path: Path):
"""Test author format fails for malformed (no email) author under default profile."""
from skillevaluator.models.result import Severity
Expand Down
Loading