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); } }