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");