') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); [vitest] Isolate test data and improve cleanup by VaguelySerious · Pull Request #1895 · vercel/workflow · GitHub
Skip to content

[vitest] Isolate test data and improve cleanup - #1895

Merged
VaguelySerious merged 1 commit into
mainfrom
peter/local-world-isolation
May 2, 2026
Merged

[vitest] Isolate test data and improve cleanup#1895
VaguelySerious merged 1 commit into
mainfrom
peter/local-world-isolation

Conversation

@VaguelySerious

Copy link
Copy Markdown
Member

Summary

Closes#1818

Squashes the contents of #1818 (vitest project-context isolation + world-local recovery scoping by Tom Dale) together with two follow-up fixes for the e2e dev tests that surfaced once the vitest changes restored test isolation:

  • @workflow/vitest project-scoped context.workflow() resolves cwd, rootDir, dataDir, and outDir once and passes them through Vitest's per-project provided context. global-setup and setup-file consume that resolved state instead of reading process-wide env vars, so workspace projects and config reloads no longer leak paths into one another. Adds a unit test suite for the harness.
  • world-local recovery scoped by tag.paginatedFileSystemQuery now keeps fileIdFilter applied across cursor pages (previously dropped after page one). start() gains a recoverActiveRuns opt-out, used by the Vitest harness so worker startup does not re-enqueue stale runs before direct handlers are registered.
  • Dev test cleanup hardening.should include steps discovered from workflow imports now tears down in-test and waits for the deferred builder to drop the discovered step from the manifest before the next test file runs, instead of relying on afterEach. should rebuild on imported step dependency change swallows the Turbopack-on-Windows MODULE_UNPARSABLE flake by rewriting the api file to invalidate Turbopack's bad cache and retrying. Both addressed cases where the Windows E2E job would burn its full 30-minute timeout polling stuck workflow runs.

Test plan

  • All Tests CI checks pass on Windows (the Turbopack flake is the focus)
  • pnpm exec vitest run packages/vitest/src/index.test.ts packages/world-local/src/fs.test.ts packages/world-local/src/reenqueue.test.ts
  • pnpm exec tsc -p packages/vitest/tsconfig.json --noEmit
  • pnpm exec tsc -p packages/world-local/tsconfig.json --noEmit
  • Manual e2e against nextjs-turbopack workbench: dev tests + e2e.test.ts succeed against the same dev server (verified locally on macOS)

🤖 Generated with Claude Code

Move the @workflow/vitest harness from process-wide env vars to project-scoped
provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once
and passes them through Vitest's per-project context; global-setup and
setup-file read that resolved state when building bundles and creating the
local test world.
On the recovery side, world-local now keeps fileIdFilter applied while
paginating and scopes startup recovery to the active tag. Adds an explicit
recoverActiveRuns opt-out and disables recovery in the Vitest harness so
worker startup does not re-enqueue stale runs before direct handlers are
registered.
Also tightens cleanup in two e2e dev tests so the deferred-builder /
Turbopack interaction does not leave the dev server in a broken state for
subsequent test files (which previously caused Windows e2e to hang for the
full 30-min job timeout).
Co-Authored-By: Tom Dale <tom@vercel.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 7ff87ae

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 19 packages
NameType
@workflow/vitestPatch
@workflow/world-localPatch
@workflow/cliPatch
@workflow/corePatch
@workflow/world-postgresPatch
workflowPatch
@workflow/world-testingPatch
@workflow/buildersPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/web-sharedPatch
@workflow/webPatch
@workflow/aiPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercelBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

All tests passed

Summary

PassedFailedSkippedTotal
✅ ▲ Vercel Production10000671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 🪟 Windows970097
✅ 📋 Other273018291
Total460403434947

Details by Category

