"""Project inception (milestone 297) — step 1: the record's shape. The WHY a project inherits what it does lives on projects.inception; the opt-out of an always-on rulebook is its own association table. Pure validation is pinned here; the effects are step 3's integration tests. """ from scribe.models import Base from scribe.models.project import Project from scribe.models.rulebook import project_rulebook_exclusions from scribe.services.inception import ( CHOICE_KEYS, INCEPTION_VIAS, is_decided, normalize_choices, validate_inception, ) def test_project_carries_an_inception_record_and_to_dict_shows_it(): assert "inception" in Project.__table__.c assert Project.__table__.c.inception.nullable # NULL = undecided p = Project(user_id=1, title="x", inception=None) assert p.to_dict()["inception"] is None and not is_decided(p) p.inception = {"via": "mcp", "decided_at": "2026-08-22T00:00:00+00:00", "decided_by": 1, "choices": normalize_choices({"seed_systems": True})} assert is_decided(p) and p.to_dict()["inception"]["via"] == "mcp" assert INCEPTION_VIAS == ("mcp", "ui", "legacy") def test_exclusions_table_is_the_suppressions_sibling(): t = Base.metadata.tables["project_rulebook_exclusions"] assert project_rulebook_exclusions is t assert {c.name for c in t.primary_key.columns} == {"project_id", "rulebook_id"} fks = {fk.column.table.name: fk.ondelete for c in t.columns for fk in c.foreign_keys} assert fks == {"projects": "CASCADE", "rulebooks": "CASCADE"} def test_validate_inception_pins_the_choice_vocabulary(): assert CHOICE_KEYS == ("exclude_always_on_rulebooks", "subscribe_rulebooks", "design_system_id", "seed_systems") assert validate_inception({}) is None assert validate_inception({"exclude_always_on_rulebooks": [1], "subscribe_rulebooks": [2], "design_system_id": 3, "seed_systems": True}) is None assert validate_inception({"design_system_id": None}) is None assert "must be an object" in validate_inception([]) assert "unknown inception choice" in validate_inception({"repo": "x"}) assert "list of rulebook ids" in validate_inception({"exclude_always_on_rulebooks": "1"}) assert "list of rulebook ids" in validate_inception({"subscribe_rulebooks": [0]}) assert "list of rulebook ids" in validate_inception({"subscribe_rulebooks": [True]}) assert "both excluded and subscribed" in validate_inception( {"exclude_always_on_rulebooks": [1, 2], "subscribe_rulebooks": [2]}) assert "positive id or null" in validate_inception({"design_system_id": 0}) assert "positive id or null" in validate_inception({"design_system_id": True}) assert "true or false" in validate_inception({"seed_systems": "yes"}) def test_normalize_choices_is_canonical_and_complete(): out = normalize_choices({"subscribe_rulebooks": [3, 1, 3], "exclude_always_on_rulebooks": [2]}) assert out == {"exclude_always_on_rulebooks": [2], "subscribe_rulebooks": [1, 3], "design_system_id": None, "seed_systems": False} assert normalize_choices(None) == {"exclude_always_on_rulebooks": [], "subscribe_rulebooks": [], "design_system_id": None, "seed_systems": False} def test_standard_systems_vocabulary_is_one_list_for_ask_and_seed(): from scribe.mcp.tools.systems import _STANDARD_SYSTEMS from scribe.services.systems import STANDARD_SYSTEMS assert _STANDARD_SYSTEMS == tuple(n for n, _ in STANDARD_SYSTEMS) assert len(STANDARD_SYSTEMS) == 8 and all(charter for _, charter in STANDARD_SYSTEMS)