using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using Xunit;
using ytLive.Services.Encoder;
namespace ytLive.Tests;
///
/// The FFmpeg locator (TASK 4 ship step 2, rewritten by TASK 48 item 1).
///
/// What changed. The locator used to pin a BtbN release URL, download the zip
/// on first run, extract it, and execute it. That is Windows Store policy 10.2.2 (dynamic
/// code inclusion — TASKS/research-store-certification.md §4) and it had already
/// failed in production: the pin aged out of BtbN's 14-day retention and the download
/// 404'd on the creator's first real recording attempt (2026-09-01). FFmpeg is now
/// bundled, fetched at build time by scripts/fetch-ffmpeg.sh and copied into
/// ffmpeg/ beside the executable. Nothing is downloaded at runtime, ever.
///
/// Why the order flipped. PATH used to win ("the user's own install wins").
/// With a bundle in the package that is backwards: a PATH ffmpeg may be a different
/// major version or a GPL build, and the bundle is the one that was pinned, license-vetted
/// and tested. PATH remains the escape hatch for someone who deliberately wants their own.
///
public class FfmpegLocatorTests
{
private static string TempDir()
{
var dir = Path.Combine(Path.GetTempPath(), "ytllive-tests-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
return dir;
}
/// Repo root, found by walking up to a file only the repo has.
private static string RepoRoot()
{
var dir = new DirectoryInfo(AppContext.BaseDirectory);
while (dir != null)
{
if (File.Exists(Path.Combine(dir.FullName, "ytLive.csproj"))) return dir.FullName;
dir = dir.Parent;
}
throw new FileNotFoundException($"could not locate ytLive.csproj above {AppContext.BaseDirectory}");
}
private static string RepoFile(params string[] parts)
{
var candidate = Path.Combine(new[] { RepoRoot() }.Concat(parts).ToArray());
if (File.Exists(candidate)) return candidate;
throw new FileNotFoundException($"could not locate {Path.Combine(parts)} under {RepoRoot()}");
}
///
/// The Good Dog integration test for this unit: prove the whole chain end to end —
/// the build actually copied the bundle into the app's output, the locator resolves it
/// without any network, and the located binary executes. Running it is the part
/// that matters most: FFmpeg is an lgpl-shared build, so it only starts if every
/// libav*.dll sits beside the exe. A packaging change that dropped the DLLs would
/// pass every existence check and still hand the user a broken encoder.
///
[Fact]
public async Task Locate_Integration_TheBuiltOutputShipsABundleThatRuns()
{
// NOTE: the output root is derived from the *project* directory, not from the
// repository folder name. The repo directory happens to also be called "ytLive",
// so naively appending "ytLive" would resolve to a path that only works on this
// machine's checkout.
var projectDir = Path.GetDirectoryName(RepoFile("ytLive.csproj"))!;
var binRoot = Path.Combine(projectDir, "bin");
Assert.True(Directory.Exists(binRoot),
$"no build output at {binRoot} — build ytLive.csproj before running this test");
var withBundle = Directory
.GetDirectories(binRoot, "*", SearchOption.AllDirectories)
.Where(d => File.Exists(Path.Combine(d, FfmpegLocator.BundleFolderName, FfmpegLocator.FileName)))
.ToArray();
Assert.True(withBundle.Length > 0,
$"No build output under {binRoot} carries ffmpeg/{FfmpegLocator.FileName}. " +
"Run ./scripts/fetch-ffmpeg.sh, then rebuild — the csproj GuardBundledFfmpeg " +
"target should have failed the build if the source folder was missing.");
var outputDir = withBundle[0];
var bundleDir = Path.Combine(outputDir, FfmpegLocator.BundleFolderName);
// The sibling DLLs are the shared-build's whole dependency story.
foreach (var required in new[] { FfmpegLocator.ProbeFileName, "avcodec-63.dll", "avformat-63.dll" })
Assert.True(File.Exists(Path.Combine(bundleDir, required)),
$"{required} must ship beside {FfmpegLocator.FileName} — a shared build resolves DLLs " +
"from the executable's own directory.");
// The locator resolves it with an empty PATH, i.e. nothing outside the bundle.
var resolved = await new FfmpegLocator(searchDirs: [], bundleRoot: outputDir).LocateAsync();
Assert.Equal(Path.Combine(bundleDir, FfmpegLocator.FileName), resolved);
// And it actually runs. This is what proves the DLLs resolve.
var info = new ProcessStartInfo
{
FileName = resolved,
Arguments = "-version",
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
};
using var process = Process.Start(info)!;
var stdout = process.StandardOutput.ReadToEnd();
Assert.True(process.WaitForExit(milliseconds: 30_000), "bundled ffmpeg -version did not exit in 30s");
Assert.Equal(0, process.ExitCode);
Assert.Contains("ffmpeg version", stdout, StringComparison.OrdinalIgnoreCase);
}
[Fact]
public async Task Locate_PrefersTheBundle_OverAPathInstall()
{
var root = TempDir();
try
{
var pathDir = Path.Combine(root, "on-path");
var bundleDir = Path.Combine(root, FfmpegLocator.BundleFolderName);
Directory.CreateDirectory(pathDir);
Directory.CreateDirectory(bundleDir);
File.WriteAllText(Path.Combine(pathDir, FfmpegLocator.FileName), "user's ffmpeg");
var bundled = Path.Combine(bundleDir, FfmpegLocator.FileName);
File.WriteAllText(bundled, "bundled ffmpeg");
var resolved = await new FfmpegLocator([pathDir], root).LocateAsync();
Assert.Equal(bundled, resolved);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public async Task Locate_FallsBackToPath_WhenNoBundleIsPresent()
{
var root = TempDir();
try
{
var pathDir = Path.Combine(root, "on-path");
Directory.CreateDirectory(pathDir);
var onPath = Path.Combine(pathDir, FfmpegLocator.FileName);
File.WriteAllText(onPath, "user's ffmpeg");
var resolved = await new FfmpegLocator([pathDir], root).LocateAsync();
Assert.Equal(onPath, resolved);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
[Fact]
public async Task Locate_ZeroByteBundle_IsNotUsable()
{
var root = TempDir();
try
{
var bundleDir = Path.Combine(root, FfmpegLocator.BundleFolderName);
Directory.CreateDirectory(bundleDir);
File.WriteAllText(Path.Combine(bundleDir, FfmpegLocator.FileName), "");
var ex = await Assert.ThrowsAsync(
() => new FfmpegLocator([], root).LocateAsync());
Assert.Contains(FfmpegLocator.BundleFolderName, ex.Message, StringComparison.Ordinal);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
///
/// A missing bundle must be LOUD and must say where to look. The old ladder had a
/// "found something on disk" path that could quietly satisfy a missing bundle with a
/// stale cached copy — silent drift in exactly the dependency Store certification
/// inspects, and silent drift is how a packaging bug ships.
///
[Fact]
public async Task Locate_NoBundleAndNoPath_ThrowsNamingTheBundleFolderAndTheFix()
{
var root = TempDir();
try
{
var ex = await Assert.ThrowsAsync(
() => new FfmpegLocator([], root).LocateAsync());
Assert.Contains(FfmpegLocator.BundleFolderName, ex.Message, StringComparison.Ordinal);
Assert.Contains("fetch-ffmpeg.sh", ex.Message, StringComparison.Ordinal);
Assert.Contains("PATH", ex.Message, StringComparison.Ordinal);
}
finally
{
Directory.Delete(root, recursive: true);
}
}
///
/// The Store-gate assertion, kept as a source-level fact so it cannot rot: the locator
/// must contain no network capability at all. Policy 10.2.2 forbids download-and-execute,
/// and this is the only type in the app that ever executed a downloaded binary. If a
/// future change reintroduces an HttpClient, a URL, or a downloader here, this fails —
/// which is the entire point.
///
/// Patterns are call-shaped (new HttpClient, not HttpClient)
/// on purpose. A bare word match flags the file's own doc comment, which legitimately
/// says "nothing is downloaded" — a test that fails on the prose explaining the
/// guarantee is a test that gets deleted instead of obeyed.
///
[Theory]
[InlineData("new HttpClient")]
[InlineData("HttpClient()")]
[InlineData("GetByteArrayAsync(")]
[InlineData("GetStreamAsync(")]
[InlineData("WebRequest.Create")]
[InlineData("DownloadFileTaskAsync")]
[InlineData("http://")]
[InlineData("https://")]
[InlineData("PinnedUrl =")]
[InlineData("ExtractBinaries(")]
public void TheLocatorCarriesNoNetworkCapability(string forbidden)
{
var source = File.ReadAllText(
RepoFile("Services", "Encoder", "FfmpegLocator.cs"));
Assert.DoesNotContain(forbidden, source, StringComparison.OrdinalIgnoreCase);
}
///
/// The guard that makes a forgotten bundle a build failure rather than a runtime one.
/// Without it, a fresh clone with no ffmpeg/ builds cleanly and the app only
/// discovers the problem when the creator presses Record.
///
[Fact]
public void TheBuildRefusesToCompileWithoutTheBundle()
{
var csproj = File.ReadAllText(RepoFile("ytLive.csproj"));
Assert.Contains("GuardBundledFfmpeg", csproj, StringComparison.Ordinal);
Assert.Contains("ffmpeg\\ffmpeg.exe", csproj, StringComparison.Ordinal);
Assert.Contains("CopyToPublishDirectory=\"Always\"", csproj, StringComparison.Ordinal);
}
}