✅ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
✅ nextjs-webpack9502
✅ nitro9007
✅ nuxt9007
✅ sveltekit9007
✅ vite9007
✅ 💻 Local Development
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable9106
✅ express-stable9106
✅ fastify-stable9106
✅ hono-stable9106
✅ nextjs-turbopack-canary78019
✅ nextjs-turbopack-stable9700
✅ nextjs-webpack-canary78019
✅ nextjs-webpack-stable9700
✅ nitro-stable9106
✅ nuxt-stable9106
✅ sveltekit-stable9106
✅ vite-stable9106
✅ 🪟 Windows
AppPassedFailedSkipped
✅ nextjs-turbopack9700
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run

@github-actions

github-actionsBot commented May 2, 2026

Copy link
Copy Markdown
Contributor

📊 Benchmark Results

📈 Comparing against baseline from main branch. Green 🟢 = faster, Red 🔺 = slower.

workflow with no steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.036s (-16.0% 🟢)1.005s (~)0.968s101.00x
💻 LocalNext.js (Turbopack)0.048s1.006s0.958s101.33x
🐘 PostgresNext.js (Turbopack)0.059s1.011s0.953s101.62x
🐘 PostgresExpress0.061s (+4.7%)1.010s (~)0.950s101.68x
🐘 PostgresNitro0.063s (-34.2% 🟢)1.010s (-3.1%)0.948s101.73x
💻 LocalExpress0.319s (+620.3% 🔺)1.032s (+2.7%)0.712s108.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express0.211s (-10.2% 🟢)1.752s (-17.9% 🟢)1.541s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro1.095s (-3.2%)2.005s (~)0.910s101.00x
💻 LocalNext.js (Turbopack)1.118s2.007s0.889s101.02x
🐘 PostgresNext.js (Turbopack)1.137s2.009s0.872s101.04x
🐘 PostgresExpress1.148s (~)2.011s (~)0.864s101.05x
🐘 PostgresNitro1.152s (+1.1%)2.011s (~)0.859s101.05x
💻 LocalExpress1.980s (+75.9% 🔺)2.133s (+6.4% 🔺)0.154s101.81x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.180s (+16.3% 🔺)4.004s (+5.2% 🔺)1.823s101.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro10.649s (-2.7%)11.023s (~)0.374s31.00x
💻 LocalNext.js (Turbopack)10.851s11.024s0.173s31.02x
🐘 PostgresNext.js (Turbopack)10.860s11.019s0.159s31.02x
🐘 PostgresExpress10.919s (~)11.019s (~)0.100s31.03x
🐘 PostgresNitro10.935s (+0.6%)11.026s (~)0.092s31.03x
💻 LocalExpress16.822s (+54.0% 🔺)17.112s (+55.2% 🔺)0.291s21.58x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express17.000s (~)19.075s (-4.7%)2.075s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro14.216s (-5.6% 🟢)15.029s (-6.2% 🟢)0.813s41.00x
🐘 PostgresNext.js (Turbopack)14.502s15.024s0.522s41.02x
🐘 PostgresExpress14.582s (~)15.022s (~)0.440s41.03x
🐘 PostgresNitro14.620s (~)15.021s (~)0.401s41.03x
💻 LocalNext.js (Turbopack)14.661s15.031s0.370s41.03x
💻 LocalExpress30.130s (+101.3% 🔺)30.656s (+104.0% 🔺)0.526s22.12x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express32.681s (-35.0% 🟢)35.075s (-33.3% 🟢)2.394s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)13.820s14.025s0.205s71.00x
🐘 PostgresExpress13.836s (-1.2%)14.022s (-3.9%)0.185s71.00x
🐘 PostgresNitro14.148s (+1.3%)15.027s (+5.0% 🔺)0.879s61.02x
💻 LocalNitro14.853s (-11.5% 🟢)15.027s (-11.8% 🟢)0.174s61.07x
💻 LocalNext.js (Turbopack)16.247s17.032s0.785s61.18x
💻 LocalExpress44.650s (+168.9% 🔺)45.194s (+165.4% 🔺)0.545s23.23x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express56.779s (-53.2% 🟢)58.992s (-52.3% 🟢)2.213s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.225s2.009s0.784s151.00x
🐘 PostgresExpress1.256s (~)2.010s (~)0.754s151.03x
🐘 PostgresNitro1.283s (+0.6%)2.009s (~)0.726s151.05x
💻 LocalNitro1.475s (-9.6% 🟢)2.006s (-3.3%)0.530s151.20x
💻 LocalNext.js (Turbopack)1.586s2.074s0.487s151.30x
💻 LocalExpress3.067s (+106.0% 🔺)3.581s (+78.6% 🔺)0.515s92.50x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.532s (-11.5% 🟢)4.226s (-8.6% 🟢)1.694s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.324s (-1.6%)3.009s (~)0.686s101.00x
🐘 PostgresNext.js (Turbopack)2.387s3.009s0.623s101.03x
🐘 PostgresNitro2.387s (+1.5%)3.009s (~)0.622s101.03x
💻 LocalNitro2.731s (-13.1% 🟢)3.108s (-20.0% 🟢)0.377s101.18x
💻 LocalNext.js (Turbopack)2.807s3.564s0.758s91.21x
💻 LocalExpress5.304s (+79.6% 🔺)5.746s (+66.4% 🔺)0.442s72.28x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.390s (-34.0% 🟢)4.463s (-12.7% 🟢)2.073s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.464s (-0.6%)4.011s (~)0.547s81.00x
🐘 PostgresNitro3.496s (~)4.010s (~)0.515s81.01x
🐘 PostgresNext.js (Turbopack)3.627s4.011s0.384s81.05x
💻 LocalNitro7.718s (-7.6% 🟢)8.269s (-8.3% 🟢)0.551s42.23x
💻 LocalNext.js (Turbopack)8.091s8.771s0.680s42.34x
💻 LocalExpress28.956s (+247.3% 🔺)35.190s (+289.9% 🔺)6.234s18.36x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.045s (-28.2% 🟢)5.060s (-17.4% 🟢)2.015s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.211s2.009s0.798s151.00x
🐘 PostgresNitro1.254s (~)2.009s (~)0.755s151.04x
🐘 PostgresExpress1.258s (~)2.007s (~)0.750s151.04x
💻 LocalNext.js (Turbopack)1.530s2.006s0.476s151.26x
💻 LocalNitro1.873s (~)2.392s (+2.2%)0.519s131.55x
💻 LocalExpress4.771s (+151.9% 🔺)5.230s (+121.3% 🔺)0.459s63.94x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.281s (-11.6% 🟢)4.049s (-6.9% 🟢)1.768s81.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.341s (~)3.009s (~)0.669s101.00x
🐘 PostgresNitro2.363s (+1.0%)3.010s (~)0.647s101.01x
🐘 PostgresNext.js (Turbopack)2.396s3.007s0.612s101.02x
💻 LocalNitro2.697s (-12.0% 🟢)3.008s (-22.6% 🟢)0.311s101.15x
💻 LocalNext.js (Turbopack)3.047s3.759s0.712s81.30x
💻 LocalExpress12.971s (+314.1% 🔺)13.445s (+257.4% 🔺)0.474s35.54x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.390s (-25.1% 🟢)4.335s (-9.5% 🟢)1.944s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.474s (-0.7%)4.009s (~)0.534s81.00x
🐘 PostgresNitro3.497s (~)4.011s (~)0.515s81.01x
🐘 PostgresNext.js (Turbopack)3.625s4.011s0.386s81.04x
💻 LocalNitro7.806s (-14.6% 🟢)8.015s (-20.0% 🟢)0.210s42.25x
💻 LocalNext.js (Turbopack)8.633s9.270s0.637s42.48x
💻 LocalExpress37.597s (+327.2% 🔺)38.199s (+311.9% 🔺)0.602s110.82x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.816s (-56.1% 🟢)4.597s (-43.8% 🟢)1.781s71.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.701s (-28.6% 🟢)1.004s (-8.2% 🟢)0.304s601.00x
🐘 PostgresNext.js (Turbopack)0.790s1.006s0.216s601.13x
🐘 PostgresExpress0.814s (-3.0%)1.023s (~)0.209s591.16x
🐘 PostgresNitro0.846s (+3.1%)1.007s (~)0.161s601.21x
💻 LocalNext.js (Turbopack)0.876s1.039s0.163s581.25x
💻 LocalExpress5.085s (+416.7% 🔺)5.677s (+427.6% 🔺)0.591s117.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express9.376s (-50.7% 🟢)11.200s (-47.5% 🟢)1.823s61.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)1.887s2.029s0.141s451.00x
🐘 PostgresExpress1.918s (-2.9%)2.124s (-5.9% 🟢)0.206s431.02x
🐘 PostgresNitro2.050s (+6.3% 🔺)2.736s (+30.3% 🔺)0.686s331.09x
💻 LocalNitro2.272s (-25.1% 🟢)3.007s (-20.0% 🟢)0.735s301.20x
💻 LocalNext.js (Turbopack)2.725s3.008s0.283s301.44x
💻 LocalExpress17.794s (+490.0% 🔺)18.499s (+416.0% 🔺)0.705s59.43x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express27.950s (-19.1% 🟢)30.591s (-16.9% 🟢)2.641s31.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)3.839s4.042s0.203s301.00x
🐘 PostgresExpress3.893s (-2.4%)4.076s (-6.7% 🟢)0.183s301.01x
🐘 PostgresNitro4.130s (+0.6%)4.942s (+7.3% 🔺)0.812s251.08x
💻 LocalNitro7.321s (-21.3% 🟢)8.015s (-20.0% 🟢)0.694s151.91x
💻 LocalNext.js (Turbopack)8.678s9.018s0.340s142.26x
💻 LocalExpress44.180s (+379.7% 🔺)44.534s (+344.5% 🔺)0.355s311.51x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express73.931s (-43.1% 🟢)78.038s (-41.0% 🟢)4.107s21.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.251s1.007s0.756s601.00x
🐘 PostgresExpress0.284s (+0.5%)1.006s (~)0.722s601.13x
🐘 PostgresNitro0.295s (+4.1%)1.007s (~)0.712s601.18x
💻 LocalNext.js (Turbopack)0.574s1.005s0.431s602.29x
💻 LocalNitro0.611s (+1.1%)1.021s (~)0.410s592.44x
💻 LocalExpress3.353s (+498.4% 🔺)4.041s (+302.4% 🔺)0.688s1613.37x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.431s (-26.8% 🟢)3.532s (-2.9%)2.101s171.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.481s1.006s0.526s901.00x
🐘 PostgresExpress0.495s (-2.9%)1.006s (~)0.512s901.03x
🐘 PostgresNitro0.518s (+4.4%)1.007s (~)0.488s901.08x
💻 LocalNitro2.432s (-4.2%)3.008s (~)0.575s305.06x
💻 LocalNext.js (Turbopack)2.583s3.010s0.426s305.37x
💻 LocalExpress10.002s (+298.0% 🔺)11.583s (+284.9% 🔺)1.581s820.80x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.968s (-2.6%)4.869s (+1.3%)1.901s191.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.785s (-4.1%)1.007s (-1.0%)0.222s1201.00x
🐘 PostgresNext.js (Turbopack)0.793s1.015s0.223s1191.01x
🐘 PostgresNitro0.858s (+8.5% 🔺)1.011s (~)0.153s1191.09x
💻 LocalNitro10.439s (-6.7% 🟢)10.933s (-6.3% 🟢)0.494s1113.30x
💻 LocalNext.js (Turbopack)10.942s11.393s0.451s1113.94x
💻 LocalExpress25.204s (+125.2% 🔺)26.108s (+118.7% 🔺)0.904s532.11x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express6.481s (-12.7% 🟢)8.708s (-5.8% 🟢)2.227s141.00x
▲ VercelNext.js (Turbopack)⚠️missing----
▲ VercelNitro⚠️missing----

