Files
vercel__workflow/e2e-diagnostics-nextjs-turbopack-vercel.json
T
Nathan Rajlich 9da2d76260 [core][world][world-vercel] Add World.createRunId() and region-aware queue routing (#1981)
* [world-vercel] Add /run-id sub-export with tagged ULID encode/decode

Encodes a tag bit, 5-bit version, and 6-bit Vercel region ID into a
ULID-shaped string used for workflow run IDs. Tagged values remain
valid 26-char Crockford-Base32 ULIDs so they still sort and round-trip
through any system that accepts ULIDs.

* [world-vercel] Add string-value assertions to run-id tests

Add exact-string expectations for encoded outputs at known inputs,
covering the default region/version pair, numeric region IDs, version
overrides, boundary values (all-zero, all-max), the dirty-input
overwrite case, and the lexicographic-order checks. Also adds an
explicit byte-array expectation for the canonical ULID-spec example
string and an additional first-char-range coverage test for isTagged.

* [world-vercel] Remove internal-repo reference from regions doc comment

* [world-vercel] Address PR review feedback on run-id sub-export

- isTaggedString now fully validates the input as a 26-char Crockford
  Base32 ULID (delegating to ulidToBytes) instead of only inspecting
  the first character. This fixes false positives on inputs like
  '4UUUU...' that have a valid tag-bit position but invalid chars
  later in the string.
- isTagged() now accepts `unknown` to match its documented behavior
  of safely rejecting non-string inputs without requiring callers to
  cast.
- Introduce `RegionKey` for the full set of keys including 'unknown',
  and narrow `RegionCode` to `Exclude<RegionKey, 'unknown'>` so the
  return type of `lookupRegion` and the `DecodedRunId.region` field
  accurately reflect that 'unknown' is never produced. Updates
  `encode` to reject 'unknown' as a region code string at runtime
  (callers wanting the unknown sentinel should pass numeric 0).

* [world] [core] [world-vercel] Add World.createRunId() and region-aware queue routing

- @workflow/world: add optional createRunId(input?) to the World
  interface so worlds can mint run IDs with embedded metadata, and
  add an optional 'region' field to QueueOptions for per-message
  routing hints.

- @workflow/core: start() now delegates run ID generation to
  world.createRunId() when defined (falling back to a monotonic
  ULID otherwise), and accepts a new 'runIdInput' option that is
  forwarded verbatim to createRunId. When runIdInput.region is a
  string, it is also threaded onto the queue options so the initial
  workflow message is dispatched to the matching region.

- @workflow/world-vercel: implement createRunId() to mint
  region-tagged ULIDs, preferring an explicit runIdInput.region and
  falling back to the VERCEL_REGION env var. The queue now resolves
  its destination region from (in order): an explicit opts.region,
  the region embedded in the payload's tagged run ID, the
  VERCEL_REGION env var, and finally a hardcoded 'iad1' default.
  This replaces the previous unconditional 'iad1' region passed to
  the @vercel/queue client.

Monotonicity within a process is preserved by tracking the last
emitted run ID and bumping the bit immediately above the 11-bit
metadata window when a same-ms collision would otherwise occur,
then re-stamping the requested region/version on top so metadata
remains stable.

* [core] [world] [world-vercel] Pass full StartOptions to World.createRunId

Drop the dedicated 'runIdInput' field on StartOptions and forward the
entire options bag to world.createRunId() instead. This keeps the
public API surface smaller and lets each World pick the fields it
recognises (e.g. world-vercel reads 'region'). The top-level 'region'
option remains on StartOptionsBase and is also threaded onto the
queue's per-call region opt when set.

* Address review feedback: doc fixes and deterministic same-ms tests

- Document the final iad1 fallback in QueueOptions.region (world)
- Correct the World.createRunId doc: start() always passes an object
- Fix the clientOptions comment: the handler client omits region and
  relies on SDK auto-detection + the ce-vqsregion header for acks
- Fix a misleading QueueClient-construction comment in queue.test.ts
- Freeze time in the same-ms monotonicity test so it deterministically
  exercises the intended path, and add a test covering the
  bump-above-metadata fallback when the region changes mid-millisecond

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: keep workflow-server override rewrite-compatible

Export WORKFLOW_SERVER_URL_OVERRIDE while keeping the one-line const shape
that workflow-server's cross-repo e2e test automation rewrites. Update
world-vercel tests to import that exported value for mock origins and URL
expectations instead of duplicating the temporary preview URL.

* fix(world): clear region tag bit before ULID timestamp validation

Region-tagged run IDs set the high bit of the ULID timestamp byte. The
shared world timestamp validator used raw decodeTime(), so current tagged
run IDs appeared thousands of years in the future and were rejected before
reaching workflow-server. Clear the tag bit before decoding, matching the
workflow-server behavior, and cover tagged IDs in tests.

* fix(world-vercel): validate tagged runId timestamps via run-id decode

Keep @workflow/world's ULID helpers generic; they should not know about
world-vercel's region-tagged run ID layout. Instead, world-vercel decodes
its tagged runId to the original ULID before using the shared timestamp
validator for run_created events. Add a world-vercel regression test that a
current sfo1-tagged runId passes validation.

* fix(world-vercel): default run ID region to iad1 instead of unknown

When neither an explicit region option nor VERCEL_REGION is available,
createRunId minted a tagged ULID with the unknown (0) region sentinel,
producing the tagged: true, region: null state. The server already
resolves unknown/untagged runs to DEFAULT_VERCEL_REGION (iad1), so mint
a concrete iad1 tag instead, keeping every run ID self-describing and
routable.

* test(e2e): use verbose reporter + per-test start heartbeat

The default vitest reporter buffers per-file output, so a stalling e2e
test produces no output until its timeout — making CI look like a silent
30-minute hang. Switch the e2e CI invocations to the verbose reporter
(prints each test result as it completes) and emit a '[e2e] ▶ start:'
heartbeat to stdout at the start of every test (bypassing vitest's console
buffering) so a stuck test is immediately identifiable in the live CI log.

* test(world-vercel): point WORKFLOW_SERVER_URL_OVERRIDE at combined 527+529 preview

Temporarily target the workflow-server combined-527-529-preview deployment,
which bundles platform-directed multi-region routing (vercel/workflow-server#527,
incl. the iad1 hook pin) and durable stream state (vercel/workflow-server#529),
so e2e can validate the full multi-region path end-to-end. Revert to empty on main.

* fix(core): region-tag the health-check correlationId

The health-check response is delivered over a Redis stream whose name (and
synthetic run ID) embed the correlationId. Under platform-directed routing
the responding endpoint and the polling reader can be served from different
physical regions; Redis is physical-region-local, so the correlationId must
carry the region for both sides to resolve the same backend.

Generate the correlationId via world.createRunId() (a region-tagged ULID)
when the world provides it, falling back to a plain ULID for worlds that
don't tag IDs (e.g. local, single-region). The synthetic wrun_hc_<id> run ID
then carries the region; workflow-server's region middleware decodes it.

* Address review feedback: validate region overrides, reset server override

- queue: validate opts.region and VERCEL_REGION against the known region
  table before routing, ignoring unrecognised codes so a bad override
  can't clobber the payload-derived region (Copilot)
- add isKnownRegionCode() runtime guard to run-id/regions
- reset WORKFLOW_SERVER_URL_OVERRIDE to '' (must be empty on main)
- fold the within-PR iad1-default changeset into the main world-vercel
  changeset and delete it (review)
- start.test: declare specVersion on createRunId mock worlds now that
  the merged world-compatibility check requires it
- cover the new region-validation fall-through paths in queue.test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(world-vercel): point WORKFLOW_SERVER_URL_OVERRIDE at wave-1 multi-region preview

BRANCH-ONLY — revert the override to '' before merge (lint enforces).

Points this PR's e2e/benchmark runs at the wave-1 multi-region
workflow-server preview (vercel/workflow-server#590: iad1+sfo1+fra1
serving, staging data backends) so region-tagged runs are validated
against real multi-region serving end-to-end.

Also makes the unit-test mock origins in events-v4.test.ts and
trace-propagation.test.ts override-aware (same pattern the rest of
the file and utils.test.ts already use), so the suite passes whether
or not the override is set — these two files were the only spots
hardcoding https://vercel-workflow.com.

* test(e2e): Vercel multi-region suite for start()'s region option

Adds a dedicated e2e suite validating @workflow/world-vercel region
routing end to end, run as its own CI job (e2e-vercel-multi-region)
against the nextjs-turbopack workbench only — deliberately separate
from e2e.test.ts, which runs as a matrix across all worlds/frameworks
where Vercel-specific multi-region behavior doesn't apply.

- workbench/nextjs-turbopack/vercel.json: deploy to iad1+sfo1+fra1 so
  region-routed flow messages have a function to land on in each region.
- workflows/99_e2e.ts: regionProbeWorkflow returns the VERCEL_REGION
  observed by both the workflow and a step, so tests can assert the run
  EXECUTED in the intended region (not just that it was tagged).
- packages/core/e2e/e2e-region.test.ts: per-region cases assert
  1) start(..., { region }) mints a region-tagged run ID (decoded via
     @workflow/world-vercel/run-id),
  2) the workflow + step both observed VERCEL_REGION === region,
  3) the server reports the run completed;
  plus a concurrent all-regions case guarding against cross-region
  misrouting under simultaneous multi-region traffic. Skips on local
  deployments.
