"""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 scribe.services.design_cascade import ancestry, 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