Skip to content

Fix Windows (yarn berry CRLF support) and first-run real-PM CI failures after #251 - #253

Merged
Mikola Lysenko (mikolalysenko) merged 20 commits into
mainfrom
fix/windows-npm-env-case-assert
Sep 25, 2026
Merged

Mikola Lysenko (mikolalysenko) merged 20 commits into
mainfrom
fix/windows-npm-env-case-assert

Conversation

@mikolalysenko

@mikolalysenko Mikola Lysenko (mikolalysenko) commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Real-PM e2e legs added in #251 (first run on this PR, since ci.yml changed): 22 failing legs fixed, no product regressions.

  • Poetry (6 legs): poetry_vendored_fresh_install_then_manifestless_vex still expected .socket/manifest.json after scan --vendor. Since Cleanup: no .socket residue, locks that never outlive a command, manifest-free vendored mode #247, vendored mode is manifest-free (CLI_CONTRACT.md). The test now asserts there is no manifest and that the ledger entry is detached: true with the embedded record.uuid.
  • Yarn berry (5 ubuntu + macOS + windows): yarn 3+ turns on enableImmutableInstalls when CI is set, so each fixture's lockfile-creating yarn install failed with YN0028. New pin_berry_ci_defaults sets CI=true and YARN_ENABLE_IMMUTABLE_INSTALLS=false. The fresh-checkout installs still pass --immutable explicitly, and a unit test covers the pin. On Windows, corepack is corepack.cmd, so the new corepack_command() spawns that. Failure messages now include yarn's stdout, where berry prints its errors.
  • Bundler 2.1.4 / 2.2.33 on Ruby 3.1 (4 legs): setup-ruby installs the pinned bundler, but bundle still runs the highest installed one (the Ruby default, 2.3.27). A new "Select the pinned Bundler" step exports BUNDLER_VERSION for every leg with a bundler pin. The harness mismatch message now names this fix.
  • Deno (2 legs): the negative capstone expected a manifest from a vendored scan whose vendor step was refused. Vendored downloads are detached, so no manifest is written. The test now asserts download.detached and that there is no ledger record, then checks nothing is attested. The "unapplied manifest attests nothing" check moved to the step that stages a manifest itself.
  • Hatch 1.18.1 (macOS): the framework CPython from setup-python resolves symlinks in sys.prefix (/private/var/… vs /var/…). Both paths are now canonicalized before the containment check.
  • Maven 4.0.0-rc-6 vendor: Maven 4 switches to a quiet download logger when it sees CI env variables, even with -B. That hid the checksum-rejection line the TAMPER probe greps for. All mvn calls now go through mvn_command(), which scrubs the CI markers. The assertion is unchanged.
  • .NET SDK 9: the parallel hosted and vendored tests race the .NET 9 runtime creating /tmp/.dotnet/shm on a fresh runner (EEXIST on the NuGet-Migrations mutex). dotnet launches in the test binary are now serialized behind one lock; the socket-patch steps still run in parallel.
  • No false-attestation assertion was weakened, and no product code changed.

