| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238 |
- """Tests for CLI migration helper utilities."""
- import json
- import os
- import re
- import subprocess
- import sys
- from pathlib import Path
- from subprocess import CalledProcessError, CompletedProcess
- import pytest
- from agency_swarm.cli import migrate_agent
- def _sanitize_name(name: str) -> str:
- """Mirror the generator's sanitizeName helper for assertions."""
- sanitized = re.sub(r"\s+", "_", name)
- sanitized = re.sub(r"[^a-zA-Z0-9_]", "", sanitized)
- sanitized = re.sub(r"_+", "_", sanitized)
- sanitized = sanitized.strip("_")
- if sanitized and sanitized[0].isdigit():
- sanitized = f"agent_{sanitized}"
- return sanitized or "agent"
- def test_check_node_dependencies_requires_node(monkeypatch: pytest.MonkeyPatch) -> None:
- """Ensure dependency check fails when Node.js is unavailable."""
- def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
- raise FileNotFoundError()
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- available, runner = migrate_agent.check_node_dependencies()
- assert available is False
- assert runner == ""
- def test_check_node_dependencies_requires_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
- """Ensure dependency check fails when tsx/ts-node is unavailable even if Node is present."""
- def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
- # Node is available
- if command[0] == "node":
- return CompletedProcess(command, 0)
- # tsx and ts-node are not available
- raise CalledProcessError(1, command)
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- available, runner = migrate_agent.check_node_dependencies()
- assert available is False
- assert runner == ""
- def test_check_node_dependencies_succeeds_with_tsx(monkeypatch: pytest.MonkeyPatch) -> None:
- """Dependency check should succeed when tsx is available as preferred runner."""
- def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
- if command[0] == "node":
- return CompletedProcess(command, 0)
- if command[:2] == ["npx", "tsx"]:
- return CompletedProcess(command, 0)
- raise CalledProcessError(1, command)
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- available, runner = migrate_agent.check_node_dependencies()
- assert available is True
- assert runner == "tsx"
- def test_check_node_dependencies_succeeds_with_npx_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
- """Dependency check should succeed when npx ts-node is available."""
- def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
- if command[0] == "node":
- return CompletedProcess(command, 0)
- if command[:2] == ["npx", "tsx"]:
- raise CalledProcessError(1, command) # tsx not available
- if command[:2] == ["npx", "ts-node"]:
- return CompletedProcess(command, 0)
- raise CalledProcessError(1, command)
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- available, runner = migrate_agent.check_node_dependencies()
- assert available is True
- assert runner == "ts-node"
- def test_check_node_dependencies_succeeds_with_global_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
- """Dependency check should succeed with globally installed ts-node when npx fails."""
- def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
- if command[0] == "node":
- return CompletedProcess(command, 0)
- if command[:2] == ["npx", "tsx"]:
- raise CalledProcessError(1, command) # tsx not available
- if command[:2] == ["npx", "ts-node"]:
- raise CalledProcessError(1, command) # npx ts-node not available
- if command[0] == "ts-node":
- return CompletedProcess(command, 0) # global install available
- raise CalledProcessError(1, command)
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- available, runner = migrate_agent.check_node_dependencies()
- assert available is True
- assert runner == "ts-node"
- def test_find_typescript_script_exists() -> None:
- """The generator script should be discoverable in the package."""
- ts_path = migrate_agent.find_typescript_script()
- assert ts_path is not None
- assert ts_path.exists()
- assert ts_path.name == "generate-agent-from-settings.ts"
- def test_migrate_agent_command_missing_settings_returns_error(tmp_path: Path) -> None:
- """Missing settings files should return exit code 1."""
- exit_code = migrate_agent.migrate_agent_command(str(tmp_path / "missing.json"), str(tmp_path))
- assert exit_code == 1
- def test_migrate_agent_command_missing_script_returns_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- """Missing TypeScript script should return exit code 1."""
- settings_path = tmp_path / "settings.json"
- settings_path.write_text("{}")
- monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: None)
- exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
- assert exit_code == 1
- def test_migrate_agent_command_dependency_failure_returns_error(
- tmp_path: Path, monkeypatch: pytest.MonkeyPatch
- ) -> None:
- """Dependency check failure should return exit code 1."""
- settings_path = tmp_path / "settings.json"
- settings_path.write_text("{}")
- ts_script = tmp_path / "generate-agent-from-settings.ts"
- ts_script.write_text("// stub")
- monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
- monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (False, ""))
- exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
- assert exit_code == 1
- def test_migrate_agent_command_successful_execution(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- """Successful execution should return exit code 0."""
- settings_path = tmp_path / "settings.json"
- settings_path.write_text("{}")
- ts_script = tmp_path / "generate-agent-from-settings.ts"
- ts_script.write_text("// stub")
- monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
- monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (True, "tsx"))
- completed = CompletedProcess(["npx", "tsx"], 0, stdout="Agent generated successfully\n", stderr="")
- def fake_run(command, capture_output, text, shell): # type: ignore[no-untyped-def]
- return completed
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
- assert exit_code == 0
- def test_migrate_agent_command_failed_execution(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
- """Failed execution should return non-zero exit code."""
- settings_path = tmp_path / "settings.json"
- settings_path.write_text("{}")
- ts_script = tmp_path / "generate-agent-from-settings.ts"
- ts_script.write_text("// stub")
- monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
- monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (True, "tsx"))
- completed = CompletedProcess(["npx", "tsx"], 2, stdout="", stderr="TypeScript error\n")
- def fake_run(command, capture_output, text, shell): # type: ignore[no-untyped-def]
- return completed
- monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
- exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
- assert exit_code == 2
- def test_generate_agent_script_escapes_strings(tmp_path: Path) -> None:
- """TypeScript generator should escape quotes in user-provided strings."""
- project_root = Path(__file__).parents[2]
- script_path = project_root / "src" / "agency_swarm" / "cli" / "utils" / "generate-agent-from-settings.ts"
- ts_node_binary = project_root / "node_modules" / ".bin" / ("ts-node.cmd" if sys.platform == "win32" else "ts-node")
- if not ts_node_binary.exists():
- pytest.skip("ts-node binary not available for generator test")
- settings = {
- "name": 'Quote "Tester"',
- "description": 'Says "hello" often',
- "instructions": "Be helpful",
- "model": "gpt-5.4-mini",
- }
- settings_path = tmp_path / "settings.json"
- settings_path.write_text(json.dumps(settings))
- env = os.environ.copy()
- env.setdefault("TS_NODE_TRANSPILE_ONLY", "true")
- subprocess.run(
- [str(ts_node_binary), str(script_path), str(settings_path)],
- cwd=tmp_path,
- check=True,
- capture_output=True,
- text=True,
- env=env,
- )
- agent_name = _sanitize_name(settings["name"])
- agent_file = tmp_path / agent_name / f"{agent_name}.py"
- content = agent_file.read_text()
- assert 'name="Quote "Tester""' not in content
- assert 'description="Says "hello" often"' not in content
- assert 'name="Quote \\"Tester\\""' in content
- assert 'description="Says \\"hello\\" often"' in content
|