Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1261,31 +1261,45 @@ jobs:
--test-threads 8 --config-file "$RUNNER_TEMP/snapshot-nextest.toml"
--archive-file windows-snapshot-tests.tar.zst --workspace-remap . --no-fail-fast
)
snapshot_registry=(
node packages/tools/src/local-npm-registry.ts --snapshot-runner
--snapshot-fixtures "$GITHUB_WORKSPACE/crates/vp_cli_snapshots/tests/cli_snapshots/registry"
--snapshot-cache "$VP_SNAP_REGISTRY_CACHE_DIR"
)
run_snapshots() {
# Keep both registries alive across nextest's one-process-per-case
# workers. Only local-registry cases receive checkout packages.
"${snapshot_registry[@]}" --request-log "$VP_SNAP_ARTIFACTS_DIR/shared-registry/registry.jsonl" -- \
"${snapshot_registry[@]}" --packages-dir "$VP_SNAP_PACKAGES_DIR/packages" \
--request-log "$VP_SNAP_ARTIFACTS_DIR/packed-registry/registry.jsonl" -- \
cargo-nextest nextest run "${nextest_run_args[@]}" "$@"
}
# --no-fail-fast: on a snapshot suite every diff is diagnostic
# signal; cancelling on the first failure hides the rest.
test_exit=0
# This fixture requires case-sensitive directory support, which the
# default temp directory on these runners does not provide.
cargo-nextest nextest run "${nextest_run_args[@]}" --partition hash:${{ matrix.shard }}/3 \
run_snapshots --partition hash:${{ matrix.shard }}/3 \
-E 'not test(windows_case_sensitive_shims)' || test_exit=$?

if [[ '${{ matrix.shard }}' == '1' ]]; then
# Run both flavors on NTFS once. Rust's GetTempPath2 ignores
# TEMP/TMP under the SYSTEM account, so override SystemTemp.
SystemTemp='${{ steps.snapshot-temp.outputs.directory }}' \
cargo-nextest nextest run "${nextest_run_args[@]}" \
run_snapshots \
-E 'test(windows_case_sensitive_shims)' || test_exit=$?

# Exercise the same environment wrapper under Windows PowerShell 5.1.
VP_SNAP_PWSH_BIN="$(cygpath -w "$(command -v powershell.exe)")" \
cargo-nextest nextest run "${nextest_run_args[@]}" \
run_snapshots \
-E 'test(command_env_powershell)' || test_exit=$?
fi
exit "$test_exit"
env:
RUST_BACKTRACE: '1'
# Share immutable tarballs across nextest processes in this job.
VP_SNAP_PACKAGES_DIR: ${{ runner.temp }}/snapshot-packages
VP_SNAP_REGISTRY_CACHE_DIR: ${{ runner.temp }}/snapshot-registry-cache
VP_SNAP_ARTIFACTS_DIR: ${{ runner.temp }}/snapshot-artifacts
# Keep Windows env parity with the `test` recipe in justfile.
__COMPAT_LAYER: RunAsInvoker
Expand Down
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,6 @@ extend-exclude = [
"packages/cli/src/oxfmt-config.ts",
# Vendored PGP key/signature blobs: base64 armor trips the spell checker.
"crates/vp_js_runtime/src/assets/**/*.asc",
# Registry recordings contain upstream signatures and integrity hashes.
"crates/vp_cli_snapshots/tests/cli_snapshots/registry/**/manifest.json",
]
4 changes: 4 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ This guide applies to this repository; `CLAUDE.md` points here. For changes to t

## Checks and Tests

Prefer local fixtures or mock servers for tests that depend on external services. Use real network requests only when they are necessary to verify the behavior under test, and explain that need in the test. Reuse existing test infrastructure, preserve behavioral coverage, and keep mutable state isolated between cases.

For behavior changes, find nearby tests before editing and add or update coverage in the same area. Choose checks for the changed layer:

| Change | Validation |
Expand All @@ -47,6 +49,8 @@ Validate `vp check`, lint, format, and type-check changes end-to-end: bundled-to
Write new CLI tests, including interactive flows, in `crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/`. Read the [snapshot runner instructions](crates/vp_cli_snapshots/tests/cli_snapshots/README.md) before adding cases; they define `snapshots.toml`, CLI flavors, `vpt` helpers, and prompt milestones. See [the RFC](rfcs/interactive-snapshot-tests.md) for design rationale.

