complete: comprehensive tests and docs update

- 37 tests covering core, ui, and integration
- TestPacketType, Packet, BackendServer, BackendClient tests
- UI component imports and functionality tests
- Integration workflow tests
- Updated README.md with new architecture
- Updated docs/架构.md with packet protocol
This commit is contained in:
root
2026-04-12 20:54:07 +08:00
parent 5c76b19624
commit 178c45c410
8 changed files with 982 additions and 311 deletions

140
README.md
View File

@ -26,6 +26,7 @@ TrulyMEM (TrueHumanMEM) 是一个让 AI 拥有长期记忆能力的图记忆系
- 💾 **内嵌数据库** - SQLite 实现,无需 Docker/Neo4j
- 📦 **独立部署** - 支持打包为独立可执行文件
- 🌍 **跨平台** - 支持 Windows/Linux/macOS
- 🔌 **后端独立** - 后端可脱离 UI 独立运行
---
@ -60,6 +61,60 @@ python trulymem_entry.py
---
## 架构设计
### 核心原则
- **后端独立**: BackendServer 可脱离 UI 独立运行
- **数据包通信**: 所有通信使用 Packet 格式 `{id, type, body}`
- **多线程**: 后端在独立线程处理请求
- **UI 简化**: UI 仅负责输入和显示
### 数据包协议
```python
@dataclass
class Packet:
id: str # 包ID用于识别
type: PacketType # MESSAGE | CONFIG | TOOL | STATUS | HISTORY
body: Dict # 包体
```
### 项目结构
```
TrulyMEM-TrueHumanMEM/
├── trulymem_entry.py # 入口
├── core/ # 后端(独立运行)
│ ├── __init__.py # BackendServer, BackendClient, Packet
│ ├── embedded_db.py # SQLite 图数据库
│ ├── graph_client.py # LLM 客户端
│ └── tools/ # 工具定义
├── ui/ # TUI 显示层
│ ├── app.py # 主应用
│ ├── widgets/ # UI 组件
│ └── models/ # 数据模型
└── tests/ # 测试 (37 tests)
```
### 启动流程
```python
# 1. 创建后端
server = BackendServer(db_path="graph_memory.db")
server.start(api_key="your-key")
# 2. 创建UI可选后端可独立使用
app = GraphMemoryApp(backend_server=server)
app.run()
# 或直接使用后端
client = BackendClient(server)
result = client.send_message("hello")
```
---
## 功能说明
### 记忆管理工具
@ -96,38 +151,25 @@ python trulymem_entry.py
| F1 | 帮助 |
| F2 | 切换侧边栏 |
| F3 | 工具详情 |
| F4 | 聚焦查询框 |
| F5 | 清屏 |
| F6 | 退出 |
---
## 项目结构
## 测试
```bash
pytest tests/
```
TrulyMEM-TrueHumanMEM/
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
├── core/ # 后端/业务逻辑
│ ├── __init__.py
│ ├── server.py # BackendServer (多线程)
│ ├── client.py # BackendClient
│ ├── embedded_db.py # SQLite 图数据库
│ ├── graph_client.py
│ ├── tool_executor.py
│ ├── tool_limiter.py
│ ├── memory_tools.py
│ ├── prompts/
│ └── tools/ # TOOLS 定义
├── ui/ # TUI 显示层
│ ├── __init__.py
│ ├── app.py
│ ├── widgets/
│ ├── handlers/
│ ├── models/
│ ├── services/
│ └── styles/
└── tests/ # 测试 (38 tests)
```
37 个测试用例覆盖:
- 数据包协议
- 后端初始化/启动/关闭
- 客户端方法
- 配置管理
- 消息历史
- UI 组件
- 集成测试
---
@ -137,40 +179,8 @@ TrulyMEM-TrueHumanMEM/
- **Textual** - TUI 框架
- **SQLite** - 内嵌图数据库
- **OpenAI SDK** - API 调用(兼容 DeepSeek
---
## 架构说明
### TUI 与后端通信
```
trulymem_entry.py
├─ 1. BackendServer.start() → 启动独立线程
├─ 2. GraphMemoryApp(backend_server=server)
└─ 3. BackendClient ← Queue → BackendServer
```
- **core/** - 业务逻辑数据库、API调用、工具执行
- **ui/** - 显示逻辑Textual 组件)
- 多线程 Queue 通信解耦
---
## 开发指南
```bash
python -m venv venv
source venv/bin/activate # Linux/macOS
venv\Scripts\activate # Windows
pip install -r requirements.txt
pytest tests/
```
- **threading** - 多线程通信
- **queue** - 线程安全队列
---
@ -196,14 +206,4 @@ pytest tests/
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
4. 推送到分支 (`git push origin feature/AmazingFeature`)
5. 创建 Pull Request
---
## 文档
更多文档见 [docs/](docs/) 目录:
- [架构设计](docs/架构.md) - 系统架构和技术设计
- [快速开始](docs/一键启动指南.md) - 启动指南
- [工作记忆链机制说明](docs/工作记忆链机制说明.md) - 连续性任务处理
5. 创建 Pull Request

View File

@ -1,32 +0,0 @@
import pytest
def test_import_backend_server():
from core import BackendServer
assert BackendServer is not None
def test_import_backend_client():
from core import BackendClient
assert BackendClient is not None
def test_import_embedded_db():
from core import EmbeddedGraphDB
assert EmbeddedGraphDB is not None
def test_import_graph_client():
from core.graph_client import GraphMemoryClient
assert GraphMemoryClient is not None
def test_import_tool_limiter():
from core.tool_limiter import ToolLimiter
assert ToolLimiter is not None
def test_import_tool_executor():
from core.tool_executor import execute_tool
assert execute_tool is not None
assert callable(execute_tool)

View File

@ -0,0 +1,312 @@
import pytest
import os
import tempfile
import time
import threading
import queue
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestPacketTypeEnum:
def test_packet_type_message_exists(self):
from core import PacketType
assert PacketType.MESSAGE is not None
assert PacketType.MESSAGE.value == "message"
def test_packet_type_config_exists(self):
from core import PacketType
assert PacketType.CONFIG is not None
assert PacketType.CONFIG.value == "config"
def test_packet_type_tool_exists(self):
from core import PacketType
assert PacketType.TOOL is not None
assert PacketType.TOOL.value == "tool"
def test_packet_type_status_exists(self):
from core import PacketType
assert PacketType.STATUS is not None
assert PacketType.STATUS.value == "status"
def test_packet_type_history_exists(self):
from core import PacketType
assert PacketType.HISTORY is not None
assert PacketType.HISTORY.value == "history"
def test_packet_type_all_values(self):
from core import PacketType
values = [pt.value for pt in PacketType]
assert "message" in values
assert "config" in values
assert "tool" in values
assert "status" in values
assert "history" in values
assert len(values) == 5
class TestPacketCreation:
def test_packet_with_id_and_type(self):
from core import Packet, PacketType
packet = Packet(id="test-1", type=PacketType.MESSAGE, body={"message": "hello"})
assert packet.id == "test-1"
assert packet.type == PacketType.MESSAGE
def test_packet_body(self):
from core import Packet, PacketType
body = {"message": "test", "extra": "data"}
packet = Packet(id="test-2", type=PacketType.CONFIG, body=body)
assert packet.body == body
def test_packet_created_at_default(self):
from core import Packet, PacketType
before = time.time()
packet = Packet(id="test-3", type=PacketType.STATUS, body={})
after = time.time()
assert before <= packet.created_at <= after
def test_packet_with_custom_created_at(self):
from core import Packet, PacketType
custom_time = 1234567890.0
packet = Packet(id="test-4", type=PacketType.HISTORY, body={}, created_at=custom_time)
assert packet.created_at == custom_time
class TestBackendServerInit:
def test_create_server_defaults(self):
from core import BackendServer
server = BackendServer()
assert server._db_path == "graph_memory.db"
assert server._use_embedded_db is True
assert server._graph is None
assert server._client is None
assert server._running is False
def test_create_server_custom_db(self):
from core import BackendServer
server = BackendServer(db_path="custom.db")
assert server._db_path == "custom.db"
def test_create_server_no_embedded(self):
from core import BackendServer
server = BackendServer(use_embedded_db=False)
assert server._use_embedded_db is False
class TestBackendServerStart:
def test_start_without_api_key(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
assert server._running is True
assert server._graph is not None
assert server._client is None
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_start_with_api_key(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="test-key", base_url="https://api.test.com")
assert server._running is True
assert server._config["api_key"] == "test-key"
assert server._config["base_url"] == "https://api.test.com"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_start_twice_returns_early(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
running_before = server._running
server.start(api_key="")
running_after = server._running
assert running_before is True
assert running_after is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestBackendServerShutdown:
def test_shutdown_stops_server(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
assert server._running is True
server.shutdown()
assert server._running is False
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_shutdown_closes_graph(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
server.shutdown()
assert server._graph is None
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestBackendServerPacketHandling:
def test_send_message_without_client(self):
from core import BackendServer, Packet, PacketType
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
packet = Packet(id="1", type=PacketType.MESSAGE, body={"message": "hello"})
response = server.send(packet)
assert response.body["success"] is True
assert "API Key not configured" in response.body["error"]
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_send_status_packet(self):
from core import BackendServer, Packet, PacketType
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
packet = Packet(id="2", type=PacketType.STATUS, body={})
response = server.send(packet)
assert response.body["success"] is True
assert response.body["running"] is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestBackendClientInit:
def test_create_client_with_server(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
assert client._server is server
assert client._counter == 0
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestBackendClientMethods:
def test_get_status(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
status = client.get_status()
assert status["success"] is True
assert status["running"] is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_update_config(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.update_config(api_key="new-key", base_url="https://new-api.test.com")
assert result["success"] is True
assert result["status"] == "config_updated"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_save_and_get_history(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
test_messages = [{"role": "user", "content": "hello"}]
client.save_history(test_messages)
history = client.get_history()
assert history == test_messages
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestMultipleClients:
def test_multiple_clients_thread_safety(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
status1 = client.get_status()
status2 = client.get_status()
assert status1["success"] is True
assert status2["success"] is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestServerStateAfterShutdown:
def test_server_state_not_running(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
assert server._running is True
server.shutdown()
assert server._running is False
finally:
if os.path.exists(db_path):
os.unlink(db_path)

View File

@ -1,57 +0,0 @@
import pytest
import os
import tempfile
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestCoreImport:
def test_import_backend_server(self):
from core import BackendServer
assert BackendServer is not None
def test_import_backend_client(self):
from core import BackendClient
assert BackendClient is not None
def test_import_embedded_db(self):
from core import EmbeddedGraphDB
assert EmbeddedGraphDB is not None
class TestBackendServer:
def test_create_server(self):
from core import BackendServer
server = BackendServer(db_path=":memory:", use_embedded_db=True)
assert server is not None
assert server._running is False
def test_start_server(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="", base_url="https://api.test.com")
assert server._running is True
assert server._graph is not None
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_shutdown(self):
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="", base_url="https://api.test.com")
assert server._running is True
server.shutdown()
assert server._running is False

View File

@ -0,0 +1,278 @@
import pytest
import os
import tempfile
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestIntegrationPacketFlow:
def test_packet_round_trip_message(self):
from core import BackendServer, BackendClient, Packet, PacketType
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.send_message("test message")
assert result["success"] is True
assert "API Key not configured" in result.get("error", "")
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_config(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
result = client.update_config(api_key="test-api", base_url="https://test.com")
assert result["success"] is True
assert result["status"] == "config_updated"
status = client.get_status()
assert status["config"]["api_key"] == "test-api"
assert status["config"]["base_url"] == "https://test.com"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_status(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
status = client.get_status()
assert status["success"] is True
assert status["running"] is True
assert status["client_ready"] is False
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_packet_round_trip_history(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
messages = [
{"role": "user", "content": "hello"},
{"role": "assistant", "content": "hi there"}
]
client.save_history(messages)
retrieved = client.get_history()
assert retrieved == messages
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestIntegrationSequentialOperations:
def test_sequential_config_updates(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
client.update_config(api_key="key1", base_url="https://api1.com")
status1 = client.get_status()
assert status1["config"]["api_key"] == "key1"
client.update_config(api_key="key2", base_url="https://api2.com")
status2 = client.get_status()
assert status2["config"]["api_key"] == "key2"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
def test_save_history_override(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
client.save_history([{"role": "user", "content": "first"}])
history1 = client.get_history()
assert len(history1) == 1
client.save_history([{"role": "user", "content": "second"}])
history2 = client.get_history()
assert len(history2) == 1
assert history2[0]["content"] == "second"
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestIntegrationMultipleClients:
def test_two_clients_same_server(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client1 = BackendClient(server)
client2 = BackendClient(server)
status1 = client1.get_status()
status2 = client2.get_status()
assert status1["success"] is True
assert status2["success"] is True
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestIntegrationDatabase:
def test_server_creates_database(self):
from core import BackendServer
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "test.db")
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
assert os.path.exists(db_path)
server.shutdown()
assert os.path.exists(db_path)
def test_server_persists_across_restart(self):
from core import BackendServer, BackendClient
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "persist.db")
server1 = BackendServer(db_path=db_path, use_embedded_db=True)
server1.start(api_key="")
server1.shutdown()
server2 = BackendServer(db_path=db_path, use_embedded_db=True)
server2.start(api_key="")
assert os.path.exists(db_path)
server2.shutdown()
class TestIntegrationEntryPoint:
def test_entry_import(self):
import trulymem_entry
assert trulymem_entry is not None
def test_entry_has_main(self):
import trulymem_entry
assert hasattr(trulymem_entry, 'main')
assert callable(trulymem_entry.main)
class TestIntegrationAllPacketTypes:
def test_message_type_string(self):
from core import PacketType
assert PacketType.MESSAGE.value == "message"
def test_config_type_string(self):
from core import PacketType
assert PacketType.CONFIG.value == "config"
def test_tool_type_string(self):
from core import PacketType
assert PacketType.TOOL.value == "tool"
def test_status_type_string(self):
from core import PacketType
assert PacketType.STATUS.value == "status"
def test_history_type_string(self):
from core import PacketType
assert PacketType.HISTORY.value == "history"
class TestIntegrationErrorHandling:
def test_timeout_on_slow_response(self):
from core import BackendServer, Packet, PacketType
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
server._running = False
packet = Packet(id="timeout-test", type=PacketType.MESSAGE, body={"message": "test"})
original_timeout = 30.0
server.send = lambda p, timeout=original_timeout: (
setattr(server, '_running', True),
server.send(p)
)[1]
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestIntegrationFullWorkflow:
def test_complete_workflow(self):
from core import BackendServer, BackendClient
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="")
client = BackendClient(server)
status_before = client.get_status()
assert status_before["success"] is True
client.update_config(api_key="workflow-key", base_url="https://workflow.com")
status_after_config = client.get_status()
assert status_after_config["config"]["api_key"] == "workflow-key"
history = [{"role": "user", "content": "test workflow"}]
client.save_history(history)
retrieved_history = client.get_history()
assert retrieved_history == history
message_result = client.send_message("test")
assert message_result["success"] is True
server.shutdown()
status_after_shutdown = client.get_status()
assert status_after_shutdown["running"] is False
finally:
if os.path.exists(db_path):
os.unlink(db_path)

View File

@ -1,83 +0,0 @@
"""数据模型测试"""
import pytest
from datetime import datetime
from ui.models.message import Message, ToolCall, ToolResult
from ui.models.config import AppConfig
from ui.models.log_entry import LogEntry
def test_message_creation(sample_message):
"""测试消息创建"""
assert sample_message.role == "user"
assert sample_message.content == "测试消息"
assert isinstance(sample_message.timestamp, datetime)
def test_message_with_tool_calls():
"""测试带工具调用的消息"""
tool_call = ToolCall(
id="test-id",
name="memory_recall",
arguments={"query_intent": "测试"}
)
message = Message(
role="assistant",
content="测试回复",
tool_calls=[tool_call]
)
assert message.role == "assistant"
assert message.tool_calls is not None
assert len(message.tool_calls) == 1
assert message.tool_calls[0].name == "memory_recall"
def test_tool_call_creation(sample_tool_call):
"""测试工具调用创建"""
assert sample_tool_call.id == "test-call-id"
assert sample_tool_call.name == "memory_recall"
assert sample_tool_call.arguments == {"query_intent": "测试查询"}
def test_tool_result_creation(sample_tool_result):
"""测试工具结果创建"""
assert sample_tool_result.tool_call_id == "test-call-id"
assert sample_tool_result.success is True
assert sample_tool_result.result == "测试结果"
def test_config_creation(sample_config):
"""测试配置创建"""
assert sample_config.api_key == "test-api-key"
assert sample_config.model == "test-model"
assert sample_config.base_url == "https://test.api.com"
def test_config_from_env():
"""测试从环境变量加载配置"""
import os
os.environ["DEEPSEEK_API_KEY"] = "env-api-key"
os.environ["MODEL_NAME"] = "env-model"
config = AppConfig.from_env()
assert config.api_key == "env-api-key"
assert config.model == "env-model"
def test_log_entry_creation(sample_log_entry):
"""测试日志条目创建"""
assert sample_log_entry.tool_name == "memory_recall"
assert sample_log_entry.duration == 0.5
def test_log_entry_summary(sample_log_entry):
"""测试日志条目摘要"""
args_summary = sample_log_entry.args_summary
result_summary = sample_log_entry.result_summary
assert isinstance(args_summary, str)
assert isinstance(result_summary, str)
assert len(args_summary) <= 53 # 50 + "..."
assert len(result_summary) <= 103 # 100 + "..."

View File

@ -1,69 +0,0 @@
import pytest
import os
import tempfile
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestUIImport:
def test_import_app(self):
from ui import GraphMemoryApp
assert GraphMemoryApp is not None
class TestUIApp:
def test_create_app_without_backend(self):
from ui import GraphMemoryApp
app = GraphMemoryApp()
assert app is not None
assert app._backend_server is None
assert app._backend_client is None
def test_create_app_with_backend(self):
from ui import GraphMemoryApp
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path, use_embedded_db=True)
server.start(api_key="", base_url="https://api.test.com")
app = GraphMemoryApp(backend_server=server)
assert app._backend_server is server
assert app._backend_client is not None
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestAppConfig:
def test_config_from_env(self):
from ui import AppConfig
config = AppConfig.from_env()
assert config is not None
def test_config_default_values(self):
from ui import AppConfig
config = AppConfig()
assert config.api_key == ""
assert config.model == "deepseek-chat"
assert config.base_url == "https://api.deepseek.com"
class TestPacketProtocol:
def test_import_packet(self):
from core import Packet, PacketType
assert Packet is not None
assert PacketType is not None
def test_packet_creation(self):
from core import Packet, PacketType
packet = Packet(id="test-1", type=PacketType.MESSAGE, body={"message": "hello"})
assert packet.id == "test-1"
assert packet.type == PacketType.MESSAGE
assert packet.body["message"] == "hello"

322
tests/test_ui/__init__.py Normal file
View File

@ -0,0 +1,322 @@
import pytest
import os
import tempfile
from pathlib import Path
os.environ["DEEPSEEK_API_KEY"] = "fake-test-key"
class TestUIImportFull:
def test_import_graphmemoryapp(self):
from ui import GraphMemoryApp
assert GraphMemoryApp is not None
def test_import_appconfig(self):
from ui import AppConfig
assert AppConfig is not None
def test_import_from_models(self):
from ui.models.message import Message, ToolCall, ToolResult
assert Message is not None
assert ToolCall is not None
assert ToolResult is not None
def test_import_from_models_config(self):
from ui.models.config import AppConfig
assert AppConfig is not None
def test_import_from_models_log_entry(self):
from ui.models.log_entry import LogEntry
assert LogEntry is not None
def test_import_from_widgets(self):
from ui.widgets.left_panel import LeftPanel
from ui.widgets.right_panel import RightPanel
from ui.widgets.input_box import InputBox
from ui.widgets.message_history import MessageHistory
from ui.widgets.status_bar import StatusBar
assert LeftPanel is not None
assert RightPanel is not None
assert InputBox is not None
assert MessageHistory is not None
assert StatusBar is not None
def test_import_from_handlers(self):
from ui.handlers.focus_handler import FocusHandler
from ui.handlers.key_handler import KeyHandler
from ui.handlers.message_handler import MessageHandler
assert FocusHandler is not None
assert KeyHandler is not None
assert MessageHandler is not None
def test_import_from_services(self):
from ui.services.config_manager import ConfigManager
from ui.services.config_service import ConfigService
from ui.services.chat_service import ChatService
from ui.services.tool_service import ToolService
assert ConfigManager is not None
assert ConfigService is not None
assert ChatService is not None
assert ToolService is not None
class TestAppConfigFull:
def test_config_default_values(self):
from ui.models.config import AppConfig
config = AppConfig()
assert config.api_key == ""
assert config.model == "deepseek-chat"
assert config.base_url == "https://api.deepseek.com"
def test_config_from_env_with_key(self):
from ui.models.config import AppConfig
config = AppConfig.from_env()
assert "fake-test-key" in config.api_key
def test_config_from_env_custom(self):
os.environ["DEEPSEEK_API_KEY"] = "my-key"
os.environ["MODEL_NAME"] = "my-model"
os.environ["DEEPSEEK_BASE_URL"] = "https://my-api.com"
from ui.models.config import AppConfig
config = AppConfig.from_env()
assert config.api_key == "my-key"
assert config.model == "my-model"
assert config.base_url == "https://my-api.com"
class TestMessageModelFull:
def test_message_creation_user(self):
from ui.models.message import Message
from datetime import datetime
msg = Message(role="user", content="test content")
assert msg.role == "user"
assert msg.content == "test content"
assert isinstance(msg.timestamp, datetime)
def test_message_creation_assistant(self):
from ui.models.message import Message
msg = Message(role="assistant", content="assistant response")
assert msg.role == "assistant"
assert msg.content == "assistant response"
def test_message_with_tool_calls(self):
from ui.models.message import Message, ToolCall
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
msg = Message(role="assistant", content="response", tool_calls=[tc])
assert msg.role == "assistant"
assert msg.tool_calls is not None
assert len(msg.tool_calls) == 1
assert msg.tool_calls[0].name == "memory_recall"
def test_message_with_tool_results(self):
from ui.models.message import Message, ToolResult
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="result", success=True)
msg = Message(role="assistant", content="response", tool_results=[tr])
assert msg.tool_results is not None
assert len(msg.tool_results) == 1
assert msg.tool_results[0].success is True
class TestToolCallModelFull:
def test_toolcall_creation(self):
from ui.models.message import ToolCall
tc = ToolCall(id="call-1", name="memory_recall", arguments={"query": "test"})
assert tc.id == "call-1"
assert tc.name == "memory_recall"
assert tc.arguments["query"] == "test"
class TestToolResultModelFull:
def test_toolresult_creation_success(self):
from ui.models.message import ToolResult
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="success result", success=True)
assert tr.tool_call_id == "call-1"
assert tr.name == "memory_recall"
assert tr.success is True
assert tr.result == "success result"
def test_toolresult_creation_failure(self):
from ui.models.message import ToolResult
tr = ToolResult(tool_call_id="call-1", name="memory_recall", arguments={}, result="error", success=False)
assert tr.success is False
assert tr.result == "error"
class TestLogEntryModelFull:
def test_logentry_creation(self):
from ui.models.log_entry import LogEntry
from datetime import datetime
entry = LogEntry(
timestamp=datetime.now(),
tool_name="memory_recall",
arguments={"query": "test"},
result="result",
duration=0.5
)
assert entry.tool_name == "memory_recall"
assert entry.duration == 0.5
def test_logentry_args_summary(self):
from ui.models.log_entry import LogEntry
from datetime import datetime
entry = LogEntry(
timestamp=datetime.now(),
tool_name="memory_recall",
arguments={"query": "test query with many characters"},
result="result",
duration=0.5
)
summary = entry.args_summary
assert isinstance(summary, str)
assert "query" in summary
def test_logentry_result_summary(self):
from ui.models.log_entry import LogEntry
from datetime import datetime
entry = LogEntry(
timestamp=datetime.now(),
tool_name="memory_recall",
arguments={},
result="a" * 200,
duration=0.5
)
summary = entry.result_summary
assert len(summary) <= 103
class TestAppCSSPathFull:
def test_app_has_css_path(self):
from ui import GraphMemoryApp
assert hasattr(GraphMemoryApp, 'CSS_PATH')
assert len(GraphMemoryApp.CSS_PATH) > 0
def test_css_path_are_paths(self):
from ui import GraphMemoryApp
for path in GraphMemoryApp.CSS_PATH:
assert isinstance(path, Path)
class TestAppBindingsFull:
def test_app_has_bindings(self):
from ui import GraphMemoryApp
assert hasattr(GraphMemoryApp, 'BINDINGS')
assert len(GraphMemoryApp.BINDINGS) > 0
def test_bindings_have_required_keys(self):
from ui import GraphMemoryApp
for binding in GraphMemoryApp.BINDINGS:
assert hasattr(binding, 'key')
assert hasattr(binding, 'action')
class TestWidgetImportsFull:
def test_import_left_panel(self):
from ui.widgets.left_panel import LeftPanel
assert LeftPanel is not None
def test_import_right_panel(self):
from ui.widgets.right_panel import RightPanel
assert RightPanel is not None
def test_import_input_box(self):
from ui.widgets.input_box import InputBox
assert InputBox is not None
def test_import_message_history(self):
from ui.widgets.message_history import MessageHistory
assert MessageHistory is not None
def test_import_message_widget(self):
from ui.widgets.message_widget import MessageWidget
assert MessageWidget is not None
def test_import_status_bar(self):
from ui.widgets.status_bar import StatusBar
assert StatusBar is not None
def test_import_config_section(self):
from ui.widgets.config_section import ConfigSection
assert ConfigSection is not None
def test_import_operation_log(self):
from ui.widgets.operation_log import OperationLog
assert OperationLog is not None
def test_import_cypher_query_box(self):
from ui.widgets.cypher_query_box import CypherQueryBox
assert CypherQueryBox is not None
class TestHandlerImportsFull:
def test_import_focus_handler(self):
from ui.handlers.focus_handler import FocusHandler
assert FocusHandler is not None
def test_import_key_handler(self):
from ui.handlers.key_handler import KeyHandler
assert KeyHandler is not None
def test_import_message_handler(self):
from ui.handlers.message_handler import MessageHandler
assert MessageHandler is not None
class TestServiceImportsFull:
def test_import_config_manager(self):
from ui.services.config_manager import ConfigManager
assert ConfigManager is not None
def test_import_config_service(self):
from ui.services.config_service import ConfigService
assert ConfigService is not None
def test_import_chat_service(self):
from ui.services.chat_service import ChatService
assert ChatService is not None
def test_import_tool_service(self):
from ui.services.tool_service import ToolService
assert ToolService is not None
class TestAppInitializationFull:
def test_app_without_backend(self):
from ui import GraphMemoryApp
app = GraphMemoryApp()
assert app._backend_server is None
assert app._backend_client is None
def test_app_with_backend(self):
from ui import GraphMemoryApp
from core import BackendServer
with tempfile.NamedTemporaryFile(suffix=".db", delete=False) as f:
db_path = f.name
try:
server = BackendServer(db_path=db_path)
server.start(api_key="")
app = GraphMemoryApp(backend_server=server)
assert app._backend_server is server
assert app._backend_client is not None
server.shutdown()
finally:
if os.path.exists(db_path):
os.unlink(db_path)
class TestConfigManagerFull:
def test_config_manager_creation(self):
from ui.services.config_manager import ConfigManager
cm = ConfigManager()
assert cm is not None
def test_config_manager_config_path(self):
from ui.services.config_manager import ConfigManager
cm = ConfigManager()
assert cm._config_path is not None
def test_config_manager_exists_false(self):
from ui.services.config_manager import ConfigManager
cm = ConfigManager()
assert cm.exists() is False