Files
ely-eye/backend/tests/test_vector_index.py
ZacharyZhang-NY f8a9428f5a Ask the repository that owns the path, and pin the wirings that had no test
Round four of the independent review found seven record and coverage
defects. The functional side it verified by running; these are the fixes.

- `_tracked` asked the current checkout's index, so a reviewer in a worktree
  pointing at the main checkout's committed report was told it was untracked.
  It now asks the repository that owns the path, and the guard no longer
  compares directories: any tracked file is committed evidence.
- `retrieval.py` claimed the dense index "catches up inside search". It does
  not: `_stale` is only used by `sync()` and `stats()`, so a store upgraded
  from v11 ranks an empty dense leg until `ely-eye build-dense-index` runs.
  The docstring now states that, and `doctor` already names it.
- Four wirings had no discriminating test, each proven by mutation:
  a stubbed `_tracked` (now a test uses the real git index), `copy_store`
  claiming the scratch (now a live holder blocks the copy before a byte is
  copied), `_stale` reading the atoms table (now a closed memory that loses
  its vector is owed one), and the v10/v11 to v12 `DROP VIEW` gate (now a
  hand-made v11 store is upgraded and its view body re-checked).
- The record corrected against the reviewer's measurements: the five
  mutations did not each turn one test red (M1 turned two, M3 turned two,
  `_stale` alone turned none), the pyright/mypy file set was wrong, and the
  round added seven tests rather than three.

Gates: 422 passed; ruff clean; filesize clean; audit clean; mypy 24 errors in
9 files repo-wide, equal to the 9cb46c1 baseline; pyright 0 on the nine
touched files; zero whole-line comment blocks over 157 tracked files.
2026-10-10 01:59:43 -04:00

314 lines
14 KiB
Python

