diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f5804049e..55e78274c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,14 +153,17 @@ jobs: strategy: matrix: pyver: ['3.10', '3.11', '3.12', '3.13', '3.14'] - os: [ubuntu] + os: ["ubuntu-latest"] experimental: [false] include: - - os: ubuntu + - os: "ubuntu-latest" pyver: "3.14t" experimental: false + - os: "ubuntu-24.04-arm" + pyver: "3.14" + experimental: false fail-fast: true - runs-on: ${{ matrix.os }}-latest + runs-on: ${{ matrix.os }} continue-on-error: ${{ matrix.experimental }} permissions: contents: read # For checkout @@ -179,12 +182,12 @@ jobs: allow-prereleases: true python-version: ${{ matrix.pyver }} - name: Install dependency for pyaudio (Ubuntu) - if: matrix.os == 'ubuntu' + if: startsWith(matrix.os, 'ubuntu') run: | sudo apt-get update sudo apt-get install -y portaudio19-dev - name: Install dependency for pyaudio (macOS) - if: matrix.os == 'macos' + if: startsWith(matrix.os, 'macos') run: brew install portaudio - name: Remove git LFS to avoid accidental large downloads run: sudo rm -f /usr/bin/git-lfs diff --git a/dimos/conftest.py b/dimos/conftest.py index 35804c8f62..a5fcbef66b 100644 --- a/dimos/conftest.py +++ b/dimos/conftest.py @@ -78,6 +78,12 @@ def _is_macos() -> bool: return platform.system() == "Darwin" +def _no_sqlite_vec() -> bool: + # The aarch64 wheel ships a 32-bit binary ("wrong ELF class: ELFCLASS32"), + # and the macOS wheel fails to load via sqlite3 in CI. + return platform.machine() == "aarch64" or _is_macos() + + def pytest_configure(config): config.addinivalue_line("markers", "tool: dev tooling") config.addinivalue_line( @@ -92,6 +98,9 @@ def pytest_configure(config): config.addinivalue_line("markers", "skipif_no_ros: skip when ROS dependencies are not present") config.addinivalue_line("markers", "skipif_macos_bug: skip known-buggy tests on macOS") config.addinivalue_line("markers", "skipif_macos: skip tests not intended to run on macOS") + config.addinivalue_line( + "markers", "skipif_no_sqlite_vec: skip when the sqlite-vec extension cannot be loaded" + ) if config.pluginmanager.hasplugin("_cov"): os.environ["COVERAGE_PROCESS_START"] = str(config.rootpath / "pyproject.toml") @@ -124,6 +133,7 @@ def pytest_collection_modifyitems(config, items): "skipif_no_ros": (not _has_ros(), "ROS dependencies are not present"), "skipif_macos_bug": (_is_macos(), "Some tests are buggy on Mac OS"), "skipif_macos": (_is_macos(), "Not intended to run on macOS"), + "skipif_no_sqlite_vec": (_no_sqlite_vec(), "sqlite-vec extension not loadable here"), } for marker_name, (condition, reason) in _skipif_markers.items(): if condition: diff --git a/dimos/memory2/conftest.py b/dimos/memory2/conftest.py index 1417658333..90f96cbd50 100644 --- a/dimos/memory2/conftest.py +++ b/dimos/memory2/conftest.py @@ -16,6 +16,7 @@ from __future__ import annotations +import platform import sqlite3 import tempfile from typing import TYPE_CHECKING, cast @@ -28,6 +29,10 @@ from dimos.memory2.store.sqlite import SqliteStore from dimos.models.embedding.clip import CLIPModel +# sqlite-vec fails to load on Linux ARM (32-bit binary in the aarch64 wheel) +# and on macOS in CI. +_SKIP_SQLITE_VEC = platform.machine() == "aarch64" or platform.system() == "Darwin" + if TYPE_CHECKING: from collections.abc import Iterator from pathlib import Path @@ -55,6 +60,8 @@ def memory_session(memory_store: MemoryStore) -> Iterator[MemoryStore]: @pytest.fixture def sqlite_store() -> Iterator[SqliteStore]: + if _SKIP_SQLITE_VEC: + pytest.skip("sqlite-vec extension not loadable here") with tempfile.NamedTemporaryFile(suffix=".db") as f: store = SqliteStore(path=f.name) with store: diff --git a/dimos/memory2/test_registry.py b/dimos/memory2/test_registry.py index 7c1b20239e..e1b6c203d6 100644 --- a/dimos/memory2/test_registry.py +++ b/dimos/memory2/test_registry.py @@ -41,6 +41,7 @@ def test_qual_notifier(self) -> None: assert qual(SubjectNotifier) == "dimos.memory2.notifier.subject.SubjectNotifier" +@pytest.mark.skipif_no_sqlite_vec class TestRegistryStore: def test_put_get_round_trip(self, tmp_path) -> None: from dimos.memory2.utils.sqlite import open_sqlite_connection @@ -106,6 +107,7 @@ def test_sqlite_blob_store_config(self) -> None: restored = SqliteBlobStoreConfig(**dumped) assert restored.path == "/tmp/test.db" + @pytest.mark.skipif_no_sqlite_vec def test_sqlite_blob_store_roundtrip(self, tmp_path) -> None: store = SqliteBlobStore(path=str(tmp_path / "blob.db")) data = store.serialize() @@ -127,6 +129,7 @@ def test_sqlite_vector_store_config(self) -> None: restored = SqliteVectorStoreConfig(**dumped) assert restored.path == "/tmp/vec.db" + @pytest.mark.skipif_no_sqlite_vec def test_sqlite_vector_store_roundtrip(self, tmp_path) -> None: store = SqliteVectorStore(path=str(tmp_path / "vec.db")) data = store.serialize() @@ -166,6 +169,7 @@ def test_backend_serialize(self, tmp_path) -> None: assert data["notifier"]["class"] == qual(SubjectNotifier) +@pytest.mark.skipif_no_sqlite_vec class TestStoreReopen: def test_reopen_preserves_data(self, tmp_path) -> None: """Create a store, write data, close, reopen, read back.""" diff --git a/dimos/memory2/test_store.py b/dimos/memory2/test_store.py index 5b6d146364..04d0dcc959 100644 --- a/dimos/memory2/test_store.py +++ b/dimos/memory2/test_store.py @@ -20,6 +20,7 @@ from __future__ import annotations +import platform from typing import TYPE_CHECKING, Any import pytest @@ -28,6 +29,8 @@ from dimos.memory2.blobstore.base import BlobStore from dimos.memory2.vectorstore.base import VectorStore +_SKIP_SQLITE_VEC = platform.machine() == "aarch64" or platform.system() == "Darwin" + if TYPE_CHECKING: from dimos.memory2.store.base import Store @@ -343,6 +346,8 @@ def memory_spy_session(): @pytest.fixture def sqlite_spy_session(tmp_path): + if _SKIP_SQLITE_VEC: + pytest.skip("sqlite-vec extension not loadable here") from dimos.memory2.store.sqlite import SqliteStore blob_spy = SpyBlobStore() @@ -417,6 +422,7 @@ def _emb(v: list[float]) -> Embedding: assert results[0].data == "north" +@pytest.mark.skipif_no_sqlite_vec class TestStandaloneComponents: """Verify each SQLite component works standalone with path= (no Store needed)."""