Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,556 @@
|
||||
"""Tests für Volltextsuche und Scope-basierte Filterung im KnowledgeStore.
|
||||
|
||||
Validiert Task 6.4:
|
||||
- SearchResult Dataclass mit entry, relevance_score, partial_results
|
||||
- Relevanz-Scoring: title > tags > summary > id
|
||||
- Sortierung nach Relevanz (höchster Score zuerst)
|
||||
- Scope-Filterung: Nur Ergebnisse aus autorisierten Scopes
|
||||
- Keine Offenlegung nicht-autorisierter Artefakte
|
||||
- Timeout-Handling: Abbruch nach 5 Sekunden mit Teilergebnis-Warnung
|
||||
|
||||
Requirements: 3.3, 3.7, 3.9
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from monorepo.knowledge.index import IndexEntry, YAMLIndex
|
||||
from monorepo.knowledge.store import (
|
||||
SEARCH_TIMEOUT_SECONDS,
|
||||
KnowledgeStore,
|
||||
SearchResult,
|
||||
_compute_relevance,
|
||||
)
|
||||
from monorepo.models import ScopeConfig
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def scope_config() -> ScopeConfig:
|
||||
"""Beispiel-ScopeConfig."""
|
||||
return ScopeConfig(scopes={
|
||||
"privat": {"scope": "privat", "paths": ["privat/"]},
|
||||
"dhive": {"scope": "dhive", "paths": ["dhive/"]},
|
||||
"bahn": {"scope": "bahn", "paths": ["bahn/"]},
|
||||
"shared": {"scope": "shared", "paths": ["shared/"]},
|
||||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_entries() -> list[IndexEntry]:
|
||||
"""Vielfältige Einträge zum Testen der Relevanz-Sortierung."""
|
||||
return [
|
||||
IndexEntry(
|
||||
id="bahn/decisions/api-design",
|
||||
title="API-Designprinzipien für Microservices",
|
||||
type="decision",
|
||||
tags=["api", "microservices", "architecture"],
|
||||
scope="bahn",
|
||||
summary="REST-API-Konventionen für DB-InfraGO-Dienste",
|
||||
path="bahn/decisions/api-design.md",
|
||||
content_hash="sha256:abc123",
|
||||
links=[],
|
||||
),
|
||||
IndexEntry(
|
||||
id="privat/notes/rest-patterns",
|
||||
title="REST-Patterns Notizen",
|
||||
type="note",
|
||||
tags=["api", "rest", "patterns"],
|
||||
scope="privat",
|
||||
summary="Sammlung bewährter REST-Patterns",
|
||||
path="privat/notes/rest-patterns.md",
|
||||
content_hash="sha256:def456",
|
||||
links=[],
|
||||
),
|
||||
IndexEntry(
|
||||
id="dhive/reference/cloud-native",
|
||||
title="Cloud-Native Architektur Guide",
|
||||
type="reference",
|
||||
tags=["cloud", "kubernetes", "architecture"],
|
||||
scope="dhive",
|
||||
summary="Leitfaden für cloud-native Anwendungen bei dhive",
|
||||
path="dhive/reference/cloud-native.md",
|
||||
content_hash="sha256:ghi789",
|
||||
links=[],
|
||||
),
|
||||
IndexEntry(
|
||||
id="shared/patterns/api-versioning",
|
||||
title="API Versioning",
|
||||
type="pattern",
|
||||
tags=["api", "versioning", "best-practices"],
|
||||
scope="shared",
|
||||
summary="Best Practices für API-Versionierung",
|
||||
path="shared/patterns/api-versioning.md",
|
||||
content_hash="sha256:jkl012",
|
||||
links=[],
|
||||
),
|
||||
IndexEntry(
|
||||
id="bahn/notes/kubernetes-deployment",
|
||||
title="Kubernetes Deployment Notizen",
|
||||
type="note",
|
||||
tags=["kubernetes", "deployment", "infrastructure"],
|
||||
scope="bahn",
|
||||
summary="Notizen zum Kubernetes-Deployment bei DB-InfraGO",
|
||||
path="bahn/notes/kubernetes-deployment.md",
|
||||
content_hash="sha256:mno345",
|
||||
links=[],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store_with_entries(
|
||||
tmp_path: Path, scope_config: ScopeConfig, sample_entries: list[IndexEntry]
|
||||
) -> KnowledgeStore:
|
||||
"""KnowledgeStore mit vorgeladenen Beispiel-Einträgen."""
|
||||
store = KnowledgeStore(tmp_path, scope_config)
|
||||
for entry in sample_entries:
|
||||
store.index.update_entry(entry)
|
||||
return store
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SearchResult Dataclass Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSearchResult:
|
||||
"""Tests für die SearchResult Dataclass."""
|
||||
|
||||
def test_default_values(self) -> None:
|
||||
"""SearchResult hat korrekte Standardwerte."""
|
||||
entry = IndexEntry(id="test", title="Test", type="note", scope="privat")
|
||||
result = SearchResult(entry=entry)
|
||||
assert result.entry is entry
|
||||
assert result.relevance_score == 0.0
|
||||
assert result.partial_results is False
|
||||
|
||||
def test_custom_values(self) -> None:
|
||||
"""SearchResult akzeptiert benutzerdefinierte Werte."""
|
||||
entry = IndexEntry(id="test", title="Test", type="note", scope="privat")
|
||||
result = SearchResult(entry=entry, relevance_score=0.85, partial_results=True)
|
||||
assert result.relevance_score == 0.85
|
||||
assert result.partial_results is True
|
||||
|
||||
def test_relevance_score_range(self) -> None:
|
||||
"""relevance_score liegt zwischen 0.0 und 1.0."""
|
||||
entry = IndexEntry(id="test", title="Test", type="note", scope="privat")
|
||||
# Grenzwerte
|
||||
result_zero = SearchResult(entry=entry, relevance_score=0.0)
|
||||
result_one = SearchResult(entry=entry, relevance_score=1.0)
|
||||
assert result_zero.relevance_score == 0.0
|
||||
assert result_one.relevance_score == 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Relevanz-Scoring Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestRelevanceScoring:
|
||||
"""Tests für die Relevanz-Score-Berechnung."""
|
||||
|
||||
def test_title_match_highest_score(self) -> None:
|
||||
"""Title-Match ergibt höchsten Score (>= 0.8)."""
|
||||
entry = IndexEntry(
|
||||
id="test/doc",
|
||||
title="API Design",
|
||||
type="note",
|
||||
tags=["documentation"],
|
||||
scope="bahn",
|
||||
summary="Allgemeine Informationen",
|
||||
)
|
||||
score = _compute_relevance("api design", entry)
|
||||
assert score >= 0.8
|
||||
|
||||
def test_exact_title_match_maximum_score(self) -> None:
|
||||
"""Exakter Title-Match ergibt Score 1.0."""
|
||||
entry = IndexEntry(
|
||||
id="test/doc",
|
||||
title="API Design",
|
||||
type="note",
|
||||
tags=["misc"],
|
||||
scope="bahn",
|
||||
summary="Irgendwas",
|
||||
)
|
||||
score = _compute_relevance("api design", entry)
|
||||
assert score == 1.0
|
||||
|
||||
def test_tag_match_medium_score(self) -> None:
|
||||
"""Tag-Match ergibt mittleren Score (0.6 - 0.79)."""
|
||||
entry = IndexEntry(
|
||||
id="test/doc",
|
||||
title="Allgemeines Dokument",
|
||||
type="note",
|
||||
tags=["kubernetes", "deployment"],
|
||||
scope="bahn",
|
||||
summary="Nicht relevanter Text",
|
||||
)
|
||||
score = _compute_relevance("kubernetes", entry)
|
||||
assert 0.6 <= score <= 0.79
|
||||
|
||||
def test_summary_match_lower_score(self) -> None:
|
||||
"""Summary-Match ergibt niedrigeren Score (0.4 - 0.59)."""
|
||||
entry = IndexEntry(
|
||||
id="test/doc",
|
||||
title="Allgemeines Dokument",
|
||||
type="note",
|
||||
tags=["misc"],
|
||||
scope="bahn",
|
||||
summary="Informationen über Kubernetes-Cluster",
|
||||
)
|
||||
score = _compute_relevance("kubernetes", entry)
|
||||
assert 0.4 <= score <= 0.59
|
||||
|
||||
def test_id_match_lowest_score(self) -> None:
|
||||
"""ID-Match ergibt niedrigsten Score (0.2 - 0.39)."""
|
||||
entry = IndexEntry(
|
||||
id="bahn/notes/kubernetes-setup",
|
||||
title="Allgemeines Dokument",
|
||||
type="note",
|
||||
tags=["misc"],
|
||||
scope="bahn",
|
||||
summary="Nicht relevanter Text",
|
||||
)
|
||||
score = _compute_relevance("kubernetes", entry)
|
||||
assert 0.2 <= score <= 0.39
|
||||
|
||||
def test_no_match_zero_score(self) -> None:
|
||||
"""Kein Match ergibt Score 0.0."""
|
||||
entry = IndexEntry(
|
||||
id="test/doc",
|
||||
title="Allgemeines Dokument",
|
||||
type="note",
|
||||
tags=["misc"],
|
||||
scope="bahn",
|
||||
summary="Nicht relevanter Text",
|
||||
)
|
||||
score = _compute_relevance("zyxwv", entry)
|
||||
assert score == 0.0
|
||||
|
||||
def test_title_beats_tags(self) -> None:
|
||||
"""Title-Match hat Vorrang vor Tag-Match."""
|
||||
entry_title = IndexEntry(
|
||||
id="test/a", title="Kubernetes", type="note",
|
||||
tags=["misc"], scope="bahn", summary="Anderes",
|
||||
)
|
||||
entry_tag = IndexEntry(
|
||||
id="test/b", title="Anderes Dokument", type="note",
|
||||
tags=["kubernetes"], scope="bahn", summary="Anderes",
|
||||
)
|
||||
score_title = _compute_relevance("kubernetes", entry_title)
|
||||
score_tag = _compute_relevance("kubernetes", entry_tag)
|
||||
assert score_title > score_tag
|
||||
|
||||
def test_tags_beat_summary(self) -> None:
|
||||
"""Tag-Match hat Vorrang vor Summary-Match."""
|
||||
entry_tag = IndexEntry(
|
||||
id="test/a", title="Anderes", type="note",
|
||||
tags=["kubernetes"], scope="bahn", summary="Anderer Text",
|
||||
)
|
||||
entry_summary = IndexEntry(
|
||||
id="test/b", title="Anderes", type="note",
|
||||
tags=["misc"], scope="bahn", summary="Kubernetes ist wichtig",
|
||||
)
|
||||
score_tag = _compute_relevance("kubernetes", entry_tag)
|
||||
score_summary = _compute_relevance("kubernetes", entry_summary)
|
||||
assert score_tag > score_summary
|
||||
|
||||
def test_summary_beats_id(self) -> None:
|
||||
"""Summary-Match hat Vorrang vor ID-Match."""
|
||||
entry_summary = IndexEntry(
|
||||
id="test/other", title="Anderes", type="note",
|
||||
tags=["misc"], scope="bahn", summary="Kubernetes Deployment Guide",
|
||||
)
|
||||
entry_id = IndexEntry(
|
||||
id="bahn/kubernetes/setup", title="Anderes", type="note",
|
||||
tags=["misc"], scope="bahn", summary="Anderer Text",
|
||||
)
|
||||
score_summary = _compute_relevance("kubernetes", entry_summary)
|
||||
score_id = _compute_relevance("kubernetes", entry_id)
|
||||
assert score_summary > score_id
|
||||
|
||||
def test_case_insensitive(self) -> None:
|
||||
"""Relevanz-Berechnung ist case-insensitive bezüglich Entry-Feldern.
|
||||
|
||||
Die Funktion erwartet query_lower als bereits lowercase Input.
|
||||
Die Entry-Felder werden intern lowercase verglichen.
|
||||
"""
|
||||
entry_upper = IndexEntry(
|
||||
id="test/doc", title="API DESIGN", type="note",
|
||||
tags=[], scope="bahn", summary="",
|
||||
)
|
||||
entry_lower = IndexEntry(
|
||||
id="test/doc", title="api design", type="note",
|
||||
tags=[], scope="bahn", summary="",
|
||||
)
|
||||
# Gleicher Query (lowercase) gegen verschiedene Entry-Schreibweisen
|
||||
score_upper_entry = _compute_relevance("api design", entry_upper)
|
||||
score_lower_entry = _compute_relevance("api design", entry_lower)
|
||||
assert score_upper_entry == score_lower_entry
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# KnowledgeStore.search() Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestKnowledgeStoreSearch:
|
||||
"""Tests für KnowledgeStore.search() mit Relevanz-Sortierung."""
|
||||
|
||||
def test_returns_search_results(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""search() liefert SearchResult-Objekte."""
|
||||
results = store_with_entries.search("api", ["bahn", "privat", "shared"])
|
||||
assert len(results) > 0
|
||||
for r in results:
|
||||
assert isinstance(r, SearchResult)
|
||||
assert isinstance(r.entry, IndexEntry)
|
||||
assert isinstance(r.relevance_score, float)
|
||||
assert isinstance(r.partial_results, bool)
|
||||
|
||||
def test_sorted_by_relevance_descending(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Ergebnisse sind nach Relevanz absteigend sortiert."""
|
||||
results = store_with_entries.search("api", ["bahn", "privat", "shared", "dhive"])
|
||||
scores = [r.relevance_score for r in results]
|
||||
assert scores == sorted(scores, reverse=True)
|
||||
|
||||
def test_title_match_ranked_higher(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Einträge mit Title-Match werden höher gerankt als Tag-Matches."""
|
||||
results = store_with_entries.search(
|
||||
"api versioning", ["bahn", "privat", "shared", "dhive"]
|
||||
)
|
||||
# "API Versioning" hat exakten Title-Match
|
||||
if results:
|
||||
assert results[0].entry.id == "shared/patterns/api-versioning"
|
||||
|
||||
def test_empty_query_returns_empty(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Leerer Query liefert leere Ergebnisliste."""
|
||||
results = store_with_entries.search("", ["bahn", "privat", "shared", "dhive"])
|
||||
assert results == []
|
||||
|
||||
def test_no_match_returns_empty(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Query ohne Treffer liefert leere Liste."""
|
||||
results = store_with_entries.search("zyxwvutsrq", ["bahn", "privat", "shared", "dhive"])
|
||||
assert results == []
|
||||
|
||||
def test_relevance_scores_within_range(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Alle Relevanz-Scores liegen zwischen 0.0 und 1.0."""
|
||||
results = store_with_entries.search("api", ["bahn", "privat", "shared", "dhive"])
|
||||
for r in results:
|
||||
assert 0.0 <= r.relevance_score <= 1.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scope-Filterung Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSearchScopeFiltering:
|
||||
"""Tests für Scope-basierte Filterung in der Suche."""
|
||||
|
||||
def test_only_authorized_scopes_returned(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Nur Ergebnisse aus autorisierten Scopes werden zurückgegeben."""
|
||||
results = store_with_entries.search("api", ["bahn"])
|
||||
for r in results:
|
||||
assert r.entry.scope == "bahn"
|
||||
|
||||
def test_unauthorized_scope_not_in_results(
|
||||
self, store_with_entries: KnowledgeStore
|
||||
) -> None:
|
||||
"""Artefakte aus nicht-autorisierten Scopes erscheinen nicht."""
|
||||
# dhive-Scope nicht autorisiert, aber "cloud" existiert dort
|
||||
results = store_with_entries.search("cloud", ["bahn", "privat"])
|
||||
for r in results:
|
||||
assert r.entry.scope != "dhive"
|
||||
|
||||
def test_existence_not_revealed(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Existenz nicht-autorisierter Artefakte wird nicht offengelegt.
|
||||
|
||||
Requirements: 3.9 – unberechtigte Artefakte aus Suchergebnissen
|
||||
ausschließen, ohne deren Existenz offenzulegen.
|
||||
"""
|
||||
# "cloud-native" existiert im dhive-Scope
|
||||
results_restricted = store_with_entries.search("cloud-native", ["bahn", "privat"])
|
||||
results_all = store_with_entries.search("cloud-native", ["bahn", "privat", "dhive"])
|
||||
|
||||
# Ohne dhive-Berechtigung: Kein Hinweis auf Existenz
|
||||
assert len(results_restricted) == 0
|
||||
# Mit dhive-Berechtigung: Ergebnis vorhanden
|
||||
assert len(results_all) >= 1
|
||||
|
||||
def test_empty_scopes_returns_empty(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Leere Scope-Liste liefert keine Ergebnisse."""
|
||||
results = store_with_entries.search("api", [])
|
||||
assert results == []
|
||||
|
||||
def test_multiple_scopes(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Suche über mehrere Scopes liefert kombinierte Ergebnisse."""
|
||||
results = store_with_entries.search("api", ["bahn", "shared"])
|
||||
scopes_found = {r.entry.scope for r in results}
|
||||
# Beide Scopes sollten vertreten sein
|
||||
assert "bahn" in scopes_found or "shared" in scopes_found
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Timeout-Handling Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSearchTimeout:
|
||||
"""Tests für das Timeout-Handling der Suche."""
|
||||
|
||||
def test_default_timeout_is_5_seconds(self) -> None:
|
||||
"""Standard-Timeout ist 5 Sekunden."""
|
||||
assert SEARCH_TIMEOUT_SECONDS == 5.0
|
||||
|
||||
def test_no_timeout_under_normal_conditions(
|
||||
self, store_with_entries: KnowledgeStore
|
||||
) -> None:
|
||||
"""Unter normalen Bedingungen kein Timeout (partial_results=False)."""
|
||||
results = store_with_entries.search("api", ["bahn", "privat", "shared", "dhive"])
|
||||
for r in results:
|
||||
assert r.partial_results is False
|
||||
|
||||
def test_timeout_marks_partial_results(
|
||||
self, tmp_path: Path, scope_config: ScopeConfig
|
||||
) -> None:
|
||||
"""Bei Timeout werden alle Ergebnisse als partial markiert."""
|
||||
store = KnowledgeStore(tmp_path, scope_config)
|
||||
|
||||
# Viele Einträge erstellen
|
||||
for i in range(100):
|
||||
entry = IndexEntry(
|
||||
id=f"bahn/test/entry-{i}",
|
||||
title=f"Testartikel {i} über API",
|
||||
type="note",
|
||||
tags=["api", "test"],
|
||||
scope="bahn",
|
||||
summary=f"Zusammenfassung für Entry {i}",
|
||||
path=f"bahn/test/entry-{i}.md",
|
||||
content_hash=f"sha256:hash{i}",
|
||||
)
|
||||
store.index.update_entry(entry)
|
||||
|
||||
# Timeout auf 0 Sekunden simulieren
|
||||
results = store.search("api", ["bahn"], timeout=0.0)
|
||||
|
||||
# Entweder keine Ergebnisse (sofort Timeout) oder alle partial
|
||||
if results:
|
||||
for r in results:
|
||||
assert r.partial_results is True
|
||||
|
||||
def test_custom_timeout_parameter(
|
||||
self, tmp_path: Path, scope_config: ScopeConfig
|
||||
) -> None:
|
||||
"""Benutzerdefinierter Timeout wird respektiert."""
|
||||
store = KnowledgeStore(tmp_path, scope_config)
|
||||
entry = IndexEntry(
|
||||
id="bahn/test/entry",
|
||||
title="API Test",
|
||||
type="note",
|
||||
tags=["api"],
|
||||
scope="bahn",
|
||||
summary="Test",
|
||||
path="bahn/test/entry.md",
|
||||
content_hash="sha256:test",
|
||||
)
|
||||
store.index.update_entry(entry)
|
||||
|
||||
# Großzügiger Timeout: Normale Ergebnisse
|
||||
results = store.search("api", ["bahn"], timeout=10.0)
|
||||
assert len(results) == 1
|
||||
assert results[0].partial_results is False
|
||||
|
||||
def test_timeout_preserves_found_results(
|
||||
self, tmp_path: Path, scope_config: ScopeConfig
|
||||
) -> None:
|
||||
"""Bei Timeout werden bereits gefundene Teilergebnisse beibehalten."""
|
||||
store = KnowledgeStore(tmp_path, scope_config)
|
||||
|
||||
# Viele Einträge erstellen
|
||||
for i in range(1000):
|
||||
entry = IndexEntry(
|
||||
id=f"bahn/test/entry-{i}",
|
||||
title=f"Testartikel {i} über API",
|
||||
type="note",
|
||||
tags=["api", "test"],
|
||||
scope="bahn",
|
||||
summary=f"Zusammenfassung für Entry {i}",
|
||||
path=f"bahn/test/entry-{i}.md",
|
||||
content_hash=f"sha256:hash{i}",
|
||||
)
|
||||
store.index.update_entry(entry)
|
||||
|
||||
# Minimaler Timeout: Sollte Teilergebnisse liefern
|
||||
# Da die Iteration auf einer modernen Maschine schnell ist,
|
||||
# mocken wir time.monotonic für einen kontrollierten Test
|
||||
call_count = 0
|
||||
real_monotonic = time.monotonic
|
||||
|
||||
def fake_monotonic() -> float:
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
# Nach 5 Aufrufen "Timeout" simulieren
|
||||
if call_count > 5:
|
||||
return real_monotonic() + 10.0
|
||||
return real_monotonic()
|
||||
|
||||
with patch("monorepo.knowledge.store.time.monotonic", side_effect=fake_monotonic):
|
||||
results = store.search("api", ["bahn"], timeout=5.0)
|
||||
|
||||
# Sollte Teilergebnisse haben (nicht alle 1000)
|
||||
if results:
|
||||
assert all(r.partial_results is True for r in results)
|
||||
assert len(results) < 1000
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Integration Tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestSearchIntegration:
|
||||
"""Integrationstests für die Volltextsuche."""
|
||||
|
||||
def test_search_finds_by_title(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Findet Einträge über Titel-Suche."""
|
||||
results = store_with_entries.search(
|
||||
"Cloud-Native Architektur", ["dhive"]
|
||||
)
|
||||
assert len(results) >= 1
|
||||
assert results[0].entry.id == "dhive/reference/cloud-native"
|
||||
assert results[0].relevance_score >= 0.8 # Title match
|
||||
|
||||
def test_search_finds_by_tag(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Findet Einträge über Tag-Suche."""
|
||||
results = store_with_entries.search("deployment", ["bahn"])
|
||||
assert len(results) >= 1
|
||||
assert any(r.entry.id == "bahn/notes/kubernetes-deployment" for r in results)
|
||||
|
||||
def test_search_finds_by_summary(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Findet Einträge über Summary-Suche."""
|
||||
results = store_with_entries.search("bewährter", ["privat"])
|
||||
assert len(results) == 1
|
||||
assert results[0].entry.id == "privat/notes/rest-patterns"
|
||||
|
||||
def test_case_insensitive_search(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Suche ist case-insensitive."""
|
||||
results_lower = store_with_entries.search("kubernetes", ["bahn", "dhive"])
|
||||
results_upper = store_with_entries.search("KUBERNETES", ["bahn", "dhive"])
|
||||
assert len(results_lower) == len(results_upper)
|
||||
|
||||
def test_search_with_all_scopes(self, store_with_entries: KnowledgeStore) -> None:
|
||||
"""Suche über alle Scopes liefert alle Treffer."""
|
||||
all_scopes = ["bahn", "privat", "dhive", "shared"]
|
||||
results = store_with_entries.search("api", all_scopes)
|
||||
# api kommt in mehreren Einträgen vor (title, tags)
|
||||
assert len(results) >= 3
|
||||
Reference in New Issue
Block a user