🔍 Observability: Express

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Nitro0.140s (-34.5% 🟢)1.004s (~)0.009s (-24.0% 🟢)1.015s (~)0.875s101.00x
💻 LocalNext.js (Turbopack)0.175s1.003s0.013s1.019s0.844s101.25x
🐘 PostgresNext.js (Turbopack)0.199s1.001s0.001s1.011s0.812s101.42x
🐘 PostgresExpress0.203s (-1.0%)0.995s (~)0.002s (+12.5% 🔺)1.010s (~)0.807s101.45x
🐘 PostgresNitro0.219s (+6.7% 🔺)0.992s (-0.8%)0.002s (+13.3% 🔺)1.011s (~)0.792s101.56x
💻 LocalExpress1.016s (+410.1% 🔺)1.720s (+71.2% 🔺)0.167s (+1276.0% 🔺)1.894s (+86.1% 🔺)0.879s107.26x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express1.508s (-39.8% 🟢)2.963s (-27.6% 🟢)1.704s (+77.3% 🔺)5.111s (-8.6% 🟢)3.602s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.602s (-4.4%)1.007s (~)0.004s (+1.3%)1.022s (~)0.420s591.00x
🐘 PostgresNext.js (Turbopack)0.612s1.010s0.006s1.026s0.414s591.02x
🐘 PostgresNitro0.649s (+4.0%)1.006s (~)0.006s (+44.7% 🔺)1.025s (~)0.376s591.08x
💻 LocalNext.js (Turbopack)0.672s1.012s0.010s1.025s0.354s591.12x
💻 LocalNitro0.684s (-18.5% 🟢)1.011s (~)0.009s (~)1.115s (~)0.431s541.14x
💻 LocalExpress2.926s (+286.5% 🔺)3.319s (+222.6% 🔺)0.031s (+232.8% 🔺)3.640s (+250.1% 🔺)0.714s184.86x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express3.914s (-39.8% 🟢)5.513s (-31.2% 🟢)0.677s (+65.6% 🔺)6.617s (-25.1% 🟢)2.703s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Next.js (Turbopack)0.884s1.053s0.000s1.060s0.176s571.00x
🐘 PostgresExpress0.964s (~)1.192s (-6.7% 🟢)0.000s (-8.0% 🟢)1.208s (-7.5% 🟢)0.244s501.09x
🐘 PostgresNitro1.033s (+6.6% 🔺)1.520s (+21.8% 🔺)0.000s (+20.0% 🔺)1.535s (+22.1% 🔺)0.502s401.17x
💻 LocalNitro1.180s (-3.5%)2.018s (~)0.000s (+300.0% 🔺)2.020s (~)0.840s301.33x
💻 LocalNext.js (Turbopack)1.276s2.021s0.000s2.024s0.748s301.44x
💻 LocalExpress6.982s (+470.0% 🔺)6.982s (+245.6% 🔺)0.001s (+333.3% 🔺)7.553s (+273.5% 🔺)0.571s97.90x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express2.910s (-22.2% 🟢)4.343s (-14.9% 🟢)0.000s (-100.0% 🟢)4.822s (-12.8% 🟢)1.912s131.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