- .github/workflows/tests.yml: new e2e-vercel-multi-region job
  mirroring e2e-vercel-prod's env/deployment-wait, running only the
  new suite.

* test(e2e): start region probes in-function; fix getWorld await

The first multi-region CI run surfaced two issues:

1. sfo1/fra1-tagged runs executed in iad1. The suite started runs from
   the external test process, which uses the api.vercel.com token proxy
   — and the proxy's queues path forwards every send to the region-less
   VQS host (the world's proxy-mode resolveBaseUrl ignores the region
   argument, and the proxy's x-vercel-vqs-api-url escape hatch only
   allowlists vqs-server-*.vercel.sh preview hosts). Production traffic
   publishes IN-FUNCTION (direct regional queue routing), so the suite
   now triggers start() through a new workbench route
   (/api/e2e-region-start) and rehydrates the run with getRun() —
   testing the path production actually takes. Proxy-mode regional
   queue routing is a known gap to address separately in api-workflow.

2. TypeError on world.runs.get: getWorld() is async and was called
   without await.

* test(e2e): cover explicit and implicit region starts in the multi-region suite

With regional VQS routing now working through the api.vercel.com proxy
(vercel/api#79056 + #2789 + this branch's per-send region resolution),
the suite covers both start configurations, asserting the same three
properties for each (region-tagged run ID, execution in the intended
region via VERCEL_REGION echoed in the return value, server-side
completion):

1. EXPLICIT: start(..., { region }) called directly in the vitest
   runner — publishes through the token proxy, per-send region carried
   by x-vercel-queue-region. Restores the direct-start shape the suite
   had originally, plus the concurrent all-regions case.

2. IMPLICIT: dedicated per-region workbench routes
   (/api/e2e-region-implicit/{iad1,sfo1,fra1}), each pinned to a single
   region via a per-function 'regions' entry in the workbench
   vercel.json, calling start() with NO region option — createRunId
   derives the tag from the minting function's VERCEL_REGION. The test
   also asserts the route reported executing in its pinned region, so
   the implicit-tagging assertion can't pass vacuously.

Replaces the interim /api/e2e-region-start route (explicit region via
request body), which existed to work around the pre-#79056 proxy gap.

* Revert WORKFLOW_SERVER_URL_OVERRIDE to '' — wave-1 multi-region serving is in production

workflow-server#590 (iad1+sfo1+fra1 serving) merged and deployed to
production and the e2e backend, so this branch's e2e/benchmark runs no
longer need to target the wave-1 preview. Restores the empty override
the No Test Overrides lint job enforces for merge.

The override-aware unit-test origins (events-v4/trace-propagation)
stay — they are correct under any override value.

* test(e2e): cross-region stream visibility (iad1 writer, sfo1 reader)

Regression coverage for a backend bug that made cross-region stream
reads report zero chunks on IN-PROGRESS streams (completed streams were
unaffected), which forced the multi-region serving rollback.

The new case exercises exactly that geometry:
- crossRegionStreamWorkflow (99_e2e.ts) writes N chunks to the default
  output stream, then holds the stream OPEN for 45s before closing —
  the in-progress window is the point, since completed streams are the
  easy case.
- The e2e starts it with region iad1, waits (same-region, via the
  api.vercel.com proxy) until all chunks are written, asserts the run
  is still 'running', then reads through a new sfo1-pinned workbench
  route (/api/e2e-stream-read/sfo1) that returns getTailIndex() plus
  its VERCEL_REGION. The reader's region served none of the stream's
  writes, so the reported chunk count must come from the backend's
  cross-region stream metadata. The test fails loudly if the route
  isn't actually executing in sfo1.

Also bumps the explicit-region test timeout to 120s: the first case in
the file absorbs every cold start at once (fresh workbench instances
in up to three regions plus a cold backend preview) and was observed
just over the 60s default.

BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE points at a multi-region backend preview
that includes the fix, so this validates cross-region stream
visibility end-to-end before multi-region serving is re-enabled.

* test(e2e): extend multi-region suite to all 19 provisioned regions

Points the suite at an all-regions backend preview and widens coverage
from the wave-1 trio to every provisioned region:

- Explicit path: a single concurrent all-regions case starts one
  tagged run per region (one shared cold-start window instead of 19
  sequential ones) and aggregates per-region failures so a single
  region's breakage reports alongside the full picture. The trio keeps
  its detailed per-region cases and the 9-way concurrent-isolation
  case.
- Implicit path: workbench gains a region-pinned
  /api/e2e-region-implicit/<region> route per provisioned region (19
  total, shared handler), the workbench itself now deploys to all of
  them, and the test.each covers the full set with per-case timeouts
  for regional cold starts.
- Multi-region CI job timeout 20m -> 35m for the sequential implicit
  cases.

BRANCH-ONLY (revert before merge, lint enforces):
WORKFLOW_SERVER_URL_OVERRIDE now targets the all-regions backend
preview instead of the previous (stale, since-merged) fix preview.

* test(e2e): tolerate geo-adjacent execution of queue callbacks

The first all-regions run surfaced a subtle execution-locality
behavior: queue delivery is guaranteed to the tagged region's
dataplane and the delivery callback egresses from that region, but the
consumer invocation's execution region is chosen by where that
callback enters Vercel's edge — and adjacent regions can geo-resolve
to each other's functions. Observed live: kix1-tagged runs (callback
egressing from Osaka) deterministically executing in hnd1/Tokyo on
both the explicit and implicit paths, with tagging, data placement,
and completion all still strictly kix1.

