Files
awoooi/packages/lewooogo-data/tests/test_pg_memory.py
OG T 7478dc0254 feat(phase6-9): Complete modular architecture and Agent Teams
Phase 6.4 - Modular Architecture:
- Add lewooogo-brain adapters for LLM providers
- Add lewooogo-data dual memory (Redis + PostgreSQL)
- Implement consensus engine for multi-agent decisions
- Add incident memory service for historical context

Phase 9 - Agent Teams (Claude Agent SDK):
- Add base agent class with Claude Sonnet 4 integration
- Implement action planner, blast radius, and security agents
- Add agent API endpoints and proposal workflow
- Integrate ADR-009 OpenClaw Agent Teams architecture

DevOps & CI/CD:
- Add GitHub Actions CI/CD workflows (ci.yaml, cd.yaml)
- Add pre-commit hooks and secrets baseline
- Add docker-compose for local development
- Update Kubernetes network policies

Frontend Improvements:
- Add auto-healing error boundary component
- Update i18n messages for agent features
- Enhance dual-state incident card with execution feedback

Documentation:
- Add 7 ADRs covering MCP, design system, architecture decisions
- Update ARCHITECTURE_MEMORY.md with modular design
- Add GLOBAL_RULES.md and SOUL.md for project identity

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-23 18:40:36 +08:00

272 lines
8.4 KiB
Python

