Files
FabledScribe/tests/test_integration_lesson_reach.py
T
bvandeusenandClaude Opus 5.5 f4e9cd429b
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 14s
CI & Build / TypeScript typecheck (push) Successful in 54s
CI & Build / integration (push) Successful in 1m1s
CI & Build / Python tests (push) Successful in 1m40s
CI & Build / Build & push image (push) Canceled after 27s
feat(embeddings): every vector records the model whose space it lives in (#4132)
The four embedding tables stamped chunker_version but not the model, and
vector(384) is a width, not an identity: a same-width model swap would
write a second geometry beside the first with no error.

- embedding_model on note/rule/milestone/system embeddings (0109; existing
  rows stamped with the only model any install has ever run).
- Every write stamps EMBEDDING_MODEL; every backfill's "current" test is
  is_current_stamp(), both halves of calibration_stamp().
- migrate_floor refuses while any row its surface searches is off the live
  model, before sampling: re-embed, then migrate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-23 19:02:30 -04:00

145 lines
5.6 KiB
Python

"""A lesson is reachable from a project it was not written on (step 3).
WHY THIS IS AN INTEGRATION TEST
The carve-out is one `OR` inside the query's project filter, and what has to be
proved is which ROWS come back — a mock session returns whatever it was told to
and would pass with the predicate inverted. Every note here embeds identically
to the query, so the only thing that can separate them is the scoping: a leak
and a correct result are otherwise indistinguishable.
The embedder is stubbed, as in the other pgvector tests, so this depends on
Postgres and the query rather than on a downloaded model. No similarity number
is asserted — only membership.
"""
import uuid
from unittest.mock import AsyncMock, patch
import pytest
import pytest_asyncio
from scribe.models import async_session
from scribe.models.embedding import EMBEDDING_DIM, NoteEmbedding
from scribe.models.note import Note
from scribe.models.project import Project
from scribe.services import lessons as lessons_svc
from scribe.services.embeddings import CHUNKER_VERSION, EMBEDDING_MODEL, semantic_search_notes
from tests.helpers import ensure_user
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
QUERY_VEC = [1.0] + [0.0] * (EMBEDDING_DIM - 1)
@pytest_asyncio.fixture
async def corpus():
"""A lesson and a plain note on project A, plus a lesson on project B.
Fresh users per run: every row matches the query equally, so a record left
behind by another test would read exactly like a scoping leak.
"""
tag = uuid.uuid4().hex[:8]
async with async_session() as s:
owner = await ensure_user(s, f"lesson_reach_owner_{tag}")
await s.flush()
a = Project(user_id=owner.id, title="Where it was learned")
b = Project(user_id=owner.id, title="Somewhere else entirely")
s.add_all([a, b])
await s.flush()
rows = {
"lesson_on_a": Note(
user_id=owner.id, project_id=a.id,
note_type=lessons_svc.LESSON_NOTE_TYPE,
title="Suspect the guard — a test fails on correct code",
body="**When to apply:** a test fails on correct code",
),
"note_on_a": Note(
user_id=owner.id, project_id=a.id, note_type="note",
title="An ordinary note", body="ordinary body",
),
"lesson_on_b": Note(
user_id=owner.id, project_id=b.id,
note_type=lessons_svc.LESSON_NOTE_TYPE,
title="A lesson that lives on B", body="**When to apply:** on B",
),
}
s.add_all(rows.values())
await s.flush()
for note in rows.values():
s.add(NoteEmbedding(
note_id=note.id, chunk_index=0, user_id=owner.id,
embedding=QUERY_VEC, chunk_text=note.title,
chunker_version=CHUNKER_VERSION,
embedding_model=EMBEDDING_MODEL,
))
ids = {k: n.id for k, n in rows.items()}
ids["owner"], ids["a"], ids["b"] = owner.id, a.id, b.id
await s.commit()
return ids
async def _search(uid, **kw):
with patch(
"scribe.services.embeddings.get_embedding", AsyncMock(return_value=QUERY_VEC)
):
hits = await semantic_search_notes(uid, "when does this apply", limit=20, **kw)
return {note.id for _score, note in hits}
async def test_a_lesson_is_found_from_another_project(corpus):
"""THE acceptance this step exists for. Searching project B reaches the
lesson written on project A — the case the kind was created for, because a
transferable insight is most useful on the project that has not learned it
yet."""
found = await _search(
corpus["owner"], project_id=corpus["b"], include_global_kinds=True,
)
assert corpus["lesson_on_a"] in found
assert corpus["lesson_on_b"] in found
async def test_an_ordinary_note_stays_where_it_was_written(corpus):
"""The other half, and the one that would make this change a bug. Project
scoping is deliberate for every other kind; the carve-out admits ONE kind
rather than weakening the filter."""
found = await _search(
corpus["owner"], project_id=corpus["b"], include_global_kinds=True,
)
assert corpus["note_on_a"] not in found
async def test_the_carve_out_is_off_unless_asked_for(corpus):
"""Default off, because the near-duplicate gate and ordinary recall both
depend on the project filter holding. A globally visible kind arriving
there would let a lesson block an unrelated note's create on a project its
author never touched."""
found = await _search(corpus["owner"], project_id=corpus["b"])
assert found == {corpus["lesson_on_b"]}
async def test_the_home_project_is_unchanged(corpus):
"""Searching the project a lesson was written on returns it either way —
the carve-out adds reach, it does not move anything."""
for flag in (False, True):
found = await _search(
corpus["owner"], project_id=corpus["a"], include_global_kinds=flag,
)
assert corpus["lesson_on_a"] in found
assert corpus["note_on_a"] in found
async def test_a_kind_filter_still_means_what_it_says(corpus):
"""The carve-out widens the PROJECT filter only. A caller narrowing to
snippets asked for snippets, and quietly handing it lessons would make
`note_type` mean something different depending on a flag it did not set."""
found = await _search(
corpus["owner"], project_id=corpus["b"],
include_global_kinds=True, note_type="snippet",
)
assert found == set()