fix: tighten json crew deploy validation

This commit is contained in:
Joao Moura
2026-06-19 11:07:26 -07:00
parent cddf658942
commit 31f9c3eeef
4 changed files with 111 additions and 6 deletions

View File

@@ -212,8 +212,16 @@ class DeployValidator:
if crew_path is None:
return self.results
agents_dir = self.project_root / "agents"
self._check_pyproject()
self._check_lockfile()
agents_dir_ok = self._check_json_agents_dir(agents_dir)
project = None
try:
project = validate_crew_project(crew_path, self.project_root / "agents")
if agents_dir_ok:
project = validate_crew_project(crew_path, agents_dir)
except JSONProjectValidationError as e:
self._add(
Severity.ERROR,
@@ -232,15 +240,27 @@ class DeployValidator:
)
return self.results
agents_dir = self.project_root / "agents"
self._check_pyproject()
self._check_lockfile()
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
if project is not None:
self._check_env_vars_json(crew_path, agents_dir, project.agent_names)
self._check_version_vs_lockfile()
return self.results
def _check_json_agents_dir(self, agents_dir: Path) -> bool:
if agents_dir.is_dir():
return True
self._add(
Severity.ERROR,
"missing_agents_dir",
"Cannot find agents/ directory",
detail=(
"JSON crew projects load agent definitions from "
f"{agents_dir.relative_to(self.project_root)}/*.jsonc or *.json."
),
hint="Create agents/ and add one JSON or JSONC file per agent.",
)
return False
def _check_env_vars_json(
self, crew_path: Path, agents_dir: Path, agent_names: list[str]
) -> None:

View File

@@ -168,6 +168,39 @@ type = "crew"
assert "run_crew" not in pyproject
def test_create_project_zip_keeps_json_project_root_shape(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""
[project]
name = "json_crew"
version = "0.1.0"
dependencies = ["crewai[tools]==1.14.8a1"]
[tool.crewai]
type = "crew"
""".strip()
+ "\n"
)
(tmp_path / "uv.lock").write_text("# lock\n")
(tmp_path / "agents").mkdir()
(tmp_path / "agents" / "foo.jsonc").write_text("{}\n")
(tmp_path / "crew.jsonc").write_text("{}\n")
archive_path = create_project_zip("json_crew", project_dir=tmp_path)
try:
with zipfile.ZipFile(archive_path) as archive:
names = set(archive.namelist())
finally:
archive_path.unlink(missing_ok=True)
assert names == {
"agents/foo.jsonc",
"crew.jsonc",
"pyproject.toml",
"uv.lock",
}
def test_create_project_zip_does_not_rewrite_json_project_scripts(tmp_path: Path):
(tmp_path / "pyproject.toml").write_text(
"""

View File

@@ -200,6 +200,41 @@ def test_json_runtime_fields_are_deploy_errors(tmp_path: Path) -> None:
assert "runtime-only" in finding.detail
def test_json_crew_requires_agents_dir_without_classic_errors(tmp_path: Path) -> None:
_scaffold_json_crew(tmp_path)
for path in (tmp_path / "agents").iterdir():
path.unlink()
(tmp_path / "agents").rmdir()
v = DeployValidator(project_root=tmp_path)
v.run()
codes = _codes(v)
assert "missing_agents_dir" in codes
assert "missing_src_dir" not in codes
assert "missing_crew_py" not in codes
assert "missing_agents_yaml" not in codes
assert "missing_tasks_yaml" not in codes
def test_json_crew_reports_project_metadata_before_invalid_json(
tmp_path: Path,
) -> None:
_scaffold_json_crew(tmp_path)
(tmp_path / "pyproject.toml").unlink()
(tmp_path / "uv.lock").unlink()
(tmp_path / "crew.jsonc").write_text('{"agents": ["researcher"], "tasks": []}\n')
v = DeployValidator(project_root=tmp_path)
v.run()
codes = _codes(v)
assert "missing_pyproject" in codes
assert "missing_lockfile" in codes
assert "invalid_crew_json" in codes
assert "missing_src_dir" not in codes
def test_missing_pyproject_errors(tmp_path: Path) -> None:
v = _run_without_import_check(tmp_path)
assert "missing_pyproject" in _codes(v)

View File

@@ -715,6 +715,23 @@ def test_json_create_provider_preselects_default_model(tmp_path, monkeypatch):
assert not (tmp_path / "json_crew" / "src").exists()
assert not (tmp_path / "json_crew" / "tests").exists()
assert not (tmp_path / "json_crew" / "config.jsonc").exists()
generated_paths = {
path.relative_to(tmp_path / "json_crew").as_posix()
for path in (tmp_path / "json_crew").rglob("*")
if path.is_file()
}
assert not any(
path.endswith("/crew.py") or path == "crew.py" for path in generated_paths
)
assert not any(
path.endswith("/agents.yaml") or path == "agents.yaml"
for path in generated_paths
)
assert not any(
path.endswith("/tasks.yaml") or path == "tasks.yaml"
for path in generated_paths
)
assert not any(path.startswith("src/") for path in generated_paths)
pyproject = tomli.loads((tmp_path / "json_crew" / "pyproject.toml").read_text())
dependency = pyproject["project"]["dependencies"][0]