diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index 4bf9ef7ba..83ff35572 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -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" @@ -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 diff --git a/README.md b/README.md index 55be8cf20..a54edd21a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/memos/mem_user/user_manager.py b/src/memos/mem_user/user_manager.py index 31d4dfc87..e5ca73b58 100644 --- a/src/memos/mem_user/user_manager.py +++ b/src/memos/mem_user/user_manager.py @@ -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") diff --git a/tests/mem_os/test_memos_core.py b/tests/mem_os/test_memos_core.py index fa4f92f05..3623bd900 100644 --- a/tests/mem_os/test_memos_core.py +++ b/tests/mem_os/test_memos_core.py @@ -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, @@ -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 diff --git a/tests/mem_user/test_mem_user.py b/tests/mem_user/test_mem_user.py index 570d92fbe..1298e2fbf 100644 --- a/tests/mem_user/test_mem_user.py +++ b/tests/mem_user/test_mem_user.py @@ -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.""" @@ -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: @@ -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.""" @@ -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.""" @@ -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 @@ -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.""" @@ -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.""" diff --git a/tests/memories/textual/test_general.py b/tests/memories/textual/test_general.py index fefbe72df..7019b1216 100644 --- a/tests/memories/textual/test_general.py +++ b/tests/memories/textual/test_general.py @@ -1,5 +1,6 @@ # TODO: Overcomplex. Use pytest fixtures instead of setUp/tearDown. import json +import os import unittest import uuid @@ -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