- Steps are argv arrays without an implicit shell. Use `vpt` helpers instead of coreutils for portable file operations.
- For package-manager cases, prefer the runner's local registry, [shared registry fixtures](crates/vp_cli_snapshots/tests/cli_snapshots/registry/README.md), and response cache over new public-registry requests. Keep package-manager caches isolated per case, and refresh recorded fixtures only through the explicit maintenance command.
- For changes to network use or scheduling, compare request counts and elapsed time with the same cases, worker count, and initial cache state. Do not remove assertions or share mutable case state to improve speed.
- Local-flavor cases require a fresh `packages/cli/dist`. Use `just snapshot-test-global <filter>` when no JS build is available.
- For intended snapshot changes, run `UPDATE_SNAPSHOTS=1 just snapshot-test <filter>`. Review and commit the recorded `.md` files with the fixture; mismatches fail the run and produce `.md.new` files.

Expand Down
101 changes: 82 additions & 19 deletions crates/vp_cli_snapshots/tests/cli_snapshots/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,32 @@ the execution gate. Scheduling happens after partitioning and does not change
shard membership or test listing order.
The existing gate still protects `serial = true` and Ctrl-C cases, including
against other runner processes.
Npm command cases use local registry fixtures and shared downloads, so they no
longer need serialization to limit concurrent connections to npm. Signal-sensitive
and explicitly isolated cases retain the execution gate.

Environment overrides, mainly for CI:

| Variable | Effect |
| --------------------------- | ----------------------------------------------------------------------------- |
| `VP_SNAP_GLOBAL_VP` | Path to a prebuilt global `vp` binary (skips the target-dir lookup) |
| `VP_SNAP_LOCAL_CLI_BIN_DIR` | Local CLI bin dir (default `<repo>/packages/cli/bin`) |
| `VP_SNAP_JS_RUNTIME_DIR` | Provisioned managed runtime to seed case homes with |
| `VP_SNAP_SH_BIN` | POSIX `sh` binary for cases that execute the generated `env` file |
| `VP_SNAP_BASH_BIN` | Bash binary for cases that execute the generated `env` file |
| `VP_SNAP_ZSH_BIN` | Zsh binary for cases that execute the generated `env` file |
| `VP_SNAP_CMD_BIN` | System cmd.exe for cases that execute generated batch files |
| `VP_SNAP_FISH_BIN` | Fish binary for cases that execute generated `env.fish` files |
| `VP_SNAP_NU_BIN` | Nushell binary for cases that execute generated `env.nu` files |
| `VP_SNAP_PWSH_BIN` | PowerShell binary for cases that execute generated `env.ps1` files |
| `VP_SNAP_SKIP_FLAVORS` | Comma-separated flavors to skip registering (e.g. `local`) |
| `VP_SNAP_PACKAGES_DIR` | Run-scoped directory for sharing packed packages across test processes |
| `VP_SNAP_ARTIFACTS_DIR` | Directory for phase timings and failure diagnostics; unset disables artifacts |
| `VP_SNAP_NEXTEST_CONFIG` | With `--list`, write nextest overrides for the discovered isolated cases |
| Variable | Effect |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- |
| `VP_SNAP_GLOBAL_VP` | Path to a prebuilt global `vp` binary (skips the target-dir lookup) |
| `VP_SNAP_LOCAL_CLI_BIN_DIR` | Local CLI bin dir (default `<repo>/packages/cli/bin`) |
| `VP_SNAP_JS_RUNTIME_DIR` | Provisioned managed runtime to seed case homes with |
| `VP_SNAP_SH_BIN` | POSIX `sh` binary for cases that execute the generated `env` file |
| `VP_SNAP_BASH_BIN` | Bash binary for cases that execute the generated `env` file |
| `VP_SNAP_ZSH_BIN` | Zsh binary for cases that execute the generated `env` file |
| `VP_SNAP_CMD_BIN` | System cmd.exe for cases that execute generated batch files |
| `VP_SNAP_FISH_BIN` | Fish binary for cases that execute generated `env.fish` files |
| `VP_SNAP_NU_BIN` | Nushell binary for cases that execute generated `env.nu` files |
| `VP_SNAP_PWSH_BIN` | PowerShell binary for cases that execute generated `env.ps1` files |
| `VP_SNAP_SKIP_FLAVORS` | Comma-separated flavors to skip registering (e.g. `local`) |
| `VP_SNAP_REGISTRY_CACHE_DIR` | Fresh run-scoped response directory shared across nextest processes |
| `VP_SNAP_REGISTRY_MODE` | `live`: count upstream requests without caching or shared fixtures; `direct`: original registry routing |
| `VP_SNAP_SHARED_REGISTRY_ENV` | Registry environment JSON from the `--snapshot-runner` wrapper, shared across runner processes |
| `VP_SNAP_PACKED_REGISTRY_ENV` | Registry environment JSON for the wrapper that serves packed checkout packages |
| `VP_SNAP_PACKAGES_DIR` | Run-scoped directory for sharing packed packages across test processes |
| `VP_SNAP_ARTIFACTS_DIR` | Directory for phase timings and failure diagnostics; unset disables artifacts |
| `VP_SNAP_NEXTEST_CONFIG` | With `--list`, write nextest overrides for the discovered isolated cases |

