Compare commits

..

5 Commits

Author SHA1 Message Date
Greyson LaLonde
1c90d574ab docs: update changelog and version for v1.14.2a5
Some checks are pending
CodeQL Advanced / Analyze (python) (push) Waiting to run
Vulnerability Scan / pip-audit (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
Check Documentation Broken Links / Check broken links (push) Waiting to run
2026-04-15 22:45:15 +08:00
Greyson LaLonde
3a7c550512 feat: bump versions to 1.14.2a5 2026-04-15 22:40:48 +08:00
Greyson LaLonde
5b6f89fe64 docs: update changelog and version for v1.14.2a4
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
2026-04-15 02:34:32 +08:00
Greyson LaLonde
ad5e66d1d0 feat: bump versions to 1.14.2a4 2026-04-15 02:29:06 +08:00
Greyson LaLonde
94e7d86df1 fix: stop forwarding strict mode to Bedrock Converse API
Forwarding strict and sanitizing tool schemas for strict mode causes
Bedrock Converse requests to hang until timeout. Drop strict forwarding
and schema sanitization from the Bedrock provider.
2026-04-15 02:22:50 +08:00
20 changed files with 171 additions and 600 deletions

View File

@@ -4,6 +4,46 @@ description: "تحديثات المنتج والتحسينات وإصلاحات
icon: "clock"
mode: "wide"
---
<Update label="15 أبريل 2026">
## v1.14.2a5
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## ما الذي تغير
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.2a4
## المساهمون
@greysonlalonde
</Update>
<Update label="15 أبريل 2026">
## v1.14.2a4
[عرض الإصدار على GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## ما الذي تغير
### الميزات
- إضافة تلميحات استئناف إلى إصدار أدوات المطورين عند الفشل
### إصلاحات الأخطاء
- إصلاح توجيه وضع الصرامة إلى واجهة برمجة تطبيقات Bedrock Converse
- إصلاح إصدار pytest إلى 9.0.3 لثغرة الأمان GHSA-6w46-j5rx-g56g
- رفع الحد الأدنى لـ OpenAI إلى >=2.0.0
### الوثائق
- تحديث سجل التغييرات والإصدار لـ v1.14.2a3
## المساهمون
@greysonlalonde
</Update>
<Update label="13 أبريل 2026">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="Apr 15, 2026">
## v1.14.2a5
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## What's Changed
### Documentation
- Update changelog and version for v1.14.2a4
## Contributors
@greysonlalonde
</Update>
<Update label="Apr 15, 2026">
## v1.14.2a4
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## What's Changed
### Features
- Add resume hints to devtools release on failure
### Bug Fixes
- Fix strict mode forwarding to Bedrock Converse API
- Fix pytest version to 9.0.3 for security vulnerability GHSA-6w46-j5rx-g56g
- Bump OpenAI lower bound to >=2.0.0
### Documentation
- Update changelog and version for v1.14.2a3
## Contributors
@greysonlalonde
</Update>
<Update label="Apr 13, 2026">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
icon: "clock"
mode: "wide"
---
<Update label="2026년 4월 15일">
## v1.14.2a5
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## 변경 사항
### 문서
- v1.14.2a4의 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 4월 15일">
## v1.14.2a4
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## 변경 사항
### 기능
- 실패 시 devtools 릴리스에 이력서 힌트 추가
### 버그 수정
- Bedrock Converse API로의 엄격 모드 포워딩 수정
- 보안 취약점 GHSA-6w46-j5rx-g56g에 대해 pytest 버전을 9.0.3으로 수정
- OpenAI 하한을 >=2.0.0으로 상향 조정
### 문서
- v1.14.2a3에 대한 변경 로그 및 버전 업데이트
## 기여자
@greysonlalonde
</Update>
<Update label="2026년 4월 13일">
## v1.14.2a3

View File

@@ -4,6 +4,46 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
icon: "clock"
mode: "wide"
---
<Update label="15 abr 2026">
## v1.14.2a5
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a5)
## O que Mudou
### Documentação
- Atualizar changelog e versão para v1.14.2a4
## Contribuidores
@greysonlalonde
</Update>
<Update label="15 abr 2026">
## v1.14.2a4
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.14.2a4)
## O que Mudou
### Recursos
- Adicionar dicas de retomar ao release do devtools em caso de falha
### Correções de Bugs
- Corrigir o encaminhamento do modo estrito para a API Bedrock Converse
- Corrigir a versão do pytest para 9.0.3 devido à vulnerabilidade de segurança GHSA-6w46-j5rx-g56g
- Aumentar o limite inferior do OpenAI para >=2.0.0
### Documentação
- Atualizar o changelog e a versão para v1.14.2a3
## Contribuidores
@greysonlalonde
</Update>
<Update label="13 abr 2026">
## v1.14.2a3