fan-out fan-in 10 streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.781s (~)2.221s (+2.0%)0.000s (NaN%)2.231s (+1.5%)0.451s271.00x
🐘 PostgresNext.js (Turbopack)1.788s2.106s0.000s2.113s0.325s291.00x
🐘 PostgresNitro1.850s (+3.3%)2.142s (~)0.000s (+100.0% 🔺)2.153s (-1.0%)0.303s281.04x
💻 LocalNitro3.481s (+2.8%)4.033s (~)0.000s (-25.0% 🟢)4.036s (~)0.555s151.95x
💻 LocalNext.js (Turbopack)3.673s4.233s0.001s4.237s0.563s152.06x
💻 LocalExpress9.983s (+187.9% 🔺)10.586s (+162.5% 🔺)0.002s (+87.5% 🔺)10.596s (+162.5% 🔺)0.613s65.61x

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Express4.217s (-8.1% 🟢)5.721s (-5.0%)0.000s (NaN%)6.376s (-1.3%)2.159s101.00x
▲ VercelNext.js (Turbopack)⚠️missing-----
▲ VercelNitro⚠️missing-----

🔍 Observability: Express

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNitro18/21
🐘 PostgresNext.js (Turbopack)14/21
▲ VercelExpress21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres19/21
Next.js (Turbopack)🐘 Postgres17/21
Nitro🐘 Postgres15/21
Column Definitions
  • Workflow Time: Runtime reported by workflow (completedAt - createdAt) - primary metric
  • TTFB: Time to First Byte - time from workflow start until first stream byte received (stream benchmarks only)
  • Slurp: Time from first byte to complete stream consumption (stream benchmarks only)
  • Wall Time: Total testbench time (trigger workflow + poll for result)
  • Overhead: Testbench overhead (Wall Time - Workflow Time)
  • Samples: Number of benchmark iterations run
  • vs Fastest: How much slower compared to the fastest configuration for this benchmark

