mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-06 09:42:39 +00:00
166 lines
5.3 KiB
Python
166 lines
5.3 KiB
Python
"""Tests for self_improve/acceptance.py."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from crewai.skills.self_improve.acceptance import (
|
|
_format_skill_md,
|
|
accept_proposal,
|
|
reject_proposal,
|
|
)
|
|
from crewai.skills.self_improve.models import SkillProposal
|
|
from crewai.skills.self_improve.storage import ProposalStore, SkillStore
|
|
|
|
|
|
@pytest.fixture
|
|
def proposal() -> SkillProposal:
|
|
return SkillProposal(
|
|
agent_role="Senior Researcher",
|
|
name="cite-sources",
|
|
description="Always cite sources in research outputs",
|
|
body="# Cite Sources\n\n*Always cite sources.*\n\n## When to use\n\nWhenever you write a research summary.\n",
|
|
rationale="Seen in 3 of 4 traces",
|
|
confidence=0.8,
|
|
derived_from_runs=["run_a", "run_b", "run_c"],
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def stores(tmp_path: Path):
|
|
return SkillStore(root=tmp_path), ProposalStore(root=tmp_path)
|
|
|
|
|
|
class TestFormatSkillMd:
|
|
def test_includes_yaml_frontmatter(self, proposal: SkillProposal) -> None:
|
|
out = _format_skill_md(proposal)
|
|
assert out.startswith("---\n")
|
|
# Values are JSON-quoted (valid YAML scalars).
|
|
assert 'name: "cite-sources"' in out
|
|
assert '"Always cite sources in research outputs"' in out
|
|
assert "# Cite Sources" in out
|
|
|
|
def test_quotes_descriptions_with_special_chars(self) -> None:
|
|
prop = SkillProposal(
|
|
agent_role="r",
|
|
name="n",
|
|
description='Has a "quote" and: colon',
|
|
body="b",
|
|
rationale="r",
|
|
confidence=0.7,
|
|
)
|
|
out = _format_skill_md(prop)
|
|
# JSON-quoted: backslash-escapes the inner quote, retains the colon literally.
|
|
assert 'description: "Has a \\"quote\\" and: colon"' in out
|
|
|
|
def test_passes_through_body_with_existing_frontmatter(self) -> None:
|
|
prop = SkillProposal(
|
|
agent_role="r",
|
|
name="n",
|
|
description="d",
|
|
body="---\nname: n\n---\n# Body\n",
|
|
rationale="r",
|
|
confidence=0.7,
|
|
)
|
|
out = _format_skill_md(prop)
|
|
# No double frontmatter
|
|
assert out.count("---") == 2 # one open, one close
|
|
|
|
|
|
class TestAcceptProposal:
|
|
def test_writes_skill_md_at_expected_path(
|
|
self, stores, proposal: SkillProposal
|
|
) -> None:
|
|
skill_store, proposal_store = stores
|
|
proposal_store.save(proposal)
|
|
|
|
path = accept_proposal(
|
|
proposal,
|
|
skill_store=skill_store,
|
|
proposal_store=proposal_store,
|
|
)
|
|
|
|
assert path.name == "SKILL.md"
|
|
# role gets slugified
|
|
assert "senior-researcher" in str(path)
|
|
assert "cite-sources" in str(path)
|
|
|
|
body = path.read_text()
|
|
assert body.startswith("---\n")
|
|
assert "# Cite Sources" in body
|
|
|
|
def test_removes_proposal_from_queue_on_accept(
|
|
self, stores, proposal: SkillProposal
|
|
) -> None:
|
|
skill_store, proposal_store = stores
|
|
proposal_store.save(proposal)
|
|
assert proposal_store.find(proposal.id) is not None
|
|
|
|
accept_proposal(
|
|
proposal, skill_store=skill_store, proposal_store=proposal_store
|
|
)
|
|
|
|
assert proposal_store.find(proposal.id) is None
|
|
|
|
def test_refuses_to_overwrite_existing(
|
|
self, stores, proposal: SkillProposal
|
|
) -> None:
|
|
skill_store, proposal_store = stores
|
|
proposal_store.save(proposal)
|
|
accept_proposal(
|
|
proposal, skill_store=skill_store, proposal_store=proposal_store
|
|
)
|
|
# Re-save and re-accept the same proposal id (or a fresh one) → conflict
|
|
proposal_store.save(proposal)
|
|
with pytest.raises(FileExistsError):
|
|
accept_proposal(
|
|
proposal,
|
|
skill_store=skill_store,
|
|
proposal_store=proposal_store,
|
|
)
|
|
|
|
def test_force_overwrites(self, stores, proposal: SkillProposal) -> None:
|
|
skill_store, proposal_store = stores
|
|
proposal_store.save(proposal)
|
|
accept_proposal(
|
|
proposal, skill_store=skill_store, proposal_store=proposal_store
|
|
)
|
|
|
|
proposal_store.save(proposal)
|
|
path = accept_proposal(
|
|
proposal,
|
|
skill_store=skill_store,
|
|
proposal_store=proposal_store,
|
|
force=True,
|
|
)
|
|
assert path.exists()
|
|
|
|
|
|
class TestRejectProposal:
|
|
def test_removes_from_queue(self, stores, proposal: SkillProposal) -> None:
|
|
_, proposal_store = stores
|
|
proposal_store.save(proposal)
|
|
assert reject_proposal(proposal, proposal_store=proposal_store) is True
|
|
assert proposal_store.find(proposal.id) is None
|
|
|
|
def test_returns_false_when_missing(
|
|
self, stores, proposal: SkillProposal
|
|
) -> None:
|
|
_, proposal_store = stores
|
|
assert reject_proposal(proposal, proposal_store=proposal_store) is False
|
|
|
|
|
|
class TestSkillStore:
|
|
def test_has_any_detects_at_least_one_skill(
|
|
self, stores, proposal: SkillProposal
|
|
) -> None:
|
|
skill_store, proposal_store = stores
|
|
assert skill_store.has_any("Senior Researcher") is False
|
|
proposal_store.save(proposal)
|
|
accept_proposal(
|
|
proposal, skill_store=skill_store, proposal_store=proposal_store
|
|
)
|
|
assert skill_store.has_any("Senior Researcher") is True
|