The creator's profile still showed the previous Starting art after the replacement, with `Art.DefaultVersion = 4` recorded — so the version gate had already declared itself done. It compared a constant against the embedded resource, and those two were compiled at different moments (version 4 landed with the OLD image embedded; the corrected image reached the assembly one build later). A version bump can always be raced that way. The seed now compares BYTES: `LayoutStore.AssetMatches` hashes the bundled resource and checks the row the background already points at. Same bytes → leave it untouched (so this is idempotent and never churns the database). Different bytes → re-stamp. The version constant and its setting are gone, because a heal that only runs once can silently skip the one launch it was needed for. `SceneThumbnailTests` planted its own art on a background still flagged as default art — in the real app that state only ever means stale bundled art, because Browse… sets the path and clears the flag. The fixture now models a creator's own picture. Tests: 572 green, 0 warnings. Verified the profile's Starting asset hash (167CDD3B…, the two-category render) differs from the shipped `starting-background.jpg` (5FC6DD22…), so the next launch re-seeds it.
95 lines
4.2 KiB
C#
95 lines
4.2 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Microsoft.Data.Sqlite;
|
|
using Xunit;
|
|
using ytLive.Services;
|
|
using ytLive.ViewModels;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// 2026-11-06: replacing a bundled background reached new installs only. Default art is hash-keyed into the
|
|
/// layout database on first seed, so an existing profile kept the old image for ever. A version-stamp heal
|
|
/// failed twice (first it iterated `AllBackgrounds()`, which returns only the LIVE scene's rows; then the
|
|
/// constant and the embedded resource were built at different moments). The seed now compares BYTES, which
|
|
/// is what this test pins: a profile already pointing at other art is re-stamped without any version bump.
|
|
/// </summary>
|
|
[Collection("RealApp")]
|
|
public sealed class BackgroundDefaultArtTests
|
|
{
|
|
private readonly RealAppHost _app;
|
|
|
|
public BackgroundDefaultArtTests(RealAppHost app) => _app = app;
|
|
|
|
[Fact]
|
|
public void ReplacingTheBundledArt_ReSeedsAnExistingProfile() => _app.Run(() =>
|
|
{
|
|
// The bundled art is a WPF resource, so this needs a real Application to resolve the pack URI.
|
|
var tempDb = Path.Combine(Path.GetTempPath(), $"ytLlive-art-{Guid.NewGuid():N}.db");
|
|
MainViewModel.LayoutPathOverride = tempDb;
|
|
try
|
|
{
|
|
// First launch: the seeded art is whatever ships today.
|
|
var first = new MainViewModel();
|
|
var firstId = StartingBackgroundId(first);
|
|
Assert.False(string.IsNullOrWhiteSpace(firstId), "the bundled Starting art never seeded");
|
|
var firstBytes = AssetLength(tempDb, firstId!);
|
|
|
|
// Simulate a profile stamped with the OLD art. No version flag is touched — the seed has to
|
|
// notice the bytes are not the shipped ones by itself.
|
|
using (var connection = new SqliteConnection($"Data Source={tempDb}"))
|
|
{
|
|
connection.Open();
|
|
using var plant = connection.CreateCommand();
|
|
plant.CommandText = """
|
|
INSERT OR REPLACE INTO Asset (Id, Hash, Data, PixelWidth, PixelHeight)
|
|
VALUES ('old-art', 'deadbeef', zeroblob(1024), 1920, 1080);
|
|
UPDATE Source SET AssetId = 'old-art' WHERE IsBackground = 1;
|
|
""";
|
|
plant.ExecuteNonQuery();
|
|
}
|
|
SqliteConnection.ClearAllPools();
|
|
|
|
var second = new MainViewModel();
|
|
var secondId = StartingBackgroundId(second);
|
|
Assert.NotNull(secondId);
|
|
|
|
// Re-stamped to the SHIPPED art — which, being the same bytes, is the same asset row the
|
|
// first launch created. What matters is that the planted "old art" is gone.
|
|
Assert.NotEqual("old-art", secondId);
|
|
Assert.Equal(firstId, secondId);
|
|
Assert.Equal(File.ReadAllBytes(Path.Combine(
|
|
AppContext.BaseDirectory, "..", "..", "..", "..", "Assets", "starting-background.jpg")).Length,
|
|
AssetLength(tempDb, secondId!));
|
|
Assert.NotEqual(firstBytes, 0);
|
|
|
|
// Idempotent: a third launch finds the shipped bytes already in place and must not re-seed.
|
|
var third = new MainViewModel();
|
|
Assert.Equal(secondId, StartingBackgroundId(third));
|
|
}
|
|
finally
|
|
{
|
|
MainViewModel.LayoutPathOverride = null;
|
|
SqliteConnection.ClearAllPools();
|
|
try { File.Delete(tempDb); } catch { /* best-effort cleanup */ }
|
|
}
|
|
});
|
|
|
|
private static string? StartingBackgroundId(MainViewModel vm)
|
|
=> vm.Scenes
|
|
.Where(scene => ytLive.Models.SceneCatalog.Is(scene.Name, ytLive.Models.SceneCatalog.Starting))
|
|
.SelectMany(scene => scene.Elements.OfType<ytLive.Models.Source>())
|
|
.FirstOrDefault(element => element.IsBackground)?.AssetId;
|
|
|
|
private static int AssetLength(string dbPath, string assetId)
|
|
{
|
|
using var connection = new SqliteConnection($"Data Source={dbPath}");
|
|
connection.Open();
|
|
using var cmd = connection.CreateCommand();
|
|
cmd.CommandText = "SELECT length(Data) FROM Asset WHERE Id = $id;";
|
|
cmd.Parameters.AddWithValue("$id", assetId);
|
|
return Convert.ToInt32(cmd.ExecuteScalar());
|
|
}
|
|
}
|