Worlds:

  • 💻 Local: In-memory filesystem world (local development)
  • 🐘 Postgres: PostgreSQL database world (local development)
  • ▲ Vercel: Vercel production/preview deployment
  • 🌐 Turso: Community world (local development)
  • 🌐 MongoDB: Community world (local development)
  • 🌐 Redis: Community world (local development)
  • 🌐 Jazz: Community world (local development)

📋 View full workflow run

@TooTallNateTooTallNate left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Three coherent fixes wrapped into one PR — vitest project-context isolation, world-local recovery scoped by tag, and Windows-specific dev-test cleanup hardening. Verified each piece independently.

Vitest project-context isolation

The shape is right: WorkflowTestOptions resolves to a ResolvedWorkflowTestOptions shape (cwd / rootDir / dataDir / outDir, all absolute), gets passed via Vitest's provide/inject (per-project context), and is consumed by both global-setup.ts and setup-file.ts. No process env vars touched. The new test confirms process.env.WORKFLOW_VITEST_* stays clean.

resolveWorkflowTestOptions is idempotent (resolving an already-resolved object is a no-op since path.resolve of an absolute path returns itself), so the double-resolution from readProvidedWorkflowTestOptions is safe.

Path resolution: cwd defaults to process.cwd(); rootDir defaults to cwd (resolved relative to cwd if provided); dataDir/outDir default to <rootDir>/.workflow-data and <rootDir>/.workflow-vitest (resolved relative to cwd if provided). Consistent with the docs table.