View File

@@ -152,4 +152,4 @@ __all__ = [
"wrap_file_source",
]
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"

View File

@@ -10,7 +10,7 @@ requires-python = ">=3.10, <3.14"
dependencies = [
"pytube~=15.0.0",
"requests>=2.33.0,<3",
"crewai==1.14.2a3",
"crewai==1.14.2a5",
"tiktoken~=0.8.0",
"beautifulsoup4~=4.13.4",
"python-docx~=1.2.0",

View File

@@ -305,4 +305,4 @@ __all__ = [
"ZapierActionTools",
]
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"

View File

@@ -55,7 +55,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
[project.optional-dependencies]
tools = [
"crewai-tools==1.14.2a3",
"crewai-tools==1.14.2a5",
]
embeddings = [
"tiktoken~=0.8.0"

View File

@@ -46,7 +46,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
_suppress_pydantic_deprecation_warnings()
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"
_telemetry_submitted = False

View File

@@ -18,7 +18,6 @@ from crewai.cli.install_crew import install_crew
from crewai.cli.kickoff_flow import kickoff_flow
from crewai.cli.organization.main import OrganizationCommand
from crewai.cli.plot_flow import plot_flow
from crewai.cli.remote_template.main import TemplateCommand
from crewai.cli.replay_from_task import replay_task_command
from crewai.cli.reset_memories_command import reset_memories_command
from crewai.cli.run_crew import run_crew
@@ -497,33 +496,6 @@ def tool_publish(is_public: bool, force: bool) -> None:
tool_cmd.publish(is_public, force)
@crewai.group()
def template() -> None:
"""Browse and install project templates."""
@template.command(name="list")
def template_list() -> None:
"""List available templates and select one to install."""
template_cmd = TemplateCommand()
template_cmd.list_templates()
@template.command(name="add")
@click.argument("name")
@click.option(
"-o",
"--output-dir",
type=str,
default=None,
help="Directory name for the template (defaults to template name)",
)
def template_add(name: str, output_dir: str | None) -> None:
"""Add a template to the current directory."""
template_cmd = TemplateCommand()
template_cmd.add_template(name, output_dir)
@crewai.group()
def flow() -> None:
"""Flow related commands."""

View File

@@ -1,248 +0,0 @@
import io
import logging
import os
import shutil
from typing import Any
import zipfile
import click
import httpx
from rich.console import Console
from rich.panel import Panel
from rich.text import Text
from crewai.cli.command import BaseCommand
logger = logging.getLogger(__name__)
console = Console()
GITHUB_ORG = "crewAIInc"
TEMPLATE_PREFIX = "template_"
GITHUB_API_BASE = "https://api.github.com"
BANNER = """\
[bold white] ██████╗██████╗ ███████╗██╗ ██╗[/bold white] [bold red] █████╗ ██╗[/bold red]
[bold white]██╔════╝██╔══██╗██╔════╝██║ ██║[/bold white] [bold red]██╔══██╗██║[/bold red]
[bold white]██║ ██████╔╝█████╗ ██║ █╗ ██║[/bold white] [bold red]███████║██║[/bold red]
[bold white]██║ ██╔══██╗██╔══╝ ██║███╗██║[/bold white] [bold red]██╔══██║██║[/bold red]
[bold white]╚██████╗██║ ██║███████╗╚███╔███╔╝[/bold white] [bold red]██║ ██║██║[/bold red]
[bold white] ╚═════╝╚═╝ ╚═╝╚══════╝ ╚══╝╚══╝[/bold white] [bold red]╚═╝ ╚═╝╚═╝[/bold red]
[dim white]████████╗███████╗███╗ ███╗██████╗ ██╗ █████╗ ████████╗███████╗███████╗[/dim white]
[dim white]╚══██╔══╝██╔════╝████╗ ████║██╔══██╗██║ ██╔══██╗╚══██╔══╝██╔════╝██╔════╝[/dim white]
[dim white] ██║ █████╗ ██╔████╔██║██████╔╝██║ ███████║ ██║ █████╗ ███████╗[/dim white]
[dim white] ██║ ██╔══╝ ██║╚██╔╝██║██╔═══╝ ██║ ██╔══██║ ██║ ██╔══╝ ╚════██║[/dim white]
[dim white] ██║ ███████╗██║ ╚═╝ ██║██║ ███████╗██║ ██║ ██║ ███████╗███████║[/dim white]
[dim white] ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝[/dim white]"""
class TemplateCommand(BaseCommand):
"""Handle template-related operations for CrewAI projects."""
def __init__(self) -> None:
super().__init__()
def list_templates(self) -> None:
"""List available templates with an interactive selector to install."""
templates = self._fetch_templates()
if not templates:
click.echo("No templates found.")
return
console.print(f"\n{BANNER}\n")
console.print(" [on cyan] templates [/on cyan]\n")
console.print(f" [green]o[/green] Source: https://github.com/{GITHUB_ORG}")
console.print(
f" [green]o[/green] Found [bold]{len(templates)}[/bold] templates\n"
)
console.print(" [green]o[/green] Select a template to install")
for idx, repo in enumerate(templates, start=1):
name = repo["name"].removeprefix(TEMPLATE_PREFIX)
description = repo.get("description") or ""
if description:
console.print(
f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white] [dim]({description})[/dim]"
)
else:
console.print(
f" [bold cyan]{idx}.[/bold cyan] [bold white]{name}[/bold white]"
)
console.print(" [bold cyan]q.[/bold cyan] [dim]Quit[/dim]\n")
while True:
choice = click.prompt("Enter your choice", type=str)
if choice.lower() == "q":
return
try:
selected_index = int(choice) - 1
if 0 <= selected_index < len(templates):
break
except ValueError:
pass
click.secho(
f"Please enter a number between 1 and {len(templates)}, or 'q' to quit.",
fg="yellow",
)
selected = templates[selected_index]
repo_name = selected["name"]
template_name = repo_name.removeprefix(TEMPLATE_PREFIX)
self.add_template(template_name)
def add_template(self, name: str, output_dir: str | None = None) -> None:
"""Download a template and copy it into the current working directory.
Args:
name: Template name (with or without the template_ prefix).
output_dir: Optional directory name. Defaults to the template name.
"""
repo_name = self._resolve_repo_name(name)
if repo_name is None:
click.secho(f"Template '{name}' not found.", fg="red")
click.echo("Run 'crewai template list' to see available templates.")
raise SystemExit(1)
folder_name = output_dir or repo_name.removeprefix(TEMPLATE_PREFIX)
dest = os.path.join(os.getcwd(), folder_name)
while os.path.exists(dest):
click.secho(f"Directory '{folder_name}' already exists.", fg="yellow")
folder_name = click.prompt(
"Enter a different directory name (or 'q' to quit)", type=str
)
if folder_name.lower() == "q":
return
dest = os.path.join(os.getcwd(), folder_name)
click.echo(
f"Downloading template '{repo_name.removeprefix(TEMPLATE_PREFIX)}'..."
)
zip_bytes = self._download_zip(repo_name)
self._extract_zip(zip_bytes, dest)
try:
from crewai.telemetry import Telemetry
telemetry = Telemetry()
telemetry.set_tracer()
telemetry.template_installed_span(repo_name.removeprefix(TEMPLATE_PREFIX))
except Exception:
logger.debug("Failed to record template install telemetry")
console.print(
f"\n [green]\u2713[/green] Installed template [bold white]{folder_name}[/bold white]"
f" [dim](source: github.com/{GITHUB_ORG}/{repo_name})[/dim]\n"
)
next_steps = Text()
next_steps.append(f" cd {folder_name}\n", style="bold white")
next_steps.append(" crewai install", style="bold white")
panel = Panel(
next_steps,
title="[green]\u25c7 Next steps[/green]",
title_align="left",
border_style="dim",
padding=(1, 2),
)
console.print(panel)
def _fetch_templates(self) -> list[dict[str, Any]]:
"""Fetch all template repos from the GitHub org."""
templates: list[dict[str, Any]] = []
page = 1
while True:
url = f"{GITHUB_API_BASE}/orgs/{GITHUB_ORG}/repos"
params: dict[str, str | int] = {
"per_page": 100,
"page": page,
"type": "public",
}
try:
response = httpx.get(url, params=params, timeout=15)
response.raise_for_status()
except httpx.HTTPError as e:
click.secho(f"Failed to fetch templates from GitHub: {e}", fg="red")
raise SystemExit(1) from e
repos = response.json()
if not repos:
break
templates.extend(
repo
for repo in repos
if repo["name"].startswith(TEMPLATE_PREFIX) and not repo.get("private")
)
page += 1
templates.sort(key=lambda r: r["name"])
return templates
def _resolve_repo_name(self, name: str) -> str | None:
"""Resolve user input to a full repo name, or None if not found."""
# Accept both 'deep_research' and 'template_deep_research'
candidates = [
f"{TEMPLATE_PREFIX}{name}"
if not name.startswith(TEMPLATE_PREFIX)
else name,
name,
]
templates = self._fetch_templates()
template_names = {t["name"] for t in templates}
for candidate in candidates:
if candidate in template_names:
return candidate
return None
def _download_zip(self, repo_name: str) -> bytes:
"""Download the default branch zipball for a repo."""
url = f"{GITHUB_API_BASE}/repos/{GITHUB_ORG}/{repo_name}/zipball"
try:
response = httpx.get(url, follow_redirects=True, timeout=60)
response.raise_for_status()
except httpx.HTTPError as e:
click.secho(f"Failed to download template: {e}", fg="red")
raise SystemExit(1) from e
return response.content
def _extract_zip(self, zip_bytes: bytes, dest: str) -> None:
"""Extract a GitHub zipball into dest, stripping the top-level directory."""
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as zf:
# GitHub zipballs have a single top-level dir like 'crewAIInc-template_xxx-<sha>/'
members = zf.namelist()
if not members:
click.secho("Downloaded archive is empty.", fg="red")
raise SystemExit(1)
top_dir = members[0].split("/")[0] + "/"
os.makedirs(dest, exist_ok=True)
for member in members:
if member == top_dir or not member.startswith(top_dir):
continue
relative_path = member[len(top_dir) :]
if not relative_path:
continue
target = os.path.join(dest, relative_path)
if member.endswith("/"):
os.makedirs(target, exist_ok=True)
else:
os.makedirs(os.path.dirname(target), exist_ok=True)
with zf.open(member) as src, open(target, "wb") as dst:
shutil.copyfileobj(src, dst)

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.2a3"
"crewai[tools]==1.14.2a5"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
authors = [{ name = "Your Name", email = "you@example.com" }]
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.2a3"
"crewai[tools]==1.14.2a5"
]
[project.scripts]

