Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', '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('^' + ".*" + ' Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, '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" + ' Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, '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('^' + ".*" + ' Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious
, '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); } })(); })(); Fix Vitest project isolation and paginated recovery filtering by tomdale · Pull Request #1818 · vercel/workflow · GitHub
Skip to content

Fix Vitest project isolation and paginated recovery filtering - #1818

Closed
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation
Closed

Fix Vitest project isolation and paginated recovery filtering#1818
tomdale wants to merge 7 commits into
mainfrom
tomdale/local-world-isolation

Conversation

@tomdale

Copy link
Copy Markdown
Contributor

Summary

Before this change, @workflow/vitest passed custom test directories through process-wide env vars, so Vitest workspace projects and config reloads could leak one project's paths into another. world-local also added tagged recovery for Vitest workers, but its paginated filesystem query dropped the tag filter after the first page, which could re-enqueue runs owned by other workers in shared local data.

This branch moves the Vitest harness to project-scoped provided context. workflow() resolves cwd, rootDir, dataDir, and outDir once, passes them through Vitest's per-project context, and global-setup plus setup-file read that resolved state when building bundles and creating the local test world. The package metadata and lockfile now also declare the Vitest test dependency and script used by the package.

On the recovery side, world-local now keeps fileIdFilter applied while paginating and uses that path to scope startup recovery to the active tag. It also 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.

Testing

  • 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

Scope local-world startup recovery to the active tag so Vitest workers do not re-enqueue runs owned by other workers. Also add an opt-out for recovery, disable it in the Vitest harness, and allow callers to override the test data and bundle directories.
Add regression coverage for tagged recovery, recovery opt-out, and custom Vitest directory configuration.
@tomdale
tomdale requested a review from a team as a code ownerApril 21, 2026 16:24
@changeset-bot

changeset-botBot commented Apr 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c9282a0

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 Apr 21, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 21, 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🥇 Next.js (Turbopack)0.040s1.005s0.965s101.00x
💻 LocalNitro0.042s (-3.0%)1.005s (~)0.963s101.06x
💻 LocalExpress0.043s (-3.8%)1.005s (~)0.963s101.08x
🐘 PostgresNitro0.057s (-40.3% 🟢)1.009s (-3.2%)0.952s101.43x
🐘 PostgresExpress0.060s (+3.3%)1.013s (~)0.953s101.51x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro0.187s (-54.4% 🟢)1.728s (-31.1% 🟢)1.541s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 1 step

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)1.100s2.005s0.904s101.00x
🐘 PostgresExpress1.117s (-2.6%)2.009s (~)0.892s101.02x
💻 LocalNitro1.129s (~)2.005s (~)0.876s101.03x
💻 LocalExpress1.140s (+1.3%)2.005s (~)0.865s101.04x
🐘 PostgresNitro1.141s (~)2.010s (~)0.869s101.04x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.054s (-47.2% 🟢)3.742s (-36.7% 🟢)1.688s101.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)10.636s11.024s0.388s31.00x
🐘 PostgresExpress10.719s (-2.2%)11.018s (~)0.299s31.01x
🐘 PostgresNitro10.912s (~)11.019s (~)0.108s31.03x
💻 LocalNitro10.944s (~)11.024s (~)0.080s31.03x
💻 LocalExpress10.968s (~)11.023s (~)0.055s31.03x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro17.144s (-27.8% 🟢)19.085s (-24.0% 🟢)1.941s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)14.152s15.027s0.875s41.00x
🐘 PostgresExpress14.161s (-2.9%)15.019s (~)0.858s41.00x
🐘 PostgresNitro14.541s (~)15.022s (~)0.481s41.03x
💻 LocalNitro15.001s (~)15.280s (-4.7%)0.280s41.06x
💻 LocalExpress15.069s (+0.7%)15.280s (+1.7%)0.212s41.06x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro31.465s (-51.2% 🟢)33.025s (-50.4% 🟢)1.560s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express13.257s (-5.4% 🟢)14.019s (-3.9%)0.762s71.00x
🐘 PostgresNitro13.815s (-1.1%)14.018s (-2.0%)0.203s71.04x
💻 LocalNext.js (Turbopack)14.776s15.028s0.252s61.11x
💻 LocalNitro16.737s (~)17.030s (~)0.293s61.26x
💻 LocalExpress16.768s (+1.0%)17.032s (~)0.264s61.26x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro55.666s (-86.8% 🟢)57.243s (-86.5% 🟢)1.577s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.217s (-3.5%)2.007s (~)0.790s151.00x
🐘 PostgresNitro1.265s (-0.7%)2.010s (~)0.744s151.04x
💻 LocalNext.js (Turbopack)1.443s2.006s0.563s151.19x
💻 LocalExpress1.494s (~)2.005s (~)0.511s151.23x
💻 LocalNitro1.569s (-3.9%)2.006s (-3.3%)0.437s151.29x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.420s (-14.1% 🟢)4.175s (-3.4%)1.755s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.284s (-3.3%)3.009s (~)0.725s101.00x
🐘 PostgresNitro2.325s (-1.1%)3.009s (~)0.684s101.02x
💻 LocalNext.js (Turbopack)2.613s3.007s0.394s101.14x
💻 LocalExpress3.132s (+6.1% 🔺)3.884s (+12.5% 🔺)0.752s81.37x
💻 LocalNitro3.199s (+1.8%)3.886s (~)0.687s81.40x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.537s (-37.4% 🟢)4.797s (-19.0% 🟢)2.260s71.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.all with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.395s (-2.6%)4.008s (~)0.613s81.00x
🐘 PostgresNitro3.466s (~)4.010s (~)0.545s81.02x
💻 LocalNext.js (Turbopack)6.649s7.013s0.364s51.96x
💻 LocalExpress8.457s (+1.4%)9.024s (~)0.567s42.49x
💻 LocalNitro8.652s (+3.6%)9.020s (~)0.367s42.55x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.816s (-20.1% 🟢)4.501s (-18.7% 🟢)1.686s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 10 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.202s (-4.4%)2.007s (~)0.805s151.00x
🐘 PostgresNitro1.262s (~)2.008s (~)0.746s151.05x
💻 LocalNext.js (Turbopack)1.458s2.005s0.547s151.21x
💻 LocalExpress1.522s (-19.6% 🟢)2.006s (-15.1% 🟢)0.484s151.27x
💻 LocalNitro1.608s (-13.8% 🟢)2.073s (-11.4% 🟢)0.465s151.34x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.193s (-10.8% 🟢)4.144s (-0.6%)1.951s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 25 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express2.257s (-3.6%)3.009s (~)0.752s101.00x
🐘 PostgresNitro2.339s (~)3.011s (~)0.672s101.04x
💻 LocalNext.js (Turbopack)2.719s3.107s0.388s101.20x
💻 LocalNitro2.993s (-2.4%)3.885s (~)0.892s81.33x
💻 LocalExpress2.994s (-4.4%)3.760s (~)0.766s81.33x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.159s (-33.2% 🟢)3.780s (-25.5% 🟢)1.621s81.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Promise.race with 50 concurrent steps

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.362s (-3.9%)4.009s (~)0.647s81.00x
🐘 PostgresNitro3.477s (~)4.010s (~)0.534s81.03x
💻 LocalNext.js (Turbopack)7.559s8.017s0.458s42.25x
💻 LocalExpress8.670s (-1.5%)9.274s (~)0.604s42.58x
💻 LocalNitro8.842s (-3.3%)9.026s (-10.0% 🟢)0.184s42.63x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.881s (-43.4% 🟢)5.010s (-26.5% 🟢)2.129s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.657s (-21.7% 🟢)1.006s (-1.7%)0.349s601.00x
💻 LocalNext.js (Turbopack)0.674s1.004s0.330s601.03x
🐘 PostgresNitro0.805s (-1.8%)1.006s (~)0.200s601.23x
💻 LocalExpress0.994s (+1.0%)1.225s (+13.8% 🔺)0.231s501.51x
💻 LocalNitro1.116s (+13.8% 🔺)1.424s (+30.2% 🔺)0.308s431.70x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro9.164s (-58.4% 🟢)11.145s (-53.6% 🟢)1.981s61.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express1.575s (-20.3% 🟢)2.007s (-11.1% 🟢)0.431s451.00x
🐘 PostgresNitro1.867s (-3.1%)2.029s (-3.4%)0.163s451.19x
💻 LocalNext.js (Turbopack)2.125s3.007s0.882s301.35x
💻 LocalExpress3.027s (~)3.649s (+1.8%)0.622s251.92x
💻 LocalNitro3.052s (+0.5%)3.729s (-0.8%)0.678s251.94x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro25.489s (-35.4% 🟢)27.415s (-33.6% 🟢)1.926s41.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 sequential data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express3.233s (-19.0% 🟢)4.008s (-8.3% 🟢)0.774s301.00x
🐘 PostgresNitro3.782s (-7.8% 🟢)4.008s (-13.0% 🟢)0.225s301.17x
💻 LocalNext.js (Turbopack)7.014s7.514s0.500s162.17x
💻 LocalExpress9.235s (~)9.941s (-0.8%)0.706s132.86x
💻 LocalNitro9.304s (~)10.019s (~)0.715s122.88x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro68.716s (-29.1% 🟢)70.793s (-28.1% 🟢)2.077s21.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 10 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.224s (-20.5% 🟢)1.007s (~)0.782s601.00x
🐘 PostgresNitro0.285s (+0.7%)1.007s (~)0.722s601.27x
💻 LocalNext.js (Turbopack)0.554s1.004s0.450s602.47x
💻 LocalNitro0.610s (+0.8%)1.005s (-1.6%)0.395s602.72x
💻 LocalExpress0.689s (+22.9% 🔺)1.116s (+11.1% 🔺)0.427s543.07x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.509s (-9.2% 🟢)3.110s (-7.2% 🟢)1.601s201.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 25 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.396s (-22.3% 🟢)1.005s (~)0.609s901.00x
🐘 PostgresNitro0.485s (-2.4%)1.006s (~)0.521s901.22x
💻 LocalNext.js (Turbopack)2.442s3.008s0.565s306.16x
💻 LocalNitro2.648s (+4.3%)3.010s (~)0.362s306.68x
💻 LocalExpress2.686s (+6.9% 🔺)3.077s (+2.3%)0.391s306.78x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.050s (-5.5% 🟢)4.903s (+1.7%)1.853s191.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

