Phase 1C: Frontend unified contact UI

This commit is contained in:
Agent Zero
2026-07-23 17:17:32 +02:00
parent a8331fbc2b
commit 879106c4eb
62 changed files with 552 additions and 1276 deletions
+23 -25
View File
@@ -167,7 +167,7 @@ async def test_ac1_search_returns_results(
client, _ = search_authed_client
mock_hybrid_search.return_value = [
{
"entity_type": "company",
"entity_type": "contact",
"entity_id": str(uuid.uuid4()),
"title": "Company Alpha",
"snippet": "IT company",
@@ -186,7 +186,7 @@ async def test_ac1_search_returns_results(
assert "results" in data
assert isinstance(data["results"], list)
assert len(data["results"]) > 0
assert data["results"][0]["entity_type"] == "company"
assert data["results"][0]["entity_type"] == "contact"
assert data["results"][0]["title"] == "Company Alpha"
assert "score" in data["results"][0]
assert "query" in data
@@ -209,13 +209,13 @@ async def test_ac2_search_entity_types_filter(
resp = await client.post(
"/api/v1/search",
json={"query": "test", "entity_types": ["company"]},
json={"query": "test", "entity_types": ["contact"]},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 200
# Verify hybrid_search was called with entity_types=["company"]
# Verify hybrid_search was called with entity_types=["contact"]
call_kwargs = mock_hybrid_search.call_args
assert call_kwargs.kwargs.get("entity_types") == ["company"]
assert call_kwargs.kwargs.get("entity_types") == ["contact"]
# ─── AC3: No auth → 401 ───
@@ -322,7 +322,7 @@ async def test_ac8_similar_returns_results(
resp = await client.post(
"/api/v1/search/similar",
json={
"entity_type": "company",
"entity_type": "contact",
"entity_id": str(seed["company_a"].id),
"limit": 5,
},
@@ -347,7 +347,7 @@ async def test_ac9_similar_tenant_isolation(
resp = await client.post(
"/api/v1/search/similar",
json={
"entity_type": "company",
"entity_type": "contact",
"entity_id": str(seed["company_b"].id),
"limit": 5,
},
@@ -398,7 +398,7 @@ async def test_ac11_reindex_admin_200(
client, _ = search_authed_client
resp = await client.post(
"/api/v1/search/reindex",
json={"entity_types": ["company"]},
json={"entity_types": ["contact"]},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 200
@@ -420,7 +420,7 @@ async def test_ac12_reindex_viewer_403(
resp = await search_client.post(
"/api/v1/search/reindex",
json={"entity_types": ["company"]},
json={"entity_types": ["contact"]},
headers=ORIGIN_HEADER,
)
assert resp.status_code == 403
@@ -440,7 +440,7 @@ def test_rrf_fusion_combines_fts_and_vector_scores():
{"id": "3", "title": "Result C", "score": 0.7},
]
fused = rrf_fusion(fts_results, vec_results, "company")
fused = rrf_fusion(fts_results, vec_results, "contact")
# All 3 unique IDs should be present
ids = [str(r.get("id", "")) for r in fused]
@@ -457,7 +457,7 @@ def test_rrf_fusion_combines_fts_and_vector_scores():
def test_rrf_fusion_empty_inputs():
"""RRF fusion with empty inputs returns empty list."""
fused = rrf_fusion([], [], "company")
fused = rrf_fusion([], [], "contact")
assert fused == []
@@ -467,7 +467,7 @@ def test_rrf_fusion_fts_only():
{"id": "1", "title": "A"},
{"id": "2", "title": "B"},
]
fused = rrf_fusion(fts_results, [], "company")
fused = rrf_fusion(fts_results, [], "contact")
assert len(fused) == 2
# First result (rank 0) should have higher score
assert fused[0]["_score"] > fused[1]["_score"]
@@ -547,7 +547,7 @@ def test_provider_get_all():
p1 = MagicMock()
p1.entity_type = "contact"
p2 = MagicMock()
p2.entity_type = "company"
p2.entity_type = "contact"
registry.register(p1)
registry.register(p2)
@@ -556,7 +556,6 @@ def test_provider_get_all():
assert len(all_providers) == 2
entity_types = [p.entity_type for p in all_providers]
assert "contact" in entity_types
assert "company" in entity_types
def test_provider_clear():
@@ -577,13 +576,12 @@ def test_provider_get_entity_types():
p1 = MagicMock()
p1.entity_type = "contact"
p2 = MagicMock()
p2.entity_type = "company"
p2.entity_type = "contact"
registry.register(p1)
registry.register(p2)
types = registry.get_entity_types()
assert "contact" in types
assert "company" in types
assert len(types) == 2
@@ -745,12 +743,12 @@ async def test_llm_aggregate_results_success():
mock_agg_resp.choices = [MagicMock()]
mock_agg_resp.choices[0].message.content = json.dumps({
"summary": "2 Ergebnisse gefunden",
"facets": {"types": {"company": 1, "contact": 1}},
"suggestions": ["Filter by company"],
"facets": {"types": {"contact": 2}},
"suggestions": ["Filter by contact"],
})
with patch("litellm.acompletion", new_callable=AsyncMock, return_value=mock_agg_resp):
results = [
{"entity_type": "company", "title": "Company Alpha"},
{"entity_type": "contact", "title": "Company Alpha"},
{"entity_type": "contact", "title": "John Doe"},
]
result = await llm_aggregate_results(results, "test")
@@ -997,7 +995,7 @@ async def test_index_contact(mock_index_entity, mock_factory, db_session: AsyncS
@patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True)
async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncSession):
"""index_company calls index_entity."""
from app.models.company import Company
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.core.auth import hash_password
@@ -1038,7 +1036,7 @@ async def test_index_company(mock_index_entity, mock_factory, db_session: AsyncS
@patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True)
async def test_reindex(mock_index_entity, mock_factory, db_session: AsyncSession):
"""reindex iterates over all entities of a type."""
from app.models.company import Company
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.core.auth import hash_password
@@ -1070,7 +1068,7 @@ async def test_reindex(mock_index_entity, mock_factory, db_session: AsyncSession
sf = async_sessionmaker(bind=db_session.bind, expire_on_commit=False, class_=AsyncSession)
mock_factory.return_value = sf
await reindex({}, "company")
await reindex({}, "contact")
mock_index_entity.assert_called()
@@ -1079,7 +1077,7 @@ async def test_reindex(mock_index_entity, mock_factory, db_session: AsyncSession
@patch("app.plugins.builtins.unified_search.embedding.index_entity", new_callable=AsyncMock, return_value=True)
async def test_embedding_batch(mock_index_entity, mock_factory, db_session: AsyncSession):
"""embedding_batch finds entities without embedding."""
from app.models.company import Company
from app.models.contact import Contact as Company
from app.models.tenant import Tenant
from app.models.user import User
from app.core.auth import hash_password
@@ -1169,7 +1167,7 @@ def test_company_provider_get_embedding_text():
"""CompanyProvider get_embedding_text is a callable method."""
provider = CompanySearchProvider()
assert hasattr(provider, "get_embedding_text")
assert provider.entity_type == "company"
assert provider.entity_type == "contact"
def test_mail_provider_get_embedding_text():
@@ -1210,7 +1208,7 @@ def test_company_provider_to_search_result():
provider = CompanySearchProvider()
entity = {"id": "456", "name": "Acme Corp", "description": "IT company"}
result = provider.to_search_result(entity)
assert result["entity_type"] == "company"
assert result["entity_type"] == "contact"
assert result["entity_id"] == "456"
assert result["title"] == "Acme Corp"
assert "IT company" in result["snippet"]