diff --git a/hindsight-api-slim/hindsight_api/api/http.py b/hindsight-api-slim/hindsight_api/api/http.py index c357f4116..e931da08c 100644 --- a/hindsight-api-slim/hindsight_api/api/http.py +++ b/hindsight-api-slim/hindsight_api/api/http.py @@ -466,6 +466,13 @@ class MemoryItem(BaseModel): description="Optional tags for visibility scoping. Memories with tags can be filtered during recall.", ) + @field_validator("content") + @classmethod + def validate_content(cls, v: str) -> str: + if not v.strip(): + raise ValueError("content cannot be empty") + return v + @field_validator("tags", mode="before") @classmethod def coerce_tags(cls, v): diff --git a/hindsight-api-slim/tests/test_none_llm_provider.py b/hindsight-api-slim/tests/test_none_llm_provider.py index 3122348f4..034868c7b 100644 --- a/hindsight-api-slim/tests/test_none_llm_provider.py +++ b/hindsight-api-slim/tests/test_none_llm_provider.py @@ -248,6 +248,21 @@ async def test_http_retain_works(none_api_client): assert response.status_code == 200 +@pytest.mark.asyncio +@pytest.mark.parametrize("content", ["", " \n\t"]) +async def test_http_retain_rejects_blank_content(none_api_client, content): + """Retain endpoint should reject empty or whitespace-only content.""" + bank_id = f"test_none_http_retain_blank_{datetime.now(timezone.utc).timestamp()}" + + response = await none_api_client.post( + f"/v1/default/banks/{bank_id}/memories", + json={"items": [{"content": content, "context": "test"}]}, + ) + + assert response.status_code == 422 + assert "content cannot be empty" in str(response.json()["detail"]) + + @pytest.mark.asyncio async def test_http_recall_works(none_api_client): """Recall endpoint should work with provider=none."""