diff --git a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
index 4643d416..d6ee77d4 100644
--- a/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
+++ b/src/c#/GeneralUpdate.Core/Bootstrap/GeneralUpdateBootstrap.cs
@@ -280,22 +280,36 @@ private void InitializeFromEnvironment()
};
}
+ ///
+ /// Applies UpdateOptions to _configInfo.
+ /// Uses ??= only for values that InitializeFromEnvironment() may have already
+ /// populated on the Upgrade path (Encoding, Format, DownloadTimeOut).
+ /// All other options are always applied from UpdateOptions — their defaults
+ /// are already functionally reasonable (e.g. MaxConcurrency=3, RetryCount=3).
+ ///
private void ApplyRuntimeOptions()
{
- _configInfo.Encoding = GetOption(UpdateOptions.Encoding);
- _configInfo.Format = GetOption(UpdateOptions.Format);
- _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
-
- // Download behaviour
+ // Preserve Upgrade path values set by InitializeFromEnvironment()
+ _configInfo.Encoding ??= GetOption(UpdateOptions.Encoding);
+ _configInfo.Format ??= GetOption(UpdateOptions.Format);
+ // Normalize legacy "ZIP" default (UpdateOptions) to Format.ZIP (".zip")
+ // so the pipeline constructs correct paths and CompressProvider matches its switch.
+ if (_configInfo.Format == "ZIP")
+ _configInfo.Format = Format.ZIP;
+ if (_configInfo.DownloadTimeOut <= 0)
+ _configInfo.DownloadTimeOut = GetOption(UpdateOptions.DownloadTimeout) ?? 60;
+
+ // bool? options: use ??= so user-configured false is preserved
+ _configInfo.PatchEnabled ??= GetOption(UpdateOptions.PatchEnabled);
+ _configInfo.BackupEnabled ??= GetOption(UpdateOptions.BackupEnabled);
+
+ // Always apply from UpdateOptions — no other code sets these before
+ // ApplyRuntimeOptions() runs. Defaults are functionally reasonable.
_configInfo.MaxConcurrency = GetOption(UpdateOptions.MaxConcurrency);
_configInfo.EnableResume = GetOption(UpdateOptions.EnableResume);
_configInfo.RetryCount = GetOption(UpdateOptions.RetryCount);
_configInfo.RetryInterval = GetOption(UpdateOptions.RetryInterval);
_configInfo.VerifyChecksum = GetOption(UpdateOptions.VerifyChecksum);
-
- // Update behaviour
- _configInfo.BackupEnabled = GetOption(UpdateOptions.BackupEnabled);
- _configInfo.PatchEnabled = GetOption(UpdateOptions.PatchEnabled);
_configInfo.DiffMode = GetOption(UpdateOptions.DiffMode);
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
index 0810c2b4..41a5a6ac 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/ClientUpdateStrategy.cs
@@ -89,16 +89,13 @@ public ClientUpdateStrategy UseUpdatePrecheck(Func fu
private async Task ExecuteWorkflowAsync()
{
- var defaultEncoding = Encoding.UTF8;
- var defaultTimeout = 60;
- if (true /* silent check would read from options */)
- {
- // Standard mode
- await ExecuteStandardWorkflowAsync(defaultEncoding, defaultTimeout);
- }
+ // Standard mode — silent mode is handled by GeneralUpdateBootstrap.LaunchSilentAsync().
+ // Runtime options (Encoding, Format, DownloadTimeOut, etc.) are already
+ // populated on _configInfo by Bootstrap.ApplyRuntimeOptions().
+ await ExecuteStandardWorkflowAsync();
}
- private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
+ private async Task ExecuteStandardWorkflowAsync()
{
GeneralTracer.Info($"ClientUpdateStrategy: validating client={_configInfo!.ClientVersion}, upgrade={_configInfo.UpgradeClientVersion}");
@@ -145,7 +142,6 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
await SafeReportUpdateStartedAsync(hooksCtx).ConfigureAwait(false);
InitBlackList();
- ApplyRuntimeOptions(encoding, timeout);
_configInfo.TempPath = StorageManager.GetTempDirectory("main_temp");
_configInfo.BackupDirectory = Path.Combine(_configInfo.InstallPath,
@@ -172,7 +168,7 @@ private async Task ExecuteStandardWorkflowAsync(Encoding encoding, int timeout)
Hash = a.SHA256,
Url = a.Url,
Version = a.Version,
- Format = "ZIP"
+ Format = _configInfo.Format ?? "ZIP"
}).ToList();
_configInfo.ProcessInfo = JsonSerializer.Serialize(
@@ -236,13 +232,6 @@ private static IStrategy ResolveOsStrategy()
throw new PlatformNotSupportedException("The current operating system is not supported!");
}
- private void ApplyRuntimeOptions(Encoding encoding, int timeout)
- {
- _configInfo!.Encoding = encoding;
- _configInfo.Format = Format.ZIP;
- _configInfo.DownloadTimeOut = timeout;
- }
-
private void InitBlackList()
{
BlackListManager.Instance.AddBlackFiles(_configInfo!.BlackFiles);
diff --git a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
index 70db1b8d..c7d6528a 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/OSSUpdateStrategy.cs
@@ -29,7 +29,7 @@ public class OSSUpdateStrategy : IStrategy
{
private GlobalConfigInfo? _configInfo;
private readonly string _appPath = AppDomain.CurrentDomain.BaseDirectory;
- private const int TimeOut = 60;
+ private const int DefaultTimeOut = 60;
/// Lifecycle hooks injected by the bootstrap.
public Hooks.IUpdateHooks Hooks { get; set; } = new Hooks.NoOpUpdateHooks();
@@ -157,7 +157,7 @@ private async Task DownloadAssetsAsync(List assets)
}
else
{
- using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(TimeOut) };
+ using var httpClient = new HttpClient { Timeout = TimeSpan.FromSeconds(_configInfo.DownloadTimeOut > 0 ? _configInfo.DownloadTimeOut : DefaultTimeOut) };
var orchestrator = new DefaultDownloadOrchestrator(httpClient);
await orchestrator.ExecuteAsync(plan, _appPath).ConfigureAwait(false);
}
diff --git a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
index f0354a4d..90ecf53a 100644
--- a/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
+++ b/src/c#/GeneralUpdate.Core/Strategy/UpgradeUpdateStrategy.cs
@@ -52,7 +52,6 @@ public async Task ExecuteAsync()
return;
}
- ApplyRuntimeOptions();
_osStrategy!.Create(_configInfo);
// Apply updates via OS-specific pipeline (Hash -> Compress -> Patch)
@@ -109,12 +108,6 @@ private static IStrategy ResolveOsStrategy()
throw new PlatformNotSupportedException("The current operating system is not supported!");
}
- private void ApplyRuntimeOptions()
- {
- _configInfo!.Encoding = Encoding.UTF8;
- _configInfo.Format = Format.ZIP;
- }
-
// ════════════════════════════════════════════════════════════════
// Hooks & Reporter safe wrappers
// ════════════════════════════════════════════════════════════════
diff --git a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
index 48748e32..5af84aa3 100644
--- a/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
+++ b/tests/CoreTest/Bootstrap/BootstrapFullParameterMatrixTests.cs
@@ -1,6 +1,10 @@
using System;
+using System.Collections.Generic;
using System.IO;
+using System.Net.Http;
using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
using GeneralUpdate.Core;
using GeneralUpdate.Core.Configuration;
using GeneralUpdate.Core.FileSystem;
@@ -78,6 +82,133 @@ [Fact] public void Hub_Configured() => Assert.NotNull(B().Option(UpdateOptions.H
new HubConfig { Url = "https://signalr.example.com/hub" }));
#endregion
+ #region Extension Injection — Hooks / Strategy / Policy / Differ / Pipeline / etc.
+
+ private sealed class StubHooks : GeneralUpdate.Core.Hooks.IUpdateHooks
+ {
+ public Task OnBeforeUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.FromResult(true);
+ public Task OnDownloadCompletedAsync(GeneralUpdate.Core.Hooks.DownloadContext ctx) => Task.CompletedTask;
+ public Task OnAfterUpdateAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask;
+ public Task OnUpdateErrorAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx, Exception ex) => Task.CompletedTask;
+ public Task OnBeforeStartAppAsync(GeneralUpdate.Core.Hooks.UpdateContext ctx) => Task.CompletedTask;
+ }
+
+ private sealed class StubStrategy : GeneralUpdate.Core.Strategy.IStrategy
+ {
+ public void Create(GlobalConfigInfo parameter) { }
+ public void Execute() { }
+ public Task ExecuteAsync() => Task.CompletedTask;
+ public void StartApp() { }
+ }
+
+ private sealed class StubSslPolicy : GeneralUpdate.Core.Security.ISslValidationPolicy
+ {
+ public bool ValidateCertificate(System.Security.Cryptography.X509Certificates.X509Certificate2? certificate,
+ System.Security.Cryptography.X509Certificates.X509Chain? chain,
+ System.Net.Security.SslPolicyErrors sslPolicyErrors) => true;
+ }
+
+ private sealed class StubBinaryDiffer : GeneralUpdate.Core.Differential.IBinaryDiffer
+ {
+ public Task CleanAsync(string oldFilePath, string newFilePath, string patchFilePath,
+ CancellationToken cancellationToken = default) => Task.CompletedTask;
+ public Task DirtyAsync(string oldFilePath, string newFilePath, string patchFilePath,
+ CancellationToken cancellationToken = default) => Task.CompletedTask;
+ }
+
+ private sealed class StubPipelineFactory : GeneralUpdate.Core.Pipeline.IUpdatePipelineFactory
+ {
+ public Task ExecutePipelineAsync(GeneralUpdate.Core.Pipeline.PipelineContext context, CancellationToken token = default) => Task.CompletedTask;
+ }
+
+ private sealed class StubDownloadPolicy : GeneralUpdate.Core.Download.Abstractions.IDownloadPolicy
+ {
+ public Task ExecuteAsync(Func> action, CancellationToken token = default) => action(token);
+ }
+
+ private sealed class StubDownloadExecutor : GeneralUpdate.Core.Download.Abstractions.IDownloadExecutor
+ {
+ public Task ExecuteAsync(string url, string destPath,
+ IProgress? progress = null, CancellationToken token = default)
+ => Task.FromResult(new GeneralUpdate.Core.Download.Models.DownloadResult(url, destPath, 0, TimeSpan.Zero, 0, true, null));
+ }
+
+ private sealed class StubDownloadSource : GeneralUpdate.Core.Download.Abstractions.IDownloadSource
+ {
+ public Task> ListAsync(CancellationToken token = default)
+ => Task.FromResult>(Array.Empty());
+ }
+
+ private sealed class StubDownloadPipeline : GeneralUpdate.Core.Download.Abstractions.IDownloadPipeline
+ {
+ public Task ProcessAsync(string downloadedPath, CancellationToken token = default) => Task.FromResult("");
+ }
+
+ private sealed class StubUpdateReporter : GeneralUpdate.Core.Download.Reporting.IUpdateReporter
+ {
+ public Task ReportAsync(GeneralUpdate.Core.Download.Reporting.UpdateReport report, CancellationToken token = default) => Task.CompletedTask;
+ }
+
+ private sealed class StubUpdateAuth : GeneralUpdate.Core.Security.IHttpAuthProvider
+ {
+ public Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default) => Task.CompletedTask;
+ }
+
+ private sealed class StubDownloadOrchestrator : GeneralUpdate.Core.Download.Abstractions.IDownloadOrchestrator
+ {
+ public Task ExecuteAsync(
+ GeneralUpdate.Core.Download.Models.DownloadPlan plan, string destDir, int maxConcurrency = 3,
+ IProgress? progress = null, CancellationToken token = default)
+ => Task.FromResult(new GeneralUpdate.Core.Download.Abstractions.DownloadReport(Array.Empty(), 0, TimeSpan.Zero, 0, 0));
+ }
+
+ private sealed class StubCleanStrategy : GeneralUpdate.Core.Differential.ICleanStrategy
+ {
+ public Task ExecuteAsync(string sourcePath, string targetPath, string patchPath) => Task.CompletedTask;
+ }
+
+ private sealed class StubDirtyStrategy : GeneralUpdate.Core.Differential.IDirtyStrategy
+ {
+ public Task ExecuteAsync(string appPath, string patchPath) => Task.CompletedTask;
+ }
+
+ [Fact] public void Inject_Hooks() => Assert.NotNull(B().Hooks());
+ [Fact] public void Inject_Strategy() => Assert.NotNull(B().Strategy());
+ [Fact] public void Inject_SslPolicy() => Assert.NotNull(B().SslPolicy());
+ [Fact] public void Inject_BinaryDiffer() => Assert.NotNull(B().BinaryDiffer());
+ [Fact] public void Inject_PipelineFactory() => Assert.NotNull(B().PipelineFactory());
+ [Fact] public void Inject_DownloadPolicy() => Assert.NotNull(B().DownloadPolicy());
+ [Fact] public void Inject_DownloadExecutor() => Assert.NotNull(B().DownloadExecutor());
+ [Fact] public void Inject_DownloadSource() => Assert.NotNull(B().DownloadSource());
+ [Fact] public void Inject_DownloadPipeline() => Assert.NotNull(B().DownloadPipeline());
+ [Fact] public void Inject_UpdateReporter() => Assert.NotNull(B().UpdateReporter());
+ [Fact] public void Inject_UpdateAuth() => Assert.NotNull(B().UpdateAuth());
+ [Fact] public void Inject_DownloadOrchestrator() => Assert.NotNull(B().DownloadOrchestrator());
+ [Fact] public void Inject_CleanStrategy() => Assert.NotNull(B().CleanStrategy());
+ [Fact] public void Inject_DirtyStrategy() => Assert.NotNull(B().DirtyStrategy());
+
+ [Fact]
+ public void Chain_AllExtensionsInjected()
+ {
+ var b = B()
+ .Hooks()
+ .UpdateReporter()
+ .DownloadPolicy()
+ .DownloadExecutor()
+ .DownloadSource()
+ .DownloadPipeline()
+ .DownloadOrchestrator()
+ .BinaryDiffer()
+ .CleanStrategy()
+ .DirtyStrategy()
+ .SslPolicy()
+ .UpdateAuth()
+ .PipelineFactory()
+ .Strategy();
+ Assert.NotNull(b);
+ }
+ #endregion
+
#region Full Combination Chains
[Fact] public void Chain_AllFrameworkOptions()
{