View File

@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"crewai[tools]==1.14.2a3"
"crewai[tools]==1.14.2a5"
]
[tool.crewai]

View File

@@ -17,10 +17,7 @@ from crewai.utilities.agent_utils import is_context_length_exceeded
from crewai.utilities.exceptions.context_window_exceeding_exception import (
LLMContextLengthExceededError,
)
from crewai.utilities.pydantic_schema_utils import (
generate_model_description,
sanitize_tool_params_for_bedrock_strict,
)
from crewai.utilities.pydantic_schema_utils import generate_model_description
from crewai.utilities.types import LLMMessage
@@ -173,7 +170,6 @@ class ToolSpec(TypedDict, total=False):
name: Required[str]
description: Required[str]
inputSchema: ToolInputSchema
strict: bool
class ConverseToolTypeDef(TypedDict):
@@ -1988,21 +1984,10 @@ class BedrockCompletion(BaseLLM):
"description": description,
}
func_info = tool.get("function", {})
strict_enabled = bool(func_info.get("strict"))
if parameters and isinstance(parameters, dict):
schema_params = (
sanitize_tool_params_for_bedrock_strict(parameters)
if strict_enabled
else parameters
)
input_schema: ToolInputSchema = {"json": schema_params}
input_schema: ToolInputSchema = {"json": parameters}
tool_spec["inputSchema"] = input_schema
if strict_enabled:
tool_spec["strict"] = True
converse_tool: ConverseToolTypeDef = {"toolSpec": tool_spec}
converse_tools.append(converse_tool)

