#!/usr/bin/env bash # next/tests/follower_graph.sh — m2 Step 6a test. # # Pure projection fold over Follow / Accept / Reject / Undo # activities per design §13.2. State tracks per-actor # {following, followers, pending_outbound, pending_inbound} lists. set -uo pipefail cd "$(git rev-parse --show-toplevel)" SX_SERVER="${SX_SERVER:-hosts/ocaml/_build/default/bin/sx_server.exe}" if [ ! -x "$SX_SERVER" ]; then SX_SERVER="/root/rose-ash/hosts/ocaml/_build/default/bin/sx_server.exe" fi if [ ! -x "$SX_SERVER" ]; then echo "ERROR: sx_server.exe not found." >&2 exit 1 fi VERBOSE="${1:-}" PASS=0; FAIL=0; ERRORS="" TMPFILE=$(mktemp); trap "rm -f $TMPFILE" EXIT # F(A→B) is the embedded Follow object Accept / Reject / Undo wrap. SETUP='F = [{type, follow}, {actor, alice}, {object, bob}], Follow = [{actor, alice}, {type, follow}, {object, bob}], Accept = [{actor, bob}, {type, accept}, {object, F}], Reject = [{actor, bob}, {type, reject}, {object, F}], Undo = [{actor, alice}, {type, undo}, {object, F}],' cat > "$TMPFILE" < [] (epoch 10) (eval "(get (erlang-eval-ast \"follower_graph:new() =:= []\") :name)") ;; Follow alice->bob: alice has pending_outbound = [bob]; bob pending_inbound = [alice] (epoch 11) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), follower_graph:pending_outbound(alice, S) =:= [bob] andalso follower_graph:pending_inbound(bob, S) =:= [alice]\") :name)") ;; After Follow alone, neither party shows the other as following/follower (epoch 12) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), follower_graph:following(alice, S) =:= [] andalso follower_graph:followers(bob, S) =:= []\") :name)") ;; Accept: alice moves into bob's followers; bob moves into alice's following (epoch 13) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Accept, S), follower_graph:followers(bob, S1) =:= [alice] andalso follower_graph:following(alice, S1) =:= [bob]\") :name)") ;; Accept: both pending lists cleared on each side (epoch 14) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Accept, S), follower_graph:pending_outbound(alice, S1) =:= [] andalso follower_graph:pending_inbound(bob, S1) =:= []\") :name)") ;; Reject: pending lists clear without populating following/followers (epoch 15) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Reject, S), follower_graph:pending_outbound(alice, S1) =:= [] andalso follower_graph:pending_inbound(bob, S1) =:= [] andalso follower_graph:following(alice, S1) =:= [] andalso follower_graph:followers(bob, S1) =:= []\") :name)") ;; Undo by alice after accept: drops both following and followers (epoch 16) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Accept, S), S2 = follower_graph:fold(Undo, S1), follower_graph:following(alice, S2) =:= [] andalso follower_graph:followers(bob, S2) =:= []\") :name)") ;; Undo before accept: pending lists clear (epoch 17) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Undo, S), follower_graph:pending_outbound(alice, S1) =:= [] andalso follower_graph:pending_inbound(bob, S1) =:= []\") :name)") ;; Self-follow ignored (alice follows alice no-ops) (epoch 18) (eval "(get (erlang-eval-ast \"SelfFollow = [{actor, alice}, {type, follow}, {object, alice}], S = follower_graph:fold(SelfFollow, follower_graph:new()), follower_graph:new() =:= S\") :name)") ;; Two distinct follows: alice->bob, carol->bob produce two pending_inbound entries on bob (epoch 19) (eval "(get (erlang-eval-ast \"${SETUP} F2 = [{actor, carol}, {type, follow}, {object, bob}], S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(F2, S), follower_graph:pending_inbound(bob, S1) =:= [alice, carol]\") :name)") ;; Duplicate Follow is idempotent (no double-add) (epoch 20) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), S1 = follower_graph:fold(Follow, S), follower_graph:pending_outbound(alice, S1) =:= [bob]\") :name)") ;; Predicates: is_following / has_follower / pendings after accept (epoch 21) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Accept, follower_graph:fold(Follow, follower_graph:new())), {follower_graph:is_following(alice, bob, S), follower_graph:has_follower(bob, alice, S), follower_graph:is_pending_outbound(alice, bob, S), follower_graph:is_pending_inbound(bob, alice, S)} =:= {true, true, false, false}\") :name)") ;; actors/1 lists every actor seen (alice + bob after one Follow, ;; in insertion order: alice's bucket added first, then bob's) (epoch 22) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Follow, follower_graph:new()), follower_graph:actors(S) =:= [alice, bob]\") :name)") ;; fold_fn/0 is a 2-arity Erlang fun (plugs into projection:start_link) (epoch 23) (eval "(get (erlang-eval-ast \"is_function(follower_graph:fold_fn(), 2)\") :name)") ;; Activity sans :type passes through (epoch 24) (eval "(get (erlang-eval-ast \"Garbage = [{actor, alice}], follower_graph:fold(Garbage, follower_graph:new()) =:= []\") :name)") ;; Accept whose embedded :object isn't a Follow passes through (epoch 25) (eval "(get (erlang-eval-ast \"BadAccept = [{actor, bob}, {type, accept}, {object, [{type, note}, {actor, alice}, {object, bob}]}], follower_graph:fold(BadAccept, follower_graph:new()) =:= []\") :name)") ;; Undo by the wrong actor (carol trying to undo F where A=alice) is a no-op (epoch 26) (eval "(get (erlang-eval-ast \"${SETUP} S = follower_graph:fold(Accept, follower_graph:fold(Follow, follower_graph:new())), BadUndo = [{actor, carol}, {type, undo}, {object, F}], S1 = follower_graph:fold(BadUndo, S), follower_graph:following(alice, S1) =:= [bob]\") :name)") EPOCHS OUTPUT=$(timeout 240 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) check() { local epoch="$1" desc="$2" expected="$3" local actual actual=$(echo "$OUTPUT" | awk -v e="$epoch" ' $0 ~ "^\\(ok-len " e " " { getline; print; exit } $0 ~ "^\\(ok " e " " { print; exit } $0 ~ "^\\(error " e " " { print; exit } ') [ -z "$actual" ] && actual="" if echo "$actual" | grep -qF -- "$expected"; then PASS=$((PASS+1)) [ "$VERBOSE" = "-v" ] && echo " ok $desc" else FAIL=$((FAIL+1)) ERRORS+=" FAIL [$desc] (epoch $epoch) expected: $expected | actual: $actual " fi } check 3 "follower_graph module loaded" "follower_graph" check 10 "new/0 -> []" "true" check 11 "Follow sets pendings each side" "true" check 12 "Follow alone: no following/follower" "true" check 13 "Accept promotes to following/followers" "true" check 14 "Accept clears pendings" "true" check 15 "Reject clears without promote" "true" check 16 "Undo after accept drops rel" "true" check 17 "Undo before accept clears pending" "true" check 18 "self-follow is a no-op" "true" check 19 "two follows -> two pending_inbound" "true" check 20 "duplicate Follow idempotent" "true" check 21 "predicates after accept" "true" check 22 "actors/1 lists every seen" "true" check 23 "fold_fn/0 is fun/2" "true" check 24 "untyped activity passes through" "true" check 25 "Accept of non-Follow passes through" "true" check 26 "Undo by wrong actor no-op" "true" TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL next/tests/follower_graph.sh passed" else echo "FAIL $PASS/$TOTAL passed, $FAIL failed:" echo "$ERRORS" fi [ $FAIL -eq 0 ]