Sixteen mutating routes now refuse a caller who cannot present the token the server started with; queries stay open, because a question is not a write. The environment variable is ELY_EYE_AUTH_TOKEN, following the repo's env_prefix convention — the shorter name I reached for first returned 403 where the smoke expected 401, and the server printed the reason. Words from a stranger are quarantined on the way in: an HTTP upload is third_party by definition, and a sentence carrying an instruction-shaped phrase is quarantined whoever wrote it. A quarantined atom stays findable and stays out of the compiled context until a second, live, unquarantined source agrees with its rarest words. Corroboration is computed at read time because it is a property of the corpus now, not of the moment a memory was written; it is term-level, and the docstring says so. forget now erases the source row and the compiled contexts beside the atoms, so a forgotten memory leaves nothing that can still speak. The semantic cache key carries a store revision, which fixes a real staleness: a context compiled before new evidence arrived kept its old quarantine decision forever. bench.audit --product still pointed at evals.py, which the p1-6 split moved into backend/src/ely_eye/proofs/. It failed fast, but the anti-self-justification rule had quietly stopped auditing the product; it now walks the package, and a test pins that the package exists and holds no re-roll. Gates: pytest 306 passed; ruff clean; mypy cold cache 24 errors in 9 files, the recorded baseline, net zero; pyright 0 errors on the 23 backend files and the 2 bench files; go build/vet/test and gofmt clean; npm run build 221.39 kB with eslint exit 0; bench/filesize_check clean; bench.audit --product clean over 31 harness files; scripts/mcp_write_smoke.sh PASS with the gate live across two servers and the MCP child.
252 lines
11 KiB
Python
252 lines
11 KiB
Python
"""The transactive test: I do not know where the keys are, I know my wife knows, I find my wife."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from pydantic import ValidationError
|
|
from typer.testing import CliRunner
|
|
|
|
import ely_eye.api as api_module
|
|
import ely_eye.cli as cli_module
|
|
from ely_eye.cli import app
|
|
from ely_eye.config import Settings
|
|
from ely_eye.db import Database
|
|
from ely_eye.ingestion import IngestionService
|
|
from ely_eye.memory import MemoryService
|
|
from ely_eye.migrations import SCHEMA_VERSION
|
|
from ely_eye.schemas import MemoryOrigin, Visibility, utc_now
|
|
from ely_eye.transactive import LIBRARY, NO_HOLDER_KNOWS, TransactiveService
|
|
from ely_eye.transactive_schemas import Holder
|
|
|
|
HOME_KEYS = "我们的钥匙放在玄关抽屉里"
|
|
WORK_KEYS = "备用钥匙在办公室第二个抽屉里"
|
|
QUERY = "钥匙"
|
|
|
|
|
|
class Store:
|
|
"""One SQLite file, the writer and the directory that read it."""
|
|
|
|
def __init__(self, settings: Settings) -> None:
|
|
self.settings = settings
|
|
self.memory = MemoryService(settings=settings)
|
|
self.db = self.memory.db
|
|
self.transactive = TransactiveService(settings=settings, database=self.db)
|
|
|
|
def wife_remembers(self, text: str, visibility: Visibility | None = None) -> str:
|
|
atom = self.memory.remember(
|
|
text, holder_id="wife", origin=MemoryOrigin.self_, scenario="home", visibility=visibility
|
|
)
|
|
return atom.atom_id
|
|
|
|
def husband_remembers(self, text: str) -> str:
|
|
atom = self.memory.remember(text, holder_id="husband", origin=MemoryOrigin.self_, scenario="home")
|
|
return atom.atom_id
|
|
|
|
def register(self, holder_id: str, **fields: Any) -> Holder:
|
|
moment = utc_now()
|
|
return self.transactive.holders.upsert(
|
|
Holder(holder_id=holder_id, created_at=moment, updated_at=moment, **fields)
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def store(tmp_path: Path) -> Store:
|
|
return Store(Settings(home=tmp_path / "home", default_holder="husband"))
|
|
|
|
|
|
def test_the_directory_names_the_holder_and_says_why(store: Store) -> None:
|
|
store.wife_remembers(HOME_KEYS)
|
|
report = store.transactive.who_knows(QUERY, as_holder="husband")
|
|
assert report.status == "found"
|
|
assert report.hits[0].holder_id == "wife"
|
|
assert report.hits[0].confidence > 0.0
|
|
assert any("matching memories" in line for line in report.hits[0].basis)
|
|
|
|
|
|
def test_the_directory_counts_a_memory_the_asker_cannot_read(store: Store) -> None:
|
|
"""The whole point: knowing that she knows does not require reading her memory."""
|
|
store.wife_remembers(HOME_KEYS)
|
|
assert store.transactive.who_knows(QUERY, as_holder="husband").hits[0].matched == 1
|
|
assert store.transactive._search(QUERY, as_holder="husband", holder="wife") == []
|
|
|
|
|
|
def test_a_private_memory_is_referred_and_not_leaked(store: Store) -> None:
|
|
store.register("wife", name="小美", contact={"channel": "signal", "address": "+86-555"})
|
|
store.wife_remembers(HOME_KEYS)
|
|
answer = store.transactive.ask("钥匙在哪里", as_holder="husband")
|
|
assert answer.status == "referred"
|
|
assert answer.answered_by == "wife"
|
|
assert answer.hops == 1
|
|
assert answer.asked == ["husband", "wife"]
|
|
assert answer.answer is None
|
|
assert answer.atom_ids == []
|
|
assert any("may not read" in line for line in answer.basis)
|
|
assert any("signal" in line for line in answer.basis)
|
|
|
|
|
|
def test_the_askers_own_memory_answers_without_a_referral(store: Store) -> None:
|
|
store.wife_remembers(HOME_KEYS)
|
|
store.husband_remembers("我的工牌挂在显示器边上")
|
|
answer = store.transactive.ask("工牌", as_holder="husband")
|
|
assert answer.status == "answered"
|
|
assert answer.answered_by == "husband"
|
|
assert answer.hops == 0
|
|
assert answer.asked == ["husband"]
|
|
assert answer.answer is None
|
|
|
|
|
|
def test_text_shown_only_when_the_asker_may_already_read_it(store: Store) -> None:
|
|
store.husband_remembers("我的工牌挂在显示器边上")
|
|
with_text = store.transactive.ask("工牌", as_holder="husband", include_text=True)
|
|
assert with_text.answer == "我的工牌挂在显示器边上"
|
|
|
|
store.register("wife")
|
|
store.wife_remembers(HOME_KEYS)
|
|
referred = store.transactive.ask("钥匙在哪里", as_holder="husband", include_text=True)
|
|
assert referred.status == "referred"
|
|
assert referred.answer is None
|
|
|
|
|
|
def test_a_shared_memory_is_readable_without_a_referral(store: Store) -> None:
|
|
"""A memory the asker may already read answers at hop 0: no referral, the text is shown."""
|
|
store.register("wife")
|
|
store.wife_remembers(HOME_KEYS, visibility=Visibility(scope="visible_to", holders=["husband"]))
|
|
answer = store.transactive.ask("钥匙在哪里", as_holder="husband", include_text=True)
|
|
assert answer.status == "answered"
|
|
assert answer.answered_by == "wife"
|
|
assert answer.hops == 0
|
|
assert answer.answer == HOME_KEYS
|
|
|
|
|
|
def test_an_unowned_artifact_answers_as_the_library(store: Store) -> None:
|
|
notebook = store.settings.home / "notebook.txt"
|
|
notebook.write_text("the garage door code is written on the fuse box", encoding="utf-8")
|
|
IngestionService(store.settings).ingest_path(notebook)
|
|
answer = store.transactive.ask("garage door code", as_holder="wife", include_text=True)
|
|
assert answer.status == "answered"
|
|
assert answer.answered_by == LIBRARY
|
|
assert "fuse box" in (answer.answer or "")
|
|
|
|
|
|
def test_nobody_knows_is_a_result_and_not_a_fabrication(store: Store) -> None:
|
|
store.wife_remembers(HOME_KEYS)
|
|
report = store.transactive.who_knows("潜水镜", as_holder="husband")
|
|
assert report.status == NO_HOLDER_KNOWS
|
|
assert report.hits == []
|
|
|
|
answer = store.transactive.ask("潜水镜在哪里", as_holder="husband")
|
|
assert answer.status == NO_HOLDER_KNOWS
|
|
assert answer.answered_by is None
|
|
assert answer.answer is None
|
|
assert answer.basis == ["no holder has a memory that answers this"]
|
|
|
|
|
|
def test_the_referral_walks_once_and_never_asks_the_same_holder_twice(store: Store) -> None:
|
|
store.register("wife")
|
|
store.register("daughter")
|
|
store.wife_remembers(HOME_KEYS)
|
|
store.memory.remember(WORK_KEYS, holder_id="daughter", origin=MemoryOrigin.self_, scenario="work")
|
|
answer = store.transactive.ask("钥匙在哪里", as_holder="husband", max_hops=1)
|
|
assert answer.status in ("referred", "answered")
|
|
assert len(answer.asked) == 2
|
|
assert len(set(answer.asked)) == len(answer.asked)
|
|
assert answer.asked[0] == "husband"
|
|
|
|
|
|
def test_the_hop_limit_is_enforced(store: Store) -> None:
|
|
for bad in (0, 11):
|
|
with pytest.raises(ValueError, match="max_hops between 1 and 10"):
|
|
store.transactive.ask(QUERY, as_holder="husband", max_hops=bad)
|
|
|
|
|
|
def test_declared_topics_raise_the_confidence_and_say_so(store: Store) -> None:
|
|
store.wife_remembers(HOME_KEYS)
|
|
before = store.transactive.who_knows(QUERY, as_holder="husband").hits[0]
|
|
store.register("wife", knows_about=["家里的钥匙"])
|
|
after = store.transactive.who_knows(QUERY, as_holder="husband").hits[0]
|
|
assert after.confidence > before.confidence
|
|
assert any("declared topics" in line for line in after.basis)
|
|
|
|
|
|
def test_the_registry_refuses_to_delete_a_holder_who_still_holds(store: Store) -> None:
|
|
store.register("wife")
|
|
atom_id = store.wife_remembers(HOME_KEYS)
|
|
with pytest.raises(ValueError, match="still holds 1 memories"):
|
|
store.transactive.holders.delete("wife")
|
|
store.memory.forget(atom_id=atom_id)
|
|
store.transactive.holders.delete("wife")
|
|
assert store.transactive.holders.list_holders() == []
|
|
|
|
|
|
def test_an_unknown_holder_kind_is_refused_at_the_door() -> None:
|
|
with pytest.raises(ValidationError, match="'person', 'agent' or 'service'"):
|
|
Holder.model_validate({"holder_id": "ghost", "kind": "ghost", "created_at": utc_now(), "updated_at": utc_now()})
|
|
|
|
|
|
def test_a_blank_holder_id_is_refused() -> None:
|
|
with pytest.raises(ValidationError, match="directory entry has to name somebody"):
|
|
Holder.model_validate({"holder_id": " ", "created_at": utc_now(), "updated_at": utc_now()})
|
|
|
|
|
|
def test_a_store_without_holders_gains_the_table_on_reopen(tmp_path: Path) -> None:
|
|
settings = Settings(home=tmp_path / "home", default_holder="husband")
|
|
database = Database(settings)
|
|
with database.connect() as connection:
|
|
connection.execute("DROP TABLE holders")
|
|
connection.execute("PRAGMA user_version = 4")
|
|
reopened = Database(settings)
|
|
with reopened.connect() as connection:
|
|
assert connection.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
|
|
assert connection.execute("SELECT COUNT(*) AS n FROM holders").fetchone()["n"] == 0
|
|
|
|
|
|
def test_the_http_surface_serves_the_directory(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, write_token: str
|
|
) -> None:
|
|
settings = Settings(home=tmp_path / "home", default_holder="husband", auth_token=write_token)
|
|
monkeypatch.setattr(api_module, "get_settings", lambda: settings)
|
|
client = TestClient(api_module.create_app(), headers={"Authorization": f"Bearer {write_token}"})
|
|
|
|
store = Store(settings)
|
|
store.wife_remembers(HOME_KEYS)
|
|
|
|
created = client.post(
|
|
"/api/holders/wife",
|
|
json={"name": "小美", "knows_about": ["家里的钥匙"], "contact": {"channel": "signal"}},
|
|
)
|
|
assert created.status_code == 200
|
|
assert created.json()["holder_id"] == "wife"
|
|
assert client.get("/api/holders").json()[0]["name"] == "小美"
|
|
assert client.get("/api/holders/nobody").status_code == 404
|
|
|
|
knows = client.get("/api/holders/wife/knows", params={"query": QUERY}).json()
|
|
assert knows["status"] == "found" and knows["hits"][0]["holder_id"] == "wife"
|
|
|
|
asked = client.post("/api/ask", json={"question": "钥匙在哪里", "as_holder": "husband"}).json()
|
|
assert asked["status"] == "referred" and asked["answered_by"] == "wife"
|
|
|
|
who = client.post("/api/who_knows", json={"query": "钥匙", "as_holder": "husband"}).json()
|
|
assert who["hits"][0]["basis"]
|
|
|
|
blocked = client.delete("/api/holders/wife")
|
|
assert blocked.status_code == 409 and "still holds" in blocked.json()["detail"]
|
|
|
|
|
|
def test_the_cli_asks_and_reports_the_referral(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
settings = Settings(home=tmp_path / "home", default_holder="husband")
|
|
monkeypatch.setattr(cli_module, "get_settings", lambda: settings)
|
|
runner = CliRunner()
|
|
Store(settings).wife_remembers(HOME_KEYS)
|
|
|
|
assert runner.invoke(app, ["holder-set", "wife", "--name", "小美", "--contact", "channel=signal"]).exit_code == 0
|
|
who = runner.invoke(app, ["who-knows", QUERY])
|
|
assert who.exit_code == 0 and '"holder_id": "wife"' in who.stdout
|
|
asked = runner.invoke(app, ["ask", "钥匙在哪里"])
|
|
assert asked.exit_code == 0 and '"status": "referred"' in asked.stdout
|
|
assert runner.invoke(app, ["holders"]).stdout.count("wife") >= 1
|
|
assert runner.invoke(app, ["holder-set", "wife", "--contact", "not-a-pair"]).exit_code != 0
|