Files
hanzo-dev 2f4fc01e0c scripts: the demo pipeline can rebuild what it shipped, and says what it built
Three native templates were live only because someone built them by hand, and
the pipeline that is supposed to own them would have broken all three on its
next run. Each failure hides behind a 200, which is why none of them showed up
as a failure.

framework() — the artifact's own filenames name it, so prep.py reports unity /
godot / unreal / static and deploy-templates.sh stops hardcoding "static".
That string is what clients/projects/sites.go turns into COOP/COEP: a Unity or
Godot export served without it loses SharedArrayBuffer and its multithreaded
wasm hangs forever on a page that answers 200. The deploy also PATCHes the
framework, because create is a no-op after the first run and every project born
before this keeps a stale one.

roots() — a page whose own same-origin scripts are not on disk cannot run,
whatever built it. Flutter's SOURCE web/index.html loads flutter_bootstrap.js,
a file that exists only in build/web, and web/ sits SHALLOWER so it outranked
the real build: the next batch would have packed a blank page over six working
Flutter demos. This is the general form of the %PUBLIC_URL% rule already here,
not a third framework special case.

build() — expo joins the static-build set. Its absence is the whole reason
android-expo-nativewind has no demo: prep.py returned before building, roots()
found nothing, and the slug 404s while its hand-built siblings are live. Flutter
gets the lane it needs too, gated on an SDK that Google publishes for Linux x64
only, so on arm64 it skips and the batch logs NO-SITE instead of shipping the
scaffold.

prep_test.py pins all of it — 12 checks, including that a wasm app which is not
a game stays un-isolated and that a CDN script tag is not ours to resolve.
__pycache__ stops being tracked; a committed .pyc conflicts on every run.

Co-authored-by: Hanzo Dev <dev@hanzo.ai>
2026-07-28 09:28:47 -07:00

76 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""python3 scripts/prep_test.py — pin the two prep.py decisions that decide
whether a demo is alive, both of which fail SILENTLY behind a 200.
framework() names what the artifact is, and that drives COOP/COEP in
clients/projects/sites.go. Call a Unity export "static" and its
multithreaded wasm hangs forever on a page that answers 200.
roots() picks the directory to serve. Pick a framework's SOURCE
index.html over its build output and every visitor gets a blank
page — the failure that shipped an un-built create-react-app
shell, and the one Flutter's web/ scaffold would repeat.
"""
import importlib.util, os, pathlib, sys, tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
spec = importlib.util.spec_from_file_location("prep", os.path.join(HERE, "prep.py"))
prep = importlib.util.module_from_spec(spec)
spec.loader.exec_module(prep)
fails = []
def is_(name, got, want):
if got != want:
fails.append(name)
print("%s %-38s %s" % ("ok " if got == want else "FAIL", name, got))
def tree(files):
d = tempfile.mkdtemp()
for f, c in files.items():
p = pathlib.Path(d, f)
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(c)
return d
# framework(): the closed set crossOriginIsolated() understands, and nothing else
is_("unity export", prep.framework(tree({
"index.html": "", "Build/x.loader.js": "", "Build/x.wasm": "", "Build/x.data": ""})), "unity")
is_("godot export", prep.framework(tree({
"index.html": "", "orb.pck": "", "orb.wasm": "", "orb.js": ""})), "godot")
is_("unreal export", prep.framework(tree({
"index.html": "", "g.utoc": "", "g.wasm": ""})), "unreal")
is_("plain site", prep.framework(tree({"index.html": "", "app.js": ""})), "static")
# A page may ship wasm without being a game (sqlite, ffmpeg): isolating it would
# break the third-party embeds it is allowed to make, so the marker is required.
is_("wasm that is not a game", prep.framework(tree({
"index.html": "", "sqlite.wasm": "", "app.js": ""})), "static")
is_("pck without wasm", prep.framework(tree({"index.html": "", "data.pck": ""})), "static")
# roots(): a page whose own same-origin scripts are not on disk cannot run
FLUTTER = ('<!DOCTYPE html><html><head><base href="/"></head><body>'
'<script src="flutter_bootstrap.js" async></script></body></html>')
d = tree({"web/index.html": FLUTTER, "web/manifest.json": "{}",
"build/web/index.html": FLUTTER, "build/web/flutter_bootstrap.js": "//",
"build/web/main.dart.js": "//"})
is_("flutter build beats its scaffold", os.path.relpath(prep.roots(d)[0], d), "build/web")
d = tree({"index.html": '<script src="/app.js"></script>', "app.js": "//"})
is_("built site is a root", [os.path.relpath(x, d) for x in prep.roots(d)], ["."])
d = tree({"index.html": '<script src="https://a.hanzo.ai/analytics.js"></script>'})
is_("CDN script is not ours to resolve", [os.path.relpath(x, d) for x in prep.roots(d)], ["."])
d = tree({"index.html": "<h1>hi</h1>"})
is_("script-free page is a root", [os.path.relpath(x, d) for x in prep.roots(d)], ["."])
is_("missing script disqualifies", prep.roots(tree({"index.html": '<script src="main.js"></script>'})), [])
# The Flutter lane needs an x64 SDK (Google ships no Linux arm64 build). Where
# there is none it must skip, not half-build: roots() then reports NO-SITE.
d = tree({"pubspec.yaml": "name: x\n"})
os.environ["PATH"] = ""
prep.build(d)
is_("flutter lane skips without an SDK", os.path.isdir(os.path.join(d, "build")), False)
print("\n%s (%d checks)" % ("FAILED: " + ", ".join(fails) if fails else "ALL PASS", 12))
sys.exit(1 if fails else 0)