CI & Build / Python lint (push) Failing after 6s
CI & Build / Plugin hooks (push) Successful in 11s
CI & Build / integration (push) Failing after 26s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 1m7s
CI & Build / Build & push image (push) Skipped
The payoff step: a rule stops having to be resident to be honoured. - list_always_on_rules returns the ALWAYS-ON tier only. It is the session-start call, made before any project is in scope, so there is no area vocabulary to match a conditional rule against yet. - get_applicable_rules carries a conditional rule when the project works in an area the rule is tagged to — resolved through systems.canonical_id, so the project's own NAME for the area is irrelevant, which is the entire reason the catalog exists. The gate is applied IN SQL, so `limit` counts rules that will actually surface rather than rules about to be dropped. - Bindingness is a deterministic TAG match, never a similarity score (D7). The vector channel stays a suggestion, in search. co_surfaced_partners is the fix that rule 144 never had. It was split off rule 46 and folded back the same day because "either rule could surface without the other and miss exposing a project to what the entire shape is intended to be" — correct, and merging was the only remedy available. Now a partner ARRIVES with its other half even when nothing else selected it, tagged `via: co_surfaces` so the payload says why. Two limits, both deliberate: only rules the caller owns, because an edge is not a back door into someone else's rulebook; and a project's suppressions are passed as exclusions, because an explicit mute is a decision and an edge does not outrank it. COMPATIBILITY, asserted first in the integration test rather than reasoned about: a rule with no tier, no areas and no edges binds exactly as it did before any of this existed. `tier` defaults to always_on, so an install upgrades and every rule it already had keeps arriving. Getting that backwards would silently stop enforcing rules people rely on, which is worse than any amount of payload bloat. Four unit tests were coupled to the ORDER of a mocked session's execute() calls, so a new query broke them. Rather than pad the sequence and deepen that coupling, the three post-query lookups are stubbed by name — they have their own coverage, and the real wiring is proven against Postgres. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
176 lines
7.9 KiB
Python
176 lines
7.9 KiB
Python
"""Real-Postgres tests for WHICH rules reach a session (milestone 307 step 5).
|
|
|
|
What mocks can't prove, and what this milestone must not get wrong:
|
|
|
|
1. **Nothing stops binding.** A rule with no tier, no areas and no edges
|
|
behaves exactly as it did before tiers existed. That is the one failure this
|
|
whole design must not produce, and it is asserted first.
|
|
2. A conditional rule is invisible to a project that doesn't work in its area,
|
|
and arrives — binding, not suggested — to one that does.
|
|
3. A `co_surfaces` partner arrives with its other half, which is the failure
|
|
that made merging rule 144 into rule 46 look like the only fix.
|
|
4. An explicit suppression outranks an edge.
|
|
"""
|
|
import pytest
|
|
import pytest_asyncio
|
|
|
|
from scribe.models import async_session
|
|
from scribe.models.project import Project
|
|
from scribe.models.rulebook import Rulebook
|
|
from scribe.services import canonical_systems as canonical_svc
|
|
from scribe.services import rulebooks as rulebooks_svc
|
|
from scribe.services import systems as systems_svc
|
|
from tests.helpers import ensure_user
|
|
|
|
pytestmark = [pytest.mark.integration, pytest.mark.usefixtures("_dispose_engine")]
|
|
|
|
|
|
@pytest_asyncio.fixture
|
|
async def world():
|
|
"""A project with TWO rulebooks, because the two payloads are different sets.
|
|
|
|
`list_always_on_rules` covers always-on rulebooks; `get_applicable_rules`
|
|
covers SUBSCRIBED ones. Conflating them is easy and would make these tests
|
|
assert nothing, so the fixture carries one of each and every test says
|
|
which payload it is about.
|
|
"""
|
|
async with async_session() as s:
|
|
owner = await ensure_user(s, "surfacing_owner")
|
|
project = Project(user_id=owner.id, title="Surfacing target")
|
|
s.add(project)
|
|
await s.flush()
|
|
ids = {"owner": owner.id, "pid": project.id}
|
|
await s.commit()
|
|
|
|
always = await rulebooks_svc.create_rulebook(ids["owner"], "Family standards")
|
|
async with async_session() as s:
|
|
rb = await s.get(Rulebook, always.id)
|
|
rb.always_on = True
|
|
await s.commit()
|
|
always_topic = await rulebooks_svc.create_topic(always.id, ids["owner"], "git")
|
|
await rulebooks_svc.create_rule(
|
|
always_topic.id, ids["owner"], "dev is home", "Work on dev.",
|
|
)
|
|
|
|
book = await rulebooks_svc.create_rulebook(ids["owner"], "Subscribed practices")
|
|
topic = await rulebooks_svc.create_topic(book.id, ids["owner"], "release")
|
|
plain = await rulebooks_svc.create_rule(
|
|
topic.id, ids["owner"], "Between batches, keep stacking", "Keep going.",
|
|
)
|
|
await rulebooks_svc.subscribe_project(
|
|
project_id=ids["pid"], rulebook_id=book.id, user_id=ids["owner"],
|
|
)
|
|
ids.update({
|
|
"always": always.id, "always_topic": always_topic.id,
|
|
"book": book.id, "topic": topic.id, "plain": plain.id,
|
|
})
|
|
return ids
|
|
|
|
|
|
async def _titles(ids) -> set[str]:
|
|
applicable = await rulebooks_svc.get_applicable_rules(ids["pid"], ids["owner"])
|
|
return {r["title"] for r in applicable["rules"]}
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_a_rule_with_no_tier_no_areas_and_no_edges_binds_exactly_as_before(world):
|
|
"""THE compatibility guarantee. An install upgrades and every rule it
|
|
already had keeps arriving — no tier set, no areas, no edges, still bound.
|
|
Getting this wrong would silently stop enforcing rules people rely on,
|
|
which is worse than any amount of payload bloat."""
|
|
always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
|
|
assert "dev is home" in {r.title for r in always_on}
|
|
assert "Between batches, keep stacking" in await _titles(world)
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_a_conditional_rule_is_reachable_not_resident(world):
|
|
"""It leaves the session-start payload entirely — that is the point of the
|
|
tier — and it does NOT reach a project with no matching area."""
|
|
# In the ALWAYS-ON book: the tier alone keeps it out of the session-start
|
|
# payload, which is the whole point of the tier.
|
|
resident = await rulebooks_svc.create_rule(
|
|
world["always_topic"], world["owner"], "Release tagging", "Derive the tag.",
|
|
when_to_apply="when cutting a release", tier="conditional",
|
|
)
|
|
assert resident.tier == "conditional"
|
|
always_on = await rulebooks_svc.list_always_on_rules(world["owner"])
|
|
assert "Release tagging" not in {r.title for r in always_on}
|
|
|
|
# In the SUBSCRIBED book, untagged: the project has no area to reach it by,
|
|
# so it stays out of the project payload too. Absent for a DIFFERENT reason
|
|
# than above, which is why both are asserted.
|
|
await rulebooks_svc.create_rule(
|
|
world["topic"], world["owner"], "Untagged conditional", "No area yet.",
|
|
when_to_apply="sometime", tier="conditional",
|
|
)
|
|
assert "Untagged conditional" not in await _titles(world)
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_a_conditional_rule_binds_a_project_that_works_in_its_area(world):
|
|
"""The payoff: the tag match carries it in deterministically. The project
|
|
reaches the area through its own System's canonical_id — its local NAME is
|
|
irrelevant, which is the whole reason the catalog exists."""
|
|
area = await canonical_svc.find_by_name("CI & Release")
|
|
assert area is not None, "migration 0087 seeds the standard vocabulary"
|
|
|
|
rule = await rulebooks_svc.create_rule(
|
|
world["topic"], world["owner"], "Release tagging", "Derive the tag.",
|
|
when_to_apply="when cutting a release", tier="conditional",
|
|
)
|
|
await rulebooks_svc.set_rule_systems(rule.id, world["owner"], [area.id])
|
|
|
|
# Still absent: the project has no Systems at all yet.
|
|
assert "Release tagging" not in await _titles(world)
|
|
|
|
# The project names the area with its OWN word, mapped to the same canon.
|
|
local = await systems_svc.create_system(
|
|
world["owner"], world["pid"], "CI & runners", description="ours",
|
|
)
|
|
await canonical_svc.set_system_canonical(world["owner"], local.id, area.id)
|
|
|
|
surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
|
|
hit = [r for r in surfaced["rules"] if r["title"] == "Release tagging"]
|
|
assert hit, "a tagged conditional rule must bind a project working in that area"
|
|
assert [s["name"] for s in hit[0]["systems"]] == ["CI & Release"]
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_co_surfaces_drags_in_the_half_that_would_have_been_missed(world):
|
|
"""Rule 144 was split off rule 46 and folded back the same day because
|
|
"either rule could surface without the other". This is the edge that makes
|
|
that unnecessary: the partner arrives even though nothing else selected
|
|
it, and says why it is here."""
|
|
partner = await rulebooks_svc.create_rule(
|
|
world["topic"], world["owner"], "Version names are labels",
|
|
"A name decides nothing.",
|
|
when_to_apply="when naming a build", tier="conditional",
|
|
)
|
|
await rulebooks_svc.add_rule_relation(
|
|
world["owner"], world["plain"], partner.id, "co_surfaces",
|
|
note="they fail together",
|
|
)
|
|
surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
|
|
hit = [r for r in surfaced["rules"] if r["title"] == "Version names are labels"]
|
|
assert hit, "a co_surfaces partner must arrive with its other half"
|
|
assert hit[0]["via"] == "co_surfaces"
|
|
|
|
|
|
@pytest.mark.integration
|
|
async def test_a_suppression_outranks_an_edge(world):
|
|
"""The edge says these belong together; the suppression says this project
|
|
does not want that one. An explicit decision beats an inferred one."""
|
|
partner = await rulebooks_svc.create_rule(
|
|
world["topic"], world["owner"], "Muted partner", "Should not arrive.",
|
|
tier="conditional",
|
|
)
|
|
await rulebooks_svc.add_rule_relation(
|
|
world["owner"], world["plain"], partner.id, "co_surfaces",
|
|
)
|
|
await rulebooks_svc.suppress_rule_for_project(
|
|
world["pid"], partner.id, world["owner"],
|
|
)
|
|
surfaced = await rulebooks_svc.get_applicable_rules(world["pid"], world["owner"])
|
|
assert "Muted partner" not in {r["title"] for r in surfaced["rules"]}
|