Files
LlamaCasty/ytLive.Tests/RecordingSaveDialogTests.cs
T
gramps 536676f11a fix: millisecond segment in the recording filename so two instances can't clobber a take
The dev multi-instance use case (one instance screen-capturing the other for
marketing/training videos) exposed a real bug. BuildStartPath in
MainViewModel.Recording.cs builds the start name with no dedup:

    var name = RecordingFile.BuildStartName(DateTime.Now) + ".mp4";
    var path = Path.Combine(folder, name);

Only FinalizeRecordingAsync calls UniquePath. So two instances that start
recording in the same minute computed the identical path
(ty-20260927-1454-0000.mp4) and both ffmpeg processes wrote one file -- a
corrupt take, not an error message.

Creator's fix (2026-09-27): add a millisecond count to the filename timestamp.
New format is ty-<yyyyMMdd>-<HHmm>-<fff>-<hh2mm2>.mp4. Both the start name and
the final name derive from _recordStartTime, so the ffff segment survives the
rename-on-stop and the final name cannot collide with another same-minute take
either. No UniquePath call needed, which keeps the start path a pure function of
the timestamp and the existing collision suffix behaviour intact.

Tests: 2 new (the same-minute guard and the rename-survival guard), 5 existing
assertions updated for the format. RecordingSaveDialogTests' stem fixtures
updated to match. 369/369 pass, 0 warnings.

Note: the first test run reported 367/367 because only ytLive.csproj had been
built -- the stale test DLL never picked up the new tests. The real count is 369.
2026-09-27 15:37:46 -07:00

118 lines
4.8 KiB
C#

using System;
using System.IO;
using Xunit;
using ytLive.ViewModels;
namespace ytLive.Tests;
/// <summary>
/// The post-recording save dialog (CREATOR RULING 2026-09-26).
/// <para>Reported symptom: clicking Cancel in the save prompt still saved the
/// recording under the default name — Cancel behaved like "accept the default". The
/// creator's intent is explicit: <b>Enter accepts the default file name and saves;
/// Cancel cancels the save and discards the footage.</b></para>
/// <para>The bug was in the caller, not the modal — <c>FinalizeRecordingAsync</c> ran
/// <c>File.Move</c> unconditionally, so <c>ShowDialog() == false</c> fell straight
/// through into the save. These tests drive the extracted commit-or-discard decision
/// against real files in a temp folder, which is what would have caught it.</para>
/// </summary>
public sealed class RecordingSaveDialogTests : IDisposable
{
private readonly string _dir = Path.Combine(
Path.GetTempPath(), "ytLive-recsave-" + Guid.NewGuid().ToString("N")[..8]);
private const string AutoStem = "ty-20260926-1405-000-0130";
private const string StartStem = "ty-20260926-1405-000-0000";
private const string Bytes = "fake-mp4-payload";
public RecordingSaveDialogTests() => Directory.CreateDirectory(_dir);
public void Dispose()
{
try { Directory.Delete(_dir, recursive: true); } catch { /* best-effort */ }
}
/// <summary>The reported bug: Cancel must leave NOTHING in the videos folder — not
/// the temp start-name file, and not a renamed copy of it either.</summary>
[Fact]
public void Cancel_DiscardsTheFootage_AndLeavesNothingOnDisk()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, chosenStem: null, creatorSaved: false);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Discarded, result.Outcome);
Assert.False(File.Exists(start), "Cancel must delete the temp recording, not move it");
Assert.Empty(Directory.GetFiles(_dir));
}
/// <summary>Enter on the pre-filled default is a Save — the file lands under the
/// auto name with its bytes intact (the creator's other half of the ruling).</summary>
[Fact]
public void EnterOnTheDefault_KeepsTheAutoName_AndSavesTheFootage()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, AutoStem, creatorSaved: true);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Saved, result.Outcome);
var expected = Path.Combine(_dir, AutoStem + ".mp4");
Assert.Equal(expected, result.FinalPath);
Assert.False(File.Exists(start), "Save renames the temp file — it must not be left behind");
Assert.Equal(Bytes, File.ReadAllText(expected));
Assert.Single(Directory.GetFiles(_dir));
}
/// <summary>A cleared name box on an explicit Save still means "keep the auto
/// name" (the dialog promises this) — Cancel is the only way to discard.</summary>
[Fact]
public void SaveWithABlankName_StillFallsBackToTheAutoName()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, " ", creatorSaved: true);
Assert.Equal(MainViewModel.RecordingSaveOutcome.Saved, result.Outcome);
Assert.True(File.Exists(Path.Combine(_dir, AutoStem + ".mp4")));
}
/// <summary>An explicit Save with a chosen name honours that name.</summary>
[Fact]
public void SaveWithAChosenName_UsesThatName()
{
var start = WriteStartFile();
var result = CompleteRecordingSave(start, _dir, AutoStem, "my-stream", creatorSaved: true);
Assert.Equal("my-stream.mp4", Path.GetFileName(result.FinalPath!));
}
/// <summary>Never clobber an earlier recording: a second save with the same name
/// gets uniquified rather than overwriting the first.</summary>
[Fact]
public void SaveWithACollidingName_UniquifiesInsteadOfOverwriting()
{
var first = WriteStartFile();
Assert.Equal(
MainViewModel.RecordingSaveOutcome.Saved,
CompleteRecordingSave(first, _dir, AutoStem, AutoStem, true).Outcome);
var second = WriteStartFile();
var result = CompleteRecordingSave(second, _dir, AutoStem, AutoStem, true);
Assert.Equal(AutoStem + "-2.mp4", Path.GetFileName(result.FinalPath!));
Assert.Equal(2, Directory.GetFiles(_dir).Length);
}
private static MainViewModel.RecordingSaveResult CompleteRecordingSave(
string startPath, string dir, string autoStem, string? chosenStem, bool creatorSaved)
=> MainViewModel.CompleteRecordingSave(startPath, dir, autoStem, chosenStem, creatorSaved);
private string WriteStartFile()
{
var start = Path.Combine(_dir, StartStem + ".mp4");
File.WriteAllText(start, Bytes);
return start;
}
}