OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / journal_lamport_gate.gd

89 lines · gd ref:
extends SceneTree
## IS THE JOURNAL CLOCK LAMPORT? — hyper-games #42 P1.
##
##     "$HYPER_TPS_GODOT" --headless --script scenes/test/journal_lamport_gate.gd
##
## reserve_seq() seeds from max(wall clock, highest seq seen). That scan used to
## cover ONLY THE AUTHOR'S OWN KEYS, which makes it a per-author counter: the
## (seq, author) order then sorts by whose CLOCK ran ahead rather than by what
## happened. Convergence was never at risk — the order was already total and
## identical everywhere — but a peer could write an edit after seeing yours and
## have it sort BEFORE yours.
##
## PRELOAD IS THE TRAP, not the autoloads. preload is a const and forces the
## dependency to compile while `GameState` is still not an identifier — which is
## why level_journal_roundtrip_gate and operator_gate cannot load headless at all
## ("Compile Error: Identifier not found: GameState", verified against a clean
## tree, so it predates this work). A runtime load() from _initialize() compiles
## lazily and works fine. The failure mode is what makes this expensive: the gate
## HANGS to its timeout instead of reporting a parse error.
##
## THE OVERWRITE GUARD IS THE THING NOT TO BREAK. The own-max scan existed because
## the counter runs ahead of the clock, so a restart inside that window would
## re-mint seqs that already name records and silently overwrite them — the durable
## view is last-write-wins per key. A scan over ALL authors is strictly stronger, and
## the last case here pins that.

var LP: GDScript = null

var _failures: Array[String] = []


func _check(label: String, ok: bool, detail: String) -> void:
	if not ok:
		_failures.append("%s (%s)" % [label, detail])
	print("  %-52s %s  %s" % [label, "OK  " if ok else "FAIL", detail])


func _initialize() -> void:
	print("journal clock — hyper-games #42 P1\n")
	LP = load("res://scenes/maps/level_persistence.gd")

	var mine: Array[String] = [
		"world:level:alice:op:100",
		"world:level:alice:op:140",
	]
	var theirs: Array[String] = [
		"world:level:bob:op:900",
	]
	var both: Array[String] = mine + theirs

	_check("scans my own keys", LP.max_seq_in(mine) == 140, str(LP.max_seq_in(mine)))

	# THE BEHAVIOUR CHANGE. Before, another author's higher seq was skipped, so my
	# next seq could land below a record I had already seen.
	_check("another author's higher seq is SEEN", LP.max_seq_in(both) == 900,
		str(LP.max_seq_in(both)))

	# Causality, stated as the thing a user would notice: I see bob's edit, then I
	# write. Mine must sort after his.
	var next_seq: int = LP.max_seq_in(both) + 1
	_check("an edit made after seeing bob's sorts after it", next_seq > 900,
		"next=%d vs bob 900" % next_seq)

	# THE OVERWRITE GUARD, preserved: the all-author max is never below the own max.
	_check("never below my own max (no re-minted seqs)",
		LP.max_seq_in(both) >= LP.max_seq_in(mine),
		"%d >= %d" % [LP.max_seq_in(both), LP.max_seq_in(mine)])

	# A memberId containing colons must not confuse the seq parse — the infix is
	# ":op:", so the split is on that and not on the last colon.
	var colony: Array[String] = ["world:level:peer:a:b:op:77"]
	_check("author with colons parses", LP.max_seq_in(colony) == 77,
		str(LP.max_seq_in(colony)))

	# Junk is skipped, not counted as 0 and not crashing.
	var junk: Array[String] = ["world:level:broken", "world:level:carol:op:5"]
	_check("malformed keys are skipped", LP.max_seq_in(junk) == 5, str(LP.max_seq_in(junk)))

	_check("empty journal is 0", LP.max_seq_in([] as Array[String]) == 0, "0")

	print("")
	if _failures.is_empty():
		print("PASS")
		quit(0)
	else:
		for f: String in _failures:
			print("FAILED: %s" % f)
		quit(1)