"""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_reads_the_catalog_not_a_constant(): """The vocabulary moved from a module constant to the global catalog table (milestone 307): a constant cannot be a foreign key, so nothing outside a project could reference an area. The seed and the bootstrap ask must both read the table, or the list they show and the list they mint diverge.""" import inspect from scribe.services import systems as systems_svc assert not hasattr(systems_svc, "STANDARD_SYSTEMS"), ( "the constant is gone — the catalog table is the single source" ) source = inspect.getsource(systems_svc.seed_standard_systems) assert "list_canonical_systems" in source assert "canonical_id=entry.id" in source, ( "a seeded System must be mapped as it is created, or a project born " "from the standard set still needs a reconciliation pass" )