* feat: introduce hindsight-api-slim and hindsight-all-slim packages Closes #552 - Move all source code from hindsight-api/ to new hindsight-api-slim/ - hindsight-api-slim has heavy ML deps (torch, sentence-transformers, transformers, einops, flashrank, mlx, mlx-lm, safetensors) and pg0-embedded as optional extras: [local-ml], [embedded-db], [all] - hindsight-api becomes a zero-code meta-package depending on hindsight-api-slim[all] for full backward compatibility - Add hindsight-all-slim meta-package: hindsight-api-slim + client + embed - hindsight-all updated to depend on hindsight-api-slim[all] - pg0.py: lazy-import pg0 with clear ImportError pointing to [embedded-db] - Dockerfile: replace sed hack with proper uv sync --extra flags - Update release.yml, test.yml, lint.sh, release.sh, CLAUDE.md and all path references throughout the repo * refactor: rename hindsight/ directory to hindsight-all/ * docs: document hindsight-api-slim and hindsight-all-slim package variants Add package variants table and extras explanation to installation.md * docs: remove emojis from installation.md, use professional tone * docs: link Docker slim variant to pip package variants section * docs: consolidate Docker image variants into single table * ci: fix working-directory paths after package restructure - Replace all hindsight-api → hindsight-api-slim in test.yml - Replace hindsight → hindsight-all in test.yml - Add --extra embedded-db to test-embed API install step * ci: add local-ml and embedded-db extras to API sync steps These extras were previously implicit in the old hindsight-api package (which bundled everything). Now that hindsight-api-slim uses optional extras, we must explicitly request local-ml and embedded-db in CI. * ci: add API install step with embedded-db to test-embed smoke test The smoke test starts hindsight-api as a daemon, which requires pg0-embedded. Add a dedicated install step for hindsight-api-slim with embedded-db extra so the daemon can start successfully. * ci: remove --no-install-project when using optional extras When --no-install-project is combined with --extra, the optional deps are not installed because extras require the project to be active. Remove --no-install-project from steps that need local-ml or embedded-db. * ci: fix ordering of uv sync steps to preserve optional extras When uv sync runs for a different workspace member, it removes optional extras installed for other members. Fix by always running extra-requiring API sync last, after other workspace member syncs. Also remove --no-install-project from embedded-db sync in test-embed, as --no-install-project prevents optional extras from being active. * ci: add local-ml extra to test-embed API install for smoke test The smoke test starts the full API server which needs sentence-transformers for local embeddings (default provider). Add local-ml extra to the install. * ci: simplify extras with --all-extras and add slim pip smoke test - Replace explicit --extra local-ml --extra embedded-db with --all-extras for cleaner, more maintainable sync steps - Add test-pip-slim job: tests hindsight-api-slim[embedded-db] without local ML models, using Cohere for embeddings/reranking (mirrors Docker slim smoke test approach) * ci: simplify slim smoke test to health check only (mirrors Docker test)
170 lines
5.5 KiB
Python
170 lines
5.5 KiB
Python
"""Test that API namespaces ensure daemon is started before each call."""
|
|
|
|
from unittest.mock import Mock, patch
|
|
|
|
import pytest
|
|
|
|
from hindsight import HindsightEmbedded
|
|
|
|
|
|
@pytest.fixture
|
|
def embedded_client():
|
|
"""Create an embedded client for testing."""
|
|
return HindsightEmbedded(
|
|
profile="test",
|
|
llm_provider="openai",
|
|
llm_api_key="test-key",
|
|
)
|
|
|
|
|
|
def test_banks_create_ensures_daemon_started(embedded_client):
|
|
"""Test that banks.create() calls _ensure_started()."""
|
|
# Mock _ensure_started to track calls
|
|
with patch.object(embedded_client, "_ensure_started") as mock_ensure:
|
|
# Mock the underlying client to avoid actual API call
|
|
mock_client = Mock()
|
|
embedded_client._client = mock_client
|
|
|
|
# Call namespace method
|
|
try:
|
|
embedded_client.banks.create(bank_id="test", name="Test Bank")
|
|
except Exception:
|
|
pass # We don't care if the actual call fails
|
|
|
|
# Verify _ensure_started was called
|
|
mock_ensure.assert_called_once()
|
|
|
|
|
|
def test_mental_models_list_ensures_daemon_started(embedded_client):
|
|
"""Test that mental_models.list() calls _ensure_started()."""
|
|
with patch.object(embedded_client, "_ensure_started") as mock_ensure:
|
|
mock_client = Mock()
|
|
embedded_client._client = mock_client
|
|
|
|
try:
|
|
embedded_client.mental_models.list(bank_id="test")
|
|
except Exception:
|
|
pass
|
|
|
|
mock_ensure.assert_called_once()
|
|
|
|
|
|
def test_directives_list_ensures_daemon_started(embedded_client):
|
|
"""Test that directives.list() calls _ensure_started()."""
|
|
with patch.object(embedded_client, "_ensure_started") as mock_ensure:
|
|
mock_client = Mock()
|
|
embedded_client._client = mock_client
|
|
|
|
try:
|
|
embedded_client.directives.list(bank_id="test")
|
|
except Exception:
|
|
pass
|
|
|
|
mock_ensure.assert_called_once()
|
|
|
|
|
|
def test_memories_list_ensures_daemon_started(embedded_client):
|
|
"""Test that memories.list() calls _ensure_started()."""
|
|
with patch.object(embedded_client, "_ensure_started") as mock_ensure:
|
|
mock_client = Mock()
|
|
embedded_client._client = mock_client
|
|
|
|
try:
|
|
embedded_client.memories.list(bank_id="test")
|
|
except Exception:
|
|
pass
|
|
|
|
mock_ensure.assert_called_once()
|
|
|
|
|
|
def test_multiple_calls_ensure_daemon_each_time(embedded_client):
|
|
"""Test that each namespace call ensures daemon is started."""
|
|
with patch.object(embedded_client, "_ensure_started") as mock_ensure:
|
|
mock_client = Mock()
|
|
embedded_client._client = mock_client
|
|
|
|
# Make multiple calls
|
|
try:
|
|
embedded_client.banks.create(bank_id="test", name="Test")
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
embedded_client.mental_models.list(bank_id="test")
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
embedded_client.directives.list(bank_id="test")
|
|
except Exception:
|
|
pass
|
|
|
|
# Should be called 3 times (once per namespace method call)
|
|
assert mock_ensure.call_count == 3
|
|
|
|
|
|
def test_daemon_restart_handling(embedded_client):
|
|
"""Test that namespace methods can recover from daemon crash."""
|
|
call_count = 0
|
|
|
|
def mock_ensure_started():
|
|
"""Mock that simulates daemon restart."""
|
|
nonlocal call_count
|
|
call_count += 1
|
|
# Create a new mock client each time (simulating daemon restart)
|
|
embedded_client._client = Mock()
|
|
embedded_client._started = True
|
|
|
|
with patch.object(embedded_client, "_ensure_started", side_effect=mock_ensure_started):
|
|
# First call - daemon starts
|
|
embedded_client.banks.create(bank_id="test", name="Test")
|
|
assert call_count == 1
|
|
|
|
# Simulate daemon crash by clearing client
|
|
embedded_client._client = None
|
|
embedded_client._started = False
|
|
|
|
# Second call - daemon restarts
|
|
embedded_client.banks.create(bank_id="test", name="Test")
|
|
assert call_count == 2
|
|
|
|
|
|
def test_ensure_started_calls_manager(embedded_client):
|
|
"""Test that _ensure_started actually starts the daemon via manager."""
|
|
# Mock the manager
|
|
mock_manager = Mock()
|
|
mock_manager.ensure_running.return_value = True
|
|
mock_manager.get_url.return_value = "http://localhost:54321"
|
|
|
|
embedded_client._manager = mock_manager
|
|
|
|
# Mock Hindsight client constructor
|
|
with patch("hindsight.embedded.Hindsight") as mock_hindsight_class:
|
|
mock_client = Mock()
|
|
mock_hindsight_class.return_value = mock_client
|
|
|
|
# Call _ensure_started
|
|
embedded_client._ensure_started()
|
|
|
|
# Verify manager was called
|
|
mock_manager.ensure_running.assert_called_once_with(
|
|
embedded_client.config, embedded_client.profile
|
|
)
|
|
mock_manager.get_url.assert_called_once_with(embedded_client.profile)
|
|
|
|
# Verify Hindsight client was created
|
|
mock_hindsight_class.assert_called_once_with(base_url="http://localhost:54321")
|
|
|
|
|
|
def test_namespace_singleton_behavior(embedded_client):
|
|
"""Test that namespace properties return the same instance."""
|
|
banks1 = embedded_client.banks
|
|
banks2 = embedded_client.banks
|
|
|
|
# Should be the same instance
|
|
assert banks1 is banks2
|
|
|
|
# Same for other namespaces
|
|
assert embedded_client.mental_models is embedded_client.mental_models
|
|
assert embedded_client.directives is embedded_client.directives
|
|
assert embedded_client.memories is embedded_client.memories
|