CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 42s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / Python tests (push) Successful in 1m37s
CI & Build / Build & push image (push) Successful in 26s
CI 7016. `test_non_task_facets_are_the_note_types_and_only_those` pins the non-task vocabulary as a literal set, so adding `lesson` to `_FACETS` turned it red — the guard working, not breaking. It stays a literal: derived from _FACETS it would assert nothing, and rule 167 wants a guard that can fail. The representative corpus had no lesson row, so `lesson` was reaching only the two tests that iterate FACET_TYPES and never the one that asserts each facet selects EXACTLY its own rows. It has one now, which is what pins the half that matters: a lesson is not picked up by the Notes facet despite both being non-task records. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
127 lines
5.0 KiB
Python
127 lines
5.0 KiB
Python
"""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(),
|
|
"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` joined in milestone 385 step
|
|
2 (#3729) and 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
|