Skip to content

chore(cli-test): exclude node_modules on Windows and tolerate dangling symlinks#1127

Open
gu-stav wants to merge 2 commits into
mainfrom
fix/cli-test-fixture-symlinks
Open

chore(cli-test): exclude node_modules on Windows and tolerate dangling symlinks#1127
gu-stav wants to merge 2 commits into
mainfrom
fix/cli-test-fixture-symlinks

Conversation

@gu-stav
Copy link
Copy Markdown
Member

@gu-stav gu-stav commented May 22, 2026

Description

Fixes two flaky test failures observed in CI (e.g. run 26275207771):

  • build.app.test.ts > should error when @sanity/sdk-react is not installed
  • build.studio.test.ts > should error when styled-components is not installed

Both crashed with ENOENT: no such file or directory, stat '.../packages/@sanity/cli-test/fixtures/basic-app/node_modules/react' at testFixture.ts:159, where stat() follows a symlink in the bundled fixtures' node_modules.

Root cause (cross-OS turbo cache corruption):

  1. copy-fixtures.ts used src.split('/').pop() to filter out node_modules/dist/.turbo. On Windows, paths use \, so split('/') returns the whole path as a single segment and the filter never matches.
  2. On Windows CI, pnpm install populates fixtures/basic-app/node_modules (workspace install). copy-fixtures then copies that node_modules into packages/@sanity/cli-test/fixtures/basic-app/node_modules, and Node's fs.cp resolves symlinks to absolute Windows paths.
  3. turbo.json declares fixtures/** as a build output, so those Windows-absolute symlinks get cached.
  4. Linux CI hits the same turbo cache (cache key is content-based, not OS-scoped — confirmed in failing logs where the cache replay shows C:\a\cli\cli\... paths). On Linux, the Windows-path symlink targets don't exist → dangling → ENOENT on stat.

Fixes:

  1. copy-fixtures.ts: switch the filter to path.basename(src) so it's platform-aware.
  2. testFixture.ts: defensively skip dangling symlinks (ENOENT on stat) so cross-OS cache replay or other leftover state can't crash fixture loading again.

What to review

  • The filter change in copy-fixtures.ts — straightforward platform-aware swap.
  • The error handling in testFixture.ts — only ENOENT is swallowed (the symlink is skipped); other errors still propagate.

Worth flagging as a follow-up (not in this PR): even after fix #1, sharing a turbo cache between Windows and Linux for @sanity/cli-test:build is fragile because its output contains resolved symlinks. Consider scoping the turbo cache key per-OS for this task, or dropping fixtures/** from outputs.

Testing

  • Ran pnpm check:types (clean) and pnpm check:lint (clean, only pre-existing warnings).
  • Ran pnpm test --changed --bail=1 → 1329/1329 passed.
  • Re-ran both previously-failing test files explicitly → 39/39 passed.
  • The Windows path-splitting bug is verified locally with path.win32.basename returning node_modules correctly.

Note

Low Risk
Low risk: small, localized changes to test/fixture build tooling that only affect how fixtures and node_modules symlinks are copied, with narrow error handling (only ENOENT is tolerated).

Overview
Fixes flaky @sanity/cli-test runs across Windows/Linux by making fixture bundling and cloning more robust.

The fixture copy script now uses path.basename() when filtering during fs.cp, ensuring node_modules, .turbo, and optional dist are correctly excluded on Windows path separators. testFixture() now skips dangling node_modules symlinks by swallowing ENOENT from stat() when determining symlink type, preventing cross-OS cache replays from crashing.

Reviewed by Cursor Bugbot for commit 489a86a. Bugbot is set up for automated code reviews on this repo. Configure here.

@gu-stav gu-stav requested a review from a team as a code owner May 22, 2026 08:21
@claude
Copy link
Copy Markdown

claude Bot commented May 22, 2026

Claude finished @gu-stav's task in 1m 32s —— View job


Review

  • Read changed files
  • Analyze the fixes for correctness
  • Check for changeset requirement
  • Post review

Findings

Missing changeset (please add)
@sanity/cli-test is a published package (no "private": true, has publishConfig.access: public). Both changes affect what consumers see:

  • copy-fixtures.ts controls what's bundled into fixtures/ (listed in files). Without this fix, Windows installs of @sanity/cli-test ship resolved-symlink node_modules inside fixtures.
  • testFixture.ts is exported and used by consumers; this changes its error behavior (no longer crashes on dangling symlinks).

Recommend adding a patch changeset, e.g.:

---
'@sanity/cli-test': patch
---

fix: exclude node_modules from bundled fixtures on Windows and skip dangling symlinks when cloning fixtures

Silent skip could surprise callers (minor)
packages/@sanity/cli-test/src/test/testFixture.ts:166 swallows ENOENT and continues. If a fixture genuinely depends on a now-dangling dep, the resulting test failure will be far from the root cause. Consider a console.warn (or debug() log) noting the skipped entry name so CI logs surface what happened — cheap insurance for the next time a cross-OS cache replay sneaks in.

Follow-up scope (already called out in description)
The deeper issue — sharing a turbo cache between Windows and Linux for @sanity/cli-test:build when its output contains resolved symlinks — remains. Worth filing as a tracking issue so the defensive ENOENT skip doesn't become load-bearing.

Verified

  • basename(src) is the correct platform-aware swap; node:path.basename handles both / and \ separators on Windows.
  • ENOENT-only catch is correctly narrow; EPERM, EACCES, etc. still propagate.
  • targetStats typed as {isDirectory: () => boolean} cleanly accepts both Dirent and Stats (tightened in 489a86a).
    • Branch: fix/cli-test-fixture-symlinks

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

📦 Bundle Stats — @sanity/cli

Compared against main (cd110e7c)

@sanity/cli

Metric Value vs main (cd110e7)
Internal (raw) 2.1 KB -
Internal (gzip) 799 B -
Bundled (raw) 10.97 MB -
Bundled (gzip) 2.06 MB -
Import time 827ms -1ms, -0.1%

bin:sanity

Metric Value vs main (cd110e7)
Internal (raw) 1023 B -
Internal (gzip) 486 B -
Bundled (raw) 9.84 MB -
Bundled (gzip) 1.77 MB -
Import time 1.95s -1ms, -0.1%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — @sanity/cli-core

Compared against main (cd110e7c)

Metric Value vs main (cd110e7)
Internal (raw) 97.0 KB -
Internal (gzip) 22.7 KB -
Bundled (raw) 21.61 MB -
Bundled (gzip) 3.42 MB -
Import time 786ms -1ms, -0.1%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

📦 Bundle Stats — create-sanity

Compared against main (cd110e7c)

Metric Value vs main (cd110e7)
Internal (raw) 908 B -
Internal (gzip) 483 B -
Bundled (raw) 931 B -
Bundled (gzip) 491 B -
Import time ❌ ChildProcess denied: node -
Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

…g symlinks

The copy-fixtures filter used `src.split('/').pop()` which fails on Windows
(paths use `\`), so `node_modules` was copied into the bundled fixtures dir
during the Windows build. Node's `fs.cp` resolves symlinks to absolute paths,
and since `fixtures/**` is a turbo build output the Windows-absolute symlinks
were cached and replayed on Linux, where they became dangling and crashed
`testFixture` setup with ENOENT.

Switch the filter to `path.basename` so it's platform-aware, and skip dangling
symlinks in `testFixture` defensively so cross-OS cache replay can't ever
crash fixture loading again.
@gu-stav gu-stav force-pushed the fix/cli-test-fixture-symlinks branch from 1d86b9b to a64133f Compare May 22, 2026 08:24
@gu-stav gu-stav changed the title fix(cli-test): exclude node_modules on Windows and tolerate dangling symlinks chore(cli-test): exclude node_modules on Windows and tolerate dangling symlinks May 22, 2026
Drop `| undefined` from `targetStats` — every path reaching `symlink(...)`
definitively assigns it (the symlink branch assigns, continues, or throws;
the else branch assigns), so the wider type was misleading.
@github-actions
Copy link
Copy Markdown
Contributor

Coverage Delta

No covered files changed in this PR.

Overall Coverage

Metric Coverage
Statements 84.3% (±0%)
Branches 74.3% (±0%)
Functions 84.2% (±0%)
Lines 84.8% (±0%)

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.

1 participant