Yarn berry on Windows: CRLF support (user decision: fix in this PR). Once the berry suites could actually run on Windows (the corepack.cmd fix), they showed a gap that existed before #251. On Windows, yarn berry writes yarn.lock and package.json with CRLF: a new file gets os.EOL, and an existing file keeps its majority ending (FakeFS.normalizeLineEndings, yarn 2 through 4.18). Git core.autocrlf produces CRLF locks too. socket-patch refused CRLF berry locks entirely, so neither hosted nor vendored yarn berry had ever worked on Windows.

  • Hosted: the berry rewriter splits off any BOM, edits the LF-normalized lock, and writes it back in the file's own ending. The ledger records CRLF fragments exactly as they appear on disk. Rollback and takeover replay them byte-exactly, including across a uniform LF↔CRLF flip after checkout. The old refusal, redirect_yarn_berry_crlf_unsupported, is gone.
  • Vendored: package.json goes through a new JsonLayout, which keeps the BOM, indent, line ending and trailing-newline shape, so revert is byte-exact. Lock blocks are spliced using the ending of the block they replace.
  • Mixed line endings (CRLF and LF together, or a bare CR) are refused before anything is written, with redirect_yarn_berry_mixed_line_endings / vendor_yarn_berry_mixed_line_endings. The message names yarn install as the fix. Reverts never refuse.
  • Review follow-ups:
    • setup / setup --remove now keep a CRLF or BOM package.json byte-exact too.
    • A hosted↔vendored takeover now runs the new mode's berry checks before it reverts the old mode, so a refusal leaves the project untouched (dry runs too).
  • Also fixed: the yarn 2/3 legacy suites no longer set YARN_ENABLE_HARDENED_MODE, which those versions reject.
  • Tests: CRLF, BOM and mixed-ending variants of every berry case (hosted, get hosted, vendored wiring plus byte-exact revert, workspaces, pnpm linker, takeover, rollback, manifest-less vex). A new SOCKET_PATCH_YARN_BERRY_EOL=crlf mode runs the real-yarn suites on CRLF files on macOS/Linux. With real yarn 4.12.0, every berry suite passes in both LF and CRLF mode. Against the old code, the CRLF mode reproduces both Windows CI failures exactly.
  • Rebased onto Stop hosted Go redirects claiming unpatched deps #252 (Go hosted redirects). The only conflicts were in CHANGELOG / CLI_CONTRACT. The berry line-ending handling applies only to the yarn edit kinds, so Stop hosted Go redirects claiming unpatched deps #252's golang takeovers and replays are unaffected. The golang suites, including real-Go builds, pass.
  • Not covered here:
    • depscan's TypeScript berry rewriter still has no CRLF path, so the shared golden fixtures stay LF.
    • The pnpm vendored package.json writers still write LF.

Two test-harness races found once the full matrix ran:

  • Windows yarn cache race: the berry suites' parallel tests share one yarn cache folder, and on Windows two yarn processes renaming the same cache zip collide with EPERM. Yarn launches in yarn_berry_common now run one at a time on Windows only (BERRY_SPAWN, mirroring DOTNET_SPAWN). Unix rename-over is atomic, so other platforms stay parallel.
  • In-process env race: in test binaries that mix in-process command runs with spawned CLI runs, apply_env_toggles set_vars SOCKET_OFFLINE / SOCKET_DEBUG / SOCKET_API_URL / SOCKET_PROXY_URL on the shared test process. The spawn helpers only removed SOCKET_* variables present when they scanned the environment, so a toggle set by a parallel test in between was inherited. test-release hit this: a hosted scan ran offline. The helpers in all 8 such binaries now remove those keys unconditionally.

CI: all 417 checks pass on 49825727. That includes test (windows-latest), test-release, the cargo Windows leg, and the full real-package-manager matrix (poetry, yarn berry on Linux/macOS/Windows, bundler, deno, maven 4, dotnet, hatch, pipenv, pip, pnpm, npm, go, uv, cargo, composer, nuget). This is the first complete run of #251's real-PM legs.

🤖 Generated with Claude Code

@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title test(hosted): fix Windows-only allow-remote env assertion after #251 Fix two Windows-only CI failures after #251 Sep 24, 2026
Mikola Lysenko (mikolalysenko) added a commit that referenced this pull request Sep 24, 2026
…ee ledger

