feat(37): restore the default layout when the build ID changes

A mismatched or missing current-build stamp restores the immutable default
snapshot atomically before the VM loads scenes. The new integration case edits
current on build A, opens on build B, and verifies the factory layout returns.
610 tests green, clean build with 0 warnings.

Reference: OBS scene collections keep scenes/sources as a separate saved set:
https://obsproject.com/kb/scene-collections
This commit is contained in:
2026-11-10 17:48:43 -08:00
parent 37791086ae
commit 1c6fc3223d
2 changed files with 83 additions and 3 deletions
+51 -3
View File
@@ -56,10 +56,18 @@ public partial class LayoutStore : IDisposable
/// <see cref="CaptureDefaultsIfMissing"/> after its factory scenes have been seeded.</summary>
public void PrepareLayoutSnapshots(string buildId)
{
if (GetSnapshotState(DefaultsInitializedKey) != "1" && HasStoredScenes())
ArgumentException.ThrowIfNullOrWhiteSpace(buildId);
var defaultsInitialized = GetSnapshotState(DefaultsInitializedKey) == "1";
if (!defaultsInitialized && HasStoredScenes())
{
CaptureDefaults();
defaultsInitialized = true;
}
if (string.IsNullOrWhiteSpace(GetSnapshotState(CurrentBuildIdKey)))
var currentBuildId = GetSnapshotState(CurrentBuildIdKey);
if (defaultsInitialized && !string.Equals(currentBuildId, buildId, StringComparison.Ordinal))
RestoreDefaults(buildId);
else if (!defaultsInitialized && string.IsNullOrWhiteSpace(currentBuildId))
SetSnapshotState(CurrentBuildIdKey, buildId);
}
@@ -121,6 +129,45 @@ public partial class LayoutStore : IDisposable
tx.Commit();
}
private void RestoreDefaults(string buildId)
{
var columnsByTable = SnapshotTables.ToDictionary(table => table, ReadColumnNames);
using var tx = _connection.BeginTransaction();
foreach (var table in SnapshotTables.Reverse())
{
using var delete = _connection.CreateCommand();
delete.Transaction = tx;
delete.CommandText = $"DELETE FROM {table};";
delete.ExecuteNonQuery();
}
foreach (var table in SnapshotTables)
{
var columns = string.Join(", ", columnsByTable[table].Select(QuoteIdentifier));
using var copy = _connection.CreateCommand();
copy.Transaction = tx;
copy.CommandText = $"INSERT INTO {table} ({columns}) SELECT {columns} FROM Default{table};";
copy.ExecuteNonQuery();
}
using (var clearSettings = _connection.CreateCommand())
{
clearSettings.Transaction = tx;
clearSettings.CommandText = "DELETE FROM Settings WHERE Key LIKE 'BackgroundPath_%' OR Key LIKE 'BackgroundUseDefault_%';";
clearSettings.ExecuteNonQuery();
}
using (var copySettings = _connection.CreateCommand())
{
copySettings.Transaction = tx;
copySettings.CommandText = "INSERT INTO Settings (Key, Value) SELECT Key, Value FROM DefaultLayoutSetting;";
copySettings.ExecuteNonQuery();
}
SetSnapshotState(CurrentBuildIdKey, buildId, tx);
tx.Commit();
}
private bool HasStoredScenes()
{
using var cmd = _connection.CreateCommand();
@@ -136,9 +183,10 @@ public partial class LayoutStore : IDisposable
return cmd.ExecuteScalar() as string;
}
private void SetSnapshotState(string key, string value)
private void SetSnapshotState(string key, string value, SqliteTransaction? transaction = null)
{
using var cmd = _connection.CreateCommand();
cmd.Transaction = transaction;
cmd.CommandText = """
INSERT INTO LayoutSnapshotState (Key, Value) VALUES ($key, $value)
ON CONFLICT(Key) DO UPDATE SET Value = excluded.Value;
+32
View File
@@ -48,6 +48,38 @@ public sealed class LayoutStoreSnapshotTests
}
}
[Fact]
public void DifferentBuild_RestoresTheDefaultSnapshotBeforeLoading()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-snapshot-{Guid.NewGuid():N}.db");
try
{
using (var store = new LayoutStore(path))
{
store.Save(new[] { MakeScene("Factory title") }, null, null);
store.PrepareLayoutSnapshots("build-a");
store.Save(new[] { MakeScene("Creator edit") }, null, null);
}
using (var reopened = new LayoutStore(path))
{
reopened.PrepareLayoutSnapshots("build-b");
var restored = Assert.Single(reopened.Load());
Assert.Equal("Factory title", Assert.Single(restored.Elements.OfType<Source>()).Name);
using var connection = GetConnection(path);
using var command = connection.CreateCommand();
command.CommandText = "SELECT Value FROM LayoutSnapshotState WHERE Key = 'CurrentBuildId';";
Assert.Equal("build-b", command.ExecuteScalar());
}
}
finally
{
SqliteConnection.ClearAllPools();
try { File.Delete(path); } catch { /* best-effort cleanup */ }
}
}
private static Scene MakeScene(string sourceName)
=> new()
{