test_migrate_agent.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. """Tests for CLI migration helper utilities."""
  2. import json
  3. import os
  4. import re
  5. import subprocess
  6. import sys
  7. from pathlib import Path
  8. from subprocess import CalledProcessError, CompletedProcess
  9. import pytest
  10. from agency_swarm.cli import migrate_agent
  11. def _sanitize_name(name: str) -> str:
  12. """Mirror the generator's sanitizeName helper for assertions."""
  13. sanitized = re.sub(r"\s+", "_", name)
  14. sanitized = re.sub(r"[^a-zA-Z0-9_]", "", sanitized)
  15. sanitized = re.sub(r"_+", "_", sanitized)
  16. sanitized = sanitized.strip("_")
  17. if sanitized and sanitized[0].isdigit():
  18. sanitized = f"agent_{sanitized}"
  19. return sanitized or "agent"
  20. def test_check_node_dependencies_requires_node(monkeypatch: pytest.MonkeyPatch) -> None:
  21. """Ensure dependency check fails when Node.js is unavailable."""
  22. def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
  23. raise FileNotFoundError()
  24. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  25. available, runner = migrate_agent.check_node_dependencies()
  26. assert available is False
  27. assert runner == ""
  28. def test_check_node_dependencies_requires_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
  29. """Ensure dependency check fails when tsx/ts-node is unavailable even if Node is present."""
  30. def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
  31. # Node is available
  32. if command[0] == "node":
  33. return CompletedProcess(command, 0)
  34. # tsx and ts-node are not available
  35. raise CalledProcessError(1, command)
  36. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  37. available, runner = migrate_agent.check_node_dependencies()
  38. assert available is False
  39. assert runner == ""
  40. def test_check_node_dependencies_succeeds_with_tsx(monkeypatch: pytest.MonkeyPatch) -> None:
  41. """Dependency check should succeed when tsx is available as preferred runner."""
  42. def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
  43. if command[0] == "node":
  44. return CompletedProcess(command, 0)
  45. if command[:2] == ["npx", "tsx"]:
  46. return CompletedProcess(command, 0)
  47. raise CalledProcessError(1, command)
  48. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  49. available, runner = migrate_agent.check_node_dependencies()
  50. assert available is True
  51. assert runner == "tsx"
  52. def test_check_node_dependencies_succeeds_with_npx_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
  53. """Dependency check should succeed when npx ts-node is available."""
  54. def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
  55. if command[0] == "node":
  56. return CompletedProcess(command, 0)
  57. if command[:2] == ["npx", "tsx"]:
  58. raise CalledProcessError(1, command) # tsx not available
  59. if command[:2] == ["npx", "ts-node"]:
  60. return CompletedProcess(command, 0)
  61. raise CalledProcessError(1, command)
  62. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  63. available, runner = migrate_agent.check_node_dependencies()
  64. assert available is True
  65. assert runner == "ts-node"
  66. def test_check_node_dependencies_succeeds_with_global_ts_node(monkeypatch: pytest.MonkeyPatch) -> None:
  67. """Dependency check should succeed with globally installed ts-node when npx fails."""
  68. def fake_run(command, capture_output, check, shell): # type: ignore[no-untyped-def]
  69. if command[0] == "node":
  70. return CompletedProcess(command, 0)
  71. if command[:2] == ["npx", "tsx"]:
  72. raise CalledProcessError(1, command) # tsx not available
  73. if command[:2] == ["npx", "ts-node"]:
  74. raise CalledProcessError(1, command) # npx ts-node not available
  75. if command[0] == "ts-node":
  76. return CompletedProcess(command, 0) # global install available
  77. raise CalledProcessError(1, command)
  78. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  79. available, runner = migrate_agent.check_node_dependencies()
  80. assert available is True
  81. assert runner == "ts-node"
  82. def test_find_typescript_script_exists() -> None:
  83. """The generator script should be discoverable in the package."""
  84. ts_path = migrate_agent.find_typescript_script()
  85. assert ts_path is not None
  86. assert ts_path.exists()
  87. assert ts_path.name == "generate-agent-from-settings.ts"
  88. def test_migrate_agent_command_missing_settings_returns_error(tmp_path: Path) -> None:
  89. """Missing settings files should return exit code 1."""
  90. exit_code = migrate_agent.migrate_agent_command(str(tmp_path / "missing.json"), str(tmp_path))
  91. assert exit_code == 1
  92. def test_migrate_agent_command_missing_script_returns_error(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
  93. """Missing TypeScript script should return exit code 1."""
  94. settings_path = tmp_path / "settings.json"
  95. settings_path.write_text("{}")
  96. monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: None)
  97. exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
  98. assert exit_code == 1
  99. def test_migrate_agent_command_dependency_failure_returns_error(
  100. tmp_path: Path, monkeypatch: pytest.MonkeyPatch
  101. ) -> None:
  102. """Dependency check failure should return exit code 1."""
  103. settings_path = tmp_path / "settings.json"
  104. settings_path.write_text("{}")
  105. ts_script = tmp_path / "generate-agent-from-settings.ts"
  106. ts_script.write_text("// stub")
  107. monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
  108. monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (False, ""))
  109. exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
  110. assert exit_code == 1
  111. def test_migrate_agent_command_successful_execution(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
  112. """Successful execution should return exit code 0."""
  113. settings_path = tmp_path / "settings.json"
  114. settings_path.write_text("{}")
  115. ts_script = tmp_path / "generate-agent-from-settings.ts"
  116. ts_script.write_text("// stub")
  117. monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
  118. monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (True, "tsx"))
  119. completed = CompletedProcess(["npx", "tsx"], 0, stdout="Agent generated successfully\n", stderr="")
  120. def fake_run(command, capture_output, text, shell): # type: ignore[no-untyped-def]
  121. return completed
  122. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  123. exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
  124. assert exit_code == 0
  125. def test_migrate_agent_command_failed_execution(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
  126. """Failed execution should return non-zero exit code."""
  127. settings_path = tmp_path / "settings.json"
  128. settings_path.write_text("{}")
  129. ts_script = tmp_path / "generate-agent-from-settings.ts"
  130. ts_script.write_text("// stub")
  131. monkeypatch.setattr(migrate_agent, "find_typescript_script", lambda: ts_script)
  132. monkeypatch.setattr(migrate_agent, "check_node_dependencies", lambda: (True, "tsx"))
  133. completed = CompletedProcess(["npx", "tsx"], 2, stdout="", stderr="TypeScript error\n")
  134. def fake_run(command, capture_output, text, shell): # type: ignore[no-untyped-def]
  135. return completed
  136. monkeypatch.setattr("agency_swarm.cli.migrate_agent.subprocess.run", fake_run)
  137. exit_code = migrate_agent.migrate_agent_command(str(settings_path), str(tmp_path))
  138. assert exit_code == 2
  139. def test_generate_agent_script_escapes_strings(tmp_path: Path) -> None:
  140. """TypeScript generator should escape quotes in user-provided strings."""
  141. project_root = Path(__file__).parents[2]
  142. script_path = project_root / "src" / "agency_swarm" / "cli" / "utils" / "generate-agent-from-settings.ts"
  143. ts_node_binary = project_root / "node_modules" / ".bin" / ("ts-node.cmd" if sys.platform == "win32" else "ts-node")
  144. if not ts_node_binary.exists():
  145. pytest.skip("ts-node binary not available for generator test")
  146. settings = {
  147. "name": 'Quote "Tester"',
  148. "description": 'Says "hello" often',
  149. "instructions": "Be helpful",
  150. "model": "gpt-5.4-mini",
  151. }
  152. settings_path = tmp_path / "settings.json"
  153. settings_path.write_text(json.dumps(settings))
  154. env = os.environ.copy()
  155. env.setdefault("TS_NODE_TRANSPILE_ONLY", "true")
  156. subprocess.run(
  157. [str(ts_node_binary), str(script_path), str(settings_path)],
  158. cwd=tmp_path,
  159. check=True,
  160. capture_output=True,
  161. text=True,
  162. env=env,
  163. )
  164. agent_name = _sanitize_name(settings["name"])
  165. agent_file = tmp_path / agent_name / f"{agent_name}.py"
  166. content = agent_file.read_text()
  167. assert 'name="Quote "Tester""' not in content
  168. assert 'description="Says "hello" often"' not in content
  169. assert 'name="Quote \\"Tester\\""' in content
  170. assert 'description="Says \\"hello\\" often"' in content