test(ci): drive the classifier over the real traces

Four cases on real data: each leg's real verdict, plus the android trace
cut before it reached the transaction screen, which is what proves the
route gate reads a real hierarchy dump.

Also corrects the hand-written fixtures. They set is_error to false on a
plain violation; internal/trace/writer.go tags that field omitempty, so a
real trace omits it entirely. Harmless to the classifier, but a fixture
that does not look like reality is the thing that hides drift.
This commit is contained in:
pj committed 2026-08-16 01:40:25 +05:30
1 parent 1e2cfc78b8
commit 6ab719fb77
1 file changed
+50 -3
+50 -3
View File
@@ -13,6 +13,7 @@ set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
script="$here/folio-run.sh" script="$here/folio-run.sh"
spec="$here/../../examples/folio/sanderling/spec.ts" spec="$here/../../examples/folio/sanderling/spec.ts"
testdata="$here/testdata"
work="$(mktemp -d)" work="$(mktemp -d)"
trap 'rm -rf "$work"' EXIT trap 'rm -rf "$work"' EXIT
@@ -25,6 +26,9 @@ run() { # <case> <platform> <stub exit code> [none] ; trace lines on stdin
local name="$1" platform="$2" code="$3" trace_mode="${4:-file}" local name="$1" platform="$2" code="$3" trace_mode="${4:-file}"
case_dir="$work/$name" case_dir="$work/$name"
mkdir -p "$case_dir" mkdir -p "$case_dir"
# the web leg serves this directory and waits for index.html before it runs
mkdir -p "$case_dir/examples/folio/app/webApp/build/dist/wasmJs/developmentExecutable"
echo ok > "$case_dir/examples/folio/app/webApp/build/dist/wasmJs/developmentExecutable/index.html"
cat > "$case_dir/trace.jsonl" cat > "$case_dir/trace.jsonl"
printf '#!/usr/bin/env bash\n' > "$case_dir/sanderling" printf '#!/usr/bin/env bash\n' > "$case_dir/sanderling"
cat >> "$case_dir/sanderling" <<'STUB' cat >> "$case_dir/sanderling" <<'STUB'
@@ -46,7 +50,7 @@ STUB
STUB_ARGV="$case_dir/argv" STUB_TRACE="$case_dir/trace.jsonl" \ STUB_ARGV="$case_dir/argv" STUB_TRACE="$case_dir/trace.jsonl" \
STUB_TRACE_MODE="$trace_mode" STUB_CODE="$code" \ STUB_TRACE_MODE="$trace_mode" STUB_CODE="$code" \
SANDERLING="$case_dir/sanderling" SPEC="${spec_override:-$spec}" \ SANDERLING="$case_dir/sanderling" SPEC="${spec_override:-$spec}" \
GITHUB_STEP_SUMMARY="$case_dir/summary.md" \ GITHUB_STEP_SUMMARY="$case_dir/summary.md" PORT=8796 \
SEED=7 MAX_STEPS=240 DURATION=20m \ SEED=7 MAX_STEPS=240 DURATION=20m \
bash -eo pipefail -c "'$script' '$platform'" \ bash -eo pipefail -c "'$script' '$platform'" \
> "$case_dir/out" 2> "$case_dir/err" || status=$? > "$case_dir/out" 2> "$case_dir/err" || status=$?
@@ -80,8 +84,11 @@ expect_argv() { # <text> <case>
grep -qxF -- "$1" "$case_dir/argv" || fail "$2: '$1' never reached the binary" grep -qxF -- "$1" "$case_dir/argv" || fail "$2: '$1' never reached the binary"
} }
convicting='{"violations":["submitCommitsOneTransactionPerAction"],"witnesses":{"submitCommitsOneTransactionPerAction":{"is_error":false,"reason":"one tap, two rows"}}}' # is_error is absent, not false: internal/trace/writer.go tags it omitempty, so a
unrelated='{"violations":["newAccountBalanceIsZero"],"witnesses":{"newAccountBalanceIsZero":{"is_error":false,"reason":"opened at 4.00"}}}' # predicate that simply returned false never writes the key. The real traces
# pinned under testdata/ carry exactly this shape.
convicting='{"violations":["submitCommitsOneTransactionPerAction"],"witnesses":{"submitCommitsOneTransactionPerAction":{"reason":"predicate false"}}}'
unrelated='{"violations":["newAccountBalanceIsZero"],"witnesses":{"newAccountBalanceIsZero":{"reason":"predicate false"}}}'
threw='{"violations":["submitMovesBalanceByAtMostTypedAmount"],"witnesses":{"submitMovesBalanceByAtMostTypedAmount":{"is_error":true,"reason":"TypeError: cannot read text of undefined"}}}' threw='{"violations":["submitMovesBalanceByAtMostTypedAmount"],"witnesses":{"submitMovesBalanceByAtMostTypedAmount":{"is_error":true,"reason":"TypeError: cannot read text of undefined"}}}'
on_txn='{"route":"AddTransactionScreen","violations":[]}' on_txn='{"route":"AddTransactionScreen","violations":[]}'
off_txn='{"route":"HomeScreen","violations":[]}' off_txn='{"route":"HomeScreen","violations":[]}'
@@ -278,6 +285,46 @@ TRACE
expect_status 0 gate-matches-spec expect_status 0 gate-matches-spec
expect_silent "no longer declares" gate-matches-spec expect_silent "no longer declares" gate-matches-spec
# --- real traces, from actions run 31902501859 on this branch's code ---------
# The cases above are hand-written, so they only prove the classifier agrees
# with what this file assumes a trace looks like. These are what the binary
# actually wrote. Every step is kept; of each step only step, violations,
# witnesses and residuals survive, and hierarchy is replaced by the "...Screen"
# resource ids it held, in order, because that dump is 95% of the bytes and the
# only place the route gate's grep can match. Running the classifier over the
# originals and over these produced byte-identical verdicts.
run ios-real ios 2 file < "$testdata/folio-ios-convicted.jsonl"
expect_status 0 ios-real
expect_says "folio/ios: found the submit bug in 49 steps (submitCommitsOneTransactionPerAction)" ios-real
expect_summary "- 49 steps recorded, exit 2" ios-real
# a real conviction carries no is_error, so reading it as a thrown predicate is
# the mistake this asserts against
expect_silent "a predicate threw" ios-real
# Both gated properties fired on the same step, and both are named.
run web-real web 2 file < "$testdata/folio-web-convicted.jsonl"
expect_status 0 web-real
expect_says "folio/web: found the submit bug in 184 steps (submitCommitsOneTransactionPerAction, submitMovesBalanceByAtMostTypedAmount)" web-real
expect_summary "- convicted on: submitCommitsOneTransactionPerAction, submitMovesBalanceByAtMostTypedAmount" web-real
# 200 steps, no violation, and it reached the transaction screen: the health
# gate's passing case on real data.
run android-real android 0 file < "$testdata/folio-android-healthy.jsonl"
expect_status 0 android-real
expect_says "folio/android: healthy run over 200 steps, reached the transaction screen" android-real
expect_silent "never reached" android-real
# The same real trace cut to before it first reached the transaction screen.
# The route gate reads the accessibility hierarchy, so this is the one case that
# proves it reads real hierarchy dumps and not just the shape assumed above.
head -10 "$testdata/folio-android-healthy.jsonl" > "$work/android-early.jsonl"
run android-real-stalled android 0 file < "$work/android-early.jsonl"
expect_status 1 android-real-stalled
expect_says "never reached AddTransactionScreen over 10 steps" android-real-stalled
expect_says "routes the trace does record: LoginScreen,HomeScreen,AddAccountScreen,LedgerScreen" android-real-stalled
if [ "$failed" = 0 ]; then if [ "$failed" = 0 ]; then
echo "folio-run.sh: ok" echo "folio-run.sh: ok"
fi fi