Recovery scoped by tag

The bug existed: on main, createLocalWorld() calls createStorage()twice (once stored as storage, once spread into the world). They're separate instances with separate instrumentObject wrappers. Recovery (storage.runs) wouldn't see updates that consumers made through the spread instance, and adding a fileIdFilter wrapper to storage.runs wouldn't be visible to consumers. The PR collapses this to a single storage and uses ...storage for the spread, fixing the latent duplication.

The new LocalListWorkflowRunsParams extends the public ListWorkflowRunsParams with an internal fileIdFilter. Kept off the public Storage['runs']['list'] surface via a separate LocalRunsStorage type, with LocalStorage structurally assignable to Storage. This is the right way to add a backend-private field without leaking it to @workflow/world consumers.

The paginatedFileSystemQuery change is the load-bearing piece: filteredFileIds is now applied to both the no-cursor branch and the cursor-paginated branch (via candidateFileIds = filteredFileIds). Test "keeps fileIdFilter applied on later cursor pages" exercises this directly with multiple ULID-stamped files, verifying that page 2 still respects the filter.

recoverActiveRuns: false opt-out for the vitest harness is the right control: tests register direct handlers AFTER setupWorkflowTests() starts, and stale runs in the data dir would otherwise be dispatched before handlers are wired up. The reordering of world.start() to happen afterworld.registerHandler() (with the inline comment explaining the future hazard if anyone re-enables recovery) is good defensive practice.

