Files
vfxreview/RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs
twotalesanimation caaa333f02
Deploy / deploy (push) Failing after 19m11s
feat(pipeline): render-derived shot thumbnails, and bulk shot delete
Two independent features landing together:

Generated shot thumbnails — a shot can now carry a thumbnail promoted
from a render export (Shot.generatedThumbnailUrl/generatedThumbnailFrom)
alongside the existing brief-supplied thumbnailUrl, which always takes
display precedence (lib/shot-thumbnail.ts) so a promoted frame can only
ever fill a gap, never overwrite the brief. Shows with
pipelineConfig.autoShotThumbnail promote automatically on every
finished export (lib/render-pipeline/preview.ts); promoteExportThumbnail/
clearGeneratedThumbnail (actions/shots.ts) are the manual equivalent for
everyone else. Worker-side seek logic for picking the promoted frame is
covered by RenderWorker/VFXReviewWorker.Tests/ThumbnailSeekTests.cs.

Bulk shot delete — the shot-status page's selection bar gets a "Bulk
Actions" dropdown (alongside the existing Change Due Date) with Delete
Shots. bulkDeleteShots() (actions/shots.ts) deletes one at a time rather
than in a single statement: exports_shotId_fkey is ON DELETE RESTRICT
by design, so a shot with renders or deliveries on record is protected
and gets skipped and reported by name rather than aborting the whole
selection or being silently destroyed along with its history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-27 17:06:55 +02:00

55 lines
2.0 KiB
C#

using VFXReviewWorker;
using Xunit;
namespace VFXReviewWorker.Tests;
/// <summary>
/// The preview MP4 opens on the Netflix slate. ffmpeg's <c>thumbnail</c> filter
/// only considers the first 100 frames, so every thumbnail the pipeline has ever
/// produced was a picture of the slate card rather than of the shot — invisible
/// while thumbnails were only decoration in the QC queue, and unacceptable now
/// they can become a shot's thumbnail.
///
/// The fix is to seek past the slate, so the seek arithmetic is worth pinning.
/// </summary>
public class ThumbnailSeekTests
{
private static PreviewBuildResult Build(double? pictureStart, int? frameCount) =>
new() { PictureStartSeconds = pictureStart, FrameCount = frameCount };
[Fact]
public void SeeksAQuarterIntoPictureNotIntoTheFile()
{
// 5s slate, then 96 frames (4s) of picture at 24fps → 5 + 1 = 6s.
var seek = PreviewStage.ThumbnailSeekSeconds(Build(5.0, 96), 24);
Assert.NotNull(seek);
Assert.Equal(6.0, seek!.Value, 3);
}
[Fact]
public void HandlesATemplateWithNoSlateAtAll()
{
var seek = PreviewStage.ThumbnailSeekSeconds(Build(0.0, 240), 24);
Assert.Equal(2.5, seek!.Value, 3);
}
[Fact]
public void FallsBackWhenTheBuildDidNotReportWherePictureStarts()
{
// Older worker, or a template with no SHOT layer: null means "keep the
// old whole-file behaviour" rather than guessing an offset.
Assert.Null(PreviewStage.ThumbnailSeekSeconds(Build(null, 96), 24));
Assert.Null(PreviewStage.ThumbnailSeekSeconds(null, 24));
}
[Theory]
[InlineData(-1.0, 96, 24)] // negative slate length
[InlineData(5.0, 0, 24)] // no picture frames
[InlineData(5.0, -5, 24)] // nonsense frame count
[InlineData(5.0, 96, 0)] // no fps to divide by
public void RefusesNumbersThatDoNotDescribeARealClip(double start, int frames, double fps)
{
Assert.Null(PreviewStage.ThumbnailSeekSeconds(Build(start, frames), fps));
}
}