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.
122 lines
5.5 KiB
Python
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]
|