Let the console search, browse, forget and mount

The store had a write path, a mount path and a search path, and the console
still showed a dashboard of counters. Nothing in the UI could ask what someone
knows, page through atoms, look at a wordless picture, forget one memory, or
mount a cartridge — so the mechanisms built in P0 were invisible.

Backend:
- senses.py: one function decides what this machine can perceive. /api/status
  and doctor each computed it before, which is two truths. Picture atoms are now
  counted by image_ref rather than by modality, so a screenshot crop from a PDF
  counts and a picture whose bytes were deleted stops claiming it can be encoded.
- browse.py: GET /api/atoms pages newest-first through v_atoms_current with
  modality/holder/source filters; GET /api/atoms/{id}/image serves the stored
  bytes so a picture with no OCR text can be looked at.
- Deleted 8 HTTP routes nobody calls (4 benchmark, research modules, dense index,
  visual-contradiction-proof, memory-map). Their functions stay: the CLI and the
  proof checks use them. The surface is 30 routes, pinned as a set by a test so a
  new unused route fails the suite.

Frontend: App.tsx (517 lines) became a 131-line shell plus seven panels that own
their own fetches, and lib/format.ts. types.ts now carries the memory dimensions
the backend has been sending since schema v2.

Two invented fields were rendering lies: AdapterPanel showed gpu_available and
probe_status, which AdapterLoadReport does not have — every machine looked GPU-less
including this one. The panel now shows only what the server says.

Proof: scripts/ui_smoke.sh starts the API on a fresh temp home, serves the built
UI from the same origin, and drives five interactions in a real browser — search
as somebody in a scenario, page and view a wordless picture, forget, mount, read
adapter states with reasons. It prints the mount refusal verbatim rather than
pretending the mount succeeded.

Gates: 263 pytest passed; ruff and mypy clean on the 8 touched backend files
(repo-wide mypy unchanged at 24 errors / 6 files); pyright 0 errors there;
go build/vet/test/gofmt clean; npm build + eslint clean; ui_smoke and
mcp_write_smoke both PASS.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 05:13:38 -04:00
1 parent a5008e1476
commit 9f91ff7ed7
27 files changed
+2146 -616

No files matched your search

