Files
ely-eye/backend/tests/test_as_of_search.py
T
ZacharyZhang-NY eab6bd7c8c Answer from the moment a question asks about
The key test in Gaps.md §6 asks what was true last week. It could not be
answered: search_atoms joined v_atoms_current, so a superseded memory is
unreachable no matter what since/until says — those two fields bound when a
memory was true, not which moment the store answers from.

SearchFilters.as_of moves that moment. Without it the words leg repeats the
view's currency predicate verbatim; with it a memory answers when
valid_from <= as_of < valid_to, its event time has arrived, and it is inside
its retention. archived_at is ignored on purpose: archival is a storage tier,
not a claim about the world, so an archived memory still answers for the time
before it was archived. The dense leg reads the same window through
vector_index._allowed, so the two legs cannot disagree about what existed.

scripts/key_test.sh runs the seven §6 scenarios end to end against one live
store — HTTP, the Go MCP binary over stdio, and the CLI mounting cartridges.
Six pass. The seventh, "a cartridge saves compute", is reported NOT MEASURED
rather than passed: it claims time-to-first-token and a cache-hit rate, no
generator has ever run here, and there is no compile or cache route to observe
even the second half.

Gates: pytest -q 406 passed; ruff clean; filesize OK; pyright 0 on the seven
touched files; mypy cold cache Success on the same seven; key test exit 0.
2026-10-09 18:25:30 -04:00

122 lines
5.5 KiB
Python

"""As-of search: what was true then, not what is true now.
The store keeps two clocks — when a memory was true and when the store learned it — so a
question about last week has to be answerable after the fact. These tests pin that the words
leg and the dense leg read the same window, and that `as_of` moves only the moment being asked
about, never who is allowed to see the answer.
"""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
import ely_eye.api as api_module
from ely_eye import lifecycle_store
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.memory import MemoryService
from ely_eye.schemas import MemoryOrigin, SearchFilters, TimePrecision
DRAWER = "钥匙放在玄关抽屉里"
CAR = "钥匙现在在车里"
WEEK = timedelta(days=9)
@pytest.fixture
def settings(tmp_path: Path) -> Settings:
return Settings(home=tmp_path, default_holder="wife", auth_token="test-token")
@pytest.fixture
def client(settings: Settings, monkeypatch: pytest.MonkeyPatch) -> TestClient:
monkeypatch.setattr(api_module, "get_settings", lambda: settings)
return TestClient(api_module.create_app(), headers={"Authorization": "Bearer test-token"})
def hits(client: TestClient, payload: dict) -> list[str]:
response = client.post("/api/search", json=payload)
assert response.status_code == 200, response.text
return [hit["atom"]["text"] for hit in response.json()["hits"]]
def store_the_pair(client: TestClient) -> tuple[str, datetime]:
"""The drawer memory is nine days old; the car replaces it now."""
week_ago = datetime.now(UTC) - WEEK
response = client.post(
"/api/memory",
json={"text": DRAWER, "event_time": week_ago.isoformat(), "event_time_precision": "day"},
)
assert response.status_code == 200, response.text
return response.json()["atom_id"], week_ago
def test_a_retired_memory_answers_for_the_time_it_was_true(client: TestClient) -> None:
atom_id, week_ago = store_the_pair(client)
superseded = client.post("/api/memory/supersede", json={"atom_id": atom_id, "text": CAR})
assert superseded.status_code == 200, superseded.text
assert hits(client, {"query": "钥匙"}) == [CAR], "asked now, the current memory answers"
asked_then = hits(client, {"query": "钥匙", "filters": {"as_of": (week_ago + timedelta(days=1)).isoformat()}})
assert asked_then == [DRAWER], "asked about last week, the retired memory answers"
def test_a_memory_that_had_not_happened_yet_does_not_answer(client: TestClient) -> None:
next_week = datetime.now(UTC) + timedelta(days=7)
response = client.post(
"/api/memory",
json={"text": CAR, "event_time": next_week.isoformat(), "event_time_precision": "day"},
)
assert response.status_code == 200, response.text
assert hits(client, {"query": "钥匙"}) == [CAR]
assert hits(client, {"query": "钥匙", "filters": {"as_of": datetime.now(UTC).isoformat()}}) == []
def test_an_archived_memory_still_answers_for_the_time_before_it_was_archived(
client: TestClient, settings: Settings
) -> None:
recorded = datetime.now(UTC) - timedelta(days=30)
response = client.post(
"/api/memory",
json={"text": DRAWER, "event_time": recorded.isoformat(), "event_time_precision": "day"},
)
assert response.status_code == 200, response.text
archived_at = datetime.now(UTC)
moved = lifecycle_store.archive_atoms(Database(settings), [response.json()["atom_id"]], archived_at)
assert moved == 1, "the archive moved nothing, so the test proves nothing"
assert hits(client, {"query": "钥匙"}) == [], "an archived memory is out of the working set"
asked_then = hits(client, {"query": "钥匙", "filters": {"as_of": (archived_at - timedelta(hours=1)).isoformat()}})
assert asked_then == [DRAWER], "archival moves a memory between tiers, it does not make it untrue"
def test_asking_as_of_another_moment_does_not_borrow_another_holders_memory(
client: TestClient, settings: Settings
) -> None:
atom_id, week_ago = store_the_pair(client)
client.post("/api/memory/supersede", json={"atom_id": atom_id, "text": CAR})
stranger = hits(client, {"query": "钥匙", "filters": {"as_holder": "stranger", "as_of": week_ago.isoformat()}})
assert stranger == [], "a moment in time does not grant access to somebody else's memory"
as_wife = hits(client, {"query": "钥匙", "filters": {"as_holder": "wife", "as_of": week_ago.isoformat()}})
assert as_wife == [DRAWER]
def test_the_dense_leg_reads_the_same_window_as_the_words_leg(settings: Settings) -> None:
"""Both legs answer an as-of question from one rule, or the fusion would mix two moments."""
service = MemoryService(settings, Database(settings))
week_ago = datetime.now(UTC) - WEEK
drawer = service.remember(
DRAWER, holder_id="wife", origin=MemoryOrigin.self_, event_time=week_ago, event_time_precision=TimePrecision.day
)
service.supersede(drawer.atom_id, text=CAR, holder_id="wife")
from ely_eye.retrieval import RetrievalService
retrieval = RetrievalService(settings=settings, database=Database(settings))
now_page = retrieval.search("钥匙", filters=SearchFilters(as_holder="wife"))
assert [hit.atom.text for hit in now_page.hits] == [CAR]
then_page = retrieval.search(
"钥匙", filters=SearchFilters(as_holder="wife", as_of=week_ago + timedelta(days=1))
)
assert [hit.atom.text for hit in then_page.hits] == [DRAWER]