Both offsets rode the debounced layout save, so a value set seconds before the app closed was lost without a word. That is what happened to the -130 ms offset on 2026-11-12: two clips were measured against a setting that was not in force, the numbers disagreed by ~65 ms, and the disagreement was blamed on the audio path. Setters now write the row immediately (two tiny rows), keeping ScheduleSave for the rest of the layout. AudioSyncSettingsTests gains the regression: set both offsets on a view model and read them back through a fresh store with no debounce flush. MyMistakes records the general shape — "a setting that didn't stick" is almost always ScheduleSave — with the check list and the two related rules: offsets never ship with a non-zero default, and scripts/avsync.py only resolves ~50 ms, so below that the creator's judgement wins. 677 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the creator's replacement art, still awaiting their visual check.
76 lines
2.7 KiB
C#
76 lines
2.7 KiB
C#
using System;
|
||
using System.IO;
|
||
using Microsoft.Data.Sqlite;
|
||
using Xunit;
|
||
using ytLive.Services;
|
||
using ytLive.ViewModels;
|
||
|
||
namespace ytLive.Tests;
|
||
|
||
/// <summary>
|
||
/// Sync offsets ship at zero. A correction belongs to one camera on one desk, so a fresh profile must
|
||
/// not arrive with audio already shifted — a non-zero default would move every creator's voice to fix
|
||
/// one machine and would hide whether the fault is theirs or ours.
|
||
/// </summary>
|
||
// The view model below touches the static LayoutPathOverride, so it shares the layout collection.
|
||
[Collection("MainViewModelLayoutPath")]
|
||
public sealed class AudioSyncSettingsTests : IDisposable
|
||
{
|
||
private readonly string _db = Path.Combine(Path.GetTempPath(), $"ytLlive-syncsettings-{Guid.NewGuid():N}.db");
|
||
|
||
public void Dispose()
|
||
{
|
||
SqliteConnection.ClearAllPools();
|
||
try { File.Delete(_db); } catch (IOException) { /* best effort */ }
|
||
}
|
||
|
||
[Fact]
|
||
public void Both_offsets_start_at_zero_and_only_then_take_a_value()
|
||
{
|
||
using (var store = new LayoutStore(_db))
|
||
{
|
||
Assert.Equal(0, store.LoadMicSyncOffsetMs()); // opt-in, not pre-applied
|
||
Assert.Equal(0, store.LoadAudioSyncOffsetMs());
|
||
|
||
// A mic delay cannot be negative — advancing is the global offset's job.
|
||
store.SaveMicSyncOffsetMs(-120);
|
||
Assert.Equal(0, store.LoadMicSyncOffsetMs());
|
||
|
||
store.SaveMicSyncOffsetMs(600); // beyond the slider's range
|
||
Assert.Equal(500, store.LoadMicSyncOffsetMs());
|
||
|
||
store.SaveMicSyncOffsetMs(240);
|
||
store.SaveAudioSyncOffsetMs(-130);
|
||
}
|
||
|
||
using var reopened = new LayoutStore(_db);
|
||
Assert.Equal(240, reopened.LoadMicSyncOffsetMs());
|
||
Assert.Equal(-130, reopened.LoadAudioSyncOffsetMs());
|
||
}
|
||
|
||
/// <summary>
|
||
/// A slider move is persisted as it happens. It used to ride the debounced layout save, so a value
|
||
/// set seconds before closing the app was lost — which cost a record-and-measure cycle on 2026-11-12
|
||
/// when a −130 ms offset vanished between two clips.
|
||
/// </summary>
|
||
[Fact]
|
||
public void A_slider_move_survives_without_any_debounce_flush()
|
||
{
|
||
MainViewModel.LayoutPathOverride = _db;
|
||
try
|
||
{
|
||
var vm = new MainViewModel();
|
||
vm.AudioSyncOffsetMs = -130;
|
||
vm.MicSyncOffsetMs = 240;
|
||
|
||
using var store = new LayoutStore(_db);
|
||
Assert.Equal(-130, store.LoadAudioSyncOffsetMs());
|
||
Assert.Equal(240, store.LoadMicSyncOffsetMs());
|
||
}
|
||
finally
|
||
{
|
||
MainViewModel.LayoutPathOverride = null;
|
||
}
|
||
}
|
||
}
|