You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The required Unit + integration (node --test) CI job fetches from the live jspm CDN (api.jspm.io / ga.jspm.io), so a jspm outage or rate-limit reds CI on a pull request that has nothing to do with vendoring.
Proven on PR #1149 (a documentation-only change touching five prose files): the job failed on a blanket .webjs/ ignore is healed so the pin is committable in test/vendor-cli/vendor-cli.test.mjs:176, asserting assert.equal(code, 0) at L187 when webjs vendor pin exited 1. A re-run of the IDENTICAL commit (91205d71, no code change) then passed all ten checks. During the same window, a local webjs boot logged could not vendor 2 packages via jspm (status 401), and a direct curl to ga.jspm.io returned 200 again shortly after, so the outage was transient and external.
This is a recurrence of the class that #312 already established (De-flake the jspm install-order vendor test (live-CDN dependency), closed 2026-06-03, which reported the vendor tests flaking roughly 1 run in 4 across 18 PRs). #312 fixed ONE instance, in packages/server/test/vendor/vendor.test.js. The root cause, a required CI job depending on a third-party CDN's availability, survives. The cost is real: a false red blocks a merge, and because the project gates merges on green CI, it either burns an investigation (it burned one on #1149) or trains people to wave red checks through.
The surface is wider than the one failing test. Three third-party hosts are reached from the required job: api.jspm.io and ga.jspm.io (16 gated tests in packages/server/test/vendor/vendor.test.js, 5 gated in test/vendor-cli/vendor-cli.test.mjs), plus registry.npmjs.org from two tests that were never gated at all (vendor.test.js:1904 and :1930, via findOutdated, up to 10s each on an outage).
Implementation plan
Scope note. Almost all of this is test and CI infrastructure. The one exception is two small packages/server/src/vendor.js robustness fixes (a missing timeout and a missing cache reset), described at the end and carried in their own commit; nothing else in shipped source moves.
The core decision
Split the surface in two by FILENAME, and let the test runners enforce the split.
*.live.test.{js,mjs} is the only shape allowed to touch a third-party host. Both runners exclude those files unless WEBJS_REQUIRE_NETWORK=1. A nightly non-required workflow sets that variable.
Everything else is offline, driven by a shared jspm double.
The current NETWORK_OK / WEBJS_SKIP_NETWORK_TESTS gate is deleted entirely. It is the wrong shape: it is opt-OUT (so CI, which never sets it, always runs live), it is enforced by convention only, and it cannot express "this file is live" to the runner.
Why "keep the one live test gated in place" is not enough. After #1219 that test skips on a throw, a 5xx, or a 429, but still reds on a 4xx (packages/server/test/vendor/vendor.test.js:744), because the ground truth having just succeeded means a 4xx implies our own request is malformed. A WAF 403, a 404 on a moved route, or a rate limit expressed as a 403 therefore still reds an unrelated PR, which is the exact failure #1149 hit. Leaving it in the required job would leave the problem unfixed.
.github/workflows/ci.yml is not edited. Its on: block is shared by eleven jobs and it is concurrently edited by #1135 and #1257; the runner-level filter achieves the same thing with no rebase conflict.
A fixture file of recorded jspm responses with a periodic refresh. A refresh job nobody runs rots into a fixture asserting a graph jspm stopped producing, which is fix(server): stop pinning a yanked version in the vendor CDN test #1219's exact failure (@codemirror/lint@6.9.6 was yanked and redded main for every PR). A generated double has nothing to go stale.
Making vendor pin treat a CDN failure as non-fatal in SHIPPED code. Pin failure is a real user-facing outcome (pinAll: refuses to write empty pin file when every install fails, L1115), and weakening the runtime to calm a test inverts the direction.
Files
New:
File
Purpose
test/fixtures/install-spec.mjs
splitInstall / packageName / subpath, lifted verbatim from test/e2e/fixtures/stub-jspm.mjs:98-130. Scope-aware and subpath-aware. No side effects.
test/fixtures/jspm-double.mjs
jspmDouble(opts) returning a fetch-compatible fn, plus withJspmDouble(opts, body). No import side effects.
test/fixtures/jspm-double-preload.mjs
Installs the double on globalThis.fetch at module load. The spawned-child arm.
packages/server/test/vendor/jspm-cdn.live.test.js
The live jspm contract tests, moved out of vendor.test.js.
test/vendor-cli/vendor-pin.live.test.mjs
One live webjs vendor pin run (no preload), so the real user command keeps live coverage.
test/repo-health/jspm-double.test.mjs
The double's own contract.
test/repo-health/live-cdn-callers.test.mjs
Enforces the filename policy.
.github/workflows/vendor-cdn.yml
Nightly, non-required, runs the live files with WEBJS_REQUIRE_NETWORK=1.
Modified:
test/e2e/fixtures/stub-jspm.mjs: import the three install-spec helpers from the new shared module and re-export them, so test/repo-health/e2e-vendor-stub.test.mjs keeps passing verbatim. No behaviour change.
test/vendor-cli/vendor-cli.test.mjs: preload wiring in runCli (L24), delete NETWORK_OK (L22) and all five gates (L65, L78, L85, L110, L165), fix the header (L7-8).
packages/server/test/vendor/vendor.test.js: delete NETWORK_OK (L404) and all sixteen gates, convert thirteen tests to the double, move the live test out, fix the section comment (L399-403).
scripts/run-node-tests.js: add the .live.test. filter beside the existing browserSeg and e2eSeg filters at L57-63.
scripts/run-bun-tests.js: the same filter; drop ${SEP}vendor${SEP} from excludeSegs (L88) and fix both comments that call the vendor suite network-bound (L7 and L83-87).
Model api.jspm.io faithfully, because vendor.js's whole fallback ladder is built on its exact failure semantics (packages/server/src/vendor.js:383, :418-448, :548-626).
Each install maps to https://ga.jspm.io/npm:<name>@<version><subpath>/double.js, keyed on <name><subpath>. Derive name, version, and subpath with test/fixtures/install-spec.mjs, never with the i.replace(/@[^@]*$/, '') shortcut the existing inline mocks use (vendor.test.js:588). That regex collapses dayjs@1.11.13/plugin/utc to dayjs, which would make partsByInstall.get(spec) miss, derivePinParts recover subpath: '', and pinAll report a spurious failure. The <name>@<version> substring must survive verbatim, since derivePinParts (vendor.js:702-704) parses the version straight out of the URL.
Any install listed in opts.unresolvable fails the whole batch with 401 { error: 'Error: Not Found' }. That is real jspm behaviour (vendor.js:383) and it is what drives the per-package probe path.
opts.transitives folds extra {spec: url} entries into the answer, so the flattened transitive pin path (vendor.js:1276-1291) becomes testable for the first time.
opts.status forces a transient code (5xx or 429) for the transient-fallback tests.
A GET of a URL the double minted answers bytes plus content-type: text/javascript, so downloadBundle (vendor.js:1079) and fetchIntegrity (vendor.js:1111) complete.
The /double.js tail is load-bearing: real jspm never emits it, so a wiring assertion can key on it.
Refusal must be loud, and a throw is not loud enough. Every caller in vendor.js swallows a throw (vendor.js:441, :1096, :1122, :2213), so a broken preload would silently degrade to {} and a weak assertion would still pass. Instead:
The double records every unexpected request on double.unexpected, and withJspmDouble asserts that array is empty in its finally.
The preload prints [jspm-double] armed to stderr at load, prints each refusal, and sets process.exitCode = 1, so a refused CLI child fails runCli's assert.equal(code, 0).
registry.npmjs.org is in the refusal set too.
This is a deliberate divergence from test/e2e/fixtures/stub-jspm.mjs:151-160, which passes through to the real network on refusal. Do not merge the two fixtures: that one must emit a real executable module for a browser, this one only needs jspm-shaped URLs and bytes.
Steps
test/fixtures/install-spec.mjs, lifted from stub-jspm.mjs; make stub-jspm.mjs import and re-export from it. Confirm test/repo-health/e2e-vendor-stub.test.mjs still passes unchanged.
test/fixtures/jspm-double.mjs per the section above, plus test/fixtures/jspm-double-preload.mjs reading its config from a WEBJS_JSPM_DOUBLE JSON env var (absent means resolve everything).
Wire the CLI test. In test/vendor-cli/vendor-cli.test.mjs, runCli (L24) prepends the preload flag. The flag is runtime-specific and neither runtime honours the other's, the test(e2e): stop the elision-off block depending on a live CDN #1229 lesson at test/e2e/e2e.test.mjs:85-89. Here the parent runtime IS the child runtime, since runCli spawns process.execPath and under bun test that is bun:
constPRELOAD=resolve(__dirname,'..','fixtures','jspm-double-preload.mjs');statSync(PRELOAD);// a rename must fail loudly, not silently go liveconstpreload=process.versions.bun
? ['--preload',PRELOAD]
: ['--import',pathToFileURL(PRELOAD).href];
The file:// form matters because runCli spawns with cwd set to the temp app dir, so a relative --import would resolve against the wrong directory. Then delete NETWORK_OK and every gate, and rewrite the header comment.
Prove the wiring for every call site, not one. Assert stderr matches /\[jspm-double\] armed/ inside runCli itself, so all seven call sites are covered by one line. Additionally assert in the L65 test that parsed.imports.picocolors ends with /double.js. The existing /^https:\/\/ga\.jspm\.io\/npm:picocolors@/ match (L75) is true against real jspm too, so it cannot detect the preload being dropped. Use match, never equality, on stderr elsewhere.
Convert the in-process tests in packages/server/test/vendor/vendor.test.js: L412, 420, 556, 1004, 1039, 1115, 1147, 1194, 1213, 1228, 1243, 1701, 1718. Every existing assertion about URL shape, bytes, integrity, prune, and pin-file removal survives verbatim, which is the point of a double that mints jspm-shaped URLs. Three need care:
L1115 (refuses to write empty pin file when every install fails) keeps asserting result.failed and a null pin file, driven by opts.unresolvable. Never relax it.
L1147 (partial success) traces batch-401, then two probes, then one survivor, at which point jspmGenerate short-circuits to a cachedjspmResolveOne (vendor.js:611), so the total is three round trips, not four. Only assert a count if you pin it to that trace.
L420 (hits in-process cache) today only deepEquals two results, which passes even if a second round trip fired. With the double it can count calls and assert exactly one, so the conversion makes it discriminating for the first time.
Add one assertion using opts.transitives that a flattened transitive lands in file.imports with a bundleFilenameWithSubpath-shaped bundle name. withJspmDouble calls clearVendorCache() on entry and exit.
Drop two gates that were never needed. L406 short-circuits before any fetch (vendor.js:549); L1950 already installs its own throwing fetch for its whole duration. Remove the gates, keep the bodies byte-identical.
Mock two ungated live calls the old gate missed. The updatePinned provider tests at vendor.test.js:1904 and :1930 reach registry.npmjs.org through findOutdated (vendor.js:1499), bounded by no gate and costing up to 10s each on an outage. Both assert only result.provider, so wrap them in the existing withMockedFetch (L466) answering a 404.
Move the live tests out. Create packages/server/test/vendor/jspm-cdn.live.test.js holding the #446 parity test (vendor.test.js:662-765) verbatim, plus one new test that re-anchors the premise the whole fallback ladder rests on: POST the same body with ['picocolors@1.1.1', 'this-package-truly-does-not-exist-xyz-789@99.0.0'] and assert !res.ok && res.status < 500 && res.status !== 429, i.e. jspm still fails the whole batch and still classifies an unknown package as permanent. Reuse the existing loud skip() helper (L693) for both. Without this, converting L556 and L1115 to the double would leave that premise asserted only by the double, which asserts our own model of jspm.
Create test/vendor-cli/vendor-pin.live.test.mjs with one test: makeApp, then runCli(['vendor','pin']) with no preload, asserting a real ga.jspm.io URL and a sha384- integrity, so the actual user command keeps live coverage.
Promote skip to failure under WEBJS_REQUIRE_NETWORK. In the shared skip() helper, fail with the recorded reason instead of calling t.skip when the variable is set. This is the lever the nightly pulls; a normal run is unaffected.
Filter live files in both runners. In scripts/run-node-tests.js, add .filter((f) => process.env.WEBJS_REQUIRE_NETWORK || !f.includes('.live.test.')) beside the browserSeg and e2eSeg filters at L57-63. Add the same filter to scripts/run-bun-tests.js, and drop ${SEP}vendor${SEP} from excludeSegs (L88) so coherence.test.js, ensure-committable.test.js, prune-differential-property.test.js, prune-pin.test.js, and the converted vendor.test.js rejoin the Bun matrix. All five were read and are runtime-safe under Bun. No DENYLIST entry is needed, because the live file is filtered by name. Fix BOTH comments that call the suite network-bound (L7 and L83-87). If one of the five then fails under Bun for a genuine runtime reason, denylist that FILE with its real reason; do not restore the blanket segment. Bun coverage matters most here, since a globalThis.fetch swap is precisely where Node and Bun could diverge.
.github/workflows/vendor-cdn.yml: on: schedule (daily) plus workflow_dispatch, npm ci, then WEBJS_REQUIRE_NETWORK=1 node --test packages/server/test/vendor/jspm-cdn.live.test.js test/vendor-cli/vendor-pin.live.test.mjs. No pull_request trigger, so it can never be a required check. Add a final if: failure() step that opens or comments on a fixed-title tracking issue via gh (permissions: issues: write), because GitHub notifies only the workflow file's last committer and a nightly nobody watches is a green-forever placebo.
Guards.
test/repo-health/jspm-double.test.mjs, modelled on test/repo-health/e2e-vendor-stub.test.mjs: a /generate POST answers a map keyed by bare name with a /double.js URL per install; the install-spec case table from e2e-vendor-stub.test.mjs:52-72 holds for scoped and subpath forms; an install in unresolvable 401s the WHOLE batch, not just its own entry; a GET of a minted URL returns bytes with a JavaScript content type; an unminted jspm.io or registry.npmjs.org request is recorded on unexpected and never reaches the network; a malformed or absent body does not answer an empty map.
test/repo-health/live-cdn-callers.test.mjs: walk every test file under test/ and packages/*/test/, and assert that a live third-party host literal (api.jspm.io, ga.jspm.io, registry.npmjs.org) inside a fetch( argument, and any call to pinAll / updatePinned / auditPinned / findOutdated outside a withMockedFetch or withJspmDouble block, appears only in a *.live.test.* file. Shape the allowlist like run-bun-tests.js:39-59's DENYLIST ({ file, tests, why }) and exclude the guard's own path by exact repo-relative match. Do not assert on the literal NETWORK_OK: that certifies a spelling, not a property, and reds on a rename or a reformat.
Docs. A framework-dev.md section beside the fix(server): elision-OFF boot intermittently 404s a module, leaving components inert #1228 note at L102 stating the policy once: live third-party calls live only in *.live.test.*, both runners exclude those unless WEBJS_REQUIRE_NETWORK=1, the nightly vendor-cdn workflow is what stops a permanent skip from hiding and who is expected to watch it, and a new vendor test uses test/fixtures/jspm-double.mjs rather than the network. Plus the two stale header comments (vendor-cli.test.mjs:1-9, vendor.test.js:399-403). No docs site, AGENTS.md, skill, or scaffold change, since no public or agent-facing surface moves.
Counterfactuals (each must red when broken)
Delete the preload flag from runCli, and the [jspm-double] armed stderr assertion reds, as does the /double.js assertion.
Point the double at a name-only key (the i.replace(/@[^@]*$/, '') shortcut), and the subpath case in jspm-double.test.mjs reds.
Make the double answer a partial map instead of 401ing the batch, and the per-package-isolation plus pinAll total-failure tests red.
Add a live-host fetch to any non-*.live.test.* file, and live-cdn-callers.test.mjs reds.
Drop WEBJS_REQUIRE_NETWORK from the nightly, and it runs zero live tests, which run-node-tests.js's existing zero-files guard plus the workflow's explicit file list surface.
Tests
Unit: the two new repo-health guards above, plus every converted test keeping its existing assertions and three of them gaining a stronger one (the call count at L420, the transitive pin, the /double.js wiring).
Bun parity: in scope and is steps 3 and 10. The vendor suite rejoins the Bun matrix and the CLI preload flag is runtime-selected, so a Node-only spelling of --import fails the matrix rather than silently skipping. No test/bun/*.mjs cross-runtime script is added, because no runtime-sensitive shipped source is touched.
Verify the premise before implementing. Reproduce the failure deliberately (an --import stub answering 503 for api.jspm.io) and confirm test/vendor-cli/vendor-cli.test.mjs hard-fails TODAY, rather than assuming the mechanism from this description.
vendor pin failing is sometimes CORRECT.pinAll: refuses to write empty pin file when every install fails (L1115) asserts a failure path on purpose, and pin names a found-but-uninstalled specifier (vendor-cli.test.mjs:133) asserts a non-zero exit. Drive those through opts.unresolvable; never relax the assertion.
A double that answers everything is a double that proves nothing. An unknown install is a 401 or a recorded refusal, never an empty map, since an absent importmap entry is an unresolved-bare-specifier error that kills a whole page graph.
Both surfaced while mapping the network surface, both are directly about this issue (an unbounded third-party call in the required job, and cross-test contamination through the cache primitive this change leans on), so they are fixed in this change rather than deferred.
fetchIntegrity (vendor.js:1111) has no AbortSignal, unlike every sibling fetch in the file (jspmCall 10s, fetchNpmJson 10s, fetchLiveIntegrity 10s). Default-mode pinAll calls it once per resolved URL (vendor.js:1306), and node --test applies no per-test deadline, so a hung CDN connection hangs the required job indefinitely rather than failing it. Give it the same INTEGRITY_FETCH_TIMEOUT_MS treatment its siblings have.
clearVendorCache() (vendor.js:679) does not reset lastLiveResolveFailed, though it clears jspmCache and liveIntegrityCache. The flag is set by every transient path (vendor.js:474, :572, :592) and reset only inside resolveVendorImports (vendor.js:2295), so a pinAll test that never goes through that function leaks a true into whatever runs next. clearVendorCache is documented as the start-clean primitive and withJspmDouble leans on it, so it should clear the flag too.
Because these touch packages/server/src, they carry their own unit tests plus a test/bun/*.mjs cross-runtime assertion if the parity hook asks for one, and they ride a separate commit from the test-infrastructure work.
What changed during implementation
Two decisions moved from the plan above, both recorded on PR #1297 with the evidence.
The static live-caller scan was replaced by a runtime deny. The plan called for a repo-health test that scans test sources for a third-party host inside a fetch(. Three review rounds found three different ways it went blind, each hiding every call below it in the file, and each fix opened a new hole: a file-level exemption; then no regex-literal awareness, so /rel=["']modulepreload["']/ desynced the mask to end of file; then regex awareness that read the / in a closing tag inside a nested html`...` template as a regex opener. Deciding whether a / starts a regex means lexing JavaScript, and a hand-rolled lexer facing nested template literals full of markup will keep being wrong. A scan also cannot see the app-boot tests, which reach jspm transitively with no fetch( in their own source. Both runners now preload test/fixtures/deny-live-hosts.mjs, which answers 503 for the three hosts unless WEBJS_REQUIRE_NETWORK is set. No parsing, no blind spots inside the test process, and a third-party-dependent test fails on every run rather than only during an outage.
clearVendorCache was left alone. The plan listed it as a robustness fix. It is unobservable: resolveVendorImports is the flag's only reader and resets it on entry, and the pinned short-circuit above that returns ok: true outright. It was implemented, then reverted when the counterfactual showed the test passed just as happily with the fix removed. A comment now records why the line is absent. fetchIntegrity AND downloadBundle both gained the missing timeout.
Acceptance criteria
A simulated jspm failure (unreachable host or forced non-200) does NOT fail the required Unit + integration CI job, including through a 4xx
npm test passes with api.jspm.io, ga.jspm.io, and registry.npmjs.org all failing
The #448 gitignore-healing property is still asserted, and still fails if the .webjs/vendor/ exception stops being written
Live jspm contract coverage still runs somewhere (the nightly), and its failure is visible rather than silently skipped forever, including for the real webjs vendor pin command
Unplugging the CLI preload reds a test, so the offline path cannot silently revert to the network
WEBJS_REQUIRE_NETWORK=1 includes the live files AND turns their skips into failures
A live third-party call added to any non-*.live.test.* file reds live-cdn-callers.test.mjs
The Bun matrix runs the vendor suite again
.github/workflows/ci.yml is unchanged by this work
Problem
The required
Unit + integration (node --test)CI job fetches from the live jspm CDN (api.jspm.io/ga.jspm.io), so a jspm outage or rate-limit reds CI on a pull request that has nothing to do with vendoring.Proven on PR #1149 (a documentation-only change touching five prose files): the job failed on
a blanket .webjs/ ignore is healed so the pin is committableintest/vendor-cli/vendor-cli.test.mjs:176, assertingassert.equal(code, 0)at L187 whenwebjs vendor pinexited 1. A re-run of the IDENTICAL commit (91205d71, no code change) then passed all ten checks. During the same window, a localwebjsboot loggedcould not vendor 2 packages via jspm (status 401), and a directcurltoga.jspm.ioreturned 200 again shortly after, so the outage was transient and external.This is a recurrence of the class that #312 already established (
De-flake the jspm install-order vendor test (live-CDN dependency), closed 2026-06-03, which reported the vendor tests flaking roughly 1 run in 4 across 18 PRs). #312 fixed ONE instance, inpackages/server/test/vendor/vendor.test.js. The root cause, a required CI job depending on a third-party CDN's availability, survives. The cost is real: a false red blocks a merge, and because the project gates merges on green CI, it either burns an investigation (it burned one on #1149) or trains people to wave red checks through.The surface is wider than the one failing test. Three third-party hosts are reached from the required job:
api.jspm.ioandga.jspm.io(16 gated tests inpackages/server/test/vendor/vendor.test.js, 5 gated intest/vendor-cli/vendor-cli.test.mjs), plusregistry.npmjs.orgfrom two tests that were never gated at all (vendor.test.js:1904and:1930, viafindOutdated, up to 10s each on an outage).Implementation plan
Scope note. Almost all of this is test and CI infrastructure. The one exception is two small
packages/server/src/vendor.jsrobustness fixes (a missing timeout and a missing cache reset), described at the end and carried in their own commit; nothing else in shipped source moves.The core decision
Split the surface in two by FILENAME, and let the test runners enforce the split.
*.live.test.{js,mjs}is the only shape allowed to touch a third-party host. Both runners exclude those files unlessWEBJS_REQUIRE_NETWORK=1. A nightly non-required workflow sets that variable.The current
NETWORK_OK/WEBJS_SKIP_NETWORK_TESTSgate is deleted entirely. It is the wrong shape: it is opt-OUT (so CI, which never sets it, always runs live), it is enforced by convention only, and it cannot express "this file is live" to the runner.Why "keep the one live test gated in place" is not enough. After #1219 that test skips on a throw, a 5xx, or a 429, but still reds on a 4xx (
packages/server/test/vendor/vendor.test.js:744), because the ground truth having just succeeded means a 4xx implies our own request is malformed. A WAF 403, a 404 on a moved route, or a rate limit expressed as a 403 therefore still reds an unrelated PR, which is the exact failure #1149 hit. Leaving it in the required job would leave the problem unfixed..github/workflows/ci.ymlis not edited. Itson:block is shared by eleven jobs and it is concurrently edited by #1135 and #1257; the runner-level filter achieves the same thing with no rebase conflict.Rejected
env: WEBJS_SKIP_NETWORK_TESTS: 1on the required unit job. It mutes 21 tests to fix 20 that should never have needed the network, deletes the one assertion worth keeping from every PR run, misses the two ungatedregistry.npmjs.orgcalls entirely, and is a third editor of a file fix: a flaky plain-link test aborts the whole Browser CI job #1135 and feat: let doctor gate CI without making every warning fatal #1257 already change..webjs/vendor/importmap.jsoninto an in-repo app so its boot resolves offline. Explicitly rejected before: a committed pin file broke the Add e2e network probes for vendor-never-fetched + inert-route zero-JS elision #170 elision e2e and was reverted in Revert the blog vendor pin that broke the #170 elision e2e #196.@codemirror/lint@6.9.6was yanked and redded main for every PR). A generated double has nothing to go stale.vendor pintreat a CDN failure as non-fatal in SHIPPED code. Pin failure is a real user-facing outcome (pinAll: refuses to write empty pin file when every install fails, L1115), and weakening the runtime to calm a test inverts the direction.Files
New:
test/fixtures/install-spec.mjssplitInstall/packageName/subpath, lifted verbatim fromtest/e2e/fixtures/stub-jspm.mjs:98-130. Scope-aware and subpath-aware. No side effects.test/fixtures/jspm-double.mjsjspmDouble(opts)returning afetch-compatible fn, pluswithJspmDouble(opts, body). No import side effects.test/fixtures/jspm-double-preload.mjsglobalThis.fetchat module load. The spawned-child arm.packages/server/test/vendor/jspm-cdn.live.test.jsvendor.test.js.test/vendor-cli/vendor-pin.live.test.mjswebjs vendor pinrun (no preload), so the real user command keeps live coverage.test/repo-health/jspm-double.test.mjstest/repo-health/live-cdn-callers.test.mjs.github/workflows/vendor-cdn.ymlWEBJS_REQUIRE_NETWORK=1.Modified:
test/e2e/fixtures/stub-jspm.mjs: import the three install-spec helpers from the new shared module and re-export them, sotest/repo-health/e2e-vendor-stub.test.mjskeeps passing verbatim. No behaviour change.test/vendor-cli/vendor-cli.test.mjs: preload wiring inrunCli(L24), deleteNETWORK_OK(L22) and all five gates (L65, L78, L85, L110, L165), fix the header (L7-8).packages/server/test/vendor/vendor.test.js: deleteNETWORK_OK(L404) and all sixteen gates, convert thirteen tests to the double, move the live test out, fix the section comment (L399-403).scripts/run-node-tests.js: add the.live.test.filter beside the existingbrowserSegande2eSegfilters at L57-63.scripts/run-bun-tests.js: the same filter; drop${SEP}vendor${SEP}fromexcludeSegs(L88) and fix both comments that call the vendor suite network-bound (L7 and L83-87).framework-dev.md: a policy section beside the existing fix(server): elision-OFF boot intermittently 404s a module, leaving components inert #1228 note at L102.The double
Model
api.jspm.iofaithfully, because vendor.js's whole fallback ladder is built on its exact failure semantics (packages/server/src/vendor.js:383,:418-448,:548-626).https://api.jspm.io/generate: parseJSON.parse(init.body).install, answer200 { map: { imports } }.https://ga.jspm.io/npm:<name>@<version><subpath>/double.js, keyed on<name><subpath>. Derive name, version, and subpath withtest/fixtures/install-spec.mjs, never with thei.replace(/@[^@]*$/, '')shortcut the existing inline mocks use (vendor.test.js:588). That regex collapsesdayjs@1.11.13/plugin/utctodayjs, which would makepartsByInstall.get(spec)miss,derivePinPartsrecoversubpath: '', andpinAllreport a spurious failure. The<name>@<version>substring must survive verbatim, sincederivePinParts(vendor.js:702-704) parses the version straight out of the URL.opts.unresolvablefails the whole batch with401 { error: 'Error: Not Found' }. That is real jspm behaviour (vendor.js:383) and it is what drives the per-package probe path.opts.transitivesfolds extra{spec: url}entries into the answer, so the flattened transitive pin path (vendor.js:1276-1291) becomes testable for the first time.opts.statusforces a transient code (5xx or 429) for the transient-fallback tests.content-type: text/javascript, sodownloadBundle(vendor.js:1079) andfetchIntegrity(vendor.js:1111) complete./double.jstail is load-bearing: real jspm never emits it, so a wiring assertion can key on it.Refusal must be loud, and a throw is not loud enough. Every caller in vendor.js swallows a throw (
vendor.js:441,:1096,:1122,:2213), so a broken preload would silently degrade to{}and a weak assertion would still pass. Instead:double.unexpected, andwithJspmDoubleasserts that array is empty in itsfinally.[jspm-double] armedto stderr at load, prints each refusal, and setsprocess.exitCode = 1, so a refused CLI child failsrunCli'sassert.equal(code, 0).registry.npmjs.orgis in the refusal set too.This is a deliberate divergence from
test/e2e/fixtures/stub-jspm.mjs:151-160, which passes through to the real network on refusal. Do not merge the two fixtures: that one must emit a real executable module for a browser, this one only needs jspm-shaped URLs and bytes.Steps
test/fixtures/install-spec.mjs, lifted fromstub-jspm.mjs; makestub-jspm.mjsimport and re-export from it. Confirmtest/repo-health/e2e-vendor-stub.test.mjsstill passes unchanged.test/fixtures/jspm-double.mjsper the section above, plustest/fixtures/jspm-double-preload.mjsreading its config from aWEBJS_JSPM_DOUBLEJSON env var (absent means resolve everything).Wire the CLI test. In
test/vendor-cli/vendor-cli.test.mjs,runCli(L24) prepends the preload flag. The flag is runtime-specific and neither runtime honours the other's, the test(e2e): stop the elision-off block depending on a live CDN #1229 lesson attest/e2e/e2e.test.mjs:85-89. Here the parent runtime IS the child runtime, sincerunClispawnsprocess.execPathand underbun testthat is bun:The
file://form matters becauserunClispawns withcwdset to the temp app dir, so a relative--importwould resolve against the wrong directory. Then deleteNETWORK_OKand every gate, and rewrite the header comment.Prove the wiring for every call site, not one. Assert
stderrmatches/\[jspm-double\] armed/insiderunCliitself, so all seven call sites are covered by one line. Additionally assert in the L65 test thatparsed.imports.picocolorsends with/double.js. The existing/^https:\/\/ga\.jspm\.io\/npm:picocolors@/match (L75) is true against real jspm too, so it cannot detect the preload being dropped. Usematch, never equality, on stderr elsewhere.Convert the in-process tests in
packages/server/test/vendor/vendor.test.js: L412, 420, 556, 1004, 1039, 1115, 1147, 1194, 1213, 1228, 1243, 1701, 1718. Every existing assertion about URL shape, bytes, integrity, prune, and pin-file removal survives verbatim, which is the point of a double that mints jspm-shaped URLs. Three need care:refuses to write empty pin file when every install fails) keeps assertingresult.failedand a null pin file, driven byopts.unresolvable. Never relax it.jspmGenerateshort-circuits to a cachedjspmResolveOne(vendor.js:611), so the total is three round trips, not four. Only assert a count if you pin it to that trace.hits in-process cache) today onlydeepEquals two results, which passes even if a second round trip fired. With the double it can count calls and assert exactly one, so the conversion makes it discriminating for the first time.Add one assertion using
opts.transitivesthat a flattened transitive lands infile.importswith abundleFilenameWithSubpath-shaped bundle name.withJspmDoublecallsclearVendorCache()on entry and exit.Drop two gates that were never needed. L406 short-circuits before any fetch (
vendor.js:549); L1950 already installs its own throwingfetchfor its whole duration. Remove the gates, keep the bodies byte-identical.Mock two ungated live calls the old gate missed. The
updatePinnedprovider tests atvendor.test.js:1904and:1930reachregistry.npmjs.orgthroughfindOutdated(vendor.js:1499), bounded by no gate and costing up to 10s each on an outage. Both assert onlyresult.provider, so wrap them in the existingwithMockedFetch(L466) answering a 404.Move the live tests out. Create
packages/server/test/vendor/jspm-cdn.live.test.jsholding the#446parity test (vendor.test.js:662-765) verbatim, plus one new test that re-anchors the premise the whole fallback ladder rests on: POST the same body with['picocolors@1.1.1', 'this-package-truly-does-not-exist-xyz-789@99.0.0']and assert!res.ok && res.status < 500 && res.status !== 429, i.e. jspm still fails the whole batch and still classifies an unknown package as permanent. Reuse the existing loudskip()helper (L693) for both. Without this, converting L556 and L1115 to the double would leave that premise asserted only by the double, which asserts our own model of jspm.Create
test/vendor-cli/vendor-pin.live.test.mjswith one test:makeApp, thenrunCli(['vendor','pin'])with no preload, asserting a realga.jspm.ioURL and asha384-integrity, so the actual user command keeps live coverage.Promote skip to failure under
WEBJS_REQUIRE_NETWORK. In the sharedskip()helper, fail with the recorded reason instead of callingt.skipwhen the variable is set. This is the lever the nightly pulls; a normal run is unaffected.Filter live files in both runners. In
scripts/run-node-tests.js, add.filter((f) => process.env.WEBJS_REQUIRE_NETWORK || !f.includes('.live.test.'))beside thebrowserSegande2eSegfilters at L57-63. Add the same filter toscripts/run-bun-tests.js, and drop${SEP}vendor${SEP}fromexcludeSegs(L88) socoherence.test.js,ensure-committable.test.js,prune-differential-property.test.js,prune-pin.test.js, and the convertedvendor.test.jsrejoin the Bun matrix. All five were read and are runtime-safe under Bun. No DENYLIST entry is needed, because the live file is filtered by name. Fix BOTH comments that call the suite network-bound (L7 and L83-87). If one of the five then fails under Bun for a genuine runtime reason, denylist that FILE with its real reason; do not restore the blanket segment. Bun coverage matters most here, since aglobalThis.fetchswap is precisely where Node and Bun could diverge..github/workflows/vendor-cdn.yml:on: schedule(daily) plusworkflow_dispatch,npm ci, thenWEBJS_REQUIRE_NETWORK=1 node --test packages/server/test/vendor/jspm-cdn.live.test.js test/vendor-cli/vendor-pin.live.test.mjs. Nopull_requesttrigger, so it can never be a required check. Add a finalif: failure()step that opens or comments on a fixed-title tracking issue viagh(permissions: issues: write), because GitHub notifies only the workflow file's last committer and a nightly nobody watches is a green-forever placebo.Guards.
test/repo-health/jspm-double.test.mjs, modelled ontest/repo-health/e2e-vendor-stub.test.mjs: a/generatePOST answers a map keyed by bare name with a/double.jsURL per install; the install-spec case table frome2e-vendor-stub.test.mjs:52-72holds for scoped and subpath forms; an install inunresolvable401s the WHOLE batch, not just its own entry; a GET of a minted URL returns bytes with a JavaScript content type; an unmintedjspm.ioorregistry.npmjs.orgrequest is recorded onunexpectedand never reaches the network; a malformed or absent body does not answer an empty map.test/repo-health/live-cdn-callers.test.mjs: walk every test file undertest/andpackages/*/test/, and assert that a live third-party host literal (api.jspm.io,ga.jspm.io,registry.npmjs.org) inside afetch(argument, and any call topinAll/updatePinned/auditPinned/findOutdatedoutside awithMockedFetchorwithJspmDoubleblock, appears only in a*.live.test.*file. Shape the allowlist likerun-bun-tests.js:39-59's DENYLIST ({ file, tests, why }) and exclude the guard's own path by exact repo-relative match. Do not assert on the literalNETWORK_OK: that certifies a spelling, not a property, and reds on a rename or a reformat.Docs. A
framework-dev.mdsection beside the fix(server): elision-OFF boot intermittently 404s a module, leaving components inert #1228 note at L102 stating the policy once: live third-party calls live only in*.live.test.*, both runners exclude those unlessWEBJS_REQUIRE_NETWORK=1, the nightlyvendor-cdnworkflow is what stops a permanent skip from hiding and who is expected to watch it, and a new vendor test usestest/fixtures/jspm-double.mjsrather than the network. Plus the two stale header comments (vendor-cli.test.mjs:1-9,vendor.test.js:399-403). No docs site,AGENTS.md, skill, or scaffold change, since no public or agent-facing surface moves.Counterfactuals (each must red when broken)
runCli, and the[jspm-double] armedstderr assertion reds, as does the/double.jsassertion.i.replace(/@[^@]*$/, '')shortcut), and the subpath case injspm-double.test.mjsreds.pinAlltotal-failure tests red..gitignoreexception-writing code, and the dogfood: webjs vendor pin writes to gitignored .webjs/vendor (opt-in pins not committable) #448 healing test reds (unchanged from today; the double does not touch thegithalf of that block).fetchto any non-*.live.test.*file, andlive-cdn-callers.test.mjsreds.WEBJS_REQUIRE_NETWORKfrom the nightly, and it runs zero live tests, whichrun-node-tests.js's existing zero-files guard plus the workflow's explicit file list surface.Tests
/double.jswiring).--importfails the matrix rather than silently skipping. Notest/bun/*.mjscross-runtime script is added, because no runtime-sensitive shipped source is touched.Landmines for the implementing agent
--importstub answering 503 forapi.jspm.io) and confirmtest/vendor-cli/vendor-cli.test.mjshard-fails TODAY, rather than assuming the mechanism from this description.vendor.test.js's install-order test (De-flake the jspm install-order vendor test (live-CDN dependency) #312) or its conflicting-graph test (fix(server): stop pinning a yanked version in the vendor CDN test #1219); both already moved to deterministic mocks.vendor pinfailing is sometimes CORRECT.pinAll: refuses to write empty pin file when every install fails(L1115) asserts a failure path on purpose, andpin names a found-but-uninstalled specifier(vendor-cli.test.mjs:133) asserts a non-zero exit. Drive those throughopts.unresolvable; never relax the assertion.node_modulescannot run these tests.makeApp()symlinks the repo rootnode_modulesinto the temp app, so runnpm run worktree:linkfirst (fix(docs): the fresh-worktree node_modules remedy produces a broken worktree #1288). Related trap seen while diagnosing docs: explain the swr cache option on GET server actions #1149: through a linked root,@webjsdev/*resolves into the PRIMARY checkout, so a dirty sibling checkout silently changes local results.packages/stays plain.jswith JSDoc, so the new helpers live under roottest/as.mjs. The pin file must stay committable (dogfood: webjs vendor pin writes to gitignored .webjs/vendor (opt-in pins not committable) #448). Do not commit a.webjs/vendor/importmap.jsoninto any in-repo app (Revert the blog vendor pin that broke the #170 elision e2e #196).Two vendor.js robustness gaps, fixed here
Both surfaced while mapping the network surface, both are directly about this issue (an unbounded third-party call in the required job, and cross-test contamination through the cache primitive this change leans on), so they are fixed in this change rather than deferred.
fetchIntegrity(vendor.js:1111) has noAbortSignal, unlike every sibling fetch in the file (jspmCall10s,fetchNpmJson10s,fetchLiveIntegrity10s). Default-modepinAllcalls it once per resolved URL (vendor.js:1306), andnode --testapplies no per-test deadline, so a hung CDN connection hangs the required job indefinitely rather than failing it. Give it the sameINTEGRITY_FETCH_TIMEOUT_MStreatment its siblings have.clearVendorCache()(vendor.js:679) does not resetlastLiveResolveFailed, though it clearsjspmCacheandliveIntegrityCache. The flag is set by every transient path (vendor.js:474,:572,:592) and reset only insideresolveVendorImports(vendor.js:2295), so apinAlltest that never goes through that function leaks atrueinto whatever runs next.clearVendorCacheis documented as the start-clean primitive andwithJspmDoubleleans on it, so it should clear the flag too.Because these touch
packages/server/src, they carry their own unit tests plus atest/bun/*.mjscross-runtime assertion if the parity hook asks for one, and they ride a separate commit from the test-infrastructure work.What changed during implementation
Two decisions moved from the plan above, both recorded on PR #1297 with the evidence.
The static live-caller scan was replaced by a runtime deny. The plan called for a repo-health test that scans test sources for a third-party host inside a
fetch(. Three review rounds found three different ways it went blind, each hiding every call below it in the file, and each fix opened a new hole: a file-level exemption; then no regex-literal awareness, so/rel=["']modulepreload["']/desynced the mask to end of file; then regex awareness that read the/in a closing tag inside a nestedhtml`...`template as a regex opener. Deciding whether a/starts a regex means lexing JavaScript, and a hand-rolled lexer facing nested template literals full of markup will keep being wrong. A scan also cannot see the app-boot tests, which reach jspm transitively with nofetch(in their own source. Both runners now preloadtest/fixtures/deny-live-hosts.mjs, which answers 503 for the three hosts unlessWEBJS_REQUIRE_NETWORKis set. No parsing, no blind spots inside the test process, and a third-party-dependent test fails on every run rather than only during an outage.clearVendorCachewas left alone. The plan listed it as a robustness fix. It is unobservable:resolveVendorImportsis the flag's only reader and resets it on entry, and the pinned short-circuit above that returnsok: trueoutright. It was implemented, then reverted when the counterfactual showed the test passed just as happily with the fix removed. A comment now records why the line is absent.fetchIntegrityANDdownloadBundleboth gained the missing timeout.Acceptance criteria
Unit + integrationCI job, including through a 4xxnpm testpasses withapi.jspm.io,ga.jspm.io, andregistry.npmjs.orgall failing#448gitignore-healing property is still asserted, and still fails if the.webjs/vendor/exception stops being writtenwebjs vendor pincommandWEBJS_REQUIRE_NETWORK=1includes the live files AND turns their skips into failures*.live.test.*file redslive-cdn-callers.test.mjs.github/workflows/ci.ymlis unchanged by this work