Files
LlamaCasty/ytLive.Tests/BackgroundDefaultArtTests.cs
T
gramps 70e2c48136 fix(art): re-seed a bundled background when the stored bytes differ
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.
2026-11-06 12:21:24 -08:00

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());
}
}