extends Node
## GATE — COLLECTIONS IN THE FOLD (#120 F1, docs/node-tree-and-stacks.md §2). Pure: hand
## records into CollectionFold, no map.
## C1 nested collections: parents and children
## C2 col.set: the latest (seq, author) wins per field, whatever the arrival order;
## two peers with the records in different orders build the same tree
## C3 a parent change that would make a cycle is ignored AND reported
## C4 link / unlink: the latest wins per (collection, member); one member linked into
## two collections is in both (Shift+M)
## C5 remove, keeping contents: children and members move to the removed one's parent
## C6 remove with hierarchy: the subtree goes
## C7 an undone remove brings the collection back
## C8 members_of recursive gathers the subtree's members once each
##
## RUN AS A SCENE: "$HYPER_TPS_GODOT" --headless res://scenes/test/gate_collection_fold.tscn
const CF := preload("res://scenes/maps/collection_fold.gd")
var fails := 0
var checks := 0
func _ok(what: String, cond: bool, note: Variant = "") -> void:
checks += 1
if not cond:
fails += 1
print(" %-64s %s %s" % [what, "OK" if cond else "**FAIL**", str(note)])
func _r(author: String, seq: int, kind: String, extra: Dictionary = {}) -> Dictionary:
var r := {"author": author, "seq": seq, "kind": kind, "undone": false}
r.merge(extra)
return r
func _ready() -> void:
# A = a:1 (root), B = a:2 under A, C = b:3 under B.
var base: Array = [
_r("a", 1, "col.create", {"name": "A", "parent": ""}),
_r("a", 2, "col.create", {"name": "B", "parent": "a:1"}),
_r("b", 3, "col.create", {"name": "C", "parent": "a:2"}),
]
var t := CF.tree(base)
var cols: Dictionary = t["cols"]
_ok("C1 three collections", cols.size() == 3, cols.keys())
_ok("C1 A at the root, B under A, C under B", String(cols["a:1"]["parent"]) == ""
and String(cols["a:2"]["parent"]) == "a:1" and String(cols["b:3"]["parent"]) == "a:2")
_ok("C1 A's children = [B]", cols["a:1"]["children"] == ["a:2"], cols["a:1"]["children"])
# ---- C2 ----
var named: Array = base + [
_r("b", 10, "col.set", {"target": "a:1", "name": "from b@10"}),
_r("a", 10, "col.set", {"target": "a:1", "name": "from a@10"}),
_r("a", 9, "col.set", {"target": "a:1", "name": "from a@9"}),
]
var t2 := CF.tree(named)
_ok("C2 the latest (seq, author) wins: b@10 over a@10 over a@9",
String(t2["cols"]["a:1"]["name"]) == "from b@10", t2["cols"]["a:1"]["name"])
var rev: Array = named.duplicate()
rev.reverse()
_ok("C2 another arrival order builds the same tree", str(CF.tree(rev)) == str(t2))
# ---- C3 ----
var cyc: Array = base + [_r("a", 11, "col.set", {"target": "a:1", "parent": "b:3"})]
var t3 := CF.tree(cyc)
_ok("C3 A under its own grandchild is ignored", String(t3["cols"]["a:1"]["parent"]) == "", t3["cols"]["a:1"]["parent"])
_ok("C3 ... and reported", t3["ignored"] == ["a:11"], t3["ignored"])
# ---- C4 ----
var linked: Array = base + [
_r("a", 20, "col.link", {"target": "a:1", "members": ["g:wall", "obj:lamp"]}),
_r("a", 21, "col.link", {"target": "b:3", "members": ["g:wall"]}),
_r("a", 22, "col.unlink", {"target": "a:1", "members": ["obj:lamp"]}),
]
var t4 := CF.tree(linked)
_ok("C4 A holds the wall, not the unlinked lamp", t4["cols"]["a:1"]["members"] == ["g:wall"], t4["cols"]["a:1"]["members"])
_ok("C4 the wall is linked into A and C", CF.collections_of(t4, "g:wall") == ["a:1", "b:3"], CF.collections_of(t4, "g:wall"))
# ---- C5 ----
var kept: Array = linked + [_r("a", 30, "col.remove", {"target": "a:2", "hierarchy": false})]
var t5 := CF.tree(kept)
_ok("C5 B is gone", not t5["cols"].has("a:2"))
_ok("C5 its child C now sits under A", String(t5["cols"]["b:3"]["parent"]) == "a:1", t5["cols"]["b:3"]["parent"])
var kept2: Array = linked + [_r("a", 31, "col.remove", {"target": "b:3", "hierarchy": false})]
_ok("C5 C's member moves up to B", (CF.tree(kept2)["cols"]["a:2"]["members"] as Array).has("g:wall"),
CF.tree(kept2)["cols"]["a:2"]["members"])
# ---- C6 ----
var gone: Array = linked + [_r("a", 32, "col.remove", {"target": "a:2", "hierarchy": true})]
var t6 := CF.tree(gone)
_ok("C6 with its hierarchy, B and C both go", not t6["cols"].has("a:2") and not t6["cols"].has("b:3"), t6["cols"].keys())
_ok("C6 ... and A stays", t6["cols"].has("a:1"))
# ---- C7 ----
var undone: Array = gone.duplicate(true)
undone[undone.size() - 1]["undone"] = true
_ok("C7 an undone remove brings B and C back", CF.tree(undone)["cols"].size() == 3)
# ---- C8 ----
_ok("C8 A's members, recursive: the wall once", CF.members_of(t4, "a:1", true) == ["g:wall"], CF.members_of(t4, "a:1", true))
_ok("C8 not recursive: also the wall (A's own)", CF.members_of(t4, "a:1", false) == ["g:wall"])
var deep: Array = linked + [_r("a", 40, "col.link", {"target": "b:3", "members": ["obj:crate"]})]
_ok("C8 recursive reaches C's crate", CF.members_of(CF.tree(deep), "a:1", true) == ["g:wall", "obj:crate"],
CF.members_of(CF.tree(deep), "a:1", true))
print("\nGATE collection_fold: %s (%d checks, %d failed)" % ["PASS" if fails == 0 else "FAIL", checks, fails])
get_tree().quit(0 if fails == 0 else 1)