* 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)
206 lines
6.4 KiB
Python
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
|