AtlatestRepositoryfolio

folio / tree / testsabotage-f3a.sh

1#!/bin/sh
2# sabotage-f3a.sh -- proves gate-f3a.sh's arms can go RED, one product-layer
3# plant per leg, plus one assertion-layer leg.
4#
5# DISCIPLINE (procedures/worker-instructions, predict-which-arms-go-red):
6# - every leg PRINTS ITS PREDICTION (red set AND green set) before running;
7# - every plant is verified LANDED (source hash moved + marker count) and
8# the planted tree must still BUILD, so a red is about behaviour, not a
9# syntax error;
10# - every restore is verified BY HASH against git's blob, and the final
11# step re-runs the full gate green on the restored tree;
12# - a mismatch between predicted and observed arm sets is SETUP-FAILED for
13# that leg, not a verdict.
15# Legs:
16# S1 remove the revision comparison -> sab-revcheck red
17# S2 remove rebase precondition 2 -> sab-precond2 red
18# S3 first-matching-text instead of span -> sab-span, sab-uncontended,
19# sab-rebase-report red
20# S4a serializer defect (due drops quote) -> postcheck DEFENDS: sab-span,
21# sab-rebase-report red;
22# sab-postcheck-defends GREEN
23# (refusal + no publish)
24# S4b S4a + verify verdict ignored -> sab-postcheck-defends red
25# (corrupt note PUBLISHED)
26# S5 coordinator bypasses the lock -> gate arm 3 (real contention)
27# red: two OKs in a round
28# S6 status lands without its @update -> sab-journal red
29# S7 acquisition degrades to a SHARED lock -> sab-coord-init red (the
30# second-acquisition direction
31# of the exclusion probe)
32# S8 a rebase stops reporting itself -> sab-rebase-report red
33# SA assertion layer: expected reason flip -> sab-revcheck red
35set -u
36repo_dir=$(cd "$(dirname "$0")/.." && pwd)
37cd "$repo_dir" || exit 1
39work=$(mktemp -d /tmp/sabotage-f3a.XXXXXX) || { echo "SAB-F3A SETUP-FAILED: no temp dir"; exit 3; }
40trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM
42sigil="${SIGIL:-sigil}"
43export SIGIL_REDIRECTS="${SIGIL_F3A_REDIRECTS:-$repo_dir/dev-redirects.sgl}"
44FIXTURES="$repo_dir/test/fixtures/f3a"
46TXN=src/folio/doc/transaction.sgl
47LOCK=src/folio/doc/lock.sgl
48COORD=src/folio/doc/coordinator.sgl
49DRIVER=test/f3a-sab-driver.sgl
51legs=0; reds=0; setup_fails=0
52fail_leg () { echo "SAB-F3A LEG $1: DID NOT GO RED AS PREDICTED -- $2" >&2; }
53setup_failed_leg () { echo "SAB-F3A LEG $1: SETUP-FAILED -- $2" >&2; setup_fails=$((setup_fails + 1)); }
55hash_of () { sha256sum "$1" | cut -d' ' -f1; }
57# Baseline hashes for restore verification.
58h_txn=$(hash_of "$TXN"); h_lock=$(hash_of "$LOCK"); h_coord=$(hash_of "$COORD"); h_driver=$(hash_of "$DRIVER")
60restore_all () {
61 git checkout -- "$TXN" "$LOCK" "$COORD" "$DRIVER" || return 1
62 [ "$(hash_of "$TXN")" = "$h_txn" ] || { echo "SAB-F3A: RESTORE FAILED for $TXN" >&2; return 1; }
63 [ "$(hash_of "$LOCK")" = "$h_lock" ] || { echo "SAB-F3A: RESTORE FAILED for $LOCK" >&2; return 1; }
64 [ "$(hash_of "$COORD")" = "$h_coord" ] || { echo "SAB-F3A: RESTORE FAILED for $COORD" >&2; return 1; }
65 [ "$(hash_of "$DRIVER")" = "$h_driver" ] || { echo "SAB-F3A: RESTORE FAILED for $DRIVER" >&2; return 1; }
66 return 0
69# Refuse to run on a dirty tree: plants and restores are only meaningful
70# against a known baseline.
71if ! git diff --quiet -- "$TXN" "$LOCK" "$COORD" "$DRIVER"; then
72 echo "SAB-F3A SETUP-FAILED: tree is dirty under the files this script plants into"; exit 3
73fi
75# plant_landed <leg> <file> <old-hash> <marker> <count>
76plant_landed () {
77 leg=$1; file=$2; old=$3; marker=$4; want=$5
78 [ "$(hash_of "$file")" != "$old" ] || { setup_failed_leg "$leg" "plant produced no diff in $file"; return 1; }
79 got=$(grep -c "$marker" "$file" || true)
80 [ "$got" -eq "$want" ] || { setup_failed_leg "$leg" "marker '$marker' count $got, wanted $want"; return 1; }
81 return 0
84build_planted () {
85 leg=$1
86 if ! "$sigil" build >"$work/build-$leg.log" 2>&1; then
87 setup_failed_leg "$leg" "the planted tree does not build; the leg would redden on a build failure, not behaviour"
88 return 1
89 fi
90 return 0
93# run_driver <leg> <arms-csv> -> leaves output in $work/driver-$leg.log
94run_driver () {
95 leg=$1; arms=$2
96 F3A_FIXTURES="$FIXTURES" F3A_SAB_ARMS="$arms" \
97 "$sigil" -L build/dev/lib -L src "$DRIVER" >"$work/driver-$leg.log" 2>&1
98 return $?
101# expect <leg> <log> <arm> <RED|GREEN>
102expect () {
103 leg=$1; log=$2; arm=$3; want=$4
104 if grep -q "SAB_ARM_${arm}_${want}" "$log"; then return 0; fi
105 fail_leg "$leg" "predicted $arm=$want; log says: $(grep "SAB_ARM_${arm}_" "$log" | head -1 || echo 'no marker at all')"
106 return 1
109leg_result () {
110 # $1 leg name, $2 = 0 if all expectations held
111 legs=$((legs + 1))
112 if [ "$2" -eq 0 ]; then
113 reds=$((reds + 1))
114 echo "SAB-F3A LEG $1: RED AS PREDICTED"
115 fi
118# ---------------------------------------------------------------------------
119echo "SAB-F3A LEG S1: remove the revision comparison"
120echo " PREDICT RED: sab-revcheck (a stale write publishes)"
121echo " PREDICT GREEN: sab-uncontended"
122sed -i 's/((equal? rev expected-revision)$/(#t ;; SAB-S1/' src/folio/doc/transaction.sgl
123if plant_landed S1 "$TXN" "$h_txn" "SAB-S1" 1 && build_planted S1; then
124 run_driver S1 "sab-revcheck,sab-uncontended" || true
125 ok=0
126 expect S1 "$work/driver-S1.log" sab-revcheck RED || ok=1
127 expect S1 "$work/driver-S1.log" sab-uncontended GREEN || ok=1
128 leg_result S1 $ok
129fi
130restore_all || exit 3
132# ---------------------------------------------------------------------------
133echo "SAB-F3A LEG S2: remove rebase precondition 2 (old value unchanged)"
134echo " PREDICT RED: sab-precond2 (a conflicting rebase publishes)"
135echo " PREDICT GREEN: sab-uncontended, sab-rebase-report"
136sed -i 's/((not (equal? (field-value f) expected-old-value))$/(#f/' src/folio/doc/transaction.sgl
137if plant_landed S2 "$TXN" "$h_txn" '(#f' 1 && build_planted S2; then
138 run_driver S2 "sab-precond2,sab-uncontended,sab-rebase-report" || true
139 ok=0
140 expect S2 "$work/driver-S2.log" sab-precond2 RED || ok=1
141 expect S2 "$work/driver-S2.log" sab-uncontended GREEN || ok=1
142 expect S2 "$work/driver-S2.log" sab-rebase-report GREEN || ok=1
143 leg_result S2 $ok
144fi
145restore_all || exit 3
147# ---------------------------------------------------------------------------
148echo "SAB-F3A LEG S3: land the edit at the FIRST MATCHING TEXT, not the span"
149echo " PREDICT RED: sab-span, sab-uncontended, sab-rebase-report (the"
150echo " fixture's prose duplicates catch all three via the"
151echo " postcheck: the value never lands in the task)"
152echo " PREDICT GREEN: sab-journal, sab-revcheck, sab-precond2, sab-postcheck-defends"
153sed -i 's/^ (define (splice source start end replacement)$/ (define (sab-first s needle) (let ((sl (string-length s)) (nl (string-length needle))) (let loop ((i 0)) (cond ((> (+ i nl) sl) 0) ((equal? (substring s i (+ i nl)) needle) i) (else (loop (+ i 1)))))))\n (define (splice source start end replacement)/' src/folio/doc/transaction.sgl
154sed -i 's/(staged (splice source (span-scalar-start sp)/(staged (splice source (sab-first source (serialize-value field (field-value f)))/' src/folio/doc/transaction.sgl
155sed -i 's/(span-scalar-end sp) replacement)))/(+ (sab-first source (serialize-value field (field-value f))) (string-length (serialize-value field (field-value f)))) replacement)))/' src/folio/doc/transaction.sgl
156if plant_landed S3 "$TXN" "$h_txn" 'sab-first' 3 && build_planted S3; then
157 run_driver S3 "sab-span,sab-uncontended,sab-rebase-report,sab-journal,sab-revcheck,sab-precond2,sab-postcheck-defends" || true
158 ok=0
159 expect S3 "$work/driver-S3.log" sab-span RED || ok=1
160 expect S3 "$work/driver-S3.log" sab-uncontended RED || ok=1
161 expect S3 "$work/driver-S3.log" sab-rebase-report RED || ok=1
162 expect S3 "$work/driver-S3.log" sab-journal GREEN || ok=1
163 expect S3 "$work/driver-S3.log" sab-revcheck GREEN || ok=1
164 expect S3 "$work/driver-S3.log" sab-precond2 GREEN || ok=1
165 expect S3 "$work/driver-S3.log" sab-postcheck-defends GREEN || ok=1
166 leg_result S3 $ok
167fi
168restore_all || exit 3
170# ---------------------------------------------------------------------------
171echo "SAB-F3A LEG S4a: serializer drops due's closing quote -- the POSTCHECK"
172echo " must refuse and nothing may publish (this leg proves the reparse"
173echo " DEFENDS; S4b then proves the defense is load-bearing)"
174echo " PREDICT RED: sab-span, sab-rebase-report (their due edits refuse)"
175echo " PREDICT GREEN: sab-postcheck-defends (refusal + unchanged bytes is"
176echo " that arm's plant-mode acceptance), sab-uncontended"
177sed -i 's/((due) (escape-string-literal value))/((due) (string-append "\\"" value))/' src/folio/doc/transaction.sgl
178if plant_landed S4a "$TXN" "$h_txn" '((due) (string-append' 1 && build_planted S4a; then
179 run_driver S4a "sab-span,sab-rebase-report,sab-postcheck-defends,sab-uncontended" || true
180 ok=0
181 expect S4a "$work/driver-S4a.log" sab-span RED || ok=1
182 expect S4a "$work/driver-S4a.log" sab-rebase-report RED || ok=1
183 expect S4a "$work/driver-S4a.log" sab-postcheck-defends GREEN || ok=1
184 expect S4a "$work/driver-S4a.log" sab-uncontended GREEN || ok=1
185 leg_result S4a $ok
186fi
187restore_all || exit 3
189# ---------------------------------------------------------------------------
190echo "SAB-F3A LEG S4b: S4a's serializer defect PLUS the verify verdict ignored"
191echo " -- skip post-write reparse: the corrupt note PUBLISHES"
192echo " PREDICT RED: sab-postcheck-defends (published corruption), sab-span"
193echo " PREDICT GREEN: sab-uncontended (nothing corrupt to catch on priority)"
194sed -i 's/((due) (escape-string-literal value))/((due) (string-append "\\"" value))/' src/folio/doc/transaction.sgl
195sed -i 's/(if bad$/(if #f/' src/folio/doc/transaction.sgl
196if plant_landed S4b "$TXN" "$h_txn" '(if #f' 1 && build_planted S4b; then
197 run_driver S4b "sab-postcheck-defends,sab-span,sab-uncontended" || true
198 ok=0
199 expect S4b "$work/driver-S4b.log" sab-postcheck-defends RED || ok=1
200 expect S4b "$work/driver-S4b.log" sab-span RED || ok=1
201 expect S4b "$work/driver-S4b.log" sab-uncontended GREEN || ok=1
202 leg_result S4b $ok
203fi
204restore_all || exit 3
206# ---------------------------------------------------------------------------
207echo "SAB-F3A LEG S5: the coordinator publishes WITHOUT taking the lock"
208echo " PREDICT RED: gate-f3a arm 3 -- some round yields two OKs (a lost"
209echo " update), which is what the lock exists to prevent"
210sed -i 's/(with-root-lock (coordinator-record-root c)/((lambda (sab-r sab-w sab-t) (sab-t)) (coordinator-record-root c)/' src/folio/doc/coordinator.sgl
211if plant_landed S5 "$COORD" "$h_coord" 'sab-t' 1 && build_planted S5; then
212 if sh test/gate-f3a.sh 3 >"$work/gate-S5.log" 2>&1; then
213 fail_leg S5 "gate arm 3 stayed GREEN with the lock bypassed"
214 legs=$((legs + 1))
215 else
216 if grep -q "expected exactly 1 OK, got 2" "$work/gate-S5.log"; then
217 legs=$((legs + 1)); reds=$((reds + 1))
218 echo "SAB-F3A LEG S5: RED AS PREDICTED ($(grep -c 'expected exactly 1 OK, got 2' "$work/gate-S5.log") round(s) with a lost update)"
219 else
220 fail_leg S5 "gate went red but not with the predicted two-OK signature: $(grep 'GATE-F3A FAIL' "$work/gate-S5.log" | head -2)"
221 legs=$((legs + 1))
222 fi
223 fi
224fi
225restore_all || exit 3
227# ---------------------------------------------------------------------------
228echo "SAB-F3A LEG S6: the status transition no longer writes its @update"
229echo " PREDICT RED: sab-journal (the count-increase postcheck REFUSES the"
230echo " unjournalled write, so the transition cannot publish)"
231echo " PREDICT GREEN: sab-uncontended"
232# The matched text contains string-append's OPENING paren, so the
233# replacement carries one fewer close than the match: "")) not ""))).
234# (The first spelling failed the planted-tree build -- caught by
235# build_planted as SETUP-FAILED, which is that guard doing its job.)
236sed -i '0,/(string-append "\\n" update-src)))/s//""))/' src/folio/doc/transaction.sgl
237if plant_landed S6 "$TXN" "$h_txn" 'string-append "\\n" update-src' 1 && build_planted S6; then
238 run_driver S6 "sab-journal,sab-uncontended" || true
239 ok=0
240 expect S6 "$work/driver-S6.log" sab-journal RED || ok=1
241 expect S6 "$work/driver-S6.log" sab-uncontended GREEN || ok=1
242 leg_result S6 $ok
243fi
244restore_all || exit 3
246# ---------------------------------------------------------------------------
247echo "SAB-F3A LEG S7: acquisition degrades to a SHARED lock (excludes nothing"
248echo " it cooperates with, still conflicts with an exclusive prober)"
249echo " PREDICT RED: sab-coord-init via the SECOND-ACQUISITION direction of"
250echo " the exclusion probe -- the raw-flock direction alone"
251echo " cannot see this plant, which is why 1b exists"
252sed -i 's/(process-spawn-pipe "flock" "-n" path/(process-spawn-pipe "flock" "-s" "-n" path/' src/folio/doc/lock.sgl
253sed -i 's/(process-spawn-pipe "flock" "-w"/(process-spawn-pipe "flock" "-s" "-w"/' src/folio/doc/lock.sgl
254if plant_landed S7 "$LOCK" "$h_lock" '"-s"' 2 && build_planted S7; then
255 run_driver S7 "sab-coord-init" || true
256 ok=0
257 expect S7 "$work/driver-S7.log" sab-coord-init RED || ok=1
258 grep -q "SECOND ACQUISITION" "$work/driver-S7.log" \
259 || { fail_leg S7 "red, but not via the second-acquisition direction"; ok=1; }
260 leg_result S7 $ok
261fi
262restore_all || exit 3
264# ---------------------------------------------------------------------------
265echo "SAB-F3A LEG S8: a successful rebase stops reporting itself"
266echo " PREDICT RED: sab-rebase-report (precondition 5: a silent rebase is a"
267echo " defect even when byte-correct)"
268echo " PREDICT GREEN: sab-uncontended"
269sed -i 's/rebased: rebased?$/rebased: #f/' src/folio/doc/transaction.sgl
270if plant_landed S8 "$TXN" "$h_txn" 'rebased: #f' 1 && build_planted S8; then
271 run_driver S8 "sab-rebase-report,sab-uncontended" || true
272 ok=0
273 expect S8 "$work/driver-S8.log" sab-rebase-report RED || ok=1
274 expect S8 "$work/driver-S8.log" sab-uncontended GREEN || ok=1
275 leg_result S8 $ok
276fi
277restore_all || exit 3
279# ---------------------------------------------------------------------------
280echo "SAB-F3A LEG SA: assertion layer -- flip sab-revcheck's expected reason"
281echo " PREDICT RED: sab-revcheck (the driver's own assertions are live)"
282sed -i "s/(assert-refusal \"stale title edit refused\" r 'revision-mismatch)/(assert-refusal \"stale title edit refused\" r 'wrong-kind)/" test/f3a-sab-driver.sgl
283if plant_landed SA "$DRIVER" "$h_driver" "'wrong-kind" 1; then
284 # No rebuild needed: the driver is interpreted from source.
285 run_driver SA "sab-revcheck" || true
286 ok=0
287 expect SA "$work/driver-SA.log" sab-revcheck RED || ok=1
288 leg_result SA $ok
289fi
290restore_all || exit 3
292# ---------------------------------------------------------------------------
293echo "SAB-F3A: restoring and re-running the FULL gate as the restore proof"
294if ! "$sigil" build >"$work/build-final.log" 2>&1; then
295 echo "SAB-F3A SETUP-FAILED: restored tree does not build"; exit 3
296fi
297if sh test/gate-f3a.sh >"$work/gate-final.log" 2>&1; then
298 echo "SAB-F3A: restored gate GREEN"
299else
300 echo "SAB-F3A FAILED: the gate is RED on the RESTORED tree -- a plant leaked"; tail -5 "$work/gate-final.log"; exit 1
301fi
303echo "---"
304echo "F3A_SABOTAGE legs=$legs red=$reds setup_failed=$setup_fails"
305if [ "$legs" -eq 10 ] && [ "$reds" -eq 10 ] && [ "$setup_fails" -eq 0 ]; then
306 exit 0
307else
308 exit 1
309fi