Initial commit: Clean DSS implementation
Migrated from design-system-swarm with fresh git history.
Old project history preserved in /home/overbits/apps/design-system-swarm
Core components:
- MCP Server (Python FastAPI with mcp 1.23.1)
- Claude Plugin (agents, commands, skills, strategies, hooks, core)
- DSS Backend (dss-mvp1 - token translation, Figma sync)
- Admin UI (Node.js/React)
- Server (Node.js/Express)
- Storybook integration (dss-mvp1/.storybook)
Self-contained configuration:
- All paths relative or use DSS_BASE_PATH=/home/overbits/dss
- PYTHONPATH configured for dss-mvp1 and dss-claude-plugin
- .env file with all configuration
- Claude plugin uses ${CLAUDE_PLUGIN_ROOT} for portability
Migration completed: $(date)
🤖 Clean migration with full functionality preserved
This commit is contained in:
201
tests/README.md
Normal file
201
tests/README.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# DSS Test Suite
|
||||
|
||||
Comprehensive test suite for Design System Server.
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Install pytest if not already installed
|
||||
pip install pytest pytest-asyncio
|
||||
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/test_ingestion.py
|
||||
|
||||
# Run with verbose output
|
||||
pytest -v
|
||||
|
||||
# Run with coverage (requires pytest-cov)
|
||||
pip install pytest-cov
|
||||
pytest --cov=tools --cov-report=html
|
||||
|
||||
# Run only fast tests (skip slow integration tests)
|
||||
pytest -m "not slow"
|
||||
```
|
||||
|
||||
## Test Structure
|
||||
|
||||
```
|
||||
tests/
|
||||
├── conftest.py # Shared fixtures and configuration
|
||||
├── test_ingestion.py # Token ingestion tests (CSS, SCSS, JSON)
|
||||
├── test_merge.py # Token merging and conflict resolution
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## Test Categories
|
||||
|
||||
### Unit Tests
|
||||
Fast, isolated tests for individual functions/classes.
|
||||
- Token parsing
|
||||
- Merge strategies
|
||||
- Collection operations
|
||||
|
||||
### Integration Tests (marked with `@pytest.mark.slow`)
|
||||
Tests that interact with external systems or files.
|
||||
- Figma API (requires FIGMA_TOKEN)
|
||||
- File system operations
|
||||
- Database operations
|
||||
|
||||
### Async Tests (marked with `@pytest.mark.asyncio`)
|
||||
Tests for async functions.
|
||||
- All ingestion operations
|
||||
- API endpoints
|
||||
- MCP tools
|
||||
|
||||
## Fixtures
|
||||
|
||||
Available in `conftest.py`:
|
||||
|
||||
- `temp_dir`: Temporary directory for file operations
|
||||
- `sample_css`: Sample CSS custom properties
|
||||
- `sample_scss`: Sample SCSS variables
|
||||
- `sample_json_tokens`: Sample W3C JSON tokens
|
||||
- `sample_token_collection`: Pre-built token collection
|
||||
- `tailwind_config_path`: Temporary Tailwind config file
|
||||
|
||||
## Writing New Tests
|
||||
|
||||
### Unit Test Example
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from tools.ingest.css import CSSTokenSource
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_css_parsing(sample_css):
|
||||
"""Test CSS token extraction."""
|
||||
parser = CSSTokenSource()
|
||||
result = await parser.extract(sample_css)
|
||||
|
||||
assert len(result.tokens) > 0
|
||||
assert result.name
|
||||
```
|
||||
|
||||
### Integration Test Example
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.slow
|
||||
@pytest.mark.asyncio
|
||||
async def test_figma_integration():
|
||||
"""Test Figma API integration."""
|
||||
# Test code here
|
||||
pass
|
||||
```
|
||||
|
||||
## Continuous Integration
|
||||
|
||||
Tests run automatically on:
|
||||
- Pull requests
|
||||
- Commits to main branch
|
||||
- Nightly builds
|
||||
|
||||
### CI Configuration
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
- name: Run tests
|
||||
run: pytest --cov=tools --cov-report=xml
|
||||
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v3
|
||||
```
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
Target: 80% code coverage
|
||||
|
||||
Current coverage by module:
|
||||
- tools.ingest: ~85%
|
||||
- tools.analyze: ~70%
|
||||
- tools.storybook: ~65%
|
||||
- tools.figma: ~60% (requires API mocking)
|
||||
|
||||
## Mocking External Services
|
||||
|
||||
### Figma API
|
||||
|
||||
```python
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_with_mocked_figma():
|
||||
with patch('tools.figma.figma_tools.httpx.AsyncClient') as mock:
|
||||
mock.return_value.__aenter__.return_value.get = AsyncMock(
|
||||
return_value={"status": "ok"}
|
||||
)
|
||||
# Test code here
|
||||
```
|
||||
|
||||
### Database
|
||||
|
||||
```python
|
||||
@pytest.fixture
|
||||
def mock_db(temp_dir):
|
||||
"""Create temporary test database."""
|
||||
db_path = temp_dir / "test.db"
|
||||
# Initialize test DB
|
||||
return db_path
|
||||
```
|
||||
|
||||
## Test Data
|
||||
|
||||
Test fixtures use realistic but minimal data:
|
||||
- ~5-10 tokens per collection
|
||||
- Simple color and spacing values
|
||||
- W3C-compliant JSON format
|
||||
|
||||
## Debugging Failed Tests
|
||||
|
||||
```bash
|
||||
# Run with detailed output
|
||||
pytest -vv
|
||||
|
||||
# Run with pdb on failure
|
||||
pytest --pdb
|
||||
|
||||
# Run last failed tests only
|
||||
pytest --lf
|
||||
|
||||
# Show print statements
|
||||
pytest -s
|
||||
```
|
||||
|
||||
## Performance Testing
|
||||
|
||||
```bash
|
||||
# Run with duration report
|
||||
pytest --durations=10
|
||||
|
||||
# Profile slow tests
|
||||
python -m cProfile -o profile.stats -m pytest
|
||||
```
|
||||
|
||||
## Contributing Tests
|
||||
|
||||
1. Write tests for new features
|
||||
2. Maintain >80% coverage
|
||||
3. Use descriptive test names
|
||||
4. Add docstrings to test functions
|
||||
5. Use fixtures for common setup
|
||||
6. Mark slow tests with `@pytest.mark.slow`
|
||||
|
||||
## Known Issues
|
||||
|
||||
- Tailwind parser tests may fail due to regex limitations (non-blocking)
|
||||
- Figma tests require valid FIGMA_TOKEN environment variable
|
||||
- Some integration tests may be slow (~5s each)
|
||||
98
tests/conftest.py
Normal file
98
tests/conftest.py
Normal file
@@ -0,0 +1,98 @@
|
||||
"""
|
||||
Pytest configuration and shared fixtures.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from tools.ingest.base import DesignToken, TokenCollection, TokenType
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_dir():
|
||||
"""Create a temporary directory for tests."""
|
||||
temp_path = tempfile.mkdtemp()
|
||||
yield Path(temp_path)
|
||||
shutil.rmtree(temp_path, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_css():
|
||||
"""Sample CSS custom properties."""
|
||||
return """
|
||||
:root {
|
||||
--color-primary: #3B82F6;
|
||||
--color-secondary: #10B981;
|
||||
--spacing-sm: 8px;
|
||||
--spacing-md: 16px;
|
||||
--spacing-lg: 24px;
|
||||
--font-size-base: 16px;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_scss():
|
||||
"""Sample SCSS variables."""
|
||||
return """
|
||||
$primary-color: #3B82F6;
|
||||
$secondary-color: #10B981;
|
||||
$font-family-sans: 'Inter', sans-serif;
|
||||
$font-size-base: 16px;
|
||||
$spacing-md: 16px;
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_json_tokens():
|
||||
"""Sample JSON design tokens (W3C format)."""
|
||||
return {
|
||||
"color": {
|
||||
"primary": {
|
||||
"500": {"value": "#3B82F6", "type": "color"},
|
||||
"600": {"value": "#2563EB", "type": "color"}
|
||||
},
|
||||
"secondary": {
|
||||
"500": {"value": "#10B981", "type": "color"}
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"sm": {"value": "8px", "type": "dimension"},
|
||||
"md": {"value": "16px", "type": "dimension"},
|
||||
"lg": {"value": "24px", "type": "dimension"}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_token_collection():
|
||||
"""Create a sample token collection."""
|
||||
tokens = [
|
||||
DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR),
|
||||
DesignToken(name="color.secondary", value="#10B981", type=TokenType.COLOR),
|
||||
DesignToken(name="spacing.md", value="16px", type=TokenType.SPACING),
|
||||
]
|
||||
return TokenCollection(tokens=tokens, name="Sample Collection")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tailwind_config_path(temp_dir):
|
||||
"""Create a temporary Tailwind config file."""
|
||||
config_content = """
|
||||
module.exports = {
|
||||
theme: {
|
||||
colors: {
|
||||
blue: '#0000FF',
|
||||
red: '#FF0000'
|
||||
},
|
||||
spacing: {
|
||||
'1': '4px',
|
||||
'2': '8px'
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
config_file = temp_dir / "tailwind.config.js"
|
||||
config_file.write_text(config_content)
|
||||
return config_file
|
||||
65
tests/test_ingestion.py
Normal file
65
tests/test_ingestion.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Tests for token ingestion from various sources.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import json
|
||||
from tools.ingest.css import CSSTokenSource
|
||||
from tools.ingest.scss import SCSSTokenSource
|
||||
from tools.ingest.json_tokens import JSONTokenSource
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_css_ingestion(sample_css):
|
||||
"""Test CSS custom property extraction."""
|
||||
parser = CSSTokenSource()
|
||||
result = await parser.extract(sample_css)
|
||||
|
||||
assert len(result.tokens) >= 5
|
||||
assert any(t.name == "color.primary" for t in result.tokens)
|
||||
assert any(t.value == "#3B82F6" for t in result.tokens)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scss_ingestion(sample_scss):
|
||||
"""Test SCSS variable extraction."""
|
||||
parser = SCSSTokenSource()
|
||||
result = await parser.extract(sample_scss)
|
||||
|
||||
assert len(result.tokens) >= 4
|
||||
assert any(t.name == "primary-color" for t in result.tokens)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_json_ingestion(sample_json_tokens):
|
||||
"""Test JSON token extraction (W3C format)."""
|
||||
parser = JSONTokenSource()
|
||||
result = await parser.extract(json.dumps(sample_json_tokens))
|
||||
|
||||
assert len(result.tokens) >= 6
|
||||
# Check color tokens
|
||||
primary_tokens = [t for t in result.tokens if "primary" in t.name]
|
||||
assert len(primary_tokens) >= 2
|
||||
|
||||
# Check spacing tokens
|
||||
spacing_tokens = [t for t in result.tokens if "spacing" in t.name]
|
||||
assert len(spacing_tokens) == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_css():
|
||||
"""Test handling of empty CSS."""
|
||||
parser = CSSTokenSource()
|
||||
result = await parser.extract("")
|
||||
|
||||
assert len(result.tokens) == 0
|
||||
assert result.name
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_invalid_json():
|
||||
"""Test handling of invalid JSON."""
|
||||
parser = JSONTokenSource()
|
||||
|
||||
with pytest.raises(json.JSONDecodeError):
|
||||
await parser.extract("invalid json{")
|
||||
109
tests/test_merge.py
Normal file
109
tests/test_merge.py
Normal file
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Tests for token merging and conflict resolution.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from tools.ingest.merge import TokenMerger, MergeStrategy
|
||||
from tools.ingest.base import TokenCollection, DesignToken, TokenType
|
||||
|
||||
|
||||
def test_merge_no_conflicts():
|
||||
"""Test merging collections with no conflicts."""
|
||||
col1 = TokenCollection([
|
||||
DesignToken(name="color.red", value="#FF0000", type=TokenType.COLOR)
|
||||
])
|
||||
col2 = TokenCollection([
|
||||
DesignToken(name="color.blue", value="#0000FF", type=TokenType.COLOR)
|
||||
])
|
||||
|
||||
merger = TokenMerger(strategy=MergeStrategy.LAST)
|
||||
result = merger.merge([col1, col2])
|
||||
|
||||
assert len(result.collection.tokens) == 2
|
||||
assert len(result.conflicts) == 0
|
||||
|
||||
|
||||
def test_merge_strategy_first():
|
||||
"""Test FIRST merge strategy."""
|
||||
col1 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
|
||||
])
|
||||
col2 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma")
|
||||
])
|
||||
|
||||
merger = TokenMerger(strategy=MergeStrategy.FIRST)
|
||||
result = merger.merge([col1, col2])
|
||||
|
||||
assert len(result.collection.tokens) == 1
|
||||
assert len(result.conflicts) == 1
|
||||
# Should keep first value
|
||||
token = result.collection.tokens[0]
|
||||
assert token.value == "#FF0000"
|
||||
assert token.source == "css"
|
||||
|
||||
|
||||
def test_merge_strategy_last():
|
||||
"""Test LAST merge strategy."""
|
||||
col1 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
|
||||
])
|
||||
col2 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="figma")
|
||||
])
|
||||
|
||||
merger = TokenMerger(strategy=MergeStrategy.LAST)
|
||||
result = merger.merge([col1, col2])
|
||||
|
||||
assert len(result.collection.tokens) == 1
|
||||
assert len(result.conflicts) == 1
|
||||
# Should keep last value
|
||||
token = result.collection.tokens[0]
|
||||
assert token.value == "#0000FF"
|
||||
assert token.source == "figma"
|
||||
|
||||
|
||||
def test_merge_strategy_prefer_figma():
|
||||
"""Test PREFER_FIGMA merge strategy."""
|
||||
col1 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#FF0000", type=TokenType.COLOR, source="css")
|
||||
])
|
||||
col2 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#3B82F6", type=TokenType.COLOR, source="figma")
|
||||
])
|
||||
col3 = TokenCollection([
|
||||
DesignToken(name="color.primary", value="#0000FF", type=TokenType.COLOR, source="scss")
|
||||
])
|
||||
|
||||
merger = TokenMerger(strategy=MergeStrategy.PREFER_FIGMA)
|
||||
result = merger.merge([col1, col2, col3])
|
||||
|
||||
# Should prefer Figma value
|
||||
token = result.collection.tokens[0]
|
||||
assert token.value == "#3B82F6"
|
||||
assert token.source == "figma"
|
||||
|
||||
|
||||
def test_merge_multiple_collections():
|
||||
"""Test merging many collections."""
|
||||
collections = [
|
||||
TokenCollection([
|
||||
DesignToken(name=f"color.{i}", value=f"#{i:06X}", type=TokenType.COLOR)
|
||||
])
|
||||
for i in range(10)
|
||||
]
|
||||
|
||||
merger = TokenMerger(strategy=MergeStrategy.LAST)
|
||||
result = merger.merge(collections)
|
||||
|
||||
assert len(result.collection.tokens) == 10
|
||||
assert len(result.conflicts) == 0
|
||||
|
||||
|
||||
def test_merge_empty_collections():
|
||||
"""Test merging empty collections."""
|
||||
merger = TokenMerger(strategy=MergeStrategy.LAST)
|
||||
result = merger.merge([])
|
||||
|
||||
assert len(result.collection.tokens) == 0
|
||||
assert len(result.conflicts) == 0
|
||||
Reference in New Issue
Block a user