"""
PgMemoryProvider 單元測試
===========================
測試案例:
- test_save_and_load: 儲存並讀取
- test_upsert: 更新已存在的資料
- test_delete: 刪除
- test_exists: 存在檢查
- test_sqlite_forbidden: SQLite 禁止測試
"""
import pytest
from unittest.mock import patch, AsyncMock, MagicMock
from pydantic import BaseModel
from lewooogo_data.providers.pg_memory import (
PgMemoryProvider,
get_database_url,
DEFAULT_PG_URL,
)
class SampleModel(BaseModel):
"""測試用的 Pydantic Model"""
id: str
name: str
value: int = 0
# =============================================================================
# 資料庫 URL 測試
# =============================================================================
class TestDatabaseUrl:
"""資料庫 URL 相關測試"""
def test_default_url(self):
"""測試預設 URL"""
with patch.dict("os.environ", {}, clear=True):
url = get_database_url()
assert url == DEFAULT_PG_URL
def test_env_url(self):
"""測試環境變數 URL"""
custom_url = "postgresql+asyncpg://user:pass@localhost:5432/mydb"
with patch.dict("os.environ", {"DATABASE_URL": custom_url}):
url = get_database_url()
assert url == custom_url
def test_sqlite_forbidden(self):
"""測試 SQLite 被禁止"""
sqlite_url = "sqlite:///test.db"
with patch.dict("os.environ", {"DATABASE_URL": sqlite_url}):
url = get_database_url()
# 統帥鐵律: SQLite 被禁止,應返回預設 PostgreSQL
assert url == DEFAULT_PG_URL
assert "sqlite" not in url.lower()
# =============================================================================
# PgMemoryProvider 基本功能測試
# =============================================================================
class TestPgMemoryProviderBasic:
"""基本功能測試"""
@pytest.mark.asyncio
async def test_load(self, patch_pg_session, mock_session):
"""測試載入資料"""
# Arrange
provider = PgMemoryProvider(model_class=SampleModel)
# 預先放入資料
expected_data = SampleModel(id="pg-1", name="PG Test", value=100)
mock_session._storage["pg-1"] = expected_data
# Act
result = await provider.load("pg-1")
# Assert
assert result is not None
assert result.id == "pg-1"
assert result.name == "PG Test"
assert result.value == 100
@pytest.mark.asyncio
async def test_load_nonexistent(self, patch_pg_session, mock_session):
"""測試載入不存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# Act
result = await provider.load("nonexistent")
# Assert
assert result is None
@pytest.mark.asyncio
async def test_save(self, patch_pg_session, mock_session):
"""測試儲存資料"""
provider = PgMemoryProvider(model_class=SampleModel)
data = SampleModel(id="pg-2", name="Save Test", value=200)
# Act
result = await provider.save("pg-2", data)
# Assert
assert result is True
mock_session.add.assert_called_once()
mock_session.commit.assert_called_once()
@pytest.mark.asyncio
async def test_save_ttl_ignored(self, patch_pg_session, mock_session):
"""測試儲存時 TTL 被忽略 (PostgreSQL 永久保存)"""
provider = PgMemoryProvider(model_class=SampleModel)
data = SampleModel(id="pg-3", name="TTL Ignored", value=300)
# Act - 傳入 TTL 應被忽略
result = await provider.save("pg-3", data, ttl_seconds=3600)
# Assert
assert result is True
mock_session.add.assert_called_once()
# =============================================================================
# 刪除測試
# =============================================================================
class TestPgMemoryProviderDelete:
"""刪除相關測試"""
@pytest.mark.asyncio
async def test_delete_existing(self, patch_pg_session, mock_session):
"""測試刪除存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# 預先放入資料
existing_data = SampleModel(id="del-1", name="To Delete", value=0)
mock_session._storage["del-1"] = existing_data
# Act
result = await provider.delete("del-1")
# Assert
assert result is True
mock_session.delete.assert_called_once()
mock_session.commit.assert_called()
@pytest.mark.asyncio
async def test_delete_nonexistent(self, patch_pg_session, mock_session):
"""測試刪除不存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# Act
result = await provider.delete("nonexistent")
# Assert
assert result is False
# =============================================================================
# 存在檢查測試
# =============================================================================
class TestPgMemoryProviderExists:
"""存在檢查測試"""
@pytest.mark.asyncio
async def test_exists_true(self, patch_pg_session, mock_session):
"""測試存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# 預先放入資料
existing_data = SampleModel(id="exist-1", name="Exists", value=1)
mock_session._storage["exist-1"] = existing_data
# Act
result = await provider.exists("exist-1")
# Assert
assert result is True
@pytest.mark.asyncio
async def test_exists_false(self, patch_pg_session, mock_session):
"""測試不存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# Act
result = await provider.exists("nonexistent")
# Assert
assert result is False
# =============================================================================
# 更新測試
# =============================================================================
class TestPgMemoryProviderUpdate:
"""更新相關測試"""
@pytest.mark.asyncio
async def test_update_existing(self, patch_pg_session, mock_session):
"""測試更新存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# 預先放入資料 (使用可變物件)
class MutableModel:
def __init__(self):
self.id = "upd-1"
self.name = "Original"
self.value = 10
existing = MutableModel()
mock_session._storage["upd-1"] = existing
# Act
result = await provider.update("upd-1", {"value": 99, "name": "Updated"})
# Assert
assert result is True
assert existing.value == 99
assert existing.name == "Updated"
mock_session.commit.assert_called()
@pytest.mark.asyncio
async def test_update_nonexistent(self, patch_pg_session, mock_session):
"""測試更新不存在的資料"""
provider = PgMemoryProvider(model_class=SampleModel)
# Act
result = await provider.update("nonexistent", {"value": 999})
# Assert
assert result is False
# =============================================================================
# 錯誤處理測試
# =============================================================================
class TestPgMemoryProviderErrors:
"""錯誤處理測試"""
@pytest.mark.asyncio
async def test_load_without_model_class(self, patch_pg_session):
"""測試無 model_class 時 load 拋異常"""
provider = PgMemoryProvider(model_class=None)
# Act & Assert
with pytest.raises(ValueError, match="model_class is required"):
await provider.load("any-key")
@pytest.mark.asyncio
async def test_delete_without_model_class(self, patch_pg_session):
"""測試無 model_class 時 delete 拋異常"""
provider = PgMemoryProvider(model_class=None)
# Act & Assert
with pytest.raises(ValueError, match="model_class is required"):
await provider.delete("any-key")
@pytest.mark.asyncio
async def test_update_without_model_class(self, patch_pg_session):
"""測試無 model_class 時 update 拋異常"""
provider = PgMemoryProvider(model_class=None)
# Act & Assert
with pytest.raises(ValueError, match="model_class is required"):
await provider.update("any-key", {"value": 1})