from __future__ import annotations from typer.testing import CliRunner from clawteam.cli.commands import app from clawteam.team.manager import TeamManager class ErrorBackend: def spawn(self, **kwargs): return ( "Error: command 'nanobot' not found in PATH. " "Install the agent CLI first or pass an executable path." ) def list_running(self): return [] class RecordingBackend: def __init__(self): self.calls = [] def spawn(self, **kwargs): self.calls.append(kwargs) return f"Agent '{kwargs['agent_name']}' spawned" def list_running(self): return [] def test_spawn_cli_exits_nonzero_and_rolls_back_failed_member(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: ErrorBackend()) runner = CliRunner() result = runner.invoke( app, ["spawn", "tmux", "nanobot", "--team", "demo", "--agent-name", "alice", "--no-workspace"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 1 assert "Error: command 'nanobot' not found in PATH" in result.output assert [member.name for member in TeamManager.list_members("demo")] == ["leader"] def test_launch_cli_passes_skip_permissions_from_config(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) monkeypatch.chdir(tmp_path) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) runner = CliRunner() result = runner.invoke( app, ["launch", "hedge-fund", "--team", "fund1", "--goal", "Analyze AAPL"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 0 assert backend.calls assert all(call["skip_permissions"] is True for call in backend.calls) def test_spawn_cli_rejects_removed_acpx_backend(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) runner = CliRunner() result = runner.invoke( app, ["spawn", "acpx", "claude", "--team", "demo", "--agent-name", "alice", "--no-workspace"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 1 assert "Unknown spawn backend: acpx. Available: subprocess, tmux" in result.output def test_launch_cli_rejects_removed_acpx_backend(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) monkeypatch.chdir(tmp_path) runner = CliRunner() result = runner.invoke( app, ["launch", "hedge-fund", "--backend", "acpx", "--team", "fund1", "--goal", "Analyze AAPL"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 1 assert "Unknown spawn backend: acpx. Available: subprocess, tmux" in result.output def test_spawn_cli_applies_profile_command_and_env(monkeypatch, tmp_path): monkeypatch.setenv("MOONSHOT_API_KEY", "moonshot-secret") monkeypatch.setenv("HOME", str(tmp_path)) monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path / ".clawteam")) from clawteam.config import AgentProfile, ClawTeamConfig, save_config save_config( ClawTeamConfig( profiles={ "moonshot-kimi": AgentProfile( agent="kimi", model="kimi-k2-thinking-turbo", base_url="https://api.moonshot.cn/v1", api_key_env="MOONSHOT_API_KEY", args=["--config-file", "/tmp/kimi.toml"], ) } ) ) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) runner = CliRunner() result = runner.invoke( app, ["spawn", "subprocess", "--profile", "moonshot-kimi", "--team", "demo", "--agent-name", "alice", "--no-workspace", "--task", "say hi"], env={"HOME": str(tmp_path), "CLAWTEAM_DATA_DIR": str(tmp_path / ".clawteam"), "MOONSHOT_API_KEY": "moonshot-secret"}, ) assert result.exit_code == 0 call = backend.calls[0] assert call["command"] == ["kimi", "--model", "kimi-k2-thinking-turbo", "--config-file", "/tmp/kimi.toml"] assert call["env"]["KIMI_BASE_URL"] == "https://api.moonshot.cn/v1" assert call["env"]["KIMI_API_KEY"] == "moonshot-secret" def test_launch_cli_applies_profile_to_template_agents(monkeypatch, tmp_path): monkeypatch.setenv("MOONSHOT_API_KEY", "moonshot-secret") monkeypatch.setenv("HOME", str(tmp_path)) monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path / ".clawteam")) from clawteam.config import AgentProfile, ClawTeamConfig, save_config save_config( ClawTeamConfig( profiles={ "moonshot-kimi": AgentProfile( agent="kimi", model="kimi-k2-thinking-turbo", base_url="https://api.moonshot.cn/v1", api_key_env="MOONSHOT_API_KEY", ) } ) ) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) monkeypatch.chdir(tmp_path) runner = CliRunner() result = runner.invoke( app, ["launch", "hedge-fund", "--team", "fund1", "--goal", "Analyze AAPL", "--profile", "moonshot-kimi"], env={"HOME": str(tmp_path), "CLAWTEAM_DATA_DIR": str(tmp_path / ".clawteam"), "MOONSHOT_API_KEY": "moonshot-secret"}, ) assert result.exit_code == 0 assert backend.calls assert all(call["command"][:3] == ["kimi", "--model", "kimi-k2-thinking-turbo"] for call in backend.calls) assert all(call["env"]["KIMI_API_KEY"] == "moonshot-secret" for call in backend.calls) def test_spawn_cli_auto_creates_team_for_orchestrator(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) runner = CliRunner() result = runner.invoke( app, [ "spawn", "tmux", "claude", "--team", "auto-team", "--agent-name", "leader", "--agent-type", "orchestrator", "--no-workspace", "--task", "Build a todo app", ], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 0 team = TeamManager.get_team("auto-team") assert team is not None assert team.members[0].name == "leader" assert team.members[0].agent_type == "orchestrator" def test_spawn_cli_rolls_back_auto_created_team_on_spawn_error(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: ErrorBackend()) runner = CliRunner() result = runner.invoke( app, [ "spawn", "tmux", "nanobot", "--team", "auto-team", "--agent-name", "leader", "--agent-type", "orchestrator", "--no-workspace", ], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 1 assert TeamManager.get_team("auto-team") is None def test_spawn_cli_rejects_duplicate_running_agent_without_replace(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) monkeypatch.setattr("clawteam.spawn.registry.is_agent_alive", lambda team, agent: True) runner = CliRunner() result = runner.invoke( app, ["spawn", "tmux", "claude", "--team", "demo", "--agent-name", "alice", "--no-workspace"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 1 assert "already running" in result.output assert not backend.calls def test_spawn_cli_replace_stops_running_agent_before_respawn(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) backend = RecordingBackend() stop_calls: list[tuple[str, str]] = [] monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) monkeypatch.setattr("clawteam.spawn.registry.is_agent_alive", lambda team, agent: True) def _stop(team: str, agent: str, timeout_seconds: float = 3.0) -> bool: stop_calls.append((team, agent)) return True monkeypatch.setattr("clawteam.spawn.registry.stop_agent", _stop) runner = CliRunner() result = runner.invoke( app, ["spawn", "tmux", "claude", "--team", "demo", "--agent-name", "alice", "--no-workspace", "--replace"], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 0 assert stop_calls == [("demo", "alice")] assert backend.calls def test_spawn_cli_passes_repo_as_cwd_without_worktree_and_uses_repo_prompt(monkeypatch, tmp_path): monkeypatch.setenv("CLAWTEAM_DATA_DIR", str(tmp_path)) TeamManager.create_team( name="demo", leader_name="leader", leader_id="leader001", ) backend = RecordingBackend() monkeypatch.setattr("clawteam.spawn.get_backend", lambda _: backend) repo_path = tmp_path / "frontend" repo_path.mkdir() runner = CliRunner() result = runner.invoke( app, [ "spawn", "tmux", "claude", "--team", "demo", "--agent-name", "alice", "--no-workspace", "--repo", str(repo_path), "--task", "Work on frontend", ], env={"CLAWTEAM_DATA_DIR": str(tmp_path)}, ) assert result.exit_code == 0 assert len(backend.calls) == 1 call = backend.calls[0] assert call["cwd"] == str(repo_path.resolve()) assert "Working directory: " + str(repo_path.resolve()) in call["prompt"] assert "Work directly in this repository path" in call["prompt"] assert "isolated git worktree" not in call["prompt"]