fischer-agentkit/tests/unit/test_onboarding.py

217 lines
9.1 KiB
Python

"""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