+34 -62
View File
@@ -5,8 +5,9 @@ import shutil
from pathlib import Path
from typing import Annotated
from fastapi import Depends, FastAPI, File, HTTPException, UploadFile
from fastapi import Depends, FastAPI, File, HTTPException, Query, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
@@ -14,33 +15,18 @@ from . import __version__
from .adapters import plan as adapter_plan
from .api_cartridges import register as register_cartridge_routes
from .api_holders import register as register_holder_routes
from .benchmarks import (
benchmark_prediction_report,
benchmark_registry_report,
benchmark_sample_report,
benchmark_source_report,
)
from .browse import browse, picture
from .cache_fabric import CacheFabric
from .cartridge import CartridgeService
from .compiler import ContextCompiler
from .config import Settings, get_settings
from .db import Database
from .embeddings import EmbeddingService
from .evaluation_schemas import (
BenchmarkPredictionReport,
BenchmarkRegistryReport,
BenchmarkSampleReport,
BenchmarkSourceReport,
HashHopProof,
ProofCheck,
ProofSuiteReport,
)
from .evaluation_schemas import HashHopProof, ProofSuiteReport
from .evidence_graph import EvidenceGraphService
from .ingestion import IngestionService
from .memory import MemoryService
from .profiles import runtime_profile_report
from .proofs import HashHopEvaluator, PRDProofSuite
from .research_modules import ResearchModuleService
from .retrieval import RetrievalService
from .runtime import QwenRuntime, RuntimeUnavailableError
from .runtime_schemas import AdapterLoadReport
@@ -48,11 +34,13 @@ from .schemas import (
AdapterManifest,
AdapterStatus,
AnswerResponse,
AtomPage,
EvidenceAtom,
ForgetReport,
ForgetRequest,
MemoryOrigin,
MemoryStatus,
Modality,
PathIngestRequest,
RememberRequest,
RuntimeProfile,
@@ -63,6 +51,7 @@ from .schemas import (
SystemStatus,
UploadIngestRequest,
)
from .senses import sense_facts
from .training import safetensor_weight_stats
from .verifier import Verifier, VisualContradictionLens
@@ -114,6 +103,7 @@ def create_app() -> FastAPI:
version=__version__,
runtime=runtime,
memory=memory,
senses=sense_facts(settings, db),
data_home=str(settings.home),
profiles={
"Live Demo": {"context_tokens": settings.live_context_tokens, "purpose": "stable 262K workflow"},
@@ -260,21 +250,33 @@ def create_app() -> FastAPI:
def adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> list[AdapterStatus]:
return list_adapter_statuses(settings)
@app.get("/api/benchmarks/registry", response_model=BenchmarkRegistryReport)
def benchmarks_registry() -> BenchmarkRegistryReport:
return benchmark_registry_report()
@app.get("/api/atoms", response_model=AtomPage)
def atoms(
settings: Annotated[Settings, Depends(settings_dep)],
limit: Annotated[int, Query(ge=1, le=200)] = 50,
cursor: str | None = None,
modality: Modality | None = None,
source_id: str | None = None,
holder: str | None = None,
) -> AtomPage:
try:
return browse(
settings,
limit=limit,
cursor=cursor,
modality=modality,
source_id=source_id,
holder=holder,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@app.get("/api/benchmarks/sources", response_model=BenchmarkSourceReport)
def benchmarks_sources() -> BenchmarkSourceReport:
return benchmark_source_report()
@app.get("/api/benchmarks/samples", response_model=BenchmarkSampleReport)
def benchmarks_samples(settings: Annotated[Settings, Depends(settings_dep)]) -> BenchmarkSampleReport:
return benchmark_sample_report(settings.data_dir / "benchmark_samples")
@app.get("/api/benchmarks/predictions", response_model=BenchmarkPredictionReport)
def benchmarks_predictions(settings: Annotated[Settings, Depends(settings_dep)]) -> BenchmarkPredictionReport:
return benchmark_prediction_report(settings, settings.data_dir / "benchmark_predictions")
@app.get("/api/atoms/{atom_id}/image")
def atom_image(atom_id: str, settings: Annotated[Settings, Depends(settings_dep)]) -> FileResponse:
found = picture(settings, atom_id)
if found is None:
raise HTTPException(status_code=404, detail=f"{atom_id} has no picture to show")
return FileResponse(found.path, media_type=found.mime)
@app.get("/api/runtime/profiles", response_model=RuntimeProfileReport)
def runtime_profiles(
@@ -286,22 +288,6 @@ def create_app() -> FastAPI:
def runtime_adapters(settings: Annotated[Settings, Depends(settings_dep)]) -> AdapterLoadReport:
return adapter_plan(settings, Database(settings))
@app.get("/api/research/modules")
def research_modules(settings: Annotated[Settings, Depends(settings_dep)]):
db = Database(settings)
compiler = ContextCompiler(settings, db)
context = compiler.compile(
"Ely-Eye V-NSA DVKV x-modal KV speculative prefetch evidence graph",
RuntimeProfile.research_theater,
)
return ResearchModuleService(settings, db).build_report(context.plan.question, context.hits)
@app.post("/api/index/dense")
def build_dense_index(settings: Annotated[Settings, Depends(settings_dep)]):
service = EmbeddingService(settings)
index = service.build_index()
return {"atoms": len(index.atom_ids), "dimensions": int(index.vectors.shape[1]) if index.vectors.size else 0}
@app.post("/api/hashhop", response_model=HashHopProof)
def hashhop(request: HashHopRequest, settings: Annotated[Settings, Depends(settings_dep)]) -> HashHopProof:
evaluator = HashHopEvaluator(settings)
@@ -328,20 +314,6 @@ def create_app() -> FastAPI:
raise HTTPException(status_code=404, detail="No PRD proof suite has been generated.")
return report
@app.post("/api/visual-contradiction-proof", response_model=ProofCheck)
def visual_contradiction_proof(settings: Annotated[Settings, Depends(settings_dep)]) -> ProofCheck:
return PRDProofSuite(settings).visual_contradiction_proof()
@app.get("/api/memory-map")
def memory_map(settings: Annotated[Settings, Depends(settings_dep)]):
db = Database(settings)
atoms = db.list_atoms(limit=200)
return {
"atoms": [atom.model_dump(mode="json") for atom in atoms],
"layers": CacheFabric(settings, db).statuses(),
"cartridges": CartridgeService(settings, db).list_manifests(),
}
mounted_ui = get_settings().ui_dir
if mounted_ui is not None:
if not mounted_ui.is_dir():
+75
View File
@@ -0,0 +1,75 @@
"""Page through the store the way a person browses a shelf: newest first, filtered by who and what."""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from .config import Settings
from .db import Database, row_to_atom
from .retrieval import decode_cursor, encode_cursor
from .schemas import AtomPage, Modality
from .storage import ObjectStore
@dataclass(frozen=True)
class Picture:
"""The bytes behind an atom and how to label them when serving."""
path: Path
mime: str
def browse(
settings: Settings,
*,
limit: int,
cursor: str | None = None,
modality: Modality | None = None,
source_id: str | None = None,
holder: str | None = None,
) -> AtomPage:
"""One page plus the total, so a list can say what it left out instead of looking endless."""
offset = decode_cursor(cursor)
wanted = (modality.value if modality else None, source_id, holder)
filters = tuple(value for bound in wanted for value in (bound, bound))
database = Database(settings)
with database.connect() as connection:
total = int(
connection.execute(
"SELECT COUNT(*) FROM v_atoms_current WHERE (? IS NULL OR modality = ?) AND (? IS NULL OR source_id = ?) AND (? IS NULL OR holder_id = ?)",
filters,
).fetchone()[0]
)
rows = connection.execute(
"SELECT * FROM v_atoms_current WHERE (? IS NULL OR modality = ?) AND (? IS NULL OR source_id = ?) AND (? IS NULL OR holder_id = ?) ORDER BY ingested_at DESC, atom_id DESC LIMIT ? OFFSET ?",
(*filters, limit + 1, offset),
).fetchall()
atoms = [row_to_atom(row) for row in rows[:limit]]
more = len(rows) > limit or offset + limit < total
return AtomPage(
atoms=atoms,
total=total,
next_cursor=encode_cursor(offset + limit) if atoms and more else None,
)
def picture(settings: Settings, atom_id: str) -> Picture | None:
"""The stored bytes behind one atom, through the same visibility view search uses.
None covers three cases the caller cannot tell apart and does not need to: the atom is unknown,
it was forgotten, or its bytes are gone. All three mean "there is no picture to show".
"""
database = Database(settings)
with database.connect() as connection:
row = connection.execute(
"SELECT image_ref, mime FROM v_atoms_current JOIN sources USING (source_id) WHERE atom_id = ?",
(atom_id,),
).fetchone()
if row is None or not row["image_ref"]:
return None
path = ObjectStore(settings).resolve(str(row["image_ref"]))
if not path.exists():
return None
mime = str(row["mime"]) if row["mime"] else "image/png"
return Picture(path=path, mime=mime)
+35 -42
View File
@@ -14,15 +14,13 @@ from . import __version__
from .cartridge_store import mounted_ids
from .config import Settings
from .db import Database
from .embeddings import EmbeddingService
from .migrations import SCHEMA_VERSION
from .schemas import utc_now
from .visual_index import VisualIndex
from .schemas import SenseStatus, utc_now
from .senses import sense_facts
BACKUP_FILE = "ely_eye.sqlite"
MANIFEST_FILE = "backup_manifest.json"
_REQUIRED_TABLES = ("atoms", "sources", "cartridges")
PICTURE_MODALITIES = ("image", "ui_screenshot")
class BackupManifest(BaseModel):
@@ -57,12 +55,6 @@ def _atoms_word(count: int) -> str:
return "atom" if count == 1 else "atoms"
def _modality_counts(database: Database) -> dict[str, int]:
with database.connect() as connection:
rows = connection.execute("SELECT modality, COUNT(*) AS total FROM atoms GROUP BY modality").fetchall()
return {str(row["modality"]): int(row["total"]) for row in rows}
def _counts(connection: sqlite3.Connection) -> dict[str, int]:
return {
"atoms": int(connection.execute("SELECT COUNT(*) FROM atoms").fetchone()[0]),
@@ -175,41 +167,41 @@ def _check_cartridges(database: Database, mounted: list[str], problems: list[str
problems.append(f"{cartridge_id} is mounted but its files at {root} are gone")
def _check_senses(settings: Settings, database: Database, visual: VisualIndex, warnings: list[str]) -> None:
counts = _modality_counts(database)
pictures = sum(counts.get(name, 0) for name in PICTURE_MODALITIES)
if settings.clip_model_dir is not None and visual.model_dir() is None:
warnings.append(f"the CLIP snapshot at {settings.clip_model_dir} is missing, so wordless pictures cannot be found")
elif pictures and not visual.available():
def _check_senses(facts: SenseStatus, warnings: list[str]) -> None:
"""Render the missing senses as sentences; the counting lives in senses.py, not here."""
encodable = facts.picture_atoms - facts.picture_bytes_missing
if facts.clip_path is not None and facts.visual == "absent":
warnings.append(f"the CLIP snapshot at {facts.clip_path} is missing, so wordless pictures cannot be found")
elif encodable and facts.visual == "absent":
warnings.append(
f"the store holds {pictures} picture {_atoms_word(pictures)} but no CLIP snapshot is set "
f"the store holds {encodable} picture {_atoms_word(encodable)} but no CLIP snapshot is set "
"(ELY_EYE_CLIP_MODEL_DIR), so they are findable only by the text already on them"
)
elif visual.visual_atoms() and not visual.is_current():
elif facts.visual == "stale":
warnings.append("the visual index is behind the store; run `ely-eye index-visual`")
audio = counts.get("audio", 0)
if settings.asr_model_dir is None:
if audio:
warnings.append(
f"the store holds {audio} audio {_atoms_word(audio)} but no speech snapshot is set "
"(ELY_EYE_ASR_MODEL_DIR), so new audio will not be transcribed"
)
elif not settings.asr_model_dir.exists():
warnings.append(f"the speech snapshot at {settings.asr_model_dir} is missing, so audio cannot be transcribed")
if settings.vlm_model_dir is not None and not settings.vlm_model_dir.exists():
warnings.append(f"the captioning model at {settings.vlm_model_dir} is missing, so images stay uncaptioned")
def _check_indexes(settings: Settings, database: Database, warnings: list[str]) -> None:
if not settings.dense_embeddings_enabled:
return
index = EmbeddingService(settings, database).load_index()
stored = database.atom_count()
if index is None:
warnings.append("dense embeddings are on but no index exists; run `ely-eye build-dense-index`")
elif len(index.atom_ids) != stored:
if facts.picture_bytes_missing:
missing = facts.picture_bytes_missing
warnings.append(
f"the dense index covers {len(index.atom_ids)} atoms while the store holds {stored}; run `ely-eye build-dense-index`"
f"{missing} picture {_atoms_word(missing)} with no bytes behind them; re-ingest the source they came from"
)
if facts.asr_path is not None and facts.speech == "missing":
warnings.append(f"the speech snapshot at {facts.asr_path} is missing, so audio cannot be transcribed")
elif facts.audio_atoms and facts.speech == "absent":
warnings.append(
f"the store holds {facts.audio_atoms} audio {_atoms_word(facts.audio_atoms)} but no speech snapshot is set "
"(ELY_EYE_ASR_MODEL_DIR), so new audio will not be transcribed"
)
if facts.vlm_path is not None and facts.captioning == "missing":
warnings.append(f"the captioning model at {facts.vlm_path} is missing, so images stay uncaptioned")
def _check_indexes(facts: SenseStatus, warnings: list[str]) -> None:
if facts.dense == "missing":
warnings.append("dense embeddings are on but no index exists; run `ely-eye build-dense-index`")
elif facts.dense == "behind":
warnings.append(
f"the dense index covers {facts.dense_covered} atoms while the store holds {facts.store_atoms}; "
"run `ely-eye build-dense-index`"
)
@@ -242,8 +234,9 @@ def doctor(settings: Settings) -> DoctorReport:
problems.append(f"the schema settled at version {version} while this build writes {SCHEMA_VERSION}")
mounted = mounted_ids(database)
_check_cartridges(database, mounted, problems)
_check_senses(settings, database, VisualIndex(settings, database), warnings)
_check_indexes(settings, database, warnings)
facts = sense_facts(settings, database)
_check_senses(facts, warnings)
_check_indexes(facts, warnings)
if settings.ui_dir is not None and not settings.ui_dir.exists():
problems.append(f"ui_dir {settings.ui_dir} is set but holds no built interface, so the API refuses to start")
return DoctorReport(
+36
View File
@@ -201,6 +201,14 @@ class SearchResult(BaseModel):
hits: list[RetrievalHit]
next_cursor: str | None = None
class AtomPage(BaseModel):
"""A page of the store plus the total behind it, so a list can admit what it left out."""
atoms: list[EvidenceAtom]
total: int
next_cursor: str | None = None
class ContextPlan(BaseModel):
question: str
profile: RuntimeProfile
@@ -261,11 +269,39 @@ class RuntimeStatus(BaseModel):
vram_total_mb: int | None = None
vram_used_mb: int | None = None
SenseState = Literal["absent", "missing", "ready"]
VisualState = Literal["absent", "stale", "ready"]
DenseState = Literal["off", "missing", "behind", "current"]
class SenseStatus(BaseModel):
"""What the machine can perceive right now, stated once for the UI and the doctor to share.
`absent` means the sense was never given a model, `missing` means it was given one that is gone.
A store full of pictures and no CLIP snapshot is a different problem, and needs a different
sentence, than a snapshot directory somebody deleted.
"""
dense: DenseState
dense_covered: int
store_atoms: int
picture_atoms: int
picture_bytes_missing: int
visual: VisualState
audio_atoms: int
speech: SenseState
captioning: SenseState
clip_path: str | None = None
asr_path: str | None = None
vlm_path: str | None = None
class SystemStatus(BaseModel):
app: str = "Ely-Eye"
version: str
runtime: RuntimeStatus
memory: MemoryStatus
senses: SenseStatus
profiles: dict[str, dict[str, Any]]
data_home: str
+84
View File
@@ -0,0 +1,84 @@
"""What this machine can actually perceive, computed once and read by status and doctor alike."""
from __future__ import annotations
from pathlib import Path
from .config import Settings
from .db import Database
from .embeddings import EmbeddingService
from .schemas import DenseState, SenseState, SenseStatus, VisualState
from .visual_index import VisualIndex
def sense_facts(settings: Settings, database: Database, visual: VisualIndex | None = None) -> SenseStatus:
"""Report the senses as facts, so the UI and the doctor cannot disagree about what is missing.
A path set but gone on disk is a different failure from a path never set: the first is a broken
install, the second is a machine that was never given that sense.
"""
index = visual or VisualIndex(settings, database)
claimed = _picture_claim(database)
on_disk = index.visual_atom_ids()
dense, covered, total = _dense_facts(settings, database)
return SenseStatus(
dense=dense,
dense_covered=covered,
store_atoms=total,
picture_atoms=claimed,
picture_bytes_missing=max(0, claimed - len(on_disk)),
visual=_visual_state(index, on_disk),
audio_atoms=_audio_count(database),
speech=_sense_state(settings.asr_model_dir),
captioning=_sense_state(settings.vlm_model_dir),
clip_path=_stated(settings.clip_model_dir),
asr_path=_stated(settings.asr_model_dir),
vlm_path=_stated(settings.vlm_model_dir),
)
def _dense_facts(settings: Settings, database: Database) -> tuple[DenseState, int, int]:
total = database.atom_count()
if not settings.dense_embeddings_enabled:
return "off", 0, total
loaded = EmbeddingService(settings, database).load_index()
if loaded is None:
return "missing", 0, total
covered = len(loaded.atom_ids)
return ("current" if covered == total else "behind"), covered, total
def _visual_state(visual: VisualIndex, pictures: set[str]) -> VisualState:
"""A store of pictures the index has not encoded yet is stale: they are invisible to search."""
if not visual.available():
return "absent"
if not pictures:
return "ready"
loaded = visual.load()
return "ready" if loaded is not None and set(loaded[0]) == pictures else "stale"
def _picture_claim(database: Database) -> int:
"""Atoms that say they hold a picture, whether or not the bytes survived."""
with database.connect() as connection:
row = connection.execute(
"SELECT COUNT(*) FROM v_atoms_current WHERE image_ref IS NOT NULL AND image_ref != ''"
).fetchone()
return int(row[0])
def _sense_state(model_dir: Path | None) -> SenseState:
"""A sense with no configured snapshot is absent; one pointing at a deleted directory is missing."""
if model_dir is None:
return "absent"
return "ready" if model_dir.exists() else "missing"
def _stated(model_dir: Path | None) -> str | None:
return None if model_dir is None else str(model_dir)
def _audio_count(database: Database) -> int:
with database.connect() as connection:
row = connection.execute("SELECT COUNT(*) AS n FROM v_atoms_current WHERE modality = 'audio'").fetchone()
return int(row["n"])
+21 -3
View File
@@ -141,12 +141,30 @@ class VisualIndex:
_LOADED.clear()
return removed
def is_current(self, atoms: list[EvidenceAtom] | None = None) -> bool:
"""True when the index covers exactly the image atoms the store holds right now."""
def visual_atom_ids(self) -> set[str]:
"""Picture atoms whose bytes are still on disk, asked with one light query."""
store = ObjectStore(self.settings)
with self.db.connect() as connection:
rows = connection.execute(
"SELECT atom_id, image_ref FROM v_atoms_current WHERE image_ref IS NOT NULL AND image_ref != ''"
).fetchall()
kept: set[str] = set()
for row in rows:
try:
path = store.resolve(str(row["image_ref"]))
except ValueError:
continue
if path.exists():
kept.add(str(row["atom_id"]))
return kept
def is_current(self, atom_ids: set[str] | None = None) -> bool:
"""True when the index covers exactly the picture atoms the store holds right now."""
loaded = self.load()
if loaded is None:
return False
return {atom.atom_id for atom in self.visual_atoms(atoms)} == set(loaded[0])
wanted = self.visual_atom_ids() if atom_ids is None else atom_ids
return wanted == set(loaded[0])
def search_text(self, query: str, top_k: int, restrict: set[str] | None = None) -> list[tuple[str, float]]:
vector = self.encoder().encode_texts([query])[0]
+33 -1
View File
@@ -19,6 +19,7 @@ from ely_eye.db import Database
from ely_eye.memory import MemoryService
from ely_eye.migrations import SCHEMA_VERSION
from ely_eye.schemas import EvidenceAtom, MemoryOrigin, Modality, SourceRecord, TrustScores, utc_now
from ely_eye.storage import ObjectStore
KEYS = "the keys are on the shelf by the door"
PASSPORT = "the passport is in the blue box"
@@ -164,7 +165,14 @@ def test_doctor_names_the_senses_a_store_holds_but_cannot_use(tmp_path: Path) ->
)
database.upsert_atoms(
[
EvidenceAtom(atom_id="img:1", modality=Modality.image, source_id="img", source="file:///img.png", trust=trust),
EvidenceAtom(
atom_id="img:1",
modality=Modality.image,
source_id="img",
source="file:///img.png",
trust=trust,
image_ref=ObjectStore(settings).put_bytes(b"\x89PNG\r\n\x1a\n", ".png", "image"),
),
EvidenceAtom(atom_id="aud:1", modality=Modality.audio, source_id="aud", source="file:///a.mp3", trust=trust),
]
)
@@ -175,6 +183,30 @@ def test_doctor_names_the_senses_a_store_holds_but_cannot_use(tmp_path: Path) ->
assert "1 audio atom but no speech snapshot" in report.warnings[1]
def test_doctor_names_picture_atoms_whose_bytes_are_gone(tmp_path: Path) -> None:
settings = _settings(tmp_path)
database = Database(settings)
database.init()
trust = TrustScores(parser=0.0, ocr=0.0, model=0.0)
database.upsert_sources(
[SourceRecord(source_id="img", path="file:///img.png", kind="image", mime="image/png", sha256="a", size_bytes=1)]
)
database.upsert_atoms(
[
EvidenceAtom(
atom_id="img:1",
modality=Modality.image,
source_id="img",
source="file:///img.png",
trust=trust,
image_ref="object://image/ab/deadbeef.png",
)
]
)
report = maintenance.doctor(settings)
assert "1 picture atom with no bytes behind them" in report.warnings[0]
def test_doctor_names_a_mount_whose_files_are_gone(tmp_path: Path) -> None:
settings = _settings(tmp_path)
database = _store(settings)
+192
View File
@@ -0,0 +1,192 @@
"""The product face: page the store, look at a picture, and say plainly what the machine can perceive."""
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from fastapi.routing import APIRoute
from fastapi.testclient import TestClient
import ely_eye.api as api_module
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.memory import MemoryService
from ely_eye.schemas import EvidenceAtom, MemoryOrigin, Modality, SourceRecord, TrustScores
from ely_eye.storage import ObjectStore
PNG = b"\x89PNG\r\n\x1a\n"
BASE = datetime(2026, 5, 1, 12, 0, tzinfo=UTC)
HTTP_SURFACE = {
"/api/adapters",
"/api/ask",
"/api/atoms",
"/api/atoms/{atom_id}/image",
"/api/cartridges",
"/api/cartridges/{cartridge_id}/append",
"/api/cartridges/{cartridge_id}/assets",
"/api/cartridges/{cartridge_id}/finalize",
"/api/cartridges/{cartridge_id}/mount",
"/api/cartridges/{cartridge_id}/refresh",
"/api/cartridges/{cartridge_id}/switch",
"/api/cartridges/{cartridge_id}/unmount",
"/api/chat",
"/api/hashhop",
"/api/holders",
"/api/holders/{holder_id}",
"/api/holders/{holder_id}/knows",
"/api/ingest/path",
"/api/ingest/upload",
"/api/memory",
"/api/memory/forget",
"/api/memory/supersede",
"/api/mounts",
"/api/proof-suite",
"/api/proof-suite/latest",
"/api/runtime/adapters",
"/api/runtime/profiles",
"/api/search",
"/api/status",
"/api/who_knows",
}
@pytest.fixture
def settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Settings:
resolved = Settings(home=tmp_path, default_holder="wife")
monkeypatch.setattr(api_module, "get_settings", lambda: resolved)
Database(resolved).init()
return resolved
@pytest.fixture
def client(settings: Settings) -> TestClient:
return TestClient(api_module.create_app())
def _seed(settings: Settings, pictures: int = 3) -> None:
"""Pictures an hour apart, so newest-first has a fact to order by rather than a coincidence."""
database = Database(settings)
trust = TrustScores(parser=1.0, ocr=0.0, model=0.0)
store = ObjectStore(settings)
stored = store.put_bytes(PNG, ".png", "image")
database.upsert_sources(
[
SourceRecord(
source_id=f"shot{i}",
path=f"file:///shot{i}.png",
kind="image",
mime="image/png",
sha256=f"{i}" * 8,
size_bytes=len(PNG),
)
for i in range(pictures)
]
)
database.upsert_atoms(
[
EvidenceAtom(
atom_id=f"shot{i}:frame",
modality=Modality.image,
source_id=f"shot{i}",
source=f"file:///shot{i}.png",
trust=trust,
image_ref=stored,
holder_id="wife" if i % 2 == 0 else "husband",
ingested_at=BASE + timedelta(hours=i),
)
for i in range(pictures)
]
)
def test_the_store_pages_newest_first_and_says_how_much_is_left(client: TestClient, settings: Settings) -> None:
_seed(settings)
first = client.get("/api/atoms", params={"limit": 2})
assert first.status_code == 200
page = first.json()
assert page["total"] == 3
assert [atom["atom_id"] for atom in page["atoms"]] == ["shot2:frame", "shot1:frame"]
assert page["next_cursor"]
second = client.get("/api/atoms", params={"limit": 2, "cursor": page["next_cursor"]})
rest = second.json()
assert [atom["atom_id"] for atom in rest["atoms"]] == ["shot0:frame"]
assert rest["next_cursor"] is None, "the last page must not promise another one"
def test_browse_narrows_by_modality_and_by_holder(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=2)
MemoryService(settings, Database(settings)).remember(
"the keys are on the shelf", holder_id="wife", origin=MemoryOrigin.self_
)
images = client.get("/api/atoms", params={"modality": "image"}).json()
assert images["total"] == 2
assert {atom["modality"] for atom in images["atoms"]} == {"image"}
his = client.get("/api/atoms", params={"holder": "husband"}).json()
assert [atom["atom_id"] for atom in his["atoms"]] == ["shot1:frame"]
def test_an_unreadable_cursor_is_refused_with_400(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=1)
response = client.get("/api/atoms", params={"cursor": "not-a-cursor"})
assert response.status_code == 400
assert "cursor" in response.json()["detail"]
def test_a_picture_atom_serves_the_bytes_behind_it(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=1)
response = client.get("/api/atoms/shot0:frame/image")
assert response.status_code == 200
assert response.content == PNG
assert response.headers["content-type"] == "image/png"
def test_a_memory_with_no_picture_says_so(client: TestClient, settings: Settings) -> None:
atom = MemoryService(settings, Database(settings)).remember(
"the keys are on the shelf", holder_id="wife", origin=MemoryOrigin.self_
)
assert client.get(f"/api/atoms/{atom.atom_id}/image").status_code == 404
assert client.get("/api/atoms/nope:image").status_code == 404
def test_a_forgotten_picture_stops_being_served(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=1)
MemoryService(settings, Database(settings)).forget(atom_id="shot0:frame")
assert client.get("/api/atoms/shot0:frame/image").status_code == 404
assert client.get("/api/atoms").json()["total"] == 0
def test_status_says_what_the_machine_can_see_and_what_it_cannot(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=2)
senses = client.get("/api/status").json()["senses"]
assert senses["dense"] == "off", "dense embeddings are off by default and status must not claim otherwise"
assert senses["picture_atoms"] == 2
assert senses["picture_bytes_missing"] == 0
assert senses["visual"] == "absent", "no CLIP snapshot on this machine means wordless pictures stay hidden"
assert senses["speech"] == "absent"
def test_status_names_pictures_whose_bytes_are_gone(client: TestClient, settings: Settings) -> None:
_seed(settings, pictures=1)
dead = Database(settings).get_atoms(["shot0:frame"])[0].model_copy(
update={"atom_id": "shot0:ghost", "image_ref": "object://image/ab/deadbeef.png"}
)
Database(settings).upsert_atoms([dead])
senses = client.get("/api/status").json()["senses"]
assert senses["picture_atoms"] == 2
assert senses["picture_bytes_missing"] == 1
def test_the_http_surface_is_exactly_what_someone_uses() -> None:
"""A route nobody calls is a contract nobody keeps: adding one has to be a deliberate act."""
served = {
route.path
for route in api_module.app.routes
if isinstance(route, APIRoute) and route.path.startswith("/api")
}
assert served == HTTP_SURFACE