"""The dense index answers from the database, so a memory is findable the moment it is written.
Every test here runs with a stand-in model: what is being proved is the index's freshness and
its boundaries, not the quality of a 500 MB snapshot.
"""
from __future__ import annotations
from collections.abc import Callable
from datetime import UTC, datetime, timedelta
from pathlib import Path
from typing import Any
import pytest
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.memory import MemoryService
from ely_eye.retrieval import RetrievalService
from ely_eye.schemas import MemoryOrigin, SearchFilters
from ely_eye.vector_index import VectorIndex, expected_vectors
KEYS = "我们的钥匙放在玄关抽屉里"
PASSPORT = "护照在书房第二个抽屉"
def _settings(tmp_path: Path, model_id: str = "hash-mini") -> Settings:
return Settings(home=tmp_path, dense_embeddings_enabled=True, embedding_model_id=model_id)
def _index(settings: Settings, database: Database, fake_embeddings: Callable[[], Any]) -> VectorIndex:
return VectorIndex(settings, database, fake_embeddings())
def _memory(settings: Settings, database: Database, index: VectorIndex) -> MemoryService:
return MemoryService(settings, database, vectors=index)
def test_a_memory_written_now_is_findable_now(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
_memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
hits = index.search(index.embeddings.embed_query(KEYS), 10)
assert list(hits) == ["mem-" + _digest(KEYS, "wife") + ":text"], "no rebuild step stands between writing and finding"
def _digest(statement: str, holder: str) -> str:
import hashlib
return hashlib.sha256(f"{holder}\n{statement}".encode()).hexdigest()[:16]
def test_a_closed_memory_stops_answering(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
old = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
assert index.search(index.embeddings.embed_query(KEYS), 10) == {old.atom_id: pytest.approx(1.0)}
replacement = memory.supersede(old.atom_id, text=PASSPORT, holder_id="wife")
after = index.search(index.embeddings.embed_query(KEYS), 10)
assert old.atom_id not in after, "a closed memory stops answering"
assert set(after) == {replacement.atom_id}
def test_a_rewritten_memory_is_encoded_again(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
atom = _memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
index.sync()
rewritten = atom.model_copy(update={"text": PASSPORT})
database.upsert_atoms([rewritten])
index.sync()
stale = index.search(index.embeddings.embed_query(KEYS), 10)
fresh = index.search(index.embeddings.embed_query(PASSPORT), 10)
assert stale[rewritten.atom_id] < 0.9, "the old wording still holds the old vector"
assert fresh[rewritten.atom_id] > 0.99
def test_a_forgetting_leaves_no_vector_behind(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
index.sync()
report = memory.forget(atom_id="mem-" + _digest(KEYS, "wife") + ":text", reason="moved house")
assert report.vectors_dropped == 1
assert index.stats()["vectors"] == 0
def test_somebody_elses_memory_is_not_mine_to_find(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
hers_atom = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
his_atom = memory.remember(PASSPORT, holder_id="husband", origin=MemoryOrigin.self_)
index.sync()
hers = index.search(index.embeddings.embed_query(KEYS), 10, as_holder="wife")
his = index.search(index.embeddings.embed_query(KEYS), 10, as_holder="husband")
assert set(hers) == {hers_atom.atom_id}
assert set(his) == {his_atom.atom_id}, "he answers from his own memory, not from hers"
def test_a_shared_memory_is_findable_by_both(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
from ely_eye.schemas import Visibility
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
memory.remember(
KEYS, holder_id="wife", origin=MemoryOrigin.self_, visibility=Visibility(scope="visible_to", holders=["husband"])
)
index.sync()
assert index.search(index.embeddings.embed_query(KEYS), 10, as_holder="wife")
assert index.search(index.embeddings.embed_query(KEYS), 10, as_holder="husband")
assert index.search(index.embeddings.embed_query(KEYS), 10, as_holder="daughter") == {}
def test_a_cartridge_scope_cannot_be_walked_around(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
from datetime import UTC, datetime
from ely_eye import cartridge_store
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
kept = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
other = memory.remember(PASSPORT, holder_id="wife", origin=MemoryOrigin.self_)
index.sync()
database.write_cartridge(
cartridge_id="cart-1",
name="household",
root_path=tmp_path / "cart-1",
manifest_json="{}",
dna="d" * 64,
token_equivalent=1,
created_at=datetime(2026, 5, 1, tzinfo=UTC),
)
cartridge_store.add_membership(database, "cart-1", [kept.atom_id])
hits = index.search(index.embeddings.embed_query(KEYS + " " + PASSPORT), 10, cartridge_ids=["cart-1"])
assert set(hits) == {kept.atom_id}, "the scope is applied inside the engine, not after it"
assert other.atom_id not in hits
def test_a_different_model_rebuilds_instead_of_mixing_vectors(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
_memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
index.sync()
assert index.stats()["vectors"] == 1
swapped = VectorIndex(_settings(tmp_path, model_id="other-model"), database, fake_embeddings())
swapped.sync()
facts = swapped.stats()
assert facts["model"] == "other-model"
assert facts["vectors"] == 1, "the table was rebuilt, not appended to"
def test_a_picture_with_no_words_is_the_other_indexs_job(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
from datetime import UTC, datetime
from ely_eye.schemas import EvidenceAtom, Modality, SourceRecord, TrustScores
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
now = datetime(2026, 5, 1, tzinfo=UTC)
database.upsert_sources(
[
SourceRecord(
source_id="pic-1",
path="/tmp/pic.png",
kind="image",
mime="image/png",
sha256="0" * 64,
size_bytes=4,
created_at=now,
)
]
)
database.upsert_atoms(
[
EvidenceAtom(
atom_id="pic-1:image",
modality=Modality.image,
source_id="pic-1",
source="pic-1",
ingested_at=now,
text="",
image_ref="pic-1",
trust=TrustScores(parser=1.0),
token_equivalent=1,
holder_id="wife",
)
]
)
assert index.sync()["encoded"] == 0
assert expected_vectors(database) == 0
assert index.stats()["vectors"] == 0, "a wordless picture is the visual index's job"
def test_the_index_survives_a_reopen(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings = _settings(tmp_path)
first = Database(settings)
index = _index(settings, first, fake_embeddings)
_memory(settings, first, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
index.sync()
reopened = VectorIndex(settings, Database(settings), fake_embeddings())
assert reopened.stats()["vectors"] == 1
assert reopened.search(reopened.embeddings.embed_query(KEYS), 10)
def test_a_superseded_memory_keeps_its_vector_but_leaves_the_current_answer_alone(
tmp_path: Path, fake_embeddings: Callable[[], Any]
) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
old = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
replacement = memory.supersede(old.atom_id, text=PASSPORT, holder_id="wife")
facts = index.stats()
assert facts["vectors"] == 2, "a closed memory keeps its vector, or a question about the past cannot reach it"
assert facts["pending"] == 0
current = index.search(index.embeddings.embed_query(KEYS), 10, as_holder="wife")
assert list(current) == [replacement.atom_id], "asked now, only the replacement answers"
then = index.search(index.embeddings.embed_query(KEYS), 10, as_holder="wife", as_of=old.ingested_at.isoformat())
assert list(then) == [old.atom_id], "asked before the retirement, the old memory is still findable"
def test_a_closed_memory_owes_the_index_a_vector_even_though_it_answers_nothing_now(
tmp_path: Path, fake_embeddings: Callable[[], Any]
) -> None:
"""`sync` walks the atoms table, not the current view, or an as-of question would find nothing."""
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
memory = _memory(settings, database, index)
old = memory.remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
memory.supersede(old.atom_id, text=PASSPORT, holder_id="wife")
index.drop_atoms([old.atom_id])
assert index.stats()["pending"] == 1, "the closed memory answers nothing now, but the index still owes it a vector"
assert index.sync()["encoded"] == 1, "sync re-encodes the closed memory rather than skipping it"
assert index.stats()["vectors"] == 2
def test_a_store_that_never_built_its_index_says_so_instead_of_stalling_a_search(
tmp_path: Path, fake_embeddings: Callable[[], Any]
) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
atom = _memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
index.drop_atoms([atom.atom_id])
facts = index.stats()
assert (facts["vectors"], facts["expected"], facts["pending"]) == (0, 1, 1)
assert index.search(index.embeddings.embed_query(KEYS), 10) == {}, "a search never rebuilds"
assert index.sync()["encoded"] == 1
assert index.search(index.embeddings.embed_query(KEYS), 10)
def test_the_dense_leg_hides_what_the_view_hides(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
"""The search repeats the view's currency rules, so the two must keep answering alike."""
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
atom = _memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
lapsed = (datetime.now(UTC) - timedelta(days=1)).isoformat()
with database.connect() as connection:
connection.execute("UPDATE atoms SET retention_until = ? WHERE atom_id = ?", (lapsed, atom.atom_id))
visible = [row["atom_id"] for row in connection.execute("SELECT atom_id FROM v_atoms_current")]
assert atom.atom_id not in visible, "the view has retired the memory"
assert index.stats()["vectors"] == 1, "the index still holds its vector"
assert index.search(index.embeddings.embed_query(KEYS), 10) == {}, "a search retires it too"
def test_a_query_asks_for_a_real_number_of_answers(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
index = _index(settings, database, fake_embeddings)
with pytest.raises(ValueError, match="positive limit"):
index.search(index.embeddings.embed_query(KEYS), 0)
def test_search_answers_from_the_index_without_a_rebuild(tmp_path: Path, fake_embeddings: Callable[[], Any]) -> None:
settings, database = _settings(tmp_path), Database(_settings(tmp_path))
embeddings = fake_embeddings()
service = RetrievalService(settings, database)
service.embeddings = embeddings # type: ignore[assignment]
index = VectorIndex(settings, database, embeddings)
service.vectors = index
_memory(settings, database, index).remember(KEYS, holder_id="wife", origin=MemoryOrigin.self_)
result = service.search(KEYS, filters=SearchFilters(as_holder="wife"))
assert result.hits, "the needle was written and found in the same breath"
assert result.hits[0].atom.atom_id == "mem-" + _digest(KEYS, "wife") + ":text"
assert result.hits[0].dense_score > 0.0