Conversation
hub test looked only for runtime/native/{bin}, printed [SKIP] when it was
not there, and returned a pass. Twenty-five of the thirty-two recipes
have no native build, so for most of the catalogue a green test run said
only that the build had produced a file of the expected name. Anything
that linked cleanly and then trapped at run time went unnoticed: that is
how four newick-utils tools reached review, and why kallisto took three
blind CI rounds.
The wasm artifact is now run when there is no native binary. A small
Node harness loads the module, writes the generated inputs into its
filesystem, calls main, and returns stdout, stderr and whatever files
the tool left behind; the Python side writes those into the temporary
directory so the existing output discovery and type checking apply
unchanged. Every recipe is therefore exercised the same way, whichever
runtime it ships.
This needs one build flag. The recipes were compiled with
ENVIRONMENT=web,worker, and such a module refuses to load anywhere else
-- "not compiled for this environment" -- which is the reason wasm tools
could only ever be skipped. Adding node costs about 3 KB of JS shim and
changes nothing for the browser.
A module that cannot run at all now fails rather than passing quietly. A
tool's own non-zero exit is still only reported, matching how the native
path treats it, since some tools exit non-zero by design.
Verified against artifacts built with the hub's own EM_FLAGS: a working
wasm-only tool passes and genuinely runs; a truncated module fails with
the load error; a tool emitting the wrong output type fails on the type
mismatch rather than passing; a native tool behaves exactly as before,
still receiving absolute paths; and a recipe with neither artifact still
skips.
Two faults found by driving the harness with deliberately awkward tools. The exit status was always reported as zero. The code assumed EXIT_RUNTIME=1 makes returning from main throw ExitStatus, so it read the status only from a caught exception. callMain in fact returns main's value and throws nothing for a normal return -- a tool returning 3 was recorded as 0, and the "exited with status" warning could never fire. Both paths are handled now: the return value, and a thrown ExitStatus for a program that calls exit() rather than returning. Files matching an input were not written back, on the reasoning that a tool should not overwrite what it was given. But editing a file in place is a normal pattern, and under the native runtime the tool writes straight into the working directory, so the two runtimes disagreed about what had been produced. Everything except the harness's own spec file is written back now. Also corrected the message for exit -1: it means the module did not complete, which covers a trap or an abort part way through, not only a failure to load. Verified against tools built with the hub's EM_FLAGS that: return 3 is reported as 3 and warns; a file edited in place survives; an abort after a partial write still fails; binary content with NUL and 0xFF bytes round-trips unchanged; and a tool writing a file named like the harness's spec cannot corrupt the result. Rebuilding and retesting the real edlib recipe still passes.
Review of the previous commits found three things that would have made this change worse than useless. hub test could not fail. test_cmd printed the list of failing tools and returned, so the command exited 0 and CI passed regardless. Everything these commits add was log text. It now exits non-zero. Nineteen recipes would have gone from skipped to failing, with a diagnosis that blamed the recipe. Only the emscripten builder gained node in ENVIRONMENT; the other nineteen are built by the biowasm container, whose flags live in its own bin/shared.sh and were out of reach. Such a module refuses to start outside a browser, which says nothing about whether the tool works. Two changes: the harness reports a refusal of that kind distinctly, and the test skips it with a plain reason instead of failing; and the biowasm image is patched at build time to add node, so these tools become testable once the image is rebuilt. Passing the wasm bytes to the module also removes its need to fetch them, which it cannot do here. The -1 sentinel collided with a real exit status. Returning -1 is an ordinary way for a tool to reject its arguments, and it was indistinguishable from a trap, so the output was discarded and the tool reported as broken. Loading and completion are separate fields now, and an exit status is only ever an exit status. Also: a tool writing non-UTF-8 to stderr raised UnicodeEncodeError out of print(), which escaped and left every remaining recipe untested; that text is now made printable, and each tool is tested inside its own handler so one cannot end the run. Verified: a browser-only module skips and does not fail; a genuinely broken module still fails with a real diagnostic; a tool returning -1 keeps its output and is not mistaken for a trap; non-UTF-8 stderr no longer aborts the run; hub test exits 1 on failure and 0 on a skip; and rebuilding and retesting the real edlib recipe still passes.
The previous commit added node to biowasm's ENVIRONMENT with a sed against bin/shared.sh in the builder image. That file is being rewritten on sbom-implementation, which pins the biowasm commit by SHA and verifies it, pins the base image by digest, records both in a lockfile, and drops privileges in the container. Patching the same file here would conflict with that for no good reason, and the version there is the better base for the change. Nothing else in this branch depended on it. Recipes built by the biowasm container still refuse to start outside a browser, and the test still reports that as a skip with a plain reason rather than a failure, which is the behaviour that matters for turning this on safely. Adding node belongs wherever that Dockerfile settles.
This was referenced Aug 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
hub testlooks only forruntime/native/{bin}, prints[SKIP]when it isn't there, and returns a pass.25 of the 32 recipes have no native build. So for 78% of the catalogue, a green test run says only that the build produced a file of the expected name. Anything that links cleanly and then traps at run time goes unnoticed — that is how four newick-utils tools reached review, and why kallisto took three blind CI rounds.
What this does
Runs the wasm artifact when there is no native binary. A small Node harness (
hub/tests/run_wasm.js) loads the module, writes the generated inputs into its filesystem, callsmain, and returns stdout, stderr and whatever files the tool left behind. The Python side writes those into the temporary directory, so the existing output discovery and type checking apply unchanged — every recipe is exercised the same way, whichever runtime it ships.The harness returns the whole working directory rather than a named list, because the test never tells a tool where to write: it looks afterwards for a file named after the output. The same discovery has to work for both runtimes.
One build flag is required
The recipes are compiled with
ENVIRONMENT=web,worker, and such a module refuses to load anywhere else:That is the actual reason wasm tools could only ever be skipped. Adding
node:ENVIRONMENT=web,worker(current)ENVIRONMENT=web,worker,nodeIt costs ~3 KB of JS shim (168,381 → 171,410 bytes on a test tool) and changes nothing for the browser targets.
A module that cannot run now fails
Previously an aborting module still produced
VERDICT: PASS. A load failure or abort is now a test failure. A tool's own non-zero exit is still only reported, matching how the native path treats it, since some tools exit non-zero by design.Verification
Against artifacts built with the hub's own
EM_FLAGS, using emsdk 4.0.18 (the version the recipes pin):Testing tool democopy (wasm)).wasmDetected: UNKNOWN, Expected: ['FASTA']The third case is the one that matters: it proves the type checking has teeth through the new path rather than passing vacuously.
Run against real recipes
Built with the hub's own pipeline (
validate→build→test) on emsdk 4.0.18, for the five wasm-only recipes that use the emscripten strategy — every one of them previously[SKIP]ped:edlibabpoacgrangesfermi-lite[WARNING] Empty outputtn93[WARNING] Output file not foundSo five real tools now execute where nothing did before, and none of them regress CI.
Two things this exposes, which are worth knowing before merging:
The pass criterion is still weak.
fermi-liteandtn93run and produce nothing usable, and still pass, because empty or missing output is only a[WARNING]. That is pre-existing behaviour and I have not changed it here — the cause is most likely that the generated example input is not meaningful for those tools rather than the tools being broken. Making it fail would need better per-type fixtures first, otherwise it would just turn recipes red without telling anyone anything useful.Where a native build exists, the wasm artifact is still never tested. Native takes precedence, which preserves existing behaviour, but the wasm build is what the SPA actually runs. That affects
ksw2,lastz,prodigalandsamblaster. Arguably both should run; I have kept the change minimal.Not covered here: the 22 biowasm/auto recipes, which carry 167 of the 176 operations and need Docker to build. Their behaviour under this change is unknown until CI runs it, and that is the real blast radius — a 9-operation sample cannot speak for 167.
Review found three things that would have made this harmful
Worth reading before the rest, because they change what the PR is:
1.
hub testcould not fail.test_cmdprinted the failing tools and returned, so the command exited 0 and CI passed regardless. Everything here was log text. It now exits non-zero — verified: exit 1 on a failing tool, 0 on a skip.2. Nineteen recipes would have flipped from skipped to failing, blaming the recipe. Only the emscripten builder gained
node; the other nineteen are built by the biowasm container, whose flags live in its ownbin/shared.shand were out of reach of the original change. Those modules abort withnot compiled for this environment— which says nothing about whether the tool works.Two changes: the harness reports that refusal distinctly, and the test skips it with a plain reason rather than failing; and the biowasm image is patched at build time to add
node, so those tools become testable once the image is rebuilt.3.
-1was both a sentinel and a real exit status.return -1is an ordinary way for a tool to reject its arguments, and it was indistinguishable from a trap — so the tool's output was discarded and it was reported broken. Loading and completion are separate fields now.Also fixed: non-UTF-8 stderr raised
UnicodeEncodeErrorout ofprint(), which escaped and left every remaining recipe untested; and each tool is now tested inside its own handler so one cannot end the run..wasm-1hub testwith a failing tooledlib, rebuiltHonest limits
git clone ... .with no ref), so what it builds depends on when the image was made. Not addressed here.[WARNING] Empty outputand a missing output file do not fail, and no output flag is ever passed to a tool. So for a large share of operations the check still cannot go red. Strengthening it needs better per-type fixtures and belongs in its own change.Review notes
master.nodeflag. This matters only ifhub testis ever run against a registry pull rather than a freshhub build; in CI the build immediately precedes the test, so the artifacts carry the flag.nodebecomes a requirement forhub test. It is already present on the runners, and the failure mode is an explicit message rather than a crash.Merge order
sbom-implementationis the priority branch and rewrites parts of the builders. This PR conflicts with it onhub/builders/emscripten.py. Rebase after it lands. The resolution is one line —-s ENVIRONMENT=web,workerbecomes-s ENVIRONMENT=web,worker,node;EM_FLAGSis otherwise unchanged there.Independent of the R work (#27, #28, #29) and of #25/#26.
Interaction with #36
Checked against
sbom-implementationas it stands on 2026-08-10, since #36 is now open.hub/tests/, so nothing here overlaps it.EM_FLAGSstill reads-s ENVIRONMENT=web,worker, so the one-line resolution described above still applies.test_cmd, so theSystemExit(1)added here is still needed and does not conflict — both branches edithub/hub.pyin different places and merge cleanly.Worth knowing while both are in flight:
hub testappears invalidate-recipes.ymlbut not inpublish-recipes.yml, which runsvalidate → build → sbom → sbom-check → publish → sign-attest. So on the publishing path the tools are never executed, and on the PR path the command currently exits 0 whatever happens. Until this PR lands, a bundle can be signed and attested without any tool in it having been run successfully. That is an observation about how the two fit together, not a claim about #36.