View File

@@ -1058,20 +1058,3 @@ class Telemetry:
close_span(span)
self._safe_telemetry_operation(_operation)
def template_installed_span(self, template_name: str) -> None:
"""Records when a template is downloaded and installed.
Args:
template_name: Name of the template that was installed
(without the template_ prefix).
"""
def _operation() -> None:
tracer = trace.get_tracer("crewai.telemetry")
span = tracer.start_span("Template Installed")
self._add_attribute(span, "crewai_version", version("crewai"))
self._add_attribute(span, "template_name", template_name)
close_span(span)
self._safe_telemetry_operation(_operation)

View File

@@ -1,281 +0,0 @@
import io
import os
import zipfile
from unittest.mock import MagicMock, patch
import httpx
import pytest
from click.testing import CliRunner
from crewai.cli.cli import template_add, template_list
from crewai.cli.remote_template.main import TemplateCommand
@pytest.fixture
def runner():
return CliRunner()
SAMPLE_REPOS = [
{"name": "template_deep_research", "description": "Deep research template", "private": False},
{"name": "template_pull_request_review", "description": "PR review template", "private": False},
{"name": "template_conversational_example", "description": "Conversational demo", "private": False},
{"name": "crewai", "description": "Main repo", "private": False},
{"name": "marketplace-crew-template", "description": "Marketplace", "private": False},
]
def _make_zipball(files: dict[str, str], top_dir: str = "crewAIInc-template_test-abc123") -> bytes:
"""Create an in-memory zipball mimicking GitHub's format."""
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
zf.writestr(f"{top_dir}/", "")
for path, content in files.items():
zf.writestr(f"{top_dir}/{path}", content)
return buf.getvalue()
# --- CLI command tests ---
@patch("crewai.cli.cli.TemplateCommand")
def test_template_list_command(mock_cls, runner):
mock_instance = MagicMock()
mock_cls.return_value = mock_instance
result = runner.invoke(template_list)
assert result.exit_code == 0
mock_cls.assert_called_once()
mock_instance.list_templates.assert_called_once()
@patch("crewai.cli.cli.TemplateCommand")
def test_template_add_command(mock_cls, runner):
mock_instance = MagicMock()
mock_cls.return_value = mock_instance
result = runner.invoke(template_add, ["deep_research"])
assert result.exit_code == 0
mock_cls.assert_called_once()
mock_instance.add_template.assert_called_once_with("deep_research", None)
@patch("crewai.cli.cli.TemplateCommand")
def test_template_add_with_output_dir(mock_cls, runner):
mock_instance = MagicMock()
mock_cls.return_value = mock_instance
result = runner.invoke(template_add, ["deep_research", "-o", "my_project"])
assert result.exit_code == 0
mock_instance.add_template.assert_called_once_with("deep_research", "my_project")
# --- TemplateCommand unit tests ---
class TestTemplateCommand:
@pytest.fixture
def cmd(self):
with patch.object(TemplateCommand, "__init__", return_value=None):
return TemplateCommand()
@patch("crewai.cli.remote_template.main.httpx.get")
def test_fetch_templates_filters_by_prefix(self, mock_get, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
# Return empty on page 2 to stop pagination
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
templates = cmd._fetch_templates()
assert len(templates) == 3
assert all(t["name"].startswith("template_") for t in templates)
@patch("crewai.cli.remote_template.main.httpx.get")
def test_fetch_templates_excludes_private(self, mock_get, cmd):
repos = [
{"name": "template_private_one", "description": "", "private": True},
{"name": "template_public_one", "description": "", "private": False},
]
mock_response = MagicMock()
mock_response.json.return_value = repos
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
templates = cmd._fetch_templates()
assert len(templates) == 1
assert templates[0]["name"] == "template_public_one"
@patch("crewai.cli.remote_template.main.httpx.get")
def test_fetch_templates_api_error(self, mock_get, cmd):
mock_get.side_effect = httpx.HTTPError("connection error")
with pytest.raises(SystemExit):
cmd._fetch_templates()
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
@patch("crewai.cli.remote_template.main.httpx.get")
def test_list_templates_prints_output(self, mock_get, mock_prompt, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
with patch("crewai.cli.remote_template.main.console") as mock_console:
cmd.list_templates()
assert mock_console.print.call_count > 0
@patch("crewai.cli.remote_template.main.httpx.get")
def test_resolve_repo_name_with_prefix(self, mock_get, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
result = cmd._resolve_repo_name("template_deep_research")
assert result == "template_deep_research"
@patch("crewai.cli.remote_template.main.httpx.get")
def test_resolve_repo_name_without_prefix(self, mock_get, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
result = cmd._resolve_repo_name("deep_research")
assert result == "template_deep_research"
@patch("crewai.cli.remote_template.main.httpx.get")
def test_resolve_repo_name_not_found(self, mock_get, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
result = cmd._resolve_repo_name("nonexistent")
assert result is None
def test_extract_zip(self, cmd, tmp_path):
files = {
"README.md": "# Test Template",
"src/main.py": "print('hello')",
"config/settings.yaml": "key: value",
}
zip_bytes = _make_zipball(files)
dest = str(tmp_path / "output")
cmd._extract_zip(zip_bytes, dest)
assert os.path.isfile(os.path.join(dest, "README.md"))
assert os.path.isfile(os.path.join(dest, "src", "main.py"))
assert os.path.isfile(os.path.join(dest, "config", "settings.yaml"))
with open(os.path.join(dest, "src", "main.py")) as f:
assert f.read() == "print('hello')"
@patch.object(TemplateCommand, "_extract_zip")
@patch.object(TemplateCommand, "_download_zip")
@patch.object(TemplateCommand, "_resolve_repo_name")
def test_add_template_success(self, mock_resolve, mock_download, mock_extract, cmd, tmp_path):
mock_resolve.return_value = "template_deep_research"
mock_download.return_value = b"fake-zip-bytes"
os.chdir(tmp_path)
cmd.add_template("deep_research")
mock_resolve.assert_called_once_with("deep_research")
mock_download.assert_called_once_with("template_deep_research")
expected_dest = os.path.join(str(tmp_path), "deep_research")
mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
@patch.object(TemplateCommand, "_resolve_repo_name")
def test_add_template_not_found(self, mock_resolve, cmd):
mock_resolve.return_value = None
with pytest.raises(SystemExit):
cmd.add_template("nonexistent")
@patch.object(TemplateCommand, "_extract_zip")
@patch.object(TemplateCommand, "_download_zip")
@patch("crewai.cli.remote_template.main.click.prompt", return_value="my_project")
@patch.object(TemplateCommand, "_resolve_repo_name")
def test_add_template_dir_exists_prompts_rename(self, mock_resolve, mock_prompt, mock_download, mock_extract, cmd, tmp_path):
mock_resolve.return_value = "template_deep_research"
mock_download.return_value = b"fake-zip-bytes"
existing = tmp_path / "deep_research"
existing.mkdir()
os.chdir(tmp_path)
cmd.add_template("deep_research")
expected_dest = os.path.join(str(tmp_path), "my_project")
mock_extract.assert_called_once_with(b"fake-zip-bytes", expected_dest)
@patch.object(TemplateCommand, "_resolve_repo_name")
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
def test_add_template_dir_exists_quit(self, mock_prompt, mock_resolve, cmd, tmp_path):
mock_resolve.return_value = "template_deep_research"
existing = tmp_path / "deep_research"
existing.mkdir()
os.chdir(tmp_path)
cmd.add_template("deep_research")
# Should return without downloading
@patch.object(TemplateCommand, "add_template")
@patch("crewai.cli.remote_template.main.click.prompt", return_value="2")
@patch("crewai.cli.remote_template.main.httpx.get")
def test_list_templates_selects_and_installs(self, mock_get, mock_prompt, mock_add, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
with patch("crewai.cli.remote_template.main.console"):
cmd.list_templates()
# Templates are sorted by name; index 1 (choice "2") = template_deep_research
mock_add.assert_called_once_with("deep_research")
@patch.object(TemplateCommand, "add_template")
@patch("crewai.cli.remote_template.main.click.prompt", return_value="q")
@patch("crewai.cli.remote_template.main.httpx.get")
def test_list_templates_quit(self, mock_get, mock_prompt, mock_add, cmd):
mock_response = MagicMock()
mock_response.json.return_value = SAMPLE_REPOS
mock_response.raise_for_status = MagicMock()
mock_empty = MagicMock()
mock_empty.json.return_value = []
mock_empty.raise_for_status = MagicMock()
mock_get.side_effect = [mock_response, mock_empty]
with patch("crewai.cli.remote_template.main.console"):
cmd.list_templates()
mock_add.assert_not_called()

View File

@@ -1,3 +1,3 @@
"""CrewAI development tools."""
__version__ = "1.14.2a3"
__version__ = "1.14.2a5"