second-brain/tests/test_smoke_embedding.py
Travis Herbranson 2e07d7a8fe alembic: skip CREATE SCHEMA when present + add embedding smoke test
The runtime role (lovebug) doesn't have CREATE on the petalbrain database
even though it owns the second_brain schema, so a bare
`CREATE SCHEMA IF NOT EXISTS` errors out with permission denied. Gate
the bootstrap on a pg_namespace lookup so we only attempt the create
when the schema is genuinely missing — operators bootstrap it once as
postgres superuser, alembic just respects it afterward.

The smoke test exercises the full Postgres + embedding path against a
live DB + Ollama (autoskipped otherwise): writes an extraction, embeds
the summary, asserts public.embeddings has the expected row count, and
re-embeds to verify the delete-before-insert idempotency.
2026-05-24 22:49:06 -04:00

149 lines
5.0 KiB
Python

"""Smoke test for the Postgres + embedding pipeline.
Runs only when SECOND_BRAIN_DATABASE_URL (or HERBYLAB_DATABASE_URL) is set
*and* an Ollama instance with nomic-embed-text is reachable. Otherwise the
test is skipped — there is no in-memory fixture for pgvector, and we
explicitly want the smoke check to exercise the real wire format.
It covers:
1. SQLAlchemy can write a Source + Extraction into the `second_brain`
schema.
2. The embeddings module chunks the summary, calls Ollama, and writes
vector rows into public.embeddings keyed correctly.
3. Re-running embedding is idempotent (delete-before-insert wins).
"""
from __future__ import annotations
import os
import socket
import uuid
import httpx
import psycopg
import pytest
def _ollama_reachable() -> bool:
url = os.environ.get("OLLAMA_URL", "http://127.0.0.1:11434")
try:
r = httpx.get(f"{url}/api/tags", timeout=2.0)
if r.status_code != 200:
return False
models = [m["name"] for m in r.json().get("models", [])]
return any(m.startswith("nomic-embed-text") for m in models)
except (httpx.HTTPError, socket.error):
return False
def _db_reachable() -> bool:
url = os.environ.get("SECOND_BRAIN_DATABASE_URL") or os.environ.get(
"HERBYLAB_DATABASE_URL"
)
if not url:
return False
raw = url.replace("postgresql+psycopg://", "postgresql://", 1)
try:
with psycopg.connect(raw, connect_timeout=2) as conn: # noqa: F841
return True
except Exception:
return False
pytestmark = pytest.mark.skipif(
not (_db_reachable() and _ollama_reachable()),
reason="Smoke test needs SECOND_BRAIN_DATABASE_URL + reachable Ollama (nomic-embed-text)",
)
def test_extraction_round_trip_with_embedding():
from second_brain.database import Database, reset_database_singleton
from second_brain.embeddings import close_pool, embed_extraction
from second_brain.models import (
Extraction,
Source,
SourceStatus,
SourceType,
utcnow,
)
reset_database_singleton()
db = Database()
# Unique URL so re-runs don't collide on the unique constraint.
unique = uuid.uuid4().hex[:8]
url = f"https://example.test/smoke/{unique}"
summary_text = (
"Smoke test extraction summary. "
"The second-brain pipeline embeds this string into Postgres. "
"Re-running the embed should be idempotent — delete-before-insert "
"keys on (source_schema, source_table, source_id, embedding_model)."
)
with db.session() as sess:
src = Source(
url=url,
title=f"smoke-test-{unique}",
domain="development",
source_type=SourceType.ARTICLE,
status=SourceStatus.ANALYZED,
ingested_at=utcnow(),
)
sess.add(src)
sess.flush()
ext = Extraction(source_id=src.id, summary=summary_text)
sess.add(ext)
sess.flush()
extraction_id = ext.id
source_id = src.id
try:
n1 = embed_extraction(extraction_id, summary_text)
assert n1 is not None and n1 >= 1, f"first embed should write rows, got {n1}"
# Idempotency: re-embed should produce the same chunk count and not
# accumulate orphan rows.
n2 = embed_extraction(extraction_id, summary_text)
assert n2 == n1, f"re-embed should match first count: {n1} vs {n2}"
raw_url = (
os.environ.get("SECOND_BRAIN_DATABASE_URL")
or os.environ.get("HERBYLAB_DATABASE_URL")
).replace("postgresql+psycopg://", "postgresql://", 1)
with psycopg.connect(raw_url) as conn, conn.cursor() as cur:
cur.execute(
"""
SELECT COUNT(*)
FROM public.embeddings
WHERE source_schema = 'second_brain'
AND source_table = 'extractions'
AND source_id = %s
AND embedding_model = 'nomic-embed-text'
""",
(extraction_id,),
)
(count,) = cur.fetchone()
assert count == n1, f"public.embeddings should hold {n1} rows, has {count}"
finally:
# Tidy up so the smoke test stays leak-free.
with db.session() as sess:
sess.query(Source).filter(Source.id == source_id).delete()
raw_url = (
os.environ.get("SECOND_BRAIN_DATABASE_URL")
or os.environ.get("HERBYLAB_DATABASE_URL")
).replace("postgresql+psycopg://", "postgresql://", 1)
with psycopg.connect(raw_url) as conn, conn.cursor() as cur:
cur.execute(
"""
DELETE FROM public.embeddings
WHERE source_schema = 'second_brain'
AND source_table = 'extractions'
AND source_id = %s
""",
(extraction_id,),
)
conn.commit()
close_pool()
reset_database_singleton()