AtlatestRepositoryfolio
1
#!/bin/sh2
# gate-f2b.sh -- the F2b gate: dual-format discovery, the bounded worker, and the3
# native/WASM revision conformance vector.4
#5
# RUN: sh test/gate-f2b.sh (all arms)6
# sh test/gate-f2b.sh 4 5 (only those arms)7
# SABOTAGE: sh test/sabotage-f2b.sh (proves these arms can go red)8
#9
# WHAT THIS GATE IS ARRANGED TO AVOID10
#11
# Every arm below either observes a positive control in the same run, or is an12
# assertion about something this gate made happen on purpose. The specific13
# failure being designed against is the one this estate keeps catching: a gate14
# that is GREEN BECAUSE NOTHING COULD MAKE IT RED. Concretely --15
#16
# - Arm 4 requires the bounds to FIRE and, in the same call, to STAND DOWN. A17
# `ulimit`/kill pair that fired unconditionally would pass a firing-only18
# check and would break every parse in production.19
# - Arm 5 requires the killer note to be contained AND the ordinary notes in20
# the same scan to come back indexed. Containment alone is satisfied by a21
# service that fails everything.22
# - Arm 8 requires the conformance vector to CATCH a known-wrong implementation23
# before its agreement between the real ones counts for anything. A vector24
# that has only ever seen agreement has not been shown to detect25
# disagreement.26
# - Arm 9 asserts the state its toolchain is ACTUALLY in and says which one27
# that was. It cannot skip: a version-gated arm that skipped would report28
# green for the same reason a missing tool does.29
#30
# SETUP-FAILED IS A DISTINCT OUTCOME from RED and from GREEN. Where this gate31
# cannot establish its subject, it says so in those words and exits non-zero32
# without claiming anything about the product.34
set -u36
repo_dir=$(cd "$(dirname "$0")/.." && pwd)37
cd "$repo_dir" || exit 139
# Scratch lives in /tmp. The one exception is `sigil build`, which writes40
# `build/dev/...` under the repo root -- gitignored, but it IS a write, and the41
# earlier version of this comment claimed otherwise. No SOURCE file is touched.42
work=$(mktemp -d /tmp/gate-f2b.XXXXXX) || { echo "SETUP-FAILED: no temp dir"; exit 3; }43
trap 'chmod -R u+rwX "$work" 2>/dev/null; rm -rf "$work"' EXIT INT TERM45
sigil="${SIGIL:-sigil}"46
redirects="${SIGIL_F2B_REDIRECTS:-$repo_dir/dev-redirects.sgl}"47
export SIGIL_REDIRECTS="$redirects"48
VECTORS="$repo_dir/test/fixtures/f2b-revision-vectors"50
fails=051
fail () { echo "GATE-F2B FAIL: $*" >&2; fails=$((fails + 1)); }52
setup_failed () { echo "GATE-F2B SETUP-FAILED: $*" >&2; exit 3; }54
# An arm's summary line must not be able to say OK when that arm failed.55
#56
# This is not tidiness. `fail` does not abort the arm -- deliberately, so one57
# broken assertion does not hide the five after it -- so an unconditional58
# "arm N OK" at the bottom prints next to its own failures and the transcript59
# reads green to anyone skimming summary lines. Observed on this gate's first60
# run: arm 1 reported two failures and then printed "arm 1 OK".61
#62
# `arm_begin` snapshots the failure count; `arm_ok` prints the summary only if63
# nothing was added since, and otherwise says the arm failed.64
arm_fails_at_start=065
arm_begin () { arm_fails_at_start=$fails; echo "--- $*"; }66
arm_ok () {67
if [ "$fails" -eq "$arm_fails_at_start" ]; then68
echo "$*"69
else70
echo "(no OK line: this arm recorded $((fails - arm_fails_at_start)) failure(s) above)"71
fi72
}74
want_arms="$*"75
should_run () {76
[ -z "$want_arms" ] && return 077
for a in $want_arms; do [ "$a" = "$1" ] && return 0; done78
return 179
}81
plain () { sed 's/\x1b\[[0-9;]*m//g' "$@"; }83
# ---------------------------------------------------------------------------84
# INPUTS. Recorded, not assumed. A caret range is an unrecorded input, so the85
# RESOLVED identities are printed -- results in this transcript are true of this86
# (tree, toolchain, dependency) triple and of nothing else.87
# ---------------------------------------------------------------------------88
command -v "$sigil" >/dev/null 2>&1 || setup_failed "no \`$sigil\` on PATH"89
command -v node >/dev/null 2>&1 || setup_failed "no \`node\` on PATH; the WASM conformance arm cannot run, and skipping it would report green for the same reason a missing tool does"90
[ -f "$redirects" ] || setup_failed "no redirects file at $redirects"91
[ -d "$VECTORS" ] || setup_failed "no revision vectors at $VECTORS"93
compiler_version=$("$sigil" --version 2>&1 | plain | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)94
[ -n "$compiler_version" ] || setup_failed "could not read the compiler version"95
sigil_doc_dir=$(sed -n 's/.*"codeberg:sigil\/sigil-doc".*//p' "$redirects" >/dev/null 2>&1; echo "$repo_dir/../sigil-doc")96
sigil_doc_sha=$(git -C "$sigil_doc_dir" rev-parse --short HEAD 2>/dev/null || echo "UNKNOWN")97
sigil_doc_desc=$(git -C "$sigil_doc_dir" describe --tags 2>/dev/null || echo "UNKNOWN")99
echo "=========================================================================="100
echo "gate-f2b inputs"101
echo " repo : $repo_dir @ $(git -C "$repo_dir" rev-parse --short HEAD 2>/dev/null || echo UNKNOWN)"102
echo " sigil : $compiler_version ($("$sigil" --version 2>&1 | plain | grep -oE '\([0-9a-f]+\)' | head -1))"103
echo " sigil-doc : $sigil_doc_sha $sigil_doc_desc (via dev-redirects, a WORKING TREE not a tag)"104
echo " node : $(node --version)"105
echo " redirects : $redirects"106
echo " scratch : $work"107
echo "=========================================================================="109
# The library tree must exist and be current; `sigil test` does not build it.110
if ! "$sigil" build --redirects "$redirects" > "$work/build.log" 2>&1; then111
tail -30 "$work/build.log" >&2112
setup_failed "the library tree would not build; no arm below is evidence about anything"113
fi114
[ -d build/dev/lib ] || setup_failed "build reported success but build/dev/lib is absent"116
# ---------------------------------------------------------------------------117
# WHICH SHAPE IS UNDER TEST118
#119
# F2B_SHAPE=dev (default) the worker script under the `sigil` runtime,120
# with `-L` paths. Cannot ship.121
# F2B_SHAPE=bundled the folio bundle itself, re-execed under the worker122
# marker. THIS IS WHAT SHIPS.123
#124
# Both drive the SAME payload -- `(folio doc worker)` -- so a difference between125
# the shapes is a difference in how the process is started, not in what it runs.126
# That is deliberate: containment was proven for the dev shape, and bundling127
# changes what is spawned, so the claim has to be RE-PROVEN rather than assumed128
# to transfer. Running the identical arms against both is what makes the129
# comparison mean anything.130
#131
# The gate itself still runs under `sigil` in both shapes -- it is a test harness,132
# not a deployed artifact. What varies is the CHILD the service spawns, which is133
# the thing whose deployability is in question.134
shape="${F2B_SHAPE:-dev}"135
case "$shape" in136
dev)137
WORKER="$sigil -L build/dev/lib -L src tools/f2b_inspect_worker.sgl"138
# Sigil expression for the service's worker argv, spliced into probes below.139
WORKER_ARGV='(default-worker-argv "build/dev/lib" "src")'140
;;141
bundled)142
# THE BUNDLE ITSELF IS THE WORKER. There is no second file.143
#144
# The gate supplies only the PATH -- it cannot know it otherwise, since it145
# runs under `sigil` rather than inside the bundle. The marker, the `exec` and146
# the quoting all come from the product's own `worker-argv-for`, so a probe147
# here cannot pass by reimplementing what it is meant to check.148
bundled_worker="$repo_dir/build/dev/bin/folio"149
[ -e "$bundled_worker" ] || setup_failed "F2B_SHAPE=bundled but $bundled_worker does not exist; run \`sigil build\` first"151
# THE PREFLIGHT ASKS WHETHER THE SHAPE STARTS, NOT WHETHER IT PARSES.152
#153
# It once ran a full inspection and demanded success. Sabotage leg 6 plants154
# uppercase hex in the revision contract; the worker computes revisions, so155
# the planted worker legitimately fails to parse -- and the preflight reported156
# SETUP-FAILED before the arm could catch the defect. The leg came back157
# INCONCLUSIVE: honest, and proving nothing.158
#159
# A PREFLIGHT MUST NOT BE ABLE TO FAIL FOR ANY REASON A LEG IS TRYING TO160
# CAUSE. The no-argument usage path exercises marker dispatch and process161
# start and touches no parsing, hashing or classification.162
bw_usage=$(env -i PATH=/usr/bin:/bin HOME=/tmp FOLIO_DOC_WORKER=1 "$bundled_worker" 2>&1 | head -1)163
case "$bw_usage" in164
*"usage: FOLIO_DOC_WORKER=1 folio"*) : ;;165
*) setup_failed "the bundled worker did not start under a stripped PATH (expected its usage line, got: '$bw_usage'). The shape under test is broken before any arm." ;;166
esac167
# Same shape the PRODUCT computes in `bundled-worker-argv`: re-exec the168
# binary with the marker set, via POSIX sh. Written here as the equivalent169
# command so direct-invocation arms match what the service does.170
WORKER="env FOLIO_DOC_WORKER=1 $bundled_worker"171
WORKER_ARGV="(worker-argv-for \"$bundled_worker\")"172
;;173
*) setup_failed "unknown F2B_SHAPE '$shape' (expected dev or bundled)" ;;174
esac176
echo " SHAPE UNDER TEST: $shape"177
echo " worker child : $WORKER"178
echo "=========================================================================="180
run_sgl () { "$sigil" -L build/dev/lib -L src "$@"; }182
# EVERY ARM MUST PRODUCE ITS OWN SUBJECT.183
#184
# Found by an adversarial review, and it voided three sabotage legs. Arms 2 and 3185
# used to grep `$work/disc.out`, which only arm 1 created, and arm 10 greps186
# `$work/scan.out`, which only arm 5 created and `$ok_root`, which only arm 2 set187
# (under `set -u`). So `sh test/gate-f2b.sh 3` failed on a CLEAN tree -- and188
# sabotage legs 1, 2 and 8, which invoke exactly those single arms, reported "RED189
# as required" with plausible `GATE-F2B FAIL` lines **whether or not their plant190
# did anything**.191
#192
# Three legs of evidence that proved nothing, and the failure text looked right.193
# The documented usage `sh test/gate-f2b.sh 4 5` invites precisely this.194
#195
# So the shared subjects are built by idempotent `ensure_*` helpers that every196
# dependent arm calls. An arm run alone now measures what it claims to.197
ensure_disc () {198
[ -f "$work/disc.out" ] && return 0199
cat > "$work/disc.sgl" <<EOF200
(import (sigil core) (folio doc discovery))201
(define c (discover-sources "$corpus"))202
(display (census->report c))203
(display (format "MACHINE\texamined=~a\tsgd=~a\tmd=~a\tskipped=~a\tbalanced=~a\tcomplete=~a\tcollisions=~a~%"204
(census-examined c) (census-count-by-format c 'sgd)205
(census-count-by-format c 'markdown) (length (census-skipped c))206
(census-balanced? c) (census-complete? c) (length (census-collisions c))))207
EOF208
run_sgl "$work/disc.sgl" > "$work/disc.out" 2>&1 \209
|| { cat "$work/disc.out" >&2; fail "discovery probe did not run"; }210
}212
ok_root="$work/readable"213
ensure_ok_root () {214
[ -d "$ok_root" ] && return 0215
mkdir -p "$ok_root/notes"216
printf '#lang sigil/doc\n@note["topics/r" title: "R"]\n\n@p{x}\n\n@end note\n' > "$ok_root/notes/r.sgd"217
cat > "$work/disc2.sgl" <<EOF218
(import (sigil core) (folio doc discovery))219
(display (census->report (discover-sources "$ok_root")))220
EOF221
run_sgl "$work/disc2.sgl" > "$work/disc2.out" 2>&1222
}224
ensure_scan () {225
[ -f "$work/scan.out" ] && return 0226
cat > "$work/scan.sgl" <<EOF227
(import (sigil core) (folio doc service) (folio doc bounded))228
(define svc (make-service $WORKER_ARGV229
(make-limits 1048576 5000)))230
(define r (scan-root svc "$corpus"))231
(display (result->report r))232
(for-each (lambda (e)233
(display (format "ENTRY\t~a\t~a\t~a~%"234
(entry-slug e) (entry-status e) (entry-reason e))))235
(result-entries r))236
(display (format "ACCOUNTED\t~a~%" (result-accounted? r)))237
EOF238
run_sgl "$work/scan.sgl" > "$work/scan.out" 2>&1239
}241
# ---------------------------------------------------------------------------242
# CORPUS. Built here rather than committed, so each arm's inputs are visible in243
# the script that asserts on them.244
# ---------------------------------------------------------------------------245
corpus="$work/corpus"246
mkdir -p "$corpus/notes/topics" "$corpus/notes/locked"247
printf '#lang sigil/doc\n@note["topics/alpha" title: "Alpha"]\n\n@p{ordinary}\n\n@end note\n' > "$corpus/notes/topics/alpha.sgd"248
printf '#lang sigil/doc\n@note["topics/beta" title: "Beta"]\n\n@p{ordinary}\n\n@end note\n' > "$corpus/notes/topics/beta.sgd"249
printf '# Gamma\n\nmarkdown\n' > "$corpus/notes/topics/gamma.md"250
cp test/fixtures/f2a-malformed.sgd "$corpus/notes/topics/broken.sgd"251
printf '#lang sigil/doc\n@note["topics/dup-one" title: "One"]\n\n@task["shared-id" title: "T" status: ready]{x}\n\n@end note\n' > "$corpus/notes/topics/dup-one.sgd"252
printf '#lang sigil/doc\n@note["topics/dup-two" title: "Two"]\n\n@task["shared-id" title: "T" status: ready]{x}\n\n@end note\n' > "$corpus/notes/topics/dup-two.sgd"253
# The .sgd/.md slug collision F2's sabotage list names.254
printf '#lang sigil/doc\n@note["topics/clash" title: "C"]\n\n@p{sgd}\n\n@end note\n' > "$corpus/notes/topics/clash.sgd"255
printf '# Clash\n\nmd\n' > "$corpus/notes/topics/clash.md"256
printf 'not a note\n' > "$corpus/notes/topics/readme.txt"257
ln -s /nonexistent/target "$corpus/notes/topics/dangling.md"258
printf '# swap\n' > "$corpus/notes/topics/.beta.md.swp"259
printf '# inside\n' > "$corpus/notes/locked/inside.md"260
chmod 000 "$corpus/notes/locked"262
# The slow note: 4 MB of body. Measured at ~90 kB/s parse throughput, so this263
# takes roughly 45 s unbounded -- comfortably past the short wall limit arm 5264
# imposes, and far enough past it that the arm is not timing-marginal.265
{ printf '#lang sigil/doc\n@note["topics/slow" title: "S"]\n\n@p{'266
head -c 4000000 /dev/zero | tr '\0' 'z'267
printf '}\n\n@end note\n'; } > "$corpus/notes/topics/slow.sgd"269
# ---------------------------------------------------------------------------270
# ARM 1 -- DISCOVERY FINDS BOTH FORMATS AND REPORTS ITS DENOMINATOR271
# ---------------------------------------------------------------------------272
if should_run 1; then273
arm_begin "arm 1: dual-format discovery and the denominator"274
ensure_disc275
m=$(grep '^MACHINE' "$work/disc.out")276
# BOTH formats must be found. A discovery layer that found only .md would277
# satisfy "it ran" and fail the entire point of the phase.278
echo "$m" | grep -q 'sgd=7' || { echo "$m" >&2; fail "arm 1: expected 7 .sgd candidates (alpha beta broken dup-one dup-two clash slow)"; }279
echo "$m" | grep -q 'md=2' || { echo "$m" >&2; fail "arm 1: expected 2 .md candidates (gamma clash; dangling.md is a SKIP, not a candidate)"; }280
# THE ACCOUNTING IDENTITY. This is what makes `examined` a denominator.281
echo "$m" | grep -q 'balanced=#t' || { echo "$m" >&2; fail "arm 1: census does not balance -- examined != candidates + skipped"; }282
# SKIPPED entries must NAME A REASON. A skip with no reason is a silent drop283
# wearing a count.284
grep -q 'not-a-source-format' "$work/disc.out" || fail "arm 1: the .txt file was not skipped with a named reason"285
grep -q 'not-a-regular-file' "$work/disc.out" || fail "arm 1: the dangling symlink was not skipped with a named reason"286
arm_ok "arm 1 OK: both formats discovered; examined = candidates + skipped; every skip names a reason"287
fi289
# ---------------------------------------------------------------------------290
# ARM 2 -- "SKIPPED" AND "NOT SEEN" DO NOT PRINT THE SAME WAY291
#292
# The corpus contains a directory that will not enumerate. Its files are in NO293
# bucket and are NOT in `examined`, so every count is a lower bound. The census294
# must say that in its own words, and -- the half that is easy to forget -- must295
# NOT say it when the root is fully readable.296
# ---------------------------------------------------------------------------297
if should_run 2; then298
arm_begin "arm 2: skipped vs not-seen"299
ensure_disc; ensure_ok_root300
grep -q 'completeness : INCOMPLETE' "$work/disc.out" \301
|| { grep completeness "$work/disc.out" >&2; fail "arm 2: an unreadable directory did not make the census INCOMPLETE"; }302
grep -q 'LOWER BOUNDS' "$work/disc.out" \303
|| fail "arm 2: the incomplete census does not say its counts are lower bounds"304
# THE TWO FACTS MUST BE SEPARATELY OBSERVABLE, on a root where they DISAGREE.305
#306
# An earlier version of this check greped the shared corpus for "INCOMPLETE and307
# 0 skips" -- unfalsifiable, because that corpus always contains two skips308
# regardless of the not-seen machinery, so the conjunction could never be true.309
# Found by an adversarial review.310
#311
# The real question is whether a root with an unreadable directory and NO skips312
# still reports INCOMPLETE. That is the case where conflating the two would be313
# invisible, so it is the case worth building.314
onlyloop="$work/onlyloop"; mkdir -p "$onlyloop/notes/shut"315
printf '#lang sigil/doc\n@note["topics/o" title: "O"]\n\n@p{x}\n\n@end note\n' > "$onlyloop/notes/o.sgd"316
printf '# hidden\n' > "$onlyloop/notes/shut/inside.md"317
chmod 000 "$onlyloop/notes/shut"318
cat > "$work/onlyloop.sgl" <<EOF319
(import (sigil core) (folio doc discovery))320
(display (census->report (discover-sources "$onlyloop")))321
EOF322
run_sgl "$work/onlyloop.sgl" > "$work/onlyloop.out" 2>&1323
chmod 755 "$onlyloop/notes/shut" 2>/dev/null324
if grep -qE '^ skipped +: 0' "$work/onlyloop.out"; then325
grep -q 'completeness : INCOMPLETE' "$work/onlyloop.out" \326
|| { cat "$work/onlyloop.out" >&2; fail "arm 2: a root with ZERO skips and an unreadable directory did not report INCOMPLETE -- 'skipped' and 'not seen' are being reported as the same fact"; }327
echo " [zero skips + one unreadable dir] -> skipped 0 AND completeness INCOMPLETE, independently"328
else329
# If the corpus grew skips, say so rather than passing silently: the check330
# needs a zero-skip root to mean anything.331
fail "arm 2: the zero-skip fixture produced skips, so the skipped-vs-not-seen separation was NOT tested"332
fi334
# POSITIVE CONTROL: the same walker over a fully readable root must report335
# COMPLETE. Without this, a census hard-coded to say INCOMPLETE would pass.336
grep -q 'completeness : COMPLETE' "$work/disc2.out" \337
|| { cat "$work/disc2.out" >&2; fail "arm 2 POSITIVE CONTROL: a fully readable root did not report COMPLETE, so INCOMPLETE means nothing"; }338
arm_ok "arm 2 OK: unreadable dir -> INCOMPLETE + lower-bounds language; readable root -> COMPLETE (control)"339
fi341
# ---------------------------------------------------------------------------342
# ARM 3 -- THE TWO-FORMAT COLLISION IS REFUSED, NOT RESOLVED343
#344
# F2's sabotage list: "silently prefer `.sgd` duplicate over `.md`".345
# ---------------------------------------------------------------------------346
if should_run 3; then347
arm_begin "arm 3: .sgd/.md slug collision"348
ensure_disc349
grep -q 'slug conflicts: 1' "$work/disc.out" \350
|| { grep 'slug conflicts' "$work/disc.out" >&2; fail "arm 3: the clash.sgd/clash.md collision was not reported"; }351
# BOTH sides must be named. Reporting one is how a silent preference looks352
# from the outside.353
grep -q 'clash.md' "$work/disc.out" || fail "arm 3: the collision report does not name the .md side"354
grep -q 'clash.sgd' "$work/disc.out" || fail "arm 3: the collision report does not name the .sgd side"355
arm_ok "arm 3 OK: collision reported with BOTH sides named; neither format silently preferred"356
fi358
# ---------------------------------------------------------------------------359
# ARM 4 -- THE BOUNDS FIRE, AND THE BOUNDS STAND DOWN360
# ---------------------------------------------------------------------------361
if should_run 4; then362
arm_begin "arm 4: bounded-worker self-test"363
cat > "$work/self.sgl" <<'EOF'364
(import (sigil core) (folio doc bounded))365
(for-each (lambda (r)366
(display (format "SELFTEST\t~a\t~a\t~a\t~a~%"367
(car r) (cadr r) (caddr r) (if (cadddr r) "PASS" "FAIL"))))368
(bounded-self-test))369
EOF370
if ! run_sgl "$work/self.sgl" > "$work/self.out" 2>&1; then371
cat "$work/self.out" >&2; fail "arm 4: the self-test did not run"372
else373
cat "$work/self.out"374
grep -q 'memory cap fires.*PASS' "$work/self.out" || fail "arm 4: the memory cap did not fire on a process that must exceed it -- the cap may not be applied at all"375
grep -q 'wall timeout fires.*PASS' "$work/self.out" || fail "arm 4: the wall timeout did not fire on a process that must exceed it"376
# THE RESTRAINT HALF. Without it, bounds that fired unconditionally pass.377
grep -q 'bounds stand down.*PASS' "$work/self.out" || fail "arm 4: the bounds fired on an ordinary fast child -- a bound that fires on everything is not a bound"378
[ "$(grep -c 'FAIL' "$work/self.out")" = "0" ] || fail "arm 4: a self-test probe reported FAIL"379
arm_ok "arm 4 OK: both bounds proven able to fire AND proven to stand down, in one call"380
fi381
fi383
# ---------------------------------------------------------------------------384
# ARM 5 -- CONTAINMENT THROUGH THE SERVICE, WITH THE RESTRAINT CONTROL385
#386
# The service scans a root containing a note measured to overrun the limits. It387
# must survive, classify that note `unindexed` with a resource reason, AND index388
# the ordinary notes in the SAME scan.389
# ---------------------------------------------------------------------------390
if should_run 5; then391
arm_begin "arm 5: the service survives an overrunning parse"392
ensure_scan393
if ! grep -q '^ACCOUNTED' "$work/scan.out"; then394
cat "$work/scan.out" >&2395
fail "arm 5: THE SCAN ITSELF DIED. This is the exact outcome the bounded worker exists to prevent."396
else397
# The overrunning note is contained, with a RESOURCE reason.398
grep -qE '^ENTRY notes/topics/slow unindexed (wall-timeout|memory-cap)' "$work/scan.out" \399
|| { grep '^ENTRY' "$work/scan.out" >&2; fail "arm 5: the overrunning note was not contained as unindexed/resource"; }400
# THE RESTRAINT CONTROL: ordinary notes in the same run must be INDEXED.401
# Without this the arm is satisfied by a service that fails every note.402
grep -q '^ENTRY notes/topics/alpha indexed' "$work/scan.out" \403
|| { grep '^ENTRY' "$work/scan.out" >&2; fail "arm 5 RESTRAINT CONTROL: an ordinary note did not index in the same scan; the bounds are firing on everything"; }404
grep -q '^ENTRY notes/topics/beta indexed' "$work/scan.out" \405
|| fail "arm 5 RESTRAINT CONTROL: a second ordinary note did not index"406
# A genuinely broken document is DOCUMENT-REJECTED, which is a different407
# answer from unindexed and must stay different.408
grep -q '^ENTRY notes/topics/broken document-rejected' "$work/scan.out" \409
|| { grep '^ENTRY' "$work/scan.out" >&2; fail "arm 5: a document with located diagnostics was not classified document-rejected"; }410
# Markdown is retained, not dropped and not failed.411
grep -q '^ENTRY notes/topics/gamma not-inspectable' "$work/scan.out" \412
|| fail "arm 5: the Markdown note was not retained as not-inspectable"413
# Every discovered candidate has exactly one entry.414
grep -q '^ACCOUNTED #t' "$work/scan.out" \415
|| fail "arm 5: candidates and entries do not reconcile -- a note went missing between discovery and inspection"416
arm_ok "arm 5 OK: overrunning note contained; ordinary notes indexed in the same scan; broken != unindexed; markdown retained"417
fi418
fi420
# ---------------------------------------------------------------------------421
# ARM 6 -- A DEAD, TRUNCATED, OR ABSENT WORKER IS CONTAINED AND CORRECTLY NAMED422
#423
# The reader pathology that motivated the bounded worker is fixed on this424
# toolchain (arm 9), so a manuscript can no longer kill the parser. The crash425
# path is therefore proven with children that genuinely die -- otherwise this426
# clause of the contract would have no evidence at all on a fixed toolchain.427
# ---------------------------------------------------------------------------428
if should_run 6; then429
arm_begin "arm 6: dead / truncated / absent worker"430
small="$work/small"; mkdir -p "$small/notes"431
printf '#lang sigil/doc\n@note["topics/s" title: "S"]\n\n@p{x}\n\n@end note\n' > "$small/notes/s.sgd"432
probe_worker () { # $1 = label, $2 = expected reason, $3.. = argv433
label=$1; expected=$2; shift 2434
argv_sgl=$(for a in "$@"; do printf '"%s" ' "$a"; done)435
cat > "$work/crash.sgl" <<EOF436
(import (sigil core) (folio doc service) (folio doc bounded))437
(define svc (make-service (list $argv_sgl) (make-limits 1048576 5000)))438
(define r (scan-root svc "$small"))439
(for-each (lambda (e) (display (format "R\t~a\t~a~%" (entry-status e) (entry-reason e))))440
(result-entries r))441
(display "SURVIVED\n")442
EOF443
if ! run_sgl "$work/crash.sgl" > "$work/crash.out" 2>&1; then444
cat "$work/crash.out" >&2; fail "arm 6 [$label]: the SCAN died rather than containing the worker"445
return446
fi447
grep -q '^SURVIVED' "$work/crash.out" || fail "arm 6 [$label]: the scan did not complete"448
grep -q "^R unindexed $expected" "$work/crash.out" \449
|| { cat "$work/crash.out" >&2; fail "arm 6 [$label]: expected unindexed/$expected"; }450
echo " [$label] -> unindexed/$expected, scan survived"451
}452
probe_worker "SIGSEGV" worker-crashed sh -c 'kill -SEGV $$' worker453
probe_worker "non-zero exit" worker-crashed sh -c 'exit 7' worker454
# A child that printed plausible output but never reached its completion455
# marker. This is the truncation case, and calling it clean is the free-pass456
# absence claim the marker exists to refuse.457
probe_worker "truncated" worker-crashed sh -c 'echo SOURCE; echo NODES 3' worker458
# A worker that was never there is SETUP-FAILED, not a crash: it says nothing459
# about the document, and a misconfigured path must not report the whole estate460
# as broken.461
probe_worker "absent program" setup-failed definitely-not-a-real-program-xyz462
# THE MARKER MUST NOT BE FORGEABLE BY THE DOCUMENT.463
#464
# The worker prints the document's own node ids. If the completion marker were465
# matched as a SUBSTRING, a note carrying `@task["F2B_WORKER_DONE" ...]` would466
# emit `ID<TAB>unique<TAB>F2B_WORKER_DONE` and thereby certify its own truncated467
# run -- the document supplying the token that says the worker finished.468
# `has-exact-line?` is what prevents that, and this probe is what makes that469
# choice falsifiable.470
cat > "$work/forge.sgl" <<'EOF'471
(import (sigil core) (folio doc bounded))472
(define forged "SOURCE\tx.sgd\nID\tunique\tF2B_WORKER_DONE\n")473
(define o (classify-worker-output forged "" 0 5 default-limits))474
(display (format "FORGE\t~a\t~a~%" (outcome-kind o) (outcome-reason o)))475
EOF476
run_sgl "$work/forge.sgl" > "$work/forge.out" 2>&1477
grep -q '^FORGE unindexed worker-crashed' "$work/forge.out" \478
|| { cat "$work/forge.out" >&2; fail "arm 6: a document whose node id is the completion marker forged a clean verdict for a truncated run"; }479
echo " [document forges the completion marker in a node id] -> unindexed/worker-crashed"481
arm_ok "arm 6 OK: crash, truncation, absence and marker-forgery each contained and named distinctly"482
fi484
# ---------------------------------------------------------------------------485
# ARM 7 -- `parse-error-unlocated` IS A SUSPECT INSTRUMENT, NOT A BROKEN DOCUMENT486
#487
# F2a's carry-forward, and on this toolchain it is reproducible rather than488
# theoretical: a healthy 1 MB manuscript reports489
# `parse-error-unlocated: list->string: out of memory` under a squeezed cap and490
# parses cleanly with headroom. The arm proves BOTH directions, because only491
# the pair establishes that the code tracks the host rather than the document.492
# ---------------------------------------------------------------------------493
if should_run 7; then494
arm_begin "arm 7: parse-error-unlocated is instrument-suspect"495
sq="$work/squeeze"; mkdir -p "$sq/notes"496
{ printf '#lang sigil/doc\n@note["topics/big" title: "B"]\n\n@p{'497
head -c 1000000 /dev/zero | tr '\0' 'z'498
printf '}\n\n@end note\n'; } > "$sq/notes/big.sgd"500
# (a) SQUEEZED. THE CAP IS DERIVED, NOT HARDCODED.501
#502
# This arm used a fixed 300 MB cap, which squeezed the DEV worker (it needs503
# somewhere between 300 and 500 MB for a 1 MB manuscript) and DID NOT squeeze504
# the BUNDLED worker at all -- measured, the bundle parses the same note505
# cleanly at 300 MB and only fails between 200 and 300 MB. So on the shape that506
# actually ships, the arm was testing nothing and said so by failing.507
#508
# Tuning the constant down until it went green would have been the wrong repair509
# twice over: it hardcodes a second shape's number, and it hides that the two510
# shapes have materially different memory profiles. Instead the arm WALKS THE511
# CAP DOWN until the worker actually fails, and reports the operating point it512
# found. If no cap in the ladder produces a failure, the arm cannot establish513
# its subject and says SETUP-FAILED -- it never passes by not squeezing.514
squeeze_cap=""515
for cap in 400000 300000 250000 200000 150000 120000; do516
cat > "$work/sqprobe.sgl" <<EOF517
(import (sigil core) (folio doc service) (folio doc bounded))518
(define svc (make-service $WORKER_ARGV (make-limits $cap 60000)))519
(for-each (lambda (e) (display (format "SQ\t~a\t~a~%" (entry-status e) (entry-reason e))))520
(result-entries (scan-root svc "$sq")))521
EOF522
run_sgl "$work/sqprobe.sgl" > "$work/sq.out" 2>&1523
if grep -q '^SQ indexed' "$work/sq.out"; then524
echo " cap ${cap} kB: still parses cleanly (not squeezed yet)"525
else526
squeeze_cap="$cap"527
echo " cap ${cap} kB: the parse stopped succeeding -- operating point for this shape"528
break529
fi530
done532
if [ -z "$squeeze_cap" ]; then533
setup_failed "arm 7: no address-space cap in the ladder made the $shape worker fail on a 1 MB manuscript, so the classification under memory pressure CANNOT BE TESTED. This is not a pass."534
fi536
cat "$work/sq.out"537
# THE ASSERTION: whatever the failure was, it must not be blamed on the document.538
if grep -q '^SQ document-rejected' "$work/sq.out"; then539
fail "arm 7: a HEALTHY document was marked document-rejected because the worker ran out of memory. This is the trust-boundary defect the carry-forward names."540
fi541
grep -qE '^SQ unindexed (instrument-suspect|memory-cap|worker-crashed)' "$work/sq.out" \542
|| { cat "$work/sq.out" >&2; fail "arm 7: the squeezed parse was not classified as a resource/instrument failure"; }544
# (b) POSITIVE CONTROL, and the half that proves the claim: THE SAME BYTES with545
# headroom must index cleanly. Without it, (a) is satisfied by a document that546
# is simply broken.547
cat > "$work/sq2.sgl" <<EOF548
(import (sigil core) (folio doc service) (folio doc bounded))549
(define svc (make-service $WORKER_ARGV550
(make-limits 2097152 120000)))551
(for-each (lambda (e) (display (format "OK\t~a\t~a~%" (entry-status e) (entry-reason e))))552
(result-entries (scan-root svc "$sq")))553
EOF554
run_sgl "$work/sq2.sgl" > "$work/sq2.out" 2>&1555
cat "$work/sq2.out"556
grep -q '^OK indexed' "$work/sq2.out" \557
|| { cat "$work/sq2.out" >&2; fail "arm 7 POSITIVE CONTROL: the same bytes did not index with headroom, so the squeezed result says nothing about memory"; }558
# (c) THE RULE ITSELF, EXERCISED DIRECTLY.559
#560
# Sabotage leg 4 -- "treat parse-error-unlocated as a broken document" -- STAYED561
# GREEN against (a) and (b) alone, and that was a real gate defect rather than a562
# pass. The reason: a squeezed parse emits a memory signature, so it is563
# classified `memory-cap` by an EARLIER rule and the unlocated rule is never564
# reached. Arm 7 was therefore testing the memory rule while claiming to test565
# the unlocated one.566
#567
# These probes drive the real classifier with a worker whose output contains an568
# unlocated diagnostic and NO resource wording, which is the only way to reach569
# rule 3. Paired with a located-error probe, so the arm proves the two codes are570
# treated DIFFERENTLY rather than merely that one of them is non-fatal.571
probe_code () { # $1 = label, $2 = the DIAGNOSTIC line, $3 = expected status, $4 = expected reason572
cat > "$work/code.sgl" <<EOF573
(import (sigil core) (folio doc bounded))574
(define o (classify-worker-output "$2\nF2B_WORKER_DONE\n" "" 0 12 default-limits))575
(display (format "CODE\t~a\t~a\t~a~%" (outcome-kind o) (outcome-reason o)576
(outcome-document-at-fault? o)))577
EOF578
run_sgl "$work/code.sgl" > "$work/code.out" 2>&1579
got=$(grep '^CODE' "$work/code.out")580
if ! echo "$got" | grep -q "^CODE $3 $4"; then581
cat "$work/code.out" >&2582
fail "arm 7 [$1]: expected $3/$4, got: $got"583
else584
echo " [$1] -> $3/$4"585
fi586
}587
probe_code "unlocated code, no resource wording" \588
'DIAGNOSTIC\terror\tparse-error-unlocated\tx.sgd:?:?: error: parse-error-unlocated: the reader gave up' \589
unindexed instrument-suspect590
# THE CONTRAST that makes the above mean something: a LOCATED error must still591
# convict the document. Without this, "never document-rejected" is satisfied by592
# a classifier that never rejects anything.593
probe_code "located code, same shape" \594
'DIAGNOSTIC\terror\tparse-error\tx.sgd:3:1: error: parse-error: unexpected token' \595
document-rejected parse-error597
# (d) DOCUMENT TEXT MUST NOT BE ABLE TO STEER THE CLASSIFIER.598
#599
# The first version of this module searched all of stdout for a memory600
# signature. A healthy note carrying @task["out of memory" ...] was then601
# classified unindexed/memory-cap and never indexed -- document content driving a602
# resource verdict. Found by an adversarial pass, fixed by scoping the search to603
# channels the document cannot write into.604
oom="$work/oomid"; mkdir -p "$oom/notes"605
printf '#lang sigil/doc\n@note["topics/oom-notes" title: "Notes on errors"]\n\n@task["out of memory" title: "T" status: ready]{x}\n\n@end note\n' > "$oom/notes/a.sgd"606
cat > "$work/oomid.sgl" <<EOF607
(import (sigil core) (folio doc service) (folio doc bounded))608
(define svc (make-service $WORKER_ARGV default-limits))609
(for-each (lambda (e) (display (format "OOMID\t~a\t~a~%" (entry-status e) (entry-reason e))))610
(result-entries (scan-root svc "$oom")))611
EOF612
run_sgl "$work/oomid.sgl" > "$work/oomid.out" 2>&1613
grep -q '^OOMID indexed' "$work/oomid.out" \614
|| { cat "$work/oomid.out" >&2; fail "arm 7: a HEALTHY note whose node id is the string 'out of memory' was not indexed -- document content is steering the resource classifier"; }615
echo " [document text cannot steer the classifier] -> indexed"617
arm_ok "arm 7 OK: same bytes squeezed => retryable / with headroom => indexed; unlocated != located at the classifier; document text cannot forge a resource verdict"618
fi620
# ---------------------------------------------------------------------------621
# ARM 8 -- THE NATIVE/WASM REVISION CONFORMANCE VECTOR622
# ---------------------------------------------------------------------------623
if should_run 8; then624
arm_begin "arm 8: native/WASM revision conformance"625
run_sgl test/f2b-revision-native.sgl "$VECTORS" > "$work/native.txt" 2> "$work/native.err" \626
|| { cat "$work/native.err" >&2; fail "arm 8: the native revision arm did not run"; }627
node test/f2b-revision-webcrypto.js --bytes "$VECTORS" > "$work/wasm.txt" 2> "$work/wasm.err" \628
|| { cat "$work/wasm.err" >&2; fail "arm 8: the WebCrypto revision arm did not run"; }630
# A FLOOR ON THE POPULATION. Two empty files compare equal, so without this a631
# vector directory that failed to load would report perfect agreement.632
n=$(grep -c . "$work/native.txt")633
[ "$n" -ge 9 ] || fail "arm 8: only $n vectors were hashed; expected at least 9 (a cap without a floor is satisfied by a dead scanner)"635
# (a) THE TWO REAL IMPLEMENTATIONS MUST AGREE, BYTE FOR BYTE.636
if ! diff -u "$work/native.txt" "$work/wasm.txt" > "$work/conf.diff" 2>&1; then637
cat "$work/conf.diff" >&2638
fail "arm 8: native and WebCrypto revisions DISAGREE. Across clients this surfaces as mysterious lost writes, not as a hash bug."639
fi641
# (b) THE DISCRIMINATION TEST -- without which (a) proves nothing. The naive642
# text-path implementation is a real mistake, not a strawman: reading a file as643
# TEXT and re-encoding is what `FileReader.readAsText` invites, and it is644
# silently lossy for ill-formed UTF-8. The vector MUST notice.645
node test/f2b-revision-webcrypto.js --text "$VECTORS" > "$work/naive.txt" 2>&1 \646
|| fail "arm 8: the naive-implementation arm did not run"647
if diff -q "$work/native.txt" "$work/naive.txt" >/dev/null 2>&1; then648
fail "arm 8: THE VECTOR IS WORTHLESS -- it did not distinguish a known-wrong implementation from the correct one. A vector that has only ever seen agreement has not been shown to detect disagreement."649
fi650
caught=$(diff "$work/native.txt" "$work/naive.txt" | grep '^<' | cut -f1 | sed 's/^< //' | tr '\n' ' ')651
echo " vector caught the naive text-path implementation on: $caught"653
# (c) A THIRD IMPLEMENTATION, sharing no code with either: coreutils.654
if command -v sha256sum >/dev/null 2>&1; then655
( cd "$VECTORS" && for f in *.sgd; do656
printf '%s\tsha256:%s\n' "$f" "$(sha256sum "$f" | cut -d' ' -f1)"657
done ) > "$work/coreutils.txt"658
diff -u "$work/native.txt" "$work/coreutils.txt" > "$work/cu.diff" 2>&1 \659
|| { cat "$work/cu.diff" >&2; fail "arm 8: folio's revisions disagree with coreutils sha256sum"; }660
echo " third implementation (coreutils sha256sum) agrees on all $n vectors"661
else662
echo " NOTE: sha256sum absent, the third-implementation check DID NOT RUN (this is a gap, not a pass)"663
fi664
arm_ok "arm 8 OK: $n vectors, native == WebCrypto == coreutils, and the vector demonstrably catches a wrong implementation"665
fi667
# ---------------------------------------------------------------------------668
# ARM 9 -- THE t-f6e098 READER PATHOLOGY, RE-MEASURED ON THIS TOOLCHAIN669
#670
# CANNOT SKIP. It asserts the state its compiler is actually in and names it.671
# ---------------------------------------------------------------------------672
if should_run 9; then673
arm_begin "arm 9: t-f6e098 on sigil $compiler_version"674
ver_ge () { [ "$(printf '%s\n%s\n' "$2" "$1" | sort -V | head -1)" = "$2" ]; }675
oomfx=test/fixtures/f2a-reader-oom.sgd676
[ -f "$oomfx" ] || setup_failed "the t-f6e098 fixture is missing; the arm has no subject"678
# PRESENCE PROBE FIRST, with a healthy-positive answer: a clean manuscript must679
# parse in this same configuration. Without it, "the pathology fixture came680
# back clean" is equally consistent with nothing parsing at all -- which is681
# exactly what a registry-resolved sigil-doc produces (every parse fails682
# identically at 2:6 with `unbound variable 'make-syntax-reader'`).683
if ! $WORKER test/fixtures/f2a-corpus.sgd > "$work/pres.log" 2>&1; then684
cat "$work/pres.log" >&2; setup_failed "arm 9 presence probe: a CLEAN manuscript did not inspect; no verdict about the pathology is available"685
fi686
grep -q '^PARSED yes' "$work/pres.log" \687
|| { cat "$work/pres.log" >&2; setup_failed "arm 9 presence probe: the clean corpus did not parse; the toolchain is misconfigured (check dev-redirects)"; }689
# This script runs under `set -u` ONLY -- errexit is never enabled. The690
# `set +e` / `set -e` pair that used to bracket this probe therefore did not691
# restore a previous state: it TURNED ERREXIT ON for arm 10 and the summary,692
# where an unprotected `run_sgl` could abort the script before `GATE-F2B RED`693
# was ever printed -- and it did so only when arm 9 happened to be selected, so694
# the gate's behaviour depended on which arms you asked for. Found by an695
# adversarial review. The status is captured directly instead.696
( ulimit -v 1500000; timeout 90 $WORKER "$oomfx" > "$work/oom.log" 2>&1 )697
oom_status=$?698
if ver_ge "$compiler_version" "0.18.5"; then699
[ "$oom_status" = "0" ] || { tail -20 "$work/oom.log" >&2; fail "arm 9: sigil $compiler_version carries the t-f6e098 fix, so the (} shape must yield a clean diagnostic; the probe exited $oom_status"; }700
grep -qE '^DIAGNOSTIC error parse-error ' "$work/oom.log" \701
|| { tail -20 "$work/oom.log" >&2; fail "arm 9: the (} shape did not produce a LOCATED parse diagnostic on a fixed toolchain"; }702
arm_ok "arm 9 OK: sigil $compiler_version >= 0.18.5 -- clean located diagnostic REQUIRED and observed; the documented OOM does NOT reproduce"703
else704
[ "$oom_status" != "0" ] || { tail -20 "$work/oom.log" >&2; fail "arm 9: sigil $compiler_version predates the fix but the (} shape did NOT fail; this arm no longer describes its subject -- re-derive it"; }705
grep -qiE 'out of memory|Killed' "$work/oom.log" \706
|| { tail -20 "$work/oom.log" >&2; fail "arm 9: the (} shape failed on sigil $compiler_version, but not with the memory signature t-f6e098 describes"; }707
echo "arm 9: sigil $compiler_version PREDATES the 0.18.5 fix; the documented OOM is REQUIRED and was observed, contained by the ulimit. THIS IS THE PATHOLOGY, NOT A PASS."708
fi709
fi711
# ---------------------------------------------------------------------------712
# ARM 10 -- DUPLICATE IDS, WITHIN A DOCUMENT AND ACROSS THE WORKSPACE713
# ---------------------------------------------------------------------------714
if should_run 10; then715
arm_begin "arm 10: duplicate ids"716
ensure_scan; ensure_ok_root717
# ACROSS documents: `shared-id` lives in dup-one.sgd and dup-two.sgd. Both must718
# be named; naming one is what a silent winner looks like from outside.719
grep -q 'duplicate ids : 1 id' "$work/scan.out" \720
|| { grep 'duplicate ids' "$work/scan.out" >&2; fail "arm 10: the workspace-level duplicate id was not reported"; }721
grep -q 'shared-id:' "$work/scan.out" || fail "arm 10: the duplicated id is not named"722
grep -q 'dup-one.sgd' "$work/scan.out" || fail "arm 10: the first holder of the duplicated id is not named"723
grep -q 'dup-two.sgd' "$work/scan.out" || fail "arm 10: the second holder of the duplicated id is not named"724
grep -q 'REFUSED, not resolved' "$work/scan.out" || fail "arm 10: the duplicate report does not say it refuses rather than picks"726
# WITHIN one document the inspector already owns this; the service must pass727
# `ambiguous` through rather than re-deriving or flattening it.728
dupdoc="$work/dupdoc"; mkdir -p "$dupdoc/notes"729
printf '#lang sigil/doc\n@note["topics/d" title: "D"]\n\n@task["twice" title: "A" status: ready]{x}\n\n@task["twice" title: "B" status: ready]{y}\n\n@end note\n' > "$dupdoc/notes/d.sgd"730
$WORKER "$dupdoc/notes/d.sgd" > "$work/dupdoc.out" 2>&1731
grep -q '^ID ambiguous twice' "$work/dupdoc.out" \732
|| { cat "$work/dupdoc.out" >&2; fail "arm 10: an id duplicated WITHIN one document did not come back with status 'ambiguous'"; }733
grep -q 'duplicate-node-id' "$work/dupdoc.out" \734
|| fail "arm 10: the within-document duplicate produced no duplicate-node-id diagnostic"736
# POSITIVE CONTROL: a workspace with no duplicates must report none, or737
# "duplicates found" is unfalsifiable.738
# (A line here used to grep `disc2.out` for 'duplicate ids' and swallow the739
# result with `|| true`. `disc2.out` is a discovery CENSUS and never contains740
# that string, so the line could neither pass nor fail -- it read as an741
# assertion and was decoration. Removed rather than repaired; the real742
# zero-duplicates control is the `nodup` probe below, which uses a SERVICE743
# result, where the string can actually appear.)744
cat > "$work/nodup.sgl" <<EOF745
(import (sigil core) (folio doc service) (folio doc bounded))746
(define svc (make-service $WORKER_ARGV default-limits))747
(display (result->report (scan-root svc "$ok_root")))748
EOF749
run_sgl "$work/nodup.sgl" > "$work/nodup.out" 2>&1750
grep -q 'duplicate ids : none' "$work/nodup.out" \751
|| { cat "$work/nodup.out" >&2; fail "arm 10 POSITIVE CONTROL: a clean workspace did not report zero duplicates"; }752
# A DOCUMENT MUST NOT BE ABLE TO FORGE A DUPLICATE AGAINST ANOTHER NOTE.753
#754
# Node ids are document-controlled and the worker serialises them into a755
# line-and-tab manifest. Before the worker escaped them, an id containing a756
# newline split into extra lines that `ids-of` accepted, so ONE note could make757
# an UNRELATED note refuse under D9 -- and could forge a whole-line758
# `F2B_WORKER_DONE` at the same time. Found by an adversarial review.759
forge="$work/forgedup"; mkdir -p "$forge/notes"760
printf '#lang sigil/doc\n@note["topics/atk" title: "A"]\n\n@task["a\\nID\\tunique\\tvictim-task\\nF2B_WORKER_DONE" title: "T" status: ready]{x}\n\n@end note\n' > "$forge/notes/atk.sgd"761
printf '#lang sigil/doc\n@note["topics/victim" title: "V"]\n\n@task["victim-task" title: "V" status: ready]{y}\n\n@end note\n' > "$forge/notes/victim.sgd"762
cat > "$work/forgedup.sgl" <<EOF763
(import (sigil core) (folio doc service) (folio doc bounded))764
(define svc (make-service $WORKER_ARGV default-limits))765
(display (result->report (scan-root svc "$forge")))766
EOF767
run_sgl "$work/forgedup.sgl" > "$work/forgedup.out" 2>&1768
grep -q 'duplicate ids : none' "$work/forgedup.out" \769
|| { cat "$work/forgedup.out" >&2; fail "arm 10: a note forged a duplicate id against an unrelated note by embedding a newline in its own id"; }770
echo " [document cannot forge a duplicate against another note] -> none"772
arm_ok "arm 10 OK: cross-document duplicate refused with both holders named; within-document 'ambiguous' passed through; clean workspace reports none (control); ids cannot be forged"773
fi775
# ---------------------------------------------------------------------------776
# ARM 11 -- THE DEPLOYMENT PATH, EXERCISED WITHOUT BEING TOLD THE ANSWER777
#778
# This arm exists because its absence hid a broken deliverable. `bundled-worker-argv`779
# -- the one function implementing "a deployed folio can find its worker" -- had780
# NO CALLER anywhere in src/ or test/, and the bundled shape below hardcodes an781
# absolute path. So ten sabotage legs passed in two shapes while the deployment782
# claim was false: the harness supplied the answer the product is supposed to783
# compute.784
#785
# `sigil app install` copies the SINGLE bundle into a FLAT content-addressed786
# store (measured on this host: 14,767 entries) and symlinks a launcher at it.787
# Nothing is installed beside it, so any design needing a second file would fail788
# exactly where it matters and pass everywhere it is tested.789
#790
# TWO CHECKS, because neither alone is enough.791
# ---------------------------------------------------------------------------792
if should_run 11; then793
arm_begin "arm 11: the deployment path"795
# (a) SHAPE. `bundled-worker-argv` must produce a SELF-REFERENTIAL invocation --796
# the marker plus the running executable -- not a sibling path. Checked in797
# process, so it fails loudly if anyone reintroduces a sibling lookup.798
cat > "$work/bwa.sgl" <<'EOF'799
(import (sigil core) (sigil string) (sigil list) (folio doc service))800
(let ((argv (bundled-worker-argv)))801
(if (not argv)802
(display "BWA\tFALSE\n")803
(begin804
(display (format "BWA\tparts=~a~%" (length argv)))805
(for-each (lambda (p) (display (format "BWAPART\t~a~%" p))) argv))))806
EOF807
run_sgl "$work/bwa.sgl" > "$work/bwa.out" 2>&1808
if grep -q '^BWA FALSE' "$work/bwa.out"; then809
fail "arm 11(a): bundled-worker-argv returned #f -- it cannot name a worker at all"810
else811
grep -q 'FOLIO_DOC_WORKER' "$work/bwa.out" \812
|| { cat "$work/bwa.out" >&2; fail "arm 11(a): the spawn argv does not carry the worker marker; a sibling lookup would fail in a flat install store"; }813
grep -qE '^BWAPART (/|.*sh$)' "$work/bwa.out" \814
|| { cat "$work/bwa.out" >&2; fail "arm 11(a): the spawn argv does not name an absolute program"; }815
echo " [shape] bundled-worker-argv re-execs the running binary under the marker"816
fi818
# (b) BEHAVIOUR, in the layout that broke it: ONE bundle, hashed name, flat819
# directory, NO sibling, stripped PATH, POSIX sh. This is the case a820
# sibling lookup cannot serve, so it cannot pass by accident.821
store="$work/fakestore"; mkdir -p "$store"822
cp "$repo_dir/build/dev/bin/folio" "$store/deadbeef-folio-0.0.0+test"823
[ "$(ls "$store" | grep -c 'doc-worker')" = "0" ] \824
|| fail "arm 11(b): the simulated store has a worker sibling, so it does not reproduce the deployed layout"825
env -i PATH=/usr/bin:/bin HOME=/tmp sh -c 'FOLIO_DOC_WORKER=1 exec "$0" "$@"' \826
"$store/deadbeef-folio-0.0.0+test" test/fixtures/f2a-corpus.sgd > "$work/fakestore.out" 2>&1827
fs_rc=$?828
grep -q '^PARSED yes' "$work/fakestore.out" \829
|| { head -5 "$work/fakestore.out" >&2; fail "arm 11(b): a lone bundle in a flat store could not act as the worker (exit $fs_rc)"; }830
grep -q '^F2B_WORKER_DONE$' "$work/fakestore.out" \831
|| fail "arm 11(b): the flat-store worker produced no completion marker"832
echo " [behaviour] a lone bundle with NO sibling, stripped PATH, POSIX sh -> full manifest"834
# (c) RESTRAINT. The same binary WITHOUT the marker must still be folio.835
# Without this, "the marker works" is satisfied by a binary that is always836
# the worker.837
env -i PATH=/usr/bin:/bin HOME=/tmp "$store/deadbeef-folio-0.0.0+test" --version > "$work/nomark.out" 2>&1838
grep -q '^folio ' "$work/nomark.out" \839
|| { cat "$work/nomark.out" >&2; fail "arm 11(c) RESTRAINT: without the marker the binary is not behaving as folio"; }840
echo " [restraint] same binary, no marker -> folio"842
# WHAT THIS ARM DOES NOT COVER, stated so nobody reads it as more than it is:843
# the service calling `bundled-worker-argv` from INSIDE a running folio is not844
# exercised end to end, because reaching the scan from the folio binary would845
# need an entry point the subcommand ruling forbids. (a) checks what that call846
# returns and (b) checks that what it returns works in the layout that broke.847
arm_ok "arm 11 OK: spawn argv is self-referential; a lone bundle in a flat store works; without the marker it is still folio"848
fi850
echo "=========================================================================="851
if [ "$fails" -eq 0 ]; then852
echo "GATE-F2B GREEN (sigil $compiler_version, sigil-doc $sigil_doc_sha)"853
exit 0854
else855
echo "GATE-F2B RED: $fails failure(s)"856
exit 1857
fi