workflow with 50 concurrent data payload steps (10KB)

💻 Local Development

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.609s (-25.6% 🟢)1.005s (-1.2%)0.397s1201.00x
🐘 PostgresNitro0.795s (+0.6%)1.025s (+1.7%)0.230s1181.31x
💻 LocalNext.js (Turbopack)9.247s9.866s0.619s1315.19x
💻 LocalExpress11.134s (-0.5%)11.666s (-2.3%)0.533s1118.29x
💻 LocalNitro11.448s (+2.3%)12.030s (+3.1%)0.582s1018.80x
🐘 PostgresNext.js (Turbopack)⚠️missing----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro6.682s (-13.5% 🟢)8.514s (-9.4% 🟢)1.832s151.00x
▲ VercelExpress⚠️missing----
▲ VercelNext.js (Turbopack)⚠️missing----

🔍 Observability: Nitro

Stream Benchmarks(includes TTFB metrics)
workflow with stream

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
💻 Local🥇 Next.js (Turbopack)0.131s1.002s0.009s1.015s0.884s101.00x
🐘 PostgresExpress0.169s (-17.5% 🟢)1.000s (~)0.001s (-56.3% 🟢)1.008s (~)0.839s101.30x
💻 LocalExpress0.202s (+1.4%)1.004s (~)0.013s (+3.3%)1.019s (~)0.817s101.55x
🐘 PostgresNitro0.205s (~)0.994s (-0.6%)0.001s (-6.7% 🟢)1.009s (~)0.805s101.57x
💻 LocalNitro0.205s (-4.2%)1.004s (~)0.013s (~)1.019s (~)0.814s101.57x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro1.744s (-54.5% 🟢)3.047s (-42.3% 🟢)1.884s (+153.9% 🔺)5.363s (-17.3% 🟢)3.619s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

