Files
FabledScribe/tests/test_integration_inception.py
T
bvandeusenandClaude Opus 5 0bcd4b5540
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 49s
CI & Build / TypeScript typecheck (push) Successful in 57s
CI & Build / Python tests (push) Failing after 1m3s
CI & Build / Build & push image (push) Skipped
feat(rules)!: retire rulebook subscriptions and per-project suppressions (#4052)
A rule's home is its scope now: a rule in a rulebook topic is global, a rule on
a project applies to that project, and retrieval reads that directly (#4074).
A subscription had stopped changing anything a session received; a suppression
muted rules from a subscription. Operator, 2026-09-15: "we have global and
project scoped rules, we don't need the subscriptions now."

What goes, whole (rule 22):
- Migration 0101 drops project_rulebook_subscriptions, project_rule_suppressions
  and project_topic_suppressions, and strips subscribe_rulebooks (and 394's
  leftover exclude_always_on_rulebooks) from stored inception choices.
- Service, MCP and REST: subscribe/unsubscribe and the four suppress/unsuppress
  operations. The Subscribers checklist, the subscribe chips, the skip buttons
  and the Suppressed section in the rules UI.
- Inception asks two questions (design system, seed Systems). create_project and
  decide_project_inception lose subscribe_rulebooks.
- Backup v15 stops exporting the three sections; older archives still restore,
  the keys simply unread. Trash no longer hard-deletes suppression rows.

What changes meaning:
- get_applicable_rules is a project's LISTING: its own rules, plus the global
  rules tagged to an area it works in. Untagged global rules apply everywhere
  and arrive by retrieval, so they are not listed. A co_surfaces partner on a
  different project is not dragged in.
- list_rules(project_id) lists that project's own rules.
- rules_payload drops subscribed_rulebooks and suppressed_*; the handshake's
  brief form is project_rules alone.
- using-scribe's "Where a new rule goes" and inception sections, tool
  docstrings and docs say global vs project. Plugin 2026.09.15.1620.

Milestone 414 step 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
2026-09-15 12:20:57 -04:00

95 lines
4.2 KiB
Python

"""Real-Postgres integration tests for project inception (milestone 297).
What mocks can't prove: a decision's effects land through the real services
(the design system points, the standard Systems seed once), the record is
written last, a bad target applies nothing.
Rules are not part of inception since milestone 414: a rule in a rulebook is
global and applies to every project, so there is nothing to subscribe to.
"""
import pytest
import pytest_asyncio
from scribe.models import async_session
from scribe.models.project import Project
from scribe.services import inception as inception_svc
from scribe.services import canonical_systems as canonical_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 seeded():
"""Owner and a fresh, undecided project."""
async with async_session() as s:
owner = await ensure_user(s, "inception_owner")
project = Project(user_id=owner.id, title="Inception target")
s.add(project)
await s.flush()
ids = {"owner": owner.id, "pid": project.id}
await s.commit()
return ids
@pytest.mark.integration
async def test_decide_applies_every_effect_and_records_last(seeded):
owner, pid = seeded["owner"], seeded["pid"]
defaults = await inception_svc.current_defaults(owner, pid)
assert set(defaults) == {"design_system_id", "design_systems", "systems"}
assert defaults["systems"] == 0 and defaults["design_system_id"] is None
out = await inception_svc.decide(owner, pid, via="mcp", choices={
"design_system_id": None,
"seed_systems": True,
})
assert set(out["effects"]) == {"design_system_id", "systems_seeded"}
catalog = await canonical_svc.list_canonical_systems()
assert len(out["effects"]["systems_seeded"]) == len(catalog)
# Seeded Systems come out mapped, not needing a later reconciliation.
seeded_systems = await systems_svc.list_systems(owner, pid)
assert all(s.canonical_id is not None for s in seeded_systems)
# The record, written last, says why.
async with async_session() as s:
project = await s.get(Project, pid)
assert inception_svc.is_decided(project)
assert project.inception["via"] == "mcp" and project.inception["decided_by"] == owner
assert project.inception["choices"] == {"design_system_id": None, "seed_systems": True}
# Re-deciding with seed again mints nothing twice.
again = await inception_svc.decide(owner, pid, via="ui", choices={"seed_systems": True})
assert again["effects"]["systems_seeded"] == []
assert len(await systems_svc.list_systems(owner, pid)) == len(catalog)
@pytest.mark.integration
async def test_a_bad_decision_applies_nothing(seeded):
owner, pid = seeded["owner"], seeded["pid"]
# A design system that is not readable is refused BEFORE any effect
# lands — the seed must not happen on a decision that fails.
with pytest.raises(ValueError, match="not found"):
await inception_svc.decide(owner, pid, via="mcp", choices={
"design_system_id": 999999, "seed_systems": True,
})
assert await systems_svc.list_systems(owner, pid) == []
# The retired rulebook choice is an unknown key, refused whole: a caller
# still passing it must not believe a rulebook was subscribed.
with pytest.raises(ValueError, match="unknown inception choice"):
await inception_svc.decide(owner, pid, via="mcp", choices={
"subscribe_rulebooks": [1], "seed_systems": True,
})
assert await systems_svc.list_systems(owner, pid) == []
with pytest.raises(ValueError, match="legacy"):
await inception_svc.decide(owner, pid, via="legacy", choices={})
async with async_session() as s:
project = await s.get(Project, pid)
assert not inception_svc.is_decided(project)
# An outsider cannot decide someone else's project.
async with async_session() as s:
other = await ensure_user(s, "inception_other")
other_id = other.id
await s.commit()
with pytest.raises(ValueError, match="not found"):
await inception_svc.decide(other_id, pid, via="mcp", choices={})