From 0f0176e1ed4dc31258b30cbd8a4ebb16cfe838b1 Mon Sep 17 00:00:00 2001
From: mleem97 <52848568+mleem97@users.noreply.github.com>
Date: Thu, 21 May 2026 13:10:20 +0000
Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A7=AA=20Add=20SubDirectoryFixerInsta?=
=?UTF-8?q?llerService=20tests?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../GregModmanager.Tests.csproj | 3 +
.../SubDirectoryFixerInstallerServiceTests.cs | 150 ++++++++++++++++++
2 files changed, 153 insertions(+)
create mode 100644 tests/GregModmanager.Tests/SubDirectoryFixerInstallerServiceTests.cs
diff --git a/tests/GregModmanager.Tests/GregModmanager.Tests.csproj b/tests/GregModmanager.Tests/GregModmanager.Tests.csproj
index 29869cb..c7c27ec 100644
--- a/tests/GregModmanager.Tests/GregModmanager.Tests.csproj
+++ b/tests/GregModmanager.Tests/GregModmanager.Tests.csproj
@@ -17,6 +17,9 @@
+
+
+
diff --git a/tests/GregModmanager.Tests/SubDirectoryFixerInstallerServiceTests.cs b/tests/GregModmanager.Tests/SubDirectoryFixerInstallerServiceTests.cs
new file mode 100644
index 0000000..a3233fc
--- /dev/null
+++ b/tests/GregModmanager.Tests/SubDirectoryFixerInstallerServiceTests.cs
@@ -0,0 +1,150 @@
+using GregModmanager.Avalonia.Services;
+using System.Security.Cryptography;
+
+namespace GregModmanager.Tests;
+
+public class SubDirectoryFixerInstallerServiceTests : IDisposable
+{
+ private readonly string _tempGameRoot;
+
+ public SubDirectoryFixerInstallerServiceTests()
+ {
+ _tempGameRoot = Path.Combine(Path.GetTempPath(), "GregModmanager_Tests_" + Guid.NewGuid().ToString());
+
+ // Note: The service uses Path.Combine(AppContext.BaseDirectory, "SubDirectoryFixer\\SubDirectoryFixer.dll").
+ // On Linux, Path.Combine does not translate '\' to '/', so it attempts to find a file literally named
+ // "SubDirectoryFixer\SubDirectoryFixer.dll" in the BaseDirectory.
+ // We create it here to ensure the tests pass on Linux when run in the CI/headless environment.
+ var payloadPath = Path.Combine(AppContext.BaseDirectory, "SubDirectoryFixer\\SubDirectoryFixer.dll");
+ var dirPath = Path.GetDirectoryName(payloadPath);
+ if (!string.IsNullOrEmpty(dirPath))
+ {
+ Directory.CreateDirectory(dirPath);
+ }
+ if (!File.Exists(payloadPath))
+ {
+ File.WriteAllText(payloadPath, "dummy-payload-content");
+ }
+ }
+
+ public void Dispose()
+ {
+ if (Directory.Exists(_tempGameRoot))
+ {
+ try
+ {
+ Directory.Delete(_tempGameRoot, true);
+ }
+ catch
+ {
+ // Ignore cleanup errors
+ }
+ }
+ }
+
+ [Theory]
+ [InlineData(null)]
+ [InlineData("")]
+ [InlineData(" ")]
+ public async Task EnsureInstalledAsync_WithInvalidGameRoot_ReturnsSkippedNoGameRoot(string? gameRoot)
+ {
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(gameRoot);
+
+ Assert.Equal(SubDirectoryFixerInstallStatus.SkippedNoGameRoot, result.Status);
+ }
+
+ [Fact]
+ public async Task EnsureInstalledAsync_WithNonExistentGameRoot_ReturnsSkippedNoGameRoot()
+ {
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ Assert.Equal(SubDirectoryFixerInstallStatus.SkippedNoGameRoot, result.Status);
+ }
+
+ [Fact]
+ public async Task EnsureInstalledAsync_WithValidGameRoot_InstallsSuccessfully()
+ {
+ // Arrange
+ Directory.CreateDirectory(_tempGameRoot);
+
+ // Act
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Assert
+ Assert.Equal(SubDirectoryFixerInstallStatus.Installed, result.Status);
+
+ var pluginsDir = Path.Combine(_tempGameRoot, "Plugins");
+ var targetFile = Path.Combine(pluginsDir, "SubDirectoryFixer.dll");
+ var markerFile = Path.Combine(pluginsDir, ".gregmodmanager-subdirfixer.sha256");
+
+ Assert.True(File.Exists(targetFile));
+ Assert.True(File.Exists(markerFile));
+ }
+
+ [Fact]
+ public async Task EnsureInstalledAsync_AlreadyInstalled_ReturnsAlreadyInstalled()
+ {
+ // Arrange
+ Directory.CreateDirectory(_tempGameRoot);
+
+ // First install
+ await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Act - Second install
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Assert
+ Assert.Equal(SubDirectoryFixerInstallStatus.AlreadyInstalled, result.Status);
+ }
+
+ [Fact]
+ public async Task EnsureInstalledAsync_MarkerHashMismatch_UpdatesSuccessfully()
+ {
+ // Arrange
+ Directory.CreateDirectory(_tempGameRoot);
+
+ // First install
+ await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Tamper with marker file
+ var pluginsDir = Path.Combine(_tempGameRoot, "Plugins");
+ var markerFile = Path.Combine(pluginsDir, ".gregmodmanager-subdirfixer.sha256");
+
+ // Make sure Plugins exists in case the first call failed
+ Directory.CreateDirectory(pluginsDir);
+ await File.WriteAllTextAsync(markerFile, "invalid-hash");
+
+ // Act - Update
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Assert
+ Assert.Equal(SubDirectoryFixerInstallStatus.Installed, result.Status);
+ var newMarkerHash = await File.ReadAllTextAsync(markerFile);
+ Assert.NotEqual("invalid-hash", newMarkerHash);
+ }
+
+ [Fact]
+ public async Task EnsureInstalledAsync_FileLocked_ReturnsFailedStatus()
+ {
+ // Arrange
+ Directory.CreateDirectory(_tempGameRoot);
+
+ // Create the directory structure where it will be installed
+ var pluginsDir = Path.Combine(_tempGameRoot, "Plugins");
+ Directory.CreateDirectory(pluginsDir);
+
+ // Create a dummy file and lock it
+ var targetFile = Path.Combine(pluginsDir, "SubDirectoryFixer.dll");
+ await File.WriteAllTextAsync(targetFile, "dummy");
+
+ // Lock the file for exclusive access
+ using var stream = new FileStream(targetFile, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
+
+ // Act
+ var result = await SubDirectoryFixerInstallerService.EnsureInstalledAsync(_tempGameRoot);
+
+ // Assert
+ Assert.Equal(SubDirectoryFixerInstallStatus.Failed, result.Status);
+ Assert.Contains("install failed", result.Message, StringComparison.OrdinalIgnoreCase);
+ }
+}
From 2a4a634c1405b5c92351083702eac18e69155282 Mon Sep 17 00:00:00 2001
From: mleem97 <52848568+mleem97@users.noreply.github.com>
Date: Thu, 21 May 2026 13:36:37 +0000
Subject: [PATCH 2/2] Fix CI build and serialization warnings
---
build/scripts/linux/build-avalonia-packages.sh | 2 +-
src/GregModmanager.Core/Models/AppJsonContext.cs | 1 +
src/GregModmanager.Core/Services/TelemetryService.cs | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
mode change 100644 => 100755 build/scripts/linux/build-avalonia-packages.sh
diff --git a/build/scripts/linux/build-avalonia-packages.sh b/build/scripts/linux/build-avalonia-packages.sh
old mode 100644
new mode 100755
index 6a35814..8a3b868
--- a/build/scripts/linux/build-avalonia-packages.sh
+++ b/build/scripts/linux/build-avalonia-packages.sh
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
-REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
PROJECT_PATH="$REPO_ROOT/src/GregModmanager.Avalonia/GregModmanager.Avalonia.csproj"
OUTPUT_ROOT="${1:-$REPO_ROOT/artifacts/avalonia-linux}"
VERSION="${2:-1.1.0}"
diff --git a/src/GregModmanager.Core/Models/AppJsonContext.cs b/src/GregModmanager.Core/Models/AppJsonContext.cs
index d7fe96d..8a3cc3c 100644
--- a/src/GregModmanager.Core/Models/AppJsonContext.cs
+++ b/src/GregModmanager.Core/Models/AppJsonContext.cs
@@ -34,6 +34,7 @@ namespace GregModmanager.Models;
[JsonSerializable(typeof(int))]
[JsonSerializable(typeof(RalphTaskStatus))]
[JsonSerializable(typeof(AssetModMetadata))]
+[JsonSerializable(typeof(object))]
public partial class AppJsonContext : JsonSerializerContext
{
}
diff --git a/src/GregModmanager.Core/Services/TelemetryService.cs b/src/GregModmanager.Core/Services/TelemetryService.cs
index 8531b66..dc74657 100644
--- a/src/GregModmanager.Core/Services/TelemetryService.cs
+++ b/src/GregModmanager.Core/Services/TelemetryService.cs
@@ -99,7 +99,7 @@ public async Task TrackEventAsync(string eventName, object payload, Dictionary JsonSerializer.Serialize(sync, AppJsonContext.Default.SyncCollectionEvent),
- _ => JsonSerializer.Serialize(payload, payload.GetType(), AppJsonContext.Default.Options)
+ _ => JsonSerializer.Serialize((object)payload, AppJsonContext.Default.Object)
};
await PushToLokiAsync(eventName, message, labels);