stream pipeline with 5 transform steps (1MB)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Express0.560s (-11.1% 🟢)1.040s (+3.4%)0.003s (-18.0% 🟢)1.054s (+3.1%)0.494s571.00x
🐘 PostgresNitro0.605s (-3.1%)1.005s (~)0.004s (+4.9%)1.022s (~)0.417s591.08x
💻 LocalNext.js (Turbopack)0.666s1.010s0.009s1.115s0.448s541.19x
💻 LocalExpress0.745s (-1.6%)1.012s (-1.6%)0.008s (-10.9% 🟢)1.022s (-1.7%)0.277s591.33x
💻 LocalNitro0.865s (+3.2%)1.012s (~)0.012s (+25.1% 🔺)1.119s (~)0.253s541.55x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.650s (-87.6% 🟢)5.236s (-83.0% 🟢)0.507s (+352.4% 🔺)6.181s (-80.5% 🟢)2.531s101.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

10 parallel streams (1MB each)

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro0.967s (~)1.165s (-6.7% 🟢)0.000s (+84.6% 🔺)1.181s (-6.1% 🟢)0.213s521.00x
🐘 PostgresExpress0.987s (+2.8%)1.181s (-7.6% 🟢)0.000s (-100.0% 🟢)1.244s (-4.8%)0.257s491.02x
💻 LocalNext.js (Turbopack)1.188s2.016s0.000s2.018s0.831s301.23x
💻 LocalExpress1.231s (+0.5%)2.021s (~)0.000s (+20.0% 🔺)2.023s (~)0.791s301.27x
💻 LocalNitro1.293s (+5.8% 🔺)2.022s (~)0.000s (+133.3% 🔺)2.024s (~)0.731s301.34x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro2.953s (-3.2%)4.496s (+2.4%)0.000s (+100.0% 🔺)4.957s (+3.1%)2.004s131.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

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

