Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ jobs:
matrix:
os:
- "ubuntu-latest"
- "windows-latest"
- "macos-14"
- "macos-15"
# Ref: https://docs.github.com/en/actions/how-tos/writing-workflows/choosing-where-your-workflow-runs/choosing-the-runner-for-a-job
python-version:
- "3.10"
- "3.11"
Expand All @@ -51,4 +55,4 @@ jobs:
poetry run ruff format --check
- name: Test with pytest
run: |
PYTHONPATH=src poetry run pytest tests -vv
poetry run pytest tests -vv
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,11 @@ For more detailed examples, please check out the [`examples`](./examples) direct
## 📦 Installation

> [!WARNING]
> Currently, MemOS primarily supports Linux platforms. You may encounter issues on Windows and macOS temporarily.
> MemOS is compatible with Linux, Windows, and macOS.
>
> However, if you're using macOS, please note that there may be dependency issues that are difficult to resolve.
>
> For example, compatibility with macOS 13 Ventura is currently challenging.

### Install via pip

Expand Down
10 changes: 10 additions & 0 deletions src/memos/mem_user/user_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -476,3 +476,13 @@ def delete_cube(self, cube_id: str) -> bool:
return False
finally:
session.close()

def close(self) -> None:
"""Close the database engine and dispose of all connections.

This method should be called when the UserManager is no longer needed
to ensure proper cleanup of database connections.
"""
if hasattr(self, "engine"):
self.engine.dispose()
logger.info("UserManager database connections closed")
6 changes: 4 additions & 2 deletions tests/mem_os/test_memos_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def mock_config():
"chat_model": {
"backend": "huggingface",
"config": {
"model_name_or_path": "Qwen/Qwen3-1.7B",
"model_name_or_path": "hf-internal-testing/tiny-random-gpt2",
"temperature": 0.1,
"remove_think_prefix": True,
"max_tokens": 4096,
Expand Down Expand Up @@ -188,8 +188,10 @@ def test_mos_init_success(
mock_user_manager.validate_user.assert_called_once_with("test_user")

@patch("memos.mem_os.core.UserManager")
def test_mos_init_invalid_user(self, mock_user_manager_class, mock_config):
@patch("memos.mem_os.core.LLMFactory")
def test_mos_init_invalid_user(self, mock_llm_factory, mock_user_manager_class, mock_config):
"""Test MOS initialization with invalid user."""
mock_llm_factory.from_config.return_value = MagicMock()
mock_user_manager = MagicMock()
mock_user_manager.validate_user.return_value = False
mock_user_manager_class.return_value = mock_user_manager
Expand Down
54 changes: 39 additions & 15 deletions tests/mem_user/test_mem_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,22 @@ def temp_db(self):
temp_dir = tempfile.mkdtemp()
db_path = os.path.join(temp_dir, "test_memos.db")
yield db_path
# Cleanup
if os.path.exists(db_path):
os.remove(db_path)
os.rmdir(temp_dir)
# Cleanup - note: file cleanup is handled by user_manager fixture
try:
if os.path.exists(db_path):
os.remove(db_path)
os.rmdir(temp_dir)
except (OSError, PermissionError):
# On Windows, files might still be locked, ignore cleanup errors
pass

@pytest.fixture
def user_manager(self, temp_db):
"""Create UserManager instance with temporary database."""
return UserManager(db_path=temp_db)
manager = UserManager(db_path=temp_db)
yield manager
# Ensure database connections are closed
manager.close()

def test_initialization(self, temp_db):
"""Test UserManager initialization."""
Expand Down Expand Up @@ -63,18 +70,27 @@ class MockSettings:
# Replace the settings import
monkeypatch.setattr("memos.mem_user.user_manager.settings", MockSettings())

manager = None
try:
manager = UserManager()
expected_path = mock_memos_dir / "memos_users.db"
assert manager.db_path == str(expected_path)
assert os.path.exists(expected_path)
finally:
# Close database connections first
if manager:
manager.close()

# Cleanup
expected_path = mock_memos_dir / "memos_users.db"
if os.path.exists(expected_path):
os.remove(expected_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
try:
expected_path = mock_memos_dir / "memos_users.db"
if os.path.exists(expected_path):
os.remove(expected_path)
if os.path.exists(temp_dir):
os.rmdir(temp_dir)
except (OSError, PermissionError):
# On Windows, files might still be locked, ignore cleanup errors
pass


class TestUserOperations:
Expand All @@ -93,7 +109,9 @@ def temp_db(self):
@pytest.fixture
def user_manager(self, temp_db):
"""Create UserManager instance with temporary database."""
return UserManager(db_path=temp_db)
manager = UserManager(db_path=temp_db)
yield manager
manager.close()

def test_create_user(self, user_manager):
"""Test user creation."""
Expand Down Expand Up @@ -239,7 +257,9 @@ def temp_db(self):
@pytest.fixture
def user_manager(self, temp_db):
"""Create UserManager instance with temporary database."""
return UserManager(db_path=temp_db)
manager = UserManager(db_path=temp_db)
yield manager
manager.close()

def test_create_cube(self, user_manager):
"""Test cube creation."""
Expand All @@ -264,7 +284,7 @@ def test_create_cube_with_path_and_custom_id(self, user_manager):
owner_id = user_manager.create_user("cube_owner", UserRole.USER)

custom_cube_id = "custom_cube_123"
cube_path = "/path/to/cube"
cube_path = str(Path("/path/to/cube")) # Use pathlib for cross-platform path handling

cube_id = user_manager.create_cube(
"custom_cube", owner_id, cube_path=cube_path, cube_id=custom_cube_id
Expand Down Expand Up @@ -433,7 +453,9 @@ def temp_db(self):
@pytest.fixture
def user_manager(self, temp_db):
"""Create UserManager instance with temporary database."""
return UserManager(db_path=temp_db)
manager = UserManager(db_path=temp_db)
yield manager
manager.close()

def test_user_roles(self, user_manager):
"""Test different user roles."""
Expand Down Expand Up @@ -483,7 +505,9 @@ def temp_db(self):
@pytest.fixture
def user_manager(self, temp_db):
"""Create UserManager instance with temporary database."""
return UserManager(db_path=temp_db)
manager = UserManager(db_path=temp_db)
yield manager
manager.close()

def test_cascade_delete_user_cubes(self, user_manager):
"""Test that user's owned cubes are handled when user is deleted."""
Expand Down
5 changes: 3 additions & 2 deletions tests/memories/textual/test_general.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# TODO: Overcomplex. Use pytest fixtures instead of setUp/tearDown.
import json
import os
import unittest
import uuid

Expand Down Expand Up @@ -455,9 +456,9 @@ def test_load(self):

def test_dump(self):
"""Test dump functionality for GeneralTextMemory."""
test_dir = "/test/directory"
test_dir = "test/directory"
memory_filename = "textual_memory.json"
memory_file_path = test_dir + "/" + memory_filename
memory_file_path = os.path.join(test_dir, memory_filename)

# Set the config's memory_filename
self.config.memory_filename = memory_filename
Expand Down