79 lines
4.6 KiB
Python
79 lines
4.6 KiB
Python
from pathlib import Path
|
|
from brain.research import get_brief, ORPHAN_PARENT
|
|
from brain.index_store import build_index
|
|
from brain.cards import parse_card
|
|
|
|
def fake_embed(text):
|
|
t = text.lower()
|
|
if "имплант" in t or "зуб" in t or "стоматолог" in t:
|
|
return [0.98, 0.05, 0.0]
|
|
if "медиц" in t or "клиник" in t or "маммолог" in t:
|
|
return [0.6, 0.6, 0.0]
|
|
return [0.0, 0.0, 1.0]
|
|
|
|
def make_vault(tmp_path):
|
|
nishi = tmp_path / "nishi"
|
|
(nishi / "медицина").mkdir(parents=True)
|
|
(nishi / "медицина.md").write_text(
|
|
"---\nnisha: Медицина\nsinonimy: [клиника]\n---\nОбщее: доверие и репутация.",
|
|
encoding="utf-8")
|
|
(nishi / "медицина" / "стоматология.md").write_text(
|
|
"---\nnisha: стоматология / имплантация\nroditel: Медицина\n"
|
|
"sinonimy: [зубной, импланты]\n---\n## Специфика\nОстрая боль срочно.\n## Тренды\nДольше выбирают.",
|
|
encoding="utf-8")
|
|
return nishi
|
|
|
|
def test_niche_hit_composes_and_no_sonar(tmp_path):
|
|
nishi = make_vault(tmp_path)
|
|
index = build_index(nishi, embed_fn=fake_embed)
|
|
called = {"sonar": 0}
|
|
def sonar(niche): called["sonar"] += 1; return "НЕ ДОЛЖНО ВЫЗВАТЬСЯ"
|
|
brief, source = get_brief("хочу импланты", nishi_dir=nishi, index=index,
|
|
embed_fn=fake_embed, sonar_fn=sonar)
|
|
assert called["sonar"] == 0
|
|
assert source == "niche"
|
|
assert "доверие" in brief and "Острая боль" in brief
|
|
|
|
def test_domain_hit_returns_parent_common(tmp_path):
|
|
nishi = make_vault(tmp_path)
|
|
index = build_index(nishi, embed_fn=fake_embed)
|
|
def sonar(niche): return "спец по маммологии"
|
|
brief, source = get_brief("маммология", nishi_dir=nishi, index=index,
|
|
embed_fn=fake_embed, sonar_fn=sonar, sonar_on_domain=False)
|
|
assert source == "domain"
|
|
assert "доверие" in brief
|
|
|
|
def test_miss_calls_sonar_writes_orphan_card_and_grows_index(tmp_path):
|
|
nishi = make_vault(tmp_path)
|
|
index = build_index(nishi, embed_fn=fake_embed)
|
|
n_before = len(index)
|
|
def sonar(niche): return f"свежая справка про {niche}"
|
|
brief, source = get_brief("грузоперевозки фурами", nishi_dir=nishi, index=index,
|
|
embed_fn=fake_embed, sonar_fn=sonar)
|
|
assert source == "miss"
|
|
assert "грузоперевозки" in brief
|
|
# карточка завелась на диск и помечена как сиротка (НЕ домен)
|
|
created = [p for p in nishi.rglob("*.md") if "груз" in p.read_text(encoding="utf-8").lower()]
|
|
assert created, "карточка-сиротка не создана"
|
|
assert parse_card(created[0]).roditel == ORPHAN_PARENT
|
|
# индекс подрос ровно на новую карточку (без полной пересборки)
|
|
assert len(index) == n_before + 1
|
|
|
|
def test_miss_does_not_overwrite_existing_card(tmp_path):
|
|
import copy
|
|
nishi = make_vault(tmp_path)
|
|
# два НЕЗАВИСИМЫХ снимка одного и того же (устаревшего) индекса — имитация гонки:
|
|
# два вызова стартуют с индекса ДО первой записи, оба классифицируют как miss.
|
|
# Общий/растущий индекс тут не подходит: с Фикс1 (точный синоним) + Фикс3/4 (точечный
|
|
# рост индекса) повторный ОДИНАКОВЫЙ запрос находит только что созданную карточку по
|
|
# имени и переиспользует её (это и есть желаемое поведение кеша) — так и должно быть.
|
|
base_index = build_index(nishi, embed_fn=fake_embed)
|
|
def sonar(niche): return "справка 1"
|
|
get_brief("грузоперевозки", nishi_dir=nishi, index=copy.deepcopy(base_index),
|
|
embed_fn=fake_embed, sonar_fn=sonar)
|
|
def sonar2(niche): return "справка 2"
|
|
get_brief("грузоперевозки", nishi_dir=nishi, index=copy.deepcopy(base_index),
|
|
embed_fn=fake_embed, sonar_fn=sonar2)
|
|
files = [p for p in nishi.rglob("*.md") if "груз" in p.read_text(encoding="utf-8").lower()]
|
|
assert len(files) == 2 # вторая не затёрла первую (защита в _write_card сработала)
|