The poetry vendored capstone (added in #251, written before its rebase onto
#247) still asserted `scan --vendor` writes `.socket/manifest.json`. Since
#247 vendored mode is manifest-free (CLI_CONTRACT `scan --vendor`): the run
writes only `.socket/vendor/**`, and each ledger entry is `detached: true`
with the patch record embedded. The assertion therefore failed on every
Poetry release in the CI matrix the first time the leg actually ran (#253).

Assert the contract instead: no manifest is written, and the ledger entry
for the vendored uuid is detached and embeds its record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Fix two Windows-only CI failures after #251 Fix Windows and first-run real-PM CI failures after #251 Sep 24, 2026
On Windows env var names are case-insensitive: run_isolated blanks
NPM_CONFIG_ALLOW_REMOTE before the test sets npm_config_allow_remote=none,
so the child sees a single variable under the first spelling and the
warning (which names the variable as the OS reports it) says
NPM_CONFIG_ALLOW_REMOTE=none. The product is right; the assertion was
POSIX-only. Fixes outer_npm_config_layers_are_respected on
test (windows-latest) after #251.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The cargo-vex-matrix windows-latest leg used the default pwsh shell, where
"$CARGO_TEST_TOOLCHAIN" is an unset PowerShell variable, so it ran
`rustup toolchain install ""` and failed. The leg was skipped on #251's
own CI, so this is its first real run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ee ledger

The poetry vendored capstone (added in #251, written before its rebase onto
#247) still asserted `scan --vendor` writes `.socket/manifest.json`. Since
#247 vendored mode is manifest-free (CLI_CONTRACT `scan --vendor`): the run
writes only `.socket/vendor/**`, and each ledger entry is `detached: true`
with the patch record embedded. The assertion therefore failed on every
Poetry release in the CI matrix the first time the leg actually ran (#253).

Assert the contract instead: no manifest is written, and the ledger entry
for the vendored uuid is detached and embeds its record.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… work under CI

yarn 3 and later turn enableImmutableInstalls on by default when ci-info
detects CI (CI / GITHUB_ACTIONS). With that default, the plain
`yarn install` that writes each fixture's first yarn.lock fails with YN0028
("The lockfile would have been created by this install, which is
explicitly forbidden"): exit 1, nothing on stderr. Every hosted, vendored,
pnpm-linker, workspaces and yarn-3 refusal fixture on the new yarn-berry
legs (4.0.2, 4.1.0, 4.6.0, 4.12.0 ubuntu+macOS, 4.18.0) failed that way. The
yarn 2 refusal legs passed because yarn 2 keeps the default off. The suites
were never run under CI before #251 added the legs; the main test job
soft-skips them.

yarn_berry_common::pin_berry_ci_defaults now sets CI=true (local runs get
the runner's defaults) and YARN_ENABLE_IMMUTABLE_INSTALLS=false. It is
applied in every berry suite's corepack helper, after the YARN_* scrub and
cache_env::isolate. The fresh-checkout installs still pass --immutable
explicitly, and yarn's flag outranks the setting, so lock enforcement is
unchanged.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Node ships corepack on Windows as the corepack.cmd batch shim, and
Command::new("corepack") only resolves corepack.exe. On the
windows-latest yarn-berry leg every suite's availability probe therefore
reported "`corepack yarn@4.12.0` unavailable" (yarn@2.4.3 / 3.8.7 in the
refusal suite), and SOCKET_PATCH_YARN_E2E_REQUIRED=1 turned each of those
into a failure.

yarn_berry_common::corepack_command() picks the spawnable name.
Every berry suite's has_corepack_pm probe and corepack helper uses it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
yarn berry prints its errors (YN0028, YN0018, …) on stdout and leaves stderr
empty. The fixture and bootstrap skip messages printed only stderr, so all
11 failures on each yarn-berry CI leg read "fixture `yarn install` failed
(registry unreachable?):" followed by nothing. The real cause was YN0028
under CI's implicit immutable default. yarn_berry_common::yarn_output
formats both streams, and every berry fixture/bootstrap skip now uses it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Ruby 3.1 legs pinned to bundler 2.1.4 and 2.2.33 never ran those
bundlers. setup-ruby's `bundler:` input only `gem install`s the release;
with no lockfile to read, RubyGems' `bundle` binstub then activates the
HIGHEST installed bundler, which on Ruby 3.1.7 is its default gem 2.3.27.
tests/common/bundler_e2e.rs correctly panicked on every test ("bundle on
PATH is 2.3.27"). The 1.17.3 legs passed only because the Bundler 1.x
step already exported BUNDLER_VERSION.

Export BUNDLER_VERSION for every pinned-bundler leg (a new step after the
1.x install), which makes the binstub pick exactly the pinned release in
every process and also turns off bundler >= 2.3's lockfile-driven
self-switch. Verified locally in Docker (Ruby 3.1.7 / 3.3.10 / 3.4.9,
setup-ruby layout): all 16 e2e_{redirect,vendor}_gem_build legs and the
setup_matrix_gem leg green with the export; the 2.1.4/2.2.33 legs fail
exactly as in CI without it. The harness panic now names the fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The real-deno negative capstone asserted that `scan --mode vendored`
"left a manifest" for the patch it could not wire, then checked that the
unapplied manifest attests nothing. Vendored mode is manifest-free: its
download phase is detached (download_patch_records_with writes nothing;
the vendor ledger alone carries records), and a vendor step refused with
vendor_lockfile_missing records nothing. So the assertion failed on the
first CI run of both deno legs (1.46.3, 2.9.7) at deno.rs:454.

Assert the real contract instead: the download is detached, no ledger
entry names the package, and there is nothing to attest (no manifest,
exit 2 manifest_not_found, zero patch-API requests). The "unapplied
manifest patch attests nothing" check moves to step 4, where the test
stages the manifest itself, before `apply --vex` runs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The macOS hatch 1.18.1 leg failed all four cases with
`".../private/var/.../six.py" outside "/var/.../app"`. The hatch
bootstrap venv is built on the runner's actions/setup-python CPython,
a macOS framework build, and a framework interpreter realpaths
`sys.prefix` — so `six.__file__` names `/private/var/folders/...` while
`hatch env find` echoes the `/var/folders/...` spelling of the same
temp dir. Linux legs (and a uv-managed standalone Python locally) keep
one spelling, which is why only the macOS leg tripped.

Canonicalize both sides for the containment check only; the env dir
handed on as VIRTUAL_ENV is unchanged. Reproduced locally by
bootstrapping hatch 1.18.1 on Homebrew's framework CPython (4/4 fail
before, 4/4 pass after).

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

The ubuntu Maven 4.0.0-rc-6 leg of e2e_vendor_maven_build failed at the
TAMPER probe: Maven rejected the tampered file:// jar and built Central's
pristine one (the load-bearing assertion held), but the output carried no
"checksum" line, so "the file:// copy was rejected on its checksum"
tripped. The CI dump shows no transfer lines at all, not even the
Central download that must have happened after the purge.

Root cause: Maven 4's CIDetectors (generic CI, GITHUB_ACTIONS, CIRCLECI,
Jenkins WORKSPACE, TEAMCITY_VERSION, TRAVIS) make MavenInvoker pick the
QuietMavenTransferListener whenever a CI is detected and
--force-interactive is absent, even under -B. That listener drops both
"Downloading from ..." and the "Checksum validation failed" warning.
Maven 3 has no such detection, so only the 4.x legs log differently on a
GitHub runner (and only this probe greps a warning a successful build
prints; the redirect suite's checksum greps are on failed builds, whose
exception text survives the quiet listener).

Scrub those markers (plus the Maven config vars run() already dropped)
in one mvn_command() used by both detect() and run(). --force-interactive
was rejected: it flips the run interactive (progress-bar listener) and
Maven 3 refuses the flag. The checksum assertion is unchanged.

Repro: CI=true GITHUB_ACTIONS=true on the unfixed tree reproduces the CI
panic at e2e_vendor_maven_build.rs:353 locally; with the fix both
e2e_vendor_maven_build and e2e_redirect_maven_build pass under the same
env on Maven 4.0.0-rc-6 and 3.9.16.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… PAL race

The ubuntu SDK 9 leg failed nuget_vendored_dotnet_restore_then_manifestless_vex
at its first fixture restore (the hosted test passed):

  System.IO.IOException: The system cannot open the device or file
  specified. : 'NuGet-Migrations'. One or more system calls failed:
  mkdir("/tmp/.dotnet/shm/session2027", AllUsers_ReadWriteExecute) == -1;
  errno == EEXIST;
    at System.Threading.Mutex..ctor ...
    at NuGet.Common.Migrations.MigrationRunner.Run ...
    at Microsoft.DotNet.Configurer.DotnetFirstTimeUseConfigurer.Configure()

Both tests run in parallel, each with a fresh HOME, so each first
`dotnet restore` runs the first-use NuGet migrations under the named
mutex `NuGet-Migrations`. On a fresh runner `/tmp/.dotnet` does not
exist yet, and the .NET 9 runtime's named-mutex setup races when two
processes create the shared-memory tree at once: the loser's session
directory mkdir fails with EEXIST. Environment/tool race (SDK 9 PAL),
exposed by the harness running two SDK processes concurrently.

Reproduced in mcr.microsoft.com/dotnet/sdk:9.0 (9.0.318, the leg's SDK)
with two concurrent first-run CLI commands per round, fresh HOMEs,
`/tmp/.dotnet` wiped before each of 60 rounds: 9, 0, 2 and 7 of 120
processes died with the exact CI message across four batches; 0/120 with
the root pre-created and 0/40 rounds run one at a time.

Hold one binary-wide lock around every `dotnet` spawn (the --version
probe and every restore). Only the SDK phases serialize; the
socket-patch runs between them stay parallel. No assertion changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
yarn 4 enables hardened mode when it detects a GitHub Actions run for a
public pull request and then re-resolves every lock entry against the
registry. The fresh-checkout installs point the registry at an unreachable
address on purpose, so the hosted berry suites failed with ECONNREFUSED
127.0.0.1:1 on PR runs only (seen in the coverage job once the fixture
installs stopped failing). Reproduced locally with a simulated public-PR
event: 3 failures without the pin, 11/11 with it. --immutable --check-cache
still verifies every checksum.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The previous commit pinned YARN_ENABLE_HARDENED_MODE=false for every
berry suite, including e2e_yarn_legacy_cachekey_refusal_build, which
drives yarn 2.4.3 and 3.8.7. Those releases predate hardened mode and
refuse every command while the variable is set:

  Usage Error: Unrecognized or legacy configuration settings found:
  enableHardenedMode

so all four refusal cells failed at their fixture install under
SOCKET_PATCH_YARN_E2E_REQUIRED=1 — how the yarn-berry-e2e job runs the
suite — and soft-skipped everywhere else. pin_berry_ci_defaults now
takes the yarn spec and pins hardened mode off for yarn 4+ only,
removing the variable for yarn 2/3. Reproduced locally: 4/4 cells fail
with the Usage Error before, 4/4 pass after.

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

yarn berry writes a file it creates with the OS line ending and keeps an
existing file's majority ending on every later write
(normalizeLineEndings in yarnpkg-fslib FakeFS.ts, called by
Project.persistLockfile and, through changeFilePromise's
automaticNewlines, Workspace.persistManifest; the same rule from 2.4.3
through 4.18.0). On Windows a fresh yarn.lock is therefore CRLF, and so
is the package.json yarn first pretty-prints; a core.autocrlf checkout
produces the same on any OS. Once the Windows yarn-berry suites ran
(corepack.cmd), both modes failed on those files:

- hosted: rewrite_yarn_berry refused every CRLF lock
  (redirect_yarn_berry_crlf_unsupported, redirected 0);
- vendored: package.json was re-serialized LF on both the wiring and
  the revert, so `vendor --revert` never restored the CRLF manifest
  byte-for-byte.

Hosted: the rewriter works on the LF-normalized lock and re-expands its
output (utils::line_endings), keeps a leading BOM, and records the
lock's on-disk CRLF fragments in the ledger, so the per-purl takeover
and the whole-ledger replay restore them byte-exactly. Both replays now
also match yarn blocks respelled in the live lock's ending when a
checkout flipped its uniform ending since the redirect (the committed
ledger keeps its fragments verbatim).

Vendored: package.json is re-serialized in its own layout
(vendor::common::JsonLayout: BOM, indent, line ending, trailing-newline
shape) and parsed past a BOM, and lock entries are spliced in the
terminator of the block they replace (yarn_classic_lock::block_eol,
also used by the shared revert, so a lock whose endings were mixed after
vendoring keeps every other line as it was).

A yarn.lock or package.json that mixes CRLF and LF, or holds a bare CR,
has no single ending to keep and fails yarn's own `--immutable` check
(YN0028): both modes refuse it before any write
(redirect_yarn_berry_mixed_line_endings /
vendor_yarn_berry_mixed_line_endings) with `yarn install` as the
remedy. Reverts never refuse on line endings.

Readers: is_berry_lock, the vendor flavor sniff, repair's sniff,
scan_blocks and the .yarnrc.yml compressionLevel read skip a leading
BOM (a header-less `__metadata:` lock is berry; a BOM'd yarnrc's
first-line knob is no longer read as unset). The lock inventory and
manifest-less VEX already split CRLF lines; unit tests pin both.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Hermetic CLI coverage of the Windows file shapes: `scan --mode hosted`
over CRLF and BOM + CRLF locks (every line kept CRLF, the ledger's
fragments the on-disk CRLF bytes, re-run a no-op, `rollback` restoring
the pristine lock byte-for-byte) and its mixed-ending refusal;
`vendor` + `vendor --revert` over CRLF (+ BOM) package.json / yarn.lock
pairs (byte-exact round trip) and the mixed-ending failed event; both
mode takeovers on CRLF files (hosted -> vendored -> revert, vendored ->
hosted -> rollback, BOM kept); and a manifest-less VEX cell over a
CRLF + BOM vendored lock and manifest.

Real yarn: SOCKET_PATCH_YARN_BERRY_EOL=crlf respells the files each
fixture's first `yarn install` wrote CRLF — what yarn itself writes on
Windows (a new lockfile and a freshly pretty-printed manifest get
os.EOL) — and yarn keeps them CRLF on every later write, so the hosted,
vendored, pnpm-linker, workspaces, legacy-refusal and mode-migration
suites run on CRLF files on macOS / Linux as they do on windows-latest.
Every fixture prints `BERRY-EOL|<yarn>|<flow>|<file>|yarn=…|flow=…`,
the ending yarn wrote and the one the flow ran on. Against the pre-fix
code this mode reproduces both Windows CI failures
(redirect_yarn_berry_crlf_unsupported on the hosted suites; "revert
must restore package.json byte-identical" on the vendored ones); with
the fix, yarn 4.12.0 passes all five suites in both modes (103
VEX-MATRIX cells each) and both mode takeovers.

Also drops a doubled doc-comment line in yarn_berry_common.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ibility page

CLI_CONTRACT: the hosted redirect keeps a CRLF lock's own line ending
and BOM and records on-disk fragments; a mixed-ending lock is refused
with redirect_yarn_berry_mixed_line_endings, which replaces v4's
redirect_yarn_berry_crlf_unsupported (no longer emitted); the vendored
yarn berry row keeps both files' layout, with the new
vendor_yarn_berry_mixed_line_endings refusal in the code table; the
per-purl revert and whole-ledger replay respell yarn blocks across a
uniform LF <-> CRLF checkout flip.

CHANGELOG [Unreleased]: the fix (hosted + vendored CRLF support, BOM
tolerance) under Fixed, the two refusal codes and the CRLF test mode
under Added.

docs/testing/yarn-berry-compatibility.md (new): supported releases, the
CI matrix, how yarn berry chooses line endings — cited to FakeFS.ts,
Project.ts, Workspace.ts, Manifest.ts and syml.ts at
@yarnpkg/cli/4.12.0 — the git autocrlf paths to CRLF, socket-patch's
contract per mode, and how to run the suites locally in CRLF mode.
docs/ecosystems.md links it from the yarn berry notes.

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

`setup` re-serialized package.json through `serialize_json`, which always
emits bare LF and never writes a BOM back. On a Windows yarn berry project
(persistManifest pretty-prints the manifest with os.EOL) a two-key script
edit became a whole-file CRLF -> LF diff that yarn then keeps (it follows
the majority ending), and `setup --remove` could never land byte-identical
on the pre-setup file.

Render through the vendored backends' `JsonLayout` instead (BOM, indent,
line ending, trailing-newline shape). The two BOM tests now assert the BOM
survives; a new round-trip test covers LF, CRLF, BOM+CRLF, BOM+LF, no final
newline and two final newlines, asserting setup keeps each shape and
setup --remove restores the original bytes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erts the old one

The reverts keep line endings as they are (a vendored or hosted revert
never refuses on them, so a lock mixed after wiring stays mixed), while the
forward hosted rewriter and vendored backend refuse a mixed file. Neither
takeover checked first:

- `scan`/`get --mode hosted` over a vendored berry purl reverted its
  wiring, ledger entry and artifact (`redirect_takeover_reverted_vendored`:
  "now fully hosted"), then the rewriter refused the mixed lock -
  `redirected: 0`, and the next `yarn install` pulled the unpatched
  registry package.
- `vendor` / `scan --mode vendored` over a hosted berry purl reverted the
  hosted edits and dropped the redirect-ledger record
  (`vendor_takeover_reverted_redirect`), then failed
  `vendor_yarn_berry_mixed_line_endings`.

Extract the rewriter's project gates into
`redirect::preflight_yarn_berry_hosted` (mixed endings, cacheKey,
`.yarnrc.yml` compressionLevel) and the backend's into
`vendor::yarn_berry_vendor_preflight` (both files' endings, cacheKey,
compressionLevel; berry flavor only), and run each before the matching
takeover revert, mirroring the bun preflights - wet and --dry-run alike.
A refused purl keeps the old mode's wiring byte-identical and is skipped /
failed with the new mode's code.

Tests: a hermetic in_process_vendor test drives both directions (mixed
lock, mixed package.json, compressionLevel 9; wet and dry-run) and asserts
the wiring snapshot is unchanged and no takeover is announced (fails on the
pre-fix code in both directions); core unit tests pin that each preflight
matches its forward gate's code and detail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikolalysenko Mikola Lysenko (mikolalysenko) changed the title Fix Windows and first-run real-PM CI failures after #251 Fix Windows (yarn berry CRLF support) and first-run real-PM CI failures after #251 Sep 24, 2026
…che rename race

The berry suites' parallel tests share one yarn cache folder. Two yarn
processes fetching the same package both rename a .tmp over the cache zip,
and on Windows the loser fails with EPERM while the winner holds the file
(windows-latest yarn-berry 4.12.0: e2e_yarn4_workspaces_build hosted test,
EPERM rename left-pad-npm-1.3.0-....zip-....tmp). A static lock in
yarn_berry_common serializes yarn processes on Windows only (Unix
rename-over is atomic), mirroring the DOTNET_SPAWN fix.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e CLI

Test binaries that mix in-process command runs with spawned CLI runs raced:
the in-process runs call apply_env_toggles, which std::env::set_var's
SOCKET_OFFLINE / SOCKET_DEBUG / SOCKET_API_URL / SOCKET_PROXY_URL on the
shared test process, and the spawn helpers only removed SOCKET_* vars that
existed when they scanned the environment. A toggle set by a parallel test
between that scan and the spawn was inherited. On test-release this made
in_process_vendor's berry takeover test run its hosted scan offline
("cannot run with --offline/SOCKET_OFFLINE"). The helpers in all eight
such binaries now remove those keys unconditionally; Command applies the
removals to the environment captured at spawn time.

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

Copy link
Copy Markdown
Collaborator Author

CI green: 417/417 on 4982572; see description for details.

@mikolalysenko
Mikola Lysenko (mikolalysenko) merged commit 1bfe532 into main Sep 25, 2026
417 checks passed
@mikolalysenko
Mikola Lysenko (mikolalysenko) deleted the fix/windows-npm-env-case-assert branch September 25, 2026 01:14
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.

2 participants