"""The type facet, in both of its dialects. The facet predicate is written twice by necessity — as SQL for rows the database hands back, and as Python for candidates the vector search has already fetched — plus a third time as the `is_task` pre-filter that narrows the candidate set before it exists. Hand-written, those three agree only by luck: adding `issue` to the SQL arm alone would set the pre-filter to is_task=False, hand the Python arm a candidate set with no tasks in it, and return an empty semantic half for the Issues facet forever with nothing red anywhere. So they are generated from one table, and these tests pin the property that makes the trap possible, so it cannot come back by a different route. """ import pytest from scribe.services.knowledge import ( _FACETS, FACET_TYPES, NON_TASK_FACETS, _apply_type_filter, facet_is_task, matches_facet, ) from tests.helpers import compiled_sql, fake_note, fake_snippet, fake_task def _sql(note_type): from sqlalchemy import select from scribe.models.note import Note return compiled_sql(_apply_type_filter(select(Note.id), note_type)) # One representative row per shape the corpus actually holds. ROWS = { "plain note": fake_note(note_type="note"), "process": fake_note(note_type="process"), "snippet": fake_snippet(), "lesson": fake_note(note_type="lesson"), "work task": fake_task(task_kind="work", note_type="note"), "issue": fake_task(task_kind="issue", note_type="note"), "spike": fake_task(task_kind="spike", note_type="note"), "legacy plan": fake_task(task_kind="plan", note_type="note"), } @pytest.mark.parametrize("facet", sorted(FACET_TYPES)) def test_pre_filter_never_excludes_a_row_the_facet_wants(facet): """THE regression. `facet_is_task` narrows the semantic candidate set before `matches_facet` ever sees it, so a pre-filter that disagrees with the predicate doesn't return wrong rows — it returns NO rows, silently, on one half of a hybrid search.""" want = facet_is_task(facet) for label, row in ROWS.items(): if matches_facet(row, facet): assert want is None or want == row.is_task, ( f"facet {facet!r} accepts the {label} row, but its pre-filter " f"asks for is_task={want} while the row has is_task={row.is_task} " f"— the semantic arm would never be handed this row" ) @pytest.mark.parametrize( "facet,expected", [ ("task", {"work task", "issue", "spike", "legacy plan"}), ("issue", {"issue"}), ("spike", {"spike"}), ("work", {"work task"}), ("plan", {"legacy plan"}), ("note", {"plain note"}), ("process", {"process"}), ("snippet", {"snippet"}), ("lesson", {"lesson"}), ("", set(ROWS)), ], ) def test_each_facet_selects_exactly_its_own_rows(facet, expected): assert {k for k, row in ROWS.items() if matches_facet(row, facet)} == expected def test_a_plain_note_is_not_selected_by_its_own_type_when_it_is_a_task(): """A task's `note_type` is 'note' — that column says nothing about task-ness. The hand-written Python arm omitted the `status IS NULL` half its SQL twin carried, so it only avoided returning every task under the Notes facet because the pre-filter had already dropped them.""" assert matches_facet(fake_task(note_type="note"), "note") is False def test_an_unknown_facet_matches_nothing_rather_than_everything(): """A typo must return an empty list, never the whole corpus.""" assert all(not matches_facet(row, "wrok") for row in ROWS.values()) assert "notes.status IS NULL" in _sql("wrok") def test_the_live_task_kinds_are_all_facets(): """`issue` shipped in 0065 and `spike` in 0091; the browse vocabulary went three kinds without noticing either.""" for kind in ("work", "issue", "spike"): assert kind in FACET_TYPES and _FACETS[kind][0] is True def test_non_task_facets_are_the_note_types_and_only_those(): """Spelled out rather than derived, so adding a kind to `_FACETS` has to be a deliberate edit in two places. `lesson` is non-task on purpose: `is_task` IS `status is not None`, so a lesson that acquired a status would stop being a lesson.""" assert set(NON_TASK_FACETS) == {"note", "process", "snippet", "lesson"} @pytest.mark.parametrize("facet", sorted(FACET_TYPES)) def test_sql_arm_constrains_the_axis_the_facet_lives_on(facet): """The SQL dialect of the same table. A task facet must pin `status` (and, for a single kind, `task_kind`); a record-type facet must pin `note_type` AND exclude tasks.""" sql = _sql(facet) is_task, value = _FACETS[facet] assert "notes.deleted_at IS NULL" in sql if is_task: assert "notes.status IS NOT NULL" in sql assert (f"notes.task_kind = '{value}'" in sql) is (value is not None) else: assert "notes.status IS NULL" in sql assert f"notes.note_type = '{value}'" in sql