"""Tests for onboarding wizard and chat command.""" from pathlib import Path from unittest.mock import patch import yaml from agentkit.cli.onboarding import ( PROVIDER_PRESETS, needs_onboarding, run_onboarding, ) class TestNeedsOnboarding: def test_needs_onboarding_when_no_config(self, tmp_path, monkeypatch): """Should return True when no config file exists.""" monkeypatch.chdir(tmp_path) monkeypatch.delenv("AGENTKIT_CONFIG_PATH", raising=False) assert needs_onboarding() is True def test_no_onboarding_when_config_exists(self, tmp_path, monkeypatch): """Should return False when agentkit.yaml exists.""" config_file = tmp_path / "agentkit.yaml" config_file.write_text("server:\n port: 8001\n") monkeypatch.chdir(tmp_path) assert needs_onboarding(config_arg=str(config_file)) is False def test_no_onboarding_with_home_config(self, tmp_path, monkeypatch): """Should return False when ~/.agentkit/agentkit.yaml exists.""" home_dir = tmp_path / "home" home_dir.mkdir() agentkit_dir = home_dir / ".agentkit" agentkit_dir.mkdir() (agentkit_dir / "agentkit.yaml").write_text("server:\n port: 8001\n") monkeypatch.setenv("HOME", str(home_dir)) monkeypatch.chdir(tmp_path / "empty" if (tmp_path / "empty").exists() else tmp_path) # Create empty cwd to ensure no local config empty_dir = tmp_path / "empty" empty_dir.mkdir() monkeypatch.chdir(empty_dir) assert needs_onboarding() is False class TestProviderPresets: def test_all_presets_have_required_fields(self): """Every provider preset must have name, env_key, base_url, models.""" for key, preset in PROVIDER_PRESETS.items(): assert "name" in preset, f"{key} missing name" assert "env_key" in preset, f"{key} missing env_key" assert "base_url" in preset, f"{key} missing base_url" assert "models" in preset, f"{key} missing models" assert preset["models"], f"{key} has empty models" assert "default_model" in preset, f"{key} missing default_model" def test_preset_keys_are_lowercase(self): """Provider keys should be lowercase.""" for key in PROVIDER_PRESETS: assert key == key.lower(), f"Provider key '{key}' should be lowercase" def test_deepseek_preset(self): """DeepSeek preset should have correct configuration.""" ds = PROVIDER_PRESETS["deepseek"] assert ds["env_key"] == "DEEPSEEK_API_KEY" assert "deepseek-chat" in ds["models"] assert ds["type"] == "openai" def test_qwen_preset(self): """Qwen preset should use DashScope endpoint.""" qwen = PROVIDER_PRESETS["qwen"] assert "dashscope" in qwen["base_url"] assert qwen["env_key"] == "DASHSCOPE_API_KEY" class TestRunOnboarding: def test_onboarding_generates_config_files(self, tmp_path, monkeypatch): """Onboarding should generate agentkit.yaml and .env.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("HOME", str(tmp_path / "home")) (tmp_path / "home").mkdir(exist_ok=True) # Mock user input with patch("agentkit.cli.onboarding.Prompt") as mock_prompt, \ patch("agentkit.cli.onboarding.Confirm") as mock_confirm: # Step 1: Select DeepSeek (option 1) # Step 2: API key # Step 2b: Select model (1 = deepseek-chat, default) # Step 5: Agent personality (name, personality, speaking_style) mock_prompt.ask.side_effect = ["1", "sk-test-deepseek-key", "1", "小王", "友好耐心", "简洁专业"] # Step 3: No second provider mock_confirm.ask.return_value = False config_path = run_onboarding(output_dir=str(tmp_path)) assert config_path is not None assert Path(config_path).exists() # Verify agentkit.yaml content with open(config_path) as f: config = yaml.safe_load(f) assert "llm" in config assert "deepseek" in config["llm"]["providers"] assert config["llm"]["providers"]["deepseek"]["base_url"] == "https://api.deepseek.com/v1" # Verify .env content env_path = tmp_path / ".env" assert env_path.exists() env_content = env_path.read_text() assert "DEEPSEEK_API_KEY=sk-test-deepseek-key" in env_content def test_onboarding_with_two_providers(self, tmp_path, monkeypatch): """Onboarding should support adding a second provider.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("HOME", str(tmp_path / "home")) (tmp_path / "home").mkdir(exist_ok=True) with patch("agentkit.cli.onboarding.Prompt") as mock_prompt, \ patch("agentkit.cli.onboarding.Confirm") as mock_confirm: # Select DeepSeek (1), API key, model (1), then Qwen as second # After removing deepseek, remaining = [openai, bailian-coding, qwen, doubao, gemini, anthropic] # qwen is at index 2, so option 3 # Step 5: Agent personality defaults mock_prompt.ask.side_effect = ["1", "sk-deepseek", "1", "3", "sk-dashscope", "1", "AgentKit", "专业、友好、注重细节", "简洁清晰"] mock_confirm.ask.return_value = True config_path = run_onboarding(output_dir=str(tmp_path)) with open(config_path) as f: config = yaml.safe_load(f) providers = config["llm"]["providers"] assert "deepseek" in providers assert "qwen" in providers env_path = tmp_path / ".env" env_content = env_path.read_text() assert "DEEPSEEK_API_KEY=sk-deepseek" in env_content assert "DASHSCOPE_API_KEY=sk-dashscope" in env_content def test_onboarding_cancelled_on_empty_api_key(self, tmp_path, monkeypatch): """Onboarding should return None if API key is empty.""" monkeypatch.chdir(tmp_path) with patch("agentkit.cli.onboarding.Prompt") as mock_prompt: mock_prompt.ask.side_effect = ["1", ""] # Empty API key result = run_onboarding(output_dir=str(tmp_path)) assert result is None def test_onboarding_config_has_memory_backend(self, tmp_path, monkeypatch): """Generated config should use memory backends by default.""" monkeypatch.chdir(tmp_path) monkeypatch.setenv("HOME", str(tmp_path / "home")) (tmp_path / "home").mkdir(exist_ok=True) with patch("agentkit.cli.onboarding.Prompt") as mock_prompt, \ patch("agentkit.cli.onboarding.Confirm") as mock_confirm: mock_prompt.ask.side_effect = ["1", "sk-test-key", "1", "AgentKit", "专业、友好、注重细节", "简洁清晰"] mock_confirm.ask.return_value = False config_path = run_onboarding(output_dir=str(tmp_path)) with open(config_path) as f: config = yaml.safe_load(f) assert config["session"]["backend"] == "memory" assert config["bus"]["backend"] == "memory" assert config["task_store"]["backend"] == "memory" class TestOnboardingSoulGeneration: """U5: Onboarding 生成 SOUL.md 测试.""" def test_onboarding_creates_soul_md(self, tmp_path, monkeypatch): """Onboarding should create SOUL.md with custom agent name.""" monkeypatch.chdir(tmp_path) home_dir = tmp_path / "home" home_dir.mkdir(exist_ok=True) monkeypatch.setenv("HOME", str(home_dir)) with patch("agentkit.cli.onboarding.Prompt") as mock_prompt, \ patch("agentkit.cli.onboarding.Confirm") as mock_confirm: mock_prompt.ask.side_effect = ["1", "sk-test-key", "1", "小王", "友好耐心", "简洁专业"] mock_confirm.ask.return_value = False run_onboarding(output_dir=str(tmp_path)) # Verify SOUL.md was created soul_path = home_dir / ".agentkit" / "SOUL.md" assert soul_path.exists() soul_content = soul_path.read_text(encoding="utf-8") assert "小王" in soul_content assert "友好耐心" in soul_content assert "简洁专业" in soul_content def test_onboarding_soul_with_defaults(self, tmp_path, monkeypatch): """Onboarding with default personality should create default SOUL.md.""" monkeypatch.chdir(tmp_path) home_dir = tmp_path / "home" home_dir.mkdir(exist_ok=True) monkeypatch.setenv("HOME", str(home_dir)) with patch("agentkit.cli.onboarding.Prompt") as mock_prompt, \ patch("agentkit.cli.onboarding.Confirm") as mock_confirm: # Prompt.ask returns the default value when user presses Enter # Our mock needs to return the actual default values mock_prompt.ask.side_effect = ["1", "sk-test-key", "1", "AgentKit", "专业、友好、注重细节", "简洁清晰"] mock_confirm.ask.return_value = False run_onboarding(output_dir=str(tmp_path)) soul_path = home_dir / ".agentkit" / "SOUL.md" assert soul_path.exists() soul_content = soul_path.read_text(encoding="utf-8") assert "AgentKit" in soul_content