Windows CI generates its nextest configuration from the same case definitions.
Exact nextest runs read only the selected fixture; listing and native shard
Expand All @@ -110,8 +117,64 @@ directory for each test run and keep the build unchanged while tests run. The
runner checks the checkout path and input/tarball file sizes and modification
times; a changed build fails instead of silently testing stale packages. This
is a per-run preparation directory, not a persistent content-addressed cache.
Each case still has its own registry server, overlays, and mutable caches.
Windows CI uses this to avoid repacking in every nextest process.
Cases without fixture overlays share two servers per runner: one for published
packages and one that also serves the packed checkout. Cases with a
`mock-manifest.json`, `tarballs/`, or `.npmrc` retain a dedicated server.
Use `isolated-registry = true` when a test needs private registry state, such as
the request counter used to verify metadata caching. All package-manager caches
remain isolated per case.

Windows CI wraps nextest with `local-npm-registry.ts --snapshot-runner` so the
two servers also survive across test processes. The wrapper passes registry
environments to the Rust runner without changing its package-manager settings.
The packed server loads archives on its first request, after the runner prepares
and validates them under the existing package lock. Both servers exit when the
wrapped command finishes, and the wrapper preserves its exit code.

All cases use a local registry by default. The small test packages in
[`registry/`](registry/README.md) are served from checked-in metadata and tarballs.
Other successful registry GET responses are cached for the duration of the run,
including package-manager downloads. Concurrent requests share one download,
including across nextest processes when `VP_SNAP_REGISTRY_CACHE_DIR` is set.
Use a fresh directory per run; this is not a persistent dependency cache.
Package-manager caches and installed packages remain isolated per case.

`local-registry = true` additionally serves the packed checkout packages.
Published versions remain available for cases that deliberately install an older
release. Their upstream metadata is fetched once per run and merged with the
local version. The standalone registry tool and ecosystem tests retain the same
upstream version merging without the snapshot response cache. Fixture
`mock-manifest.json` entries override shared metadata, and fixture tarballs
override shared tarballs. Explicit case and step registry settings still take
precedence over runner defaults.

With artifacts enabled, the shared server writes `registry.jsonl` in the run
artifact directory. Windows wrappers write separate `shared-registry/registry.jsonl`
and `packed-registry/registry.jsonl` files in the artifact root.
Per-case servers write the same file in their case artifact
directory. Combine both locations when counting requests: `fixture` events
identify locally served data, `request` events count proxy requests, `hit` events
identify reused responses, and `upstream` events count actual upstream HTTP
requests (including redirects). Non-GET requests are forwarded without caching.
`upstream-stalled` identifies requests with no response progress for 15 seconds,
including connection setup. These requests release their cache locks so clients
can retry. Each upstream request also has a 60-second total deadline.
These counts cover the registry; requests to other services, such as Node.js
release downloads or Git hosts, are outside this log.

For a network comparison, run the same cases with eight workers first with
`VP_SNAP_REGISTRY_MODE=live`, then with the variable unset. Both runs route
registry traffic through the logger; `live` disables published recordings and
response cache. `VP_SNAP_REGISTRY_MODE=direct` restores the original routing for
an independent wall-time baseline, but cannot log traffic that bypasses the
local registry. Keep builds unchanged and use fresh response directories.
Local tarball URLs can cause snapshot differences in comparison modes.
Fixture overlays remain active in both comparison modes. Cases with a
`mock-manifest.json` or `tarballs/` start a private registry even in `direct`
mode, so fixed templates remain the same test inputs. Cases that use shared
locally authored packages must set `local-registry = true` so `direct` mode
also starts their registry; these packages have no published version.

