Skip to content

fix(package-manager): match pnpm's GVS path segment for injected file: deps#12039

Merged
zkochan merged 1 commit into
mainfrom
fix/12038
May 28, 2026
Merged

fix(package-manager): match pnpm's GVS path segment for injected file: deps#12039
zkochan merged 1 commit into
mainfrom
fix/12038

Conversation

@zkochan

@zkochan zkochan commented May 28, 2026

Copy link
Copy Markdown
Member

Problem

Closes #12038.

When materialising an injected file: workspace dep (dependenciesMeta[<alias>].injected: true, dedupeInjectedDeps: false) under the global virtual store (enableGlobalVirtualStore: true), pacquet built the slot path as:

<store>/links/@/b/file:packages/b/<hash>/node_modules/b

The raw file:packages/b version segment embeds a : and a /, which:

  • is rejected on Windows with ERROR_INVALID_NAME (os error 123), failing the install; and
  • diverges from pnpm, which renders @/b/undefined/<hash> (frozen-lockfile) / @/b/1.0.0/<hash> (fresh).

The materialise step the issue describes is already implemented — the LockfileResolution::Directory arm runs the directory fetcher (landed with the injectWorkspacePackages / dedupeInjectedDeps ports), so on non-GVS installs the dep already copies into the escaped b@file+packages+b slot and resolves correctly. The remaining gap was the GVS path segment.

Fix

gvs_version_segment() mirrors pnpm's nameVerFromPkgSnapshot (pkgSnapshot.version ?? pkgInfo.version) feeding formatGlobalVirtualStorePath. An injected file: dep has no lockfile version and a non-semver depPath, so upstream produces the literal segment undefined; pacquet now does the same. Semver / non-semver deps are unchanged.

Verified pacquet now writes …/links/@/b/undefined/<hash>/… — colon-free and matching pnpm's --frozen-lockfile output.

Tests

  • Re-enabled injected_workspace_dep_with_dedupe_off_writes_file_arm on Windows (removed the #[cfg_attr(target_os = "windows", ignore)]) and strengthened it to assert the dep actually materialises (resolves to a real dir with package.json), not just lockfile contents.
  • Added injected_workspace_dep_with_dedupe_off_materialises_under_gvs — a GVS regression test asserting the slot resolves and the symlink target carries no file:/colon segment. Confirmed it fails when the fix is reverted.
  • Added a gvs_version_segment unit test.

just fmt + just lint clean; the affected cli tests and all 365 pacquet-package-manager unit tests pass.


Written by an agent (Claude Code, claude-opus-4-8).

Summary by CodeRabbit

  • Bug Fixes

    • Improved Windows compatibility when processing workspace dependency paths.
    • Fixed global virtual store slot generation for file-based workspace dependencies.
  • Tests

    • Enhanced test coverage for injected dependency deduplication with various virtual store configurations and materialization scenarios.

Review Change Stack

…: deps

Under `enableGlobalVirtualStore: true`, pacquet built the slot path for an
injected `file:` workspace dep as `<store>/links/@/<name>/file:<path>/<hash>`,
embedding the raw depPath version. The `:` (and embedded `/`) make the path
invalid on Windows (`ERROR_INVALID_NAME`) and diverge from pnpm.

`gvs_version_segment` now mirrors pnpm's `nameVerFromPkgSnapshot` feeding
`formatGlobalVirtualStorePath`: a `file:` directory dep has no lockfile
`version` and a non-semver depPath, so upstream renders the literal segment
`undefined`. Pacquet does the same, keeping the colon out of the path while
matching pnpm's frozen-lockfile output byte-for-byte.

The materialise step itself already works (the directory fetcher landed with
the injectWorkspacePackages/dedupeInjectedDeps ports), so re-enable the
Windows-skipped e2e test and strengthen it to assert materialisation. Add a
GVS regression test covering the colon fix.

Closes #12038
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: cc4663e2-32fa-42ec-a53e-744516eb89c5

📥 Commits

Reviewing files that changed from the base of the PR and between ddf4ec4 and 5b47129.

📒 Files selected for processing (2)
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
  • pacquet/crates/package-manager/src/virtual_store_layout.rs
📜 Recent review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Compare Benchmarks
  • GitHub Check: ubuntu-latest / Node.js 24 / Test
  • GitHub Check: Lint and Test (windows-latest)
  • GitHub Check: Lint and Test (ubuntu-latest)
  • GitHub Check: Lint and Test (macos-latest)
  • GitHub Check: Code Coverage
  • GitHub Check: Run benchmark on ubuntu-latest
🧰 Additional context used
📓 Path-based instructions (2)
pacquet/**/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/*.rs: Warnings are errors (--deny warnings in lint). Do not silence them with #[allow(...)] unless there is a specific, justified reason.
Choose owned vs. borrowed parameters to minimize copies; widen to the most encompassing type (&Path over &PathBuf, &str over &String) when it doesn't force extra copies.
Prefer Arc::clone(&x) / Rc::clone(&x) over x.clone() for reference-counted types, so the cost is visible at the call site.
Follow Rust API Guidelines for naming: https://rust-lang.github.io/api-guidelines/naming.html
No star imports inside module bodies. Write use super::{Foo, bar} instead of use super::*;, and the same for any other glob whose target is a module you control. Two forms stay allowed: external-crate preludes such as use rayon::prelude::*; and root-of-module re-exports such as pub use submodule::*; in a lib.rs.
Doc comments (///, //!) document the contract: preconditions, postconditions, panics, the reason the function exists. They are not a re-narration of the body.
Do not restate at call sites what the callee's doc comment already says. If /// on the function says 'no-op when …', the caller should not repeat that. Update the doc once; let every call site benefit.
Tests are documentation. Do not duplicate them in prose. If a behavioral scenario, edge case, failure mode, or worked example is already captured by a test, do not also narrate it in the doc comment on the implementation. The same applies in reverse: a test's own doc comment should not re-explain what the asserts already say, only the why if it is not obvious.
Use // SAFETY:, // TODO:, and similar prefixes to signal hidden invariants or known follow-ups that a reader cannot recover from the code alone.
When editing existing code, do not break a method chain (including pipe-trait .pipe(...) chains) into intermediate let bindings unless you can justify the rewrite. Valid justifications include a chain that fails to compile after your...

Files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
pacquet/**/{tests,test}/*.rs

📄 CodeRabbit inference engine (pacquet/AGENTS.md)

pacquet/**/{tests,test}/*.rs: Tests must not be tolerant of a missing build / runtime environment by silently returning early when a tool isn't found. If the test needs a tool, just call into it and let the existing .unwrap() / .expect(...) panic when the tool is absent.
Prefer platform-specific gates like #[cfg_attr(target_os = "windows", ignore = "...")] or #[cfg(unix)] over runtime probe-and-skip helpers for platform-locked tools, because the gate is visible to cargo test and shows up in the test report.
Follow the test-logging guidance in the style guide — log before non-assert_eq! assertions, dbg! complex structures, skip logging for simple scalar assert_eq!.

Files:

  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
🧠 Learnings (3)
📚 Learning: 2026-05-20T19:40:55.051Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11774
File: pacquet/crates/resolving-deps-resolver/src/resolve_peers.rs:0-0
Timestamp: 2026-05-20T19:40:55.051Z
Learning: In the pacquet Rust code, ensure the semver implementation uses the `node-semver` crate (not `nodejs-semver`). `node-semver`’s public API does not include a `satisfies_with_prerelease`-style method; prerelease-tolerant matching should be implemented inline by first calling `Range::satisfies`, and when it rejects a prerelease version, retry matching against a stripped `MAJOR.MINOR.PATCH` base of the prerelease version.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
📚 Learning: 2026-05-22T00:08:44.646Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11837
File: pacquet/crates/resolving-npm-resolver/src/pick_package.rs:33-51
Timestamp: 2026-05-22T00:08:44.646Z
Learning: In the pnpm/pnpm repo’s pacquet Rust crates, do not flag Unicode ellipsis characters (U+2026, `…`) in Rust doc comments (`///` / `/** */`) as a lint violation. The pacquet crate’s `dylint.toml` only enables `perfectionist::derive_ordering`, and the Dylint `unicode-ellipsis` rule is not enabled for this project—so `…` in doc comments is an intentional, repo-consistent style.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
📚 Learning: 2026-05-20T23:07:58.444Z
Learnt from: zkochan
Repo: pnpm/pnpm PR: 11784
File: pacquet/crates/resolving-deps-resolver/src/hoist_peers.rs:120-133
Timestamp: 2026-05-20T23:07:58.444Z
Learning: When reviewing code in this pacquet Rust port, follow the upstream pnpm compatibility rule: only match pnpm’s behavior exactly. Do not propose review changes that intentionally deviate from pnpm’s documented/observed behavior, even if pnpm appears buggy. If you identify a real bug in pnpm behavior, the review should prioritize fixing it upstream in pnpm first, and avoid implementing a pnpm-behavior workaround here unless the same fix has already landed upstream.

Applied to files:

  • pacquet/crates/package-manager/src/virtual_store_layout.rs
  • pacquet/crates/cli/tests/dedupe_injected_deps.rs
🔇 Additional comments (2)
pacquet/crates/package-manager/src/virtual_store_layout.rs (1)

31-32: LGTM!

Also applies to: 243-243, 292-310, 816-827

pacquet/crates/cli/tests/dedupe_injected_deps.rs (1)

11-13: LGTM!

Also applies to: 93-98, 155-166, 168-237


📝 Walkthrough

Walkthrough

This PR fixes Global Virtual Store (GVS) slot path computation for file: workspace dependencies by introducing a version segment helper that renders file: deps as "undefined" instead of embedding path strings. It includes extended test coverage for both dedupe-off and GVS-enabled scenarios.

Changes

GVS version segment computation for file: dependencies

Layer / File(s) Summary
Version segment helper and unit test
pacquet/crates/package-manager/src/virtual_store_layout.rs
Adds VersionPart import and defines gvs_version_segment helper to map VersionPart::File to literal "undefined" and other variants to their stringified versions. Includes unit test validating both semver and file: dependency rendering.
Integration into VirtualStoreLayout::new
pacquet/crates/package-manager/src/virtual_store_layout.rs
Updates VirtualStoreLayout::new to use gvs_version_segment helper instead of direct suffix.version().to_string() when computing GVS slot path version segments.
Extended and new test coverage for dedupe and GVS scenarios
pacquet/crates/cli/tests/dedupe_injected_deps.rs
Imports test helper for GVS setup, updates existing dedupeInjectedDeps: false test with symlink materialization assertions and removes Windows ignore gate, and introduces new test validating GVS-enabled scenario produces correct symlink targets without embedded file: segments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Poem

🐰 A version segment helper hops in,
File: deps now render as undefined
No more broken paths on Windows win,
GVS slots materialize, symlinks relieved! 🎯

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR partially addresses issue #12038. It fixes GVS path segment formatting for injected file: deps (matching pnpm), and re-enables/strengthens the Windows e2e test. However, it does NOT implement the core materialisation requirement of copying workspace project contents into the virtual store. Implement the materialisation logic in create_virtual_store to copy/hardlink workspace project contents into the virtual store GVS slot, as required by issue #12038 scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing pnpm GVS path segment formatting for injected file: dependencies by using 'undefined' instead of raw depPath.
Out of Scope Changes check ✅ Passed All changes directly support fixing GVS path segments for file: workspace deps and re-enabling the Windows test. The new test helper and unit tests are in scope for validating the fix.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/12038

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@zkochan zkochan marked this pull request as ready for review May 28, 2026 20:47
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Review Summary by Qodo

Fix GVS path segment for injected file: workspace dependencies

🐞 Bug fix 🧪 Tests

Grey Divider

Walkthroughs

Description
• Fix GVS path segment for injected file: workspace deps to match pnpm
  - Render undefined instead of raw file: to avoid Windows ERROR_INVALID_NAME
• Re-enable Windows test for injected workspace deps with dedupe disabled
  - Strengthen test to assert materialisation into virtual store
• Add GVS regression test for injected file: deps materialisation
• Add unit test for gvs_version_segment function
Diagram
flowchart LR
  A["Injected file: dep<br/>file:packages/b"] -->|gvs_version_segment| B["Version segment<br/>undefined"]
  B -->|formatGlobalVirtualStorePath| C["GVS slot path<br/>@/b/undefined/hash"]
  C -->|No colon| D["Windows compatible<br/>Matches pnpm"]

Loading

Grey Divider

File Changes

1. pacquet/crates/cli/tests/dedupe_injected_deps.rs 🧪 Tests +92/-15

Enable and strengthen injected deps tests with GVS coverage

• Re-enabled injected_workspace_dep_with_dedupe_off_writes_file_arm test on Windows by removing
 #[cfg_attr(target_os = "windows", ignore)]
• Strengthened test to assert materialisation by checking package.json exists in resolved symlink
 target
• Added new injected_workspace_dep_with_dedupe_off_materialises_under_gvs test for GVS regression
 coverage
• New test verifies GVS slot path contains no file: segment with colon on Unix platforms
• Added utility module import _utils::enable_gvs_in_workspace_yaml

pacquet/crates/cli/tests/dedupe_injected_deps.rs


2. pacquet/crates/package-manager/src/virtual_store_layout.rs 🐞 Bug fix +36/-3

Implement gvs_version_segment to match pnpm behavior

• Added gvs_version_segment() function that mirrors pnpm's nameVerFromPkgSnapshot logic
• Function returns "undefined" for file: deps and semver/non-semver string for other deps
• Updated GVS suffix calculation to use gvs_version_segment() instead of raw
 version().to_string()
• Added imports for PkgVerPeer and VersionPart types
• Added unit test gvs_version_segment_renders_file_deps_as_undefined verifying correct behavior

pacquet/crates/package-manager/src/virtual_store_layout.rs


Grey Divider

Qodo Logo

@github-actions

Copy link
Copy Markdown
Contributor

Micro-Benchmark Results

Linux

group                          main                                   pr
-----                          ----                                   --
tarball/download_dependency    1.01      8.2±0.25ms   531.8 KB/sec    1.00      8.1±0.34ms   535.2 KB/sec

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.69%. Comparing base (ddf4ec4) to head (5b47129).

Additional details and impacted files
@@            Coverage Diff             @@
##             main   #12039      +/-   ##
==========================================
+ Coverage   88.67%   88.69%   +0.01%     
==========================================
  Files         233      233              
  Lines       30051    30062      +11     
==========================================
+ Hits        26648    26663      +15     
+ Misses       3403     3399       -4     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Integrated-Benchmark Report (Linux)

Scenario: Isolated linker: fresh restore, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.120 ± 0.090 2.001 2.323 1.04 ± 0.05
pacquet@main 2.036 ± 0.035 1.986 2.098 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.11964580446,
      "stddev": 0.08957524439868893,
      "median": 2.10061290356,
      "user": 2.64566326,
      "system": 3.3912613799999995,
      "min": 2.00059956806,
      "max": 2.32339750606,
      "times": [
        2.07056166806,
        2.00059956806,
        2.04534565506,
        2.15343683806,
        2.09618918406,
        2.10503662306,
        2.14517098506,
        2.32339750606,
        2.07410087106,
        2.18261914606
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.0358943150599997,
      "stddev": 0.03465961460969254,
      "median": 2.02651803956,
      "user": 2.64054746,
      "system": 3.3623169799999992,
      "min": 1.9863280570600002,
      "max": 2.09827034006,
      "times": [
        2.09827034006,
        1.9863280570600002,
        2.01995194606,
        2.03308413306,
        2.05033613306,
        2.08335537506,
        2.01010026406,
        2.01069631206,
        2.01877762906,
        2.04804296106
      ]
    }
  ]
}

Scenario: Isolated linker: fresh restore, hot cache + hot store

Command Mean [ms] Min [ms] Max [ms] Relative
pacquet@HEAD 656.3 ± 28.3 629.3 725.7 1.02 ± 0.05
pacquet@main 645.6 ± 11.9 631.3 675.7 1.00
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 0.65633259718,
      "stddev": 0.028341352357959648,
      "median": 0.64958071118,
      "user": 0.35402624,
      "system": 1.3176751799999997,
      "min": 0.62929931918,
      "max": 0.7256536421800001,
      "times": [
        0.7256536421800001,
        0.62929931918,
        0.66288768618,
        0.6379874191800001,
        0.6545163781800001,
        0.6784864461800001,
        0.6523909841800001,
        0.6348842521800001,
        0.6404494061800001,
        0.6467704381800001
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 0.64560775788,
      "stddev": 0.011937389585277234,
      "median": 0.64296404768,
      "user": 0.35759574,
      "system": 1.3327233799999998,
      "min": 0.6312867121800001,
      "max": 0.6757071131800001,
      "times": [
        0.6757071131800001,
        0.6529233591800001,
        0.6424448061800001,
        0.6399437961800001,
        0.6411561451800001,
        0.6312867121800001,
        0.6449407181800001,
        0.6384624631800001,
        0.6434832891800001,
        0.64572917618
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, cold cache + cold store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 2.342 ± 0.029 2.297 2.396 1.00
pacquet@main 2.358 ± 0.042 2.303 2.441 1.01 ± 0.02
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 2.3417380375600003,
      "stddev": 0.02939970810977194,
      "median": 2.34924199096,
      "user": 3.7739335000000005,
      "system": 3.17679956,
      "min": 2.29732427446,
      "max": 2.39641507446,
      "times": [
        2.34902552446,
        2.35401996646,
        2.34344382746,
        2.29732427446,
        2.30768511946,
        2.35985039546,
        2.34996511646,
        2.34945845746,
        2.31019261946,
        2.39641507446
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 2.3583009533599997,
      "stddev": 0.04190521710035663,
      "median": 2.34346224846,
      "user": 3.8048925999999996,
      "system": 3.16510376,
      "min": 2.30341094146,
      "max": 2.44086020646,
      "times": [
        2.44086020646,
        2.30341094146,
        2.39308991146,
        2.33340720446,
        2.33129352746,
        2.33228431146,
        2.38483394046,
        2.38672017046,
        2.35351729246,
        2.32359202746
      ]
    }
  ]
}

Scenario: Isolated linker: fresh install, hot cache + hot store

Command Mean [s] Min [s] Max [s] Relative
pacquet@HEAD 1.552 ± 0.029 1.512 1.607 1.00
pacquet@main 1.563 ± 0.052 1.497 1.675 1.01 ± 0.04
BENCHMARK_REPORT.json
{
  "results": [
    {
      "command": "pacquet@HEAD",
      "mean": 1.5515298031800002,
      "stddev": 0.02856380091339649,
      "median": 1.54663641548,
      "user": 1.7760127,
      "system": 1.87099288,
      "min": 1.51230408248,
      "max": 1.6069151854800001,
      "times": [
        1.51230408248,
        1.54596754048,
        1.5636891584800001,
        1.52090644948,
        1.5718889654800001,
        1.53282807248,
        1.5765420054800001,
        1.5369512814800002,
        1.6069151854800001,
        1.5473052904800002
      ]
    },
    {
      "command": "pacquet@main",
      "mean": 1.5633565896800001,
      "stddev": 0.05157403958270373,
      "median": 1.54885185698,
      "user": 1.7542197000000002,
      "system": 1.8802105800000004,
      "min": 1.49739147348,
      "max": 1.67480387148,
      "times": [
        1.49739147348,
        1.51707768148,
        1.5331999734800001,
        1.67480387148,
        1.53900812948,
        1.5749227214800001,
        1.6077556894800002,
        1.59170264248,
        1.53901047048,
        1.55869324348
      ]
    }
  ]
}

@github-actions

Copy link
Copy Markdown
Contributor

🐰 Bencher Report

Branchpr/12039
Testbedpacquet
Click to view all benchmark results
BenchmarkLatencyBenchmark Result
milliseconds (ms)
(Result Δ%)
Upper Boundary
milliseconds (ms)
(Limit %)
isolated-linker.fresh-install.cold-cache.cold-store📈 view plot
🚷 view threshold
2,341.74 ms
(+1.66%)Baseline: 2,303.53 ms
2,764.24 ms
(84.72%)
isolated-linker.fresh-install.hot-cache.hot-store📈 view plot
🚷 view threshold
1,551.53 ms
(+7.02%)Baseline: 1,449.81 ms
1,739.77 ms
(89.18%)
isolated-linker.fresh-restore.cold-cache.cold-store📈 view plot
🚷 view threshold
2,119.65 ms
(+2.68%)Baseline: 2,064.32 ms
2,477.18 ms
(85.57%)
isolated-linker.fresh-restore.hot-cache.hot-store📈 view plot
🚷 view threshold
656.33 ms
(-2.96%)Baseline: 676.34 ms
811.61 ms
(80.87%)
🐰 View full continuous benchmarking report in Bencher

@zkochan zkochan merged commit 50c4271 into main May 28, 2026
28 checks passed
@zkochan zkochan deleted the fix/12038 branch May 28, 2026 21:00
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.

pacquet: materialise file:<workspace> snapshots in create_virtual_store

2 participants