AtlatestRepositoryfolio

folio / tree / testgate-f3b.sh

1#!/bin/sh
2# gate-f3b.sh -- the F3b gate: structural operations, crash recovery, and
3# cross-file transactions.
4#
5# RUN: sh test/gate-f3b.sh (all arms)
6# SABOTAGE: sh test/sabotage-f3b.sh (proves these arms can go red)
7#
8# ARMS
9# 1 harness self-tests: both probe batteries' assertion primitives CAN fail
10# 2 the F3b battery (test/f3b-probe.sgl, 12 arms: stand-down control first,
11# both recovery branches with the discriminator, the 3-file rename with
12# graph closure, referrer-guarded delete, cross-file move with document
13# re-addressing, scoped-out wording, invalid-doc read-only, body
14# replacement, the hash-pinned Markdown bystander)
15# 3 the F3a battery (22 arms) -- the scalar layer must not regress
16# 4 REAL SIGKILL mid-rename at staggered delays: after recovery, every
17# file is fully-old or fully-new by content -- NEVER mixed -- and when a
18# manifest existed at kill time the recovery is REPORTED
19# 5 a VALIDATED manifest whose staged temp was corrupted after the crash:
20# recovery REFUSES to a human and touches nothing
22# SETUP-FAILED is distinct from RED and GREEN throughout.
24set -u
25repo_dir=$(cd "$(dirname "$0")/.." && pwd)
26cd "$repo_dir" || exit 1
28work=$(mktemp -d /tmp/gate-f3b.XXXXXX) || { echo "GATE-F3B SETUP-FAILED: no temp dir"; exit 3; }
29trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM
31sigil="${SIGIL:-sigil}"
32export SIGIL_REDIRECTS="${SIGIL_F3B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"
33F3A_FIX="$repo_dir/test/fixtures/f3a"
34F3B_FIX="$repo_dir/test/fixtures/f3b"
36fails=0
37fail () { echo "GATE-F3B FAIL: $*" >&2; fails=$((fails + 1)); }
38setup_failed () { echo "GATE-F3B SETUP-FAILED: $*" >&2; exit 3; }
39prev_fails=0
40arm_summary () {
41 if [ "$fails" -gt "$prev_fails" ]; then
42 echo "GATE-F3B arm $1: FAILED ($((fails - prev_fails)) failure(s))"
43 else
44 echo "GATE-F3B arm $1: OK"
45 fi
46 prev_fails=$fails
49only_arms="${*:-1 2 3 4 5}"
50want () { case " $only_arms " in *" $1 "*) return 0;; *) return 1;; esac; }
52fresh_f3b_root () { mkdir -p "$1" && cp "$F3B_FIX"/* "$1"/ || setup_failed "cannot populate f3b root"; }
54echo "GATE-F3B: building (dev config)..."
55"$sigil" build >"$work/build.log" 2>&1 || setup_failed "sigil build failed"
56[ -d build/dev/lib ] || setup_failed "build/dev/lib absent after build"
57run_sgl () { "$sigil" -L build/dev/lib -L src "$@"; }
59# Sequential cache warm-up before any concurrent spawn (the F3a lesson).
60warm="$work/warm"; fresh_f3b_root "$warm"
61F3B_PROBE_SELFTEST=1 F3B_ROOT="$warm" run_sgl test/f3b-probe.sgl >/dev/null 2>&1 || true
63# ---------------------------------------------------------------------------
64if want 1; then
65 r1="$work/st1"; fresh_f3b_root "$r1"
66 F3B_PROBE_SELFTEST=1 F3B_ROOT="$r1" run_sgl test/f3b-probe.sgl >"$work/st-b.log" 2>&1 \
67 && grep -q "F3B_ARM_selftest_RED" "$work/st-b.log" \
68 || fail "arm1: the f3b harness did not record a red for a false assertion"
69 r1a="$work/st1a"; mkdir -p "$r1a"; cp "$F3A_FIX"/*.sgd "$r1a"/
70 F3A_PROBE_SELFTEST=1 F3A_ROOT="$r1a" run_sgl test/f3a-txn-probe.sgl >"$work/st-a.log" 2>&1 \
71 && grep -q "F3A_ARM_selftest_RED" "$work/st-a.log" \
72 || fail "arm1: the f3a harness did not record a red for a false assertion"
73 arm_summary 1
74fi
76# ---------------------------------------------------------------------------
77if want 2; then
78 r2="$work/b"; fresh_f3b_root "$r2"
79 F3B_ROOT="$r2" run_sgl test/f3b-probe.sgl >"$work/b.log" 2>&1 || fail "arm2: battery exited non-zero"
80 grep -q "F3B_PROBE_DONE arms=15 green=15 red=0" "$work/b.log" \
81 || { fail "arm2: expected 15/15; got $(grep F3B_PROBE_DONE "$work/b.log" || echo none)"; \
82 grep "_RED" "$work/b.log" >&2 || true; }
83 arm_summary 2
84fi
86# ---------------------------------------------------------------------------
87if want 3; then
88 r3="$work/a"; mkdir -p "$r3"; cp "$F3A_FIX"/*.sgd "$r3"/
89 F3A_ROOT="$r3" run_sgl test/f3a-txn-probe.sgl >"$work/a.log" 2>&1 || fail "arm3: f3a battery exited non-zero"
90 grep -q "F3A_PROBE_DONE arms=22 green=22 red=0" "$work/a.log" \
91 || { fail "arm3: expected 22/22; got $(grep F3A_PROBE_DONE "$work/a.log" || echo none)"; \
92 grep "_RED" "$work/a.log" >&2 || true; }
93 arm_summary 3
94fi
96# ---------------------------------------------------------------------------
97# Arm 4: SIGKILL mid-rename. Coherence contract per file after recovery:
98# fully-old (has t-target, no t-retargeted) or fully-new (the reverse), and
99# ALL FILES AGREE. A file with both, or files disagreeing, is a torn write.
100# ---------------------------------------------------------------------------
101if want 4; then
102 for delay in 0.05 0.20 0.60 1.20; do
103 rk="$work/kill-$delay"; fresh_f3b_root "$rk"
104 startk="$work/ks-$delay"; rm -f "$startk"
105 F3B_ROOT="$rk" F3B_START_FILE="$startk" \
106 "$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-$delay.log" 2>&1 &
107 rn=$!
108 tries=0
109 while ! grep -q RENAMER_READY "$work/rn-$delay.log" 2>/dev/null; do
110 tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1
111 done
112 grep -q RENAMER_READY "$work/rn-$delay.log" || setup_failed "arm4: renamer never became ready"
113 : > "$startk"
114 sleep "$delay"
115 kill -9 "$rn" 2>/dev/null
116 wait "$rn" 2>/dev/null
117 manifest_at_kill=no
118 [ -f "$rk/.folio-transaction-manifest" ] && manifest_at_kill=yes
119 # Recovery driver: any next transaction (its log carries the report).
120 F3B_ROOT="$rk" run_sgl test/f3b-recover-driver.sgl >"$work/rec-$delay.log" 2>&1 \
121 || fail "arm4 delay=$delay: the post-kill transaction failed"
122 old_files=0; new_files=0; torn=0
123 for f in owner.sgd ref-one.sgd ref-two.sgd; do
124 has_old=no; has_new=no
125 grep -q '"t-target"' "$rk/$f" && has_old=yes
126 grep -q '"t-retargeted"' "$rk/$f" && has_new=yes
127 if [ "$has_old" = yes ] && [ "$has_new" = yes ]; then torn=$((torn+1)); fi
128 [ "$has_old" = yes ] && old_files=$((old_files+1))
129 [ "$has_new" = yes ] && new_files=$((new_files+1))
130 done
131 [ "$torn" -eq 0 ] || fail "arm4 delay=$delay: a file carries BOTH ids -- torn"
132 if [ "$old_files" -gt 0 ] && [ "$new_files" -gt 0 ]; then
133 fail "arm4 delay=$delay: files DISAGREE (old=$old_files new=$new_files) -- partial rewrite survived recovery"
134 fi
135 if [ "$manifest_at_kill" = yes ]; then
136 grep -q "recovery:" "$work/rec-$delay.log" \
137 || fail "arm4 delay=$delay: a manifest existed at kill time but no recovery was reported"
138 [ "$new_files" -eq 3 ] \
139 || fail "arm4 delay=$delay: manifest existed (validated) but recovery did not complete to the new state"
140 fi
141 [ -f "$rk/.folio-transaction-manifest" ] && fail "arm4 delay=$delay: manifest survived recovery"
142 echo "GATE-F3B arm4 delay=$delay: manifest_at_kill=$manifest_at_kill old=$old_files new=$new_files torn=$torn"
143 done
144 # Best-effort MANIFEST-WINDOW round: busy-poll for the manifest's atomic
145 # appearance and kill the instant it exists. The window (renames +
146 # cleanup) is milliseconds wide, so catching it is not guaranteed and NO
147 # assertion here requires it -- the coherence contract holds either way,
148 # and when the window IS caught this round exercises real-kill completion
149 # (the deterministic completion coverage is the battery's b3 arm, which
150 # constructs the identical bytes). The round REPORTS which case ran.
151 rkw="$work/kill-window"; fresh_f3b_root "$rkw"
152 startw="$work/ks-window"; rm -f "$startw"
153 F3B_ROOT="$rkw" F3B_START_FILE="$startw" \
154 "$sigil" -L build/dev/lib -L src test/f3b-renamer.sgl >"$work/rn-window.log" 2>&1 &
155 rnw=$!
156 tries=0
157 while ! grep -q RENAMER_READY "$work/rn-window.log" 2>/dev/null; do
158 tries=$((tries + 1)); [ "$tries" -gt 600 ] && break; sleep 0.1
159 done
160 : > "$startw"
161 while kill -0 "$rnw" 2>/dev/null && [ ! -f "$rkw/.folio-transaction-manifest" ]; do :; done
162 kill -9 "$rnw" 2>/dev/null
163 wait "$rnw" 2>/dev/null
164 window_hit=no
165 [ -f "$rkw/.folio-transaction-manifest" ] && window_hit=yes
166 F3B_ROOT="$rkw" run_sgl test/f3b-recover-driver.sgl >"$work/rec-window.log" 2>&1 \
167 || fail "arm4 window: the post-kill transaction failed"
168 wold=0; wnew=0; wtorn=0
169 for f in owner.sgd ref-one.sgd ref-two.sgd; do
170 ho=no; hn=no
171 grep -q '"t-target"' "$rkw/$f" && ho=yes
172 grep -q '"t-retargeted"' "$rkw/$f" && hn=yes
173 [ "$ho" = yes ] && [ "$hn" = yes ] && wtorn=$((wtorn+1))
174 [ "$ho" = yes ] && wold=$((wold+1))
175 [ "$hn" = yes ] && wnew=$((wnew+1))
176 done
177 [ "$wtorn" -eq 0 ] || fail "arm4 window: torn file"
178 if [ "$wold" -gt 0 ] && [ "$wnew" -gt 0 ]; then
179 fail "arm4 window: files disagree (old=$wold new=$wnew) after recovery"
180 fi
181 if [ "$window_hit" = yes ]; then
182 [ "$wnew" -eq 3 ] || fail "arm4 window: manifest present at kill but recovery did not complete"
183 grep -q "recovery:" "$work/rec-window.log" || fail "arm4 window: unreported recovery"
184 fi
185 echo "GATE-F3B arm4 window: manifest_at_kill=$window_hit old=$wold new=$wnew torn=$wtorn"
186 arm_summary 4
187fi
189# ---------------------------------------------------------------------------
190# Arm 5: a validated manifest whose temp was corrupted post-crash: recovery
191# REFUSES to a human, the target does not move, the manifest stays for the
192# human. (This is the unprovable state; guessing here is the defect.)
193# ---------------------------------------------------------------------------
194if want 5; then
195 r5="$work/corrupt"; fresh_f3b_root "$r5"
196 F3B_ROOT="$r5" run_sgl test/f3b-corrupt-temp-driver.sgl >"$work/corrupt.log" 2>&1
197 grep -q "CORRUPT_TEMP_REFUSED_TO_HUMAN" "$work/corrupt.log" \
198 || { fail "arm5: recovery did not refuse the unprovable state; log:"; tail -5 "$work/corrupt.log" >&2; }
199 arm_summary 5
200fi
202echo "---"
203if [ "$fails" -eq 0 ]; then
204 echo "F3B_GATE_GREEN arms_run=[$only_arms] f3b_arms=15 f3a_arms=22 kill_rounds=5 sha=$(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo unknown)"
205 exit 0
206else
207 echo "F3B_GATE_RED failures=$fails arms_run=[$only_arms]"
208 exit 1
209fi