v2.8.0 (#616)
Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
95336528f6
commit
04305bd77c
68 changed files with 3473 additions and 1764 deletions
51
tests/skills/test_builtin_sync.py
Normal file
51
tests/skills/test_builtin_sync.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.skills.conftest import create_skill
|
||||
from vibe.core.skills.builtins import BUILTIN_SKILLS
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
|
||||
|
||||
class TestBuiltinSkills:
|
||||
def test_vibe_skill_is_registered(self) -> None:
|
||||
assert "vibe" in BUILTIN_SKILLS
|
||||
|
||||
def test_vibe_skill_has_no_path(self) -> None:
|
||||
assert BUILTIN_SKILLS["vibe"].skill_path is None
|
||||
|
||||
def test_vibe_skill_has_inline_prompt(self) -> None:
|
||||
assert BUILTIN_SKILLS["vibe"].prompt
|
||||
|
||||
def test_discovers_builtin_skills(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("vibe.core.skills.manager.BUILTIN_SKILLS", BUILTIN_SKILLS)
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests", include_project_context=False
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert "vibe" in manager.available_skills
|
||||
|
||||
def test_user_skill_cannot_override_builtin(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
monkeypatch.setattr("vibe.core.skills.manager.BUILTIN_SKILLS", BUILTIN_SKILLS)
|
||||
skills_dir = tmp_path / "skills"
|
||||
skills_dir.mkdir()
|
||||
create_skill(skills_dir, "vibe", "Custom vibe override")
|
||||
|
||||
config = build_test_vibe_config(
|
||||
system_prompt_id="tests",
|
||||
include_project_context=False,
|
||||
skill_paths=[skills_dir],
|
||||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert "vibe" in manager.available_skills
|
||||
assert (
|
||||
manager.available_skills["vibe"].description
|
||||
== BUILTIN_SKILLS["vibe"].description
|
||||
)
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.conftest import build_test_vibe_config
|
||||
from tests.skills.conftest import create_skill
|
||||
from vibe.core.config import VibeConfig
|
||||
from vibe.core.skills.builtins import BUILTIN_SKILLS
|
||||
from vibe.core.skills.manager import SkillManager
|
||||
from vibe.core.trusted_folders import trusted_folders_manager
|
||||
|
||||
|
|
@ -24,10 +26,15 @@ def skill_manager(config: VibeConfig) -> SkillManager:
|
|||
|
||||
|
||||
class TestSkillManagerDiscovery:
|
||||
def test_available_skills_is_frozen(self, skill_manager: SkillManager) -> None:
|
||||
frozen_skills = cast(dict[str, object], skill_manager.available_skills)
|
||||
with pytest.raises(TypeError):
|
||||
frozen_skills["new-skill"] = object()
|
||||
|
||||
def test_discovers_no_skills_when_directory_empty(
|
||||
self, skill_manager: SkillManager
|
||||
) -> None:
|
||||
assert skill_manager.available_skills == {}
|
||||
assert skill_manager.available_skills == BUILTIN_SKILLS
|
||||
|
||||
def test_discovers_skill_from_skill_paths(self, skills_dir: Path) -> None:
|
||||
create_skill(skills_dir, "test-skill", "A test skill")
|
||||
|
|
@ -54,7 +61,7 @@ class TestSkillManagerDiscovery:
|
|||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 3
|
||||
assert len(manager.available_skills) == 3 + len(BUILTIN_SKILLS)
|
||||
assert "skill-one" in manager.available_skills
|
||||
assert "skill-two" in manager.available_skills
|
||||
assert "skill-three" in manager.available_skills
|
||||
|
|
@ -76,7 +83,7 @@ class TestSkillManagerDiscovery:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert "valid-skill" in skills
|
||||
assert "not-a-skill" not in skills
|
||||
|
||||
|
|
@ -95,7 +102,7 @@ class TestSkillManagerDiscovery:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert "valid-skill" in skills
|
||||
|
||||
|
||||
|
|
@ -159,7 +166,7 @@ class TestSkillManagerParsing:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert "valid-skill" in skills
|
||||
assert "invalid-skill" not in skills
|
||||
|
||||
|
|
@ -180,7 +187,7 @@ class TestSkillManagerParsing:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert "valid-skill" in skills
|
||||
|
||||
|
||||
|
|
@ -239,9 +246,10 @@ class TestSkillManagerSearchPaths:
|
|||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 2
|
||||
assert manager.available_skills["vibe-only"].description == "From .vibe"
|
||||
assert manager.available_skills["agents-only"].description == "From .agents"
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 2 + len(BUILTIN_SKILLS)
|
||||
assert skills["vibe-only"].description == "From .vibe"
|
||||
assert skills["agents-only"].description == "From .agents"
|
||||
|
||||
def test_first_discovered_wins_when_same_skill_in_vibe_and_agents(
|
||||
self, tmp_working_directory: Path
|
||||
|
|
@ -259,10 +267,9 @@ class TestSkillManagerSearchPaths:
|
|||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 1
|
||||
assert (
|
||||
manager.available_skills["shared-skill"].description == "First from .vibe"
|
||||
)
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert skills["shared-skill"].description == "First from .vibe"
|
||||
|
||||
def test_discovers_from_multiple_skill_paths(self, tmp_path: Path) -> None:
|
||||
# Create two separate skill directories
|
||||
|
|
@ -282,7 +289,7 @@ class TestSkillManagerSearchPaths:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 2
|
||||
assert len(skills) == 2 + len(BUILTIN_SKILLS)
|
||||
assert "skill-from-dir1" in skills
|
||||
assert "skill-from-dir2" in skills
|
||||
|
||||
|
|
@ -304,7 +311,7 @@ class TestSkillManagerSearchPaths:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert skills["duplicate-skill"].description == "First version"
|
||||
|
||||
def test_ignores_nonexistent_skill_paths(self, tmp_path: Path) -> None:
|
||||
|
|
@ -319,8 +326,9 @@ class TestSkillManagerSearchPaths:
|
|||
)
|
||||
manager = SkillManager(lambda: config)
|
||||
|
||||
assert len(manager.available_skills) == 1
|
||||
assert "valid-skill" in manager.available_skills
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 1 + len(BUILTIN_SKILLS)
|
||||
assert "valid-skill" in skills
|
||||
|
||||
|
||||
class TestSkillManagerGetSkill:
|
||||
|
|
@ -376,7 +384,7 @@ class TestSkillManagerFiltering:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 2
|
||||
assert len(skills) == 2 + len(BUILTIN_SKILLS)
|
||||
assert "skill-a" in skills
|
||||
assert "skill-b" not in skills
|
||||
assert "skill-c" in skills
|
||||
|
|
@ -514,7 +522,7 @@ class TestSkillUserInvocable:
|
|||
manager = SkillManager(lambda: config)
|
||||
|
||||
skills = manager.available_skills
|
||||
assert len(skills) == 3
|
||||
assert len(skills) == 3 + len(BUILTIN_SKILLS)
|
||||
assert skills["visible-skill"].user_invocable is True
|
||||
assert skills["hidden-skill"].user_invocable is False
|
||||
assert skills["default-skill"].user_invocable is True
|
||||
|
|
@ -561,16 +569,6 @@ class TestParseSkillCommand:
|
|||
assert parsed is not None
|
||||
assert parsed.name == "my-skill"
|
||||
|
||||
def test_raises_on_unreadable_skill(
|
||||
self, skills_dir: Path, skill_config: VibeConfig
|
||||
) -> None:
|
||||
create_skill(skills_dir, "bad-skill", body="content")
|
||||
manager = SkillManager(lambda: skill_config)
|
||||
(skills_dir / "bad-skill" / "SKILL.md").unlink()
|
||||
|
||||
with pytest.raises(OSError):
|
||||
manager.parse_skill_command("/bad-skill")
|
||||
|
||||
|
||||
class TestBuildSkillPrompt:
|
||||
def test_without_args(self, skills_dir: Path, skill_config: VibeConfig) -> None:
|
||||
|
|
|
|||
|
|
@ -127,13 +127,14 @@ class TestSkillInfo:
|
|||
meta = SkillMetadata(
|
||||
name="test-skill", description="A test skill", license="MIT"
|
||||
)
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
info = SkillInfo.from_metadata(meta, skill_path, prompt="Skill body")
|
||||
skill_dir = info.skill_dir
|
||||
|
||||
assert info.name == "test-skill"
|
||||
assert info.description == "A test skill"
|
||||
assert info.license == "MIT"
|
||||
assert info.skill_path == skill_path.resolve()
|
||||
assert info.skill_dir == skill_path.parent.resolve()
|
||||
assert skill_dir == skill_path.parent.resolve()
|
||||
|
||||
def test_creates_with_all_fields(self, tmp_path: Path) -> None:
|
||||
skill_path = tmp_path / "full-skill" / "SKILL.md"
|
||||
|
|
@ -149,6 +150,7 @@ class TestSkillInfo:
|
|||
allowed_tools=["bash"],
|
||||
user_invocable=False,
|
||||
skill_path=skill_path,
|
||||
prompt="Skill body",
|
||||
)
|
||||
|
||||
assert info.name == "full-skill"
|
||||
|
|
@ -167,9 +169,11 @@ class TestSkillInfo:
|
|||
skill_path.touch()
|
||||
|
||||
meta = SkillMetadata(name="test-skill", description="A test skill")
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
info = SkillInfo.from_metadata(meta, skill_path, prompt="Skill body")
|
||||
|
||||
assert info.skill_path is not None
|
||||
assert info.skill_path.is_absolute()
|
||||
assert info.skill_dir is not None
|
||||
assert info.skill_dir.is_absolute()
|
||||
|
||||
def test_inherits_all_metadata_fields(self, tmp_path: Path) -> None:
|
||||
|
|
@ -186,10 +190,18 @@ class TestSkillInfo:
|
|||
allowed_tools=["bash", "grep"],
|
||||
user_invocable=False,
|
||||
)
|
||||
info = SkillInfo.from_metadata(meta, skill_path)
|
||||
info = SkillInfo.from_metadata(meta, skill_path, prompt="Skill body")
|
||||
|
||||
assert info.license == meta.license
|
||||
assert info.compatibility == meta.compatibility
|
||||
assert info.metadata == meta.metadata
|
||||
assert info.allowed_tools == meta.allowed_tools
|
||||
assert info.user_invocable == meta.user_invocable
|
||||
|
||||
def test_can_omit_skill_path_for_builtin_inline_prompt(self) -> None:
|
||||
info = SkillInfo(
|
||||
name="vibe", description="Builtin skill", prompt="Inline prompt"
|
||||
)
|
||||
|
||||
assert info.skill_path is None
|
||||
assert info.skill_dir is None
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ from __future__ import annotations
|
|||
|
||||
import pytest
|
||||
|
||||
from vibe.core.skills.parser import SkillParseError, parse_frontmatter
|
||||
from vibe.core.skills.parser import SkillParseError, parse_skill_markdown
|
||||
|
||||
|
||||
class TestParseFrontmatter:
|
||||
class TestParseSkillMarkdown:
|
||||
def test_parses_valid_frontmatter(self) -> None:
|
||||
content = """---
|
||||
name: test-skill
|
||||
|
|
@ -14,7 +14,7 @@ description: A test skill
|
|||
|
||||
## Body content here
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
frontmatter, body = parse_skill_markdown(content)
|
||||
|
||||
assert frontmatter["name"] == "test-skill"
|
||||
assert frontmatter["description"] == "A test skill"
|
||||
|
|
@ -34,7 +34,7 @@ allowed-tools: bash read_file
|
|||
|
||||
Instructions here.
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
frontmatter, body = parse_skill_markdown(content)
|
||||
|
||||
assert frontmatter["name"] == "full-skill"
|
||||
assert frontmatter["description"] == "A skill with all fields"
|
||||
|
|
@ -49,7 +49,7 @@ Instructions here.
|
|||
content = "Just markdown content without frontmatter"
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
parse_skill_markdown(content)
|
||||
|
||||
assert "Missing or invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
|
|
@ -60,7 +60,7 @@ description: Missing closing delimiter
|
|||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
parse_skill_markdown(content)
|
||||
|
||||
assert "Missing or invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
|
|
@ -74,7 +74,7 @@ Body here.
|
|||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
parse_skill_markdown(content)
|
||||
|
||||
assert "Invalid YAML frontmatter" in str(exc_info.value)
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ Body here.
|
|||
"""
|
||||
|
||||
with pytest.raises(SkillParseError) as exc_info:
|
||||
parse_frontmatter(content)
|
||||
parse_skill_markdown(content)
|
||||
|
||||
assert "must be a mapping" in str(exc_info.value)
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ Body here.
|
|||
|
||||
Body content.
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
frontmatter, body = parse_skill_markdown(content)
|
||||
|
||||
assert frontmatter == {}
|
||||
assert "Body content." in body
|
||||
|
|
@ -109,7 +109,7 @@ name: minimal
|
|||
description: No body
|
||||
---
|
||||
"""
|
||||
frontmatter, body = parse_frontmatter(content)
|
||||
frontmatter, body = parse_skill_markdown(content)
|
||||
|
||||
assert frontmatter["name"] == "minimal"
|
||||
assert body.strip() == ""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue