From 0c699240e04c72b020fbdb3c9bc35c215ac5c22c Mon Sep 17 00:00:00 2001 From: Erik Date: Tue, 25 Aug 2026 19:38:10 +0200 Subject: [PATCH] fix(ci): make release gates portable and deterministic --- .../UI/Layout/CharacterStatController.cs | 12 ++++++------ .../Installation/LauncherContentStateStore.cs | 2 ++ .../UI/Layout/CharacterStatControllerTests.cs | 19 ++++++++++--------- .../PreparedAssetVerificationCacheTests.cs | 7 +++++++ 4 files changed, 25 insertions(+), 15 deletions(-) diff --git a/src/AcDream.App/UI/Layout/CharacterStatController.cs b/src/AcDream.App/UI/Layout/CharacterStatController.cs index 550eb897..dd48e83d 100644 --- a/src/AcDream.App/UI/Layout/CharacterStatController.cs +++ b/src/AcDream.App/UI/Layout/CharacterStatController.cs @@ -2083,12 +2083,12 @@ public static class CharacterStatController long skillCost = skill.AdvancementClass >= CharacterSkillAdvancementClass.Trained ? skill.RaiseCost : skill.TrainedCost; - return skillCost > 0 ? skillCost.ToString("N0") : "Infinity!"; + return skillCost > 0 ? FormatXp(skillCost) : "Infinity!"; } if (attrSel[0] < 0) return sheet.SkillCredits.ToString(); long cost = GetRaiseCost(sheet, attrSel[0]); - return cost > 0 ? cost.ToString("N0") : "Infinity!"; + return cost > 0 ? FormatXp(cost) : "Infinity!"; }); // Line-2 elements: pass null → keep dat font. @@ -2114,7 +2114,7 @@ public static class CharacterStatController if (skill is not null && skill.AdvancementClass < CharacterSkillAdvancementClass.Trained) return sheet.SkillCredits.ToString(); } - return sheet.UnassignedXp.ToString("N0"); + return FormatXp(sheet.UnassignedXp); }); BindSelectedFooterState(stateB); @@ -2166,12 +2166,12 @@ public static class CharacterStatController long skillCost = skill.AdvancementClass >= CharacterSkillAdvancementClass.Trained ? skill.RaiseCost : skill.TrainedCost; - return skillCost > 0 ? skillCost.ToString("N0") : "Infinity!"; + return skillCost > 0 ? FormatXp(skillCost) : "Infinity!"; } if (attrSel[0] < 0) return sheet.SkillCredits.ToString(); long cost = GetRaiseCost(sheet, attrSel[0]); - return cost > 0 ? cost.ToString("N0") : "Infinity!"; + return cost > 0 ? FormatXp(cost) : "Infinity!"; }); LabelProvider(TextById(state, FooterLine2Label), null, Body, () => @@ -2194,7 +2194,7 @@ public static class CharacterStatController if (skill is not null && skill.AdvancementClass < CharacterSkillAdvancementClass.Trained) return sheet.SkillCredits.ToString(); } - return sheet.UnassignedXp.ToString("N0"); + return FormatXp(sheet.UnassignedXp); }); } } diff --git a/src/AcDream.Launcher.Core/Installation/LauncherContentStateStore.cs b/src/AcDream.Launcher.Core/Installation/LauncherContentStateStore.cs index 2a14452b..6e0e5e84 100644 --- a/src/AcDream.Launcher.Core/Installation/LauncherContentStateStore.cs +++ b/src/AcDream.Launcher.Core/Installation/LauncherContentStateStore.cs @@ -363,6 +363,8 @@ public sealed class LauncherContentStateStore { if (string.IsNullOrWhiteSpace(path) || Path.IsPathFullyQualified(path) + || path.Contains('/') + || path.Contains('\\') || !string.Equals(path, Path.GetFileName(path), StringComparison.Ordinal) || path is "." or ".." || !path.EndsWith(".pak", StringComparison.OrdinalIgnoreCase)) diff --git a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs index 70d0913e..e80f46f3 100644 --- a/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/CharacterStatControllerTests.cs @@ -1,5 +1,6 @@ using AcDream.App.UI; using AcDream.App.UI.Layout; +using System.Globalization; using System.Numerics; namespace AcDream.App.Tests.UI.Layout; @@ -184,8 +185,8 @@ public class CharacterStatControllerTests Assert.Equal("Non-Player Killer", visiblePk.LinesProvider()[0].Text); Assert.Equal("126", visibleLevel.LinesProvider()[0].Text); Assert.Equal("Total Experience (XP):", visibleTotalXpLabel.LinesProvider()[0].Text); - Assert.Equal((1_250_000_000L).ToString("N0"), visibleTotalXp.LinesProvider()[0].Text); - Assert.Equal((42_000_000L).ToString("N0"), visibleXpNext.LinesProvider()[0].Text); + Assert.Equal((1_250_000_000L).ToString("N0", CultureInfo.InvariantCulture), visibleTotalXp.LinesProvider()[0].Text); + Assert.Equal((42_000_000L).ToString("N0", CultureInfo.InvariantCulture), visibleXpNext.LinesProvider()[0].Text); Assert.Empty(hiddenName.LinesProvider()); Assert.Empty(hiddenXpNext.LinesProvider()); Assert.Empty(hiddenPk.LinesProvider()); @@ -682,7 +683,7 @@ public class CharacterStatControllerTests CharacterStatController.Bind(layout, SampleData.SampleCharacter); - var expected = (87_757_321_741L).ToString("N0"); + var expected = (87_757_321_741L).ToString("N0", CultureInfo.InvariantCulture); Assert.Equal(expected, val.LinesProvider()[0].Text); } @@ -737,7 +738,7 @@ public class CharacterStatControllerTests Descendants(list).OfType().ToList()[4].OnClick!(); // Focus raise cost = 110 (SampleData fixture). - Assert.Equal((110L).ToString("N0"), val.LinesProvider()[0].Text); + Assert.Equal((110L).ToString("N0", CultureInfo.InvariantCulture), val.LinesProvider()[0].Text); } [Fact] @@ -768,7 +769,7 @@ public class CharacterStatControllerTests Descendants(list).OfType().ToList()[4].OnClick!(); // UnassignedXp = 87_757_321_741L - var expected = (87_757_321_741L).ToString("N0"); + var expected = (87_757_321_741L).ToString("N0", CultureInfo.InvariantCulture); Assert.Equal(expected, val.LinesProvider()[0].Text); } @@ -1443,9 +1444,9 @@ public class CharacterStatControllerTests // which was previously computed but never surfaced in the title text. Assert.Equal("War Magic: 285 (+5)", title.LinesProvider()[0].Text); Assert.Equal("Experience To Raise:", l1Label.LinesProvider()[0].Text); - Assert.Equal((11_100_000L).ToString("N0"), l1Value.LinesProvider()[0].Text); + Assert.Equal((11_100_000L).ToString("N0", CultureInfo.InvariantCulture), l1Value.LinesProvider()[0].Text); Assert.Equal("Unassigned Experience:", l2Label.LinesProvider()[0].Text); - Assert.Equal((87_757_321_741L).ToString("N0"), l2Value.LinesProvider()[0].Text); + Assert.Equal((87_757_321_741L).ToString("N0", CultureInfo.InvariantCulture), l2Value.LinesProvider()[0].Text); // CT5: RowHighlightSprite corrected to 0x06000F93 (see the sealed // verdict on RowClick_WithSpriteResolve_SelectedRowHasHighlightSprite // above); UseSelectionBars was retired outright (retail's actual @@ -2113,7 +2114,7 @@ public class CharacterStatControllerTests CharacterStatController.Bind(layout, SampleData.SampleCharacter); - Assert.Equal((1_250_000_000L).ToString("N0"), value.LinesProvider()[0].Text); + Assert.Equal((1_250_000_000L).ToString("N0", CultureInfo.InvariantCulture), value.LinesProvider()[0].Text); Assert.False(value.Centered); Assert.True(value.RightAligned); } @@ -2257,7 +2258,7 @@ public class CharacterStatControllerTests var lines = xpValue.LinesProvider(); Assert.Single(lines); // XpToNextLevel from SampleData = 42_000_000L formatted as "42,000,000" - Assert.Equal((42_000_000L).ToString("N0"), lines[0].Text); + Assert.Equal((42_000_000L).ToString("N0", CultureInfo.InvariantCulture), lines[0].Text); } /// diff --git a/tests/AcDream.Launcher.Core.Tests/Installation/PreparedAssetVerificationCacheTests.cs b/tests/AcDream.Launcher.Core.Tests/Installation/PreparedAssetVerificationCacheTests.cs index a2d5d479..f0bc6925 100644 --- a/tests/AcDream.Launcher.Core.Tests/Installation/PreparedAssetVerificationCacheTests.cs +++ b/tests/AcDream.Launcher.Core.Tests/Installation/PreparedAssetVerificationCacheTests.cs @@ -238,11 +238,18 @@ public sealed class PreparedAssetVerificationCacheTests : IDisposable // Crash shape: the verified package was moved aside and what sits in // its place is wrong. The cache still describes the ORIGINAL bytes, so // if recovery trusted it the launcher would accept a bad package. + DateTime verifiedWriteTime = File.GetLastWriteTimeUtc(store.PreparedAssetPath); string backup = LauncherInstallRecordStore.GetBackupPath(store.PreparedAssetPath); File.Move(store.PreparedAssetPath, backup); await File.WriteAllTextAsync( store.PreparedAssetPath, new string('z', PackageContent.Length)); + File.SetLastWriteTimeUtc( + store.PreparedAssetPath, + verifiedWriteTime.AddMinutes(1)); + Assert.NotEqual( + verifiedWriteTime, + File.GetLastWriteTimeUtc(store.PreparedAssetPath)); InstallRecordVerification verification = await store.LoadAndVerifyAsync();