Files
ely-eye/backend/tests/test_product_api.py
T
ZacharyZhang-NY 9f91ff7ed7 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.
2026-10-08 05:13:38 -04:00

193 lines
7.0 KiB
Python

"""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