2025-12-28 21:55:01 +08:00
|
|
|
"""
|
|
|
|
|
Tests for configuration loading and validation.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import tempfile
|
2025-12-29 00:11:02 +08:00
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
import pytest
|
2025-12-28 21:55:01 +08:00
|
|
|
|
2025-12-29 11:15:18 +08:00
|
|
|
from flakestorm.core.config import (
|
2025-12-28 21:55:01 +08:00
|
|
|
AgentConfig,
|
|
|
|
|
AgentType,
|
2025-12-29 11:15:18 +08:00
|
|
|
FlakeStormConfig,
|
2025-12-29 00:11:02 +08:00
|
|
|
InvariantConfig,
|
2025-12-28 21:55:01 +08:00
|
|
|
InvariantType,
|
2025-12-29 00:11:02 +08:00
|
|
|
MutationConfig,
|
|
|
|
|
MutationType,
|
|
|
|
|
create_default_config,
|
|
|
|
|
load_config,
|
2025-12-28 21:55:01 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2025-12-29 11:15:18 +08:00
|
|
|
class TestFlakeStormConfig:
|
|
|
|
|
"""Tests for FlakeStormConfig."""
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_create_default_config(self):
|
|
|
|
|
"""Test creating a default configuration."""
|
|
|
|
|
config = create_default_config()
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
assert config.version == "1.0"
|
|
|
|
|
assert config.agent.type == AgentType.HTTP
|
|
|
|
|
assert config.model.provider == "ollama"
|
|
|
|
|
assert config.model.name == "qwen3:8b"
|
|
|
|
|
assert len(config.golden_prompts) >= 1
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_config_to_yaml(self):
|
|
|
|
|
"""Test serializing config to YAML."""
|
|
|
|
|
config = create_default_config()
|
|
|
|
|
yaml_str = config.to_yaml()
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
assert "version" in yaml_str
|
|
|
|
|
assert "agent" in yaml_str
|
|
|
|
|
assert "golden_prompts" in yaml_str
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_config_from_yaml(self):
|
|
|
|
|
"""Test parsing config from YAML."""
|
|
|
|
|
yaml_content = """
|
|
|
|
|
version: "1.0"
|
|
|
|
|
agent:
|
|
|
|
|
endpoint: "http://localhost:8000/test"
|
|
|
|
|
type: "http"
|
|
|
|
|
timeout: 5000
|
|
|
|
|
model:
|
|
|
|
|
provider: "ollama"
|
|
|
|
|
name: "qwen3:8b"
|
|
|
|
|
golden_prompts:
|
|
|
|
|
- "Test prompt 1"
|
|
|
|
|
- "Test prompt 2"
|
|
|
|
|
invariants:
|
|
|
|
|
- type: "latency"
|
|
|
|
|
max_ms: 1000
|
|
|
|
|
"""
|
2025-12-29 11:15:18 +08:00
|
|
|
config = FlakeStormConfig.from_yaml(yaml_content)
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
assert config.agent.endpoint == "http://localhost:8000/test"
|
|
|
|
|
assert config.agent.timeout == 5000
|
|
|
|
|
assert len(config.golden_prompts) == 2
|
|
|
|
|
assert len(config.invariants) == 1
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_load_config_file_not_found(self):
|
|
|
|
|
"""Test loading a non-existent config file."""
|
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
|
|
|
load_config("/nonexistent/path/config.yaml")
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_load_config_from_file(self):
|
|
|
|
|
"""Test loading config from an actual file."""
|
|
|
|
|
yaml_content = """
|
|
|
|
|
version: "1.0"
|
|
|
|
|
agent:
|
|
|
|
|
endpoint: "http://test:8000/invoke"
|
|
|
|
|
golden_prompts:
|
|
|
|
|
- "Hello world"
|
2026-03-06 23:33:21 +08:00
|
|
|
invariants:
|
|
|
|
|
- type: "latency"
|
|
|
|
|
max_ms: 5000
|
2025-12-28 21:55:01 +08:00
|
|
|
"""
|
2025-12-29 00:11:02 +08:00
|
|
|
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as f:
|
2025-12-28 21:55:01 +08:00
|
|
|
f.write(yaml_content)
|
|
|
|
|
f.flush()
|
2026-03-06 23:33:21 +08:00
|
|
|
path = f.name
|
|
|
|
|
config = load_config(path)
|
|
|
|
|
assert config.agent.endpoint == "http://test:8000/invoke"
|
|
|
|
|
Path(path).unlink(missing_ok=True)
|
2025-12-28 21:55:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestAgentConfig:
|
|
|
|
|
"""Tests for AgentConfig validation."""
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_valid_http_config(self):
|
|
|
|
|
"""Test valid HTTP agent config."""
|
|
|
|
|
config = AgentConfig(
|
|
|
|
|
endpoint="http://localhost:8000/invoke",
|
|
|
|
|
type=AgentType.HTTP,
|
|
|
|
|
timeout=30000,
|
|
|
|
|
)
|
|
|
|
|
assert config.endpoint == "http://localhost:8000/invoke"
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_timeout_bounds(self):
|
|
|
|
|
"""Test timeout validation."""
|
|
|
|
|
# Valid
|
|
|
|
|
config = AgentConfig(endpoint="http://test", timeout=1000)
|
|
|
|
|
assert config.timeout == 1000
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
# Too low
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
AgentConfig(endpoint="http://test", timeout=500)
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_env_var_expansion(self):
|
|
|
|
|
"""Test environment variable expansion in headers."""
|
|
|
|
|
import os
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
os.environ["TEST_API_KEY"] = "secret123"
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
config = AgentConfig(
|
|
|
|
|
endpoint="http://test",
|
|
|
|
|
headers={"Authorization": "Bearer ${TEST_API_KEY}"},
|
|
|
|
|
)
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
assert config.headers["Authorization"] == "Bearer secret123"
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
del os.environ["TEST_API_KEY"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMutationConfig:
|
|
|
|
|
"""Tests for MutationConfig."""
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_default_mutation_types(self):
|
|
|
|
|
"""Test default mutation types are set."""
|
|
|
|
|
config = MutationConfig()
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
assert MutationType.PARAPHRASE in config.types
|
|
|
|
|
assert MutationType.NOISE in config.types
|
|
|
|
|
assert MutationType.PROMPT_INJECTION in config.types
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_mutation_weights(self):
|
|
|
|
|
"""Test mutation weights."""
|
|
|
|
|
config = MutationConfig()
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
# Prompt injection should have higher weight
|
2025-12-29 00:11:02 +08:00
|
|
|
assert (
|
|
|
|
|
config.weights[MutationType.PROMPT_INJECTION]
|
|
|
|
|
> config.weights[MutationType.NOISE]
|
|
|
|
|
)
|
2025-12-28 21:55:01 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestInvariantConfig:
|
|
|
|
|
"""Tests for InvariantConfig validation."""
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_latency_invariant(self):
|
|
|
|
|
"""Test latency invariant requires max_ms."""
|
|
|
|
|
config = InvariantConfig(type=InvariantType.LATENCY, max_ms=2000)
|
|
|
|
|
assert config.max_ms == 2000
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_latency_missing_max_ms(self):
|
|
|
|
|
"""Test latency invariant fails without max_ms."""
|
|
|
|
|
with pytest.raises(ValueError):
|
|
|
|
|
InvariantConfig(type=InvariantType.LATENCY)
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_contains_invariant(self):
|
|
|
|
|
"""Test contains invariant requires value."""
|
|
|
|
|
config = InvariantConfig(type=InvariantType.CONTAINS, value="test")
|
|
|
|
|
assert config.value == "test"
|
2025-12-29 00:11:02 +08:00
|
|
|
|
2025-12-28 21:55:01 +08:00
|
|
|
def test_similarity_invariant(self):
|
|
|
|
|
"""Test similarity invariant."""
|
|
|
|
|
config = InvariantConfig(
|
|
|
|
|
type=InvariantType.SIMILARITY,
|
|
|
|
|
expected="Expected response",
|
|
|
|
|
threshold=0.8,
|
|
|
|
|
)
|
|
|
|
|
assert config.threshold == 0.8
|