With `VP_SNAP_ARTIFACTS_DIR` set, each runner process creates a unique `run-*`
directory. `runner/timing.json` records discovery, test execution, and final
Expand Down Expand Up @@ -299,8 +362,8 @@ a milestone, or a redaction rule; never rerun until green. Set
For registry fallback tests, use `local-registry = true` and a fixture
`mock-manifest.json` entry such as `"npm/latest"` to control the resolved version.
Keep the CLI input unpinned so the fallback is still tested, and keep exact
version assertions. Unmocked registry requests go upstream, so
`local-registry = true` alone does not freeze dist-tags.
version assertions. Unmocked registry responses are fetched upstream once per run, so
`local-registry = true` alone does not freeze dist-tags across runs.

Fixture trees are excluded from repo-wide fmt, lint, typecheck, and vitest
(`vite.config.ts`, `tsconfig.json`); recorded snapshots and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import path from 'node:path';

if (process.platform !== 'win32') {
const server = createServer();
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(path.join(tmpdir(), 'supervisor.sock'), resolve);
});
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
name = "change_passthrough_env_config"
vp = "local"
steps = [
{ argv = ["node", "check-temp-socket.mjs"], comment = "The isolated temporary directory must support Unix sockets even for long fixture names", snapshot = false },
{ argv = ["vp", "run", "hello"], envs = [["MY_ENV", "1"]], continue-on-failure = true },
{ argv = ["vp", "run", "hello"], comment = "MY_ENV is pass-through. should hit the cache created in step 1", envs = [["MY_ENV", "2"]], continue-on-failure = true },
{ argv = ["vp", "run", "hello"], comment = "MY_ENV is pass-through. should hit the cache created by the first run", envs = [["MY_ENV", "2"]], continue-on-failure = true },
{ argv = ["vp", "run", "hello"], comment = "cache should be invalidated because untrackedEnv config changed", envs = [["VITE_TASK_PASS_THROUGH_ENVS", "MY_ENV,MY_ENV2"], ["MY_ENV", "2"]], continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# change_passthrough_env_config

## `node check-temp-socket.mjs`

The isolated temporary directory must support Unix sockets even for long fixture names


## `MY_ENV=1 vp run hello`

```
Expand All @@ -9,7 +14,7 @@ $ node -p process.env.MY_ENV

## `MY_ENV=2 vp run hello`

MY_ENV is pass-through. should hit the cache created in step 1
MY_ENV is pass-through. should hit the cache created by the first run

```
$ node -p process.env.MY_ENV ◉ cache hit, replaying
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[[case]]
name = "command_add_npm10"
vp = "global"
serial = true
skip-platforms = ["windows"]
steps = [
{ argv = ["vp", "add", "--help"], comment = "should show help", continue-on-failure = true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[[case]]
name = "command_add_npm10_with_workspace"
vp = "global"
serial = true
skip-platforms = ["windows"]
steps = [
{ argv = ["vp", "add", "testnpm2", "-D", "-w", "--", "--no-audit"], comment = "should add package to workspace root" },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
[[case]]
name = "command_cache_npm10"
vp = "global"
serial = true
skip-platforms = ["windows"]
steps = [
{ argv = ["vp", "pm", "cache", "dir"], comment = "should show cache directory (uses npm config get cache)", continue-on-failure = true },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
[[case]]
name = "command_cache_yarn4"
vp = "global"
# This case checks Yarn's default cache location in the isolated case home.
unset-env = ["YARN_GLOBAL_FOLDER"]
skip-platforms = ["windows", "macos"]
steps = [
{ argv = ["vp", "pm", "cache", "dir"], comment = "should show cache directory (uses yarn config get cacheFolder)", continue-on-failure = true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name = "command_config_npm10"
# Exceeded 20 seconds on Windows in CI run 35299447671.
skip-platforms = ["windows"]
vp = "global"
serial = true
steps = [
{ argv = ["vp", "pm", "config", "--help"], comment = "should show help", continue-on-failure = true },
{ argv = ["vp", "pm", "config", "get", "vite-plus-pm-config-test-key", "--location", "project"], comment = "should get config value from project scope", continue-on-failure = true },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ steps = [

[[case]]
name = "berry_user_config_preserves_project"
# File configuration precedence is the subject of this case.
unset-env = ["YARN_NPM_REGISTRY_SERVER"]
vp = "global"
steps = [
{ argv = ["vpt", "json-edit", "package.json", "packageManager", "yarn@4.18.0"], snapshot = false },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name = "command_dedupe_npm10"
# Exceeded 20 seconds on Windows in CI run 35299447671.
skip-platforms = ["windows"]
vp = "global"
serial = true
steps = [
{ argv = ["vp", "dedupe"], comment = "should dedupe dependencies" },
{ argv = ["vpt", "print-file", "package.json"], continue-on-failure = true },
Expand Down
Loading
Loading