Files
crewAI/lib/crewai/tests/project/test_json_loader.py
João Moura bb477f8a91
Some checks failed
CodeQL Advanced / Analyze (actions) (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Check Documentation Broken Links / Check broken links (push) Has been cancelled
Vulnerability Scan / pip-audit (push) Has been cancelled
Nightly Canary Release / Check for new commits (push) Has been cancelled
Nightly Canary Release / Build nightly packages (push) Has been cancelled
Nightly Canary Release / Publish nightly to PyPI (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
JSON first crews (#6131)
* feat(cli): introduce JSON crew project support and TUI enhancements

- Added support for creating and running JSON-defined crew projects, allowing users to scaffold projects with a new `create_json_crew.py` file.
- Implemented a full-screen Textual TUI for crew execution in `crew_run_tui.py`, enhancing user interaction with a two-column layout.
- Updated `run_crew.py` to prioritize JSON crew projects and added daemon mode for running without TUI.
- Introduced interactive pickers in `tui_picker.py` for improved CLI prompts.
- Enhanced validation for JSON crew files in `validate.py` to ensure proper structure and agent definitions.
- Updated `.gitignore` to exclude demo and crewai directories.

* feat: update LLM model references to gpt-5.4-mini

- Changed default LLM model from gpt-4o-mini to gpt-5.4-mini across various files, including CLI options, JSON crew configurations, and agent definitions.
- Enhanced benchmark and human feedback functionalities to utilize the new model.
- Improved user interface elements in the TUI for better interaction and feedback during execution.
- Added support for new skills directory in JSON crew project creation.

* feat(benchmark): add crew-level benchmarking functionality

- Introduced a new `benchmark` command in the CLI for crew-level benchmarking, allowing users to specify agents, models, and timeout settings.
- Implemented `CrewBenchmarkCase` to handle crew-level benchmark cases with inputs and criteria.
- Enhanced the benchmark runner to support progress tracking and detailed reporting of results for multiple models.
- Added tests for loading crew benchmark cases and validating their structure.
- Updated existing benchmark functions to accommodate the new crew-level execution model.

* feat(cli): enhance JSON crew project functionality and TUI improvements

- Added optional agent-level guardrails and advanced options in JSON crew configurations to improve output validation and flexibility.
- Updated the TUI to better handle plan step statuses, including visual indicators for task completion and failure.
- Introduced methods for parsing and managing step observation events, ensuring accurate updates to task statuses during execution.
- Enhanced validation for JSON crew projects, ensuring proper structure and error handling for agent and task definitions.
- Added comprehensive tests for new features and validation logic, ensuring robustness in JSON crew project handling.

* refactor(cli): streamline JSON crew project handling and improve validation

- Refactored JSON crew project loading and validation logic to enhance clarity and maintainability.
- Introduced utility functions for finding JSON crew files, improving code reuse across modules.
- Removed deprecated benchmark functionality and associated tests to simplify the codebase.
- Updated CLI commands to utilize the new JSON project structure, ensuring compatibility with recent changes.
- Enhanced test coverage for JSON crew project features, ensuring robust validation and error handling.

* feat(cli): enhance activity log navigation and focus management

- Added functionality to focus on the activity log when navigating through log entries.
- Implemented refresh logic for the log panel to ensure updates are displayed correctly during navigation.
- Improved keyboard navigation for log entries, allowing users to expand and scroll through logs seamlessly.
- Added tests to verify the correct behavior of log navigation and focus management in the TUI.

* feat(cli): enhance JSON crew project interaction and input handling

- Introduced a new function to enable prompt line editing for better user experience during input prompts.
- Updated the JSON crew project wizards to show interpolation hints for dynamic values, improving user guidance.
- Enhanced the handling of missing input placeholders by prompting users for required values during crew setup.
- Refactored the crew run logic to ensure proper loading and preparation of JSON-defined crews, including runtime input management.
- Added tests to verify the correct behavior of new input handling features and JSON crew project interactions.

* feat(cli): improve crew project input prompts and event handling

- Enhanced the `_prompt_text` function to allow for configurable spacing before prompts, improving user experience during input collection.
- Updated the wizards for agent and task creation to utilize the new prompt configuration, ensuring a more compact and streamlined interaction.
- Introduced new plan step lifecycle events (`PlanStepStartedEvent`, `PlanStepCompletedEvent`) to better track the execution status of plan steps.
- Refactored the step executor to emit these events during the execution of tasks, improving observability and debugging capabilities.
- Added tests to verify the correct behavior of new prompt handling and event emissions during crew project execution.

* fix: refine json-first crew interactions

* fix: prioritize common json crew tools

* fix: make json crew more tools expandable

* fix: show json crew tools by category

* feat(memory): update default embedder to OpenAI text-embedding-3-large and enhance memory compatibility

- Changed the default embedding model for Memory to OpenAI text-embedding-3-large, which uses 3072-dimensional vectors.
- Added warnings regarding compatibility issues with existing local memory stores created with 1536-dimensional embeddings.
- Updated documentation to reflect the new default embedder and its configuration options.
- Enhanced the CLI and codebase to support the new embedding model across various components, ensuring a seamless transition for users.

* fix: address PR review feedback for JSON-first crews

Review blockers:
- Forward trained_agents_file to JSON crews: crewai run -f now exports
  CREWAI_TRAINED_AGENTS_FILE for the in-process JSON crew path
- Wizard agent picker: Esc/cancel now reprompts instead of silently
  assigning the first agent
- JSON tool resolution hard-fails: unknown tool names, missing custom
  tool files, and invalid custom tool modules raise JSONProjectError
  with actionable messages instead of warn-and-continue
- Embedding dimension mismatch: LanceDB and Qdrant Edge storages raise
  EmbeddingDimensionMismatchError with reset/pin guidance instead of
  silently zero-filling vectors or returning empty search results
- Custom tool code execution documented in loader docstring and the
  scaffolded project README

CI fixes:
- ruff format across lib/
- All 133 PR-introduced mypy errors fixed (llm.py lazy-litellm and
  cli.py lazy command shims now use TYPE_CHECKING imports; textual
  is_mounted misuse fixed; pick_many overloads; misc annotations)

Bot review comments:
- Empty except blocks now have explanatory comments or debug logging
- Removed unused _C_BG/_C_PANEL/_C_BORDER globals and redundant
  import re; tests use a single import style for create_json_crew

Tests: trained-agents propagation, wizard cancel, tool resolution
failures, and dimension mismatch guidance.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: address second round of PR review comments

Cursor Bugbot:
- Wizard agent slugs: strip to [a-z0-9_] and fall back to agent_<n> so
  symbol-only roles can't produce an empty agents/.jsonc filename
- Wizard task names: dedupe against prior task names and fall back to
  task_<n> for symbol-only descriptions

CodeRabbit:
- Agent.message(): import Task explicitly at runtime instead of relying
  on the namespace injection done by crewai/__init__
- Async executor: move the native-tools-unsupported fallback from
  _ainvoke_loop_react (self-recursion) to _ainvoke_loop_native_tools,
  mirroring the sync implementation
- StepExecutor downgrade: keep the in-step conversation and append the
  text-tooling instructions instead of rebuilding messages, so completed
  native tool calls are not re-executed
- crewai-files: extension-based MIME lookup now runs before byte
  sniffing so csv/xml types are not degraded to text/plain
- Memory storages: validate every record in a save() batch against a
  consistent embedding dimension (LanceDB previously checked only the
  first record); added mixed-batch tests
- _print_post_tui_summary now typed against CrewRunApp
- Docs: Azure OpenAI default embedder change called out in the memory
  migration warning and provider table

Code quality bots:
- Removed unused _C_YELLOW/_C_CYAN (crew_run_tui) and _GREEN (tui_picker)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(cli): accordion tool picker in JSON crew wizard

The flat tool list had grown to ~90 rows. The picker now shows:
- Common tools always visible at the top
- Every other category as a single expandable row with tool and
  selection counts (e.g. "Search & Research  (27 tools, 2 selected)")
- Expanding a category collapses the previously expanded one
- Selections persist across expand/collapse via new preselected
  support in pick_many; cursor follows the toggled category row

tui_picker gains preselected + initial_cursor options on pick_many,
and Esc in multi-select now confirms the current selection instead of
discarding it (required so collapsing can't silently drop choices).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(cli): remove --daemon flag from crewai run

The flag only affected JSON crew projects — classic and flow projects
ignored it entirely, which made the behavior inconsistent. Removed the
option, the daemon code path (_run_json_crew_daemon), and its helper
(_load_json_crew_with_inputs).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: update run command tests after --daemon removal

lib/crewai/tests/cli/test_run_crew.py still asserted the old
run_crew(trained_agents_file=..., daemon=False) call signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): exit codes, mid-run quit, async statuses, hyphen placeholders

Addresses the latest Bugbot review round:

- Failed JSON crew runs now exit non-zero (SystemExit(1)) so scripts
  and CI don't treat failures as success, mirroring the classic path
- Quitting the TUI mid-run now ends the process (os._exit(130));
  kickoff runs in a thread worker that cannot be force-cancelled, so
  letting the CLI return would leave LLM/tool work burning tokens in
  the background
- Sidebar task statuses are now async-safe: completion/failure events
  resolve the task's own row via identity instead of assuming the most
  recently started task, and starting a task no longer blanket-marks
  earlier active rows as done
- The runtime-input prompt regex now accepts hyphenated placeholder
  names ({my-topic}), matching kickoff's interpolation pattern

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: validation safety, custom tool sandboxing, TUI log integrity, memory error surfacing

- Deploy validation no longer executes project code: validation mode
  checks tool declarations structurally (well-formed entries, custom
  tool file exists) without importing or instantiating anything.
  custom:<name> resolution only happens on the actual run path.
- custom:<name> is constrained to [A-Za-z_][A-Za-z0-9_]* and the
  resolved path must stay inside the project's tools/ directory, so
  custom:../foo or absolute-path names cannot execute code outside it.
  Tool paths resolve relative to the crew project root, not cwd.
- TUI task logs are built from per-task state captured at task start
  (idx, description, agent, start time); an out-of-order completion
  takes its output from the event and no longer steals or resets the
  current task's streamed steps/output.
- EmbeddingDimensionMismatchError now inherits ValueError instead of
  RuntimeError so background saves surface it through
  MemorySaveFailedEvent instead of silently dropping the save; the
  shutdown catch in _background_encode_batch is narrowed to the
  "cannot schedule new futures" case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): declared project type wins over crew.json presence

A flow project that also contains a crew.json(c) file now runs and
validates as the flow it declares in pyproject.toml instead of being
hijacked by the JSON crew path. Both crewai run (_has_json_crew) and
deploy validation (_is_json_crew) check tool.crewai.type; a missing or
unreadable pyproject still means a bare JSON crew project.

Also documents why StepObservationFailedEvent intentionally marks the
plan step "done": the event signals an observer failure, not a step
failure, and the executor continues past it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): type the declared_type locals so mypy stays clean

Comparing an Any-typed .get() chain returns Any, which tripped
no-any-return on the previous commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-14 04:19:48 -03:00

466 lines
16 KiB
Python

"""Tests for crewai.project.json_loader."""
from __future__ import annotations
import json
from pathlib import Path
import sys
import pytest
from crewai.llms.base_llm import BaseLLM
from crewai.project.json_loader import (
JSONProjectValidationError,
find_json_project_file,
load_agent,
strip_jsonc_comments,
)
class TestStripJsoncComments:
def test_strips_single_line_comments(self):
text = '{\n "key": "value" // this is a comment\n}'
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data["key"] == "value"
def test_strips_block_comments(self):
text = '{\n /* block comment */\n "key": "value"\n}'
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data["key"] == "value"
def test_preserves_urls_with_double_slash(self):
text = '{\n "url": "https://example.com"\n}'
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data["url"] == "https://example.com"
def test_preserves_comment_markers_inside_strings(self):
text = """{
"url": "https://example.com/a//b",
"pattern": "keep /* this */ text",
"text": "value // not a comment",
}"""
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data["url"] == "https://example.com/a//b"
assert data["pattern"] == "keep /* this */ text"
assert data["text"] == "value // not a comment"
def test_removes_trailing_commas(self):
text = '{\n "a": 1,\n "b": 2,\n}'
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data == {"a": 1, "b": 2}
def test_removes_trailing_commas_in_arrays(self):
text = '{"arr": [1, 2, 3,]}'
result = strip_jsonc_comments(text)
data = json.loads(result)
assert data["arr"] == [1, 2, 3]
def test_plain_json_unchanged(self):
text = '{"key": "value"}'
result = strip_jsonc_comments(text)
assert json.loads(result) == {"key": "value"}
def test_find_json_project_file_prefers_jsonc(tmp_path: Path):
(tmp_path / "agent.json").write_text("{}")
jsonc_path = tmp_path / "agent.jsonc"
jsonc_path.write_text("{}")
assert find_json_project_file(tmp_path, "agent") == jsonc_path
class TestLoadAgent:
def test_load_minimal_agent(self, tmp_path: Path):
agent_def = {
"role": "Researcher",
"goal": "Find information",
"backstory": "Expert researcher.",
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.role == "Researcher"
assert agent.goal == "Find information"
assert agent.backstory == "Expert researcher."
def test_load_agent_with_llm(self, tmp_path: Path):
agent_def = {
"role": "Coder",
"goal": "Write code",
"backstory": "Expert coder.",
"llm": "openai/gpt-4o",
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.role == "Coder"
def test_load_agent_with_llm_config_object(self, tmp_path: Path):
agent_def = {
"role": "Coder",
"goal": "Write code",
"backstory": "Expert coder.",
"llm": {
"model": "llama3",
"provider": "ollama",
"temperature": 0.2,
"base_url": "http://localhost:11434",
},
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert isinstance(agent.llm, BaseLLM)
assert agent.llm.model == "llama3"
assert agent.llm.provider == "ollama"
assert agent.llm.temperature == 0.2
assert agent.llm.base_url == "http://localhost:11434/v1"
def test_load_agent_with_planning_config_llm_object(self, tmp_path: Path):
agent_def = {
"role": "Planner",
"goal": "Plan work",
"backstory": "Expert planner.",
"llm": "ollama/llama3",
"planning_config": {
"reasoning_effort": "high",
"llm": {
"model": "deepseek-chat",
"provider": "deepseek",
"api_key": "test-key",
},
},
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.planning_config is not None
assert isinstance(agent.planning_config.llm, BaseLLM)
assert agent.planning_config.llm.model == "deepseek-chat"
assert agent.planning_config.llm.provider == "deepseek"
assert agent.planning_config.llm.api_key == "test-key"
def test_load_agent_with_settings_block(self, tmp_path: Path):
agent_def = {
"role": "Analyst",
"goal": "Analyze data",
"backstory": "Data expert.",
"settings": {
"verbose": True,
"allow_delegation": True,
"max_iter": 10,
"cache": False,
},
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.role == "Analyst"
assert agent.verbose is True
assert agent.allow_delegation is True
assert agent.max_iter == 10
assert agent.cache is False
def test_load_agent_with_top_level_settings(self, tmp_path: Path):
agent_def = {
"role": "Analyst",
"goal": "Analyze data",
"backstory": "Data expert.",
"verbose": True,
"max_iter": 15,
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.verbose is True
assert agent.max_iter == 15
def test_load_agent_accepts_public_agent_config_fields(self, tmp_path: Path):
agent_def = {
"role": "Analyst",
"goal": "Analyze data",
"backstory": "Data expert.",
"max_execution_time": 30,
"use_system_prompt": False,
"system_template": "system: {{ .System }}",
"prompt_template": "prompt: {{ .Prompt }}",
"response_template": "response: {{ .Response }}",
"inject_date": True,
"date_format": "%Y",
"guardrail": "Only return concise answers.",
"guardrail_max_retries": 1,
"security_config": {"fingerprint": "agent-seed"},
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.max_execution_time == 30
assert agent.use_system_prompt is False
assert agent.system_template == "system: {{ .System }}"
assert agent.inject_date is True
assert agent.guardrail == "Only return concise answers."
def test_load_agent_accepts_serialized_tool_dict(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
):
module = tmp_path / "test_tools.py"
module.write_text(
"from crewai.tools.base_tool import BaseTool\n"
"class EchoTool(BaseTool):\n"
" name: str = 'echo'\n"
" description: str = 'Echo input'\n"
" def _run(self, value: str = '') -> str:\n"
" return value\n"
)
monkeypatch.syspath_prepend(str(tmp_path))
sys.modules.pop("test_tools", None)
agent_def = {
"role": "Tool User",
"goal": "Use tools",
"backstory": "Tool expert.",
"tools": [
{
"tool_type": "test_tools.EchoTool",
"name": "echo",
"description": "Echo input",
}
],
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert len(agent.tools or []) == 1
assert agent.tools[0].name == "echo"
def test_load_agent_rejects_runtime_fields(self, tmp_path: Path):
agent_def = {
"id": "00000000-0000-4000-8000-000000000000",
"role": "Analyst",
"goal": "Analyze data",
"backstory": "Data expert.",
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
with pytest.raises(JSONProjectValidationError, match="runtime-only"):
load_agent(agent_file)
def test_settings_block_takes_precedence(self, tmp_path: Path):
agent_def = {
"role": "Analyst",
"goal": "Analyze data",
"backstory": "Data expert.",
"verbose": False,
"settings": {
"verbose": True,
},
}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
agent = load_agent(agent_file)
assert agent.verbose is True
def test_load_agent_from_jsonc(self, tmp_path: Path):
jsonc_content = """{
// This is a JSONC file with comments
"role": "Writer",
"goal": "Write articles",
"backstory": "Expert writer.",
/* multi-line
comment */
}"""
agent_file = tmp_path / "agent.jsonc"
agent_file.write_text(jsonc_content)
agent = load_agent(agent_file)
assert agent.role == "Writer"
def test_load_agent_missing_required_fields(self, tmp_path: Path):
agent_def = {"role": "Incomplete"}
agent_file = tmp_path / "agent.json"
agent_file.write_text(json.dumps(agent_def))
with pytest.raises(Exception):
load_agent(agent_file)
def test_load_agent_file_not_found(self):
with pytest.raises(FileNotFoundError):
load_agent(Path("/nonexistent/agent.json"))
class TestResolveTools:
def test_unknown_tool_raises_with_guidance(self):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
with pytest.raises(JSONProjectError, match="Unknown tool 'NotARealToolXYZ'"):
_resolve_tools(["NotARealToolXYZ"])
def test_missing_custom_tool_raises(self, tmp_path, monkeypatch):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
monkeypatch.chdir(tmp_path)
with pytest.raises(JSONProjectError, match="custom:missing"):
_resolve_tools(["custom:missing"])
def test_custom_tool_without_basetool_subclass_raises(self, tmp_path, monkeypatch):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
monkeypatch.chdir(tmp_path)
tools_dir = tmp_path / "tools"
tools_dir.mkdir()
(tools_dir / "empty.py").write_text("x = 1\n")
with pytest.raises(JSONProjectError, match="No BaseTool subclass"):
_resolve_tools(["custom:empty"])
def test_custom_tool_resolves(self, tmp_path, monkeypatch):
from crewai.project.json_loader import _resolve_tools
monkeypatch.chdir(tmp_path)
tools_dir = tmp_path / "tools"
tools_dir.mkdir()
(tools_dir / "echo.py").write_text(
"from crewai.tools.base_tool import BaseTool\n"
"\n"
"class EchoTool(BaseTool):\n"
" name: str = 'echo'\n"
" description: str = 'echo input'\n"
"\n"
" def _run(self, text: str) -> str:\n"
" return text\n"
)
tools = _resolve_tools(["custom:echo"])
assert len(tools) == 1
assert tools[0].name == "echo"
def test_serialized_tool_dicts_pass_through(self):
from crewai.project.json_loader import _resolve_tools
spec = {"tool_type": "some.module.Tool"}
assert _resolve_tools([spec]) == [spec]
class TestValidationDoesNotExecuteTools:
def _write_project(self, root, tool_line='"custom:landmine"'):
agents_dir = root / "agents"
agents_dir.mkdir()
(agents_dir / "worker.jsonc").write_text(
"{\n"
' "role": "Worker",\n'
' "goal": "Work",\n'
' "backstory": "Works hard",\n'
f' "tools": [{tool_line}]\n'
"}\n"
)
crew_path = root / "crew.jsonc"
crew_path.write_text(
"{\n"
' "agents": ["worker"],\n'
' "tasks": [\n'
' {"name": "t1", "description": "Do work", '
'"expected_output": "Done", "agent": "worker"}\n'
" ]\n"
"}\n"
)
return crew_path
def test_validate_does_not_execute_custom_tool_code(self, tmp_path):
from crewai.project.json_loader import validate_crew_project
sentinel = tmp_path / "executed.txt"
tools_dir = tmp_path / "tools"
tools_dir.mkdir()
(tools_dir / "landmine.py").write_text(
f"open({str(sentinel)!r}, 'w').write('boom')\n"
)
crew_path = self._write_project(tmp_path)
project = validate_crew_project(crew_path, tmp_path / "agents")
assert not sentinel.exists(), "validation must not execute tools/<name>.py"
assert project.agent_names == ["worker"]
def test_validate_reports_missing_custom_tool_file(self, tmp_path):
from crewai.project.json_loader import (
JSONProjectValidationError,
validate_crew_project,
)
crew_path = self._write_project(tmp_path)
with pytest.raises(JSONProjectValidationError) as exc_info:
validate_crew_project(crew_path, tmp_path / "agents")
assert "custom:landmine" in str(exc_info.value)
assert "not found" in str(exc_info.value)
def test_validate_reports_path_escaping_custom_tool(self, tmp_path):
from crewai.project.json_loader import (
JSONProjectValidationError,
validate_crew_project,
)
crew_path = self._write_project(tmp_path, tool_line='"custom:../evil"')
with pytest.raises(JSONProjectValidationError) as exc_info:
validate_crew_project(crew_path, tmp_path / "agents")
assert "Invalid custom tool name" in str(exc_info.value)
class TestCustomToolPathSafety:
@pytest.mark.parametrize(
"bad_name",
["../evil", "..", "sub/inner", "/etc/passwd", "a-b", "", "name.py"],
)
def test_unsafe_names_rejected_at_runtime(self, bad_name, tmp_path, monkeypatch):
from crewai.project.json_loader import JSONProjectError, _resolve_tools
monkeypatch.chdir(tmp_path)
with pytest.raises(JSONProjectError, match="Invalid custom tool name"):
_resolve_tools([f"custom:{bad_name}"])
def test_resolves_relative_to_project_root_not_cwd(self, tmp_path, monkeypatch):
from crewai.project.json_loader import _resolve_tools
project_root = tmp_path / "project"
tools_dir = project_root / "tools"
tools_dir.mkdir(parents=True)
(tools_dir / "echo.py").write_text(
"from crewai.tools.base_tool import BaseTool\n"
"\n"
"class EchoTool(BaseTool):\n"
" name: str = 'echo'\n"
" description: str = 'echo input'\n"
"\n"
" def _run(self, text: str) -> str:\n"
" return text\n"
)
elsewhere = tmp_path / "elsewhere"
elsewhere.mkdir()
monkeypatch.chdir(elsewhere)
tools = _resolve_tools(["custom:echo"], project_root=project_root)
assert len(tools) == 1
assert tools[0].name == "echo"