The new hasTag(fileId, tag) helper is a clean primitive — wrun_ABC.vitest-0 matches vitest-0, untagged fileIds never match. Three new tests cover: tag isolation between two workers, tag filtering across pagination (25 tagged + 5 untagged + 5 differently-tagged runs), and recovery skip when recoverActiveRuns: false.

Dev-test cleanup hardening

These are Windows-specific Turbopack flake fixes, well-explained by inline comments at every behavior change:

  • afterEach reorders restore-then-delete to avoid leaving the dev server with broken imports between deletes (Turbopack on Windows caches the parse failure).
  • The "imported step hot-reload" test catches triggerWorkflowRun failures during polling, rewrites the api file to invalidate Turbopack's cache, and continues polling. Conservative — only rewrites on error, doesn't bail.
  • The "discovered via workflow imports" test does in-test cleanup (instead of afterEach) so the deferred builder has time to drop the discovered step before the next test file's dev server requests start failing.

Timeout was bumped 30s → 60s on the third one. Reasonable for the additional polling.

Test verification

Ran on the PR branch:

  • pnpm test in packages/vitest: 5 new tests pass.
  • pnpm test in packages/world-local: all 339 tests pass (including the new pagination filter test and the three tag-recovery tests).

Incidental cleanup

vitest moves from a non-listed-but-locked dependency to a proper devDep. Consumers continue to resolve it via the existing peerDependencies (vitest: >=3.0.0). No surface change for users of @workflow/vitest.

Nits (non-blocking)

The two changesets swift-cobras-repair.md and moody-rivers-play.md have somewhat overlapping content — one's the package-version bumps, the other's the empty-changeset note about the dev-test cleanup. Slightly confusing to have both, but it preserves attribution from the original PR(s) so probably worth keeping.

@VaguelySerious
VaguelySerious merged commit 2f52d14 into mainMay 2, 2026
156 of 164 checks passed
@VaguelySerious
VaguelySerious deleted the peter/local-world-isolation branch May 2, 2026 10:30
@github-actionsgithub-actionsBot mentioned this pull request May 2, 2026
@pranaygppranaygp added the backport-stable Cherry-pick this PR to the stable branch when merged label May 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Backport to stable failed — the cherry-pick had conflicts that could not be resolved automatically.

To resolve manually:

git fetch origin stable
git checkout stable
git cherry-pick 2f52d14f3844c999f6b89baeb8e04289d6dd34a9
# Fix conflicts, then:
git cherry-pick --continue
git push origin stable

pranaygp added a commit that referenced this pull request May 2, 2026
…ignal
* origin/main:
[vitest] [world-local] Fix local-world data recovery isolation (#1895)
ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources (#1882)
Add additional tests for event consumer fixes for hook/sleep/step race conditions (#1528)
# Conflicts:
#	packages/core/src/workflow.test.ts
pranaygp added a commit that referenced this pull request May 2, 2026
…lier-errors-followups
* origin-https/main:
[vitest] [world-local] Fix local-world data recovery isolation (#1895)
ci: switch Vercel deployment-protection bypass to OIDC Trusted Sources (#1882)
Add additional tests for event consumer fixes for hook/sleep/step race conditions (#1528)
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-stableCherry-pick this PR to the stable branch when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

@VaguelySerious@TooTallNate@pranaygp