diff --git a/README.md b/README.md index e4fa6d8..0b31537 100644 --- a/README.md +++ b/README.md @@ -248,6 +248,34 @@ Pinget is designed to keep **source-backed functionality** working cross-platfor - `list` and upgrade inventory return empty results with an unsupported warning - `install`, `uninstall`, executed `upgrade`, and non-dry-run `import` return explicit no-op results with unsupported warnings +## Storage location and source mode + +Two environment variables control where Pinget keeps its state and which sources it resolves +against. Both are read by the Rust CLI and the C# core. + +| Variable | Effect | +| --- | --- | +| `PINGET_APPROOT` | Overrides the per-user storage root (`%LOCALAPPDATA%\Devolutions\Pinget` by default). Useful for embedding Pinget in a portable or sandboxed host. | +| `PINGET_SOURCE_MODE` | Selects the source mode explicitly: `auto`, `private`, or `system-winget-mirror`. | + +Source modes: + +- `auto` mirrors the machine's configured WinGet sources, falling back to the private store when + they cannot be read. +- `private` uses only the source list stored under the app root, seeded with the stock `winget` + and `msstore` entries. +- `system-winget-mirror` mirrors the machine's WinGet sources with no fallback. + +Without `PINGET_SOURCE_MODE`, setting `PINGET_APPROOT` also selects `private`, on the assumption +that a caller supplying its own root wants an isolated instance. A host that relocates storage but +still wants the machine's real source list must set `PINGET_SOURCE_MODE=auto` as well; otherwise +sources the user added to WinGet are silently absent. + +Precedence is the same in both implementations: an explicitly chosen mode wins, then +`PINGET_SOURCE_MODE`, then the app-root inference. In the C# API `RepositoryOptions.SourceMode` is +nullable for exactly this reason — leave it unset to let the environment decide, and note that +setting it to `SourceMode.Auto` is an explicit choice that the environment will not override. + ## Custom REST sources Both implementations support custom REST sources, including third-party services such as `winget.pro`. diff --git a/dotnet/src/Devolutions.Pinget.Cli/Program.cs b/dotnet/src/Devolutions.Pinget.Cli/Program.cs index 688b57e..62fcd04 100644 --- a/dotnet/src/Devolutions.Pinget.Cli/Program.cs +++ b/dotnet/src/Devolutions.Pinget.Cli/Program.cs @@ -6,7 +6,7 @@ using Devolutions.Pinget.Cli; using Devolutions.Pinget.Core; -const string Version = "0.10.0"; +const string Version = "0.11.0"; const string UpgradeUnsupportedWarning = "Upgrading packages is not supported on this platform; no changes were made."; if (args.Length == 1 && (string.Equals(args[0], "--version", StringComparison.OrdinalIgnoreCase) || string.Equals(args[0], "-v", StringComparison.OrdinalIgnoreCase))) diff --git a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs index c32f093..5117a67 100644 --- a/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs +++ b/dotnet/src/Devolutions.Pinget.Core.Tests/CoreTests.cs @@ -12,6 +12,12 @@ namespace Devolutions.Pinget.Core.Tests; +[CollectionDefinition(Name, DisableParallelization = true)] +public class RepositoryStateCollection +{ + public const string Name = "RepositoryState"; +} + public class VersionCompareTests { [Theory] @@ -112,6 +118,7 @@ public void SelectRestVersion_WithRequestedChannelMismatch_Throws() } } +[Collection(RepositoryStateCollection.Name)] public class SourceStoreTests { [Fact] @@ -365,6 +372,127 @@ public void PackagedSecureSettingsStub_DelegatesToSystemWingetExport() } } + [Theory] + [InlineData("auto", SourceMode.Auto)] + [InlineData(" Auto ", SourceMode.Auto)] + [InlineData("PRIVATE", SourceMode.Private)] + [InlineData("system-winget-mirror", SourceMode.SystemWingetMirror)] + [InlineData("SystemWingetMirror", SourceMode.SystemWingetMirror)] + public void TryParseSourceMode_AcceptsKnownSpellings(string value, SourceMode expected) + { + Assert.True(Repository.TryParseSourceMode(value, out var parsed)); + Assert.Equal(expected, parsed); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("mirror")] + [InlineData("system_winget_mirror")] + public void TryParseSourceMode_RejectsUnknownSpellings(string? value) + { + Assert.False(Repository.TryParseSourceMode(value, out _)); + } + + [Fact] + public void ResolveRequestedSourceMode_KeepsPrivateDefaultForACustomAppRoot() + { + Assert.Equal( + SourceMode.Private, + Repository.ResolveRequestedSourceMode(null, @"C:\custom", null)); + Assert.Equal( + SourceMode.Auto, + Repository.ResolveRequestedSourceMode(null, null, null)); + } + + [Fact] + public void ResolveRequestedSourceMode_LetsTheEnvironmentOverrideACustomAppRoot() + { + Assert.Equal( + SourceMode.Auto, + Repository.ResolveRequestedSourceMode(null, @"C:\custom", "auto")); + Assert.Equal( + SourceMode.SystemWingetMirror, + Repository.ResolveRequestedSourceMode(null, @"C:\custom", "system-winget-mirror")); + Assert.Equal( + SourceMode.Private, + Repository.ResolveRequestedSourceMode(null, null, "private")); + } + + [Fact] + public void ResolveRequestedSourceMode_IgnoresTheEnvironmentWhenTheCallerIsExplicit() + { + Assert.Equal( + SourceMode.SystemWingetMirror, + Repository.ResolveRequestedSourceMode(SourceMode.SystemWingetMirror, @"C:\custom", "private")); + Assert.Equal( + SourceMode.Private, + Repository.ResolveRequestedSourceMode(SourceMode.Private, null, "auto")); + } + + [Fact] + public void ResolveRequestedSourceMode_TreatsAnExplicitAutoAsAChoiceRatherThanAnUnsetValue() + { + Assert.Equal( + SourceMode.Auto, + Repository.ResolveRequestedSourceMode(SourceMode.Auto, @"C:\custom", "private")); + Assert.Equal( + SourceMode.Auto, + Repository.ResolveRequestedSourceMode(SourceMode.Auto, @"C:\custom", null)); + } + + [Fact] + public void ResolveRequestedSourceMode_IgnoresAnUnparseableEnvironmentValue() + { + Assert.Equal( + SourceMode.Private, + Repository.ResolveRequestedSourceMode(null, @"C:\custom", "not-a-mode")); + Assert.Equal( + SourceMode.Auto, + Repository.ResolveRequestedSourceMode(null, null, "not-a-mode")); + } + + [Theory] + [InlineData(null, false)] + [InlineData("auto", true)] + [InlineData("system-winget-mirror", true)] + [InlineData("private", false)] + [InlineData("not-a-mode", false)] + public void RepositoryOpen_ReadsTheSourceModeEnvironmentVariable(string? sourceMode, bool expectsMirror) + { + var appRoot = TestPaths.CreateTempAppRoot(); + var originalRunner = SystemWingetSourceStore.CommandRunner; + var originalSourceMode = Environment.GetEnvironmentVariable("PINGET_SOURCE_MODE"); + try + { + SystemWingetSourceStore.CommandRunner = _ => new WingetCommandResult( + 0, + """ +{"Arg":"https://api.contoso.test/feed","Data":"","Explicit":false,"Identifier":"api.contoso.test","Name":"contoso","TrustLevel":["Trusted"],"Type":"Microsoft.Rest"} +""", + ""); + Environment.SetEnvironmentVariable("PINGET_SOURCE_MODE", sourceMode); + + using var repo = Repository.Open(new RepositoryOptions + { + AppRoot = appRoot, + UserAgent = "pinget-dotnet-tests/1.0", + }); + + var mirrorPath = Path.Combine(appRoot, "system-sources.json"); + Assert.Equal(expectsMirror, File.Exists(mirrorPath)); + + if (expectsMirror) + Assert.Contains("contoso", File.ReadAllText(mirrorPath)); + } + finally + { + SystemWingetSourceStore.CommandRunner = originalRunner; + Environment.SetEnvironmentVariable("PINGET_SOURCE_MODE", originalSourceMode); + TestPaths.DeleteAppRoot(appRoot); + } + } + [Fact] public void RepositoryOpen_UsesCustomAppRoot() { @@ -1177,6 +1305,7 @@ PRIMARY KEY (package_id, source_id) } } +[Collection(RepositoryStateCollection.Name)] public class RepositoryParityTests { [Fact] @@ -2992,6 +3121,7 @@ public void CreateRepairInstallRequest_ForcesReinstallOfResolvedInstalledPackage } } +[Collection(RepositoryStateCollection.Name)] public class RepositoryEmbeddingTests { private const string TesslPackageId = "tessl.tessl"; diff --git a/dotnet/src/Devolutions.Pinget.Core/Models.cs b/dotnet/src/Devolutions.Pinget.Core/Models.cs index 50f1059..71a78d9 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Models.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Models.cs @@ -47,7 +47,12 @@ public record RepositoryOptions /// Storage root for Pinget state and caches. /// public string? AppRoot { get; init; } - public SourceMode SourceMode { get; init; } = SourceMode.Auto; + /// + /// Source resolution mode. Leave unset to let the app root and the + /// PINGET_SOURCE_MODE environment variable decide; any explicit value, + /// including , wins over both. + /// + public SourceMode? SourceMode { get; init; } public string UserAgent { get; init; } = "pinget-dotnet/0.1"; public Action? Diagnostics { get; init; } diff --git a/dotnet/src/Devolutions.Pinget.Core/Repository.cs b/dotnet/src/Devolutions.Pinget.Core/Repository.cs index e14e503..9fb53dc 100644 --- a/dotnet/src/Devolutions.Pinget.Core/Repository.cs +++ b/dotnet/src/Devolutions.Pinget.Core/Repository.cs @@ -12,6 +12,7 @@ namespace Devolutions.Pinget.Core; public class Repository : IDisposable { private const string AppRootEnvironmentVariable = "PINGET_APPROOT"; + private const string SourceModeEnvironmentVariable = "PINGET_SOURCE_MODE"; internal const string InstalledStateUnsupportedWarning = "Installed package discovery is not supported on this platform; returning no installed packages."; internal const string InstallUnsupportedWarning = "Installing packages is not supported on this platform; no changes were made."; @@ -65,9 +66,7 @@ public static Repository Open(RepositoryOptions? options = null) var requestedAppRoot = options.AppRoot ?? Environment.GetEnvironmentVariable(AppRootEnvironmentVariable); var appRoot = SourceStoreManager.NormalizeAppRoot(requestedAppRoot); SourceStoreManager.EnsureAppDirs(appRoot); - var requestedSourceMode = options.SourceMode == SourceMode.Auto && requestedAppRoot is not null - ? SourceMode.Private - : options.SourceMode; + var requestedSourceMode = ResolveRequestedSourceMode(options.SourceMode, requestedAppRoot); var (sourceMode, store) = SourceStoreManager.LoadEffective(appRoot, requestedSourceMode); var client = new HttpClient(new SocketsHttpHandler { @@ -79,6 +78,46 @@ public static Repository Open(RepositoryOptions? options = null) return new Repository(appRoot, client, store, sourceMode, options.Diagnostics, options.PreIndexedSourceAutoUpdateInterval); } + internal static SourceMode ResolveRequestedSourceMode(SourceMode? configuredSourceMode, string? requestedAppRoot) => + ResolveRequestedSourceMode( + configuredSourceMode, + requestedAppRoot, + Environment.GetEnvironmentVariable(SourceModeEnvironmentVariable)); + + internal static SourceMode ResolveRequestedSourceMode( + SourceMode? configuredSourceMode, + string? requestedAppRoot, + string? sourceModeFromEnvironment) + { + if (configuredSourceMode is { } explicitSourceMode) + return explicitSourceMode; + + if (TryParseSourceMode(sourceModeFromEnvironment, out var parsedSourceMode)) + return parsedSourceMode; + + return requestedAppRoot is null ? SourceMode.Auto : SourceMode.Private; + } + + internal static bool TryParseSourceMode(string? value, out SourceMode sourceMode) + { + switch (value?.Trim().ToLowerInvariant()) + { + case "auto": + sourceMode = SourceMode.Auto; + return true; + case "private": + sourceMode = SourceMode.Private; + return true; + case "system-winget-mirror": + case "systemwingetmirror": + sourceMode = SourceMode.SystemWingetMirror; + return true; + default: + sourceMode = SourceMode.Auto; + return false; + } + } + internal static IEnumerable GetSqliteNativeLibraryCandidates(string assemblyDirectory) { yield return Path.Combine(assemblyDirectory, "e_sqlite3.dll"); diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 index 040e78f..ceeec40 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Cmdlets/ModuleFiles/Devolutions.Pinget.Client.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'Devolutions.Pinget.Client.psm1' - ModuleVersion = '0.10.0' + ModuleVersion = '0.11.0' CompatiblePSEditions = @('Desktop', 'Core') GUID = 'c6d1b5f2-5ccd-4771-9480-25caad7c58bd' Author = 'Devolutions' diff --git a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs index 23bef94..b35780e 100644 --- a/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs +++ b/dotnet/src/Devolutions.Pinget.PowerShell.Engine/PowerShellEngineVersion.cs @@ -2,5 +2,5 @@ namespace Devolutions.Pinget.PowerShell.Engine; public static class PowerShellEngineVersion { - public const string Current = "0.10.0"; + public const string Current = "0.11.0"; } diff --git a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj index fd4eb82..88d4ffb 100644 --- a/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj +++ b/nuget/Devolutions.Pinget.Cli.DotNet/Devolutions.Pinget.Cli.DotNet.csproj @@ -1,7 +1,7 @@ - 0.10.0 + 0.11.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.DotNet diff --git a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj index 7b5639e..3ee2ebe 100644 --- a/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj +++ b/nuget/Devolutions.Pinget.Cli.Rust/Devolutions.Pinget.Cli.Rust.csproj @@ -1,7 +1,7 @@ - 0.10.0 + 0.11.0 Devolutions Inc. Devolutions Devolutions.Pinget.Cli.Rust diff --git a/rust/crates/pinget-cli/Cargo.toml b/rust/crates/pinget-cli/Cargo.toml index 98c702c..2a81cef 100644 --- a/rust/crates/pinget-cli/Cargo.toml +++ b/rust/crates/pinget-cli/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-cli" -version = "0.10.0" +version = "0.11.0" edition = "2024" [lints] @@ -13,7 +13,7 @@ path = "src/main.rs" [dependencies] anyhow = "1.0.102" clap = { version = "4.6.1", features = ["derive"] } -pinget-core = { version = "0.10.0", path = "../pinget-core" } +pinget-core = { version = "0.11.0", path = "../pinget-core" } chrono = "0.4.44" dirs = "6.0" jsonschema = { version = "0.30", default-features = false, features = ["resolve-file"] } diff --git a/rust/crates/pinget-cli/tests/source_mode_env.rs b/rust/crates/pinget-cli/tests/source_mode_env.rs new file mode 100644 index 0000000..d1426a6 --- /dev/null +++ b/rust/crates/pinget-cli/tests/source_mode_env.rs @@ -0,0 +1,61 @@ +// The observable assertion needs a real `winget` to mirror from, so the whole file is +// Windows-only. Gating at the crate level rather than per-test keeps the helpers and +// imports from being dead code on other platforms. Parsing and precedence are covered +// by pinget-core unit tests on every platform. +#![cfg(windows)] + +//! Covers the `PINGET_SOURCE_MODE` wiring through a real process. +//! +//! This lives here rather than as a unit test because asserting on an environment +//! variable requires setting one, and `std::env::set_var` may not race with any +//! concurrent read of the environment. The core test binary is multi-threaded, so a +//! child process is the only way to satisfy that precondition. + +use std::path::{Path, PathBuf}; +use std::process::Command; + +fn temp_app_root(name: &str) -> PathBuf { + let root = std::env::temp_dir() + .join("pinget-cli-tests") + .join(format!("{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&root); + std::fs::create_dir_all(&root).expect("create temp app root"); + root +} + +fn run_source_list(app_root: &Path, source_mode: Option<&str>) { + let mut command = Command::new(env!("CARGO_BIN_EXE_pinget")); + command.args(["source", "list"]).env("PINGET_APPROOT", app_root); + + match source_mode { + Some(value) => command.env("PINGET_SOURCE_MODE", value), + None => command.env_remove("PINGET_SOURCE_MODE"), + }; + + let output = command.output().expect("run pinget source list"); + assert!( + output.status.success(), + "pinget source list failed: {}", + String::from_utf8_lossy(&output.stderr) + ); +} + +#[test] +fn source_mode_environment_variable_overrides_the_custom_app_root_default() { + let app_root = temp_app_root("with-source-mode"); + run_source_list(&app_root, Some("auto")); + assert!( + app_root.join("system-sources.json").exists(), + "expected PINGET_SOURCE_MODE=auto to mirror the system WinGet sources" + ); +} + +#[test] +fn a_custom_app_root_alone_still_selects_the_private_store() { + let app_root = temp_app_root("without-source-mode"); + run_source_list(&app_root, None); + assert!( + !app_root.join("system-sources.json").exists(), + "expected a custom app root alone to keep using the private source store" + ); +} diff --git a/rust/crates/pinget-com/Cargo.toml b/rust/crates/pinget-com/Cargo.toml index 7e4a36f..bf0129f 100644 --- a/rust/crates/pinget-com/Cargo.toml +++ b/rust/crates/pinget-com/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-com" -version = "0.10.0" +version = "0.11.0" edition = "2024" description = "Windows-only native COM bridge for Pinget backed by pinget-core." license = "MIT" diff --git a/rust/crates/pinget-core/Cargo.toml b/rust/crates/pinget-core/Cargo.toml index 2332fb9..1ff3f35 100644 --- a/rust/crates/pinget-core/Cargo.toml +++ b/rust/crates/pinget-core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pinget-core" -version = "0.10.0" +version = "0.11.0" edition = "2024" description = "Pure Rust Pinget core library that works directly with source caches, REST endpoints, and installed package state without COM." license = "MIT" diff --git a/rust/crates/pinget-core/src/lib.rs b/rust/crates/pinget-core/src/lib.rs index 7de2cda..8e7f81c 100644 --- a/rust/crates/pinget-core/src/lib.rs +++ b/rust/crates/pinget-core/src/lib.rs @@ -50,6 +50,8 @@ const DEFAULT_USER_AGENT: &str = "pinget-rs/0.1"; const DEFAULT_PREINDEXED_AUTO_UPDATE_MINUTES: i64 = 15; const PREINDEXED_REFRESH_RETRY_MINUTES: i64 = 5; const SYSTEM_WINGET_MIRROR_STORE_FILE_NAME: &str = "system-sources.json"; +const APP_ROOT_ENV_VAR: &str = "PINGET_APPROOT"; +const SOURCE_MODE_ENV_VAR: &str = "PINGET_SOURCE_MODE"; #[cfg(windows)] const PACKAGED_FAMILY_NAME: &str = "Microsoft.DesktopAppInstaller_8wekyb3d8bbwe"; #[cfg(windows)] @@ -172,6 +174,29 @@ pub enum SourceMode { SystemWingetMirror, } +impl SourceMode { + fn parse(value: &str) -> Option { + match value.trim().to_ascii_lowercase().as_str() { + "auto" => Some(Self::Auto), + "private" => Some(Self::Private), + "system-winget-mirror" | "systemwingetmirror" => Some(Self::SystemWingetMirror), + _ => None, + } + } + + fn from_env() -> Option { + Self::parse(&std::env::var(SOURCE_MODE_ENV_VAR).ok()?) + } + + fn resolve(from_env: Option, has_app_root_override: bool) -> Self { + from_env.unwrap_or(if has_app_root_override { + Self::Private + } else { + Self::Auto + }) + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum EffectiveSourceMode { Private, @@ -193,11 +218,7 @@ impl RepositoryOptions { /// Uses the default per-user app-data root that the CLI also uses. pub fn for_current_user() -> Result { - let source_mode = if std::env::var_os("PINGET_APPROOT").is_some() { - SourceMode::Private - } else { - SourceMode::Auto - }; + let source_mode = SourceMode::resolve(SourceMode::from_env(), std::env::var_os(APP_ROOT_ENV_VAR).is_some()); Ok(Self::new(default_app_root()?).with_source_mode(source_mode)) } @@ -5044,7 +5065,7 @@ fn ensure_app_dirs(app_root: &Path) -> Result<()> { } fn default_app_root() -> Result { - if let Some(app_root) = std::env::var_os("PINGET_APPROOT") { + if let Some(app_root) = std::env::var_os(APP_ROOT_ENV_VAR) { return Ok(PathBuf::from(app_root)); } @@ -10642,7 +10663,7 @@ fn write_portable_arp_entry(entry: &PortableArpEntry<'_>) -> Result<()> { #[cfg(test)] mod tests { - use std::io::{Read as _, Write}; + use std::io::Write; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering as AtomicOrdering}; @@ -11147,21 +11168,62 @@ mod tests { ); } + #[test] + fn source_mode_parses_accepted_spellings() { + assert_eq!(SourceMode::parse("auto"), Some(SourceMode::Auto)); + assert_eq!(SourceMode::parse(" Auto "), Some(SourceMode::Auto)); + assert_eq!(SourceMode::parse("PRIVATE"), Some(SourceMode::Private)); + assert_eq!( + SourceMode::parse("system-winget-mirror"), + Some(SourceMode::SystemWingetMirror) + ); + assert_eq!( + SourceMode::parse("SystemWingetMirror"), + Some(SourceMode::SystemWingetMirror) + ); + } + + #[test] + fn source_mode_rejects_unknown_spellings() { + assert_eq!(SourceMode::parse(""), None); + assert_eq!(SourceMode::parse("mirror"), None); + assert_eq!(SourceMode::parse("system_winget_mirror"), None); + } + + #[test] + fn source_mode_resolution_keeps_private_default_for_a_custom_app_root() { + assert_eq!(SourceMode::resolve(None, true), SourceMode::Private); + assert_eq!(SourceMode::resolve(None, false), SourceMode::Auto); + } + + #[test] + fn source_mode_resolution_lets_the_environment_override_a_custom_app_root() { + assert_eq!(SourceMode::resolve(Some(SourceMode::Auto), true), SourceMode::Auto); + assert_eq!( + SourceMode::resolve(Some(SourceMode::SystemWingetMirror), true), + SourceMode::SystemWingetMirror + ); + assert_eq!( + SourceMode::resolve(Some(SourceMode::Private), false), + SourceMode::Private + ); + } + #[cfg(windows)] #[test] fn default_app_root_outside_package_avoids_packaged_layout() { // Tests run without AppX package identity, so the default app root must not // resolve to the WinGet packaged LocalState — that location requires a // brokered/elevated writer for its secure-settings stream. - let prior = std::env::var_os("PINGET_APPROOT"); + let prior = std::env::var_os(APP_ROOT_ENV_VAR); // SAFETY: tests in this module are not run concurrently with other env mutators. - unsafe { std::env::remove_var("PINGET_APPROOT") }; + unsafe { std::env::remove_var(APP_ROOT_ENV_VAR) }; let result = default_app_root(); if let Some(prior) = prior { // SAFETY: restoring the prior value before assertions panic, single-threaded test. - unsafe { std::env::set_var("PINGET_APPROOT", prior) }; + unsafe { std::env::set_var(APP_ROOT_ENV_VAR, prior) }; } let app_root = result.expect("default app root");