Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: XNLLLLH <XNLLLLH@users.noreply.github.com>
132 lines
5 KiB
Python
132 lines
5 KiB
Python
"""F-05 regression: MemoryStore reader must see cross-connection writes.
|
|
|
|
Symptom that prompted this test:
|
|
The sleep daemon ticks every 30 s and calls ``_store_is_empty(store)``
|
|
against a connection it opened at process start. When short-lived MCP
|
|
tool calls (e.g. ``memory_capture``) wrote new rows through a
|
|
DIFFERENT connection to the same LanceDB directory, the daemon kept
|
|
reporting ``last_tick_skipped_reason: empty_store`` forever — it had
|
|
pinned the manifest snapshot at boot and LanceDB's default
|
|
``read_consistency_interval=None`` meant the handle never
|
|
auto-refreshed.
|
|
|
|
Fix shape:
|
|
``MemoryStore`` gained a ``read_consistency_interval: timedelta | None``
|
|
kwarg that is passed through to ``lancedb.connect``. Long-lived
|
|
readers (the daemon) opt into ``timedelta(seconds=0)`` — strong
|
|
consistency — so every read re-checks the latest committed
|
|
version. Short-lived MCP callers keep the default ``None`` because
|
|
they create a fresh connection per call and exit before staleness
|
|
matters.
|
|
|
|
These tests pin both sides of the contract:
|
|
1. With strong consistency the reader sees a writer's insert even
|
|
when the reader opened first.
|
|
2. With the default interval the reader's handle stays pinned to the
|
|
snapshot it opened against (documents the knob the daemon now
|
|
overrides, and guards against an accidental default change).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from uuid import uuid4
|
|
|
|
import pytest
|
|
|
|
|
|
def _make_record(store):
|
|
"""Construct a minimal MemoryRecord sized to the store's embed dim."""
|
|
from iai_mcp.types import MemoryRecord
|
|
|
|
now = datetime.now(timezone.utc)
|
|
return MemoryRecord(
|
|
id=uuid4(),
|
|
tier="semantic",
|
|
literal_surface="f-05 regression probe",
|
|
aaak_index="",
|
|
embedding=[0.0] * store.embed_dim,
|
|
community_id=None,
|
|
centrality=0.0,
|
|
detail_level=1,
|
|
pinned=False,
|
|
stability=0.0,
|
|
difficulty=0.0,
|
|
last_reviewed=None,
|
|
never_decay=False,
|
|
never_merge=False,
|
|
provenance=[],
|
|
created_at=now,
|
|
updated_at=now,
|
|
language="en",
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def tmp_store_env(tmp_path, monkeypatch):
|
|
monkeypatch.setenv("IAI_MCP_STORE", str(tmp_path / "iai"))
|
|
monkeypatch.setenv("IAI_MCP_EMBED_DIM", "384")
|
|
return tmp_path
|
|
|
|
|
|
def test_reader_with_strong_consistency_sees_writer_insert(tmp_store_env):
|
|
"""F-05 core regression: daemon-style reader (opened first, long-lived)
|
|
must observe records written by a separate connection."""
|
|
from iai_mcp.daemon import _store_is_empty
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
# Reader opens first while store is empty — this is the daemon at boot.
|
|
reader = MemoryStore(read_consistency_interval=timedelta(seconds=0))
|
|
assert _store_is_empty(reader) is True # baseline: nothing written yet
|
|
|
|
# Writer is a distinct connection to the same directory — this is the
|
|
# MCP tool call (memory_capture) running in a short-lived process.
|
|
writer = MemoryStore()
|
|
writer.insert(_make_record(writer))
|
|
|
|
# With strong consistency the reader sees the commit on the next read.
|
|
# Before the fix this stayed True forever.
|
|
assert _store_is_empty(reader) is False
|
|
|
|
|
|
def test_default_connection_is_snapshot_pinned(tmp_store_env):
|
|
"""Documents the non-default behaviour: a reader opened with the
|
|
default ``read_consistency_interval=None`` pins its view of the
|
|
``records`` table to the version present at ``open_table`` time.
|
|
|
|
This is the semantics short-lived MCP callers rely on (they finish
|
|
and exit before staleness matters). Guards against an accidental
|
|
default change that would regress MCP latency. The test also proves
|
|
``checkout_latest()`` is the manual escape hatch, matching the
|
|
LanceDB consistency contract.
|
|
"""
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
reader = MemoryStore() # no consistency kwarg — default None
|
|
records_tbl = reader.db.open_table("records")
|
|
assert records_tbl.count_rows() == 0
|
|
|
|
writer = MemoryStore()
|
|
writer.insert(_make_record(writer))
|
|
|
|
# Same table handle, reader did not ask for strong consistency:
|
|
# the pinned snapshot still shows zero.
|
|
assert records_tbl.count_rows() == 0
|
|
|
|
# Manual refresh restores visibility — this is the second blessed
|
|
# pattern (strong consistency being the first).
|
|
records_tbl.checkout_latest()
|
|
assert records_tbl.count_rows() == 1
|
|
|
|
|
|
def test_kwarg_is_persisted_for_introspection(tmp_store_env):
|
|
"""Callers (ops tooling, tests) can read the interval back."""
|
|
from iai_mcp.store import MemoryStore
|
|
|
|
default_store = MemoryStore()
|
|
assert default_store._read_consistency_interval is None
|
|
|
|
strong_store = MemoryStore(read_consistency_interval=timedelta(seconds=0))
|
|
assert strong_store._read_consistency_interval == timedelta(seconds=0)
|
|
|
|
eventual_store = MemoryStore(read_consistency_interval=timedelta(seconds=30))
|
|
assert eventual_store._read_consistency_interval == timedelta(seconds=30)
|