"""The parent chain: walking it, and refusing to close it (milestone #254 step 1). Pure functions over a `{id: parent_id}` literal, so a whole hierarchy is one line of setup and no database is involved. That is the reason the cascade lives in its own import-free module — see services/design_cascade.py. """ from types import SimpleNamespace from scribe.services.design_cascade import ancestry, resolve_tokens, would_cycle # --- ancestry --------------------------------------------------------------- def test_ancestry_returns_the_chain_deepest_first(): """Deepest first because that is the order resolution consumes it in: the system being resolved wins over its parent, which wins over the root.""" parents = {1: None, 2: 1, 3: 2} assert ancestry(3, parents) == [3, 2, 1] def test_a_root_is_its_own_whole_chain(): """A family system has no parent, and that is an ordinary state — not an incomplete one. It must resolve to exactly itself.""" assert ancestry(1, {1: None}) == [1] def test_a_missing_parent_truncates_rather_than_raising(): """A parent that was soft-deleted (or filtered out of the caller's scope) is a shorter chain, not a failed request. The alternative — raising — would make one deleted system break every descendant's rendering.""" assert ancestry(3, {3: 2}) == [3, 2] def test_a_system_absent_from_the_map_still_yields_itself(): assert ancestry(9, {}) == [9] def test_ancestry_terminates_on_a_cycle_instead_of_hanging(): """LOAD-BEARING, and the reason a visited-set exists even though writes are guarded. A loop introduced by a direct DB edit or a future bug must degrade to a truncated chain: truncation shows up in the result, a hang shows up as an outage. Every id appears exactly once.""" parents = {1: 3, 2: 1, 3: 2} chain = ancestry(1, parents) assert chain == [1, 3, 2] assert len(chain) == len(set(chain)) def test_ancestry_terminates_on_a_self_parent(): assert ancestry(1, {1: 1}) == [1] # --- would_cycle ------------------------------------------------------------ def test_clearing_the_parent_never_cycles(): """None means "make this a root", which is always safe.""" assert would_cycle(2, None, {1: None, 2: 1}) is False def test_a_system_cannot_be_its_own_parent(): assert would_cycle(1, 1, {1: None}) is True def test_an_ordinary_reparent_is_allowed(): """family <- app is the shape the whole model exists for; it must not trip the guard.""" assert would_cycle(2, 1, {1: None, 2: None}) is False def test_a_direct_swap_is_refused(): """A -> B, then B -> A. The two-system case, and the one a UI produces first because both systems are on screen together.""" parents = {1: None, 2: 1} assert would_cycle(1, 2, parents) is True def test_an_indirect_loop_is_refused(): """Three deep: root <- mid <- leaf, then root's parent set to leaf. Catching this is what makes the check a chain walk rather than a parent comparison.""" parents = {1: None, 2: 1, 3: 2} assert would_cycle(1, 3, parents) is True def test_reparenting_onto_a_sibling_subtree_is_allowed(): """Two branches off one root. Moving one under the other is legitimate — the guard must refuse loops, not reorganisation.""" parents = {1: None, 2: 1, 3: 1} assert would_cycle(3, 2, parents) is False def test_the_guard_survives_a_hierarchy_that_is_already_corrupt(): """If a cycle somehow already exists, the guard still has to answer rather than spin — the write path is exactly where such a hierarchy gets repaired.""" parents = {1: 2, 2: 1, 3: None} assert would_cycle(3, 1, parents) is False assert would_cycle(1, 2, parents) is True # --- resolution ------------------------------------------------------------- # # The cascade proper (milestone #254 step 2). Hierarchies are stated as literals # because resolve_tokens is pure and duck-typed — which is the whole reason it # lives here rather than inside the service. def _token(name, value_by_mode, group_name=None, purpose=None, order_index=0, supersedes=None): return SimpleNamespace( name=name, value_by_mode=value_by_mode, group_name=group_name, purpose=purpose, order_index=order_index, supersedes=supersedes or [], ) # A family (1) and an app inheriting from it (2) — the shape the model exists for. FAMILY, APP = 1, 2 PARENTS = {FAMILY: None, APP: FAMILY} def _by_name(tokens): return {t.name: t for t in tokens} def test_a_system_with_no_tokens_of_its_own_inherits_the_whole_family_set(): """The correct answer for an app that has not departed from the family yet — and the state every app system starts in.""" resolved = resolve_tokens( APP, PARENTS, {FAMILY: [_token("--fs-obsidian", {"base": "#14171a"})], APP: []}, ) assert [t.name for t in resolved] == ["--fs-obsidian"] assert resolved[0].value_by_mode == {"base": "#14171a"} assert resolved[0].origin_by_mode == {"base": FAMILY} def test_the_deepest_system_wins_and_says_what_it_overrode(): """Provenance is the point of the whole model: the effective set is just a list without it, and "where does this app depart from the family?" becomes a diff someone has to compute.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-accent", {"base": "#6b2118"})], APP: [_token("--fs-accent", {"base": "#5b4a8a"})], }, )) accent = resolved["--fs-accent"] assert accent.value_by_mode == {"base": "#5b4a8a"} assert accent.origin_by_mode == {"base": APP} # The shadowed ancestor is still there, behind the winner. assert [c.system_id for c in accent.contributions["base"]] == [APP, FAMILY] assert accent.contributions["base"][1].value == "#6b2118" def test_overriding_one_mode_leaves_the_others_inherited(): """LOAD-BEARING, and the argument that decided the storage shape. An accent deepened for light backgrounds while dark is left alone must own `base` and still inherit `dark` — which is why merging is per (name, MODE) and why the value column is a map rather than a pair of columns.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-accent", {"base": "#34a877", "dark": "#34a877"})], APP: [_token("--fs-accent", {"base": "#15803d"})], }, )) accent = resolved["--fs-accent"] assert accent.value_by_mode == {"base": "#15803d", "dark": "#34a877"} assert accent.origin_by_mode == {"base": APP, "dark": FAMILY} def test_a_token_only_the_app_defines_is_not_an_override(): """Introducing a token and overriding one are different acts, and a UI that labelled both "overridden here" would misdescribe the first.""" resolved = _by_name(resolve_tokens( APP, PARENTS, {FAMILY: [], APP: [_token("--fs-editor-caret", {"base": "#5b4a8a"})]}, )) caret = resolved["--fs-editor-caret"] assert caret.origin_by_mode == {"base": APP} assert caret.is_overridden_in(APP) is False def test_is_overridden_in_is_true_only_for_the_system_that_shadowed(): resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-accent", {"base": "#6b2118"})], APP: [_token("--fs-accent", {"base": "#5b4a8a"})], }, )) accent = resolved["--fs-accent"] assert accent.is_overridden_in(APP) is True assert accent.is_overridden_in(FAMILY) is False def test_three_levels_stack_nearest_first(): """A chain deeper than family->app, to prove the walk isn't a single hop.""" parents = {1: None, 2: 1, 3: 2} resolved = _by_name(resolve_tokens( 3, parents, { 1: [_token("--fs-bg", {"base": "a"})], 2: [_token("--fs-bg", {"base": "b"})], 3: [_token("--fs-bg", {"base": "c"})], }, )) bg = resolved["--fs-bg"] assert bg.value_by_mode == {"base": "c"} assert [c.value for c in bg.contributions["base"]] == ["c", "b", "a"] def test_resolving_the_family_itself_ignores_its_children(): """Inheritance runs one way. A family system resolved on its own must not pick up an app's overrides — otherwise every app would restyle the house.""" resolved = _by_name(resolve_tokens( FAMILY, PARENTS, { FAMILY: [_token("--fs-accent", {"base": "#6b2118"})], APP: [_token("--fs-accent", {"base": "#5b4a8a"})], }, )) assert resolved["--fs-accent"].value_by_mode == {"base": "#6b2118"} def test_value_for_falls_back_to_the_base_mode(): """A token that isn't mode-dependent carries only `base`, and asking it for "dark" must yield that rather than nothing — the read rule the storage shape implies.""" resolved = _by_name(resolve_tokens( FAMILY, PARENTS, {FAMILY: [_token("--fs-radius-md", {"base": "8px"})]}, )) radius = resolved["--fs-radius-md"] assert radius.value_for("dark") == "8px" assert radius.value_for("base") == "8px" def test_value_for_prefers_an_explicit_mode_over_the_fallback(): resolved = _by_name(resolve_tokens( FAMILY, PARENTS, {FAMILY: [_token("--fs-bg", {"base": "#f7f5ef", "dark": "#14171a"})]}, )) assert resolved["--fs-bg"].value_for("dark") == "#14171a" def test_metadata_is_inherited_when_the_override_leaves_it_blank(): """A child overriding a colour routinely says nothing about what the token is FOR. Inheriting the family's description beats blanking it — the override was about the value, not the meaning.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token( "--fs-obsidian", {"base": "#14171a"}, group_name="surface", purpose="page bg, deepest surface", )], APP: [_token("--fs-obsidian", {"base": "#101317"})], }, )) obsidian = resolved["--fs-obsidian"] assert obsidian.group_name == "surface" assert obsidian.purpose == "page bg, deepest surface" assert obsidian.value_by_mode == {"base": "#101317"} # the value still won def test_an_override_that_states_metadata_wins_it_too(): resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-x", {"base": "a"}, purpose="family says")], APP: [_token("--fs-x", {"base": "b"}, purpose="app says")], }, )) assert resolved["--fs-x"].purpose == "app says" def test_an_override_at_default_order_keeps_the_familys_position(): """order_index 0 is the column DEFAULT, so it reads as unstated. Treating it as "first" would let a colour-only override drag its token to the top of the group — a visible reshuffle nobody asked for.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-x", {"base": "a"}, order_index=7)], APP: [_token("--fs-x", {"base": "b"})], }, )) assert resolved["--fs-x"].order_index == 7 def test_the_effective_set_is_ordered_by_group_then_position_with_ungrouped_last(): resolved = resolve_tokens( FAMILY, PARENTS, {FAMILY: [ _token("--fs-z", {"base": "1"}), # ungrouped _token("--fs-b", {"base": "2"}, group_name="text", order_index=1), _token("--fs-a", {"base": "3"}, group_name="surface", order_index=2), _token("--fs-c", {"base": "4"}, group_name="surface", order_index=1), ]}, ) assert [t.name for t in resolved] == ["--fs-c", "--fs-a", "--fs-b", "--fs-z"] def test_resolution_terminates_on_a_corrupt_hierarchy(): """Resolution inherits ancestry's visited-set. A loop reaching this function must produce a truncated set, not a hung request — the defensive half of the cycle guard, exercised end to end.""" parents = {1: 2, 2: 1} resolved = _by_name(resolve_tokens( 1, parents, {1: [_token("--fs-a", {"base": "one"})], 2: [_token("--fs-b", {"base": "two"})]}, )) assert set(resolved) == {"--fs-a", "--fs-b"} # Each system contributes exactly once, not endlessly. assert len(resolved["--fs-a"].contributions["base"]) == 1 # --- supersedes ------------------------------------------------------------- # # The declaration that replaces a prohibition. A design system stores what things # ARE, so "pure white is never text" has no row — but "write this token instead # of #fff" does, and it is the same fact stated forwards. def test_supersedes_is_inherited_when_the_override_is_silent_about_it(): """LOAD-BEARING. A child overriding a colour says nothing about which literals it replaces, and blanking the family's declaration there would silently disarm the check for every app that customises the token.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-text", {"base": "#e8e4d8"}, supersedes=["#fff", "#ffffff"])], APP: [_token("--fs-text", {"base": "#f0ece0"})], }, )) text = resolved["--fs-text"] assert text.supersedes == ("#fff", "#ffffff") assert text.value_by_mode == {"base": "#f0ece0"} # the value still overrode def test_an_override_that_states_its_own_supersedes_replaces_the_list(): """Whole-list replacement, not a merge — an app that means "only #fff" must be able to say so without inheriting entries it deliberately dropped.""" resolved = _by_name(resolve_tokens( APP, PARENTS, { FAMILY: [_token("--fs-text", {"base": "a"}, supersedes=["#fff", "#ffffff"])], APP: [_token("--fs-text", {"base": "b"}, supersedes=["#fff"])], }, )) assert resolved["--fs-text"].supersedes == ("#fff",) def test_a_token_that_supersedes_nothing_resolves_to_an_empty_tuple(): """Most tokens replace nothing. That has to be an empty sequence rather than None, so no caller has to test for two kinds of nothing.""" resolved = _by_name(resolve_tokens( FAMILY, PARENTS, {FAMILY: [_token("--fs-radius-md", {"base": "8px"})]}, )) assert resolved["--fs-radius-md"].supersedes == () def test_supersedes_survives_serialisation_as_a_list(): resolved = _by_name(resolve_tokens( FAMILY, PARENTS, {FAMILY: [_token("--fs-text", {"base": "#e8e4d8"}, supersedes=["#fff"])]}, )) assert resolved["--fs-text"].to_dict()["supersedes"] == ["#fff"] def test_the_superseded_literal_need_not_match_the_tokens_own_value(): """The whole reason this is DECLARED rather than derived. `#fff` and Parchment are different colours, so a value-matching rule could never have connected them — which is why the prohibition looked unrepresentable until it was turned around.""" resolved = _by_name(resolve_tokens( FAMILY, PARENTS, {FAMILY: [_token("--fs-text", {"base": "#e8e4d8"}, supersedes=["#fff"])]}, )) text = resolved["--fs-text"] assert text.value_by_mode["base"] not in text.supersedes def test_rows_without_a_supersedes_attribute_at_all_still_resolve(): """Duck-typed input: a caller passing rows from before the column existed must not crash the cascade.""" legacy = SimpleNamespace( name="--fs-x", value_by_mode={"base": "a"}, group_name=None, purpose=None, order_index=0, ) resolved = _by_name(resolve_tokens(FAMILY, PARENTS, {FAMILY: [legacy]})) assert resolved["--fs-x"].supersedes == ()