mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-07-01 05:08:12 +00:00
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
* Enhance crew loading and validation logic - Updated `crew_loader.py` to pass the project root when loading task and agent definitions, improving the handling of Python references. - Refactored `json_loader.py` to include additional validation for Python references, ensuring they are resolved within the project root and enforcing depth limits. - Added tests in `test_crew_loader.py` and `test_json_loader.py` to validate rejection of unsafe Python references and input files outside the project root. - Improved error handling for JSON project validation, ensuring clearer feedback for invalid configurations. * Refactor tests for hierarchical verbose manager agent - Removed `@pytest.mark.vcr()` decorators from `test_hierarchical_verbose_manager_agent` and `test_hierarchical_verbose_false_manager_agent`. - Introduced mocking for task outputs in both tests to simulate execution without relying on external dependencies. - Ensured that the `crew.kickoff()` method is called within a context that patches the `Task.execute_sync` method, improving test isolation and reliability. * Fix JSON loader PR review comments * Fix JSON loader project root after rebase * Handle UNC paths in JSON input files
966 lines
31 KiB
Python
966 lines
31 KiB
Python
"""Tests for crewai.project.crew_loader."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
import types
|
|
|
|
import pytest
|
|
|
|
from crewai.llms.base_llm import BaseLLM
|
|
from crewai.project.json_loader import JSONProjectError, JSONProjectValidationError
|
|
from crewai.project.crew_loader import load_crew, load_crew_from_definition
|
|
|
|
|
|
def _write_python_defs(tmp_path: Path) -> None:
|
|
module = tmp_path / "json_refs.py"
|
|
module.write_text(
|
|
"from pydantic import BaseModel\n"
|
|
"from crewai import Agent, Task\n"
|
|
"from crewai.security.security_config import SecurityConfig\n"
|
|
"from crewai.utilities.converter import Converter\n"
|
|
"\n"
|
|
"def always_true(_context):\n"
|
|
" return True\n"
|
|
"\n"
|
|
"def task_callback(output):\n"
|
|
" return output\n"
|
|
"\n"
|
|
"class SpecialAgent(Agent):\n"
|
|
" specialty: str = 'general'\n"
|
|
"\n"
|
|
"class SpecialTask(Task):\n"
|
|
" priority: int = 0\n"
|
|
"\n"
|
|
"class ReportModel(BaseModel):\n"
|
|
" summary: str\n"
|
|
"\n"
|
|
"class SpecialConverter(Converter):\n"
|
|
" pass\n"
|
|
"\n"
|
|
"security_config = SecurityConfig(fingerprint='agent-seed')\n"
|
|
)
|
|
|
|
|
|
def _write_agent(agents_dir: Path, name: str, **overrides) -> Path:
|
|
defn = {
|
|
"role": f"{name} role",
|
|
"goal": f"{name} goal",
|
|
"backstory": f"{name} backstory",
|
|
}
|
|
defn.update(overrides)
|
|
f = agents_dir / f"{name}.jsonc"
|
|
f.write_text(json.dumps(defn))
|
|
return f
|
|
|
|
|
|
def _write_crew(project_dir: Path, crew_def: dict) -> Path:
|
|
f = project_dir / "crew.jsonc"
|
|
f.write_text(json.dumps(crew_def))
|
|
return f
|
|
|
|
|
|
def _input_file_path(value) -> Path:
|
|
if isinstance(value, dict):
|
|
source = value.get("source", value)
|
|
else:
|
|
source = getattr(value, "source", value)
|
|
path = getattr(source, "path", source)
|
|
return Path(str(path))
|
|
|
|
|
|
class TestLoadCrew:
|
|
def test_load_crew_from_inline_definition(self):
|
|
crew, inputs = load_crew_from_definition(
|
|
{
|
|
"name": "inline_crew",
|
|
"agents": {
|
|
"researcher": {
|
|
"role": "Researcher",
|
|
"goal": "Research {topic}",
|
|
"backstory": "Knows things.",
|
|
}
|
|
},
|
|
"tasks": [
|
|
{
|
|
"name": "research",
|
|
"description": "Research {topic}",
|
|
"expected_output": "Findings about {topic}",
|
|
"agent": "researcher",
|
|
}
|
|
],
|
|
"inputs": {"topic": "AI"},
|
|
}
|
|
)
|
|
|
|
assert crew.name == "inline_crew"
|
|
assert crew.agents[0].role == "Researcher"
|
|
assert crew.tasks[0].description == "Research {topic}"
|
|
assert inputs == {"topic": "AI"}
|
|
|
|
def test_inline_definition_accepts_null_inputs(self):
|
|
_, inputs = load_crew_from_definition(
|
|
{
|
|
"agents": {
|
|
"researcher": {
|
|
"role": "Researcher",
|
|
"goal": "Research",
|
|
"backstory": "Knows things.",
|
|
}
|
|
},
|
|
"tasks": [
|
|
{
|
|
"description": "Research",
|
|
"expected_output": "Findings",
|
|
"agent": "researcher",
|
|
}
|
|
],
|
|
"inputs": None,
|
|
}
|
|
)
|
|
|
|
assert inputs == {}
|
|
|
|
def test_inline_hierarchical_manager_agent_is_not_duplicated(self):
|
|
crew, _ = load_crew_from_definition(
|
|
{
|
|
"name": "inline_hier_manager_crew",
|
|
"agents": {
|
|
"worker": {
|
|
"role": "Worker",
|
|
"goal": "Do work",
|
|
"backstory": "Does things.",
|
|
},
|
|
"manager": {
|
|
"role": "Manager",
|
|
"goal": "Coordinate work",
|
|
"backstory": "Keeps the work moving.",
|
|
},
|
|
},
|
|
"tasks": [
|
|
{
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "manager",
|
|
}
|
|
],
|
|
"process": "hierarchical",
|
|
"manager_agent": "manager",
|
|
}
|
|
)
|
|
|
|
assert len(crew.agents) == 1
|
|
assert crew.agents[0].role == "Worker"
|
|
assert crew.manager_agent is not None
|
|
assert crew.manager_agent.role == "Manager"
|
|
assert crew.tasks[0].agent is crew.manager_agent
|
|
|
|
def test_minimal_crew(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "researcher")
|
|
|
|
crew_def = {
|
|
"name": "test_crew",
|
|
"agents": ["researcher"],
|
|
"tasks": [
|
|
{
|
|
"name": "research",
|
|
"description": "Do research",
|
|
"expected_output": "Research findings",
|
|
"agent": "researcher",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, inputs = load_crew(crew_file)
|
|
assert crew.name == "test_crew"
|
|
assert len(crew.agents) == 1
|
|
assert len(crew.tasks) == 1
|
|
assert crew.tasks[0].description == "Do research"
|
|
assert inputs == {}
|
|
|
|
def test_crew_with_default_inputs(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "researcher")
|
|
|
|
crew_def = {
|
|
"name": "test_crew",
|
|
"agents": ["researcher"],
|
|
"tasks": [
|
|
{
|
|
"name": "research",
|
|
"description": "Research {topic}",
|
|
"expected_output": "Findings about {topic}",
|
|
"agent": "researcher",
|
|
}
|
|
],
|
|
"inputs": {"topic": "AI"},
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, inputs = load_crew(crew_file)
|
|
assert inputs == {"topic": "AI"}
|
|
|
|
def test_crew_with_multiple_agents(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "researcher")
|
|
_write_agent(agents_dir, "writer")
|
|
|
|
crew_def = {
|
|
"name": "multi_crew",
|
|
"agents": ["researcher", "writer"],
|
|
"tasks": [
|
|
{
|
|
"name": "research",
|
|
"description": "Do research",
|
|
"expected_output": "Findings",
|
|
"agent": "researcher",
|
|
},
|
|
{
|
|
"name": "write",
|
|
"description": "Write report",
|
|
"expected_output": "Report",
|
|
"agent": "writer",
|
|
"context": ["research"],
|
|
},
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
assert len(crew.agents) == 2
|
|
assert len(crew.tasks) == 2
|
|
# Second task should have context referencing first task
|
|
assert crew.tasks[1].context is not None
|
|
assert len(crew.tasks[1].context) == 1
|
|
|
|
def test_crew_hierarchical_process(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "hier_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
"process": "hierarchical",
|
|
"manager_llm": "openai/gpt-4o",
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
from crewai import Process
|
|
assert crew.process == Process.hierarchical
|
|
|
|
def test_crew_hierarchical_manager_agent_from_separate_agent_file(
|
|
self, tmp_path: Path
|
|
):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
_write_agent(agents_dir, "manager")
|
|
|
|
crew_def = {
|
|
"name": "hier_manager_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "manager",
|
|
}
|
|
],
|
|
"process": "hierarchical",
|
|
"manager_agent": "manager",
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
|
|
assert len(crew.agents) == 1
|
|
assert crew.agents[0].role == "worker role"
|
|
assert crew.manager_agent is not None
|
|
assert crew.manager_agent.role == "manager role"
|
|
assert crew.tasks[0].agent is crew.manager_agent
|
|
|
|
def test_crew_accepts_llm_config_objects(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker", llm="ollama/llama3")
|
|
|
|
crew_def = {
|
|
"name": "llm_config_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
"process": "hierarchical",
|
|
"manager_llm": {
|
|
"model": "llama3",
|
|
"provider": "ollama",
|
|
"base_url": "http://localhost:11434",
|
|
},
|
|
"planning_llm": {
|
|
"model": "deepseek-chat",
|
|
"provider": "deepseek",
|
|
"api_key": "test-key",
|
|
},
|
|
"chat_llm": {
|
|
"model": "openrouter/anthropic/claude-3-opus",
|
|
"api_key": "test-key",
|
|
},
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
|
|
assert isinstance(crew.manager_llm, BaseLLM)
|
|
assert crew.manager_llm.model == "llama3"
|
|
assert crew.manager_llm.provider == "ollama"
|
|
assert crew.manager_llm.base_url == "http://localhost:11434/v1"
|
|
assert isinstance(crew.planning_llm, BaseLLM)
|
|
assert crew.planning_llm.model == "deepseek-chat"
|
|
assert crew.planning_llm.provider == "deepseek"
|
|
assert isinstance(crew.chat_llm, BaseLLM)
|
|
assert crew.chat_llm.model == "anthropic/claude-3-opus"
|
|
assert crew.chat_llm.provider == "openrouter"
|
|
|
|
def test_crew_accepts_public_crew_config_fields(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "config_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
"cache": False,
|
|
"max_rpm": 12,
|
|
"planning": True,
|
|
"planning_llm": "openai/gpt-4o-mini",
|
|
"share_crew": False,
|
|
"output_log_file": "crew.log",
|
|
"tracing": False,
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
assert crew.cache is False
|
|
assert crew.max_rpm == 12
|
|
assert crew.planning is True
|
|
assert crew.planning_llm == "openai/gpt-4o-mini"
|
|
assert crew.output_log_file == "crew.log"
|
|
assert crew.tracing is False
|
|
|
|
def test_crew_with_output_file(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "writer")
|
|
|
|
crew_def = {
|
|
"name": "output_crew",
|
|
"agents": ["writer"],
|
|
"tasks": [
|
|
{
|
|
"name": "write",
|
|
"description": "Write something",
|
|
"expected_output": "Written content",
|
|
"agent": "writer",
|
|
"output_file": "output.md",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
assert crew.tasks[0].output_file == "output.md"
|
|
|
|
def test_task_accepts_public_task_config_fields(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "writer")
|
|
|
|
schema = {
|
|
"title": "ReportOutput",
|
|
"type": "object",
|
|
"properties": {
|
|
"summary": {"type": "string"},
|
|
},
|
|
"required": ["summary"],
|
|
}
|
|
crew_def = {
|
|
"name": "task_config_crew",
|
|
"agents": ["writer"],
|
|
"tasks": [
|
|
{
|
|
"name": "write",
|
|
"description": "Write something",
|
|
"expected_output": "Written content",
|
|
"agent": "writer",
|
|
"output_json": schema,
|
|
"response_model": schema,
|
|
"create_directory": False,
|
|
"human_input": True,
|
|
"markdown": True,
|
|
"guardrail": "Return a summary field.",
|
|
"guardrail_max_retries": 1,
|
|
"allow_crewai_trigger_context": False,
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
task = crew.tasks[0]
|
|
assert task.output_json is not None
|
|
assert "summary" in task.output_json.model_fields
|
|
assert task.response_model is not None
|
|
assert task.create_directory is False
|
|
assert task.human_input is True
|
|
assert task.markdown is True
|
|
assert task.guardrail == "Return a summary field."
|
|
assert task.allow_crewai_trigger_context is False
|
|
|
|
def test_crew_loads_conditional_task_with_python_condition(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
_write_python_defs(tmp_path)
|
|
monkeypatch.syspath_prepend(str(tmp_path))
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "conditional_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "first",
|
|
"description": "First task",
|
|
"expected_output": "First output",
|
|
"agent": "worker",
|
|
},
|
|
{
|
|
"type": "ConditionalTask",
|
|
"name": "second",
|
|
"description": "Second task",
|
|
"expected_output": "Second output",
|
|
"agent": "worker",
|
|
"condition": {"python": "json_refs.always_true"},
|
|
},
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
|
|
from crewai.tasks.conditional_task import ConditionalTask
|
|
|
|
assert isinstance(crew.tasks[1], ConditionalTask)
|
|
assert crew.tasks[1].should_execute(None)
|
|
|
|
def test_crew_loads_custom_agent_and_task_types(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
_write_python_defs(tmp_path)
|
|
monkeypatch.syspath_prepend(str(tmp_path))
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(
|
|
agents_dir,
|
|
"specialist",
|
|
type={"python": "json_refs.SpecialAgent"},
|
|
security_config={"python": "json_refs.security_config"},
|
|
specialty="research",
|
|
)
|
|
|
|
crew_def = {
|
|
"name": "custom_types_crew",
|
|
"agents": ["specialist"],
|
|
"tasks": [
|
|
{
|
|
"type": {"python": "json_refs.SpecialTask"},
|
|
"name": "prioritized",
|
|
"description": "Do prioritized work",
|
|
"expected_output": "Prioritized output",
|
|
"agent": "specialist",
|
|
"priority": 7,
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
|
|
assert crew.agents[0].__class__.__name__ == "SpecialAgent"
|
|
assert crew.agents[0].specialty == "research"
|
|
from crewai.security.fingerprint import Fingerprint
|
|
|
|
assert crew.agents[0].security_config.fingerprint == Fingerprint.generate(
|
|
seed="agent-seed"
|
|
)
|
|
assert crew.tasks[0].__class__.__name__ == "SpecialTask"
|
|
assert crew.tasks[0].priority == 7
|
|
|
|
def test_crew_loads_python_ref_task_fields(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
_write_python_defs(tmp_path)
|
|
monkeypatch.syspath_prepend(str(tmp_path))
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "writer")
|
|
|
|
crew_def = {
|
|
"name": "python_refs_crew",
|
|
"agents": ["writer"],
|
|
"tasks": [
|
|
{
|
|
"name": "write",
|
|
"description": "Write something",
|
|
"expected_output": "Written content",
|
|
"agent": "writer",
|
|
"callback": {"python": "json_refs.task_callback"},
|
|
"output_json": {"python": "json_refs.ReportModel"},
|
|
"converter_cls": {"python": "json_refs.SpecialConverter"},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
task = crew.tasks[0]
|
|
|
|
assert task.callback.__name__ == "task_callback"
|
|
assert task.output_json.__name__ == "ReportModel"
|
|
assert "summary" in task.output_json.model_fields
|
|
assert task.converter_cls.__name__ == "SpecialConverter"
|
|
|
|
def test_crew_rejects_stdlib_python_ref_for_agent_callback(
|
|
self, tmp_path: Path
|
|
):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(
|
|
agents_dir,
|
|
"worker",
|
|
step_callback={"python": "os.system"},
|
|
)
|
|
|
|
crew_def = {
|
|
"name": "unsafe_callback_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectError, match="project root"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_rejects_stdlib_python_ref_for_mcp_tool_filter(
|
|
self, tmp_path: Path
|
|
):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(
|
|
agents_dir,
|
|
"worker",
|
|
mcps=[
|
|
{
|
|
"command": "python",
|
|
"args": ["server.py"],
|
|
"tool_filter": {"python": "os.system"},
|
|
}
|
|
],
|
|
)
|
|
|
|
crew_def = {
|
|
"name": "unsafe_mcp_filter_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectError, match="project root"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_rejects_callable_python_ref_for_object_field(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
_write_python_defs(tmp_path)
|
|
monkeypatch.syspath_prepend(str(tmp_path))
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(
|
|
agents_dir,
|
|
"worker",
|
|
security_config={"python": "json_refs.always_true"},
|
|
)
|
|
|
|
crew_def = {
|
|
"name": "unsafe_object_ref_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectError, match="supported object reference"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_loads_project_relative_input_files(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "reader")
|
|
data_dir = tmp_path / "data"
|
|
data_dir.mkdir()
|
|
brief_path = data_dir / "brief.txt"
|
|
spec_path = data_dir / "spec.md"
|
|
brief_path.write_text("brief")
|
|
spec_path.write_text("spec")
|
|
|
|
crew_def = {
|
|
"name": "input_files_crew",
|
|
"agents": ["reader"],
|
|
"tasks": [
|
|
{
|
|
"name": "read",
|
|
"description": "Read files",
|
|
"expected_output": "File summary",
|
|
"agent": "reader",
|
|
"input_files": {
|
|
"brief": "data/brief.txt",
|
|
"spec": {"source": "data/spec.md"},
|
|
},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
input_files = crew.tasks[0].input_files
|
|
|
|
assert _input_file_path(input_files["brief"]) == brief_path
|
|
assert _input_file_path(input_files["spec"]) == spec_path
|
|
|
|
def test_crew_rejects_relative_input_file_outside_project(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "reader")
|
|
|
|
crew_def = {
|
|
"name": "unsafe_input_files_crew",
|
|
"agents": ["reader"],
|
|
"tasks": [
|
|
{
|
|
"name": "read",
|
|
"description": "Read files",
|
|
"expected_output": "File summary",
|
|
"agent": "reader",
|
|
"input_files": {"secret": "../secret.txt"},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectValidationError, match="outside the project root"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_rejects_absolute_input_file_outside_project(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "reader")
|
|
outside_path = tmp_path.parent / "secret.txt"
|
|
|
|
crew_def = {
|
|
"name": "unsafe_absolute_input_files_crew",
|
|
"agents": ["reader"],
|
|
"tasks": [
|
|
{
|
|
"name": "read",
|
|
"description": "Read files",
|
|
"expected_output": "File summary",
|
|
"agent": "reader",
|
|
"input_files": {"secret": str(outside_path)},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectValidationError, match="outside the project root"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_rejects_file_uri_input_file_outside_project(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "reader")
|
|
outside_uri = (tmp_path.parent / "secret.txt").as_uri()
|
|
|
|
crew_def = {
|
|
"name": "unsafe_file_uri_input_files_crew",
|
|
"agents": ["reader"],
|
|
"tasks": [
|
|
{
|
|
"name": "read",
|
|
"description": "Read files",
|
|
"expected_output": "File summary",
|
|
"agent": "reader",
|
|
"input_files": {"secret": outside_uri},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectValidationError, match="outside the project root"):
|
|
load_crew(crew_file)
|
|
|
|
@pytest.mark.parametrize(
|
|
"outside_path",
|
|
[
|
|
r"C:\Users\alice\.ssh\id_rsa",
|
|
"C:/Users/alice/.ssh/id_rsa",
|
|
r"\\server\share\secret.txt",
|
|
"//server/share/secret.txt",
|
|
],
|
|
)
|
|
def test_crew_rejects_windows_input_file_outside_project(
|
|
self, tmp_path: Path, outside_path: str
|
|
):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "reader")
|
|
|
|
crew_def = {
|
|
"name": "unsafe_windows_input_files_crew",
|
|
"agents": ["reader"],
|
|
"tasks": [
|
|
{
|
|
"name": "read",
|
|
"description": "Read files",
|
|
"expected_output": "File summary",
|
|
"agent": "reader",
|
|
"input_files": {"secret": outside_path},
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectValidationError, match="outside the project root"):
|
|
load_crew(crew_file)
|
|
|
|
def test_crew_restores_external_module_cache_after_project_ref(
|
|
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
):
|
|
_write_python_defs(tmp_path)
|
|
external_module = types.ModuleType("json_refs")
|
|
external_module.__file__ = str(tmp_path.parent / "json_refs.py")
|
|
external_module.marker = "external"
|
|
monkeypatch.setitem(sys.modules, "json_refs", external_module)
|
|
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(
|
|
agents_dir,
|
|
"worker",
|
|
step_callback={"python": "json_refs.task_callback"},
|
|
)
|
|
|
|
crew_def = {
|
|
"name": "cache_restore_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Do work",
|
|
"expected_output": "Work done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
|
|
assert crew.agents[0].step_callback.__name__ == "task_callback"
|
|
assert sys.modules["json_refs"] is external_module
|
|
|
|
def test_missing_agent_file_raises(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
|
|
crew_def = {
|
|
"name": "broken_crew",
|
|
"agents": ["nonexistent"],
|
|
"tasks": [],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(FileNotFoundError, match="nonexistent"):
|
|
load_crew(crew_file)
|
|
|
|
def test_task_references_unknown_agent_raises(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "researcher")
|
|
|
|
crew_def = {
|
|
"name": "bad_ref_crew",
|
|
"agents": ["researcher"],
|
|
"tasks": [
|
|
{
|
|
"name": "task1",
|
|
"description": "Do something",
|
|
"expected_output": "Something",
|
|
"agent": "unknown_agent",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectError, match="unknown_agent"):
|
|
load_crew(crew_file)
|
|
|
|
def test_task_context_order_dependency(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "order_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "task2",
|
|
"description": "Second task",
|
|
"expected_output": "Output",
|
|
"agent": "worker",
|
|
"context": ["task1"],
|
|
},
|
|
{
|
|
"name": "task1",
|
|
"description": "First task",
|
|
"expected_output": "Output",
|
|
"agent": "worker",
|
|
},
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectError, match="task1"):
|
|
load_crew(crew_file)
|
|
|
|
def test_runtime_fields_are_rejected(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "bad_runtime_crew",
|
|
"id": "00000000-0000-4000-8000-000000000000",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Work",
|
|
"expected_output": "Done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
with pytest.raises(JSONProjectValidationError, match="runtime-only"):
|
|
load_crew(crew_file)
|
|
|
|
def test_custom_agents_dir(self, tmp_path: Path):
|
|
custom_dir = tmp_path / "my_agents"
|
|
custom_dir.mkdir()
|
|
_write_agent(custom_dir, "analyst")
|
|
|
|
crew_def = {
|
|
"name": "custom_dir_crew",
|
|
"agents": ["analyst"],
|
|
"tasks": [
|
|
{
|
|
"name": "analyze",
|
|
"description": "Analyze data",
|
|
"expected_output": "Analysis",
|
|
"agent": "analyst",
|
|
}
|
|
],
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file, agents_dir=custom_dir)
|
|
assert len(crew.agents) == 1
|
|
|
|
def test_crew_verbose_and_memory_flags(self, tmp_path: Path):
|
|
agents_dir = tmp_path / "agents"
|
|
agents_dir.mkdir()
|
|
_write_agent(agents_dir, "worker")
|
|
|
|
crew_def = {
|
|
"name": "flags_crew",
|
|
"agents": ["worker"],
|
|
"tasks": [
|
|
{
|
|
"name": "work",
|
|
"description": "Work",
|
|
"expected_output": "Done",
|
|
"agent": "worker",
|
|
}
|
|
],
|
|
"verbose": True,
|
|
"memory": True,
|
|
}
|
|
crew_file = _write_crew(tmp_path, crew_def)
|
|
|
|
crew, _ = load_crew(crew_file)
|
|
assert crew.verbose is True
|