test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs

The asyncify single-slot work, executed red-green (full ledger:
docs/features/asyncify-arbiter/redgreen.md; decisions record:
docs/features/async/07-decisions-and-outcome.md):

- tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated
  playwright config: 8 scenarios reproducing the KiCad asyncify failure
  family with the kicad-faithful startup topology (pre-park fiber swap →
  park throw through the live trampoline). Built in 3 variants; the
  SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds
  keep the historical hang and index-out-of-bounds crash reproducible
  forever (mutation-style pins for the existing shims).
- scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel
  in the wakeUp path — when main's last pre-park suspension was a sleep,
  the main-loop park throw escaped through that sleep's promise reaction
  as an uncaught rejection (the calculator/gerbview console errors).
- scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs.
- Spec tightening (the acceptance bar): 'uncaught exception: unwind'
  tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard
  clean-console gate over 5 asyncify corruption signatures.
- wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error,
  sync clipboard IsSupported (014f67e6c1).

Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40
passed / 2 skipped with ZERO corruption signatures in any log across all
six apps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Viktor Vaczi 2026-06-12 16:59:07 +02:00
commit 14ca16cbd3
16 changed files with 1593 additions and 13 deletions

View file

@ -122,7 +122,11 @@ echo "Total: Fixed $TOTAL_FIXED empty callback(s)"
# --- 3. Nested-Asyncify handleSleep fix ---------------------------------------
# Injected after Emscripten's fiber glue (the _emscripten_fiber_swap.isAsync marker).
if grep -q '__nestedHandleSleepInstalled' "$JS_FILE"; then
# SHIM_DISABLE_HANDLESLEEP=1 skips it: used by the asyncify-races red-green harness
# to keep the historical "sleep buffer clobbered by fiber swap" crash reproducible.
if [ "${SHIM_DISABLE_HANDLESLEEP:-0}" = "1" ]; then
echo "handleSleep fix DISABLED (SHIM_DISABLE_HANDLESLEEP=1) - ablation build"
elif grep -q '__nestedHandleSleepInstalled' "$JS_FILE"; then
echo "handleSleep fix already present - skipping"
else
HS_MARKER=$(grep -n '^_emscripten_fiber_swap\.isAsync = true;$' "$JS_FILE" | head -1 | cut -d: -f1)
@ -163,7 +167,12 @@ fi
# so every later fiber swap silently fails to switch — the schematic load and all
# post-idle tool actions hang. Wrap the loop in try/finally so the flag is always
# reset (self-healing).
if grep -qF '} finally { Fibers.trampolineRunning = false; }' "$JS_FILE"; then
# SHIM_DISABLE_TRAMPOLINE_HEAL=1 skips it: used by the asyncify-races red-green
# harness to keep the historical "park throw wedges the trampoline guard" hang
# reproducible.
if [ "${SHIM_DISABLE_TRAMPOLINE_HEAL:-0}" = "1" ]; then
echo "fiber trampoline self-heal DISABLED (SHIM_DISABLE_TRAMPOLINE_HEAL=1) - ablation build"
elif grep -qF '} finally { Fibers.trampolineRunning = false; }' "$JS_FILE"; then
echo "fiber trampoline self-heal already present - skipping"
elif grep -qF 'Fibers.trampolineRunning = true;' "$JS_FILE"; then
perl -0pi -e 's/(Fibers\.trampolineRunning = true;)(\s*)(do \{.*?\} while \(Fibers\.nextFiber\);)(\s*)(Fibers\.trampolineRunning = false;)/$1$2try {$3} finally { $5 }/s' "$JS_FILE"

View file

@ -52,7 +52,20 @@ if (typeof Asyncify !== "undefined") {
Asyncify.currData = sleepCtx.capturedData;
}
cleanup();
return wakeUp(result);
try {
return wakeUp(result);
} catch (e) {
// emscripten_set_main_loop(...,1) parks main() by throwing the
// "unwind" sentinel. When main's LAST pre-park suspension was a
// sleep, main is resumed from THIS wakeUp, so the sentinel
// propagates here instead of into callMain's catch — surfacing as
// an uncaught "unwind" promise rejection. Swallow it exactly like
// callMain/handleException do on the direct path.
if (e === "unwind") {
return;
}
throw e;
}
});
});
} catch (e) {