💻 Local Development

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
🐘 Postgres🥇 Nitro1.785s (~)2.173s (+1.5%)0.000s (~)2.217s (+2.0%)0.432s281.00x
🐘 PostgresExpress1.860s (+4.9%)2.399s (+10.2% 🔺)0.000s (+Infinity% 🔺)2.427s (+10.4% 🔺)0.568s251.04x
💻 LocalExpress3.423s (-1.3%)4.035s (~)0.001s (-8.3% 🟢)4.038s (~)0.615s151.92x
💻 LocalNext.js (Turbopack)3.483s4.031s0.000s4.034s0.551s151.95x
💻 LocalNitro3.804s (+12.3% 🔺)4.392s (+8.9% 🔺)0.000s (-46.4% 🟢)4.394s (+8.9% 🔺)0.590s142.13x
🐘 PostgresNext.js (Turbopack)⚠️missing-----

▲ Production (Vercel)

WorldFrameworkWorkflow TimeTTFBSlurpWall TimeOverheadSamplesvs Fastest
▲ Vercel🥇 Nitro3.811s (-6.9% 🟢)5.234s (-2.6%)0.000s (-100.0% 🟢)5.655s (-2.4%)1.844s111.00x
▲ VercelExpress⚠️missing-----
▲ VercelNext.js (Turbopack)⚠️missing-----

🔍 Observability: Nitro

Summary

Fastest Framework by World

Winner determined by most benchmark wins

World🥇 Fastest FrameworkWins
💻 LocalNext.js (Turbopack)20/21
🐘 PostgresExpress18/21
▲ VercelNitro21/21
Fastest World by Framework

Winner determined by most benchmark wins

Framework🥇 Fastest WorldWins
Express🐘 Postgres20/21
Next.js (Turbopack)💻 Local21/21
Nitro🐘 Postgres16/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

@github-actions

github-actionsBot commented Apr 21, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ ▲ Vercel Production9991671067
✅ 💻 Local Development10780861164
✅ 📦 Local Production10780861164
✅ 🐘 Local Postgres10780861164
✅ 📋 Other273018291
Total450613434850

❌ Failed Tests

▲ Vercel Production (1 failed)

nextjs-webpack (1 failed):

  • runClassSerializationWorkflow - Run instances serialize across workflow/step boundaries | wrun_01KQK8A7TEB02RGAKJYWKS09K7 | 🔍 observability

Details by Category

❌ ▲ Vercel Production
AppPassedFailedSkipped
✅ astro9007
✅ example9007
✅ express9007
✅ fastify9007
✅ hono9007
✅ nextjs-turbopack9502
❌ nextjs-webpack9412
✅ 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
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable9106
✅ e2e-local-postgres-nest-stable9106
✅ e2e-local-prod-nest-stable9106

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: success
  • Local Prod: success
  • Local Postgres: success
  • Windows: failure

Check the workflow run for details.

@VaguelySeriousVaguelySerious 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.

AI review: blocking issues found

Comment threadpackages/vitest/src/index.test.ts Outdated
path.join(os.tmpdir(), 'workflow-vitest-build-')
);
tempDirs.push(rootDir);
const cwd = '/repo/app';

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.

AI Review: Blocking

The new tests in this file hardcode POSIX-style absolute paths (/repo/app, /tmp/workflow-vitest-root, etc.) and compare them directly against the output of resolve(...) inside options.ts. On Windows, path.resolve('/repo/app') expands to the current drive (e.g. D:\repo\app), so the assertions diverge and the test file fails.

This is why Unit Tests (windows-latest) is failing on this PR (4 failed in src/index.test.ts) while it passes on main. Lines that bleed into the flake: 108 (cwd), 186 (cwd), 226–229 / 236–239 / 265–268 (the __workflowVitestOptions objects passed to global-setup and setup-file).

Either compute the expected values with path.resolve(...) / path.join(...) at assertion time, or anchor the fixtures to os.tmpdir() (the filesystem-touching tests above already do this). Without that, the Windows unit-test lane stays red.

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.

(AI) Fixed in aecde1a — the test fixtures now pass absolute paths through path.resolve(...) / path.join(...), so the expected values match the internal resolve() output on both POSIX and Windows.

