using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using System.Xml.Linq; using Xunit; namespace ytLive.Tests; /// TASK 48, slice 3 — the creator's ruling (2026-09-27): the app ships as a /// **Microsoft Store MSIX** and the **Store updates it**. The self-update subsystem that /// existed for the abandoned self-hosted route (a Velopack bootstrap plus the DigitalOcean /// update-URL droplet) is therefore deleted, not parked: a second updater inside a /// Store-installed package fights the Store's own updater. /// /// This test is the guard: the app project references no updater package and no app /// source file uses the updater API — so the deleted subsystem cannot creep back in, and /// the "the Store updates it" decision stays the only update path. public class StorePackagingTests { /// Known self-update packages. If one of these comes back, the Store route has /// been quietly reversed. private static readonly string[] UpdaterPackages = { "Velopack", "Squirrel", "NetSparkle", "AutoUpdater", "WinSparkle" }; [Fact] public void The_app_ships_no_self_update_subsystem() { // 1. The app project references no updater package. var project = XDocument.Load(SourceAssert.RepoPath("ytLive.csproj")); var packages = project.Descendants() .Where(e => e.Name.LocalName == "PackageReference") .Select(e => (string?)e.Attribute("Include")) .Where(name => name != null) .ToHashSet(StringComparer.OrdinalIgnoreCase); foreach (var updater in UpdaterPackages) Assert.DoesNotContain(updater, packages); // 2. No app source file uses the updater API. Matched as CODE (a using directive, // a type usage), not as a bare word — a file's prose is allowed to name what it // deliberately does not do. var root = Path.GetDirectoryName(SourceAssert.RepoPath("ytLive.csproj"))!; foreach (var file in AppSourceFiles(root)) { var text = File.ReadAllText(file); Assert.False( Regex.IsMatch(text, @"^\s*using\s+Velopack\s*;", RegexOptions.Multiline), $"{Path.GetFileName(file)} still imports the Velopack updater — the Store updates this app."); Assert.False( Regex.IsMatch(text, @"\bVelopackApp\b"), $"{Path.GetFileName(file)} still calls the Velopack updater — the Store updates this app."); } } /// Every C# file of the app project (not the test project, not build output). private static IEnumerable AppSourceFiles(string root) { return Directory.EnumerateFiles(root, "*.cs", SearchOption.AllDirectories) .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}ytLive.Tests{Path.DirectorySeparatorChar}")) .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}obj{Path.DirectorySeparatorChar}")) .Where(path => !path.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}")); } private static readonly XNamespace Foundation = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"; private static readonly XNamespace Uap = "http://schemas.microsoft.com/appx/manifest/uap/windows10"; private static readonly XNamespace Uap10 = "http://schemas.microsoft.com/appx/manifest/uap/windows10/10"; private static readonly XNamespace Rescap = "http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"; /// TASK 48, slice 2 — the MSIX manifest. This is the file that decides whether /// recording, capturing and the bundled ffmpeg still work once the app is packaged: /// a full-trust packagedClassicApp at mediumIL can launch ffmpeg as a child process and /// write to the user's profile; an AppContainer cannot, and recording would silently /// land in a virtualised folder. So the guard pins every property that makes that true, /// plus the x64/en-US decisions and the logo files the packer requires. [Fact] public void The_package_manifest_declares_full_trust_camera_and_microphone() { var manifestPath = SourceAssert.RepoPath(Path.Combine("Packaging", "Package.appxmanifest")); Assert.True(File.Exists(manifestPath), "Packaging/Package.appxmanifest is missing."); var manifest = XDocument.Load(manifestPath); var package = manifest.Root!; Assert.Equal(Foundation + "Package", package.Name); // 1. Identity: x64 only (creator ruling 2026-10-09), and the title the Store expects. var identity = package.Element(Foundation + "Identity")!; Assert.Equal("LlamaCasty", (string?)identity.Attribute("Name")); Assert.Equal("x64", (string?)identity.Attribute("ProcessorArchitecture")); // 2. Full trust: packagedClassicApp + mediumIL + the restricted runFullTrust capability. var application = package.Element(Foundation + "Applications")! .Element(Foundation + "Application")!; Assert.Equal("Windows.FullTrustApplication", (string?)application.Attribute("EntryPoint")); Assert.Equal("ytLive.exe", (string?)application.Attribute("Executable")); Assert.Equal("mediumIL", (string?)application.Attribute(Uap10 + "TrustLevel")); Assert.Equal("packagedClassicApp", (string?)application.Attribute(Uap10 + "RuntimeBehavior")); var capabilities = package.Element(Foundation + "Capabilities")!; Assert.Contains(capabilities.Elements(Rescap + "Capability"), element => (string?)element.Attribute("Name") == "runFullTrust"); var devices = capabilities.Elements(Foundation + "DeviceCapability") .Select(element => (string?)element.Attribute("Name")) .ToArray(); Assert.Contains("webcam", devices); Assert.Contains("microphone", devices); // 3. ⛔ Never appContainer — that is the change that breaks recording (item 6). // Matched on ATTRIBUTE VALUES, not the file text: the manifest's own comment // explains why it must not be appContainer, and prose is allowed to say so. Assert.DoesNotContain( manifest.Descendants() .SelectMany(element => element.Attributes()) .Select(attribute => attribute.Value), value => string.Equals(value, "appContainer", StringComparison.OrdinalIgnoreCase)); // 4. English only (policy 10.7): a second language forces the listing to be localised. var languages = package.Element(Foundation + "Resources")! .Elements(Foundation + "Resource") .Select(element => (string?)element.Attribute("Language")) .ToArray(); Assert.Equal(new[] { "en-US" }, languages); // 5. Every image the manifest references is actually in the package source tree — // makeappx refuses to pack otherwise. var packaging = Path.GetDirectoryName(manifestPath)!; foreach (var logo in manifest.Descendants() .SelectMany(element => element.Attributes()) .Where(attribute => attribute.Name.LocalName.EndsWith("Logo", StringComparison.Ordinal)) .Select(attribute => attribute.Value.Replace('\\', Path.DirectorySeparatorChar))) { Assert.True(File.Exists(Path.Combine(packaging, logo)), $"manifest references a missing logo: {logo}"); } } }