Files
LlamaCasty/ytLive.Tests/ClipLibraryTests.cs
T
gramps f7d3d1301e feat(65): clips on disk — write one, keep 3/4/5, delete the oldest
Slice 3 of TASK 65. 606 green, 0 warnings.

`ClipLibrary` is the whole of clip "management": a file lands in `clips\` beside the recordings, the
oldest is deleted past the retention count, and `Recent()` returns newest-first because that is also the
replay layer's playlist order. No gallery, no prompt, no naming.

Two guards the tests pin: an **empty snapshot never becomes a zero-byte file** (a save that fails loudly
is better than a file that won't open), and **pruning only touches our own prefix** — another file in the
folder survives.

`RingBytesFor(seconds, bitrate)` is the memory cap the ring is sized with: exact wire size × 1.25
headroom, because bitrate moves and the ring trims whole fragments.

Still to come: the replay layer, the F10 hotkey, and the Settings switch.
2026-11-10 12:35:38 -08:00

77 lines
2.6 KiB
C#

using System;
using System.IO;
using System.Linq;
using Xunit;
using ytLive.Services;
namespace ytLive.Tests;
/// <summary>
/// TASK 65 slice 3 — clips on disk, with retention and nothing else.
///
/// <para>The rule: write one file, delete the oldest past the count. No gallery, no prompt. The tests pin
/// the two ways that could go wrong — a zero-byte clip from an empty ring, and pruning taking something
/// that isn't ours.</para>
/// </summary>
public sealed class ClipLibraryTests : IDisposable
{
private readonly string _root = Path.Combine(Path.GetTempPath(), $"ytLlive-clips-{Guid.NewGuid():N}");
public ClipLibraryTests() => Directory.CreateDirectory(_root);
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); } catch { /* best effort */ }
}
private ClipLibrary Library() => new(() => _root);
[Fact]
public void SaveWritesIntoClipsBesideTheRecordings()
{
var path = Library().Save(new byte[] { 1, 2, 3 }, DateTime.Now, keep: 3);
Assert.NotNull(path);
Assert.EndsWith(".mp4", path);
Assert.Equal(Path.Combine(_root, "clips"), Path.GetDirectoryName(path));
Assert.True(File.Exists(path));
}
[Fact]
public void AnEmptySnapshotNeverBecomesAZeroByteFile()
{
Assert.Null(Library().Save(Array.Empty<byte>(), DateTime.Now, keep: 3));
Assert.False(Directory.Exists(Path.Combine(_root, "clips"))
&& Directory.EnumerateFiles(Path.Combine(_root, "clips")).Any());
}
[Fact]
public void RetentionKeepsTheNewestAndDeletesTheRest()
{
var library = Library();
var folder = library.EnsureFolder();
for (var i = 0; i < 6; i++)
File.WriteAllText(Path.Combine(folder, $"clip-20261110-12000{i}.mp4"), "x");
File.WriteAllText(Path.Combine(folder, "someone-elses-file.mp4"), "keep me");
var removed = library.Prune(keep: 3);
Assert.Equal(3, removed);
var remaining = library.Recent();
Assert.Equal(3, remaining.Count);
Assert.Contains("120005", remaining[0]); // newest first
Assert.True(File.Exists(Path.Combine(folder, "someone-elses-file.mp4")));
}
[Fact]
public void RingBytesFollowLengthAndBitrate_WithHeadroom()
{
var tenAt8 = ClipLibrary.RingBytesFor(10, 8000);
var fifteenAt8 = ClipLibrary.RingBytesFor(15, 8000);
Assert.True(tenAt8 > 10L * 8000 * 1000 / 8); // more than the exact wire size
Assert.True(fifteenAt8 > tenAt8);
Assert.True(tenAt8 < 15L * 1024 * 1024); // and still modest
}
}