Skip to content

fix(vtz): semver resolver — ^0.27.3 must not match 0.25.12 (#2738)#2747

Merged
viniciusdacal merged 1 commit into
mainfrom
fix/semver-resolver-2738
Apr 17, 2026
Merged

fix(vtz): semver resolver — ^0.27.3 must not match 0.25.12 (#2738)#2747
viniciusdacal merged 1 commit into
mainfrom
fix/semver-resolver-2738

Conversation

@viniciusdacal
Copy link
Copy Markdown
Contributor

Summary

Fixes #2738. vtz install's semver resolver was producing lockfile entries like esbuild@^0.27.3 → 0.25.12, even though 0.25.12 clearly does not satisfy ^0.27.3. This caused every esbuild spawn across the monorepo to fail with Host version "0.27.3" does not match binary version "0.25.12" because the optional platform binaries still pinned to 0.27.3 while the host library resolved to 0.25.12.

Root Cause

Two fail-open paths in native/vtz/src/pm/resolver.rs:

  1. Lockfile fast path in resolve_one_task (~line 262) trusted any pinned version under the matching name@range key without verifying the pin still satisfies the range. A stale entry left over from a prior install or registry-state change was silently reused.
  2. Root-dep wiring in graph_to_lockfile (~line 526) looked up the hoisted package by name only, then wrote a lockfile entry pointing at whatever hoisted version happened to be present — even if it was out of range.

The low-level resolve_version() and node_semver itself were already correct — the bug was purely in the reuse / wiring glue that bypassed the satisfies-check.

Fix

Two new helpers, both in resolver.rs:

  • version_satisfies_range(version, range) — thin wrapper over Range::parse(...).satisfies(Version::parse(...)) returning false on parse failure (fail-closed).
  • lockfile_entry_satisfies_range(entry, range) — same, but treats github: and link: specs as always-valid since they're pinned outside semver.

Applied at both bug sites:

  • The lockfile fast path now also requires lockfile_entry_satisfies_range(entry, range) before reusing a pinned version. A failing check falls through to a fresh registry resolve.
  • graph_to_lockfile's hoisted-by-name lookup is replaced with hoisted-by-name-and-range.

Test Strategy (TDD)

All tests in native/vtz/src/pm/resolver.rs, ran cargo test -p vtz --lib to confirm red → green.

  • test_version_satisfies_range_caret_zero_x^0.27.3 must reject 0.25.12 / 0.28.0 and accept 0.27.3 / 0.27.5.
  • test_version_satisfies_range_tilde — same bounds for ~0.27.3.
  • test_version_satisfies_range_explicit>=0.27.3 <0.28.0 accept/reject.
  • test_resolve_version_caret_rejects_lower_minor^0.27.3 against a version list containing 0.25.12 must pick 0.27.3.
  • test_lockfile_entry_satisfies_for_current_range_rejects_stale — a pinned 0.25.12 under the ^0.27.3 key must fail the guard; 0.27.3 passes.
  • test_graph_to_lockfile_rejects_hoisted_version_outside_range — confirmed RED on the prior code path; GREEN with the fix.

E2E Verification

Rebuilt the release binary (cargo build --release -p vtz), linked via scripts/link-runtime.sh, cleared ~/.vertz/cache/npm/store/esbuild* + @esbuild+*, ran vtz install:

Before: esbuild@^0.27.3: version "0.25.12" (WRONG)
After: esbuild@^0.27.3: version "0.27.7" (CORRECT — highest in [0.27.3, 0.28.0))

And node_modules/.bin/esbuild --version now prints 0.27.7, matching the installed package version.

Quality Gates

  • cargo test -p vtz --lib — 3456 passed, 0 failed
  • cargo clippy -p vtz --all-targets -- -D warnings — clean
  • cargo fmt --all -- --check — clean

Test Plan

  • Rust unit tests pass
  • Rust clippy clean with -D warnings
  • Rust fmt clean
  • Live vtz install produces correct esbuild@^0.27.3 → 0.27.x lockfile entry
  • node_modules/.bin/esbuild matches installed package version
  • CI green

🤖 Generated with Claude Code

`vtz install` was incorrectly resolving `esbuild: ^0.27.3` to `0.25.12` when a
stale lockfile entry existed. Two places in the resolver were fail-open:

1. **Lockfile fast path** (`resolve_one_task`): trusted any pinned version under
   the matching `name@range` key without verifying the pinned version still
   satisfies the range. A stale entry left over from a prior install (or
   registry-state change) would be silently reused.

2. **Root-dep wiring** (`graph_to_lockfile`): looked up the hoisted package by
   name only, then wrote the lockfile entry without checking the chosen
   version satisfies the declared range.

Both paths now revalidate via two new helpers, `version_satisfies_range()` and
`lockfile_entry_satisfies_range()`. A mismatch falls through to a fresh
registry resolve so the correct in-range version is picked. `github:` and
`link:` specifiers are exempt (they're not semver).

Test strategy (TDD):
- Direct unit tests on the helpers for `^0.27.3`, `~0.27.3`, and explicit
  `>=0.27.3 <0.28.0` covering accept/reject of 0.25.12, 0.27.3, 0.27.5, 0.28.0.
- Regression test `test_graph_to_lockfile_rejects_hoisted_version_outside_range`
  that was confirmed RED without the fix (asserts ^0.27.3 does not wire to a
  hoisted 0.25.12).
- E2E: fresh `vtz install` on a lockfile that previously had the bug — the
  `esbuild@^0.27.3` entry now resolves to 0.27.7 (highest in the 0.27.x range)
  and the `node_modules/.bin/esbuild` version matches the package version.

Closes #2738

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@viniciusdacal viniciusdacal merged commit 7fea5d7 into main Apr 17, 2026
6 checks passed
This was referenced Apr 17, 2026
viniciusdacal added a commit that referenced this pull request Apr 21, 2026
Repo-wide CI failure: @vertz/ci build step dies with
"Cannot start service: Host version 0.27.3 does not match binary
version 0.27.7". Same signature on every PR built against current main,
including unrelated ones (#2910, #2894). The lockfile held two esbuild
resolutions — esbuild@0.27.3 and esbuild@^0.27.3 → 0.27.7 — and the
platform-specific @esbuild/<os>-<arch> binaries got hoisted at 0.27.7
while the esbuild JS host stayed at 0.27.3, so any vertz-build spawn
failed the version handshake.

Fix: add "esbuild": "0.27.3" to root package.json overrides. This is
the same approach as 01ae939 on a prior branch — collapses every
esbuild range to a single resolution so the host + binary always match.
vtz install then dedupes the lockfile down to one esbuild entry.

The underlying issue is that the semver resolver fix in #2747 correctly
started resolving "^0.27.3" to the latest 0.27.7 the moment esbuild
released it, which exposed this hoist-inconsistency. Filing a follow-up
to have esbuild treated as platform-linked so host + binary always
pin together.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
viniciusdacal added a commit that referenced this pull request Apr 21, 2026
…#2889] (#2907)

* fix(db): stringify primitive values written to d.jsonb<T>() on SQLite [#2889]

d.jsonb<T>() on SQLite stores values as TEXT and the read path always
runs JSON.parse on the raw cell. Writes only stringified plain objects
and arrays, so primitives (strings, numbers, booleans) were persisted
raw and blew up with JsonbParseError on read-back.

Add a CRUD-layer marshaling pass that runs after runJsonbValidators and
wraps every non-null, non-DbExpr value for jsonb/json columns in
JSON.stringify when the dialect is SQLite. null passes through to emit
SQL NULL; DbExpr SQL fragments are left alone. All six write call sites
(create/createMany/createManyAndReturn/update/updateMany/upsert,
including the upsert updateValues path) go through the new pass.
Postgres writes are unchanged.

Closes #2889.

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

* test(db): cover createMany/updateMany/upsert primitive round-trip [#2889]

Review S1 follow-up: the original round-trip tests only exercised
create() and update(). Add matching primitive-jsonb round-trip tests
for createMany, createManyAndReturn, updateMany, and upsert (both
insert and update branches) so a copy-paste regression in any of the
4 remaining call sites is caught.

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

* test(db): share PGlite instance across migrateStatus tests to fix CI timeout

status.test.ts was creating a fresh PGlite instance in beforeEach for all 24
tests. Each WASM init costs ~300-500ms on CI runners, pushing the file past
the vtz test-runner's 60s per-file cap under resource pressure — reproduced
twice on this PR's CI while the file passes locally in 9.8s (CI was a
stable 9.8s when resources were not contended but failed under load).

Hoist PGlite creation to beforeAll and reset state in afterEach by dropping
the three tables any test creates (_vertz_migrations, users, posts).
Reduces file runtime 9.8s → 0.8s locally (12x faster) without changing test
isolation semantics — each test still sees a fresh, empty schema.

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

* chore(deps): pin esbuild to 0.27.3 via overrides (unblock CI)

Repo-wide CI failure: @vertz/ci build step dies with
"Cannot start service: Host version 0.27.3 does not match binary
version 0.27.7". Same signature on every PR built against current main,
including unrelated ones (#2910, #2894). The lockfile held two esbuild
resolutions — esbuild@0.27.3 and esbuild@^0.27.3 → 0.27.7 — and the
platform-specific @esbuild/<os>-<arch> binaries got hoisted at 0.27.7
while the esbuild JS host stayed at 0.27.3, so any vertz-build spawn
failed the version handshake.

Fix: add "esbuild": "0.27.3" to root package.json overrides. This is
the same approach as 01ae939 on a prior branch — collapses every
esbuild range to a single resolution so the host + binary always match.
vtz install then dedupes the lockfile down to one esbuild entry.

The underlying issue is that the semver resolver fix in #2747 correctly
started resolving "^0.27.3" to the latest 0.27.7 the moment esbuild
released it, which exposed this hoist-inconsistency. Filing a follow-up
to have esbuild treated as platform-linked so host + binary always
pin together.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to 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.

vtz install resolver: ^0.27.3 incorrectly resolves to 0.25.12

1 participant