diff --git a/hindsight-api-slim/tests/test_fact_extraction_retry.py b/hindsight-api-slim/tests/test_fact_extraction_retry.py index 97baf721..5c1ee5f3 100644 --- a/hindsight-api-slim/tests/test_fact_extraction_retry.py +++ b/hindsight-api-slim/tests/test_fact_extraction_retry.py @@ -139,3 +139,76 @@ async def test_retain_llm_max_retries_overrides_global(): assert facts == [] # Verify it retried exactly retain_llm_max_retries times assert llm_config.call.call_count == 5 + + +@pytest.mark.asyncio +async def test_none_event_date_with_empty_facts_no_crash(): + """ + When event_date is None and the LLM returns an empty facts list, + the debug log should not crash with AttributeError on .isoformat(). + + Regression test for https://github.com/vectorize-io/hindsight/issues/874 + """ + from hindsight_api.engine.retain.fact_extraction import _extract_facts_from_chunk + + config = _make_config(llm_max_retries=1) + + # LLM returns a valid dict but with no facts — triggers the debug log path + llm_config = _make_llm_config(mock_response={"facts": []}) + + with patch( + "hindsight_api.engine.retain.fact_extraction._build_extraction_prompt_and_schema", + return_value=("system prompt", MagicMock()), + ): + facts, usage = await _extract_facts_from_chunk( + chunk="A plain text document with no timestamp.", + chunk_index=0, + total_chunks=1, + event_date=None, + context="", + llm_config=llm_config, + config=config, + agent_name="test-agent", + ) + + assert facts == [] + + +@pytest.mark.asyncio +async def test_none_event_date_with_valid_facts_no_crash(): + """ + When event_date is None but the LLM returns valid facts, + extraction should succeed without errors. + """ + from hindsight_api.engine.retain.fact_extraction import _extract_facts_from_chunk + + config = _make_config(llm_max_retries=1) + + llm_config = _make_llm_config(mock_response={ + "facts": [ + { + "what": "Alice visited Paris", + "when": "2023", + "who": "Alice", + "why": "vacation", + } + ] + }) + + with patch( + "hindsight_api.engine.retain.fact_extraction._build_extraction_prompt_and_schema", + return_value=("system prompt", MagicMock()), + ): + facts, usage = await _extract_facts_from_chunk( + chunk="Alice visited Paris in 2023.", + chunk_index=0, + total_chunks=1, + event_date=None, + context="", + llm_config=llm_config, + config=config, + agent_name="test-agent", + ) + + assert len(facts) == 1 + assert "Alice visited Paris" in facts[0].fact diff --git a/hindsight-embed/tests/test_embed_manager.py b/hindsight-embed/tests/test_embed_manager.py index eb91d924..a2ffefd0 100644 --- a/hindsight-embed/tests/test_embed_manager.py +++ b/hindsight-embed/tests/test_embed_manager.py @@ -1,6 +1,9 @@ """Tests for EmbedManager interface.""" +from unittest.mock import MagicMock + from hindsight_embed import get_embed_manager +from hindsight_embed.daemon_embed_manager import DaemonEmbedManager def test_sanitize_profile_name_via_db_url(): @@ -51,3 +54,43 @@ def test_manager_singleton(): # They should produce the same results assert manager1.get_database_url("test") == manager2.get_database_url("test") + + +def test_register_profile_skips_when_no_api_keys(): + """ + When config contains only short keys (no HINDSIGHT_API_* prefix), + _register_profile should not call create_profile, preserving any + existing profile .env file. + + Regression test for https://github.com/vectorize-io/hindsight/issues/894 + """ + manager = DaemonEmbedManager() + manager._profile_manager = MagicMock() + + # Config with short keys (as passed from cli.py's get_config()) + config = {"llm_api_key": "sk-123", "llm_provider": "openai", "llm_model": "gpt-4o"} + manager._register_profile("myprofile", 8100, config) + + manager._profile_manager.create_profile.assert_not_called() + + +def test_register_profile_calls_create_when_api_keys_present(): + """ + When config contains HINDSIGHT_API_* keys, _register_profile should + forward them to create_profile. + """ + manager = DaemonEmbedManager() + manager._profile_manager = MagicMock() + + config = { + "HINDSIGHT_API_LLM_PROVIDER": "openai", + "HINDSIGHT_API_LLM_API_KEY": "sk-123", + "some_internal_key": "ignored", + } + manager._register_profile("myprofile", 8100, config) + + manager._profile_manager.create_profile.assert_called_once_with( + "myprofile", + 8100, + {"HINDSIGHT_API_LLM_PROVIDER": "openai", "HINDSIGHT_API_LLM_API_KEY": "sk-123"}, + )