fleet-memory/hindsight-api-slim/tests/test_mental_model_hooks.py
Nicolò Boschi 15ea23d5d6
feat: introduce hindsight-api-slim and hindsight-all-slim packages (#560)
* 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)
2026-03-13 13:50:03 +01:00

206 lines
6.4 KiB
Python

"""Unit tests for mental model operation validator hooks.
Tests that the operation validator hooks are called correctly for
mental model GET and refresh operations.
"""
import pytest
from hindsight_api.extensions.operation_validator import (
MentalModelGetContext,
MentalModelGetResult,
MentalModelRefreshResult,
OperationValidatorExtension,
ValidationResult,
)
class TestMentalModelGetContextDataclass:
"""Tests for MentalModelGetContext dataclass."""
def test_create_context(self):
"""Test creating a MentalModelGetContext."""
from unittest.mock import MagicMock
request_context = MagicMock()
ctx = MentalModelGetContext(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=request_context,
)
assert ctx.bank_id == "bank-1"
assert ctx.mental_model_id == "mm-1"
assert ctx.request_context is request_context
class TestMentalModelGetResultDataclass:
"""Tests for MentalModelGetResult dataclass."""
def test_create_result_success(self):
"""Test creating a successful MentalModelGetResult."""
from unittest.mock import MagicMock
request_context = MagicMock()
result = MentalModelGetResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=request_context,
output_tokens=250,
)
assert result.bank_id == "bank-1"
assert result.mental_model_id == "mm-1"
assert result.output_tokens == 250
assert result.success is True
assert result.error is None
def test_create_result_failure(self):
"""Test creating a failed MentalModelGetResult."""
from unittest.mock import MagicMock
result = MentalModelGetResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
output_tokens=0,
success=False,
error="Not found",
)
assert result.success is False
assert result.error == "Not found"
class TestMentalModelRefreshResultDataclass:
"""Tests for MentalModelRefreshResult dataclass."""
def test_create_result_with_all_fields(self):
"""Test creating a MentalModelRefreshResult with all fields."""
from unittest.mock import MagicMock
result = MentalModelRefreshResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
query_tokens=50,
output_tokens=500,
context_tokens=0,
facts_used=10,
mental_models_used=2,
)
assert result.query_tokens == 50
assert result.output_tokens == 500
assert result.context_tokens == 0
assert result.facts_used == 10
assert result.mental_models_used == 2
assert result.success is True
assert result.error is None
def test_create_result_failure(self):
"""Test creating a failed MentalModelRefreshResult."""
from unittest.mock import MagicMock
result = MentalModelRefreshResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
query_tokens=50,
output_tokens=0,
context_tokens=0,
facts_used=0,
mental_models_used=0,
success=False,
error="Reflect failed",
)
assert result.success is False
assert result.error == "Reflect failed"
class TestDefaultHookBehavior:
"""Tests for default (no-op) behavior of mental model hooks on OperationValidatorExtension."""
@pytest.fixture
def validator(self):
"""Create a concrete subclass for testing default behavior."""
from unittest.mock import MagicMock
# Create a concrete subclass that implements the abstract methods
class TestValidator(OperationValidatorExtension):
async def validate_retain(self, ctx):
return ValidationResult.accept()
async def validate_recall(self, ctx):
return ValidationResult.accept()
async def validate_reflect(self, ctx):
return ValidationResult.accept()
return TestValidator(config={})
@pytest.mark.asyncio
async def test_validate_mental_model_get_default_accepts(self, validator):
"""Test that default validate_mental_model_get accepts."""
from unittest.mock import MagicMock
ctx = MentalModelGetContext(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
)
result = await validator.validate_mental_model_get(ctx)
assert result.allowed is True
@pytest.mark.asyncio
async def test_on_mental_model_get_complete_default_noop(self, validator):
"""Test that default on_mental_model_get_complete is a no-op."""
from unittest.mock import MagicMock
result = MentalModelGetResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
output_tokens=100,
)
# Should not raise
await validator.on_mental_model_get_complete(result)
@pytest.mark.asyncio
async def test_on_mental_model_refresh_complete_default_noop(self, validator):
"""Test that default on_mental_model_refresh_complete is a no-op."""
from unittest.mock import MagicMock
result = MentalModelRefreshResult(
bank_id="bank-1",
mental_model_id="mm-1",
request_context=MagicMock(),
query_tokens=50,
output_tokens=500,
context_tokens=0,
facts_used=5,
mental_models_used=1,
)
# Should not raise
await validator.on_mental_model_refresh_complete(result)
class TestExportsAvailable:
"""Test that mental model hooks are properly exported."""
def test_imports_from_extensions_package(self):
"""Test that all mental model types can be imported from hindsight_api.extensions."""
from hindsight_api.extensions import (
MentalModelGetContext,
MentalModelGetResult,
MentalModelRefreshResult,
)
assert MentalModelGetContext is not None
assert MentalModelGetResult is not None
assert MentalModelRefreshResult is not None