fix(37): preserve camera choice when restoring default layouts

Build-mismatch restoration now keeps the currently selected webcam identity,
restores default scene placements against that camera, and leaves cameras absent
when none is selected. The integration regression test pins the creator's camera
choice and restored default geometry.
612 tests green, clean build with 0 warnings.
This commit is contained in:
2026-11-10 17:56:24 -08:00
parent c4da06b56b
commit ff1c1965e0
2 changed files with 50 additions and 1 deletions
+9 -1
View File
@@ -246,10 +246,12 @@ public partial class LayoutStore : IDisposable
private void RestoreDefaults(string buildId)
{
var columnsByTable = SnapshotTables.ToDictionary(table => table, ReadColumnNames);
var currentWebcamId = GetCurrentWebcamId();
using var tx = _connection.BeginTransaction();
foreach (var table in SnapshotTables.Reverse())
{
if (table == "Webcam") continue;
using var delete = _connection.CreateCommand();
delete.Transaction = tx;
delete.CommandText = $"DELETE FROM {table};";
@@ -258,10 +260,16 @@ public partial class LayoutStore : IDisposable
foreach (var table in SnapshotTables)
{
if (table == "Webcam" || (table == "WebcamSceneConfig" && currentWebcamId == null)) continue;
var columns = string.Join(", ", columnsByTable[table].Select(QuoteIdentifier));
var values = string.Join(", ", columnsByTable[table].Select(column =>
column == "WebcamId" && table == "WebcamSceneConfig"
? "$webcamId"
: QuoteIdentifier(column)));
using var copy = _connection.CreateCommand();
copy.Transaction = tx;
copy.CommandText = $"INSERT INTO {table} ({columns}) SELECT {columns} FROM Default{table};";
copy.CommandText = $"INSERT INTO {table} ({columns}) SELECT {values} FROM Default{table};";
if (table == "WebcamSceneConfig") copy.Parameters.AddWithValue("$webcamId", currentWebcamId!);
copy.ExecuteNonQuery();
}
+41
View File
@@ -116,6 +116,39 @@ public sealed class LayoutStoreSnapshotTests
}
}
[Fact]
public void DifferentBuild_RestoresWebcamPlacement_WithoutReplacingChosenCamera()
{
var path = Path.Combine(Path.GetTempPath(), $"ytLlive-layout-snapshot-{Guid.NewGuid():N}.db");
var sceneId = Guid.NewGuid().ToString();
var defaultWebcam = new Webcam { Id = "factory-webcam", DeviceId = "factory-device", Name = "Factory camera" };
var chosenWebcam = new Webcam { Id = "chosen-webcam", DeviceId = "chosen-device", Name = "Chosen camera" };
try
{
using (var store = new LayoutStore(path))
{
store.Save(new[] { MakeWebcamScene(sceneId, defaultWebcam.Id, 20) }, defaultWebcam, null);
store.PrepareLayoutSnapshots("build-a");
store.Save(new[] { MakeWebcamScene(sceneId, chosenWebcam.Id, 200) }, chosenWebcam, null);
}
using (var reopened = new LayoutStore(path))
{
reopened.PrepareLayoutSnapshots("build-b");
var restored = Assert.Single(reopened.Load());
var config = Assert.Single(restored.Elements.OfType<WebcamSceneConfig>());
Assert.Equal(chosenWebcam.DeviceId, reopened.Webcam?.DeviceId);
Assert.Equal(chosenWebcam.Id, config.WebcamId);
Assert.Equal(20, config.X);
}
}
finally
{
SqliteConnection.ClearAllPools();
try { File.Delete(path); } catch { /* best-effort cleanup */ }
}
}
private static Scene MakeScene(string sourceName)
=> MakeScene(SceneCatalog.Starting, sourceName);
@@ -134,6 +167,14 @@ public sealed class LayoutStoreSnapshotTests
Elements = { new Source { Name = sourceName, Type = SourceType.TextOverlay } },
};
private static Scene MakeWebcamScene(string id, string webcamId, double x)
=> new()
{
Id = id,
Name = SceneCatalog.Live,
Elements = { new WebcamSceneConfig { WebcamId = webcamId, Name = "Webcam", X = x, Y = 20, Width = 320, Height = 180 } },
};
private static SqliteConnection GetConnection(string path)
{
var connection = new SqliteConnection($"Data Source={path}");