* Directory for generated workflow and step bundles.
* Defaults to `<rootDir>/.workflow-vitest`.
*/
outDir?: string;

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.

AI Review: Blocking

This adds three new user-facing options to WorkflowTestOptions (rootDir, dataDir, outDir), and the workflow() signature below now accepts a WorkflowTestOptions argument — previously it took none. The reference docs under docs/content/docs/api-reference/vitest/index.mdx still document only cwd on WorkflowTestOptions and still show workflow() invoked with no arguments.

Per the repo convention that user-facing changes in vercel/workflow are documented in docs, please update that page to: (1) add rootDir, dataDir, outDir to the WorkflowTestOptions table, noting how relative paths resolve against cwd and the <rootDir>/.workflow-data / <rootDir>/.workflow-vitest defaults; (2) mention that workflow() accepts an optional WorkflowTestOptions and show a usage snippet for a workspace with a non-standard layout — that is the motivating use case for this PR, so it's worth surfacing.

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.

(AI) Fixed in aecde1a — the @workflow/vitest reference page now documents rootDir, dataDir, outDir with their resolution rules and default locations, notes that workflow() accepts an optional WorkflowTestOptions, and shows a usage snippet for a non-standard layout.

Comment threadpackages/world-local/src/index.ts Outdated
storage.runs.list({
...params,
fileIdFilter: (fileId: string) => hasTag(fileId, tag),
} as any)) as typeof storage.runs.list,

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.

AI Review: Nit

This wrapper re-implements storage.runs.list with two as any casts (on params and on the call itself) and therefore bypasses the outer instrumentObject('world.runs', ...) span for the recovery call — the inner storage.runs.list(...) call is still instrumented, so telemetry is preserved, but the shape is awkward.

A cleaner alternative would be to plumb fileIdFilter through an internal extension of ListWorkflowRunsParams (still kept out of the public Storage['runs']['list'] surface) so the wrapper can drop the double as any. Not blocking — the code is internal and correct — just flagging for future cleanup.

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.

(AI) Addressed in aecde1a — plumbed fileIdFilter through a local-only LocalListWorkflowRunsParams (and LocalRunsStorage / LocalStorage) that structurally assigns to Storage['runs'] at the reenqueueActiveRuns boundary. The recovery wrapper in createLocalWorld no longer needs the double as any, and the option stays off the public @workflow/world surface.

world = createLocalWorld({
dataDir: join(cwd, '.workflow-data'),
dataDir,
recoverActiveRuns: false,

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.

AI Review: Note

Good call registering handlers before world.start?.() — defensive against any future change where recoverActiveRuns gets re-enabled here (or a caller of setupWorkflowTests passes it through). Since the harness hardcodes recoverActiveRuns: false today the ordering is defensive, not load-bearing; a short comment noting that register-before-start is intentional would help prevent a later cosmetic reorder from silently reintroducing the race this PR just fixed.

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.

(AI) Added an inline comment in aecde1a noting that register-before-start is intentional and that reordering would reintroduce the race if recoverActiveRuns is ever re-enabled or passed through by a caller.

@VaguelySerious

Copy link
Copy Markdown
Member

I like the approach overall and would love to ship this after fixing the blocking issues

- Use path.resolve() in vitest test fixtures so assertions match on Windows
- Document rootDir/dataDir/outDir and workflow() options in docs
- Note intent of register-before-start ordering in setupWorkflowTests
- Plumb fileIdFilter via internal LocalListWorkflowRunsParams type to
drop the double `as any` on the recovery wrapper
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Signed-off-by: Peter Wielander <mittgfu@gmail.com>
…next test
The 'should include steps discovered from workflow imports' test creates a
workflow + step file pair, lets the deferred builder discover them, and
restores cleanup via afterEach. Since #1796 the generated step route bundle
imports the discovered step source directly (no copy), so when the source is
deleted the bundle is left with a literal `import '../workflows/discovered-via-workflow-step.ts'`
until the deferred builder rebuilds and `filterExistingFiles` prunes it.
On Windows the rebuild can lag the deletion, leaving the step route bundle
unable to compile for the next test file (which shares the same dev server).
Every step request then returns 500 and the e2e suite hangs until the 30-min
job timeout. The bug was masked on main by an earlier dev test failure that
short-circuits the suite.
Tear the test down in-test instead of via afterEach: restore the api file,
delete the workflow + step files, and poll the manifest until the discovered
step is gone before returning. Also reorder afterEach to restore content
before deleting files, so the dev server never sees an api import pointing
at a missing module.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@tomdale@VaguelySerious