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.
118 lines
4.8 KiB
C#
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;
|
|
}
|
|
}
|