mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-06-30 20:58:11 +00:00
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
* 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>
528 lines
18 KiB
Python
528 lines
18 KiB
Python
"""Tests for `crewai.cli.deploy.validate`.
|
|
|
|
The fixtures here correspond 1:1 to the deployment-failure patterns observed
|
|
in the #crewai-deployment-failures Slack channel that motivated this work.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from textwrap import dedent
|
|
from typing import Iterable
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from crewai_cli.deploy.validate import (
|
|
DeployValidator,
|
|
Severity,
|
|
normalize_package_name,
|
|
)
|
|
|
|
|
|
def _make_pyproject(
|
|
name: str = "my_crew",
|
|
dependencies: Iterable[str] = ("crewai>=1.14.0",),
|
|
*,
|
|
hatchling: bool = False,
|
|
flow: bool = False,
|
|
extra: str = "",
|
|
) -> str:
|
|
deps = ", ".join(f'"{d}"' for d in dependencies)
|
|
lines = [
|
|
"[project]",
|
|
f'name = "{name}"',
|
|
'version = "0.1.0"',
|
|
f"dependencies = [{deps}]",
|
|
]
|
|
if hatchling:
|
|
lines += [
|
|
"",
|
|
"[build-system]",
|
|
'requires = ["hatchling"]',
|
|
'build-backend = "hatchling.build"',
|
|
]
|
|
if flow:
|
|
lines += ["", "[tool.crewai]", 'type = "flow"']
|
|
if extra:
|
|
lines += ["", extra]
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def _scaffold_standard_crew(
|
|
root: Path,
|
|
*,
|
|
name: str = "my_crew",
|
|
include_crew_py: bool = True,
|
|
include_agents_yaml: bool = True,
|
|
include_tasks_yaml: bool = True,
|
|
include_lockfile: bool = True,
|
|
pyproject: str | None = None,
|
|
) -> Path:
|
|
(root / "pyproject.toml").write_text(pyproject or _make_pyproject(name=name))
|
|
if include_lockfile:
|
|
(root / "uv.lock").write_text("# dummy uv lockfile\n")
|
|
|
|
pkg_dir = root / "src" / normalize_package_name(name)
|
|
pkg_dir.mkdir(parents=True)
|
|
(pkg_dir / "__init__.py").write_text("")
|
|
|
|
if include_crew_py:
|
|
(pkg_dir / "crew.py").write_text(
|
|
dedent(
|
|
"""
|
|
from crewai.project import CrewBase, crew
|
|
|
|
@CrewBase
|
|
class MyCrew:
|
|
agents_config = "config/agents.yaml"
|
|
tasks_config = "config/tasks.yaml"
|
|
|
|
@crew
|
|
def crew(self):
|
|
from crewai import Crew
|
|
return Crew(agents=[], tasks=[])
|
|
"""
|
|
).strip()
|
|
+ "\n"
|
|
)
|
|
|
|
config_dir = pkg_dir / "config"
|
|
config_dir.mkdir()
|
|
if include_agents_yaml:
|
|
(config_dir / "agents.yaml").write_text("{}\n")
|
|
if include_tasks_yaml:
|
|
(config_dir / "tasks.yaml").write_text("{}\n")
|
|
|
|
return pkg_dir
|
|
|
|
|
|
def _codes(validator: DeployValidator) -> set[str]:
|
|
return {r.code for r in validator.results}
|
|
|
|
|
|
def _run_without_import_check(root: Path) -> DeployValidator:
|
|
"""Run validation with the subprocess-based import check stubbed out;
|
|
the classifier is exercised directly in its own tests below."""
|
|
with patch.object(DeployValidator, "_check_module_imports", lambda self: None):
|
|
v = DeployValidator(project_root=root)
|
|
v.run()
|
|
return v
|
|
|
|
|
|
def _scaffold_json_crew(root: Path, *, task_agent: str = "researcher") -> None:
|
|
(root / "pyproject.toml").write_text(_make_pyproject(name="json_crew"))
|
|
(root / "uv.lock").write_text("# dummy uv lockfile\n")
|
|
agents_dir = root / "agents"
|
|
agents_dir.mkdir()
|
|
(agents_dir / "researcher.jsonc").write_text(
|
|
dedent(
|
|
"""
|
|
{
|
|
"role": "Researcher",
|
|
"goal": "Research things",
|
|
"backstory": "Experienced researcher",
|
|
"llm": "openai/gpt-4o-mini"
|
|
}
|
|
"""
|
|
).strip()
|
|
+ "\n"
|
|
)
|
|
(root / "crew.jsonc").write_text(
|
|
dedent(
|
|
f"""
|
|
{{
|
|
"name": "json_crew",
|
|
"agents": ["researcher"],
|
|
"tasks": [
|
|
{{
|
|
"name": "research",
|
|
"description": "Research https://example.com/a//b",
|
|
"expected_output": "Findings",
|
|
"agent": "{task_agent}"
|
|
}}
|
|
]
|
|
}}
|
|
"""
|
|
).strip()
|
|
+ "\n"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"project_name, expected",
|
|
[
|
|
("my-crew", "my_crew"),
|
|
("My Cool-Project", "my_cool_project"),
|
|
("crew123", "crew123"),
|
|
("crew.name!with$chars", "crewnamewithchars"),
|
|
],
|
|
)
|
|
def test_normalize_package_name(project_name: str, expected: str) -> None:
|
|
assert normalize_package_name(project_name) == expected
|
|
|
|
|
|
def test_valid_standard_crew_project_passes(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert v.ok, f"expected clean run, got {v.results}"
|
|
|
|
|
|
def test_valid_json_crew_project_passes(tmp_path: Path) -> None:
|
|
_scaffold_json_crew(tmp_path)
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v.run()
|
|
assert "invalid_crew_json" not in _codes(v)
|
|
|
|
|
|
def test_json_task_agent_mismatch_is_error(tmp_path: Path) -> None:
|
|
_scaffold_json_crew(tmp_path, task_agent="missing_agent")
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v.run()
|
|
finding = next(r for r in v.results if r.code == "invalid_crew_json")
|
|
assert finding.severity is Severity.ERROR
|
|
assert "missing_agent" in finding.detail
|
|
|
|
|
|
def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None:
|
|
_scaffold_json_crew(tmp_path)
|
|
crew_path = tmp_path / "crew.jsonc"
|
|
crew_path.write_text(
|
|
crew_path.read_text().replace(
|
|
'"name": "json_crew",',
|
|
'"name": "json_crew",\n "id": "00000000-0000-4000-8000-000000000000",',
|
|
)
|
|
)
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v.run()
|
|
finding = next(r for r in v.results if r.code == "invalid_crew_json")
|
|
assert finding.severity is Severity.ERROR
|
|
assert "runtime-only" in finding.detail
|
|
|
|
|
|
def test_missing_pyproject_errors(tmp_path: Path) -> None:
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_pyproject" in _codes(v)
|
|
assert not v.ok
|
|
|
|
|
|
def test_invalid_pyproject_errors(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text("this is not valid toml ====\n")
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "invalid_pyproject" in _codes(v)
|
|
|
|
|
|
def test_missing_project_name_errors(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
'[project]\nversion = "0.1.0"\ndependencies = ["crewai>=1.14.0"]\n'
|
|
)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_project_name" in _codes(v)
|
|
|
|
|
|
def test_missing_lockfile_errors(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path, include_lockfile=False)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_lockfile" in _codes(v)
|
|
|
|
|
|
def test_poetry_lock_is_accepted(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path, include_lockfile=False)
|
|
(tmp_path / "poetry.lock").write_text("# poetry lockfile\n")
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_lockfile" not in _codes(v)
|
|
|
|
|
|
def test_stale_lockfile_warns(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path)
|
|
lock = tmp_path / "uv.lock"
|
|
pyproject = tmp_path / "pyproject.toml"
|
|
old_time = pyproject.stat().st_mtime - 60
|
|
import os
|
|
|
|
os.utime(lock, (old_time, old_time))
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "stale_lockfile" in _codes(v)
|
|
# Stale is a warning, so the run can still be ok (no errors).
|
|
assert v.ok
|
|
|
|
|
|
def test_missing_package_dir_errors(tmp_path: Path) -> None:
|
|
# pyproject says name=my_crew but we only create src/other_pkg/
|
|
(tmp_path / "pyproject.toml").write_text(_make_pyproject(name="my_crew"))
|
|
(tmp_path / "uv.lock").write_text("")
|
|
(tmp_path / "src" / "other_pkg").mkdir(parents=True)
|
|
v = _run_without_import_check(tmp_path)
|
|
codes = _codes(v)
|
|
assert "missing_package_dir" in codes
|
|
finding = next(r for r in v.results if r.code == "missing_package_dir")
|
|
assert "other_pkg" in finding.hint
|
|
|
|
|
|
def test_egg_info_only_errors_with_targeted_hint(tmp_path: Path) -> None:
|
|
"""Regression for the case where only src/<name>.egg-info/ exists."""
|
|
(tmp_path / "pyproject.toml").write_text(_make_pyproject(name="odoo_pm_agents"))
|
|
(tmp_path / "uv.lock").write_text("")
|
|
(tmp_path / "src" / "odoo_pm_agents.egg-info").mkdir(parents=True)
|
|
v = _run_without_import_check(tmp_path)
|
|
finding = next(r for r in v.results if r.code == "missing_package_dir")
|
|
assert "egg-info" in finding.hint
|
|
|
|
|
|
def test_stale_egg_info_sibling_warns(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path)
|
|
(tmp_path / "src" / "my_crew.egg-info").mkdir()
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "stale_egg_info" in _codes(v)
|
|
|
|
|
|
def test_missing_crew_py_errors(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path, include_crew_py=False)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_crew_py" in _codes(v)
|
|
|
|
|
|
def test_missing_agents_yaml_errors(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path, include_agents_yaml=False)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_agents_yaml" in _codes(v)
|
|
|
|
|
|
def test_missing_tasks_yaml_errors(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path, include_tasks_yaml=False)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_tasks_yaml" in _codes(v)
|
|
|
|
|
|
def test_flow_project_requires_main_py(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
_make_pyproject(name="my_flow", flow=True)
|
|
)
|
|
(tmp_path / "uv.lock").write_text("")
|
|
(tmp_path / "src" / "my_flow").mkdir(parents=True)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_flow_main" in _codes(v)
|
|
|
|
|
|
def test_flow_project_with_main_py_passes(tmp_path: Path) -> None:
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
_make_pyproject(name="my_flow", flow=True)
|
|
)
|
|
(tmp_path / "uv.lock").write_text("")
|
|
pkg = tmp_path / "src" / "my_flow"
|
|
pkg.mkdir(parents=True)
|
|
(pkg / "main.py").write_text("# flow entrypoint\n")
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "missing_flow_main" not in _codes(v)
|
|
|
|
|
|
def test_hatchling_without_wheel_config_passes_when_pkg_dir_matches(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
_scaffold_standard_crew(
|
|
tmp_path, pyproject=_make_pyproject(name="my_crew", hatchling=True)
|
|
)
|
|
v = _run_without_import_check(tmp_path)
|
|
# src/my_crew/ exists, so hatch default should find it — no wheel error.
|
|
assert "hatch_wheel_target_missing" not in _codes(v)
|
|
|
|
|
|
def test_hatchling_with_explicit_wheel_config_passes(tmp_path: Path) -> None:
|
|
extra = (
|
|
"[tool.hatch.build.targets.wheel]\n"
|
|
'packages = ["src/my_crew"]'
|
|
)
|
|
_scaffold_standard_crew(
|
|
tmp_path,
|
|
pyproject=_make_pyproject(name="my_crew", hatchling=True, extra=extra),
|
|
)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "hatch_wheel_target_missing" not in _codes(v)
|
|
|
|
|
|
def test_classify_missing_openai_key_is_warning(tmp_path: Path) -> None:
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error(
|
|
"ImportError",
|
|
"Error importing native provider: 1 validation error for OpenAICompletion\n"
|
|
" Value error, OPENAI_API_KEY is required",
|
|
tb="",
|
|
)
|
|
assert len(v.results) == 1
|
|
result = v.results[0]
|
|
assert result.code == "llm_init_missing_key"
|
|
assert result.severity is Severity.WARNING
|
|
assert "OPENAI_API_KEY" in result.title
|
|
|
|
|
|
def test_classify_azure_extra_missing_is_error(tmp_path: Path) -> None:
|
|
"""The real message raised by the Azure provider module uses plain
|
|
double quotes around the install command (no backticks). Match the
|
|
exact string that ships in the provider source so this test actually
|
|
guards the regex used in production."""
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error(
|
|
"ImportError",
|
|
'Azure AI Inference native provider not available, to install: uv add "crewai[azure-ai-inference]"',
|
|
tb="",
|
|
)
|
|
assert "missing_provider_extra" in _codes(v)
|
|
finding = next(r for r in v.results if r.code == "missing_provider_extra")
|
|
assert finding.title.startswith("Azure AI Inference")
|
|
assert 'uv add "crewai[azure-ai-inference]"' in finding.hint
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"pkg_label, install_cmd",
|
|
[
|
|
("Anthropic", 'uv add "crewai[anthropic]"'),
|
|
("AWS Bedrock", 'uv add "crewai[bedrock]"'),
|
|
("Google Gen AI", 'uv add "crewai[google-genai]"'),
|
|
],
|
|
)
|
|
def test_classify_missing_provider_extra_matches_real_messages(
|
|
tmp_path: Path, pkg_label: str, install_cmd: str
|
|
) -> None:
|
|
"""Regression for the four provider error strings verbatim."""
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error(
|
|
"ImportError",
|
|
f"{pkg_label} native provider not available, to install: {install_cmd}",
|
|
tb="",
|
|
)
|
|
assert "missing_provider_extra" in _codes(v)
|
|
finding = next(r for r in v.results if r.code == "missing_provider_extra")
|
|
assert install_cmd in finding.hint
|
|
|
|
|
|
def test_classify_keyerror_at_import_is_warning(tmp_path: Path) -> None:
|
|
"""Regression for `KeyError: 'SERPLY_API_KEY'` raised at import time."""
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error("KeyError", "'SERPLY_API_KEY'", tb="")
|
|
codes = _codes(v)
|
|
assert "env_var_read_at_import" in codes
|
|
|
|
|
|
def test_classify_no_crewbase_class_is_error(tmp_path: Path) -> None:
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error(
|
|
"ValueError",
|
|
"Crew class annotated with @CrewBase not found.",
|
|
tb="",
|
|
)
|
|
assert "no_crewbase_class" in _codes(v)
|
|
|
|
|
|
def test_classify_no_flow_subclass_is_error(tmp_path: Path) -> None:
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error("ValueError", "No Flow subclass found in the module.", tb="")
|
|
assert "no_flow_subclass" in _codes(v)
|
|
|
|
|
|
def test_classify_stale_crewai_pin_attribute_error(tmp_path: Path) -> None:
|
|
"""Regression for a stale crewai pin missing `_load_response_format`."""
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error(
|
|
"AttributeError",
|
|
"'EmploymentServiceDecisionSupportSystemCrew' object has no attribute '_load_response_format'",
|
|
tb="",
|
|
)
|
|
assert "stale_crewai_pin" in _codes(v)
|
|
|
|
|
|
def test_classify_unknown_error_is_fallback(tmp_path: Path) -> None:
|
|
v = DeployValidator(project_root=tmp_path)
|
|
v._classify_import_error("RuntimeError", "something weird happened", tb="")
|
|
assert "import_failed" in _codes(v)
|
|
|
|
|
|
def test_env_var_referenced_but_missing_warns(tmp_path: Path) -> None:
|
|
pkg = _scaffold_standard_crew(tmp_path)
|
|
(pkg / "tools.py").write_text(
|
|
'import os\nkey = os.getenv("TAVILY_API_KEY")\n'
|
|
)
|
|
import os
|
|
|
|
# Make sure the test doesn't inherit the key from the host environment.
|
|
with patch.dict(os.environ, {}, clear=False):
|
|
os.environ.pop("TAVILY_API_KEY", None)
|
|
v = _run_without_import_check(tmp_path)
|
|
codes = _codes(v)
|
|
assert "env_vars_not_in_dotenv" in codes
|
|
|
|
|
|
def test_env_var_in_dotenv_does_not_warn(tmp_path: Path) -> None:
|
|
pkg = _scaffold_standard_crew(tmp_path)
|
|
(pkg / "tools.py").write_text(
|
|
'import os\nkey = os.getenv("TAVILY_API_KEY")\n'
|
|
)
|
|
(tmp_path / ".env").write_text("TAVILY_API_KEY=abc\n")
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "env_vars_not_in_dotenv" not in _codes(v)
|
|
|
|
|
|
def test_old_crewai_pin_in_uv_lock_warns(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path)
|
|
(tmp_path / "uv.lock").write_text(
|
|
'name = "crewai"\nversion = "1.10.0"\nsource = { registry = "..." }\n'
|
|
)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "old_crewai_pin" in _codes(v)
|
|
|
|
|
|
def test_modern_crewai_pin_does_not_warn(tmp_path: Path) -> None:
|
|
_scaffold_standard_crew(tmp_path)
|
|
(tmp_path / "uv.lock").write_text(
|
|
'name = "crewai"\nversion = "1.14.1"\nsource = { registry = "..." }\n'
|
|
)
|
|
v = _run_without_import_check(tmp_path)
|
|
assert "old_crewai_pin" not in _codes(v)
|
|
|
|
|
|
def test_create_crew_aborts_on_validation_error(tmp_path: Path) -> None:
|
|
"""`crewai deploy create` must not contact the API when validation fails."""
|
|
from unittest.mock import MagicMock, patch as mock_patch
|
|
|
|
from crewai_cli.deploy.main import DeployCommand
|
|
|
|
with (
|
|
mock_patch("crewai_cli.command.get_auth_token", return_value="tok"),
|
|
mock_patch("crewai_cli.deploy.main.get_project_name", return_value="p"),
|
|
mock_patch("crewai_cli.command.PlusAPI") as mock_api,
|
|
mock_patch(
|
|
"crewai_cli.deploy.main.validate_project"
|
|
) as mock_validate,
|
|
):
|
|
mock_validate.return_value = MagicMock(ok=False)
|
|
cmd = DeployCommand()
|
|
cmd.create_crew()
|
|
assert not cmd.plus_api_client.create_crew.called
|
|
del mock_api # silence unused-var lint
|
|
|
|
|
|
def test_is_json_crew_defers_to_declared_flow_type(tmp_path):
|
|
"""A flow project with a stray crew.jsonc must validate as a flow."""
|
|
(tmp_path / "crew.jsonc").write_text("{}")
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
|
'[tool.crewai]\ntype = "flow"\n'
|
|
)
|
|
|
|
assert DeployValidator(project_root=tmp_path)._is_json_crew is False
|
|
|
|
|
|
def test_is_json_crew_true_for_declared_crew_type(tmp_path):
|
|
(tmp_path / "crew.jsonc").write_text("{}")
|
|
(tmp_path / "pyproject.toml").write_text(
|
|
'[project]\nname = "demo"\nversion = "0.1.0"\n\n'
|
|
'[tool.crewai]\ntype = "crew"\n'
|
|
)
|
|
|
|
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|
|
|
|
|
|
def test_is_json_crew_true_without_pyproject(tmp_path):
|
|
(tmp_path / "crew.jsonc").write_text("{}")
|
|
|
|
assert DeployValidator(project_root=tmp_path)._is_json_crew is True
|