"""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. Those three used to be hand-written and agreed only by luck: adding `issue` to the SQL arm alone would have set the pre-filter to is_task=False, handed the Python arm a candidate set with no tasks in it, and returned an empty semantic half for the Issues facet forever with nothing red anywhere (#3128). They are now generated from one table. These tests pin the property that made 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(), "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"}), ("", 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(): assert set(NON_TASK_FACETS) == {"note", "process", "snippet"} @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