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:
140
README.md
140
README.md
@ -26,6 +26,7 @@ TrulyMEM (TrueHumanMEM) 是一个让 AI 拥有长期记忆能力的图记忆系
|
|||||||
- 💾 **内嵌数据库** - SQLite 实现,无需 Docker/Neo4j
|
- 💾 **内嵌数据库** - SQLite 实现,无需 Docker/Neo4j
|
||||||
- 📦 **独立部署** - 支持打包为独立可执行文件
|
- 📦 **独立部署** - 支持打包为独立可执行文件
|
||||||
- 🌍 **跨平台** - 支持 Windows/Linux/macOS
|
- 🌍 **跨平台** - 支持 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 | 帮助 |
|
| F1 | 帮助 |
|
||||||
| F2 | 切换侧边栏 |
|
| F2 | 切换侧边栏 |
|
||||||
| F3 | 工具详情 |
|
| F3 | 工具详情 |
|
||||||
| F4 | 聚焦查询框 |
|
|
||||||
| F5 | 清屏 |
|
| F5 | 清屏 |
|
||||||
| F6 | 退出 |
|
| F6 | 退出 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 项目结构
|
## 测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/
|
||||||
```
|
```
|
||||||
TrulyMEM-TrueHumanMEM/
|
|
||||||
├── trulymem_entry.py # 入口:先启动 core → 再启动 ui
|
37 个测试用例覆盖:
|
||||||
├── core/ # 后端/业务逻辑
|
- 数据包协议
|
||||||
│ ├── __init__.py
|
- 后端初始化/启动/关闭
|
||||||
│ ├── server.py # BackendServer (多线程)
|
- 客户端方法
|
||||||
│ ├── client.py # BackendClient
|
- 配置管理
|
||||||
│ ├── embedded_db.py # SQLite 图数据库
|
- 消息历史
|
||||||
│ ├── graph_client.py
|
- UI 组件
|
||||||
│ ├── 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)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -137,40 +179,8 @@ TrulyMEM-TrueHumanMEM/
|
|||||||
- **Textual** - TUI 框架
|
- **Textual** - TUI 框架
|
||||||
- **SQLite** - 内嵌图数据库
|
- **SQLite** - 内嵌图数据库
|
||||||
- **OpenAI SDK** - API 调用(兼容 DeepSeek)
|
- **OpenAI SDK** - API 调用(兼容 DeepSeek)
|
||||||
|
- **threading** - 多线程通信
|
||||||
---
|
- **queue** - 线程安全队列
|
||||||
|
|
||||||
## 架构说明
|
|
||||||
|
|
||||||
### 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/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -196,14 +206,4 @@ pytest tests/
|
|||||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||||
5. 创建 Pull Request
|
5. 创建 Pull Request
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 文档
|
|
||||||
|
|
||||||
更多文档见 [docs/](docs/) 目录:
|
|
||||||
|
|
||||||
- [架构设计](docs/架构.md) - 系统架构和技术设计
|
|
||||||
- [快速开始](docs/一键启动指南.md) - 启动指南
|
|
||||||
- [工作记忆链机制说明](docs/工作记忆链机制说明.md) - 连续性任务处理
|
|
||||||
@ -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)
|
|
||||||
312
tests/test_core/test_packet.py
Normal file
312
tests/test_core/test_packet.py
Normal 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)
|
||||||
@ -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
|
|
||||||
278
tests/test_integration/__init__.py
Normal file
278
tests/test_integration/__init__.py
Normal 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)
|
||||||
@ -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 + "..."
|
|
||||||
@ -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
322
tests/test_ui/__init__.py
Normal 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
|
||||||
Reference in New Issue
Block a user