expectRunInRegion now asserts execution lands in the tagged region OR
one of its geographic neighbors (EXECUTION_ADJACENCY), while run-ID
tagging and server-side completion remain strictly the requested
region. Gross misrouting (e.g. kix1 -> iad1) still fails.

* Revert WORKFLOW_SERVER_URL_OVERRIDE to '' — all-regions serving is in production

The all-regions workflow-server rollout is deployed and serving
production traffic from every Vercel region, so this branch's e2e no
longer needs to target a branch preview. Restores the empty override
the No Test Overrides lint enforces for merge.

With this the PR is complete: region-tagged run IDs, region-aware
queue routing, and the multi-region e2e suite (explicit + implicit +
all-regions + cross-region streams) all validate against the
production-default backends.

* docs: fix three stale comments flagged in review

- start.ts: StartOptionsBase.region fallback is iad1, not the unknown
  sentinel (createRunId always mints a concrete routable region)
- queue.ts: example used a nonexistent start({ runIdInput }) API; the
  real option is start({ region })
- events.ts: decode() clears only the tag bit (top bit of the 48-bit
  timestamp field) — it does not restore the original untagged ULID;
  reword to say what actually matters for timestamp validation

* test(e2e): cover hook resolve/resume for runs owned by non-iad1 regions

Hooks are resolved by opaque token, which carries no region hint, so
lookup and resume must work regardless of which region owns the run's
data. Exercises the full follow-up-message path on sfo1- and
fra1-tagged runs: create inside the workflow, resolve by token from
the test process, resume twice sequentially, and assert payload order
and completion.

Regression coverage for the failure mode where the first message to a
hook-driven app on a non-iad1 run worked but every follow-up failed
with 'Hook not found'.

---------

Co-authored-by: Peter Wielander <peter.wielander@vercel.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 21:03:56 +00:00

9 lines
362 B
JSON

[
{
"testName": "WorkflowNotRegisteredError fails the run when workflow does not exist",
"runId": "wrun_41KV8YMWKC00RAQCRH320GSWR4",
"timestamp": "2026-06-16T19:31:07.845Z",
"dashboardUrl": "https://vercel.com/vercel-labs/example-nextjs-workflow-turbopack/observability/workflows/runs/wrun_41KV8YMWKC00RAQCRH320GSWR4?environment=preview"
}
]